Vault Plugin Part 3 - Read Plugin Code

มาเขียน Vault Plugin กันเถอะ : ตอนที่ 3 ทำความเข้าใจ Code

จากตอนที่แล้วเราได้สร้าง Mock plugin และนำมันขึ้นไปบน server เรียบร้อยแล้ว ตอนนี้เรามาทำความเข้าใจ Code ดีกว่า เพราะเมื่อเข้าใจแล้วเราก็สามารถทำตัว Plugin ของเราขึ้นมาเองได้ หรือ ไปเอา Plugin คนอื่นมา Customize ให้ได้ตรงตามความต้องการของเรา

Default code ที่ต้องมี

1
2
3
4
5
git clone https://github.com/hashicorp/vault-guides.git
cd vault-guides

# Source code มันมีการ Update หลังจากผมเขียน Blog เลยต้องย้ายมา commit
git checkout e0eb4285d8066770bc1f50056c27e2c6e4a989e1

Code ส่วนที่ต้องมีคือ Code ในไฟล์ secrets/mock/cmd/mock/main.go ซึ่งในไฟล์นี้ไม่ทำอะไรมากมันเหมือนเป็น interface ของ plugin เวลาตัว vault มาเรียกใช้งานจะทำการมาเรียกที่ไฟล์นี้ ดังนั้นถ้าเราจะ Custom อะไรเราก็เพียงแค่ import ตัว soruce code ของเราที่มี Function Factory เหมือนกับ mock ในตัวอย่าง

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"os"
"github.com/hashicorp/go-hclog"
// ตัวนี้ถูก import มา
mock "github.com/hashicorp/vault-guides/secrets/mock"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/sdk/plugin"
)

func main() {
apiClientMeta := &api.PluginAPIClientMeta{}
flags := apiClientMeta.FlagSet()
flags.Parse(os.Args[1:])

tlsConfig := apiClientMeta.GetTLSConfig()
tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig)

err := plugin.Serve(&plugin.ServeOpts{
// ตรงนี้แหละ จะไปเรียก package mock ให้ทำงาน
BackendFactoryFunc: mock.Factory,
TLSProviderFunc: tlsProviderFunc,
})
if err != nil {
logger := hclog.New(&hclog.LoggerOptions{})

logger.Error("plugin shutting down", "error", err)
os.Exit(1)
}
}

ตัว Code จริงๆที่ทำงาน

ตัว Code จริงๆที่ทำงานทั้งหมดจะอยู่ที่ secrets/mock/backend.go โดยจะขออธิบายเป็นส่วนๆนะครับ

ส่วน Factory

ส่วนของ Factory นั้นไม่มีอะไรมากเป็นการ Config ว่า Plugin โดยที่สำคัญสำหรับคือส่วนที่เป็น Path ครับ โดยจะประกาศใน Factory ก็ได้หรือจะทำเป็น Method เหมือน code ตัวอย่างก็ได้ครับ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Factory configures and returns Mock backends
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
b := &backend{
store: make(map[string][]byte),
}

b.Backend = &framework.Backend{
Help: strings.TrimSpace(mockHelp),
BackendType: logical.TypeLogical,
}

// ตรงนี้เป็นส่วน Register path ว่า plugin ของเราจะมี path อะไรบ้าง
// แล้วแต่ล่ะ path จะให้ทำอะไรได้บ้าง Create Read Update Delete (CRUD)
b.Backend.Paths = append(b.Backend.Paths, b.paths()...)

if conf == nil {
return nil, fmt.Errorf("configuration passed into backend is nil")
}

b.Backend.Setup(ctx, conf)

return b, nil
}

ส่วน Path

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
func (b *backend) paths() []*framework.Path {
// ตรงนี้มัน Return เป็น Array
return []*framework.Path{
{
// อันนี้เป็นการบอกว่า path ของเราหน้าตาเป็นยังไง ประกาศแบบนี้คือ path ของเราจะเป็น /
// อะไรที่ตามมาจากนี้จะถูกส่งเข้าตัวแปร path
// เช่น จากตอนที่แล้วเรายิงมาที่ /test
// path จะมีค่าเป็น test
Pattern: framework.MatchAllRegex("path"),

// แต่ถ้าประกาศแบบนี้จะบอกว่า path นี้จะ match กับ /bell เวลายิงจะต้องยิงมาเป็น
// /bell/test
// แทน
//Pattern: "bell/" + framework.MatchAllRegex("path"),

// อันนี้เป็นการบอกว่า path นี้รับตัวแปรอะไรบ้าง
Fields: map[string]*framework.FieldSchema{
"path": {
Type: framework.TypeString,
Description: "Specifies the path of the secret.",
},
},

// ตรงนี้เป็นส่วนบอกว่า Path ของเรารับ Operation อะไรบ้างโดยจากตัวอย่าง
// เขารับ 4 Operation เลย
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
// ตรงนี้บอกว่าจะเอา function อะไรมารับ โดยตัวนี้รับที่ handleRead
// ในของ Operation อื่นๆก็เช่นกัน
Callback: b.handleRead,
Summary: "Retrieve the secret from the map.",
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.handleWrite,
Summary: "Store a secret at the specified location.",
},
logical.CreateOperation: &framework.PathOperation{
Callback: b.handleWrite,
},
logical.DeleteOperation: &framework.PathOperation{
Callback: b.handleDelete,
Summary: "Deletes the secret at the specified location.",
},
},

// ตรงนี้จะเป็นการ fucntion ไว้ใช้ตรวจสอบว่าสิ่งที่ client ส่งมามีอยู่
// บน server แล้วรึยัง
ExistenceCheck: b.handleExistenceCheck,
},
}
}

ส่วน Operation

ขอพูดถึง handleWrite เลยละกันเพราะ code ส่วนนี้มีทั้งดึงค่าจาก body และ path

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func (b *backend) handleWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {

if req.ClientToken == "" {
return nil, fmt.Errorf("client token empty")
}

// Check to make sure that kv pairs provided
if len(req.Data) == 0 {
return nil, fmt.Errorf("data must be provided to store in secret")
}

// ส่วนนี้คือดึง data จาก path
path := data.Get("path").(string)

// JSON encode the data
// ส่วนนี้คือดึง Body จาก client ออกมา
buf, err := json.Marshal(req.Data)
if err != nil {
return nil, errwrap.Wrapf("json encoding failed: {{err}}", err)
}

// Store kv pairs in map at specified path
b.store[req.ClientToken+"/"+path] = buf

return nil, nil
}

จบละ

สำหรับตอนนี้เป็นการอธิบาย Code ของ Mock Plugin โดยจริงๆแล้วไม่มีอะไรมากเลย แต่ผมอยากเขียนเผื่อคนที่อยากทำ Custom plugin บ้างจะได้ไม่ต้องเสียเวลาทดลองเอง ซึ่งตอนนี้คงเป็นตอนสุดท้ายของ Vault Custom Plugin แล้ว ส่วนตอนหน้าจะเป็นอะไรรอดูกันต่อไปครับ

เพลงประกอบการเขียน Blog