add container-image resource
This commit is contained in:
parent
50020e7a44
commit
0888ae2045
@ -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
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
|
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
16
go.sum
@ -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=
|
||||||
|
@ -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 ""
|
||||||
}
|
}
|
||||||
|
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() {
|
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()
|
||||||
|
@ -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 := `
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)))
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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#",
|
"$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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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" ],
|
@ -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" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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": {
|
@ -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" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user