Compare commits

...

4 Commits

Author SHA1 Message Date
78e52933eb Merge branch 'main' of https://gitea.rosskeen.house/Declarative/decl
Some checks failed
Declarative Tests / test (push) Waiting to run
Lint / golangci-lint (push) Has been cancelled
2024-05-09 00:46:08 -07:00
9c0ec52560 add resource storage states 2024-05-09 00:39:45 -07:00
05b229d500 add jx diff image
All checks were successful
Lint / golangci-lint (push) Successful in 9m58s
Declarative Tests / test (push) Successful in 1m28s
2024-05-08 20:41:08 -07:00
3ef1915ab7 add screencap of jx diff
All checks were successful
Lint / golangci-lint (push) Successful in 10m2s
Declarative Tests / test (push) Successful in 1m36s
2024-05-08 17:49:50 -07:00
15 changed files with 274 additions and 81 deletions

View File

@ -24,12 +24,13 @@ type CommandArg string
type Command struct { type Command struct {
Path string `json:"path" yaml:"path"` Path string `json:"path" yaml:"path"`
Args []CommandArg `json:"args" yaml:"args"` Args []CommandArg `json:"args" yaml:"args"`
Split bool `json:"split" yaml:"split`
Executor CommandExecutor `json:"-" yaml:"-"` Executor CommandExecutor `json:"-" yaml:"-"`
Extractor CommandExtractAttributes `json:"-" yaml:"-"` Extractor CommandExtractAttributes `json:"-" yaml:"-"`
} }
func NewCommand() *Command { func NewCommand() *Command {
c := &Command{} c := &Command{ Split: true }
c.Executor = func(value any) ([]byte, error) { c.Executor = func(value any) ([]byte, error) {
args, err := c.Template(value) args, err := c.Template(value)
if err != nil { if err != nil {
@ -88,7 +89,12 @@ func (c *Command) Template(value any) ([]string, error) {
return nil, err return nil, err
} }
if commandLineArg.Len() > 0 { 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)) slog.Info("Template()", "split", splitArg, "len", len(splitArg))
args = append(args, splitArg...) args = append(args, splitArg...)
} }

View File

@ -31,6 +31,7 @@ type ContainerNetworkClient interface {
} }
type ContainerNetwork struct { type ContainerNetwork struct {
stater machine.Stater `json:"-" yaml:"-"`
Id string `json:"ID,omitempty" yaml:"ID,omitempty"` Id string `json:"ID,omitempty" yaml:"ID,omitempty"`
Name string `json:"name" yaml:"name"` Name string `json:"name" yaml:"name"`
@ -71,7 +72,26 @@ func (n *ContainerNetwork) Clone() Resource {
} }
func (n *ContainerNetwork) StateMachine() machine.Stater { 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 { func (n *ContainerNetwork) URI() string {

View File

@ -39,6 +39,7 @@ func NewDeclaration() *Declaration {
func (d *Declaration) Clone() *Declaration { func (d *Declaration) Clone() *Declaration {
return &Declaration { return &Declaration {
Type: d.Type, Type: d.Type,
Transition: d.Transition,
Attributes: d.Attributes.Clone(), Attributes: d.Attributes.Clone(),
} }
} }
@ -66,7 +67,14 @@ func (d *Declaration) Resource() Resource {
func (d *Declaration) Apply() error { func (d *Declaration) Apply() error {
stater := d.Attributes.StateMachine() 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 { func (d *Declaration) SetURI(uri string) error {

View File

@ -111,3 +111,24 @@ func TestDeclarationJson(t *testing.T) {
assert.Equal(t, "10012", userResourceDeclaration.Attributes.(*User).UID) 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)
}

View File

@ -122,6 +122,7 @@ func (d *Document) YAML() ([]byte, error) {
} }
func (d *Document) Diff(with *Document, output io.Writer) (string, error) { func (d *Document) Diff(with *Document, output io.Writer) (string, error) {
slog.Info("Document.Diff()")
opts := []yamldiff.DoOptionFunc{} opts := []yamldiff.DoOptionFunc{}
if output == nil { if output == nil {
output = &strings.Builder{} output = &strings.Builder{}
@ -130,7 +131,6 @@ func (d *Document) Diff(with *Document, output io.Writer) (string, error) {
if yerr != nil { if yerr != nil {
return "", yerr return "", yerr
} }
yamlDiff,yamlDiffErr := yamldiff.Load(string(ydata)) yamlDiff,yamlDiffErr := yamldiff.Load(string(ydata))
if yamlDiffErr != nil { if yamlDiffErr != nil {
return "", yamlDiffErr return "", yamlDiffErr
@ -145,9 +145,9 @@ func (d *Document) Diff(with *Document, output io.Writer) (string, error) {
return "", withDiffErr return "", withDiffErr
} }
for _,diff := range yamldiff.Do(yamlDiff, withDiff, opts...) { for _,docDiffResults := range yamldiff.Do(yamlDiff, withDiff, opts...) {
slog.Info("Diff()", "diff", diff) slog.Info("Diff()", "diff", docDiffResults, "dump", docDiffResults.Dump())
_,e := output.Write([]byte(diff.Dump())) _,e := output.Write([]byte(docDiffResults.Dump()))
if e != nil { if e != nil {
return "", e return "", e
} }

View File

@ -49,6 +49,7 @@ func init() {
// Manage the state of file system objects // Manage the state of file system objects
type File struct { type File struct {
stater machine.Stater `json:"-" yaml:"-"`
normalizePath bool `json:"-" yaml:"-"` normalizePath bool `json:"-" yaml:"-"`
Path string `json:"path" yaml:"path"` Path string `json:"path" yaml:"path"`
Owner string `json:"owner" yaml:"owner"` Owner string `json:"owner" yaml:"owner"`
@ -105,7 +106,26 @@ func (f *File) Clone() Resource {
} }
func (f *File) StateMachine() machine.Stater { 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 { func (f *File) URI() string {
@ -139,63 +159,7 @@ func (f *File) Apply() error {
return removeErr return removeErr
} }
case "present": case "present":
{ return f.Create(context.Background())
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 nil return nil
@ -263,6 +227,58 @@ func (f *ResourceFileInfo) Sys() any {
return nil 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() { func (f *File) UpdateContentAttributes() {
f.Size = int64(len(f.Content)) f.Size = int64(len(f.Content))
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256([]byte(f.Content))) f.Sha256 = fmt.Sprintf("%x", sha256.Sum256([]byte(f.Content)))

View File

@ -35,6 +35,7 @@ type HTTPHeader struct {
// Manage the state of an HTTP endpoint // Manage the state of an HTTP endpoint
type HTTP struct { type HTTP struct {
stater machine.Stater `yaml:"-" json:"-"`
client *http.Client `yaml:"-" json:"-"` client *http.Client `yaml:"-" json:"-"`
Endpoint string `yaml:"endpoint" json:"endpoint"` Endpoint string `yaml:"endpoint" json:"endpoint"`
Headers []HTTPHeader `yaml:"headers,omitempty" json:"headers,omitempty"` Headers []HTTPHeader `yaml:"headers,omitempty" json:"headers,omitempty"`
@ -57,7 +58,26 @@ func (h *HTTP) Clone() Resource {
} }
func (h *HTTP) StateMachine() machine.Stater { 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 { func (h *HTTP) URI() string {
@ -112,7 +132,7 @@ func (h *HTTP) ResolveId(ctx context.Context) string {
return h.Endpoint return h.Endpoint
} }
func (h *HTTP) Create() error { func (h *HTTP) Create(ctx context.Context) error {
body := strings.NewReader(h.Body) body := strings.NewReader(h.Body)
req, reqErr := http.NewRequest("POST", h.Endpoint, body) req, reqErr := http.NewRequest("POST", h.Endpoint, body)
if reqErr != nil { if reqErr != nil {

View File

@ -67,6 +67,7 @@ endpoint: "%s/resource/user/foo"
} }
func TestHTTPCreate(t *testing.T) { func TestHTTPCreate(t *testing.T) {
ctx := context.Background()
userdecl := ` userdecl := `
type: "user" type: "user"
attributes: attributes:
@ -96,6 +97,6 @@ body: |
`, server.URL, re.ReplaceAllString(userdecl, " $1")) `, server.URL, re.ReplaceAllString(userdecl, " $1"))
assert.Nil(t, h.LoadDecl(decl)) assert.Nil(t, h.LoadDecl(decl))
assert.Greater(t, len(h.Body), 0) assert.Greater(t, len(h.Body), 0)
e := h.Create() e := h.Create(ctx)
assert.Nil(t, e) assert.Nil(t, e)
} }

View File

@ -103,6 +103,7 @@ const (
// Manage the state of iptables rules // Manage the state of iptables rules
// iptable://filter/INPUT/0 // iptable://filter/INPUT/0
type Iptable struct { type Iptable struct {
stater machine.Stater `json:"-" yaml:"-"`
Id uint `json:"id,omitempty" yaml:"id,omitempty"` Id uint `json:"id,omitempty" yaml:"id,omitempty"`
Table IptableName `json:"table" yaml:"table"` Table IptableName `json:"table" yaml:"table"`
Chain IptableChain `json:"chain" yaml:"chain"` Chain IptableChain `json:"chain" yaml:"chain"`
@ -151,7 +152,26 @@ func (i *Iptable) Clone() Resource {
} }
func (i *Iptable) StateMachine() machine.Stater { 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 { func (i *Iptable) URI() string {

View File

@ -108,6 +108,7 @@ const (
// Manage the state of network routes // Manage the state of network routes
type NetworkRoute struct { type NetworkRoute struct {
stater machine.Stater `json:"-" yaml:"-"`
Id string Id string
To string `json:"to" yaml:"to"` To string `json:"to" yaml:"to"`
Interface string `json:"interface" yaml:"interface"` Interface string `json:"interface" yaml:"interface"`
@ -140,7 +141,30 @@ func (n *NetworkRoute) Clone() Resource {
} }
func (n *NetworkRoute) StateMachine() machine.Stater { 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 { func (n *NetworkRoute) URI() string {

View File

@ -31,9 +31,10 @@ const (
) )
type Package struct { type Package struct {
stater machine.Stater `yaml:"-" json:"-"`
Name string `yaml:"name" json:"name"` Name string `yaml:"name" json:"name"`
Required string `json:"required" yaml:"required"` Required string `json:"required,omitempty" yaml:"required,omitempty"`
Version string `yaml:"version" json:"version"` Version string `yaml:"version,omitempty" json:"version,omitempty"`
PackageType PackageType `yaml:"type" json:"type"` PackageType PackageType `yaml:"type" json:"type"`
CreateCommand *Command `yaml:"-" json:"-"` CreateCommand *Command `yaml:"-" json:"-"`
@ -41,7 +42,7 @@ type Package struct {
UpdateCommand *Command `yaml:"-" json:"-"` UpdateCommand *Command `yaml:"-" json:"-"`
DeleteCommand *Command `yaml:"-" json:"-"` DeleteCommand *Command `yaml:"-" json:"-"`
// state attributes // state attributes
State string `yaml:"state" json:"state"` State string `yaml:"state,omitempty" json:"state,omitempty"`
} }
func init() { func init() {
@ -80,7 +81,7 @@ func init() {
} }
func NewPackage() *Package { func NewPackage() *Package {
return &Package{} return &Package{ PackageType: PackageTypeApk }
} }
func (p *Package) Clone() Resource { func (p *Package) Clone() Resource {
@ -96,7 +97,26 @@ func (p *Package) Clone() Resource {
} }
func (p *Package) StateMachine() machine.Stater { 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 { func (p *Package) URI() string {
@ -140,6 +160,18 @@ func (p *Package) ResolveId(ctx context.Context) string {
return "" 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 { func (p *Package) Apply() error {
if p.Version == "latest" { if p.Version == "latest" {
p.Version = "" p.Version = ""
@ -292,10 +324,11 @@ func NewApkDeleteCommand() *Command {
func NewAptCreateCommand() *Command { func NewAptCreateCommand() *Command {
c := NewCommand() c := NewCommand()
c.Path = "apt-get" c.Path = "apt-get"
c.Split = false
c.Args = []CommandArg{ c.Args = []CommandArg{
CommandArg("satisfy"), CommandArg("satisfy"),
CommandArg("-y"), CommandArg("-y"),
CommandArg("{{ .Name }} ({{ .Required }})"), CommandArg("{{ .Name }} ({{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }})"),
} }
return c return c
} }

View File

@ -58,17 +58,21 @@ func NewResource(uri string) Resource {
return nil return nil
} }
func StorageMachine() machine.Stater { func StorageMachine(sub machine.Subscriber) machine.Stater {
// start_destroy -> absent -> start_create -> present -> start_destroy // start_destroy -> absent -> start_create -> present -> start_destroy
stater := machine.New("absent") stater := machine.New("absent")
stater.AddStates("absent", "start_create", "present", "start_delete", "start_read", "start_update") stater.AddStates("absent", "start_create", "present", "start_delete", "start_read", "start_update")
stater.AddTransition("create", "absent", "start_create") stater.AddTransition("create", "absent", "start_create")
stater.AddSubscription("create", sub)
stater.AddTransition("created", "start_create", "present") stater.AddTransition("created", "start_create", "present")
stater.AddTransition("read", "*", "start_read") stater.AddTransition("read", "*", "start_read")
stater.AddSubscription("read", sub)
stater.AddTransition("state_read", "start_read", "present") stater.AddTransition("state_read", "start_read", "present")
stater.AddTransition("update", "*", "start_update") stater.AddTransition("update", "*", "start_update")
stater.AddSubscription("update", sub)
stater.AddTransition("updated", "start_update", "present") stater.AddTransition("updated", "start_update", "present")
stater.AddTransition("delete", "*", "start_delete") stater.AddTransition("delete", "*", "start_delete")
stater.AddSubscription("delete", sub)
stater.AddTransition("deleted", "start_delete", "absent") stater.AddTransition("deleted", "start_delete", "absent")
return stater return stater
} }

View File

@ -7,12 +7,12 @@
"properties": { "properties": {
"name": { "name": {
"type": "string", "type": "string",
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9+.-_]+$" "pattern": "^[a-zA-Z0-9][-a-zA-Z0-9+._]+$"
}, },
"required": { "required": {
"description": "version requirement", "description": "version requirement",
"type": "string", "type": "string",
"pattern": "^([><~=]{0,1}[-_a-zA-Z0-9+.]+|)$" "pattern": "^([><~=]{0,2}[-_a-zA-Z0-9+.]+|)$"
}, },
"version": { "version": {
"type": "string" "type": "string"

View File

@ -28,6 +28,7 @@ const (
) )
type User struct { type User struct {
stater machine.Stater `json:"-" yaml:"-"`
Name string `json:"name" yaml:"name"` Name string `json:"name" yaml:"name"`
UID string `json:"uid,omitempty" yaml:"uid,omitempty"` UID string `json:"uid,omitempty" yaml:"uid,omitempty"`
Group string `json:"group,omitempty" yaml:"group,omitempty"` Group string `json:"group,omitempty" yaml:"group,omitempty"`
@ -83,7 +84,26 @@ func (u *User) Clone() Resource {
} }
func (u *User) StateMachine() machine.Stater { 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 { func (u *User) SetURI(uri string) error {

BIN
md-images/jx-diff.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB