Compare commits

...

3 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
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 {
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...)
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)))

View File

@ -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 {

View File

@ -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)
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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"

View File

@ -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 {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 6.6 MiB