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 ImportMerge *bool
var ImportResource *string var ImportResource *string
var ApplyDelete *bool
var ctx context.Context = context.Background() var ctx context.Context = context.Background()
@ -72,7 +74,7 @@ func LoggerConfig() {
var programLevel = new(slog.LevelVar) var programLevel = new(slog.LevelVar)
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel})) logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel}))
slog.SetDefault(logger) 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) programLevel.Set(slog.LevelDebug)
} else { } else {
programLevel.Set(slog.LevelError) 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) { 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 { if e := cmd.Parse(os.Args[2:]); e != nil {
return e return e
} }
@ -183,8 +186,14 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
slog.Info("main.Apply()", "documents", documents) slog.Info("main.Apply()", "documents", documents)
for _,d := range documents { for _,d := range documents {
slog.Info("main.Appl()", "doc", d) slog.Info("main.Apply()", "doc", d)
if e := d.Apply(); e != nil { 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 return e
} }
@ -305,7 +314,7 @@ func main() {
} }
if os.Args[1] == subCmd.Name { if os.Args[1] == subCmd.Name {
if e := subCmd.Run(cmdFlagSet, os.Stdout); e != nil { if e := subCmd.Run(cmdFlagSet, os.Stdout); e != nil {
log.Fatal(e) slog.Error("Failed running command", "command", os.Args[1], "error", e)
} }
return 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 module decl
go 1.21.1 go 1.22.1
require ( 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/docker v25.0.5+incompatible
github.com/docker/go-connections v0.5.0 github.com/docker/go-connections v0.5.0
github.com/opencontainers/image-spec v1.1.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/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3 h1:ge74Hmzxp+bqVwSK9hOOBlZB9KeL3xuwMIXAYLPHBxA=
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/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 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-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 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 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= github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=

View File

@ -22,7 +22,7 @@ import (
_ "os" _ "os"
_ "os/exec" _ "os/exec"
"path/filepath" "path/filepath"
_ "strings" "strings"
"encoding/json" "encoding/json"
"io" "io"
"gitea.rosskeen.house/rosskeen.house/machine" "gitea.rosskeen.house/rosskeen.house/machine"
@ -35,10 +35,13 @@ type ContainerClient interface {
ContainerList(context.Context, container.ListOptions) ([]types.Container, error) ContainerList(context.Context, container.ListOptions) ([]types.Container, error)
ContainerInspect(context.Context, string) (types.ContainerJSON, error) ContainerInspect(context.Context, string) (types.ContainerJSON, error)
ContainerRemove(context.Context, string, container.RemoveOptions) 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 Close() error
} }
type Container struct { type Container struct {
stater machine.Stater `yaml:"-" json:"-"`
Id string `json:"ID,omitempty" yaml:"ID,omitempty"` Id string `json:"ID,omitempty" yaml:"ID,omitempty"`
Name string `json:"name" yaml:"name"` Name string `json:"name" yaml:"name"`
Path string `json:"path" yaml:"path"` Path string `json:"path" yaml:"path"`
@ -133,7 +136,10 @@ func (c *Container) Clone() Resource {
} }
func (c *Container) StateMachine() machine.Stater { func (c *Container) StateMachine() machine.Stater {
return ProcessMachine() if c.stater == nil {
c.stater = ProcessMachine(c)
}
return c.stater
} }
func (c *Container) URI() string { func (c *Container) URI() string {
@ -160,6 +166,58 @@ func (c *Container) Validate() error {
return fmt.Errorf("failed") 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 { func (c *Container) Apply() error {
ctx := context.Background() ctx := context.Background()
switch c.State { 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) resp, err := c.apiClient.ContainerCreate(ctx, config, &c.HostConfig, networkConfig, nil, c.Name)
if err != nil { if err != nil {
panic(err) return err
} }
c.Id = resp.ID c.Id = resp.ID
@ -252,7 +310,7 @@ func (c *Container) Read(ctx context.Context) ([]byte, error) {
}) })
if err != nil { 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 { 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 { 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{ err := c.apiClient.ContainerRemove(ctx, c.Id, container.RemoveOptions{
RemoveVolumes: true, RemoveVolumes: true,
Force: false, Force: false,
}) })
if err != nil { if err != nil {
slog.Error("Failed to remove: ", "Id", c.Id) slog.Error("Container.Delete() - failed to remove: ", "Id", c.Id, "error", err)
panic(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 return err
} }
@ -311,18 +386,24 @@ func (c *Container) ResolveId(ctx context.Context) string {
Filters: filterArgs, Filters: filterArgs,
}) })
if err != nil { if err != nil {
c.StateMachine().Trigger("notexists")
panic(fmt.Errorf("%w: %s %s", err, c.Type(), c.Name)) panic(fmt.Errorf("%w: %s %s", err, c.Type(), c.Name))
} }
slog.Info("Container.ResolveId()", "containers", containers)
for _, container := range containers { for _, container := range containers {
for _, containerName := range container.Names { for _, containerName := range container.Names {
if containerName == c.Name { if containerName == "/"+c.Name {
slog.Info("Container.ResolveId()", "state", c.StateMachine())
if c.Id == "" { if c.Id == "" {
c.Id = container.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 return container.ID
} }
} }
} }
c.StateMachine().Trigger("notexists")
return "" 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() { func init() {
ResourceTypes.Register("container_network", func(u *url.URL) Resource { ResourceTypes.Register("container-network", func(u *url.URL) Resource {
n := NewContainerNetwork(nil) n := NewContainerNetwork(nil)
n.Name = filepath.Join(u.Hostname(), u.Path) n.Name = filepath.Join(u.Hostname(), u.Path)
return n return n
@ -100,7 +100,7 @@ func (n *ContainerNetwork) Notify(m *machine.EventMessage) {
} }
func (n *ContainerNetwork) URI() string { 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 { func (n *ContainerNetwork) SetURI(uri string) error {
@ -164,7 +164,7 @@ func (n *ContainerNetwork) Delete(ctx context.Context) error {
return nil 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 { func (n *ContainerNetwork) ResolveId(ctx context.Context) string {
filterArgs := filters.NewArgs() filterArgs := filters.NewArgs()

View File

@ -48,6 +48,13 @@ func TestReadContainer(t *testing.T) {
Image: "alpine", Image: "alpine",
}}, nil }}, 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) 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) { 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 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 { InjectContainerRemove: func(context.Context, string, container.RemoveOptions) error {
return nil 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 := ` decl := `

View File

@ -9,7 +9,8 @@ import (
"io" "io"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"log/slog" "log/slog"
"gitea.rosskeen.house/rosskeen.house/machine" _ "gitea.rosskeen.house/rosskeen.house/machine"
"gitea.rosskeen.house/pylon/luaruntime"
"decl/internal/codec" "decl/internal/codec"
) )
@ -19,10 +20,10 @@ type DeclarationType struct {
} }
type Declaration struct { type Declaration struct {
StateMatchine machine.Stater `json:"-" yaml:"-"`
Type TypeName `json:"type" yaml:"type"` Type TypeName `json:"type" yaml:"type"`
Transition string `json:"transition,omitempty" yaml:"transition,omitempty"` Transition string `json:"transition,omitempty" yaml:"transition,omitempty"`
Attributes Resource `json:"attributes" yaml:"attributes"` Attributes Resource `json:"attributes" yaml:"attributes"`
runtime luaruntime.LuaRunner
} }
type ResourceLoader interface { type ResourceLoader interface {
@ -37,11 +38,24 @@ func NewDeclaration() *Declaration {
return &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 { func (d *Declaration) Clone() *Declaration {
return &Declaration { return &Declaration {
Type: d.Type, Type: d.Type,
Transition: d.Transition, Transition: d.Transition,
Attributes: d.Attributes.Clone(), Attributes: d.Attributes.Clone(),
runtime: luaruntime.New(),
} }
} }
@ -64,16 +78,34 @@ func (d *Declaration) Resource() Resource {
return d.Attributes 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() stater := d.Attributes.StateMachine()
switch d.Transition { 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: default:
fallthrough fallthrough
case "create", "present": 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 { func (d *Declaration) SetURI(uri string) error {
@ -87,19 +119,28 @@ func (d *Declaration) SetURI(uri string) error {
return e 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 { func (d *Declaration) UnmarshalYAML(value *yaml.Node) error {
t := &DeclarationType{} t := &DeclarationType{}
if unmarshalResourceTypeErr := value.Decode(t); unmarshalResourceTypeErr != nil { if unmarshalResourceTypeErr := value.Decode(t); unmarshalResourceTypeErr != nil {
return unmarshalResourceTypeErr return unmarshalResourceTypeErr
} }
d.Type = t.Type if err := d.UnmarshalValue(t); err != nil {
d.Transition = t.Transition return err
newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", d.Type))
if resourceErr != nil {
return resourceErr
} }
d.Attributes = newResource
resourceAttrs := struct { resourceAttrs := struct {
Attributes yaml.Node `json:"attributes"` Attributes yaml.Node `json:"attributes"`
}{} }{}
@ -118,18 +159,13 @@ func (d *Declaration) UnmarshalJSON(data []byte) error {
return unmarshalResourceTypeErr return unmarshalResourceTypeErr
} }
d.Type = t.Type if err := d.UnmarshalValue(t); err != nil {
d.Transition = t.Transition return err
newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", d.Type))
if resourceErr != nil {
return resourceErr
} }
d.Attributes = newResource
resourceAttrs := struct { resourceAttrs := struct {
Attributes Resource `json:"attributes"` Attributes Resource `json:"attributes"`
}{Attributes: newResource} }{Attributes: d.Attributes}
if unmarshalAttributesErr := json.Unmarshal(data, &resourceAttrs); unmarshalAttributesErr != nil { if unmarshalAttributesErr := json.Unmarshal(data, &resourceAttrs); unmarshalAttributesErr != nil {
return unmarshalAttributesErr return unmarshalAttributesErr
} }
@ -150,3 +186,39 @@ func (d *Declaration) MarshalJSON() ([]byte, error) {
func (d *Declaration) MarshalYAML() (any, error) { func (d *Declaration) MarshalYAML() (any, error) {
return d, nil 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" "github.com/sters/yaml-diff/yamldiff"
"strings" "strings"
"decl/internal/codec" "decl/internal/codec"
"context"
) )
type Document struct { type Document struct {
@ -71,15 +72,37 @@ func (d *Document) Resources() []Declaration {
return d.ResourceDecls 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 { if d == nil {
panic("Undefined Document") panic("Undefined Document")
} }
slog.Info("Document.Apply()", "declarations", d) slog.Info("Document.Apply()", "declarations", d, "override", state)
for i := range d.ResourceDecls { var start, i int = 0, 0
if e := d.ResourceDecls[i].Apply(); e != nil { 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 return e
} }
if i >= len(d.ResourceDecls) - 1 {
break
}
i++
} }
return nil return nil
} }

View File

@ -13,6 +13,7 @@ import (
"syscall" "syscall"
"testing" "testing"
"time" "time"
"os/user"
) )
func TestNewDocumentLoader(t *testing.T) { func TestNewDocumentLoader(t *testing.T) {
@ -35,8 +36,8 @@ resources:
- type: file - type: file
attributes: attributes:
path: "%s" path: "%s"
owner: "nobody" owner: "%s"
group: "nobody" group: "%s"
mode: "0600" mode: "0600"
content: |- content: |-
test line 1 test line 1
@ -50,9 +51,9 @@ resources:
home: "/home/testuser" home: "/home/testuser"
createhome: true createhome: true
state: present state: present
`, file) `, file, ProcessTestUserName, ProcessTestGroupName)
d := NewDocument() d := NewDocument()
assert.NotEqual(t, nil, d) assert.NotNil(t, d)
docReader := strings.NewReader(document) docReader := strings.NewReader(document)
@ -82,14 +83,18 @@ func TestDocumentGenerator(t *testing.T) {
aTime := time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) aTime := time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec))
cTime := time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.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(` expected := fmt.Sprintf(`
resources: resources:
- type: file - type: file
attributes: attributes:
path: %s path: %s
owner: "root" owner: "%s"
group: "root" group: "%s"
mode: "0644" mode: "0644"
content: | content: |
%s %s
@ -100,7 +105,7 @@ resources:
size: 82 size: 82
filetype: "regular" filetype: "regular"
state: present 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 var documentYaml strings.Builder
d := NewDocument() d := NewDocument()

View File

@ -18,6 +18,7 @@ _ "strings"
) )
type Exec struct { type Exec struct {
stater machine.Stater `yaml:"-" json:"-"`
Id string `yaml:"id" json:"id"` Id string `yaml:"id" json:"id"`
CreateTemplate Command `yaml:"create" json:"create"` CreateTemplate Command `yaml:"create" json:"create"`
ReadTemplate Command `yaml:"read" json:"read"` ReadTemplate Command `yaml:"read" json:"read"`
@ -51,7 +52,11 @@ func (x *Exec) Clone() Resource {
} }
func (x *Exec) StateMachine() machine.Stater { func (x *Exec) StateMachine() machine.Stater {
return ProcessMachine() if x.stater == nil {
x.stater = ProcessMachine(x)
}
return x.stater
} }
func (x *Exec) URI() string { func (x *Exec) URI() string {
@ -82,6 +87,25 @@ func (x *Exec) Apply() error {
return nil 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 { func (x *Exec) Load(r io.Reader) error {
return codec.NewYAMLDecoder(r).Decode(x) 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) Type() string { return "exec" }
func (x *Exec) Create(ctx context.Context) error {
return nil
}
func (x *Exec) Read(ctx context.Context) ([]byte, error) { func (x *Exec) Read(ctx context.Context) ([]byte, error) {
return yaml.Marshal(x) return yaml.Marshal(x)
} }

View File

@ -119,14 +119,40 @@ func (f *File) Notify(m *machine.EventMessage) {
switch m.On { switch m.On {
case machine.ENTERSTATEEVENT: case machine.ENTERSTATEEVENT:
switch m.Dest { 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": case "start_create":
if e := f.Create(ctx); e == nil { if e := f.Create(ctx); e == nil {
if triggerErr := f.stater.Trigger("created"); triggerErr == nil { if triggerErr := f.StateMachine().Trigger("created"); triggerErr == nil {
return return
} }
} }
f.State = "absent" 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" f.State = "present"
} }
case machine.EXITSTATEEVENT: case machine.EXITSTATEEVENT:
@ -157,14 +183,12 @@ func (f *File) Validate() error {
} }
func (f *File) Apply() error { func (f *File) Apply() error {
ctx := context.Background()
switch f.State { switch f.State {
case "absent": case "absent":
removeErr := os.Remove(f.Path) return f.Delete(ctx)
if removeErr != nil {
return removeErr
}
case "present": case "present":
return f.Create(context.Background()) return f.Create(ctx)
} }
return nil return nil
@ -285,6 +309,10 @@ func (f *File) Create(ctx context.Context) error {
return nil return nil
} }
func (f *File) Delete(ctx context.Context) error {
return os.Remove(f.Path)
}
func (f *File) UpdateContentAttributes() { func (f *File) UpdateContentAttributes() {
f.Size = int64(len(f.Content)) f.Size = int64(len(f.Content))
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256([]byte(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() ctx := context.Background()
file, _ := filepath.Abs(filepath.Join(TempDir, "fooread.txt")) 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 := ` declarationAttributes := `
path: "%s" path: "%s"
owner: "nobody" owner: "%s"
group: "nobody" group: "%s"
mode: "0600" mode: "0600"
atime: 2001-12-15T01:01:01.000000001Z atime: %s
ctime: %s ctime: %s
mtime: 2001-12-15T01:01:01.000000001Z mtime: %s
content: |- content: |-
test line 1 test line 1
test line 2 test line 2
@ -70,11 +74,11 @@ func TestReadFile(t *testing.T) {
state: present 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() testFile := NewFile()
e := testFile.LoadDecl(decl) e := testFile.LoadDecl(decl)
assert.Equal(t, nil, e) assert.Nil(t, e)
applyErr := testFile.Apply() applyErr := testFile.Apply()
assert.Nil(t, applyErr) assert.Nil(t, applyErr)
@ -83,8 +87,8 @@ func TestReadFile(t *testing.T) {
f.Path = file f.Path = file
r, e := f.Read(ctx) r, e := f.Read(ctx)
assert.Equal(t, nil, e) assert.Nil(t, e)
assert.Equal(t, "nobody", f.Owner) assert.Equal(t, ProcessTestUserName, f.Owner)
info, statErr := os.Stat(file) info, statErr := os.Stat(file)
assert.Nil(t, statErr) assert.Nil(t, statErr)
@ -92,7 +96,7 @@ func TestReadFile(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
cTime := time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec)) 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)) assert.YAMLEq(t, expected, string(r))
} }
@ -113,19 +117,19 @@ func TestCreateFile(t *testing.T) {
decl := fmt.Sprintf(` decl := fmt.Sprintf(`
path: "%s" path: "%s"
owner: "nobody" owner: "%s"
group: "nobody" group: "%s"
mode: "0600" mode: "0600"
content: |- content: |-
test line 1 test line 1
test line 2 test line 2
state: present state: present
`, file) `, file, ProcessTestUserName, ProcessTestGroupName)
f := NewFile() f := NewFile()
e := f.LoadDecl(decl) e := f.LoadDecl(decl)
assert.Equal(t, nil, e) assert.Equal(t, nil, e)
assert.Equal(t, "nobody", f.Owner) assert.Equal(t, ProcessTestUserName, f.Owner)
applyErr := f.Apply() applyErr := f.Apply()
assert.Equal(t, nil, applyErr) assert.Equal(t, nil, applyErr)
@ -155,17 +159,17 @@ func TestFileDirectory(t *testing.T) {
decl := fmt.Sprintf(` decl := fmt.Sprintf(`
path: "%s" path: "%s"
owner: "nobody" owner: "%s"
group: "nobody" group: "%s"
mode: "0700" mode: "0700"
filetype: "directory" filetype: "directory"
state: present state: present
`, file) `, file, ProcessTestUserName, ProcessTestGroupName)
f := NewFile() f := NewFile()
e := f.LoadDecl(decl) e := f.LoadDecl(decl)
assert.Equal(t, nil, e) assert.Equal(t, nil, e)
assert.Equal(t, "nobody", f.Owner) assert.Equal(t, ProcessTestUserName, f.Owner)
applyErr := f.Apply() applyErr := f.Apply()
assert.Equal(t, nil, applyErr) assert.Equal(t, nil, applyErr)
@ -181,13 +185,13 @@ func TestFileTimes(t *testing.T) {
file, _ := filepath.Abs(filepath.Join(TempDir, "testtimes.txt")) file, _ := filepath.Abs(filepath.Join(TempDir, "testtimes.txt"))
decl := fmt.Sprintf(` decl := fmt.Sprintf(`
path: "%s" path: "%s"
owner: "nobody" owner: "%s"
group: "nobody" group: "%s"
mtime: 2001-12-15T01:01:01.1Z mtime: 2001-12-15T01:01:01.1Z
mode: "0600" mode: "0600"
filtetype: "regular" filtetype: "regular"
state: "present" state: "present"
`, file) `, file, ProcessTestUserName, ProcessTestGroupName)
expectedTime, timeErr := time.Parse(time.RFC3339, "2001-12-15T01:01:01.1Z") expectedTime, timeErr := time.Parse(time.RFC3339, "2001-12-15T01:01:01.1Z")
assert.Nil(t, timeErr) assert.Nil(t, timeErr)
@ -195,7 +199,7 @@ func TestFileTimes(t *testing.T) {
f := NewFile() f := NewFile()
e := f.LoadDecl(decl) e := f.LoadDecl(decl)
assert.Nil(t, e) assert.Nil(t, e)
assert.Equal(t, "nobody", f.Owner) assert.Equal(t, ProcessTestUserName, f.Owner)
assert.True(t, f.Mtime.Equal(expectedTime)) assert.True(t, f.Mtime.Equal(expectedTime))
} }
@ -252,8 +256,8 @@ func TestFileReadStat(t *testing.T) {
statErr := f.ReadStat() statErr := f.ReadStat()
assert.Error(t, statErr) assert.Error(t, statErr)
f.Owner = "nobody" f.Owner = ProcessTestUserName
f.Group = "nobody" f.Group = ProcessTestGroupName
f.State = "present" f.State = "present"
assert.Nil(t, f.Apply()) assert.Nil(t, f.Apply())
@ -332,28 +336,27 @@ func TestFileClone(t *testing.T) {
} }
func TestFileErrors(t *testing.T) { func TestFileErrors(t *testing.T) {
ctx := context.Background() //ctx := context.Background()
testFile := filepath.Join(TempDir, "testerr.txt") testFile := filepath.Join(TempDir, "testerr.txt")
f := NewFile() f := NewFile()
assert.NotNil(t, f) assert.NotNil(t, f)
stater := f.StateMachine()
f.Path = testFile f.Path = testFile
f.Mode = "631" f.Mode = "631"
f.State = "present" assert.Nil(t, stater.Trigger("create"))
assert.Nil(t, f.Apply())
read := NewFile() read := NewFile()
readStater := read.StateMachine()
read.Path = testFile read.Path = testFile
_, readErr := read.Read(ctx) assert.Nil(t, readStater.Trigger("read"))
assert.Nil(t, readErr)
assert.Equal(t, "0631", read.Mode) assert.Equal(t, "0631", read.Mode)
f.Mode = "900" 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, readStater.Trigger("read"))
assert.Nil(t, updateReadErr)
assert.Equal(t, "0631", read.Mode) assert.Equal(t, "0631", read.Mode)
f.Mode = "0631" f.Mode = "0631"
@ -363,3 +366,34 @@ func TestFileErrors(t *testing.T) {
assert.Error(t, uidErr, UnknownUser) 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" "gopkg.in/yaml.v3"
"io" "io"
"net/url" "net/url"
"os/exec"
"regexp" "regexp"
_ "strconv" _ "strconv"
"strings" "strings"
@ -119,15 +118,23 @@ type NetworkRoute struct {
RouteType NetworkRouteType `json:"routetype" yaml:"routetype"` RouteType NetworkRouteType `json:"routetype" yaml:"routetype"`
Scope NetworkRouteScope `json:"scope" yaml:"scope"` Scope NetworkRouteScope `json:"scope" yaml:"scope"`
Proto NetworkRouteProto `json:"proto" yaml:"proto"` 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"` State string `json:"state" yaml:"state"`
} }
func NewNetworkRoute() *NetworkRoute { 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 { func (n *NetworkRoute) Clone() Resource {
return &NetworkRoute { newn := &NetworkRoute {
Id: n.Id, Id: n.Id,
To: n.To, To: n.To,
Interface: n.Interface, Interface: n.Interface,
@ -139,6 +146,8 @@ func (n *NetworkRoute) Clone() Resource {
Proto: n.Proto, Proto: n.Proto,
State: n.State, State: n.State,
} }
newn.CreateCommand, newn.ReadCommand, newn.UpdateCommand, newn.DeleteCommand = n.NewCRUD()
return newn
} }
func (n *NetworkRoute) StateMachine() machine.Stater { 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 { 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 return nil
} }
@ -184,7 +199,6 @@ func (n *NetworkRoute) Validate() error {
} }
func (n *NetworkRoute) Apply() error { func (n *NetworkRoute) Apply() error {
switch n.State { switch n.State {
case "absent": case "absent":
case "present": case "present":
@ -209,6 +223,16 @@ func (n *NetworkRoute) ResolveId(ctx context.Context) string {
} }
func (n *NetworkRoute) Read(ctx context.Context) ([]byte, error) { 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) var cmdArgs []string = make([]string, 17)
cmdArgs[0] = "route" cmdArgs[0] = "route"
cmdArgs[1] = "show" cmdArgs[1] = "show"
@ -249,6 +273,7 @@ func (n *NetworkRoute) Read(ctx context.Context) ([]byte, error) {
} }
} }
} }
*/
n.ResolveId(ctx) n.ResolveId(ctx)
return yaml.Marshal(n) return yaml.Marshal(n)
} }
@ -410,3 +435,75 @@ func (n *NetworkRoute) UnmarshalJSON(data []byte) error {
} }
return nil 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

@ -39,7 +39,6 @@ func TestReadNetworkRoute(t *testing.T) {
declarationAttributes := ` declarationAttributes := `
to: "192.168.0.0/24" to: "192.168.0.0/24"
interface: "eth0"
gateway: "192.168.0.1" gateway: "192.168.0.1"
metric: 0 metric: 0
routetype: "unicast" routetype: "unicast"
@ -49,12 +48,13 @@ func TestReadNetworkRoute(t *testing.T) {
testRoute := NewNetworkRoute() testRoute := NewNetworkRoute()
e := testRoute.LoadDecl(declarationAttributes) e := testRoute.LoadDecl(declarationAttributes)
assert.Equal(t, nil, e) assert.Nil(t, e)
testRouteErr := testRoute.Apply() testRouteErr := testRoute.Apply()
assert.Nil(t, testRouteErr) assert.Nil(t, testRouteErr)
r, e := testRoute.Read(ctx) r, e := testRoute.Read(ctx)
assert.Nil(t, e) assert.Equal(t, "", ExitError(e))
assert.NotNil(t, r) assert.NotNil(t, r)
assert.Equal(t, NetworkRouteType("unicast"), testRoute.RouteType) assert.Equal(t, NetworkRouteType("unicast"), testRoute.RouteType)
} }

View File

@ -1,4 +1,6 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved. // Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource package resource
import ( import (
@ -25,10 +27,10 @@ func TestLookupUID(t *testing.T) {
} }
func TestLookupGID(t *testing.T) { func TestLookupGID(t *testing.T) {
gid, e := LookupGID("nobody") gid, e := LookupGID("adm")
assert.Nil(t, e) assert.Nil(t, e)
assert.Equal(t, 65534, gid) assert.Equal(t, 4, gid)
ngid, ne := LookupGID("1001") ngid, ne := LookupGID("1001")
assert.Nil(t, ne) assert.Nil(t, ne)

View File

@ -40,16 +40,23 @@ type ResourceReader interface {
} }
type ResourceUpdater interface { type ResourceUpdater interface {
Update() error Update(context.Context) error
} }
type ResourceDeleter interface { type ResourceDeleter interface {
Delete() error Delete(context.Context) error
} }
type ResourceDecoder struct { type ResourceDecoder struct {
} }
type ResourceCrudder struct {
ResourceCreator
ResourceReader
ResourceUpdater
ResourceDeleter
}
func NewResource(uri string) Resource { func NewResource(uri string) Resource {
r, e := ResourceTypes.New(uri) r, e := ResourceTypes.New(uri)
if e == nil { if e == nil {

View File

@ -7,12 +7,17 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"log" "log"
"os" "os"
"os/user"
"os/exec"
"path/filepath" "path/filepath"
"testing" "testing"
) )
var TempDir string var TempDir string
var ProcessTestUserName string
var ProcessTestGroupName string
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
var err error var err error
TempDir, err = os.MkdirTemp("", "testresourcefile") TempDir, err = os.MkdirTemp("", "testresourcefile")
@ -20,12 +25,37 @@ func TestMain(m *testing.M) {
log.Fatal(err) log.Fatal(err)
} }
ProcessTestUserName, ProcessTestGroupName = ProcessUserName()
rc := m.Run() rc := m.Run()
os.RemoveAll(TempDir) os.RemoveAll(TempDir)
os.Exit(rc) 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) { func TestNewResource(t *testing.T) {
resourceUri := "file://foo" resourceUri := "file://foo"
testFile := NewResource(resourceUri) testFile := NewResource(resourceUri)

View File

@ -33,14 +33,17 @@ func TestSchemaValidateJSON(t *testing.T) {
file, _ := filepath.Abs(filepath.Join(TempDir, "fooread.txt")) 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 := ` declarationAttributes := `
path: "%s" path: "%s"
owner: "nobody" owner: "%s"
group: "nobody" group: "%s"
mode: "0600" mode: "0600"
atime: 2001-12-15T01:01:01.000000001Z atime: %s
ctime: %s ctime: %s
mtime: 2001-12-15T01:01:01.000000001Z mtime: %s
content: |- content: |-
test line 1 test line 1
test line 2 test line 2
@ -50,11 +53,11 @@ func TestSchemaValidateJSON(t *testing.T) {
state: present 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() testFile := NewFile()
e := testFile.LoadDecl(decl) e := testFile.LoadDecl(decl)
assert.Equal(t, nil, e) assert.Nil(t, e)
fileApplyErr := testFile.Apply() fileApplyErr := testFile.Apply()
assert.Nil(t, fileApplyErr) assert.Nil(t, fileApplyErr)
@ -65,12 +68,12 @@ func TestSchemaValidateJSON(t *testing.T) {
assert.Nil(t, schemaErr) assert.Nil(t, schemaErr)
f := NewFile() f := NewFile()
assert.NotEqual(t, nil, f) assert.NotNil(t, f)
f.Path = file f.Path = file
r, e := f.Read(ctx) r, e := f.Read(ctx)
assert.Equal(t, nil, e) assert.Nil(t, e)
assert.Equal(t, "nobody", f.Owner) assert.Equal(t, ProcessTestUserName, f.Owner)
info, statErr := os.Stat(file) info, statErr := os.Stat(file)
assert.Nil(t, statErr) assert.Nil(t, statErr)
@ -78,7 +81,7 @@ func TestSchemaValidateJSON(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
cTime := time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec)) 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)) 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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "declaration", "title": "declaration",
"type": "object", "type": "object",
@ -8,10 +8,10 @@
"type": { "type": {
"type": "string", "type": "string",
"description": "Resource type name.", "description": "Resource type name.",
"enum": [ "container_network" ] "enum": [ "container-network" ]
}, },
"attributes": { "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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "container_network", "title": "container-network",
"description": "A docker container network", "description": "A docker container network",
"type": "object", "type": "object",
"required": [ "name" ], "required": [ "name" ],

View File

@ -15,10 +15,11 @@
{ "$ref": "http-declaration.jsonschema" }, { "$ref": "http-declaration.jsonschema" },
{ "$ref": "user-declaration.jsonschema" }, { "$ref": "user-declaration.jsonschema" },
{ "$ref": "exec-declaration.jsonschema" }, { "$ref": "exec-declaration.jsonschema" },
{ "$ref": "network_route-declaration.jsonschema" }, { "$ref": "network-route-declaration.schema.json" },
{ "$ref": "iptable-declaration.jsonschema" }, { "$ref": "iptable-declaration.jsonschema" },
{ "$ref": "container-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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "network_route-declaration", "title": "network-route-declaration",
"type": "object", "type": "object",
"required": [ "type", "attributes" ], "required": [ "type", "attributes" ],
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string",
"description": "Resource type name.", "description": "Resource type name.",
"enum": [ "network_route" ] "enum": [ "route" ]
}, },
"attributes": { "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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "network_route", "title": "network-route",
"type": "object", "type": "object",
"required": [ "to", "gateway", "interface", "rtid", "metric", "type", "scope" ], "required": [ "to", "gateway", "interface", "rtid", "metric", "type", "scope" ],
"properties": { "properties": {

View File

@ -4,6 +4,6 @@
"title": "processtransition", "title": "processtransition",
"type": "string", "type": "string",
"description": "Process state transition", "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", "title": "storagetransition",
"type": "string", "type": "string",
"description": "Storage state transition", "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) { func (t *Types) New(uri string) (Resource, error) {
u, e := url.Parse(uri) u, e := url.Parse(uri)
if u == nil || e != nil { 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 { 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 return document, readErr
} }
f.Content = string(readFileData) f.Content = string(readFileData)
f.UpdateContentAttributes()
} }
document.AddResourceDeclaration("file", f) document.AddResourceDeclaration("file", f)

View File

@ -7,17 +7,17 @@ _ "context"
_ "encoding/json" _ "encoding/json"
_ "fmt" _ "fmt"
_ "gopkg.in/yaml.v3" _ "gopkg.in/yaml.v3"
"net/url" _ "net/url"
"regexp" _ "regexp"
_ "strings" _ "strings"
"os" _ "os"
"io" _ "io"
"compress/gzip" _ "compress/gzip"
"archive/tar" _ "archive/tar"
"errors" _ "errors"
"path/filepath" _ "path/filepath"
"decl/internal/resource" "decl/internal/resource"
"decl/internal/codec" _ "decl/internal/codec"
) )
type ResourceSelector func(r resource.Resource) bool type ResourceSelector func(r resource.Resource) bool
@ -35,88 +35,3 @@ func NewDocSource(uri string) DocSource {
} }
return nil 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 { if exErr := cmd.Extractor(out, &iptRules); exErr != nil {
return documents, exErr return documents, exErr
} }
for _, rule := range iptRules {
document := resource.NewDocument() document := resource.NewDocument()
for _, rule := range iptRules {
if rule == nil { if rule == nil {
rule = resource.NewIptable() rule = resource.NewIptable()
} }
@ -59,8 +59,8 @@ func (i *Iptable) ExtractResources(filter ResourceSelector) ([]*resource.Documen
rule.Chain = resource.IptableChain(i.Chain) rule.Chain = resource.IptableChain(i.Chain)
document.AddResourceDeclaration("iptable", rule) document.AddResourceDeclaration("iptable", rule)
documents = append(documents, document)
} }
documents = append(documents, document)
} else { } else {
slog.Info("iptable chain source ExtractResources()", "output", out, "error", err) slog.Info("iptable chain source ExtractResources()", "output", out, "error", err)
return documents, err return documents, err

View File

@ -1,12 +1,14 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved. // Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package mocks package mocks
import ( import (
"context" "context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/docker/docker/api/types" "io"
) )
type MockContainerClient struct { type MockContainerClient struct {
@ -16,9 +18,30 @@ type MockContainerClient struct {
InjectContainerList func(context.Context, container.ListOptions) ([]types.Container, error) InjectContainerList func(context.Context, container.ListOptions) ([]types.Container, error)
InjectContainerInspect func(context.Context, string) (types.ContainerJSON, error) InjectContainerInspect func(context.Context, string) (types.ContainerJSON, error)
InjectContainerRemove func(context.Context, string, container.RemoveOptions) 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 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) { 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)
} }
@ -42,6 +65,10 @@ func (m *MockContainerClient) ContainerRemove(ctx context.Context, containerID s
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 { func (m *MockContainerClient) Close() error {
if m.InjectClose == nil { if m.InjectClose == nil {
return nil return nil