diff --git a/cmd/cli/main.go b/cmd/cli/main.go index f111645..0dad515 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -36,6 +36,8 @@ var GlobalQuiet *bool var ImportMerge *bool var ImportResource *string +var ApplyDelete *bool + var ctx context.Context = context.Background() @@ -72,7 +74,7 @@ func LoggerConfig() { var programLevel = new(slog.LevelVar) logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel})) slog.SetDefault(logger) - if debugLogging,ok := os.LookupEnv("DECL_DEBUG"); ok && debugLogging != "" { + if debugLogging,ok := os.LookupEnv("JX_DEBUG"); ok && debugLogging != "" { programLevel.Set(slog.LevelDebug) } else { programLevel.Set(slog.LevelError) @@ -169,6 +171,7 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) { } func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) { + ApplyDelete = cmd.Bool("delete", false, "Delete resources defined in the available documents.") if e := cmd.Parse(os.Args[2:]); e != nil { return e } @@ -183,8 +186,14 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) { slog.Info("main.Apply()", "documents", documents) for _,d := range documents { - slog.Info("main.Appl()", "doc", d) - if e := d.Apply(); e != nil { + slog.Info("main.Apply()", "doc", d) + var overrideState string = "" + if *ApplyDelete { + overrideState = "delete" + } + d.ResolveIds(ctx) + if e := d.Apply(overrideState); e != nil { + slog.Info("main.Apply() error", "error", e) return e } @@ -305,7 +314,7 @@ func main() { } if os.Args[1] == subCmd.Name { if e := subCmd.Run(cmdFlagSet, os.Stdout); e != nil { - log.Fatal(e) + slog.Error("Failed running command", "command", os.Args[1], "error", e) } return } diff --git a/examples/fedora.jx.yaml b/examples/fedora.jx.yaml new file mode 100644 index 0000000..5481733 --- /dev/null +++ b/examples/fedora.jx.yaml @@ -0,0 +1,5 @@ +resources: +- type: container-image + transition: read + attributes: + name: "fedora:latest" diff --git a/go.mod b/go.mod index 9bf78b2..266ef8b 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ module decl -go 1.21.1 +go 1.22.1 require ( - gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240507055918-d126067c56d0 + 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 v25.0.5+incompatible github.com/docker/go-connections v0.5.0 github.com/opencontainers/image-spec v1.1.0 diff --git a/go.sum b/go.sum index 3e15f35..1e43b67 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,19 @@ -gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240507054129-2b3068fcd02c h1:XjmoAauFsu6f7Xliqvf2Gn/TYCLOq8nUcPG5n0nGeWQ= -gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240507054129-2b3068fcd02c/go.mod h1:5J2OFjFIBaCfsjcC9kSyycbIL8g/qAJH2A8BnbIig+Y= +gitea.rosskeen.house/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3 h1:ge74Hmzxp+bqVwSK9hOOBlZB9KeL3xuwMIXAYLPHBxA= +gitea.rosskeen.house/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3/go.mod h1:9sKIXsGDcf1uBnHhY29wi38Vll8dpVNUOxkXphN2KEk= gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240507055918-d126067c56d0 h1:HU+5GHr29qSxIUKWNJVwOqqf2GwG1SD+H30hYzgWykE= gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240507055918-d126067c56d0/go.mod h1:5J2OFjFIBaCfsjcC9kSyycbIL8g/qAJH2A8BnbIig+Y= +gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240517002849-51b3a21acad8 h1:IvF6TfCfKvjMBSrRfmyP+hMik8WUIb3d/H0COZvAuow= +gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240517002849-51b3a21acad8/go.mod h1:5J2OFjFIBaCfsjcC9kSyycbIL8g/qAJH2A8BnbIig+Y= +gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240517033715-36f18d6e31a0 h1:xR4yTecuNj/yMZ8QgvLkItup5DLl8Kd2nbpcV+hBNyI= +gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240517033715-36f18d6e31a0/go.mod h1:5J2OFjFIBaCfsjcC9kSyycbIL8g/qAJH2A8BnbIig+Y= +gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520165649-7690d725fba4 h1:9RqJPzVMIB22vZzvtNReV8gnuJpolilvxPNjcShd4UE= +gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520165649-7690d725fba4/go.mod h1:5J2OFjFIBaCfsjcC9kSyycbIL8g/qAJH2A8BnbIig+Y= +gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520165955-fbffd2b947a5 h1:m1PGG0oh019j+YPYTuIoE9nqxbLxNGRUj0hO7OM2rjE= +gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520165955-fbffd2b947a5/go.mod h1:5J2OFjFIBaCfsjcC9kSyycbIL8g/qAJH2A8BnbIig+Y= +gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520193117-1835255b6d02 h1:FLRmUvu0mz8Ac+/VZf/P4yuv2e6++SSkKOcEIHSlpAI= +gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520193117-1835255b6d02/go.mod h1:5J2OFjFIBaCfsjcC9kSyycbIL8g/qAJH2A8BnbIig+Y= +gitea.rosskeen.house/rosskeen.house/testing v0.0.0-20240509163950-64f2fc3e00d5 h1:1TUeKrJ12K6+Iobc8rpL/gUaGPFBmTqKjJnkT+2B5nM= +gitea.rosskeen.house/rosskeen.house/testing v0.0.0-20240509163950-64f2fc3e00d5/go.mod h1:gbxopbzqpz0ZMAcsPu2XqtprOoFdxwTGz45p06zuI0A= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= diff --git a/internal/resource/container.go b/internal/resource/container.go index 462ba44..679a562 100644 --- a/internal/resource/container.go +++ b/internal/resource/container.go @@ -22,7 +22,7 @@ import ( _ "os" _ "os/exec" "path/filepath" -_ "strings" + "strings" "encoding/json" "io" "gitea.rosskeen.house/rosskeen.house/machine" @@ -35,10 +35,13 @@ type ContainerClient interface { ContainerList(context.Context, container.ListOptions) ([]types.Container, error) ContainerInspect(context.Context, string) (types.ContainerJSON, error) ContainerRemove(context.Context, string, container.RemoveOptions) error + ContainerStop(context.Context, string, container.StopOptions) error + ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) Close() error } type Container struct { + stater machine.Stater `yaml:"-" json:"-"` Id string `json:"ID,omitempty" yaml:"ID,omitempty"` Name string `json:"name" yaml:"name"` Path string `json:"path" yaml:"path"` @@ -133,7 +136,10 @@ func (c *Container) Clone() Resource { } func (c *Container) StateMachine() machine.Stater { - return ProcessMachine() + if c.stater == nil { + c.stater = ProcessMachine(c) + } + return c.stater } func (c *Container) URI() string { @@ -160,6 +166,58 @@ func (c *Container) Validate() error { return fmt.Errorf("failed") } +func (c *Container) Notify(m *machine.EventMessage) { + ctx := context.Background() + switch m.On { + case machine.ENTERSTATEEVENT: + switch m.Dest { + case "start_read": + if _,readErr := c.Read(ctx); readErr == nil { + if triggerErr := c.stater.Trigger("state_read"); triggerErr == nil { + return + } else { + c.State = "absent" + panic(triggerErr) + } + } else { + c.State = "absent" + panic(readErr) + } + case "start_create": + if createErr := c.Create(ctx); createErr == nil { + if triggerErr := c.StateMachine().Trigger("created"); triggerErr == nil { + return + } else { + c.State = "absent" + panic(triggerErr) + } + } else { + c.State = "absent" + panic(createErr) + } + case "start_delete": + if deleteErr := c.Delete(ctx); deleteErr == nil { + if triggerErr := c.StateMachine().Trigger("deleted"); triggerErr == nil { + return + } else { + c.State = "present" + panic(triggerErr) + } + } else { + c.State = "present" + panic(deleteErr) + } + case "present", "created", "read": + c.State = "present" + case "running": + c.State = "running" + case "absent": + c.State = "absent" + } + case machine.EXITSTATEEVENT: + } +} + func (c *Container) Apply() error { ctx := context.Background() switch c.State { @@ -219,7 +277,7 @@ func (c *Container) Create(ctx context.Context) error { resp, err := c.apiClient.ContainerCreate(ctx, config, &c.HostConfig, networkConfig, nil, c.Name) if err != nil { - panic(err) + return err } c.Id = resp.ID @@ -252,7 +310,7 @@ func (c *Container) Read(ctx context.Context) ([]byte, error) { }) if err != nil { - panic(fmt.Errorf("%w: %s %s", err, c.Type(), c.Name)) + return nil, fmt.Errorf("%w: %s %s", err, c.Type(), c.Name) } for _, container := range containers { @@ -290,14 +348,31 @@ func (c *Container) Read(ctx context.Context) ([]byte, error) { } func (c *Container) Delete(ctx context.Context) error { + if stopErr := c.apiClient.ContainerStop(ctx, c.Id, container.StopOptions{}); stopErr != nil { + slog.Error("Container.Delete() - failed to stop: ", "Id", c.Id, "error", stopErr) + return stopErr + } err := c.apiClient.ContainerRemove(ctx, c.Id, container.RemoveOptions{ RemoveVolumes: true, Force: false, }) if err != nil { - slog.Error("Failed to remove: ", "Id", c.Id) - panic(err) + slog.Error("Container.Delete() - failed to remove: ", "Id", c.Id, "error", err) + return err } + + statusCh, errCh := c.apiClient.ContainerWait(ctx, c.Id, container.WaitConditionNotRunning) + select { + case waitErr := <-errCh: + if waitErr != nil { + if strings.Contains(waitErr.Error(), "No such container:") { + return nil + } + return waitErr + } + case <-statusCh: + } + return err } @@ -311,18 +386,24 @@ func (c *Container) ResolveId(ctx context.Context) string { Filters: filterArgs, }) if err != nil { + c.StateMachine().Trigger("notexists") panic(fmt.Errorf("%w: %s %s", err, c.Type(), c.Name)) } + slog.Info("Container.ResolveId()", "containers", containers) for _, container := range containers { for _, containerName := range container.Names { - if containerName == c.Name { + if containerName == "/"+c.Name { + slog.Info("Container.ResolveId()", "state", c.StateMachine()) if c.Id == "" { c.Id = container.ID } + c.StateMachine().Trigger("exists") + slog.Info("Container.ResolveId() trigger created", "machine", c.StateMachine(), "state", c.StateMachine().CurrentState()) return container.ID } } } + c.StateMachine().Trigger("notexists") return "" } diff --git a/internal/resource/container_image.go b/internal/resource/container_image.go new file mode 100644 index 0000000..3f31162 --- /dev/null +++ b/internal/resource/container_image.go @@ -0,0 +1,287 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +// Container resource +package resource + +import ( + "context" + "fmt" + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "gopkg.in/yaml.v3" + _ "gopkg.in/yaml.v3" + "log/slog" + "net/url" +_ "os" +_ "os/exec" + "strings" + "encoding/json" + "io" + "gitea.rosskeen.house/rosskeen.house/machine" + "decl/internal/codec" +) + +type ContainerImageClient interface { + ImagePull(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error) + ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) + ImageRemove(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) + Close() error +} + +type ContainerImage struct { + stater machine.Stater `yaml:"-" json:"-"` + Id string `json:"id,omitempty" yaml:"id,omitempty"` + Name string `json:"name" yaml:"name"` + Created string `json:"created,omitempty" yaml:"created,omitempty"` + Architecture string `json:"architecture,omitempty" yaml:"architecture,omitempty"` + Variant string `json:"variant,omitempty" yaml:"variant,omitempty"` + OS string `json:"os" yaml:"os"` + 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"` + + apiClient ContainerImageClient +} + +func init() { + ResourceTypes.Register("container-image", func(u *url.URL) Resource { + c := NewContainerImage(nil) + c.Name = strings.Join([]string{u.Hostname(), u.Path}, ":") + return c + }) +} + +func NewContainerImage(containerClientApi ContainerImageClient) *ContainerImage { + var apiClient ContainerImageClient = containerClientApi + if apiClient == nil { + var err error + apiClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) + } + } + return &ContainerImage{ + apiClient: apiClient, + } +} + +func (c *ContainerImage) Clone() Resource { + return &ContainerImage { + Id: c.Id, + Name: c.Name, + Created: c.Created, + Architecture: c.Architecture, + Variant: c.Variant, + OS: c.OS, + Size: c.Size, + Author: c.Author, + Comment: c.Comment, + State: c.State, + apiClient: c.apiClient, + } +} + +func (c *ContainerImage) StateMachine() machine.Stater { + if c.stater == nil { + c.stater = StorageMachine(c) + } + return c.stater +} + +func (c *ContainerImage) URI() string { + var host, namespace, repo string + elements := strings.Split(c.Name, "/") + switch len(elements) { + case 1: + repo = elements[0] + case 2: + namespace = elements[0] + repo = elements[1] + case 3: + host = elements[0] + namespace = elements[1] + repo = elements[2] + } + if namespace == "" { + return fmt.Sprintf("container-image://%s/%s", host, repo) + } + return fmt.Sprintf("container-image://%s/%s", host, strings.Join([]string{namespace, repo}, "/")) +} + +func (c *ContainerImage) SetURI(uri string) error { + resourceUri, e := url.Parse(uri) + if e == nil { + if resourceUri.Scheme == c.Type() { + c.Name = strings.Join([]string{resourceUri.Hostname(), resourceUri.RequestURI()}, ":") + } else { + e = fmt.Errorf("%w: %s is not a %s", ErrInvalidResourceURI, uri, c.Type()) + } + } + return e +} + +func (c *ContainerImage) JSON() ([]byte, error) { + return json.Marshal(c) +} + +func (c *ContainerImage) Validate() error { + return fmt.Errorf("failed") +} + +func (c *ContainerImage) Notify(m *machine.EventMessage) { + slog.Info("ContainerImage.Notify()", "event", m, "state", c.State) + ctx := context.Background() + switch m.On { + case machine.ENTERSTATEEVENT: + switch m.Dest { + case "start_read": + if _,readErr := c.Read(ctx); readErr == nil { + if triggerErr := c.stater.Trigger("state_read"); triggerErr == nil { + return + } else { + c.State = "absent" + panic(triggerErr) + } + } else { + c.State = "absent" + panic(readErr) + } + case "start_create": + if createErr := c.Create(ctx); createErr == nil { + if triggerErr := c.stater.Trigger("created"); triggerErr == nil { + return + } else { + c.State = "absent" + panic(triggerErr) + } + } else { + c.State = "absent" + panic(createErr) + } + case "start_delete": + if deleteErr := c.Delete(ctx); deleteErr == nil { + if triggerErr := c.StateMachine().Trigger("deleted"); triggerErr == nil { + return + } else { + c.State = "present" + panic(triggerErr) + } + } else { + c.State = "present" + panic(deleteErr) + } + case "present", "created", "read": + c.State = "present" + case "absent": + c.State = "absent" + } + case machine.EXITSTATEEVENT: + } +} + +func (c *ContainerImage) Apply() error { + ctx := context.Background() + switch c.State { + case "absent": + return c.Delete(ctx) + case "present": + return c.Create(ctx) + } + return nil +} + +func (c *ContainerImage) Load(r io.Reader) error { + return codec.NewYAMLDecoder(r).Decode(c) +} + +func (c *ContainerImage) LoadDecl(yamlResourceDeclaration string) error { + return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(c) +} + +func (c *ContainerImage) Create(ctx context.Context) error { + return nil +} + +func (c *ContainerImage) Read(ctx context.Context) ([]byte, error) { + out, err := c.apiClient.ImagePull(ctx, c.Name, types.ImagePullOptions{}) + slog.Info("Read()", "name", c.Name, "error", err) + + _, outputErr := io.ReadAll(out) + + + if err != nil { + return nil, fmt.Errorf("%w: %s %s", err, c.Type(), c.Name) + } + + if outputErr != nil { + return nil, fmt.Errorf("%w: %s %s", outputErr, c.Type(), c.Name) + } + + imageInspect, _, err := c.apiClient.ImageInspectWithRaw(ctx, c.Name) + if err != nil { + if client.IsErrNotFound(err) { + slog.Info("ContainerImage.Read()", "oldstate", c.State, "newstate", "absent", "error", err) + c.State = "absent" + } else { + panic(err) + } + return nil, err + } + + c.State = "present" + c.Id = imageInspect.ID +/* + if c.Name == "" { + c.Name = imageInspect.Name + } +*/ + c.Created = imageInspect.Created + c.Author = imageInspect.Author + c.Architecture = imageInspect.Architecture + c.Variant = imageInspect.Variant + c.OS = imageInspect.Os + c.Size = imageInspect.Size + c.Comment = imageInspect.Comment + slog.Info("Read() ", "type", c.Type(), "name", c.Name, "Id", c.Id) + return yaml.Marshal(c) +} + +func (c *ContainerImage) Delete(ctx context.Context) error { + slog.Info("ContainerImage.Delete()", "image", c) + options := types.ImageRemoveOptions{ + Force: false, + PruneChildren: false, + } + + _, err := c.apiClient.ImageRemove(ctx, c.Id, options) + return err +/* +for _, img := range deletedImages { +fmt.Printf("Deleted image: %s\n", img.Deleted) +fmt.Printf("Untagged image: %s\n", img.Untagged) +} +*/ +} + +func (c *ContainerImage) Type() string { return "container-image" } + +func (c *ContainerImage) ResolveId(ctx context.Context) string { + slog.Info("ContainerImage.ResolveId()", "name", c.Name, "machine.state", c.StateMachine().CurrentState()) + imageInspect, _, err := c.apiClient.ImageInspectWithRaw(ctx, c.Name) + if err != nil { + triggerResult := c.StateMachine().Trigger("notexists") + slog.Info("ContainerImage.ResolveId()", "name", c.Name, "machine.state", c.StateMachine().CurrentState(), "resource.state", c.State, "trigger.error", triggerResult) + panic(fmt.Errorf("%w: %s %s", err, c.Type(), c.Name)) + } + slog.Info("ContainerImage.ResolveId()", "name", c.Name, "machine.state", c.StateMachine().CurrentState(), "resource.state", c.State) + c.Id = imageInspect.ID + if c.Id != "" { + c.StateMachine().Trigger("exists") + slog.Info("ContainerImage.ResolveId() trigger created", "machine", c.StateMachine(), "state", c.StateMachine().CurrentState()) + } else { + c.StateMachine().Trigger("notexists") + slog.Info("ContainerImage.ResolveId()", "name", c.Name, "machine.state", c.StateMachine().CurrentState(), "resource.state", c.State) + } + return c.Id +} diff --git a/internal/resource/container_image_test.go b/internal/resource/container_image_test.go new file mode 100644 index 0000000..3633d50 --- /dev/null +++ b/internal/resource/container_image_test.go @@ -0,0 +1,89 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package resource + +import ( + "context" + "decl/tests/mocks" +_ "encoding/json" +_ "fmt" + "github.com/docker/docker/api/types" + "github.com/stretchr/testify/assert" + "io" + "io/ioutil" +_ "net/http" +_ "net/http/httptest" +_ "net/url" +_ "os" + "strings" + "testing" +) + +func TestNewContainerImageResource(t *testing.T) { + c := NewContainerImage(&mocks.MockContainerClient{}) + assert.NotNil(t, c) +} + +func TestReadContainerImage(t *testing.T) { + output := ioutil.NopCloser(strings.NewReader("testdata")) + ctx := context.Background() + decl := ` + name: "alpine:latest" + state: present +` + m := &mocks.MockContainerClient{ + InjectImagePull: func(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error) { + return output, nil + }, + InjectImageRemove: func(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { + return nil, nil + }, + InjectImageInspectWithRaw: func(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) { + return types.ImageInspect{ + ID: "sha256:123456789abc", + }, nil, nil + }, + } + + c := NewContainerImage(m) + assert.NotNil(t, c) + + e := c.LoadDecl(decl) + assert.Equal(t, nil, e) + assert.Equal(t, "alpine:latest", c.Name) + + resourceYaml, readContainerErr := c.Read(ctx) + assert.Equal(t, nil, readContainerErr) + 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 + }, + } + + decl := ` + name: "testcontainer" + image: "alpine" + state: present +` + c := NewContainerImage(m) + e := c.LoadDecl(decl) + assert.Equal(t, nil, e) + assert.Equal(t, "testcontainer", c.Name) + + applyErr := c.Apply() + assert.Equal(t, nil, applyErr) + + c.State = "absent" + + applyDeleteErr := c.Apply() + assert.Equal(t, nil, applyDeleteErr) +} +*/ diff --git a/internal/resource/container_network.go b/internal/resource/container_network.go index df5994e..8f867d2 100644 --- a/internal/resource/container_network.go +++ b/internal/resource/container_network.go @@ -42,7 +42,7 @@ type ContainerNetwork struct { } func init() { - ResourceTypes.Register("container_network", func(u *url.URL) Resource { + ResourceTypes.Register("container-network", func(u *url.URL) Resource { n := NewContainerNetwork(nil) n.Name = filepath.Join(u.Hostname(), u.Path) return n @@ -100,7 +100,7 @@ func (n *ContainerNetwork) Notify(m *machine.EventMessage) { } func (n *ContainerNetwork) URI() string { - return fmt.Sprintf("container_network://%s", n.Name) + return fmt.Sprintf("container-network://%s", n.Name) } func (n *ContainerNetwork) SetURI(uri string) error { @@ -164,7 +164,7 @@ func (n *ContainerNetwork) Delete(ctx context.Context) error { return nil } -func (n *ContainerNetwork) Type() string { return "container_network" } +func (n *ContainerNetwork) Type() string { return "container-network" } func (n *ContainerNetwork) ResolveId(ctx context.Context) string { filterArgs := filters.NewArgs() diff --git a/internal/resource/container_test.go b/internal/resource/container_test.go index a8c230b..a650a08 100644 --- a/internal/resource/container_test.go +++ b/internal/resource/container_test.go @@ -48,6 +48,13 @@ func TestReadContainer(t *testing.T) { Image: "alpine", }}, nil }, + InjectContainerWait: func(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) { + var res container.WaitResponse + resChan := make(chan container.WaitResponse) + errChan := make(chan error, 1) + go func() { resChan <- res }() + return resChan, errChan + }, } c := NewContainer(m) @@ -67,9 +74,19 @@ func TestCreateContainer(t *testing.T) { 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 }, + InjectContainerStop: func(context.Context, string, container.StopOptions) error { + return nil + }, InjectContainerRemove: func(context.Context, string, container.RemoveOptions) error { return nil }, + InjectContainerWait: func(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) { + var res container.WaitResponse + resChan := make(chan container.WaitResponse) + errChan := make(chan error, 1) + go func() { resChan <- res }() + return resChan, errChan + }, } decl := ` diff --git a/internal/resource/declaration.go b/internal/resource/declaration.go index 8f1b266..4d81b57 100644 --- a/internal/resource/declaration.go +++ b/internal/resource/declaration.go @@ -9,7 +9,8 @@ import ( "io" "gopkg.in/yaml.v3" "log/slog" - "gitea.rosskeen.house/rosskeen.house/machine" +_ "gitea.rosskeen.house/rosskeen.house/machine" + "gitea.rosskeen.house/pylon/luaruntime" "decl/internal/codec" ) @@ -19,10 +20,10 @@ type DeclarationType struct { } type Declaration struct { - StateMatchine machine.Stater `json:"-" yaml:"-"` Type TypeName `json:"type" yaml:"type"` Transition string `json:"transition,omitempty" yaml:"transition,omitempty"` Attributes Resource `json:"attributes" yaml:"attributes"` + runtime luaruntime.LuaRunner } type ResourceLoader interface { @@ -37,11 +38,24 @@ func NewDeclaration() *Declaration { return &Declaration{} } +func (d *Declaration) ResolveId(ctx context.Context) string { + defer func() { + if r := recover(); r != nil { + slog.Info("Declaration.ResolveId() - panic", "recover", r, "state", d.Attributes.StateMachine()) + d.Attributes.StateMachine().Trigger("notexists") + } + }() + slog.Info("Declaration.ResolveId()") + id := d.Attributes.ResolveId(ctx) + return id +} + func (d *Declaration) Clone() *Declaration { return &Declaration { Type: d.Type, Transition: d.Transition, Attributes: d.Attributes.Clone(), + runtime: luaruntime.New(), } } @@ -64,16 +78,34 @@ func (d *Declaration) Resource() Resource { return d.Attributes } -func (d *Declaration) Apply() error { +func (d *Declaration) Apply() (result error) { + defer func() { + if r := recover(); r != nil { + result = fmt.Errorf("%s", r) + } + }() + stater := d.Attributes.StateMachine() switch d.Transition { - case "absent": + case "read": + result = stater.Trigger("read") + case "delete", "absent": + slog.Info("Declaration.Apply()", "machine", stater, "machine.state", stater.CurrentState(), "uri", d.Attributes.URI()) + if stater.CurrentState() == "present" { + result = stater.Trigger("delete") + } default: fallthrough case "create", "present": - return stater.Trigger("create") + slog.Info("Declaration.Apply()", "machine", stater, "machine.state", stater.CurrentState(), "uri", d.Attributes.URI()) + if stater.CurrentState() == "absent" || stater.CurrentState() == "unknown" { + if result = stater.Trigger("create"); result != nil { + return result + } + } + result = stater.Trigger("read") } - return nil + return result } func (d *Declaration) SetURI(uri string) error { @@ -87,19 +119,28 @@ func (d *Declaration) SetURI(uri string) error { return e } + +func (d *Declaration) UnmarshalValue(value *DeclarationType) error { + d.Type = value.Type + d.Transition = value.Transition + newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", value.Type)) + if resourceErr != nil { + return resourceErr + } + d.Attributes = newResource + return nil +} + func (d *Declaration) UnmarshalYAML(value *yaml.Node) error { t := &DeclarationType{} if unmarshalResourceTypeErr := value.Decode(t); unmarshalResourceTypeErr != nil { return unmarshalResourceTypeErr } - d.Type = t.Type - d.Transition = t.Transition - newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", d.Type)) - if resourceErr != nil { - return resourceErr + if err := d.UnmarshalValue(t); err != nil { + return err } - d.Attributes = newResource + resourceAttrs := struct { Attributes yaml.Node `json:"attributes"` }{} @@ -118,18 +159,13 @@ func (d *Declaration) UnmarshalJSON(data []byte) error { return unmarshalResourceTypeErr } - d.Type = t.Type - d.Transition = t.Transition - - newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", d.Type)) - if resourceErr != nil { - return resourceErr + if err := d.UnmarshalValue(t); err != nil { + return err } - d.Attributes = newResource resourceAttrs := struct { Attributes Resource `json:"attributes"` - }{Attributes: newResource} + }{Attributes: d.Attributes} if unmarshalAttributesErr := json.Unmarshal(data, &resourceAttrs); unmarshalAttributesErr != nil { return unmarshalAttributesErr } @@ -150,3 +186,39 @@ func (d *Declaration) MarshalJSON() ([]byte, error) { func (d *Declaration) MarshalYAML() (any, error) { return d, nil } + +/* +func (l *LuaWorker) Receive(m message.Envelope) { + s := m.Sender() + switch b := m.Body().(type) { + case *message.Error: + // case *worker.Terminated: + case *CodeExecute: + stackSize := l.runtime.Api().GetTop() + if e := l.runtime.LoadScriptFromString(b.Code); e != nil { + s.Send(message.New(&message.Error{ E: e }, l)) + } + returnsCount := l.runtime.Api().GetTop() - stackSize + if len(b.Entrypoint) == 0 { + if ! l.runtime.Api().IsNil(-1) { + if returnsCount == 0 { + s.Send(message.New(&CodeResult{ Result: []interface{}{ 0 } }, l)) + } else { + lr,le := l.runtime.CopyReturnValuesFromCall(int(returnsCount)) + if le != nil { + s.Send(message.New(&message.Error{ E: le }, l)) + } else { + s.Send(message.New(&CodeResult{ Result: lr }, l)) + } + } + } + } else { + r,ce := l.runtime.CallFunction(b.Entrypoint, b.Args) + if ce != nil { + s.Send(message.New(&message.Error{ E: ce }, l)) + } + s.Send(message.New(&CodeResult{ Result: r }, l)) + } + } +} +*/ diff --git a/internal/resource/document.go b/internal/resource/document.go index e3a539c..2208291 100644 --- a/internal/resource/document.go +++ b/internal/resource/document.go @@ -12,6 +12,7 @@ _ "net/url" "github.com/sters/yaml-diff/yamldiff" "strings" "decl/internal/codec" + "context" ) type Document struct { @@ -71,15 +72,37 @@ func (d *Document) Resources() []Declaration { return d.ResourceDecls } -func (d *Document) Apply() error { +func (d *Document) ResolveIds(ctx context.Context) { + for i := range d.ResourceDecls { + d.ResourceDecls[i].ResolveId(ctx) + } +} + +func (d *Document) Apply(state string) error { if d == nil { panic("Undefined Document") } - slog.Info("Document.Apply()", "declarations", d) - for i := range d.ResourceDecls { - if e := d.ResourceDecls[i].Apply(); e != nil { + slog.Info("Document.Apply()", "declarations", d, "override", state) + var start, i int = 0, 0 + if state == "delete" { + start = len(d.ResourceDecls) - 1 + } + for { + idx := i - start + if idx < 0 { idx = - idx } + + slog.Info("Document.Apply() applying resource", "index", idx, "uri", d.ResourceDecls[idx].Resource().URI(), "resource", d.ResourceDecls[idx].Resource()) + if state != "" { + d.ResourceDecls[idx].Transition = state + } + if e := d.ResourceDecls[idx].Apply(); e != nil { + slog.Error("Document.Apply() error applying resource", "index", idx, "uri", d.ResourceDecls[idx].Resource().URI(), "resource", d.ResourceDecls[idx].Resource(), "error", e) return e } + if i >= len(d.ResourceDecls) - 1 { + break + } + i++ } return nil } diff --git a/internal/resource/document_test.go b/internal/resource/document_test.go index ebfcfd2..85555ba 100644 --- a/internal/resource/document_test.go +++ b/internal/resource/document_test.go @@ -13,6 +13,7 @@ import ( "syscall" "testing" "time" + "os/user" ) func TestNewDocumentLoader(t *testing.T) { @@ -35,8 +36,8 @@ resources: - type: file attributes: path: "%s" - owner: "nobody" - group: "nobody" + owner: "%s" + group: "%s" mode: "0600" content: |- test line 1 @@ -50,9 +51,9 @@ resources: home: "/home/testuser" createhome: true state: present -`, file) +`, file, ProcessTestUserName, ProcessTestGroupName) d := NewDocument() - assert.NotEqual(t, nil, d) + assert.NotNil(t, d) docReader := strings.NewReader(document) @@ -82,14 +83,18 @@ func TestDocumentGenerator(t *testing.T) { aTime := time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) cTime := time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec)) + processUser, userErr := user.Current() + assert.Nil(t, userErr) + processGroup, groupErr := user.LookupGroupId(processUser.Gid) + assert.Nil(t, groupErr) expected := fmt.Sprintf(` resources: - type: file attributes: path: %s - owner: "root" - group: "root" + owner: "%s" + group: "%s" mode: "0644" content: | %s @@ -100,7 +105,7 @@ resources: size: 82 filetype: "regular" state: present -`, file, fileContent, aTime.Format(time.RFC3339Nano), cTime.Format(time.RFC3339Nano), mTime.Format(time.RFC3339Nano)) +`, file, processUser.Username, processGroup.Name, fileContent, aTime.Format(time.RFC3339Nano), cTime.Format(time.RFC3339Nano), mTime.Format(time.RFC3339Nano)) var documentYaml strings.Builder d := NewDocument() diff --git a/internal/resource/exec.go b/internal/resource/exec.go index e451aa9..ff20292 100644 --- a/internal/resource/exec.go +++ b/internal/resource/exec.go @@ -18,6 +18,7 @@ _ "strings" ) 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"` @@ -51,7 +52,11 @@ func (x *Exec) Clone() Resource { } func (x *Exec) StateMachine() machine.Stater { - return ProcessMachine() + if x.stater == nil { + x.stater = ProcessMachine(x) + + } + return x.stater } func (x *Exec) URI() string { @@ -82,6 +87,25 @@ func (x *Exec) Apply() error { return nil } +func (x *Exec) Notify(m *machine.EventMessage) { + ctx := context.Background() + switch m.On { + case machine.ENTERSTATEEVENT: + switch m.Dest { + case "start_create": + if e := x.Create(ctx); e == nil { + if triggerErr := x.stater.Trigger("created"); triggerErr == nil { + return + } + } + x.State = "absent" + case "present": + x.State = "present" + } + case machine.EXITSTATEEVENT: + } +} + func (x *Exec) Load(r io.Reader) error { return codec.NewYAMLDecoder(r).Decode(x) } @@ -92,6 +116,10 @@ func (x *Exec) LoadDecl(yamlResourceDeclaration string) error { func (x *Exec) Type() string { return "exec" } +func (x *Exec) Create(ctx context.Context) error { + return nil +} + func (x *Exec) Read(ctx context.Context) ([]byte, error) { return yaml.Marshal(x) } diff --git a/internal/resource/file.go b/internal/resource/file.go index 1616777..09a69f5 100644 --- a/internal/resource/file.go +++ b/internal/resource/file.go @@ -119,14 +119,40 @@ func (f *File) Notify(m *machine.EventMessage) { switch m.On { case machine.ENTERSTATEEVENT: switch m.Dest { + case "start_read": + if _,readErr := f.Read(ctx); readErr == nil { + if triggerErr := f.StateMachine().Trigger("state_read"); triggerErr == nil { + return + } else { + f.State = "absent" + panic(triggerErr) + } + } else { + f.State = "absent" + panic(readErr) + } case "start_create": if e := f.Create(ctx); e == nil { - if triggerErr := f.stater.Trigger("created"); triggerErr == nil { + if triggerErr := f.StateMachine().Trigger("created"); triggerErr == nil { return } } f.State = "absent" - case "present": + case "start_delete": + if deleteErr := f.Delete(ctx); deleteErr == nil { + if triggerErr := f.StateMachine().Trigger("deleted"); triggerErr == nil { + return + } else { + f.State = "present" + panic(triggerErr) + } + } else { + f.State = "present" + panic(deleteErr) + } + case "absent": + f.State = "absent" + case "present", "created", "read": f.State = "present" } case machine.EXITSTATEEVENT: @@ -157,14 +183,12 @@ func (f *File) Validate() error { } func (f *File) Apply() error { + ctx := context.Background() switch f.State { case "absent": - removeErr := os.Remove(f.Path) - if removeErr != nil { - return removeErr - } + return f.Delete(ctx) case "present": - return f.Create(context.Background()) + return f.Create(ctx) } return nil @@ -285,6 +309,10 @@ func (f *File) Create(ctx context.Context) error { return nil } +func (f *File) Delete(ctx context.Context) error { + return os.Remove(f.Path) +} + func (f *File) UpdateContentAttributes() { f.Size = int64(len(f.Content)) f.Sha256 = fmt.Sprintf("%x", sha256.Sum256([]byte(f.Content))) diff --git a/internal/resource/file_test.go b/internal/resource/file_test.go index 89ae1c6..172a7be 100644 --- a/internal/resource/file_test.go +++ b/internal/resource/file_test.go @@ -53,14 +53,18 @@ func TestReadFile(t *testing.T) { ctx := context.Background() file, _ := filepath.Abs(filepath.Join(TempDir, "fooread.txt")) + expectedTime, timeErr := time.Parse(time.RFC3339Nano, "2001-12-15T01:01:01.000000001Z") + assert.Nil(t, timeErr) + expectedTimestamp := expectedTime.Local().Format(time.RFC3339Nano) + declarationAttributes := ` path: "%s" - owner: "nobody" - group: "nobody" + owner: "%s" + group: "%s" mode: "0600" - atime: 2001-12-15T01:01:01.000000001Z + atime: %s ctime: %s - mtime: 2001-12-15T01:01:01.000000001Z + mtime: %s content: |- test line 1 test line 2 @@ -70,11 +74,11 @@ func TestReadFile(t *testing.T) { state: present ` - decl := fmt.Sprintf(declarationAttributes, file, "2001-12-15T01:01:01.000000001Z") + decl := fmt.Sprintf(declarationAttributes, file, ProcessTestUserName, ProcessTestGroupName, expectedTimestamp, expectedTimestamp, expectedTimestamp) testFile := NewFile() e := testFile.LoadDecl(decl) - assert.Equal(t, nil, e) + assert.Nil(t, e) applyErr := testFile.Apply() assert.Nil(t, applyErr) @@ -83,8 +87,8 @@ func TestReadFile(t *testing.T) { f.Path = file r, e := f.Read(ctx) - assert.Equal(t, nil, e) - assert.Equal(t, "nobody", f.Owner) + assert.Nil(t, e) + assert.Equal(t, ProcessTestUserName, f.Owner) info, statErr := os.Stat(file) assert.Nil(t, statErr) @@ -92,7 +96,7 @@ func TestReadFile(t *testing.T) { assert.True(t, ok) cTime := time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec)) - expected := fmt.Sprintf(declarationAttributes, file, cTime.Format(time.RFC3339Nano)) + expected := fmt.Sprintf(declarationAttributes, file, ProcessTestUserName, ProcessTestGroupName, expectedTimestamp, cTime.Local().Format(time.RFC3339Nano), expectedTimestamp) assert.YAMLEq(t, expected, string(r)) } @@ -113,19 +117,19 @@ func TestCreateFile(t *testing.T) { decl := fmt.Sprintf(` path: "%s" - owner: "nobody" - group: "nobody" + owner: "%s" + group: "%s" mode: "0600" content: |- test line 1 test line 2 state: present -`, file) +`, file, ProcessTestUserName, ProcessTestGroupName) f := NewFile() e := f.LoadDecl(decl) assert.Equal(t, nil, e) - assert.Equal(t, "nobody", f.Owner) + assert.Equal(t, ProcessTestUserName, f.Owner) applyErr := f.Apply() assert.Equal(t, nil, applyErr) @@ -155,17 +159,17 @@ func TestFileDirectory(t *testing.T) { decl := fmt.Sprintf(` path: "%s" - owner: "nobody" - group: "nobody" + owner: "%s" + group: "%s" mode: "0700" filetype: "directory" state: present -`, file) +`, file, ProcessTestUserName, ProcessTestGroupName) f := NewFile() e := f.LoadDecl(decl) assert.Equal(t, nil, e) - assert.Equal(t, "nobody", f.Owner) + assert.Equal(t, ProcessTestUserName, f.Owner) applyErr := f.Apply() assert.Equal(t, nil, applyErr) @@ -181,13 +185,13 @@ func TestFileTimes(t *testing.T) { file, _ := filepath.Abs(filepath.Join(TempDir, "testtimes.txt")) decl := fmt.Sprintf(` path: "%s" - owner: "nobody" - group: "nobody" + owner: "%s" + group: "%s" mtime: 2001-12-15T01:01:01.1Z mode: "0600" filtetype: "regular" state: "present" -`, file) +`, file, ProcessTestUserName, ProcessTestGroupName) expectedTime, timeErr := time.Parse(time.RFC3339, "2001-12-15T01:01:01.1Z") assert.Nil(t, timeErr) @@ -195,7 +199,7 @@ func TestFileTimes(t *testing.T) { f := NewFile() e := f.LoadDecl(decl) assert.Nil(t, e) - assert.Equal(t, "nobody", f.Owner) + assert.Equal(t, ProcessTestUserName, f.Owner) assert.True(t, f.Mtime.Equal(expectedTime)) } @@ -252,8 +256,8 @@ func TestFileReadStat(t *testing.T) { statErr := f.ReadStat() assert.Error(t, statErr) - f.Owner = "nobody" - f.Group = "nobody" + f.Owner = ProcessTestUserName + f.Group = ProcessTestGroupName f.State = "present" assert.Nil(t, f.Apply()) @@ -332,28 +336,27 @@ func TestFileClone(t *testing.T) { } func TestFileErrors(t *testing.T) { - ctx := context.Background() + //ctx := context.Background() testFile := filepath.Join(TempDir, "testerr.txt") f := NewFile() assert.NotNil(t, f) + stater := f.StateMachine() f.Path = testFile f.Mode = "631" - f.State = "present" - assert.Nil(t, f.Apply()) + assert.Nil(t, stater.Trigger("create")) read := NewFile() + readStater := read.StateMachine() read.Path = testFile - _, readErr := read.Read(ctx) - assert.Nil(t, readErr) + assert.Nil(t, readStater.Trigger("read")) assert.Equal(t, "0631", read.Mode) f.Mode = "900" - assert.ErrorAs(t, f.Apply(), &ErrInvalidFileMode, "Apply should fail with NumError when converting invalid octal") + assert.ErrorAs(t, stater.Trigger("create"), &ErrInvalidFileMode, "Apply should fail with NumError when converting invalid octal") - _, updateReadErr := read.Read(ctx) - assert.Nil(t, updateReadErr) + assert.Nil(t, readStater.Trigger("read")) assert.Equal(t, "0631", read.Mode) f.Mode = "0631" @@ -363,3 +366,34 @@ func TestFileErrors(t *testing.T) { assert.Error(t, uidErr, UnknownUser) } + +func TestFileDelete(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 + state: present +`, file, ProcessTestUserName, ProcessTestGroupName) + + f := NewFile() + stater := f.StateMachine() + e := f.LoadDecl(decl) + assert.Nil(t, e) + assert.Equal(t, ProcessTestUserName, f.Owner) + + assert.Nil(t, stater.Trigger("create")) + assert.FileExists(t, file, nil) + s, e := os.Stat(file) + assert.Nil(t, e) + + assert.Greater(t, s.Size(), int64(0)) + + assert.Nil(t, stater.Trigger("delete")) + assert.NoFileExists(t, file, nil) +} diff --git a/internal/resource/network_route.go b/internal/resource/network_route.go index 186d5e3..4c1e6b8 100644 --- a/internal/resource/network_route.go +++ b/internal/resource/network_route.go @@ -11,7 +11,6 @@ import ( "gopkg.in/yaml.v3" "io" "net/url" - "os/exec" "regexp" _ "strconv" "strings" @@ -119,15 +118,23 @@ type NetworkRoute struct { RouteType NetworkRouteType `json:"routetype" yaml:"routetype"` Scope NetworkRouteScope `json:"scope" yaml:"scope"` Proto NetworkRouteProto `json:"proto" yaml:"proto"` + + CreateCommand *Command `yaml:"-" json:"-"` + ReadCommand *Command `yaml:"-" json:"-"` + UpdateCommand *Command `yaml:"-" json:"-"` + DeleteCommand *Command `yaml:"-" json:"-"` + State string `json:"state" yaml:"state"` } func NewNetworkRoute() *NetworkRoute { - return &NetworkRoute{Rtid: NetworkRouteTableMain} + n := &NetworkRoute{Rtid: NetworkRouteTableMain} + n.CreateCommand, n.ReadCommand, n.UpdateCommand, n.DeleteCommand = n.NewCRUD() + return n } func (n *NetworkRoute) Clone() Resource { - return &NetworkRoute { + newn := &NetworkRoute { Id: n.Id, To: n.To, Interface: n.Interface, @@ -139,6 +146,8 @@ func (n *NetworkRoute) Clone() Resource { Proto: n.Proto, State: n.State, } + newn.CreateCommand, newn.ReadCommand, newn.UpdateCommand, newn.DeleteCommand = n.NewCRUD() + return newn } func (n *NetworkRoute) StateMachine() machine.Stater { @@ -168,6 +177,12 @@ func (n *NetworkRoute) Notify(m *machine.EventMessage) { } func (n *NetworkRoute) Create(ctx context.Context) error { + _, err := n.CreateCommand.Execute(n) + if n.CreateCommand.Extractor != nil { + if err != nil { + return n.CreateCommand.Extractor([]byte(err.Error()), n) + } + } return nil } @@ -184,7 +199,6 @@ func (n *NetworkRoute) Validate() error { } func (n *NetworkRoute) Apply() error { - switch n.State { case "absent": case "present": @@ -209,6 +223,16 @@ func (n *NetworkRoute) ResolveId(ctx context.Context) string { } func (n *NetworkRoute) Read(ctx context.Context) ([]byte, error) { + out, err := n.ReadCommand.Execute(n) + if err != nil { + return nil, err + } + exErr := n.ReadCommand.Extractor(out, n) + if exErr != nil { + return nil, exErr + } + +/* var cmdArgs []string = make([]string, 17) cmdArgs[0] = "route" cmdArgs[1] = "show" @@ -249,6 +273,7 @@ func (n *NetworkRoute) Read(ctx context.Context) ([]byte, error) { } } } +*/ n.ResolveId(ctx) return yaml.Marshal(n) } @@ -410,3 +435,75 @@ func (n *NetworkRoute) UnmarshalJSON(data []byte) error { } return nil } + + +func (n *NetworkRoute) NewCRUD() (create *Command, read *Command, update *Command, del *Command) { + return NewNetworkRouteCreateCommand(), NewNetworkRouteReadCommand(), NewNetworkRouteUpdateCommand(), NewNetworkRouteDeleteCommand() +} + +func NewNetworkRouteCreateCommand() *Command { + c := NewCommand() + c.Path = "ip" + c.Args = []CommandArg{ + CommandArg("route"), + CommandArg("add"), + CommandArg("{{ if .To }}to {{ .To }}{{ end }}"), + CommandArg("{{ if .Rtid }}table {{ .Rtid }}{{ end }}"), + CommandArg("{{ if .Gateway }}via {{ .Gateway }}{{ end }}"), + CommandArg("{{ if .Proto }}protocol {{ .Proto }}{{ end }}"), + CommandArg("{{ if .Scope }}scope {{ .Scope }}{{ end }}"), + CommandArg("{{ if .RouteType }}type {{ .RouteType }}{{ end }}"), + CommandArg("{{ if .Interface }}dev {{ .Interface }}{{ end }}"), + CommandArg("{{ if .Metric }}metric {{ .Metric }}{{ end }}"), + } + return c +} + +func NewNetworkRouteReadCommand() *Command { + c := NewCommand() + c.Path = "ip" + c.Args = []CommandArg{ + CommandArg("route"), + CommandArg("show"), + CommandArg("{{ if .To }}to {{ .To }}{{ end }}"), + CommandArg("{{ if .Rtid }}table {{ .Rtid }}{{ end }}"), + CommandArg("{{ if .Gateway }}via {{ .Gateway }}{{ end }}"), + CommandArg("{{ if .Proto }}protocol {{ .Proto }}{{ end }}"), + CommandArg("{{ if .Scope }}scope {{ .Scope }}{{ end }}"), + CommandArg("{{ if .RouteType }}type {{ .RouteType }}{{ end }}"), + CommandArg("{{ if .Interface }}dev {{ .Interface }}{{ end }}"), + } + c.Extractor = func(out []byte, target any) error { + n := target.(*NetworkRoute) + routes := strings.Split(string(out), "\n") + if len(routes) == 1 { + fields := strings.Split(routes[0], " ") + numberOfFields := len(fields) + if numberOfFields > 1 { + n.To = fields[0] + for i := 1; i < numberOfFields; i += 2 { + n.SetField(fields[i], fields[i + 1]) + } + } + n.State = "present" + } else { + n.State = "absent" + } + return nil + } + return c +} + +func NewNetworkRouteUpdateCommand() *Command { + c := NewCommand() + c.Path = "ip" + c.Args = []CommandArg{ + CommandArg("del"), + CommandArg("{{ .Name }}"), + } + return c +} + +func NewNetworkRouteDeleteCommand() *Command { + return nil +} diff --git a/internal/resource/network_route_test.go b/internal/resource/network_route_test.go index 5144306..cda8883 100644 --- a/internal/resource/network_route_test.go +++ b/internal/resource/network_route_test.go @@ -4,21 +4,21 @@ package resource import ( "context" - _ "encoding/json" - _ "fmt" +_ "encoding/json" +_ "fmt" "github.com/stretchr/testify/assert" - _ "gopkg.in/yaml.v3" - _ "io" - _ "log" - _ "net/http" - _ "net/http/httptest" - _ "net/url" - _ "os" - _ "path/filepath" - _ "strings" - _ "syscall" +_ "gopkg.in/yaml.v3" +_ "io" +_ "log" +_ "net/http" +_ "net/http/httptest" +_ "net/url" +_ "os" +_ "path/filepath" +_ "strings" +_ "syscall" "testing" - _ "time" +_ "time" ) func TestNewNetworkRouteResource(t *testing.T) { @@ -39,7 +39,6 @@ func TestReadNetworkRoute(t *testing.T) { declarationAttributes := ` to: "192.168.0.0/24" - interface: "eth0" gateway: "192.168.0.1" metric: 0 routetype: "unicast" @@ -49,12 +48,13 @@ func TestReadNetworkRoute(t *testing.T) { testRoute := NewNetworkRoute() e := testRoute.LoadDecl(declarationAttributes) - assert.Equal(t, nil, e) + assert.Nil(t, e) + testRouteErr := testRoute.Apply() assert.Nil(t, testRouteErr) r, e := testRoute.Read(ctx) - assert.Nil(t, e) + assert.Equal(t, "", ExitError(e)) assert.NotNil(t, r) assert.Equal(t, NetworkRouteType("unicast"), testRoute.RouteType) } diff --git a/internal/resource/os_test.go b/internal/resource/os_test.go index 7bea6be..82bd6df 100644 --- a/internal/resource/os_test.go +++ b/internal/resource/os_test.go @@ -1,4 +1,6 @@ // Copyright 2024 Matthew Rich . All rights reserved. + + package resource import ( @@ -25,10 +27,10 @@ func TestLookupUID(t *testing.T) { } func TestLookupGID(t *testing.T) { - gid, e := LookupGID("nobody") + gid, e := LookupGID("adm") assert.Nil(t, e) - assert.Equal(t, 65534, gid) + assert.Equal(t, 4, gid) ngid, ne := LookupGID("1001") assert.Nil(t, ne) diff --git a/internal/resource/resource.go b/internal/resource/resource.go index 2600513..bf5b7b5 100644 --- a/internal/resource/resource.go +++ b/internal/resource/resource.go @@ -40,16 +40,23 @@ type ResourceReader interface { } type ResourceUpdater interface { - Update() error + Update(context.Context) error } type ResourceDeleter interface { - Delete() error + Delete(context.Context) error } type ResourceDecoder struct { } +type ResourceCrudder struct { + ResourceCreator + ResourceReader + ResourceUpdater + ResourceDeleter +} + func NewResource(uri string) Resource { r, e := ResourceTypes.New(uri) if e == nil { diff --git a/internal/resource/resource_test.go b/internal/resource/resource_test.go index e58ba58..af00ac9 100644 --- a/internal/resource/resource_test.go +++ b/internal/resource/resource_test.go @@ -7,12 +7,17 @@ import ( "github.com/stretchr/testify/assert" "log" "os" + "os/user" + "os/exec" "path/filepath" "testing" ) var TempDir string +var ProcessTestUserName string +var ProcessTestGroupName string + func TestMain(m *testing.M) { var err error TempDir, err = os.MkdirTemp("", "testresourcefile") @@ -20,12 +25,37 @@ func TestMain(m *testing.M) { log.Fatal(err) } + ProcessTestUserName, ProcessTestGroupName = ProcessUserName() rc := m.Run() os.RemoveAll(TempDir) os.Exit(rc) } +func ProcessUserName() (string, string) { + processUser, userErr := user.Current() + if userErr != nil { + panic(userErr) + } + processGroup, groupErr := user.LookupGroupId(processUser.Gid) + if groupErr != nil { + panic(groupErr) + } + return processUser.Username, processGroup.Name +} + +func ExitError(e error) string { + if e != nil { + switch v := e.(type) { + case *exec.ExitError: + return string(v.Stderr) + default: + return e.Error() + } + } + return "" +} + func TestNewResource(t *testing.T) { resourceUri := "file://foo" testFile := NewResource(resourceUri) diff --git a/internal/resource/schema_test.go b/internal/resource/schema_test.go index ef61418..031f234 100644 --- a/internal/resource/schema_test.go +++ b/internal/resource/schema_test.go @@ -33,14 +33,17 @@ func TestSchemaValidateJSON(t *testing.T) { file, _ := filepath.Abs(filepath.Join(TempDir, "fooread.txt")) + expectedAtime, atimeErr := time.Parse(time.RFC3339Nano, "2001-12-15T01:01:01.000000001Z") + assert.Nil(t, atimeErr) + expectedTime := expectedAtime.Local().Format(time.RFC3339Nano) declarationAttributes := ` path: "%s" - owner: "nobody" - group: "nobody" + owner: "%s" + group: "%s" mode: "0600" - atime: 2001-12-15T01:01:01.000000001Z + atime: %s ctime: %s - mtime: 2001-12-15T01:01:01.000000001Z + mtime: %s content: |- test line 1 test line 2 @@ -50,11 +53,11 @@ func TestSchemaValidateJSON(t *testing.T) { state: present ` - decl := fmt.Sprintf(declarationAttributes, file, "2001-12-15T01:01:01.000000001Z") + decl := fmt.Sprintf(declarationAttributes, file, ProcessTestUserName, ProcessTestGroupName, expectedTime, expectedTime, expectedTime) testFile := NewFile() e := testFile.LoadDecl(decl) - assert.Equal(t, nil, e) + assert.Nil(t, e) fileApplyErr := testFile.Apply() assert.Nil(t, fileApplyErr) @@ -65,12 +68,12 @@ func TestSchemaValidateJSON(t *testing.T) { assert.Nil(t, schemaErr) f := NewFile() - assert.NotEqual(t, nil, f) + assert.NotNil(t, f) f.Path = file r, e := f.Read(ctx) - assert.Equal(t, nil, e) - assert.Equal(t, "nobody", f.Owner) + assert.Nil(t, e) + assert.Equal(t, ProcessTestUserName, f.Owner) info, statErr := os.Stat(file) assert.Nil(t, statErr) @@ -78,7 +81,7 @@ func TestSchemaValidateJSON(t *testing.T) { assert.True(t, ok) cTime := time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec)) - expected := fmt.Sprintf(declarationAttributes, file, cTime.Format(time.RFC3339Nano)) + expected := fmt.Sprintf(declarationAttributes, file, ProcessTestUserName, ProcessTestGroupName, expectedTime, cTime.Local().Format(time.RFC3339Nano), expectedTime) assert.YAMLEq(t, expected, string(r)) } diff --git a/internal/resource/schemas/container-image-declaration.schema.json b/internal/resource/schemas/container-image-declaration.schema.json new file mode 100644 index 0000000..3e5262e --- /dev/null +++ b/internal/resource/schemas/container-image-declaration.schema.json @@ -0,0 +1,20 @@ +{ + "$id": "container-image-declaration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "declaration", + "type": "object", + "required": [ "type", "attributes" ], + "properties": { + "type": { + "type": "string", + "description": "Resource type name.", + "enum": [ "container-image" ] + }, + "transition": { + "$ref": "storagetransition.schema.json" + }, + "attributes": { + "$ref": "container-image.schema.json" + } + } +} diff --git a/internal/resource/schemas/container-image.schema.json b/internal/resource/schemas/container-image.schema.json new file mode 100644 index 0000000..7469982 --- /dev/null +++ b/internal/resource/schemas/container-image.schema.json @@ -0,0 +1,14 @@ +{ + "$id": "container-image.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "container-image", + "description": "A docker container image", + "type": "object", + "required": [ "name" ], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z]([-_a-z0-9:]{0,31})$" + } + } +} diff --git a/internal/resource/schemas/container_network-declaration.jsonschema b/internal/resource/schemas/container-network-declaration.schema.json similarity index 67% rename from internal/resource/schemas/container_network-declaration.jsonschema rename to internal/resource/schemas/container-network-declaration.schema.json index c04aa90..532fa49 100644 --- a/internal/resource/schemas/container_network-declaration.jsonschema +++ b/internal/resource/schemas/container-network-declaration.schema.json @@ -1,5 +1,5 @@ { - "$id": "container_network-declaration.jsonschema", + "$id": "container-network-declaration.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "title": "declaration", "type": "object", @@ -8,10 +8,10 @@ "type": { "type": "string", "description": "Resource type name.", - "enum": [ "container_network" ] + "enum": [ "container-network" ] }, "attributes": { - "$ref": "container_network.jsonschema" + "$ref": "container-network.schema.json" } } } diff --git a/internal/resource/schemas/container_network.jsonschema b/internal/resource/schemas/container-network.schema.json similarity index 78% rename from internal/resource/schemas/container_network.jsonschema rename to internal/resource/schemas/container-network.schema.json index 59941bc..91f7e60 100644 --- a/internal/resource/schemas/container_network.jsonschema +++ b/internal/resource/schemas/container-network.schema.json @@ -1,7 +1,7 @@ { - "$id": "container_network.jsonschema", + "$id": "container-network.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", - "title": "container_network", + "title": "container-network", "description": "A docker container network", "type": "object", "required": [ "name" ], diff --git a/internal/resource/schemas/document.jsonschema b/internal/resource/schemas/document.jsonschema index 86057f2..ae26641 100644 --- a/internal/resource/schemas/document.jsonschema +++ b/internal/resource/schemas/document.jsonschema @@ -15,10 +15,11 @@ { "$ref": "http-declaration.jsonschema" }, { "$ref": "user-declaration.jsonschema" }, { "$ref": "exec-declaration.jsonschema" }, - { "$ref": "network_route-declaration.jsonschema" }, + { "$ref": "network-route-declaration.schema.json" }, { "$ref": "iptable-declaration.jsonschema" }, { "$ref": "container-declaration.jsonschema" }, - { "$ref": "container_network-declaration.jsonschema" } + { "$ref": "container-network-declaration.schema.json" }, + { "$ref": "container-image-declaration.schema.json" } ] } } diff --git a/internal/resource/schemas/network_route-declaration.jsonschema b/internal/resource/schemas/network-route-declaration.schema.json similarity index 61% rename from internal/resource/schemas/network_route-declaration.jsonschema rename to internal/resource/schemas/network-route-declaration.schema.json index 1e2a81b..910bfe5 100644 --- a/internal/resource/schemas/network_route-declaration.jsonschema +++ b/internal/resource/schemas/network-route-declaration.schema.json @@ -1,17 +1,17 @@ { - "$id": "network_route-declaration.jsonschema", + "$id": "network-route-declaration.jsonschema", "$schema": "http://json-schema.org/draft-07/schema#", - "title": "network_route-declaration", + "title": "network-route-declaration", "type": "object", "required": [ "type", "attributes" ], "properties": { "type": { "type": "string", "description": "Resource type name.", - "enum": [ "network_route" ] + "enum": [ "route" ] }, "attributes": { - "$ref": "network_route.jsonschema" + "$ref": "network-route.schema.json" } } } diff --git a/internal/resource/schemas/network_route.jsonschema b/internal/resource/schemas/network-route.schema.json similarity index 94% rename from internal/resource/schemas/network_route.jsonschema rename to internal/resource/schemas/network-route.schema.json index 828389d..035d329 100644 --- a/internal/resource/schemas/network_route.jsonschema +++ b/internal/resource/schemas/network-route.schema.json @@ -1,7 +1,7 @@ { - "$id": "network_route.jsonschema", + "$id": "network-route.schema.json", "$schema": "http://json-schema.org/draft-07/schema#", - "title": "network_route", + "title": "network-route", "type": "object", "required": [ "to", "gateway", "interface", "rtid", "metric", "type", "scope" ], "properties": { diff --git a/internal/resource/schemas/processtransition.schema.json b/internal/resource/schemas/processtransition.schema.json index 9e3ed49..f230066 100644 --- a/internal/resource/schemas/processtransition.schema.json +++ b/internal/resource/schemas/processtransition.schema.json @@ -4,6 +4,6 @@ "title": "processtransition", "type": "string", "description": "Process state transition", - "enum": [ "created", "restarting", "running", "paused", "exited", "dead" ] + "enum": [ "absent", "start_create", "present", "created", "restarting", "running", "paused", "exited", "dead", "start_delete", "start_read", "start_update" ] } diff --git a/internal/resource/schemas/storagetransition.schema.json b/internal/resource/schemas/storagetransition.schema.json index 80fe749..79fe4fb 100644 --- a/internal/resource/schemas/storagetransition.schema.json +++ b/internal/resource/schemas/storagetransition.schema.json @@ -4,7 +4,7 @@ "title": "storagetransition", "type": "string", "description": "Storage state transition", - "enum": [ "absent", "present" ] + "enum": [ "absent", "present", "create", "read", "update", "delete" ] } } diff --git a/internal/resource/types.go b/internal/resource/types.go index 4b1a57f..e22c3d0 100644 --- a/internal/resource/types.go +++ b/internal/resource/types.go @@ -33,7 +33,7 @@ func (t *Types) Register(name string, factory TypeFactory) { func (t *Types) New(uri string) (Resource, error) { u, e := url.Parse(uri) if u == nil || e != nil { - return nil, fmt.Errorf("%w: %s", ErrUnknownResourceType, e) + return nil, fmt.Errorf("%w: %s - uri %s", ErrUnknownResourceType, e, uri) } if r, ok := t.registry[u.Scheme]; ok { diff --git a/internal/source/dir.go b/internal/source/dir.go index 2da88fe..2810a02 100644 --- a/internal/source/dir.go +++ b/internal/source/dir.go @@ -71,6 +71,7 @@ func (d *Dir) ExtractDirectory(path string) (*resource.Document, error) { return document, readErr } f.Content = string(readFileData) + f.UpdateContentAttributes() } document.AddResourceDeclaration("file", f) diff --git a/internal/source/docsource.go b/internal/source/docsource.go index 06ee55b..bf715a0 100644 --- a/internal/source/docsource.go +++ b/internal/source/docsource.go @@ -7,17 +7,17 @@ _ "context" _ "encoding/json" _ "fmt" _ "gopkg.in/yaml.v3" - "net/url" - "regexp" +_ "net/url" +_ "regexp" _ "strings" - "os" - "io" - "compress/gzip" - "archive/tar" - "errors" - "path/filepath" +_ "os" +_ "io" +_ "compress/gzip" +_ "archive/tar" +_ "errors" +_ "path/filepath" "decl/internal/resource" - "decl/internal/codec" +_ "decl/internal/codec" ) type ResourceSelector func(r resource.Resource) bool @@ -35,88 +35,3 @@ func NewDocSource(uri string) DocSource { } return nil } - -func ExtractResources(uri string, filter ResourceSelector) ([]*resource.Document, error) { - documents := make([]*resource.Document, 0, 100) - d := resource.NewDocument() - documents = append(documents, d) - - TarGzipFileName := regexp.MustCompile(`^.*\.(tar\.gz|tgz)$`) - TarFileName := regexp.MustCompile(`^.*\.tar$`) - - u,e := url.Parse(uri) - if e != nil { - return nil, e - } - switch u.Scheme { - case "file": - fileAbsolutePath, _ := filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI())) - file, fileErr := os.Open(fileAbsolutePath) - if fileErr != nil { - return documents, fileErr - } - var gzipReader io.Reader - switch u.Path { - case TarGzipFileName.FindString(u.Path): - zr, err := gzip.NewReader(file) - if err != nil { - return documents, err - } - gzipReader = zr - fallthrough - case TarFileName.FindString(u.Path): - var fileReader io.Reader - if gzipReader == nil { - fileReader = file - } else { - fileReader = gzipReader - } - tarReader := tar.NewReader(fileReader) - for { - hdr, err := tarReader.Next() - if err == io.EOF { - break - } - if err != nil { - return documents, err - } - f := resource.NewFile() - if fiErr := f.UpdateAttributesFromFileInfo(hdr.FileInfo()); fiErr != nil { - return documents, fiErr - } - readFileData, readErr := io.ReadAll(tarReader) - if readErr != nil { - return documents, readErr - } - f.Content = string(readFileData) - d.AddResourceDeclaration("file", f) - } - default: - decoder := codec.NewYAMLDecoder(file) - index := 0 - for { - doc := documents[index] - e := decoder.Decode(doc) - if errors.Is(e, io.EOF) { - if len(documents) > 1 { - documents[index] = nil - } - break - } - if e != nil { - return documents, e - } - if validationErr := doc.Validate(); validationErr != nil { - return documents, validationErr - } - if applyErr := doc.Apply(); applyErr != nil { - return documents, applyErr - } - documents = append(documents, resource.NewDocument()) - index++ - } - - } - } - return documents, nil -} diff --git a/internal/source/iptable.go b/internal/source/iptable.go index 814ada9..f498815 100644 --- a/internal/source/iptable.go +++ b/internal/source/iptable.go @@ -50,8 +50,8 @@ func (i *Iptable) ExtractResources(filter ResourceSelector) ([]*resource.Documen if exErr := cmd.Extractor(out, &iptRules); exErr != nil { return documents, exErr } + document := resource.NewDocument() for _, rule := range iptRules { - document := resource.NewDocument() if rule == nil { rule = resource.NewIptable() } @@ -59,8 +59,8 @@ func (i *Iptable) ExtractResources(filter ResourceSelector) ([]*resource.Documen rule.Chain = resource.IptableChain(i.Chain) document.AddResourceDeclaration("iptable", rule) - documents = append(documents, document) } + documents = append(documents, document) } else { slog.Info("iptable chain source ExtractResources()", "output", out, "error", err) return documents, err diff --git a/tests/mocks/container.go b/tests/mocks/container.go index f6a4cb3..d43e88e 100644 --- a/tests/mocks/container.go +++ b/tests/mocks/container.go @@ -1,52 +1,79 @@ // Copyright 2024 Matthew Rich . All rights reserved. + package mocks import ( - "context" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/docker/docker/api/types" + "context" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "io" ) type MockContainerClient struct { - InjectContainerStart func(ctx context.Context, containerID string, options container.StartOptions) error - InjectContainerCreate func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) - InjectNetworkCreate func(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) - InjectContainerList func(context.Context, container.ListOptions) ([]types.Container, error) - InjectContainerInspect func(context.Context, string) (types.ContainerJSON, error) - InjectContainerRemove func(context.Context, string, container.RemoveOptions) error - InjectClose func() error + InjectContainerStart func(ctx context.Context, containerID string, options container.StartOptions) error + InjectContainerCreate func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) + InjectNetworkCreate func(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) + InjectContainerList func(context.Context, container.ListOptions) ([]types.Container, error) + InjectContainerInspect func(context.Context, string) (types.ContainerJSON, error) + InjectContainerRemove func(context.Context, string, container.RemoveOptions) error + InjectContainerStop func(context.Context, string, container.StopOptions) error + InjectContainerWait func(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) + InjectImagePull func(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error) + InjectImageInspectWithRaw func(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) + InjectImageRemove func(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) + InjectClose func() error +} + +func (m *MockContainerClient) ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) { + return m.InjectContainerWait(ctx, containerID, condition) +} + +func (m *MockContainerClient) ImageRemove(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { + return m.InjectImageRemove(ctx, imageID, options) +} + +func (m *MockContainerClient) ImagePull(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error) { + return m.InjectImagePull(ctx, refStr, options) +} + +func (m *MockContainerClient) ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) { + return m.InjectImageInspectWithRaw(ctx, imageID) } func (m *MockContainerClient) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) { - return m.InjectContainerCreate(ctx, config, hostConfig, networkingConfig, platform, containerName) + return m.InjectContainerCreate(ctx, config, hostConfig, networkingConfig, platform, containerName) } func (m *MockContainerClient) ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error { - if m.InjectContainerStart == nil { - return nil - } - return m.InjectContainerStart(ctx, containerID, options) + if m.InjectContainerStart == nil { + return nil + } + return m.InjectContainerStart(ctx, containerID, options) } func (m *MockContainerClient) ContainerList(ctx context.Context, options container.ListOptions) ([]types.Container, error) { - return m.InjectContainerList(ctx, options) + return m.InjectContainerList(ctx, options) } func (m *MockContainerClient) ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error) { - return m.InjectContainerInspect(ctx, containerID) + return m.InjectContainerInspect(ctx, containerID) } func (m *MockContainerClient) ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error { - return m.InjectContainerRemove(ctx, containerID, options) + return m.InjectContainerRemove(ctx, containerID, options) +} + +func (m *MockContainerClient) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error { + return m.InjectContainerStop(ctx, containerID, options) } func (m *MockContainerClient) Close() error { - if m.InjectClose == nil { - return nil - } - return m.InjectClose() + if m.InjectClose == nil { + return nil + } + return m.InjectClose() } func (m *MockContainerClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {