From 9c0ec525602fcfd3b5851be8999ef553ef266fec Mon Sep 17 00:00:00 2001 From: Matthew Rich Date: Thu, 9 May 2024 00:39:45 -0700 Subject: [PATCH] add resource storage states --- internal/resource/command.go | 10 +- internal/resource/container_network.go | 22 +++- internal/resource/declaration.go | 10 +- internal/resource/declaration_test.go | 21 +++ internal/resource/document.go | 8 +- internal/resource/file.go | 132 +++++++++++-------- internal/resource/http.go | 24 +++- internal/resource/http_test.go | 3 +- internal/resource/iptables.go | 22 +++- internal/resource/network_route.go | 26 +++- internal/resource/package.go | 45 ++++++- internal/resource/resource.go | 6 +- internal/resource/schemas/package.jsonschema | 4 +- internal/resource/user.go | 22 +++- 14 files changed, 274 insertions(+), 81 deletions(-) diff --git a/internal/resource/command.go b/internal/resource/command.go index 0028532..01d6f7a 100644 --- a/internal/resource/command.go +++ b/internal/resource/command.go @@ -24,12 +24,13 @@ type CommandArg string type Command struct { Path string `json:"path" yaml:"path"` Args []CommandArg `json:"args" yaml:"args"` + Split bool `json:"split" yaml:"split` Executor CommandExecutor `json:"-" yaml:"-"` Extractor CommandExtractAttributes `json:"-" yaml:"-"` } func NewCommand() *Command { - c := &Command{} + c := &Command{ Split: true } c.Executor = func(value any) ([]byte, error) { args, err := c.Template(value) if err != nil { @@ -88,7 +89,12 @@ func (c *Command) Template(value any) ([]string, error) { return nil, err } if commandLineArg.Len() > 0 { - splitArg := strings.Split(commandLineArg.String(), " ") + var splitArg []string + if c.Split { + splitArg = strings.Split(commandLineArg.String(), " ") + } else { + splitArg = []string{commandLineArg.String()} + } slog.Info("Template()", "split", splitArg, "len", len(splitArg)) args = append(args, splitArg...) } diff --git a/internal/resource/container_network.go b/internal/resource/container_network.go index be95227..fef4d92 100644 --- a/internal/resource/container_network.go +++ b/internal/resource/container_network.go @@ -31,6 +31,7 @@ type ContainerNetworkClient interface { } type ContainerNetwork struct { + stater machine.Stater `json:"-" yaml:"-"` Id string `json:"ID,omitempty" yaml:"ID,omitempty"` Name string `json:"name" yaml:"name"` @@ -71,7 +72,26 @@ func (n *ContainerNetwork) Clone() Resource { } func (n *ContainerNetwork) StateMachine() machine.Stater { - return StorageMachine() + if n.stater == nil { + n.stater = StorageMachine(n) + } + return n.stater +} + +func (n *ContainerNetwork) Notify(m *machine.EventMessage) { + ctx := context.Background() + switch m.On { + case machine.ENTERSTATEEVENT: + switch m.Dest { + case "start_create": + if e := n.Create(ctx); e != nil { + n.stater.Trigger("created") + } + case "present": + n.State = "present" + } + case machine.EXITSTATEEVENT: + } } func (n *ContainerNetwork) URI() string { diff --git a/internal/resource/declaration.go b/internal/resource/declaration.go index e652626..600a674 100644 --- a/internal/resource/declaration.go +++ b/internal/resource/declaration.go @@ -39,6 +39,7 @@ func NewDeclaration() *Declaration { func (d *Declaration) Clone() *Declaration { return &Declaration { Type: d.Type, + Transition: d.Transition, Attributes: d.Attributes.Clone(), } } @@ -66,7 +67,14 @@ func (d *Declaration) Resource() Resource { func (d *Declaration) Apply() error { stater := d.Attributes.StateMachine() - return stater.Trigger(d.Transition) + switch d.Transition { + case "absent": + default: + fallthrough + case "create", "present": + return stater.Trigger("create") + } + return nil } func (d *Declaration) SetURI(uri string) error { diff --git a/internal/resource/declaration_test.go b/internal/resource/declaration_test.go index 6035dc2..6fc801b 100644 --- a/internal/resource/declaration_test.go +++ b/internal/resource/declaration_test.go @@ -111,3 +111,24 @@ func TestDeclarationJson(t *testing.T) { assert.Equal(t, "10012", userResourceDeclaration.Attributes.(*User).UID) } + +func TestDeclarationTransition(t *testing.T) { + fileName := filepath.Join(TempDir, "testdecl.txt") + fileDeclJson := fmt.Sprintf(` +{ + "type": "file", + "transition": "present", + "attributes": { + "path": "%s" + } +} +`, fileName) + + resourceDeclaration := NewDeclaration() + e := json.Unmarshal([]byte(fileDeclJson), resourceDeclaration) + assert.Nil(t, e) + assert.Equal(t, TypeName("file"), resourceDeclaration.Type) + assert.Equal(t, fileName, resourceDeclaration.Attributes.(*File).Path) + resourceDeclaration.Apply() + assert.FileExists(t, fileName) +} diff --git a/internal/resource/document.go b/internal/resource/document.go index d2a56ed..2fd8c9a 100644 --- a/internal/resource/document.go +++ b/internal/resource/document.go @@ -122,6 +122,7 @@ func (d *Document) YAML() ([]byte, error) { } func (d *Document) Diff(with *Document, output io.Writer) (string, error) { + slog.Info("Document.Diff()") opts := []yamldiff.DoOptionFunc{} if output == nil { output = &strings.Builder{} @@ -130,7 +131,6 @@ func (d *Document) Diff(with *Document, output io.Writer) (string, error) { if yerr != nil { return "", yerr } - yamlDiff,yamlDiffErr := yamldiff.Load(string(ydata)) if yamlDiffErr != nil { return "", yamlDiffErr @@ -145,9 +145,9 @@ func (d *Document) Diff(with *Document, output io.Writer) (string, error) { return "", withDiffErr } - for _,diff := range yamldiff.Do(yamlDiff, withDiff, opts...) { - slog.Info("Diff()", "diff", diff) - _,e := output.Write([]byte(diff.Dump())) + for _,docDiffResults := range yamldiff.Do(yamlDiff, withDiff, opts...) { + slog.Info("Diff()", "diff", docDiffResults, "dump", docDiffResults.Dump()) + _,e := output.Write([]byte(docDiffResults.Dump())) if e != nil { return "", e } diff --git a/internal/resource/file.go b/internal/resource/file.go index e36d1c2..61ac861 100644 --- a/internal/resource/file.go +++ b/internal/resource/file.go @@ -49,6 +49,7 @@ func init() { // Manage the state of file system objects type File struct { + stater machine.Stater `json:"-" yaml:"-"` normalizePath bool `json:"-" yaml:"-"` Path string `json:"path" yaml:"path"` Owner string `json:"owner" yaml:"owner"` @@ -105,7 +106,26 @@ func (f *File) Clone() Resource { } func (f *File) StateMachine() machine.Stater { - return StorageMachine() + if f.stater == nil { + f.stater = StorageMachine(f) + } + return f.stater +} + +func (f *File) Notify(m *machine.EventMessage) { + ctx := context.Background() + switch m.On { + case machine.ENTERSTATEEVENT: + switch m.Dest { + case "start_create": + if e := f.Create(ctx); e != nil { + f.stater.Trigger("created") + } + case "present": + f.State = "present" + } + case machine.EXITSTATEEVENT: + } } func (f *File) URI() string { @@ -139,63 +159,7 @@ func (f *File) Apply() error { return removeErr } case "present": - { - uid, uidErr := LookupUID(f.Owner) - if uidErr != nil { - return fmt.Errorf("%w: unkwnon user %d", ErrInvalidFileOwner, uid) - } - - gid, gidErr := LookupGID(f.Group) - if gidErr != nil { - return gidErr - } - - slog.Info("File.Mode", "mode", f.Mode) - mode, modeErr := strconv.ParseInt(f.Mode, 8, 64) - if modeErr != nil { - return fmt.Errorf("%w: invalid mode %d", ErrInvalidFileMode, mode) - } - - //e := os.Stat(f.path) - //if os.IsNotExist(e) { - switch f.FileType { - case SymbolicLinkFile: - linkErr := os.Symlink(f.Target, f.Path) - if linkErr != nil { - return linkErr - } - case DirectoryFile: - if mkdirErr := os.MkdirAll(f.Path, os.FileMode(mode)); mkdirErr != nil { - return mkdirErr - } - default: - fallthrough - case RegularFile: - createdFile, e := os.Create(f.Path) - if e != nil { - return e - } - defer createdFile.Close() - - if chmodErr := createdFile.Chmod(os.FileMode(mode)); chmodErr != nil { - return chmodErr - } - _, writeErr := createdFile.Write([]byte(f.Content)) - if writeErr != nil { - return writeErr - } - if !f.Mtime.IsZero() && !f.Atime.IsZero() { - if chtimesErr := os.Chtimes(f.Path, f.Atime, f.Mtime); chtimesErr != nil { - return chtimesErr - } - } - } - - if chownErr := os.Chown(f.Path, uid, gid); chownErr != nil { - return chownErr - } - - } + return f.Create(context.Background()) } return nil @@ -263,6 +227,58 @@ func (f *ResourceFileInfo) Sys() any { return nil } +func (f *File) Create(ctx context.Context) error { + uid, uidErr := LookupUID(f.Owner) + if uidErr != nil { + return fmt.Errorf("%w: unkwnon user %d", ErrInvalidFileOwner, uid) + } + gid, gidErr := LookupGID(f.Group) + if gidErr != nil { + return gidErr + } + mode, modeErr := strconv.ParseInt(f.Mode, 8, 64) + if modeErr != nil { + return fmt.Errorf("%w: invalid mode %d", ErrInvalidFileMode, mode) + } + //e := os.Stat(f.path) + //if os.IsNotExist(e) { + switch f.FileType { + case SymbolicLinkFile: + linkErr := os.Symlink(f.Target, f.Path) + if linkErr != nil { + return linkErr + } + case DirectoryFile: + if mkdirErr := os.MkdirAll(f.Path, os.FileMode(mode)); mkdirErr != nil { + return mkdirErr + } + default: + fallthrough + case RegularFile: + createdFile, e := os.Create(f.Path) + if e != nil { + return e + } + defer createdFile.Close() + if chmodErr := createdFile.Chmod(os.FileMode(mode)); chmodErr != nil { + return chmodErr + } + _, writeErr := createdFile.Write([]byte(f.Content)) + if writeErr != nil { + return writeErr + } + if !f.Mtime.IsZero() && !f.Atime.IsZero() { + if chtimesErr := os.Chtimes(f.Path, f.Atime, f.Mtime); chtimesErr != nil { + return chtimesErr + } + } + } + if chownErr := os.Chown(f.Path, uid, gid); chownErr != nil { + return chownErr + } + return nil +} + func (f *File) UpdateContentAttributes() { f.Size = int64(len(f.Content)) f.Sha256 = fmt.Sprintf("%x", sha256.Sum256([]byte(f.Content))) diff --git a/internal/resource/http.go b/internal/resource/http.go index 9a04344..d6376d8 100644 --- a/internal/resource/http.go +++ b/internal/resource/http.go @@ -35,6 +35,7 @@ type HTTPHeader struct { // Manage the state of an HTTP endpoint type HTTP struct { + stater machine.Stater `yaml:"-" json:"-"` client *http.Client `yaml:"-" json:"-"` Endpoint string `yaml:"endpoint" json:"endpoint"` Headers []HTTPHeader `yaml:"headers,omitempty" json:"headers,omitempty"` @@ -57,7 +58,26 @@ func (h *HTTP) Clone() Resource { } func (h *HTTP) StateMachine() machine.Stater { - return StorageMachine() + if h.stater == nil { + h.stater = StorageMachine(h) + } + return h.stater +} + +func (h *HTTP) Notify(m *machine.EventMessage) { + ctx := context.Background() + switch m.On { + case machine.ENTERSTATEEVENT: + switch m.Dest { + case "start_create": + if e := h.Create(ctx); e != nil { + h.stater.Trigger("created") + } + case "present": + h.State = "present" + } + case machine.EXITSTATEEVENT: + } } func (h *HTTP) URI() string { @@ -112,7 +132,7 @@ func (h *HTTP) ResolveId(ctx context.Context) string { return h.Endpoint } -func (h *HTTP) Create() error { +func (h *HTTP) Create(ctx context.Context) error { body := strings.NewReader(h.Body) req, reqErr := http.NewRequest("POST", h.Endpoint, body) if reqErr != nil { diff --git a/internal/resource/http_test.go b/internal/resource/http_test.go index c9f8577..28cfc0c 100644 --- a/internal/resource/http_test.go +++ b/internal/resource/http_test.go @@ -67,6 +67,7 @@ endpoint: "%s/resource/user/foo" } func TestHTTPCreate(t *testing.T) { + ctx := context.Background() userdecl := ` type: "user" attributes: @@ -96,6 +97,6 @@ body: | `, server.URL, re.ReplaceAllString(userdecl, " $1")) assert.Nil(t, h.LoadDecl(decl)) assert.Greater(t, len(h.Body), 0) - e := h.Create() + e := h.Create(ctx) assert.Nil(t, e) } diff --git a/internal/resource/iptables.go b/internal/resource/iptables.go index 0b26976..3a283eb 100644 --- a/internal/resource/iptables.go +++ b/internal/resource/iptables.go @@ -103,6 +103,7 @@ const ( // Manage the state of iptables rules // iptable://filter/INPUT/0 type Iptable struct { + stater machine.Stater `json:"-" yaml:"-"` Id uint `json:"id,omitempty" yaml:"id,omitempty"` Table IptableName `json:"table" yaml:"table"` Chain IptableChain `json:"chain" yaml:"chain"` @@ -151,7 +152,26 @@ func (i *Iptable) Clone() Resource { } func (i *Iptable) StateMachine() machine.Stater { - return StorageMachine() + if i.stater == nil { + i.stater = StorageMachine(i) + } + return i.stater +} + +func (i *Iptable) Notify(m *machine.EventMessage) { + ctx := context.Background() + switch m.On { + case machine.ENTERSTATEEVENT: + switch m.Dest { + case "start_create": + if e := i.Create(ctx); e != nil { + i.stater.Trigger("created") + } + case "present": + i.State = "present" + } + case machine.EXITSTATEEVENT: + } } func (i *Iptable) URI() string { diff --git a/internal/resource/network_route.go b/internal/resource/network_route.go index 511835d..0504e7d 100644 --- a/internal/resource/network_route.go +++ b/internal/resource/network_route.go @@ -108,6 +108,7 @@ const ( // Manage the state of network routes type NetworkRoute struct { + stater machine.Stater `json:"-" yaml:"-"` Id string To string `json:"to" yaml:"to"` Interface string `json:"interface" yaml:"interface"` @@ -140,7 +141,30 @@ func (n *NetworkRoute) Clone() Resource { } func (n *NetworkRoute) StateMachine() machine.Stater { - return StorageMachine() + if n.stater == nil { + n.stater = StorageMachine(n) + } + return n.stater +} + +func (n *NetworkRoute) Notify(m *machine.EventMessage) { + ctx := context.Background() + switch m.On { + case machine.ENTERSTATEEVENT: + switch m.Dest { + case "start_create": + if e := n.Create(ctx); e != nil { + n.stater.Trigger("created") + } + case "present": + n.State = "present" + } + case machine.EXITSTATEEVENT: + } +} + +func (n *NetworkRoute) Create(ctx context.Context) error { + return nil } func (n *NetworkRoute) URI() string { diff --git a/internal/resource/package.go b/internal/resource/package.go index e47100a..15c9a72 100644 --- a/internal/resource/package.go +++ b/internal/resource/package.go @@ -31,9 +31,10 @@ const ( ) type Package struct { + stater machine.Stater `yaml:"-" json:"-"` Name string `yaml:"name" json:"name"` - Required string `json:"required" yaml:"required"` - Version string `yaml:"version" json:"version"` + Required string `json:"required,omitempty" yaml:"required,omitempty"` + Version string `yaml:"version,omitempty" json:"version,omitempty"` PackageType PackageType `yaml:"type" json:"type"` CreateCommand *Command `yaml:"-" json:"-"` @@ -41,7 +42,7 @@ type Package struct { UpdateCommand *Command `yaml:"-" json:"-"` DeleteCommand *Command `yaml:"-" json:"-"` // state attributes - State string `yaml:"state" json:"state"` + State string `yaml:"state,omitempty" json:"state,omitempty"` } func init() { @@ -80,7 +81,7 @@ func init() { } func NewPackage() *Package { - return &Package{} + return &Package{ PackageType: PackageTypeApk } } func (p *Package) Clone() Resource { @@ -96,7 +97,26 @@ func (p *Package) Clone() Resource { } func (p *Package) StateMachine() machine.Stater { - return StorageMachine() + if p.stater == nil { + p.stater = StorageMachine(p) + } + return p.stater +} + +func (p *Package) Notify(m *machine.EventMessage) { + ctx := context.Background() + switch m.On { + case machine.ENTERSTATEEVENT: + switch m.Dest { + case "start_create": + if e := p.Create(ctx); e != nil { + p.stater.Trigger("created") + } + case "present": + p.State = "present" + } + case machine.EXITSTATEEVENT: + } } func (p *Package) URI() string { @@ -140,6 +160,18 @@ func (p *Package) ResolveId(ctx context.Context) string { return "" } +func (p *Package) Create(ctx context.Context) error { + if p.Version == "latest" { + p.Version = "" + } + _, err := p.CreateCommand.Execute(p) + if err != nil { + return err + } + _,e := p.Read(ctx) + return e +} + func (p *Package) Apply() error { if p.Version == "latest" { p.Version = "" @@ -292,10 +324,11 @@ func NewApkDeleteCommand() *Command { func NewAptCreateCommand() *Command { c := NewCommand() c.Path = "apt-get" + c.Split = false c.Args = []CommandArg{ CommandArg("satisfy"), CommandArg("-y"), - CommandArg("{{ .Name }} ({{ .Required }})"), + CommandArg("{{ .Name }} ({{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }})"), } return c } diff --git a/internal/resource/resource.go b/internal/resource/resource.go index f7c93ba..a8513ce 100644 --- a/internal/resource/resource.go +++ b/internal/resource/resource.go @@ -58,17 +58,21 @@ func NewResource(uri string) Resource { return nil } -func StorageMachine() machine.Stater { +func StorageMachine(sub machine.Subscriber) machine.Stater { // start_destroy -> absent -> start_create -> present -> start_destroy stater := machine.New("absent") stater.AddStates("absent", "start_create", "present", "start_delete", "start_read", "start_update") stater.AddTransition("create", "absent", "start_create") + stater.AddSubscription("create", sub) stater.AddTransition("created", "start_create", "present") stater.AddTransition("read", "*", "start_read") + stater.AddSubscription("read", sub) stater.AddTransition("state_read", "start_read", "present") stater.AddTransition("update", "*", "start_update") + stater.AddSubscription("update", sub) stater.AddTransition("updated", "start_update", "present") stater.AddTransition("delete", "*", "start_delete") + stater.AddSubscription("delete", sub) stater.AddTransition("deleted", "start_delete", "absent") return stater } diff --git a/internal/resource/schemas/package.jsonschema b/internal/resource/schemas/package.jsonschema index d6896a9..b99f130 100644 --- a/internal/resource/schemas/package.jsonschema +++ b/internal/resource/schemas/package.jsonschema @@ -7,12 +7,12 @@ "properties": { "name": { "type": "string", - "pattern": "^[a-zA-Z0-9][a-zA-Z0-9+.-_]+$" + "pattern": "^[a-zA-Z0-9][-a-zA-Z0-9+._]+$" }, "required": { "description": "version requirement", "type": "string", - "pattern": "^([><~=]{0,1}[-_a-zA-Z0-9+.]+|)$" + "pattern": "^([><~=]{0,2}[-_a-zA-Z0-9+.]+|)$" }, "version": { "type": "string" diff --git a/internal/resource/user.go b/internal/resource/user.go index 9803b0e..64723b3 100644 --- a/internal/resource/user.go +++ b/internal/resource/user.go @@ -28,6 +28,7 @@ const ( ) type User struct { + stater machine.Stater `json:"-" yaml:"-"` Name string `json:"name" yaml:"name"` UID string `json:"uid,omitempty" yaml:"uid,omitempty"` Group string `json:"group,omitempty" yaml:"group,omitempty"` @@ -83,7 +84,26 @@ func (u *User) Clone() Resource { } func (u *User) StateMachine() machine.Stater { - return StorageMachine() + if u.stater == nil { + u.stater = StorageMachine(u) + } + return u.stater +} + +func (u *User) Notify(m *machine.EventMessage) { + ctx := context.Background() + switch m.On { + case machine.ENTERSTATEEVENT: + switch m.Dest { + case "start_create": + if e := u.Create(ctx); e != nil { + u.stater.Trigger("created") + } + case "present": + u.State = "present" + } + case machine.EXITSTATEEVENT: + } } func (u *User) SetURI(uri string) error {