Fix an issue with the package resource where a missing package would cause a fatal error
Some checks are pending
Lint / golangci-lint (push) Waiting to run
Declarative Tests / test (push) Waiting to run
Declarative Tests / build-fedora (push) Waiting to run
Declarative Tests / build-ubuntu-focal (push) Waiting to run

WIP: add support container image build using local filesytem contexts or contextes generated from resource definitions
WIP: added support for the create command in the exec resource
Fix a type matching error in `types` package use of generics
This commit is contained in:
Matthew Rich 2024-07-22 15:03:22 -07:00
parent a6426da6e1
commit bcf4e768ff
41 changed files with 405 additions and 264 deletions

View File

@ -1,6 +1,6 @@
IMAGE?=fedora:latest 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')'" 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') VERSION?=$(shell git describe --tags | sed -e 's/^v//' -e 's/-/_/g')
.PHONY=jx-cli .PHONY=jx-cli
@ -26,7 +26,7 @@ ubuntu-deps:
deb: ubuntu-deps deb: ubuntu-deps
: :
run: 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: clean:
go clean -modcache go clean -modcache
rm jx rm jx

6
examples/golang.jx.yaml Normal file
View File

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

2
go.mod
View File

@ -3,7 +3,7 @@ module decl
go 1.22.5 go 1.22.5
require ( 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 gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520193117-1835255b6d02
github.com/docker/docker v27.0.3+incompatible github.com/docker/docker v27.0.3+incompatible
github.com/docker/go-connections v0.5.0 github.com/docker/go-connections v0.5.0

View File

@ -39,6 +39,13 @@ type Command struct {
func NewCommand() *Command { func NewCommand() *Command {
c := &Command{ Split: true, FailOnError: true } c := &Command{ Split: true, FailOnError: true }
c.Defaults()
return c
}
func (c *Command) Defaults() {
c.Split = true
c.FailOnError = true
c.CommandExists = func() error { c.CommandExists = func() error {
if _, err := exec.LookPath(c.Path); err != nil { if _, err := exec.LookPath(c.Path); err != nil {
return fmt.Errorf("%w - %w", ErrUnknownCommand, err) return fmt.Errorf("%w - %w", ErrUnknownCommand, err)
@ -82,7 +89,6 @@ func NewCommand() *Command {
} }
return stdOutOutput, waitErr return stdOutOutput, waitErr
} }
return c
} }
func (c *Command) Load(r io.Reader) error { func (c *Command) Load(r io.Reader) error {

View File

@ -33,7 +33,7 @@ func NewConfigFile() *ConfigFile {
func NewConfigFileFromURI(u *url.URL) *ConfigFile { func NewConfigFileFromURI(u *url.URL) *ConfigFile {
t := NewConfigFile() t := NewConfigFile()
if u.Scheme == "file" { 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 { } else {
t.Path = filepath.Join(u.Hostname(), u.Path) t.Path = filepath.Join(u.Hostname(), u.Path)
} }

View File

@ -35,7 +35,7 @@ func init() {
ConfigSourceTypes.Register([]string{"fs"}, func(u *url.URL) ConfigSource { ConfigSourceTypes.Register([]string{"fs"}, func(u *url.URL) ConfigSource {
t := NewConfigFS(nil) 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) t.fsys = os.DirFS(t.Path)
return t return t
}) })

View File

@ -26,6 +26,7 @@ type ContainerImageClient interface {
ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error) ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error)
ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error)
ImageRemove(ctx context.Context, imageID string, options image.RemoveOptions) ([]image.DeleteResponse, 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 Close() error
} }
@ -40,6 +41,9 @@ type ContainerImage struct {
Size int64 `json:"size" yaml:"size"` Size int64 `json:"size" yaml:"size"`
Author string `json:"author,omitempty" yaml:"author,omitempty"` Author string `json:"author,omitempty" yaml:"author,omitempty"`
Comment string `json:"comment,omitempty" yaml:"comment,omitempty"` Comment string `json:"comment,omitempty" yaml:"comment,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"` State string `yaml:"state,omitempty" json:"state,omitempty"`
config ConfigurationValueGetter config ConfigurationValueGetter
@ -67,6 +71,7 @@ func NewContainerImage(containerClientApi ContainerImageClient) *ContainerImage
} }
return &ContainerImage{ return &ContainerImage{
apiClient: apiClient, apiClient: apiClient,
InjectJX: true,
} }
} }
@ -85,6 +90,7 @@ func (c *ContainerImage) Clone() Resource {
Size: c.Size, Size: c.Size,
Author: c.Author, Author: c.Author,
Comment: c.Comment, Comment: c.Comment,
InjectJX: c.InjectJX,
State: c.State, State: c.State,
apiClient: c.apiClient, apiClient: c.apiClient,
} }
@ -196,6 +202,17 @@ func (c *ContainerImage) Notify(m *machine.EventMessage) {
c.State = "absent" c.State = "absent"
panic(createErr) 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": case "start_delete":
if deleteErr := c.Delete(ctx); deleteErr == nil { if deleteErr := c.Delete(ctx); deleteErr == nil {
if triggerErr := c.StateMachine().Trigger("deleted"); triggerErr == 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 { 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 return nil
} }
func (c *ContainerImage) Update(ctx context.Context) error {
return c.Create(ctx)
}
func (c *ContainerImage) Pull(ctx context.Context) error { func (c *ContainerImage) Pull(ctx context.Context) error {
out, err := c.apiClient.ImagePull(ctx, c.Name, image.PullOptions{}) out, err := c.apiClient.ImagePull(ctx, c.Name, image.PullOptions{})
slog.Info("ContainerImage.Pull()", "name", c.Name, "error", err) slog.Info("ContainerImage.Pull()", "name", c.Name, "error", err)
@ -345,3 +390,5 @@ func (c *ContainerImage) ResolveId(ctx context.Context) string {
} }
return c.Id return c.Id
} }

View File

@ -6,7 +6,7 @@ import (
"context" "context"
"decl/tests/mocks" "decl/tests/mocks"
_ "encoding/json" _ "encoding/json"
_ "fmt" "fmt"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -96,33 +96,24 @@ func TestReadContainerImage(t *testing.T) {
assert.Greater(t, len(resourceYaml), 0) assert.Greater(t, len(resourceYaml), 0)
} }
/*
func TestCreateContainerImage(t *testing.T) { func TestCreateContainerImage(t *testing.T) {
m := &mocks.MockContainerClient{ 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) { InjectImageBuild: func(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
return container.CreateResponse{ID: "abcdef012", Warnings: []string{}}, nil return types.ImageBuildResponse{Body: io.NopCloser(strings.NewReader("image built")) }, nil
},
InjectContainerRemove: func(context.Context, string, container.RemoveOptions) error {
return nil
}, },
} }
decl := ` decl := fmt.Sprintf(`
name: "testcontainer" name: "testcontainerimage"
image: "alpine" image: "alpine"
state: present contextref: file://%s
` `, "")
c := NewContainerImage(m) c := NewContainerImage(m)
stater := c.StateMachine()
e := c.LoadDecl(decl) e := c.LoadDecl(decl)
assert.Equal(t, nil, e) assert.Equal(t, nil, e)
assert.Equal(t, "testcontainer", c.Name) assert.Equal(t, "testcontainerimage", c.Name)
applyErr := c.Apply() assert.Nil(t, stater.Trigger("create"))
assert.Equal(t, nil, applyErr)
c.State = "absent"
applyDeleteErr := c.Apply()
assert.Equal(t, nil, applyDeleteErr)
} }
*/

View File

@ -11,7 +11,7 @@ _ "errors"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"log/slog" "log/slog"
_ "gitea.rosskeen.house/rosskeen.house/machine" _ "gitea.rosskeen.house/rosskeen.house/machine"
"gitea.rosskeen.house/pylon/luaruntime" //_ "gitea.rosskeen.house/pylon/luaruntime"
"decl/internal/codec" "decl/internal/codec"
"decl/internal/config" "decl/internal/config"
) )
@ -29,7 +29,7 @@ type Declaration struct {
Transition string `json:"transition,omitempty" yaml:"transition,omitempty"` Transition string `json:"transition,omitempty" yaml:"transition,omitempty"`
Attributes Resource `json:"attributes" yaml:"attributes"` Attributes Resource `json:"attributes" yaml:"attributes"`
Config ConfigName `json:"config,omitempty" yaml:"config,omitempty"` Config ConfigName `json:"config,omitempty" yaml:"config,omitempty"`
runtime luaruntime.LuaRunner // runtime luaruntime.LuaRunner
document *Document document *Document
configBlock *config.Block configBlock *config.Block
} }
@ -76,7 +76,7 @@ func (d *Declaration) Clone() *Declaration {
Type: d.Type, Type: d.Type,
Transition: d.Transition, Transition: d.Transition,
Attributes: d.Attributes.Clone(), Attributes: d.Attributes.Clone(),
runtime: luaruntime.New(), //runtime: luaruntime.New(),
Config: d.Config, Config: d.Config,
} }
} }
@ -89,6 +89,19 @@ func (d *Declaration) LoadDecl(yamlResourceDeclaration string) error {
return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(d) 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 { func (d *Declaration) NewResource() error {
uri := fmt.Sprintf("%s://", d.Type) uri := fmt.Sprintf("%s://", d.Type)
newResource, err := ResourceTypes.New(uri) newResource, err := ResourceTypes.New(uri)

View File

@ -58,12 +58,12 @@ func TestNewResourceDeclarationType(t *testing.T) {
`, file) `, file)
resourceDeclaration := NewDeclaration() resourceDeclaration := NewDeclaration()
assert.NotEqual(t, nil, resourceDeclaration) assert.NotNil(t, resourceDeclaration)
e := resourceDeclaration.LoadDecl(decl) e := resourceDeclaration.LoadDecl(decl)
assert.Nil(t, e) assert.Nil(t, e)
assert.Equal(t, TypeName("file"), resourceDeclaration.Type) assert.Equal(t, TypeName("file"), resourceDeclaration.Type)
assert.NotEqual(t, nil, resourceDeclaration.Attributes) assert.NotNil(t, resourceDeclaration.Attributes)
} }
func TestDeclarationNewResource(t *testing.T) { func TestDeclarationNewResource(t *testing.T) {

View File

@ -6,6 +6,7 @@ import (
"context" "context"
"fmt" "fmt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"encoding/json"
_ "log" _ "log"
"net/url" "net/url"
_ "os" _ "os"
@ -15,19 +16,18 @@ _ "strings"
"io" "io"
"gitea.rosskeen.house/rosskeen.house/machine" "gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec" "decl/internal/codec"
"decl/internal/command"
) )
type Exec struct { type Exec struct {
stater machine.Stater `yaml:"-" json:"-"` stater machine.Stater `yaml:"-" json:"-"`
Id string `yaml:"id" json:"id"` Id string `yaml:"id,omitempty" json:"id,omitempty"`
CreateTemplate Command `yaml:"create" json:"create"` CreateTemplate *command.Command `yaml:"create,omitempty" json:"create,omitempty"`
ReadTemplate Command `yaml:"read" json:"read"` ReadTemplate *command.Command `yaml:"read,omitempty" json:"read,omitempty"`
UpdateTemplate Command `yaml:"update" json:"update"` UpdateTemplate *command.Command `yaml:"update,omitempty" json:"update,omitempty"`
DeleteTemplate Command `yaml:"delete" json:"delete"` DeleteTemplate *command.Command `yaml:"delete,omitempty" json:"delete,omitempty"`
config ConfigurationValueGetter config ConfigurationValueGetter
// state attributes
State string `yaml:"state"`
Resources ResourceMapper `yaml:"-" json:"-"` Resources ResourceMapper `yaml:"-" json:"-"`
} }
@ -53,14 +53,12 @@ func (x *Exec) Clone() Resource {
ReadTemplate: x.ReadTemplate, ReadTemplate: x.ReadTemplate,
UpdateTemplate: x.UpdateTemplate, UpdateTemplate: x.UpdateTemplate,
DeleteTemplate: x.DeleteTemplate, DeleteTemplate: x.DeleteTemplate,
State: x.State,
} }
} }
func (x *Exec) StateMachine() machine.Stater { func (x *Exec) StateMachine() machine.Stater {
if x.stater == nil { if x.stater == nil {
x.stater = ProcessMachine(x) x.stater = ProcessMachine(x)
} }
return x.stater return x.stater
} }
@ -89,8 +87,13 @@ func (x *Exec) ResolveId(ctx context.Context) string {
return "" return ""
} }
func (x *Exec) Validate() error { func (x *Exec) Validate() (err error) {
return fmt.Errorf("failed") 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 { func (x *Exec) Apply() error {
@ -108,9 +111,7 @@ func (x *Exec) Notify(m *machine.EventMessage) {
return return
} }
} }
x.State = "absent"
case "present": case "present":
x.State = "present"
} }
case machine.EXITSTATEEVENT: case machine.EXITSTATEEVENT:
} }
@ -124,12 +125,21 @@ func (x *Exec) LoadDecl(yamlResourceDeclaration string) error {
return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(x) 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) Type() string { return "exec" }
func (x *Exec) Create(ctx context.Context) error { func (x *Exec) Create(ctx context.Context) (err error) {
return nil x.CreateTemplate.Defaults()
if _, err = x.CreateTemplate.Execute(x); err == nil {
}
return err
} }
func (x *Exec) Read(ctx context.Context) ([]byte, error) { func (x *Exec) Read(ctx context.Context) ([]byte, error) {
x.ReadTemplate.Defaults()
return yaml.Marshal(x) return yaml.Marshal(x)
} }

