diff --git a/Makefile b/Makefile index c26dd25..f52e968 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ IMAGE?=fedora:latest LDFLAGS?=--ldflags '-extldflags "-static"' --ldflags="-X 'main.commit=$(shell git rev-parse HEAD)' -X 'main.version=$(shell git describe --tags)' -X 'main.date=$(shell date '+%Y-%m-%d %T.%s%z')'" -export CGO_ENABLED=1 +export CGO_ENABLED=0 VERSION?=$(shell git describe --tags | sed -e 's/^v//' -e 's/-/_/g') .PHONY=jx-cli @@ -26,7 +26,7 @@ ubuntu-deps: deb: ubuntu-deps : run: - docker run -it -v $(HOME)/.git-credentials:/root/.git-credentials -v $(HOME)/.gitconfig:/root/.gitconfig -v $(shell pwd):/src $(IMAGE) bash + docker run -it -v $(HOME)/.git-credentials:/root/.git-credentials -v $(HOME)/.gitconfig:/root/.gitconfig -v $(shell pwd):/src $(IMAGE) sh clean: go clean -modcache rm jx diff --git a/examples/golang.jx.yaml b/examples/golang.jx.yaml new file mode 100644 index 0000000..138e1fe --- /dev/null +++ b/examples/golang.jx.yaml @@ -0,0 +1,6 @@ +resources: +- type: file + transition: create + attributes: + path: go1.22.5.linux-amd64.tar.gz + sourceref: https://go.dev/dl/go1.22.5.linux-amd64.tar.gz diff --git a/go.mod b/go.mod index cf931ca..8c43dc0 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module decl go 1.22.5 require ( - gitea.rosskeen.house/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3 +// gitea.rosskeen.house/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3 gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520193117-1835255b6d02 github.com/docker/docker v27.0.3+incompatible github.com/docker/go-connections v0.5.0 diff --git a/internal/command/command.go b/internal/command/command.go index 461e69b..0f98e78 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -39,6 +39,13 @@ type Command struct { func NewCommand() *Command { c := &Command{ Split: true, FailOnError: true } + c.Defaults() + return c +} + +func (c *Command) Defaults() { + c.Split = true + c.FailOnError = true c.CommandExists = func() error { if _, err := exec.LookPath(c.Path); err != nil { return fmt.Errorf("%w - %w", ErrUnknownCommand, err) @@ -82,7 +89,6 @@ func NewCommand() *Command { } return stdOutOutput, waitErr } - return c } func (c *Command) Load(r io.Reader) error { diff --git a/internal/config/file.go b/internal/config/file.go index 7eea628..cb296d3 100644 --- a/internal/config/file.go +++ b/internal/config/file.go @@ -33,7 +33,7 @@ func NewConfigFile() *ConfigFile { func NewConfigFileFromURI(u *url.URL) *ConfigFile { t := NewConfigFile() if u.Scheme == "file" { - t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI())) + t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.Path)) } else { t.Path = filepath.Join(u.Hostname(), u.Path) } diff --git a/internal/config/fs.go b/internal/config/fs.go index d1c25c7..ecb6dd3 100644 --- a/internal/config/fs.go +++ b/internal/config/fs.go @@ -35,7 +35,7 @@ func init() { ConfigSourceTypes.Register([]string{"fs"}, func(u *url.URL) ConfigSource { t := NewConfigFS(nil) - t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI())) + t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.Path)) t.fsys = os.DirFS(t.Path) return t }) diff --git a/internal/resource/container_image.go b/internal/resource/container_image.go index f945ad2..a7ed2d3 100644 --- a/internal/resource/container_image.go +++ b/internal/resource/container_image.go @@ -26,6 +26,7 @@ type ContainerImageClient interface { ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error) ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) ImageRemove(ctx context.Context, imageID string, options image.RemoveOptions) ([]image.DeleteResponse, error) + ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) Close() error } @@ -40,7 +41,10 @@ type ContainerImage struct { Size int64 `json:"size" yaml:"size"` Author string `json:"author,omitempty" yaml:"author,omitempty"` Comment string `json:"comment,omitempty" yaml:"comment,omitempty"` - State string `yaml:"state,omitempty" json:"state,omitempty"` + Dockerfile string `json:"dockerfile,omitempty" yaml:"dockerfile,omitempty"` + ContextRef ResourceReference `json:"contextref,omitempty" yaml:"contextref,omitempty"` + InjectJX bool `json:"injectjx,omitempty" yaml:"injectjx,omitempty"` + State string `yaml:"state,omitempty" json:"state,omitempty"` config ConfigurationValueGetter apiClient ContainerImageClient @@ -67,6 +71,7 @@ func NewContainerImage(containerClientApi ContainerImageClient) *ContainerImage } return &ContainerImage{ apiClient: apiClient, + InjectJX: true, } } @@ -85,6 +90,7 @@ func (c *ContainerImage) Clone() Resource { Size: c.Size, Author: c.Author, Comment: c.Comment, + InjectJX: c.InjectJX, State: c.State, apiClient: c.apiClient, } @@ -196,6 +202,17 @@ func (c *ContainerImage) Notify(m *machine.EventMessage) { c.State = "absent" panic(createErr) } + case "start_update": + if createErr := c.Update(ctx); createErr == nil { + if triggerErr := c.stater.Trigger("updated"); triggerErr == nil { + return + } else { + c.State = "absent" + } + } else { + c.State = "absent" + panic(createErr) + } case "start_delete": if deleteErr := c.Delete(ctx); deleteErr == nil { if triggerErr := c.StateMachine().Trigger("deleted"); triggerErr == nil { @@ -237,9 +254,37 @@ func (c *ContainerImage) LoadDecl(yamlResourceDeclaration string) error { } func (c *ContainerImage) Create(ctx context.Context) error { + buildOptions := types.ImageBuildOptions{ + Dockerfile: c.Dockerfile, + Tags: []string{c.Name}, + } + + if c.ContextRef.Exists() { + if c.ContextRef.ContentType() == "tar" { + ref := c.ContextRef.Lookup(c.Resources) + reader, readerErr := ref.ContentReaderStream() + if readerErr != nil { + return readerErr + } + + buildResponse, buildErr := c.apiClient.ImageBuild(ctx, reader, buildOptions) + if buildErr != nil { + return buildErr + } + defer buildResponse.Body.Close() + if _, outputErr := io.ReadAll(buildResponse.Body); outputErr != nil { + return fmt.Errorf("%w %s %s", outputErr, c.Type(), c.Name) + } + } + } + return nil } +func (c *ContainerImage) Update(ctx context.Context) error { + return c.Create(ctx) +} + func (c *ContainerImage) Pull(ctx context.Context) error { out, err := c.apiClient.ImagePull(ctx, c.Name, image.PullOptions{}) slog.Info("ContainerImage.Pull()", "name", c.Name, "error", err) @@ -345,3 +390,5 @@ func (c *ContainerImage) ResolveId(ctx context.Context) string { } return c.Id } + + diff --git a/internal/resource/container_image_test.go b/internal/resource/container_image_test.go index 6333cf0..c102c02 100644 --- a/internal/resource/container_image_test.go +++ b/internal/resource/container_image_test.go @@ -6,7 +6,7 @@ import ( "context" "decl/tests/mocks" _ "encoding/json" -_ "fmt" + "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/image" "github.com/stretchr/testify/assert" @@ -96,33 +96,24 @@ func TestReadContainerImage(t *testing.T) { assert.Greater(t, len(resourceYaml), 0) } -/* func TestCreateContainerImage(t *testing.T) { m := &mocks.MockContainerClient{ - InjectContainerCreate: func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) { - return container.CreateResponse{ID: "abcdef012", Warnings: []string{}}, nil - }, - InjectContainerRemove: func(context.Context, string, container.RemoveOptions) error { - return nil + InjectImageBuild: func(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { + return types.ImageBuildResponse{Body: io.NopCloser(strings.NewReader("image built")) }, nil }, } - decl := ` - name: "testcontainer" + decl := fmt.Sprintf(` + name: "testcontainerimage" image: "alpine" - state: present -` + contextref: file://%s +`, "") c := NewContainerImage(m) + stater := c.StateMachine() + e := c.LoadDecl(decl) assert.Equal(t, nil, e) - assert.Equal(t, "testcontainer", c.Name) + assert.Equal(t, "testcontainerimage", c.Name) - applyErr := c.Apply() - assert.Equal(t, nil, applyErr) - - c.State = "absent" - - applyDeleteErr := c.Apply() - assert.Equal(t, nil, applyDeleteErr) + assert.Nil(t, stater.Trigger("create")) } -*/ diff --git a/internal/resource/declaration.go b/internal/resource/declaration.go index 33c8890..a86689c 100644 --- a/internal/resource/declaration.go +++ b/internal/resource/declaration.go @@ -11,7 +11,7 @@ _ "errors" "gopkg.in/yaml.v3" "log/slog" _ "gitea.rosskeen.house/rosskeen.house/machine" - "gitea.rosskeen.house/pylon/luaruntime" +//_ "gitea.rosskeen.house/pylon/luaruntime" "decl/internal/codec" "decl/internal/config" ) @@ -29,7 +29,7 @@ type Declaration struct { Transition string `json:"transition,omitempty" yaml:"transition,omitempty"` Attributes Resource `json:"attributes" yaml:"attributes"` Config ConfigName `json:"config,omitempty" yaml:"config,omitempty"` - runtime luaruntime.LuaRunner +// runtime luaruntime.LuaRunner document *Document configBlock *config.Block } @@ -76,7 +76,7 @@ func (d *Declaration) Clone() *Declaration { Type: d.Type, Transition: d.Transition, Attributes: d.Attributes.Clone(), - runtime: luaruntime.New(), + //runtime: luaruntime.New(), Config: d.Config, } } @@ -89,6 +89,19 @@ func (d *Declaration) LoadDecl(yamlResourceDeclaration string) error { return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(d) } +func (d *Declaration) JSON() ([]byte, error) { + return json.Marshal(d) +} + +func (d *Declaration) Validate() (err error) { + var declarationJson []byte + if declarationJson, err = d.JSON(); err == nil { + s := NewSchema(fmt.Sprintf("%s-declaration", d.Type)) + err = s.Validate(string(declarationJson)) + } + return err +} + func (d *Declaration) NewResource() error { uri := fmt.Sprintf("%s://", d.Type) newResource, err := ResourceTypes.New(uri) diff --git a/internal/resource/declaration_test.go b/internal/resource/declaration_test.go index 09e2d9f..94471fd 100644 --- a/internal/resource/declaration_test.go +++ b/internal/resource/declaration_test.go @@ -58,12 +58,12 @@ func TestNewResourceDeclarationType(t *testing.T) { `, file) resourceDeclaration := NewDeclaration() - assert.NotEqual(t, nil, resourceDeclaration) + assert.NotNil(t, resourceDeclaration) e := resourceDeclaration.LoadDecl(decl) assert.Nil(t, e) assert.Equal(t, TypeName("file"), resourceDeclaration.Type) - assert.NotEqual(t, nil, resourceDeclaration.Attributes) + assert.NotNil(t, resourceDeclaration.Attributes) } func TestDeclarationNewResource(t *testing.T) { diff --git a/internal/resource/exec.go b/internal/resource/exec.go index d7cce74..afa7e4d 100644 --- a/internal/resource/exec.go +++ b/internal/resource/exec.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "gopkg.in/yaml.v3" + "encoding/json" _ "log" "net/url" _ "os" @@ -15,19 +16,18 @@ _ "strings" "io" "gitea.rosskeen.house/rosskeen.house/machine" "decl/internal/codec" + "decl/internal/command" ) type Exec struct { stater machine.Stater `yaml:"-" json:"-"` - 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"` + Id string `yaml:"id,omitempty" json:"id,omitempty"` + CreateTemplate *command.Command `yaml:"create,omitempty" json:"create,omitempty"` + ReadTemplate *command.Command `yaml:"read,omitempty" json:"read,omitempty"` + UpdateTemplate *command.Command `yaml:"update,omitempty" json:"update,omitempty"` + DeleteTemplate *command.Command `yaml:"delete,omitempty" json:"delete,omitempty"` config ConfigurationValueGetter - // state attributes - State string `yaml:"state"` Resources ResourceMapper `yaml:"-" json:"-"` } @@ -53,14 +53,12 @@ func (x *Exec) Clone() Resource { ReadTemplate: x.ReadTemplate, UpdateTemplate: x.UpdateTemplate, DeleteTemplate: x.DeleteTemplate, - State: x.State, } } func (x *Exec) StateMachine() machine.Stater { if x.stater == nil { x.stater = ProcessMachine(x) - } return x.stater } @@ -89,8 +87,13 @@ func (x *Exec) ResolveId(ctx context.Context) string { return "" } -func (x *Exec) Validate() error { - return fmt.Errorf("failed") +func (x *Exec) Validate() (err error) { + var execJson []byte + if execJson, err = x.JSON(); err == nil { + s := NewSchema(x.Type()) + err = s.Validate(string(execJson)) + } + return err } func (x *Exec) Apply() error { @@ -108,9 +111,7 @@ func (x *Exec) Notify(m *machine.EventMessage) { return } } - x.State = "absent" case "present": - x.State = "present" } case machine.EXITSTATEEVENT: } @@ -124,12 +125,21 @@ func (x *Exec) LoadDecl(yamlResourceDeclaration string) error { return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(x) } +func (x *Exec) JSON() ([]byte, error) { + return json.Marshal(x) +} + func (x *Exec) Type() string { return "exec" } -func (x *Exec) Create(ctx context.Context) error { - return nil +func (x *Exec) Create(ctx context.Context) (err error) { + x.CreateTemplate.Defaults() + if _, err = x.CreateTemplate.Execute(x); err == nil { + + } + return err } func (x *Exec) Read(ctx context.Context) ([]byte, error) { + x.ReadTemplate.Defaults() return yaml.Marshal(x) } diff --git a/internal/resource/exec_test.go b/internal/resource/exec_test.go index bbd59b3..df12a05 100644 --- a/internal/resource/exec_test.go +++ b/internal/resource/exec_test.go @@ -1,4 +1,6 @@ // Copyright 2024 Matthew Rich . All rights reserved + + package resource import ( @@ -15,6 +17,7 @@ import ( _ "os" _ "strings" "testing" + "decl/internal/command" ) func TestNewExecResource(t *testing.T) { @@ -38,6 +41,17 @@ func TestReadExecError(t *testing.T) { } func TestCreateExec(t *testing.T) { + x := NewExec() + decl := ` + create: + path: go + args: + - install + - golang.org/x/vuln/cmd/govulncheck@latest +` + assert.Nil(t, x.LoadDecl(decl)) + assert.Equal(t, "go", x.CreateTemplate.Path) + assert.Equal(t, command.CommandArg("golang.org/x/vuln/cmd/govulncheck@latest"), x.CreateTemplate.Args[1]) } func TestExecSetURI(t *testing.T) { diff --git a/internal/resource/file.go b/internal/resource/file.go index 64de732..520e064 100644 --- a/internal/resource/file.go +++ b/internal/resource/file.go @@ -8,6 +8,7 @@ import ( "fmt" "log/slog" "gopkg.in/yaml.v3" + "encoding/json" "io" "io/fs" "net/url" @@ -38,7 +39,6 @@ const ( SocketFile FileType = "socket" ) -var ErrInvalidResourceURI error = errors.New("Invalid resource URI") var ErrInvalidFileInfo error = errors.New("Invalid FileInfo") var ErrInvalidFileMode error = errors.New("Invalid Mode") var ErrInvalidFileOwner error = errors.New("Unknown User") @@ -149,7 +149,9 @@ func (f *File) Notify(m *machine.EventMessage) { } } else { f.State = "absent" - panic(readErr) + if ! errors.Is(readErr, ErrResourceStateAbsent) { + panic(readErr) + } } case "start_create": if e := f.Create(ctx); e == nil { @@ -187,7 +189,7 @@ func (f *File) SetURI(uri string) error { resourceUri, e := url.Parse(uri) if e == nil { if resourceUri.Scheme == "file" { - f.Path = filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI()) + f.Path = filepath.Join(resourceUri.Hostname(), resourceUri.Path) if err := f.NormalizePath(); err != nil { return err } @@ -202,8 +204,17 @@ func (f *File) UseConfig(config ConfigurationValueGetter) { f.config = config } -func (f *File) Validate() error { - return fmt.Errorf("failed") +func (f *File) JSON() ([]byte, error) { + return json.Marshal(f) +} + +func (f *File) Validate() (err error) { + var fileJson []byte + if fileJson, err = f.JSON(); err == nil { + s := NewSchema(f.Type()) + err = s.Validate(string(fileJson)) + } + return err } func (f *File) Apply() error { @@ -427,7 +438,7 @@ func (f *File) Read(ctx context.Context) ([]byte, error) { statErr := f.ReadStat() if statErr != nil { - return nil, statErr + return nil, fmt.Errorf("%w - %w", ErrResourceStateAbsent, statErr) } switch f.FileType { diff --git a/internal/resource/file_test.go b/internal/resource/file_test.go index 1ec7290..b614b75 100644 --- a/internal/resource/file_test.go +++ b/internal/resource/file_test.go @@ -20,6 +20,7 @@ import ( "testing" "time" "os/user" + "io/fs" ) func TestNewFileResource(t *testing.T) { @@ -108,7 +109,7 @@ func TestReadFileError(t *testing.T) { assert.NotEqual(t, nil, f) f.Path = file _, e := f.Read(ctx) - assert.True(t, os.IsNotExist(e)) + assert.ErrorIs(t, e, fs.ErrNotExist) assert.Equal(t, "absent", f.State) } @@ -448,3 +449,47 @@ func TestFileContentRef(t *testing.T) { assert.Nil(t, stater.Trigger("delete")) assert.NoFileExists(t, file, nil) } + +func TestFilePathURI(t *testing.T) { +// file, _ := filepath.Abs(filepath.Join(TempDir, "foo.txt")) + + decl := fmt.Sprintf(` + path: "%s" + owner: "%s" + group: "%s" + mode: "0600" + content: |- + test line 1 + test line 2 +`, "", ProcessTestUserName, ProcessTestGroupName) + + f := NewFile() + e := f.LoadDecl(decl) + assert.Nil(t, e) + assert.Equal(t, "", f.Path) + assert.ErrorContains(t, f.Validate(), "path: String length must be greater than or equal to 1") +} + +func TestFileAbsent(t *testing.T) { + file, _ := filepath.Abs(filepath.Join(TempDir, "testabsentstate.txt")) + + decl := fmt.Sprintf(` + path: "%s" + owner: "%s" + group: "%s" + mode: "0600" + filetype: "regular" +`, file, ProcessTestUserName, ProcessTestGroupName) + + f := NewFile() + stater := f.StateMachine() + e := f.LoadDecl(decl) + assert.Equal(t, nil, e) + assert.Equal(t, ProcessTestUserName, f.Owner) + + err := stater.Trigger("read") + assert.Nil(t, err) + + assert.Equal(t, "absent", f.State) + +} diff --git a/internal/resource/package.go b/internal/resource/package.go index c2fea38..ca4672f 100644 --- a/internal/resource/package.go +++ b/internal/resource/package.go @@ -115,7 +115,9 @@ func (p *Package) Notify(m *machine.EventMessage) { } } else { p.State = "absent" - panic(readErr) + if ! errors.Is(readErr, ErrResourceStateAbsent) { + panic(readErr) + } } case "start_create": if e := p.Create(ctx); e == nil { @@ -124,6 +126,12 @@ func (p *Package) Notify(m *machine.EventMessage) { } } p.State = "absent" + case "start_update": + if e := p.Update(ctx); e == nil { + if triggerErr := p.StateMachine().Trigger("updated"); triggerErr == nil { + return + } + } case "start_delete": if deleteErr := p.Delete(ctx); deleteErr == nil { if triggerErr := p.StateMachine().Trigger("deleted"); triggerErr == nil { @@ -138,7 +146,7 @@ func (p *Package) Notify(m *machine.EventMessage) { } case "absent": p.State = "absent" - case "present", "created", "read": + case "present", "created", "updated", "read": p.State = "present" } case machine.EXITSTATEEVENT: @@ -187,7 +195,46 @@ func (p *Package) Validate() error { } func (p *Package) ResolveId(ctx context.Context) string { - return "" + slog.Info("Package.ResolveId()", "name", p.Name, "machine.state", p.StateMachine().CurrentState()) +/* + imageInspect, _, err := p.apiClient.ImageInspectWithRaw(ctx, p.Name) + if err != nil { + triggerResult := p.StateMachine().Trigger("notexists") + slog.Info("ContainerImage.ResolveId()", "name", p.Name, "machine.state", p.StateMachine().CurrentState(), "resource.state", p.State, "trigger.error", triggerResult) + panic(fmt.Errorf("%w: %s %s", err, p.Type(), p.Name)) + } + slog.Info("ContainerImage.ResolveId()", "name", c.Name, "machine.state", c.StateMachine().CurrentState(), "resource.state", c.State) + c.Id = imageInspect.ID + if c.Id != "" { + if triggerErr := c.StateMachine().Trigger("exists"); triggerErr != nil { + panic(fmt.Errorf("%w: %s %s", triggerErr, c.Type(), c.Name)) + } + slog.Info("ContainerImage.ResolveId() trigger created", "machine", c.StateMachine(), "state", c.StateMachine().CurrentState()) + } else { + if triggerErr := c.StateMachine().Trigger("notexists"); triggerErr != nil { + panic(fmt.Errorf("%w: %s %s", triggerErr, c.Type(), c.Name)) + } + slog.Info("ContainerImage.ResolveId()", "name", c.Name, "machine.state", c.StateMachine().CurrentState(), "resource.state", c.State) + } + return c.Id +*/ + if p.ReadCommand.Exists() { + if _, err := p.ReadCommand.Execute(p); err != nil { + if triggerResult := p.StateMachine().Trigger("notexists"); triggerResult != nil { + panic(fmt.Errorf("%w: %s %s", err, p.Type(), p.Name)) + } + } +/* + exErr := p.ReadCommand.Extractor(out, p) + if exErr != nil { + return nil, exErr + } + return yaml.Marshal(p) + } else { + return nil, ErrUnsupportedPackageType +*/ + } + return p.Name } func (p *Package) Create(ctx context.Context) error { @@ -202,6 +249,10 @@ func (p *Package) Create(ctx context.Context) error { return e } +func (p *Package) Update(ctx context.Context) error { + return p.Create(ctx) +} + func (p *Package) Delete(ctx context.Context) error { _, err := p.DeleteCommand.Execute(p) if err != nil { @@ -235,20 +286,24 @@ func (p *Package) LoadDecl(yamlResourceDeclaration string) error { func (p *Package) Type() string { return "package" } -func (p *Package) Read(ctx context.Context) ([]byte, error) { +func (p *Package) Read(ctx context.Context) (resourceYaml []byte, err error) { if p.ReadCommand.Exists() { - out, err := p.ReadCommand.Execute(p) - if err != nil { - return nil, err + var out []byte + out, err = p.ReadCommand.Execute(p) + if err == nil { + err = p.ReadCommand.Extractor(out, p) + } else { + err = fmt.Errorf("%w - %w", ErrResourceStateAbsent, err) } - exErr := p.ReadCommand.Extractor(out, p) - if exErr != nil { - return nil, exErr - } - return yaml.Marshal(p) } else { - return nil, ErrUnsupportedPackageType + err = ErrUnsupportedPackageType } + var yamlErr error + resourceYaml, yamlErr = yaml.Marshal(p) + if err == nil { + err = yamlErr + } + return } func (p *Package) UnmarshalJSON(data []byte) error { diff --git a/internal/resource/package_test.go b/internal/resource/package_test.go index fbd4bde..ac47243 100644 --- a/internal/resource/package_test.go +++ b/internal/resource/package_test.go @@ -4,17 +4,17 @@ package resource import ( "context" - _ "encoding/json" - _ "fmt" +_ "encoding/json" + "fmt" "github.com/stretchr/testify/assert" - _ "gopkg.in/yaml.v3" - _ "io" +_ "gopkg.in/yaml.v3" +_ "io" "log/slog" - _ "net/http" - _ "net/http/httptest" - _ "net/url" - _ "os" - _ "strings" +_ "net/http" +_ "net/http/httptest" +_ "net/url" +_ "os" +_ "strings" "testing" "decl/internal/command" ) @@ -95,6 +95,34 @@ Version: 1.2.2 } func TestReadPackageError(t *testing.T) { + ctx := context.Background() + expected := fmt.Sprintf(` +name: missing +state: absent +type: %s +`, SystemPackageType) + + decl := ` +name: missing +type: apt +` + p := NewPackage() + assert.NotNil(t, p) + loadErr := p.LoadDecl(decl) + assert.Nil(t, loadErr) + p.ReadCommand = NewAptReadCommand() +/* + p.ReadCommand.Executor = func(value any) ([]byte, error) { + return []byte(``), fmt.Errorf("exit status 1 dpkg-query: package 'makef' is not installed and no information is available\nUse dpkg --info (= dpkg-deb --info) to examine archive files.\n") + } +*/ + p.ResolveId(ctx) + yaml, readErr := p.Read(ctx) + assert.ErrorIs(t, readErr, ErrResourceStateAbsent) + assert.YAMLEq(t, expected, string(yaml)) + slog.Info("read()", "yaml", yaml) + assert.Equal(t, "", p.Version) + assert.Nil(t, p.Validate()) } func TestCreatePackage(t *testing.T) { diff --git a/internal/resource/pki.go b/internal/resource/pki.go index 999c978..c0b467d 100644 --- a/internal/resource/pki.go +++ b/internal/resource/pki.go @@ -161,6 +161,9 @@ func (k *PKI) Notify(m *machine.EventMessage) { func (k *PKI) URI() string { u := k.PrivateKeyRef.Parse() + if u.Scheme == "file" || u.Scheme == "pki" { + return fmt.Sprintf("pki://%s", filepath.Join(u.Hostname(), u.Path)) + } return fmt.Sprintf("pki://%s", filepath.Join(u.Hostname(), u.RequestURI())) } @@ -168,7 +171,7 @@ func (k *PKI) SetURI(uri string) error { resourceUri, e := url.Parse(uri) if e == nil { if resourceUri.Scheme == "pki" { - k.PrivateKeyRef = ResourceReference(fmt.Sprintf("pki://%s", filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI()))) + k.PrivateKeyRef = ResourceReference(fmt.Sprintf("pki://%s", filepath.Join(resourceUri.Hostname(), resourceUri.Path))) } else { e = fmt.Errorf("%w: %s is not a cert", ErrInvalidResourceURI, uri) } diff --git a/internal/resource/resource.go b/internal/resource/resource.go index b12cdfb..61e5e38 100644 --- a/internal/resource/resource.go +++ b/internal/resource/resource.go @@ -12,6 +12,13 @@ import ( "gitea.rosskeen.house/rosskeen.house/machine" "decl/internal/transport" "log/slog" + "errors" +) + + +var ( + ErrInvalidResourceURI error = errors.New("Invalid resource URI") + ErrResourceStateAbsent = errors.New("Resource state absent") ) type ResourceReference string diff --git a/internal/resource/schemas/command.schema.json b/internal/resource/schemas/command.schema.json new file mode 100644 index 0000000..d51b067 --- /dev/null +++ b/internal/resource/schemas/command.schema.json @@ -0,0 +1,31 @@ +{ + "$id": "command.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "command", + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "command path", + "minLength": 1 + }, + "args": { + "type": "array", + "description": "list of command args", + "items": { + "type": "string" + } + }, + "split": { + "type": "boolean", + "description": "split command line args by space" + }, + "failonerror": { + "type": "boolean", + "description": "Generate an error if the command fails", + "items": { + "type": "string" + } + } + } +} diff --git a/internal/resource/schemas/container-declaration.schema.json b/internal/resource/schemas/container-declaration.schema.json index 5279188..230b224 100644 --- a/internal/resource/schemas/container-declaration.schema.json +++ b/internal/resource/schemas/container-declaration.schema.json @@ -1,7 +1,7 @@ { "$id": "container-declaration.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", - "title": "declaration", + "title": "container-declaration", "type": "object", "required": [ "type", "attributes" ], "properties": { diff --git a/internal/resource/schemas/container-image-declaration.schema.json b/internal/resource/schemas/container-image-declaration.schema.json index 3e5262e..04932b6 100644 --- a/internal/resource/schemas/container-image-declaration.schema.json +++ b/internal/resource/schemas/container-image-declaration.schema.json @@ -1,7 +1,7 @@ { "$id": "container-image-declaration.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", - "title": "declaration", + "title": "container-image-declaration", "type": "object", "required": [ "type", "attributes" ], "properties": { diff --git a/internal/resource/schemas/container-image.schema.json b/internal/resource/schemas/container-image.schema.json index 7e78a89..9c1eab4 100644 --- a/internal/resource/schemas/container-image.schema.json +++ b/internal/resource/schemas/container-image.schema.json @@ -9,6 +9,12 @@ "name": { "type": "string", "pattern": "^(?:[-0-9A-Za-z_.]+((?::[0-9]+|)(?:/[-a-z0-9._]+/[-a-z0-9._]+))|)(?:/|)(?:[-a-z0-9._]+(?:/[-a-z0-9._]+|))(:(?:[-0-9A-Za-z_.]{1,127})|)$" + }, + "Contextref": { + "type": "string" + }, + "Injectjx": { + "type": "boolean" } } } diff --git a/internal/resource/schemas/container-network-declaration.schema.json b/internal/resource/schemas/container-network-declaration.schema.json index 532fa49..b842a92 100644 --- a/internal/resource/schemas/container-network-declaration.schema.json +++ b/internal/resource/schemas/container-network-declaration.schema.json @@ -1,7 +1,7 @@ { "$id": "container-network-declaration.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", - "title": "declaration", + "title": "container-network-declaration", "type": "object", "required": [ "type", "attributes" ], "properties": { diff --git a/internal/resource/schemas/exec.schema.json b/internal/resource/schemas/exec.schema.json index 7d297ab..217fa65 100644 --- a/internal/resource/schemas/exec.schema.json +++ b/internal/resource/schemas/exec.schema.json @@ -3,19 +3,22 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "exec", "type": "object", - "required": [ "create", "read" ], "properties": { + "Id": { + "type": "string" + }, "create": { - "type": "string" + "$ref": "command.schema.json" }, - "read": { - "type": "string" + "Read": { + "$ref": "command.schema.json" }, - "update": { - "type": "string" + "Update": { + "$ref": "command.schema.json" }, - "delete": { - "type": "string" + "Delete": { + "$ref": "command.schema.json" } - } + }, + "required": [ "create" ] } diff --git a/internal/resource/schemas/group.schema.json b/internal/resource/schemas/group.schema.json index 272cf80..e55c5ef 100644 --- a/internal/resource/schemas/group.schema.json +++ b/internal/resource/schemas/group.schema.json @@ -1,7 +1,7 @@ { "$id": "group.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", - "title": "group", + "title": "group-declaration", "description": "A group account", "type": "object", "required": [ "name" ], diff --git a/internal/resource/schemas/pki-declaration.schema.json b/internal/resource/schemas/pki-declaration.schema.json index d00603e..8c590cf 100644 --- a/internal/resource/schemas/pki-declaration.schema.json +++ b/internal/resource/schemas/pki-declaration.schema.json @@ -1,7 +1,7 @@ { "$id": "pki-declaration.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", - "title": "declaration", + "title": "pki-declaration", "type": "object", "required": [ "type", "attributes" ], "properties": { diff --git a/internal/resource/schemas/storagetransition.schema.json b/internal/resource/schemas/storagetransition.schema.json index 79fe4fb..d002cc6 100644 --- a/internal/resource/schemas/storagetransition.schema.json +++ b/internal/resource/schemas/storagetransition.schema.json @@ -5,6 +5,5 @@ "type": "string", "description": "Storage state transition", "enum": [ "absent", "present", "create", "read", "update", "delete" ] - } } diff --git a/internal/resource/schemas/user-declaration.schema.json b/internal/resource/schemas/user-declaration.schema.json index 40e0a12..906c0a4 100644 --- a/internal/resource/schemas/user-declaration.schema.json +++ b/internal/resource/schemas/user-declaration.schema.json @@ -1,7 +1,7 @@ { "$id": "user-declaration.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", - "title": "declaration", + "title": "user-declaration", "type": "object", "required": [ "type", "attributes" ], "properties": { diff --git a/internal/source/decl.go b/internal/source/decl.go index cec349c..c55b723 100644 --- a/internal/source/decl.go +++ b/internal/source/decl.go @@ -32,7 +32,7 @@ func NewDeclFile() *DeclFile { func init() { SourceTypes.Register([]string{"decl"}, func(u *url.URL) DocSource { t := NewDeclFile() - t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI())) + t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.Path)) t.transport,_ = transport.NewReader(u) return t }) @@ -40,7 +40,7 @@ func init() { SourceTypes.Register([]string{"yaml","yml","yaml.gz","yml.gz"}, func(u *url.URL) DocSource { t := NewDeclFile() if u.Scheme == "file" { - fileAbsolutePath, _ := filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI())) + fileAbsolutePath, _ := filepath.Abs(filepath.Join(u.Hostname(), u.Path)) t.Path = fileAbsolutePath } else { t.Path = filepath.Join(u.Hostname(), u.Path) diff --git a/internal/source/dir.go b/internal/source/dir.go index 2810a02..1dd6a29 100644 --- a/internal/source/dir.go +++ b/internal/source/dir.go @@ -28,7 +28,7 @@ func NewDir() *Dir { func init() { SourceTypes.Register([]string{"file"}, func(u *url.URL) DocSource { t := NewDir() - t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI())) + t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.Path)) return t }) diff --git a/internal/source/tar.go b/internal/source/tar.go index 35afff7..f69d4cd 100644 --- a/internal/source/tar.go +++ b/internal/source/tar.go @@ -38,7 +38,7 @@ func init() { SourceTypes.Register([]string{"tar.gz", "tgz"}, func(u *url.URL) DocSource { t := NewTar() if u.Scheme == "file" { - fileAbsolutePath, _ := filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI())) + fileAbsolutePath, _ := filepath.Abs(filepath.Join(u.Hostname(), u.Path)) t.Path = fileAbsolutePath } else { t.Path = filepath.Join(u.Hostname(), u.Path) diff --git a/internal/target/decl.go b/internal/target/decl.go index c674c2f..5e98029 100644 --- a/internal/target/decl.go +++ b/internal/target/decl.go @@ -40,7 +40,7 @@ func NewFileDocTarget(u *url.URL, format string, gzip bool, fileUri bool) DocTar t.Format = format t.Gzip = gzip if fileUri { - fileAbsolutePath, _ := filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI())) + fileAbsolutePath, _ := filepath.Abs(filepath.Join(u.Hostname(), u.Path)) t.Path = fileAbsolutePath } else { t.Path = filepath.Join(u.Hostname(), u.Path) diff --git a/internal/target/tar.go b/internal/target/tar.go index 86e94e0..66c1090 100644 --- a/internal/target/tar.go +++ b/internal/target/tar.go @@ -43,7 +43,7 @@ func init() { TargetTypes.Register([]string{"tar.gz", "tgz"}, func(u *url.URL) DocTarget { t := NewTar() if u.Scheme == "file" { - fileAbsolutePath, _ := filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI())) + fileAbsolutePath, _ := filepath.Abs(filepath.Join(u.Hostname(), u.Path)) t.Path = fileAbsolutePath } else { t.Path = filepath.Join(u.Hostname(), u.Path) diff --git a/internal/transport/buffer.go b/internal/transport/buffer.go deleted file mode 100644 index 63ee8bc..0000000 --- a/internal/transport/buffer.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2024 Matthew Rich . All rights reserved. - -package transport - -import ( -_ "errors" - "path/filepath" - "io" - "os" - "net/url" - "strings" - "fmt" - "compress/gzip" -) - -type Buffer struct { - uri *url.URL - path string - exttype string - fileext string - readHandle *os.File - writeHandle *os.File - gzip bool - gzipWriter io.WriteCloser - gzipReader io.ReadCloser -} - -func NewBuffer(u *url.URL) (b *Buffer, err error) { - b = &Buffer{ - uri: u, - path: filepath.Join(u.Hostname(), u.RequestURI()), - } - b.extension() - b.DetectGzip() - - if b.path == "" || b.path == "-" { - b.readHandle = os.Stdin - b.writeHandle = os.Stdout - } else { - if b.readHandle, err = os.OpenFile(b.Path(), os.O_RDWR|os.O_CREATE, 0644); err != nil { - return - } - b.writeHandle = b.readHandle - } - - if b.Gzip() { - b.gzipWriter = gzip.NewWriter(b.writeHandle) - if b.gzipReader, err = gzip.NewReader(b.readHandle); err != nil { - return - } - } - return -} - -func (b *Buffer) extension() { - elements := strings.Split(b.path, ".") - numberOfElements := len(elements) - if numberOfElements > 2 { - b.exttype = elements[numberOfElements - 2] - b.fileext = elements[numberOfElements - 1] - } - b.exttype = elements[numberOfElements - 1] -} - -func (b *Buffer) DetectGzip() { - b.gzip = (b.uri.Query().Get("gzip") == "true" || b.fileext == "gz") -} - -func (b *Buffer) URI() *url.URL { - return b.uri -} - -func (b *Buffer) Path() string { - return b.path -} - -func (b *Buffer) Signature() (documentSignature string) { - if signatureResp, signatureErr := os.Open(fmt.Sprintf("%s.sig", b.uri.String())); signatureErr == nil { - defer signatureResp.Close() - readSignatureBody, readSignatureErr := io.ReadAll(signatureResp) - if readSignatureErr == nil { - documentSignature = string(readSignatureBody) - } else { - panic(readSignatureErr) - } - } else { - panic(signatureErr) - } - return documentSignature -} - -func (b *Buffer) ContentType() string { - return b.exttype -} - -func (b *Buffer) SetGzip(gzip bool) { - b.gzip = gzip -} - -func (b *Buffer) Gzip() bool { - return b.gzip -} - -func (b *Buffer) Reader() io.ReadCloser { - if b.Gzip() { - return b.gzipReader - } - return b.readHandle -} - -func (b *Buffer) Writer() io.WriteCloser { - if b.Gzip() { - return b.gzipWriter - } - return b.writeHandle -} diff --git a/internal/transport/buffer_test.go b/internal/transport/buffer_test.go deleted file mode 100644 index bb1462f..0000000 --- a/internal/transport/buffer_test.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2024 Matthew Rich . All rights reserved. - -package transport - -import ( - "github.com/stretchr/testify/assert" - "testing" - "fmt" - "os" - "net/url" - "path/filepath" -) - -var TransportBufferTestFile = fmt.Sprintf("%s/foo", TempDir) - -func TestNewTransportBufferReader(t *testing.T) { - path := fmt.Sprintf("%s/foo", TempDir) - u, e := url.Parse(fmt.Sprintf("file://%s", path)) - assert.Nil(t, e) - - writeErr := os.WriteFile(path, []byte("test"), 0644) - assert.Nil(t, writeErr) - - file, err := NewBuffer(u) - assert.Nil(t, err) - assert.Equal(t, file.Path(), path) -} - -func TestNewTransportBufferReaderExtension(t *testing.T) { - u, e := url.Parse(fmt.Sprintf("file://%s.yaml", TransportBufferTestFile)) - assert.Nil(t, e) - - b := &Buffer{ - uri: u, - path: filepath.Join(u.Hostname(), u.RequestURI()), - } - b.extension() - assert.Equal(t, b.exttype, "yaml") -} diff --git a/internal/transport/file.go b/internal/transport/file.go index 0dea74a..335fcbb 100644 --- a/internal/transport/file.go +++ b/internal/transport/file.go @@ -26,7 +26,7 @@ type File struct { } func FilePath(u *url.URL) string { - return filepath.Join(u.Hostname(), u.RequestURI()) + return filepath.Join(u.Hostname(), u.Path) } func FileExists(u *url.URL) bool { diff --git a/internal/transport/file_test.go b/internal/transport/file_test.go index 4129b36..cfc8195 100644 --- a/internal/transport/file_test.go +++ b/internal/transport/file_test.go @@ -32,7 +32,7 @@ func TestNewTransportFileReaderExtension(t *testing.T) { f := &File{ uri: u, - path: filepath.Join(u.Hostname(), u.RequestURI()), + path: filepath.Join(u.Hostname(), u.Path), } f.extension() assert.Equal(t, f.exttype, "yaml") diff --git a/internal/transport/transport.go b/internal/transport/transport.go index e3e3bd3..f7bf40f 100644 --- a/internal/transport/transport.go +++ b/internal/transport/transport.go @@ -86,9 +86,10 @@ func NewWriterURI(uri string) (writer *Writer, e error) { } func ExistsURI(uri string) bool { - var u *url.URL - u, _ = url.Parse(uri) - return Exists(u) + if u, e := url.Parse(uri); e == nil { + return Exists(u) + } + return false } func Exists(u *url.URL) bool { diff --git a/internal/transport/transport_test.go b/internal/transport/transport_test.go index d54b8c7..6aa7477 100644 --- a/internal/transport/transport_test.go +++ b/internal/transport/transport_test.go @@ -65,6 +65,16 @@ func TestTransportReaderContentType(t *testing.T) { assert.Equal(t, reader.ContentType(), "yaml") } +func TestTransportReaderDir(t *testing.T) { + u, e := url.Parse(fmt.Sprintf("file://%s", TempDir)) + assert.Nil(t, e) + + reader, err := NewReader(u) + assert.ErrorContains(t, err, "is a directory") + assert.True(t, reader.Exists()) + assert.NotNil(t, reader) +} + func TestTransportWriter(t *testing.T) { path := fmt.Sprintf("%s/writefoo", TempDir) u, e := url.Parse(fmt.Sprintf("file://%s", path)) diff --git a/internal/types/types.go b/internal/types/types.go index cbca7f1..c6ef400 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -18,18 +18,19 @@ The `types` package provides a generic method of registering a type factory. var ( ErrUnknownType = errors.New("Unknown type") + ErrInvalidProduct = errors.New("Invalid product") ) //type Name[Registry any] string //`json:"type"` -type Factory[Product any] func(*url.URL) Product -type RegistryTypeMap[Product any] map[string]Factory[Product] +type Factory[Product comparable] func(*url.URL) Product +type RegistryTypeMap[Product comparable] map[string]Factory[Product] -type Types[Product any] struct { +type Types[Product comparable] struct { registry RegistryTypeMap[Product] } -func New[Product any]() *Types[Product] { +func New[Product comparable]() *Types[Product] { return &Types[Product]{registry: make(map[string]Factory[Product])} } @@ -70,7 +71,11 @@ func (t *Types[Product]) New(uri string) (result Product, err error) { } if r, ok := t.registry[u.Scheme]; ok { - return r(u), nil + if result = r(u); result != any(nil) { + return result, nil + } else { + return result, fmt.Errorf("%w: factory failed creating %s", ErrInvalidProduct, uri) + } } err = fmt.Errorf("%w: %s", ErrUnknownType, u.Scheme) return diff --git a/tests/mocks/container.go b/tests/mocks/container.go index a55408c..34a20a9 100644 --- a/tests/mocks/container.go +++ b/tests/mocks/container.go @@ -26,6 +26,7 @@ type MockContainerClient struct { InjectImagePull func(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error) InjectImageInspectWithRaw func(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) InjectImageRemove func(ctx context.Context, imageID string, options image.RemoveOptions) ([]image.DeleteResponse, error) + InjectImageBuild func(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) InjectClose func() error } @@ -41,6 +42,10 @@ func (m *MockContainerClient) ImagePull(ctx context.Context, refStr string, opti return m.InjectImagePull(ctx, refStr, options) } +func (m *MockContainerClient) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { + return m.InjectImageBuild(ctx, buildContext, options) +} + func (m *MockContainerClient) ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) { return m.InjectImageInspectWithRaw(ctx, imageID) }