From c4b781971305b02004e47cc604964a5144d01d26 Mon Sep 17 00:00:00 2001 From: Matthew Rich Date: Fri, 5 Apr 2024 10:22:17 -0700 Subject: [PATCH] add command helper --- internal/resource/command.go | 80 +++++++++++++ internal/resource/command_test.go | 58 ++++++++++ internal/resource/declaration.go | 9 +- internal/resource/decoder.go | 15 ++- internal/resource/decoder_test.go | 21 +++- internal/resource/document.go | 7 +- internal/resource/encoder.go | 6 +- internal/resource/exec.go | 20 ++-- internal/resource/exec_test.go | 4 +- internal/resource/http.go | 30 +++-- internal/resource/http_test.go | 2 +- internal/resource/package.go | 112 +++++++++++++++++++ internal/resource/package_test.go | 65 +++++++++++ internal/resource/schemas/exec.jsonschema | 20 ++++ internal/resource/schemas/package.jsonschema | 4 +- internal/resource/user.go | 22 ++-- 16 files changed, 432 insertions(+), 43 deletions(-) create mode 100644 internal/resource/command.go create mode 100644 internal/resource/command_test.go create mode 100644 internal/resource/package.go create mode 100644 internal/resource/package_test.go create mode 100644 internal/resource/schemas/exec.jsonschema diff --git a/internal/resource/command.go b/internal/resource/command.go new file mode 100644 index 0000000..a7d4c29 --- /dev/null +++ b/internal/resource/command.go @@ -0,0 +1,80 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package resource + +import ( +_ "context" + "fmt" + "gopkg.in/yaml.v3" +_ "log" +_ "net/url" +_ "os" + "os/exec" + "strings" + "text/template" + "io" + "encoding/json" +) + +type CommandArg string + +type Command struct { + Path string `json:"path" yaml:"path"` + Args []CommandArg `json:"args" yaml:"args"` +} + +func NewCommand() *Command { + return &Command{} +} + +func (c *Command) Load(r io.Reader) error { + decoder := NewYAMLDecoder(r) + return decoder.Decode(c) +} + +func (c *Command) LoadDecl(yamlResourceDeclaration string) error { + decoder := NewYAMLStringDecoder(yamlResourceDeclaration) + return decoder.Decode(c) +} + +func (c *Command) Template(value any) ([]string, error) { + var args []string = make([]string, len(c.Args)) + for i, arg := range c.Args { + var commandLineArg strings.Builder + err := template.Must(template.New(fmt.Sprintf("arg%d", i)).Parse(string(arg))).Execute(&commandLineArg, value) + if err != nil { + return nil, err + } + args[i] = commandLineArg.String() + } + return args, nil +} + +func (c *Command) Execute(value any) ([]byte, error) { + args, err := c.Template(value) + if err != nil { + return nil, err + } + return exec.Command(c.Path, args...).Output() +} + +func (c *CommandArg) UnmarshalValue(value string) error { + *c = CommandArg(value) + return nil +} + +func (c *CommandArg) UnmarshalJSON(data []byte) error { + var s string + if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil { + return unmarshalRouteTypeErr + } + return c.UnmarshalValue(s) +} + +func (c *CommandArg) UnmarshalYAML(value *yaml.Node) error { + var s string + if err := value.Decode(&s); err != nil { + return err + } + return c.UnmarshalValue(s) +} diff --git a/internal/resource/command_test.go b/internal/resource/command_test.go new file mode 100644 index 0000000..44dbb70 --- /dev/null +++ b/internal/resource/command_test.go @@ -0,0 +1,58 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + + +package resource + +import ( +_ "fmt" + "github.com/stretchr/testify/assert" +_ "os" +_ "strings" + "testing" +) + +func TestNewCommand(t *testing.T) { + c := NewCommand() + assert.NotNil(t, c) +} + +func TestCommandLoad(t *testing.T) { + c := NewCommand() + assert.NotNil(t, c) + + decl := ` +path: find +args: +- {{ .Path }} +` + + c.LoadDecl(decl) + assert.Equal(t, "find", c.Path) +} + +func TestCommandTemplate(t *testing.T) { + c := NewCommand() + assert.NotNil(t, c) + + decl := ` +path: find +args: +- "{{ .Path }}" +` + + c.LoadDecl(decl) + assert.Equal(t, "find", c.Path) + assert.Equal(t, 1, len(c.Args)) + + f := NewFile() + f.Path = "./" + args, templateErr := c.Template(f) + assert.Nil(t, templateErr) + assert.Equal(t, 1, len(args)) + + assert.Equal(t, "./", string(args[0])) + + out, err := c.Execute(f) + assert.Nil(t, err) + assert.Greater(t, len(out), 0) +} diff --git a/internal/resource/declaration.go b/internal/resource/declaration.go index 23badde..27e4de2 100644 --- a/internal/resource/declaration.go +++ b/internal/resource/declaration.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "fmt" + "io" "gopkg.in/yaml.v3" "log/slog" ) @@ -40,8 +41,14 @@ func NewDeclaration() *Declaration { return &Declaration{} } +func (d *Declaration) Load(r io.Reader) error { + c := NewYAMLDecoder(r) + return c.Decode(d) +} + func (d *Declaration) LoadDecl(yamlResourceDeclaration string) error { - return YamlLoadDecl(yamlResourceDeclaration, d) + c := NewYAMLStringDecoder(yamlResourceDeclaration) + return c.Decode(d) } func (d *Declaration) NewResource() error { diff --git a/internal/resource/decoder.go b/internal/resource/decoder.go index 3d8cf7d..f5362df 100644 --- a/internal/resource/decoder.go +++ b/internal/resource/decoder.go @@ -4,11 +4,12 @@ package resource import ( "encoding/json" - _ "fmt" - _ "github.com/xeipuuv/gojsonschema" +_ "fmt" +_ "github.com/xeipuuv/gojsonschema" "gopkg.in/yaml.v3" "io" - _ "log" +_ "log" + "strings" ) //type JSONDecoder json.Decoder @@ -25,10 +26,18 @@ func NewJSONDecoder(r io.Reader) Decoder { return json.NewDecoder(r) } +func NewJSONStringDecoder(s string) Decoder { + return json.NewDecoder(strings.NewReader(s)) +} + func NewYAMLDecoder(r io.Reader) Decoder { return yaml.NewDecoder(r) } +func NewYAMLStringDecoder(s string) Decoder { + return yaml.NewDecoder(strings.NewReader(s)) +} + func NewProtoBufDecoder(r io.Reader) Decoder { return nil } diff --git a/internal/resource/decoder_test.go b/internal/resource/decoder_test.go index 10afa67..d291eea 100644 --- a/internal/resource/decoder_test.go +++ b/internal/resource/decoder_test.go @@ -3,9 +3,9 @@ package resource import ( - _ "fmt" +_ "fmt" "github.com/stretchr/testify/assert" - _ "log" +_ "log" "strings" "testing" ) @@ -37,3 +37,20 @@ func TestNewDecoderDecodeJSON(t *testing.T) { validateErr := s.Validate(decl) assert.Nil(t, validateErr) } + +func TestNewJSONStringDecoder(t *testing.T) { + decl := `{ + "name": "testuser", + "uid": 12001, + "group": "12001", + "home": "/home/testuser", + "state": "present" +}` + + user := NewUser() + + e := NewJSONStringDecoder(decl) + assert.NotNil(t, e) + docErr := e.Decode(user) + assert.Nil(t, docErr) +} diff --git a/internal/resource/document.go b/internal/resource/document.go index 550ac6b..885eefb 100644 --- a/internal/resource/document.go +++ b/internal/resource/document.go @@ -23,11 +23,8 @@ func NewDocument() *Document { } func (d *Document) Load(r io.Reader) error { - yamlDecoder := yaml.NewDecoder(r) - if e := yamlDecoder.Decode(d); e != nil { - return e - } - return nil + c := NewYAMLDecoder(r) + return c.Decode(d); } func (d *Document) Validate() error { diff --git a/internal/resource/encoder.go b/internal/resource/encoder.go index a3ecb2d..9eb48e0 100644 --- a/internal/resource/encoder.go +++ b/internal/resource/encoder.go @@ -4,11 +4,11 @@ package resource import ( "encoding/json" - _ "fmt" - _ "github.com/xeipuuv/gojsonschema" +_ "fmt" +_ "github.com/xeipuuv/gojsonschema" "gopkg.in/yaml.v3" "io" - _ "log" +_ "log" ) type JSONEncoder json.Encoder diff --git a/internal/resource/exec.go b/internal/resource/exec.go index be6aa0a..f302994 100644 --- a/internal/resource/exec.go +++ b/internal/resource/exec.go @@ -16,11 +16,11 @@ import ( type Exec struct { loader YamlLoader - Id string `yaml:"id"` - // create command - // read command - // update command - // delete command + Id string `yaml:"id" json:"id"` + CreateTemplate Command `yaml:"create" json:"create"` + ReadTemplate Command `yaml:"read" json:"read"` + UpdateTemplate Command `yaml:"update" json:"update"` + DeleteTemplate Command `yaml:"delete" json:"delete"` // state attributes State string `yaml:"state"` @@ -43,10 +43,12 @@ func (x *Exec) URI() string { func (x *Exec) SetURI(uri string) error { resourceUri, e := url.Parse(uri) - if resourceUri.Scheme == "exec" { - x.Id = filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI()) - } else { - e = fmt.Errorf("%w: %s is not an exec resource ", ErrInvalidResourceURI, uri) + if e == nil { + if resourceUri.Scheme == "exec" { + x.Id = filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI()) + } else { + e = fmt.Errorf("%w: %s is not an exec resource ", ErrInvalidResourceURI, uri) + } } return e } diff --git a/internal/resource/exec_test.go b/internal/resource/exec_test.go index 5bd040b..bbd59b3 100644 --- a/internal/resource/exec_test.go +++ b/internal/resource/exec_test.go @@ -19,12 +19,12 @@ import ( func TestNewExecResource(t *testing.T) { x := NewExec() - assert.NotEqual(t, nil, x) + assert.NotNil(t, x) } func TestExecApplyResourceTransformation(t *testing.T) { x := NewExec() - assert.NotEqual(t, nil, x) + assert.NotNil(t, x) //e := f.Apply() //assert.Equal(t, nil, e) diff --git a/internal/resource/http.go b/internal/resource/http.go index 46eb027..4994052 100644 --- a/internal/resource/http.go +++ b/internal/resource/http.go @@ -4,12 +4,13 @@ package resource import ( "context" - _ "errors" +_ "errors" "fmt" "gopkg.in/yaml.v3" - _ "io" + "io" "net/url" - _ "os" + "net/http" +_ "os" ) func init() { @@ -24,7 +25,6 @@ func HTTPFactory(u *url.URL) Resource { // Manage the state of an HTTP endpoint type HTTP struct { - loader YamlLoader Endpoint string `yaml:"endpoint"` Body string `yaml:"body,omitempty"` @@ -32,7 +32,7 @@ type HTTP struct { } func NewHTTP() *HTTP { - return &HTTP{loader: YamlLoadDecl} + return &HTTP{} } func (h *HTTP) URI() string { @@ -57,8 +57,14 @@ func (h *HTTP) Apply() error { return nil } -func (h *HTTP) LoadDecl(yamlFileResourceDeclaration string) error { - return h.loader(yamlFileResourceDeclaration, h) +func (h *HTTP) Load(r io.Reader) error { + c := NewYAMLDecoder(r) + return c.Decode(h) +} + +func (h *HTTP) LoadDecl(yamlResourceDeclaration string) error { + c := NewYAMLStringDecoder(yamlResourceDeclaration) + return c.Decode(h) } func (h *HTTP) ResolveId(ctx context.Context) string { @@ -66,6 +72,16 @@ func (h *HTTP) ResolveId(ctx context.Context) string { } func (h *HTTP) Read(ctx context.Context) ([]byte, error) { + resp, err := http.Get(h.Endpoint) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, errReadBody := io.ReadAll(resp.Body) + if errReadBody != nil { + return nil, errReadBody + } + h.Body = string(body) return yaml.Marshal(h) } diff --git a/internal/resource/http_test.go b/internal/resource/http_test.go index b0408b9..2bc38da 100644 --- a/internal/resource/http_test.go +++ b/internal/resource/http_test.go @@ -21,5 +21,5 @@ import ( func TestNewHTTPResource(t *testing.T) { f := NewHTTP() - assert.NotEqual(t, nil, f) + assert.NotNil(t, f) } diff --git a/internal/resource/package.go b/internal/resource/package.go new file mode 100644 index 0000000..6348be8 --- /dev/null +++ b/internal/resource/package.go @@ -0,0 +1,112 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package resource + +import ( + "context" + "fmt" + "gopkg.in/yaml.v3" +_ "log" + "net/url" +_ "os" +_ "os/exec" + "io" + "path/filepath" +_ "strings" +) + +type Package struct { + Name string `yaml:"name" json:"name"` + Version string `yaml:"version" json:"version"` + + // state attributes + State string `yaml:"state"` +} + +func init() { + ResourceTypes.Register("package", func(u *url.URL) Resource { + p := NewPackage() + return p + }) +} + +func NewPackage() *Package { + return &Package{} +} + +func (p *Package) URI() string { + return fmt.Sprintf("package://%s?version=%s", p.Name, p.Version) +} + +func (p *Package) SetURI(uri string) error { + resourceUri, e := url.Parse(uri) + if e == nil { + if resourceUri.Scheme == "package" { + p.Name = filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI()) + p.Version = resourceUri.Query().Get("version") + if p.Version == "" { + p.Version = "latest" + } + } else { + e = fmt.Errorf("%w: %s is not a package resource ", ErrInvalidResourceURI, uri) + } + } + return e +} + +func (p *Package) ResolveId(ctx context.Context) string { + return "" +} + +func (p *Package) Apply() error { + return nil +} + + +func (p *Package) Load(r io.Reader) error { + c := NewYAMLDecoder(r) + return c.Decode(p) +} + +func (p *Package) LoadDecl(yamlResourceDeclaration string) error { + c := NewYAMLStringDecoder(yamlResourceDeclaration) + return c.Decode(p) +} + +func (p *Package) Type() string { return "package" } + +func (p *Package) Read(ctx context.Context) ([]byte, error) { + return yaml.Marshal(p) +} + + +func NewApkCreateCommand() *Command { + c := NewCommand() + c.Path = "apk" + c.Args = []CommandArg{ + CommandArg("add"), + CommandArg("{{ .Name }}={{ .Version }}"), + } + return c +} + +func NewApkReadCommand() *Command { + c := NewCommand() + c.Path = "apk" + c.Args = []CommandArg{ + CommandArg("info"), + CommandArg("-ev"), + CommandArg("{{ .Name }}"), + } + return c +} + +func NewApkDeleteCommand() *Command { + c := NewCommand() + c.Path = "apk" + c.Args = []CommandArg{ + CommandArg("del"), + CommandArg("{{ .Name }}"), + } + return c +} diff --git a/internal/resource/package_test.go b/internal/resource/package_test.go new file mode 100644 index 0000000..873210a --- /dev/null +++ b/internal/resource/package_test.go @@ -0,0 +1,65 @@ +// Copyright 2024 Matthew Rich . All rights reserved + +package resource + +import ( + "context" +_ "encoding/json" +_ "fmt" + "github.com/stretchr/testify/assert" +_ "gopkg.in/yaml.v3" +_ "io" +_ "log" +_ "net/http" +_ "net/http/httptest" +_ "net/url" +_ "os" +_ "strings" + "testing" +) + +func TestNewPackageResource(t *testing.T) { + p := NewPackage() + assert.NotNil(t, p) +} + +func TestPackageApplyResourceTransformation(t *testing.T) { + p := NewPackage() + assert.NotNil(t, p) + + //e := f.Apply() + //assert.Equal(t, nil, e) +} + +func TestReadPackage(t *testing.T) { + decl:=` +name: vim +version: latest +type: apk +` + + p := NewPackage() + assert.NotNil(t, p) + + loadErr := p.LoadDecl(decl) + assert.Nil(t, loadErr) + + yaml, readErr := p.Read(context.Background()) + assert.Nil(t, readErr) + assert.Greater(t, len(yaml), 0) +} + +func TestReadPackageError(t *testing.T) { +} + +func TestCreatePackage(t *testing.T) { +} + +func TestPackageSetURI(t *testing.T) { + p := NewPackage() + assert.NotNil(t, p) + e := p.SetURI("package://" + "12345_key") + assert.Nil(t, e) + assert.Equal(t, "package", p.Type()) + assert.Equal(t, "12345_key", p.Name) +} diff --git a/internal/resource/schemas/exec.jsonschema b/internal/resource/schemas/exec.jsonschema new file mode 100644 index 0000000..7568025 --- /dev/null +++ b/internal/resource/schemas/exec.jsonschema @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "exec", + "type": "object", + "required": [ "create", "read" ], + "properties": { + "create": { + "type": "string" + }, + "read": { + "type": "string" + }, + "update": { + "type": "string" + }, + "delete": { + "type": "string" + } + } +} diff --git a/internal/resource/schemas/package.jsonschema b/internal/resource/schemas/package.jsonschema index cc26a3b..6177c2a 100644 --- a/internal/resource/schemas/package.jsonschema +++ b/internal/resource/schemas/package.jsonschema @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "package", "type": "object", - "required": [ "name", "type" ], + "required": [ "name", "version", "type" ], "properties": { "name": { "type": "string" @@ -13,7 +13,7 @@ "type": { "type": "string", "description": "package type", - "enum": [ "rpm", "deb", "yum", "dnf", "apt", "pip", "go" ] + "enum": [ "rpm", "deb", "yum", "dnf", "apt", "apk", "pip", "go" ] } } } diff --git a/internal/resource/user.go b/internal/resource/user.go index 52a2993..ed92e65 100644 --- a/internal/resource/user.go +++ b/internal/resource/user.go @@ -6,17 +6,17 @@ import ( "context" "fmt" "gopkg.in/yaml.v3" - "log" + "log/slog" "net/url" - _ "os" +_ "os" "os/exec" "os/user" + "io" "strconv" "strings" ) type User struct { - loader YamlLoader Name string `json:"name" yaml:"name"` UID int `json:"uid,omitempty" yaml:"uid,omitempty"` Group string `json:"group,omitempty" yaml:"group,omitempty"` @@ -30,7 +30,7 @@ type User struct { } func NewUser() *User { - return &User{loader: YamlLoadDecl} + return &User{} } func init() { @@ -76,7 +76,7 @@ func (u *User) Apply() error { args = append(args, u.Name) cmd := exec.Command(userCommandName, args...) cmdOutput, cmdErr := cmd.CombinedOutput() - log.Printf("%s\n", cmdOutput) + slog.Info("user command", "command", cmd.String(), "output", string(cmdOutput)) return cmdErr } case "absent": @@ -91,14 +91,20 @@ func (u *User) Apply() error { args = append(args, u.Name) cmd := exec.Command(userDelCommandName, args...) cmdOutput, cmdErr := cmd.CombinedOutput() - log.Printf("%s\n", cmdOutput) + slog.Info("user command", "command", cmd.String(), "output", string(cmdOutput)) return cmdErr } return nil } -func (u *User) LoadDecl(yamlFileResourceDeclaration string) error { - return u.loader(yamlFileResourceDeclaration, u) +func (u *User) Load(r io.Reader) error { + c := NewYAMLDecoder(r) + return c.Decode(u) +} + +func (u *User) LoadDecl(yamlResourceDeclaration string) error { + c := NewYAMLStringDecoder(yamlResourceDeclaration) + return c.Decode(u) } func (u *User) AddUserCommand(args *[]string) error {