View File

@ -1,4 +1,6 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved // Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved
package resource package resource
import ( import (
@ -15,6 +17,7 @@ import (
_ "os" _ "os"
_ "strings" _ "strings"
"testing" "testing"
"decl/internal/command"
) )
func TestNewExecResource(t *testing.T) { func TestNewExecResource(t *testing.T) {
@ -38,6 +41,17 @@ func TestReadExecError(t *testing.T) {
} }
func TestCreateExec(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) { func TestExecSetURI(t *testing.T) {

View File

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"encoding/json"
"io" "io"
"io/fs" "io/fs"
"net/url" "net/url"
@ -38,7 +39,6 @@ const (
SocketFile FileType = "socket" SocketFile FileType = "socket"
) )
var ErrInvalidResourceURI error = errors.New("Invalid resource URI")
var ErrInvalidFileInfo error = errors.New("Invalid FileInfo") var ErrInvalidFileInfo error = errors.New("Invalid FileInfo")
var ErrInvalidFileMode error = errors.New("Invalid Mode") var ErrInvalidFileMode error = errors.New("Invalid Mode")
var ErrInvalidFileOwner error = errors.New("Unknown User") var ErrInvalidFileOwner error = errors.New("Unknown User")
@ -149,8 +149,10 @@ func (f *File) Notify(m *machine.EventMessage) {
} }
} else { } else {
f.State = "absent" f.State = "absent"
if ! errors.Is(readErr, ErrResourceStateAbsent) {
panic(readErr) panic(readErr)
} }
}
case "start_create": case "start_create":
if e := f.Create(ctx); e == nil { if e := f.Create(ctx); e == nil {
if triggerErr := f.StateMachine().Trigger("created"); triggerErr == nil { if triggerErr := f.StateMachine().Trigger("created"); triggerErr == nil {
@ -187,7 +189,7 @@ func (f *File) SetURI(uri string) error {
resourceUri, e := url.Parse(uri) resourceUri, e := url.Parse(uri)
if e == nil { if e == nil {
if resourceUri.Scheme == "file" { 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 { if err := f.NormalizePath(); err != nil {
return err return err
} }
@ -202,8 +204,17 @@ func (f *File) UseConfig(config ConfigurationValueGetter) {
f.config = config f.config = config
} }
func (f *File) Validate() error { func (f *File) JSON() ([]byte, error) {
return fmt.Errorf("failed") 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 { func (f *File) Apply() error {
@ -427,7 +438,7 @@ func (f *File) Read(ctx context.Context) ([]byte, error) {
statErr := f.ReadStat() statErr := f.ReadStat()
if statErr != nil { if statErr != nil {
return nil, statErr return nil, fmt.Errorf("%w - %w", ErrResourceStateAbsent, statErr)
} }
switch f.FileType { switch f.FileType {

View File

@ -20,6 +20,7 @@ import (
"testing" "testing"
"time" "time"
"os/user" "os/user"
"io/fs"
) )
func TestNewFileResource(t *testing.T) { func TestNewFileResource(t *testing.T) {
@ -108,7 +109,7 @@ func TestReadFileError(t *testing.T) {
assert.NotEqual(t, nil, f) assert.NotEqual(t, nil, f)
f.Path = file f.Path = file
_, e := f.Read(ctx) _, e := f.Read(ctx)
assert.True(t, os.IsNotExist(e)) assert.ErrorIs(t, e, fs.ErrNotExist)
assert.Equal(t, "absent", f.State) assert.Equal(t, "absent", f.State)
} }
@ -448,3 +449,47 @@ func TestFileContentRef(t *testing.T) {
assert.Nil(t, stater.Trigger("delete")) assert.Nil(t, stater.Trigger("delete"))
assert.NoFileExists(t, file, nil) 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)
}

View File

@ -115,8 +115,10 @@ func (p *Package) Notify(m *machine.EventMessage) {
} }
} else { } else {
p.State = "absent" p.State = "absent"
if ! errors.Is(readErr, ErrResourceStateAbsent) {
panic(readErr) panic(readErr)
} }
}
case "start_create": case "start_create":
if e := p.Create(ctx); e == nil { if e := p.Create(ctx); e == nil {
if triggerErr := p.StateMachine().Trigger("created"); triggerErr == nil { if triggerErr := p.StateMachine().Trigger("created"); triggerErr == nil {
@ -124,6 +126,12 @@ func (p *Package) Notify(m *machine.EventMessage) {
} }
} }
p.State = "absent" 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": case "start_delete":
if deleteErr := p.Delete(ctx); deleteErr == nil { if deleteErr := p.Delete(ctx); deleteErr == nil {
if triggerErr := p.StateMachine().Trigger("deleted"); triggerErr == nil { if triggerErr := p.StateMachine().Trigger("deleted"); triggerErr == nil {
@ -138,7 +146,7 @@ func (p *Package) Notify(m *machine.EventMessage) {
} }
case "absent": case "absent":
p.State = "absent" p.State = "absent"
case "present", "created", "read": case "present", "created", "updated", "read":
p.State = "present" p.State = "present"
} }
case machine.EXITSTATEEVENT: case machine.EXITSTATEEVENT:
@ -187,7 +195,46 @@ func (p *Package) Validate() error {
} }
func (p *Package) ResolveId(ctx context.Context) string { 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 { func (p *Package) Create(ctx context.Context) error {
@ -202,6 +249,10 @@ func (p *Package) Create(ctx context.Context) error {
return e return e
} }
func (p *Package) Update(ctx context.Context) error {
return p.Create(ctx)
}
func (p *Package) Delete(ctx context.Context) error { func (p *Package) Delete(ctx context.Context) error {
_, err := p.DeleteCommand.Execute(p) _, err := p.DeleteCommand.Execute(p)
if err != nil { if err != nil {
@ -235,20 +286,24 @@ func (p *Package) LoadDecl(yamlResourceDeclaration string) error {
func (p *Package) Type() string { return "package" } 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() { if p.ReadCommand.Exists() {
out, err := p.ReadCommand.Execute(p) var out []byte
if err != nil { out, err = p.ReadCommand.Execute(p)
return nil, err if err == nil {
} err = p.ReadCommand.Extractor(out, p)
exErr := p.ReadCommand.Extractor(out, p)
if exErr != nil {
return nil, exErr
}
return yaml.Marshal(p)
} else { } else {
return nil, ErrUnsupportedPackageType err = fmt.Errorf("%w - %w", ErrResourceStateAbsent, err)
} }
} else {
err = ErrUnsupportedPackageType
}
var yamlErr error
resourceYaml, yamlErr = yaml.Marshal(p)
if err == nil {
err = yamlErr
}
return
} }
func (p *Package) UnmarshalJSON(data []byte) error { func (p *Package) UnmarshalJSON(data []byte) error {

View File

@ -4,17 +4,17 @@ package resource
import ( import (
"context" "context"
_ "encoding/json" _ "encoding/json"
_ "fmt" "fmt"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
_ "gopkg.in/yaml.v3" _ "gopkg.in/yaml.v3"
_ "io" _ "io"
"log/slog" "log/slog"
_ "net/http" _ "net/http"
_ "net/http/httptest" _ "net/http/httptest"
_ "net/url" _ "net/url"
_ "os" _ "os"
_ "strings" _ "strings"
"testing" "testing"
"decl/internal/command" "decl/internal/command"
) )
@ -95,6 +95,34 @@ Version: 1.2.2
} }
func TestReadPackageError(t *testing.T) { 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) { func TestCreatePackage(t *testing.T) {

View File

@ -161,6 +161,9 @@ func (k *PKI) Notify(m *machine.EventMessage) {
func (k *PKI) URI() string { func (k *PKI) URI() string {
u := k.PrivateKeyRef.Parse() 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())) 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) resourceUri, e := url.Parse(uri)
if e == nil { if e == nil {
if resourceUri.Scheme == "pki" { 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 { } else {
e = fmt.Errorf("%w: %s is not a cert", ErrInvalidResourceURI, uri) e = fmt.Errorf("%w: %s is not a cert", ErrInvalidResourceURI, uri)
} }

View File

@ -12,6 +12,13 @@ import (
"gitea.rosskeen.house/rosskeen.house/machine" "gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/transport" "decl/internal/transport"
"log/slog" "log/slog"
"errors"
)
var (
ErrInvalidResourceURI error = errors.New("Invalid resource URI")
ErrResourceStateAbsent = errors.New("Resource state absent")
) )
type ResourceReference string type ResourceReference string

View File

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

View File

@ -1,7 +1,7 @@
{ {
"$id": "container-declaration.schema.json", "$id": "container-declaration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "declaration", "title": "container-declaration",
"type": "object", "type": "object",
"required": [ "type", "attributes" ], "required": [ "type", "attributes" ],
"properties": { "properties": {

View File

@ -1,7 +1,7 @@
{ {
"$id": "container-image-declaration.schema.json", "$id": "container-image-declaration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "declaration", "title": "container-image-declaration",
"type": "object", "type": "object",
"required": [ "type", "attributes" ], "required": [ "type", "attributes" ],
"properties": { "properties": {

View File

@ -9,6 +9,12 @@
"name": { "name": {
"type": "string", "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})|)$" "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"
} }
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"$id": "container-network-declaration.schema.json", "$id": "container-network-declaration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "declaration", "title": "container-network-declaration",
"type": "object", "type": "object",
"required": [ "type", "attributes" ], "required": [ "type", "attributes" ],
"properties": { "properties": {

View File

@ -3,19 +3,22 @@
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "exec", "title": "exec",
"type": "object", "type": "object",
"required": [ "create", "read" ],
"properties": { "properties": {
"Id": {
"type": "string"
},
"create": { "create": {
"type": "string" "$ref": "command.schema.json"
}, },
"read": { "Read": {
"type": "string" "$ref": "command.schema.json"
}, },
"update": { "Update": {
"type": "string" "$ref": "command.schema.json"
}, },
"delete": { "Delete": {
"type": "string" "$ref": "command.schema.json"
}
} }
},
"required": [ "create" ]
} }

View File

@ -1,7 +1,7 @@
{ {
"$id": "group.schema.json", "$id": "group.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "group", "title": "group-declaration",
"description": "A group account", "description": "A group account",
"type": "object", "type": "object",
"required": [ "name" ], "required": [ "name" ],

View File

@ -1,7 +1,7 @@
{ {
"$id": "pki-declaration.schema.json", "$id": "pki-declaration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "declaration", "title": "pki-declaration",
"type": "object", "type": "object",
"required": [ "type", "attributes" ], "required": [ "type", "attributes" ],
"properties": { "properties": {

View File

@ -5,6 +5,5 @@
"type": "string", "type": "string",
"description": "Storage state transition", "description": "Storage state transition",
"enum": [ "absent", "present", "create", "read", "update", "delete" ] "enum": [ "absent", "present", "create", "read", "update", "delete" ]
}
} }

View File

@ -1,7 +1,7 @@
{ {
"$id": "user-declaration.schema.json", "$id": "user-declaration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "declaration", "title": "user-declaration",
"type": "object", "type": "object",
"required": [ "type", "attributes" ], "required": [ "type", "attributes" ],
"properties": { "properties": {

View File

@ -32,7 +32,7 @@ func NewDeclFile() *DeclFile {
func init() { func init() {
SourceTypes.Register([]string{"decl"}, func(u *url.URL) DocSource { SourceTypes.Register([]string{"decl"}, func(u *url.URL) DocSource {
t := NewDeclFile() 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) t.transport,_ = transport.NewReader(u)
return t return t
}) })
@ -40,7 +40,7 @@ func init() {
SourceTypes.Register([]string{"yaml","yml","yaml.gz","yml.gz"}, func(u *url.URL) DocSource { SourceTypes.Register([]string{"yaml","yml","yaml.gz","yml.gz"}, func(u *url.URL) DocSource {
t := NewDeclFile() t := NewDeclFile()
if u.Scheme == "file" { 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 t.Path = fileAbsolutePath
} else { } else {
t.Path = filepath.Join(u.Hostname(), u.Path) t.Path = filepath.Join(u.Hostname(), u.Path)

View File

@ -28,7 +28,7 @@ func NewDir() *Dir {
func init() { func init() {
SourceTypes.Register([]string{"file"}, func(u *url.URL) DocSource { SourceTypes.Register([]string{"file"}, func(u *url.URL) DocSource {
t := NewDir() t := NewDir()
t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI())) t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.Path))
return t return t
}) })

View File

@ -38,7 +38,7 @@ func init() {
SourceTypes.Register([]string{"tar.gz", "tgz"}, func(u *url.URL) DocSource { SourceTypes.Register([]string{"tar.gz", "tgz"}, func(u *url.URL) DocSource {
t := NewTar() t := NewTar()
if u.Scheme == "file" { 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 t.Path = fileAbsolutePath
} else { } else {
t.Path = filepath.Join(u.Hostname(), u.Path) t.Path = filepath.Join(u.Hostname(), u.Path)

View File

@ -40,7 +40,7 @@ func NewFileDocTarget(u *url.URL, format string, gzip bool, fileUri bool) DocTar
t.Format = format t.Format = format
t.Gzip = gzip t.Gzip = gzip
if fileUri { if fileUri {
fileAbsolutePath, _ := filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI())) fileAbsolutePath, _ := filepath.Abs(filepath.Join(u.Hostname(), u.Path))
t.Path = fileAbsolutePath t.Path = fileAbsolutePath
} else { } else {
t.Path = filepath.Join(u.Hostname(), u.Path) t.Path = filepath.Join(u.Hostname(), u.Path)

View File

@ -43,7 +43,7 @@ func init() {
TargetTypes.Register([]string{"tar.gz", "tgz"}, func(u *url.URL) DocTarget { TargetTypes.Register([]string{"tar.gz", "tgz"}, func(u *url.URL) DocTarget {
t := NewTar() t := NewTar()
if u.Scheme == "file" { 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 t.Path = fileAbsolutePath
} else { } else {
t.Path = filepath.Join(u.Hostname(), u.Path) t.Path = filepath.Join(u.Hostname(), u.Path)

View File

@ -1,116 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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
}

View File

@ -1,39 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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")
}

View File

@ -26,7 +26,7 @@ type File struct {
} }
func FilePath(u *url.URL) string { 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 { func FileExists(u *url.URL) bool {

View File

@ -32,7 +32,7 @@ func TestNewTransportFileReaderExtension(t *testing.T) {
f := &File{ f := &File{
uri: u, uri: u,
path: filepath.Join(u.Hostname(), u.RequestURI()), path: filepath.Join(u.Hostname(), u.Path),
} }
f.extension() f.extension()
assert.Equal(t, f.exttype, "yaml") assert.Equal(t, f.exttype, "yaml")

View File

@ -86,9 +86,10 @@ func NewWriterURI(uri string) (writer *Writer, e error) {
} }
func ExistsURI(uri string) bool { func ExistsURI(uri string) bool {
var u *url.URL if u, e := url.Parse(uri); e == nil {
u, _ = url.Parse(uri)
return Exists(u) return Exists(u)
}
return false
} }
func Exists(u *url.URL) bool { func Exists(u *url.URL) bool {

View File

@ -65,6 +65,16 @@ func TestTransportReaderContentType(t *testing.T) {
assert.Equal(t, reader.ContentType(), "yaml") 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) { func TestTransportWriter(t *testing.T) {
path := fmt.Sprintf("%s/writefoo", TempDir) path := fmt.Sprintf("%s/writefoo", TempDir)
u, e := url.Parse(fmt.Sprintf("file://%s", path)) u, e := url.Parse(fmt.Sprintf("file://%s", path))

View File

@ -18,18 +18,19 @@ The `types` package provides a generic method of registering a type factory.
var ( var (
ErrUnknownType = errors.New("Unknown type") ErrUnknownType = errors.New("Unknown type")
ErrInvalidProduct = errors.New("Invalid product")
) )
//type Name[Registry any] string //`json:"type"` //type Name[Registry any] string //`json:"type"`
type Factory[Product any] func(*url.URL) Product type Factory[Product comparable] func(*url.URL) Product
type RegistryTypeMap[Product any] map[string]Factory[Product] type RegistryTypeMap[Product comparable] map[string]Factory[Product]
type Types[Product any] struct { type Types[Product comparable] struct {
registry RegistryTypeMap[Product] registry RegistryTypeMap[Product]
} }
func New[Product any]() *Types[Product] { func New[Product comparable]() *Types[Product] {
return &Types[Product]{registry: make(map[string]Factory[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 { 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) err = fmt.Errorf("%w: %s", ErrUnknownType, u.Scheme)
return return

View File

@ -26,6 +26,7 @@ type MockContainerClient struct {
InjectImagePull func(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error) InjectImagePull func(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error)
InjectImageInspectWithRaw func(ctx context.Context, imageID string) (types.ImageInspect, []byte, 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) 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 InjectClose func() error
} }
@ -41,6 +42,10 @@ func (m *MockContainerClient) ImagePull(ctx context.Context, refStr string, opti
return m.InjectImagePull(ctx, refStr, options) 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) { func (m *MockContainerClient) ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) {
return m.InjectImageInspectWithRaw(ctx, imageID) return m.InjectImageInspectWithRaw(ctx, imageID)
} }