add container-image resource
This commit is contained in:
parent
50020e7a44
commit
0888ae2045
@ -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
5
examples/fedora.jx.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
resources:
|
||||
- type: container-image
|
||||
transition: read
|
||||
attributes:
|
||||
name: "fedora:latest"
|
5
go.mod
5
go.mod
@ -1,9 +1,10 @@
|
||||
module decl
|
||||
|
||||
go 1.21.1
|
||||
go 1.22.1
|
||||
|
||||
require (
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240507055918-d126067c56d0
|
||||
gitea.rosskeen.house/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520193117-1835255b6d02
|
||||
github.com/docker/docker v25.0.5+incompatible
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
|
16
go.sum
16
go.sum
@ -1,7 +1,19 @@
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240507054129-2b3068fcd02c h1:XjmoAauFsu6f7Xliqvf2Gn/TYCLOq8nUcPG5n0nGeWQ=
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240507054129-2b3068fcd02c/go.mod h1:5J2OFjFIBaCfsjcC9kSyycbIL8g/qAJH2A8BnbIig+Y=
|
||||
gitea.rosskeen.house/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3 h1:ge74Hmzxp+bqVwSK9hOOBlZB9KeL3xuwMIXAYLPHBxA=
|
||||
gitea.rosskeen.house/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3/go.mod h1:9sKIXsGDcf1uBnHhY29wi38Vll8dpVNUOxkXphN2KEk=
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240507055918-d126067c56d0 h1:HU+5GHr29qSxIUKWNJVwOqqf2GwG1SD+H30hYzgWykE=
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240507055918-d126067c56d0/go.mod h1:5J2OFjFIBaCfsjcC9kSyycbIL8g/qAJH2A8BnbIig+Y=
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240517002849-51b3a21acad8 h1:IvF6TfCfKvjMBSrRfmyP+hMik8WUIb3d/H0COZvAuow=
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240517002849-51b3a21acad8/go.mod h1:5J2OFjFIBaCfsjcC9kSyycbIL8g/qAJH2A8BnbIig+Y=
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240517033715-36f18d6e31a0 h1:xR4yTecuNj/yMZ8QgvLkItup5DLl8Kd2nbpcV+hBNyI=
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240517033715-36f18d6e31a0/go.mod h1:5J2OFjFIBaCfsjcC9kSyycbIL8g/qAJH2A8BnbIig+Y=
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520165649-7690d725fba4 h1:9RqJPzVMIB22vZzvtNReV8gnuJpolilvxPNjcShd4UE=
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520165649-7690d725fba4/go.mod h1:5J2OFjFIBaCfsjcC9kSyycbIL8g/qAJH2A8BnbIig+Y=
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520165955-fbffd2b947a5 h1:m1PGG0oh019j+YPYTuIoE9nqxbLxNGRUj0hO7OM2rjE=
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520165955-fbffd2b947a5/go.mod h1:5J2OFjFIBaCfsjcC9kSyycbIL8g/qAJH2A8BnbIig+Y=
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520193117-1835255b6d02 h1:FLRmUvu0mz8Ac+/VZf/P4yuv2e6++SSkKOcEIHSlpAI=
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520193117-1835255b6d02/go.mod h1:5J2OFjFIBaCfsjcC9kSyycbIL8g/qAJH2A8BnbIig+Y=
|
||||
gitea.rosskeen.house/rosskeen.house/testing v0.0.0-20240509163950-64f2fc3e00d5 h1:1TUeKrJ12K6+Iobc8rpL/gUaGPFBmTqKjJnkT+2B5nM=
|
||||
gitea.rosskeen.house/rosskeen.house/testing v0.0.0-20240509163950-64f2fc3e00d5/go.mod h1:gbxopbzqpz0ZMAcsPu2XqtprOoFdxwTGz45p06zuI0A=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
||||
|
@ -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 ""
|
||||
}
|
||||
|
287
internal/resource/container_image.go
Normal file
287
internal/resource/container_image.go
Normal 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
|
||||
}
|
89
internal/resource/container_image_test.go
Normal file
89
internal/resource/container_image_test.go
Normal 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)
|
||||
}
|
||||
*/
|
@ -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()
|
||||
|
@ -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 := `
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)))
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
14
internal/resource/schemas/container-image.schema.json
Normal file
14
internal/resource/schemas/container-image.schema.json
Normal 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})$"
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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" ],
|
@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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": {
|
@ -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" ]
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
"title": "storagetransition",
|
||||
"type": "string",
|
||||
"description": "Storage state transition",
|
||||
"enum": [ "absent", "present" ]
|
||||
"enum": [ "absent", "present", "create", "read", "update", "delete" ]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user