add container-image resource
Some checks failed
Lint / golangci-lint (push) Failing after 9m55s
Declarative Tests / test (push) Failing after 5s

This commit is contained in:
Matthew Rich 2024-05-23 22:11:51 -07:00
parent 50020e7a44
commit 0888ae2045
35 changed files with 1066 additions and 258 deletions

View File

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

5
examples/fedora.jx.yaml Normal file
View File

@ -0,0 +1,5 @@
resources:
- type: container-image
transition: read
attributes:
name: "fedora:latest"

5
go.mod
View File

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

16
go.sum
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 := `

View File

@ -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
}
return nil
}
result = stater.Trigger("read")
}
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))
}
}
}
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,6 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
"title": "storagetransition",
"type": "string",
"description": "Storage state transition",
"enum": [ "absent", "present" ]
"enum": [ "absent", "present", "create", "read", "update", "delete" ]
}
}

View File

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

View File

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

View File

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

View File

@ -50,8 +50,8 @@ func (i *Iptable) ExtractResources(filter ResourceSelector) ([]*resource.Documen
if exErr := cmd.Extractor(out, &iptRules); exErr != nil {
return documents, exErr
}
for _, rule := range iptRules {
document := resource.NewDocument()
for _, rule := range iptRules {
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

View File

@ -1,12 +1,14 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package mocks
import (
"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"
"github.com/docker/docker/api/types"
"io"
)
type MockContainerClient struct {
@ -16,9 +18,30 @@ type MockContainerClient struct {
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)
}
@ -42,6 +65,10 @@ func (m *MockContainerClient) ContainerRemove(ctx context.Context, containerID s
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