add support for remote command execution
This commit is contained in:
parent
94f3998fcd
commit
07808c62fd
@ -10,6 +10,7 @@ import (
|
||||
"decl/internal/data"
|
||||
"decl/internal/folio"
|
||||
"log/slog"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type UriSchemeValidator func(scheme string) bool
|
||||
@ -31,6 +32,7 @@ type Common struct {
|
||||
State string `json:"state,omitempty" yaml:"state,omitempty"`
|
||||
config data.ConfigurationValueGetter
|
||||
Resources data.ResourceMapper `json:"-" yaml:"-"`
|
||||
Errors []error `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
func NewCommon(resourceType TypeName, includeQueryParams bool) *Common {
|
||||
@ -100,6 +102,11 @@ func (c *Common) SetParsedURI(u data.URIParser) (err error) {
|
||||
} else {
|
||||
c.Path = filepath.Join(c.parsedURI.Hostname(), c.parsedURI.Path)
|
||||
}
|
||||
if c.config != nil {
|
||||
if prefixPath, configErr := c.config.GetValue("prefix"); configErr == nil {
|
||||
c.Path = filepath.Join(prefixPath.(string), c.Path)
|
||||
}
|
||||
}
|
||||
if c.absPath, err = filepath.Abs(c.Path); err != nil {
|
||||
return
|
||||
}
|
||||
@ -130,11 +137,6 @@ func (c *Common) ResolveId(ctx context.Context) string {
|
||||
|
||||
// Common path normalization for a file resource.
|
||||
func (c *Common) NormalizeFilePath() (err error) {
|
||||
if c.config != nil {
|
||||
if prefixPath, configErr := c.config.GetValue("prefix"); configErr == nil {
|
||||
c.Path = filepath.Join(prefixPath.(string), c.Path)
|
||||
}
|
||||
}
|
||||
if c.normalizePath {
|
||||
c.Path = c.absPath
|
||||
}
|
||||
@ -142,3 +144,21 @@ func (c *Common) NormalizeFilePath() (err error) {
|
||||
}
|
||||
|
||||
func (c *Common) Type() string { return string(c.resourceType) }
|
||||
|
||||
|
||||
// If a resource update has errors but the resource is not actually absent
|
||||
func (c *Common) IsResourceInconsistent() (result bool) {
|
||||
for _, err := range c.Errors {
|
||||
if ! errors.Is(err, ErrResourceStateAbsent) {
|
||||
result = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Common) AddError(err error) (error) {
|
||||
if err != nil {
|
||||
c.Errors = append(c.Errors, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -29,6 +29,9 @@ _ "os/exec"
|
||||
"decl/internal/codec"
|
||||
"decl/internal/data"
|
||||
"decl/internal/folio"
|
||||
"decl/internal/containerlog"
|
||||
"bytes"
|
||||
_ "encoding/base64"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -43,44 +46,50 @@ type ContainerClient interface {
|
||||
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)
|
||||
ContainerLogs(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
*Common `yaml:",inline" json:",inline"`
|
||||
stater machine.Stater `yaml:"-" json:"-"`
|
||||
Id string `json:"ID,omitempty" yaml:"ID,omitempty"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Cmd []string `json:"cmd,omitempty" yaml:"cmd,omitempty"`
|
||||
Entrypoint strslice.StrSlice `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty"`
|
||||
Args []string `json:"args,omitempty" yaml:"args,omitempty"`
|
||||
Ports []string `json:"ports,omitempty" yaml:"ports,omitempty"`
|
||||
Environment map[string]string `json:"environment" yaml:"environment"`
|
||||
Image string `json:"image" yaml:"image"`
|
||||
*Common `yaml:",inline" json:",inline"`
|
||||
stater machine.Stater `yaml:"-" json:"-"`
|
||||
Id string `json:"ID,omitempty" yaml:"ID,omitempty"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Cmd []string `json:"cmd,omitempty" yaml:"cmd,omitempty"`
|
||||
WorkingDir string `json:"workingdir,omitempty" yaml:"workingdir,omitempty"`
|
||||
Entrypoint strslice.StrSlice `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty"`
|
||||
Args []string `json:"args,omitempty" yaml:"args,omitempty"`
|
||||
Ports []string `json:"ports,omitempty" yaml:"ports,omitempty"`
|
||||
Environment map[string]string `json:"environment" yaml:"environment"`
|
||||
Image string `json:"image" yaml:"image"`
|
||||
ResolvConfPath string `json:"resolvconfpath" yaml:"resolvconfpath"`
|
||||
HostnamePath string `json:"hostnamepath" yaml:"hostnamepath"`
|
||||
HostsPath string `json:"hostpath" yaml:"hostspath"`
|
||||
LogPath string `json:"logpath" yaml:"logpath"`
|
||||
Created string `json:"created" yaml:"created"`
|
||||
HostnamePath string `json:"hostnamepath" yaml:"hostnamepath"`
|
||||
HostsPath string `json:"hostpath" yaml:"hostspath"`
|
||||
LogPath string `json:"logpath" yaml:"logpath"`
|
||||
Created string `json:"created" yaml:"created"`
|
||||
ContainerState types.ContainerState `json:"containerstate" yaml:"containerstate"`
|
||||
RestartCount int `json:"restartcount" yaml:"restartcount"`
|
||||
Driver string `json:"driver" yaml:"driver"`
|
||||
Platform string `json:"platform" yaml:"platform"`
|
||||
MountLabel string `json:"mountlabel" yaml:"mountlabel"`
|
||||
ProcessLabel string `json:"processlabel" yaml:"processlabel"`
|
||||
RestartCount int `json:"restartcount" yaml:"restartcount"`
|
||||
Driver string `json:"driver" yaml:"driver"`
|
||||
Platform string `json:"platform" yaml:"platform"`
|
||||
MountLabel string `json:"mountlabel" yaml:"mountlabel"`
|
||||
ProcessLabel string `json:"processlabel" yaml:"processlabel"`
|
||||
AppArmorProfile string `json:"apparmorprofile" yaml:"apparmorprofile"`
|
||||
ExecIDs []string `json:"execids" yaml:"execids"`
|
||||
HostConfig container.HostConfig `json:"hostconfig" yaml:"hostconfig"`
|
||||
GraphDriver types.GraphDriverData `json:"graphdriver" yaml:"graphdriver"`
|
||||
SizeRw *int64 `json:",omitempty" yaml:",omitempty"`
|
||||
SizeRootFs *int64 `json:",omitempty" yaml:",omitempty"`
|
||||
Networks []string `json:"networks,omitempty" yaml:"networks,omitempty"`
|
||||
ExecIDs []string `json:"execids" yaml:"execids"`
|
||||
HostConfig container.HostConfig `json:"hostconfig" yaml:"hostconfig"`
|
||||
GraphDriver types.GraphDriverData `json:"graphdriver" yaml:"graphdriver"`
|
||||
SizeRw *int64 `json:",omitempty" yaml:",omitempty"`
|
||||
SizeRootFs *int64 `json:",omitempty" yaml:",omitempty"`
|
||||
Networks []string `json:"networks,omitempty" yaml:"networks,omitempty"`
|
||||
/*
|
||||
Mounts []MountPoint
|
||||
Config *container.Config
|
||||
NetworkSettings *NetworkSettings
|
||||
*/
|
||||
|
||||
Wait bool `json:"wait,omitempty" yaml:"wait,omitempty"`
|
||||
Stdout string `json:"stdout,omitempty" yaml:"stdout,omitempty"`
|
||||
Stderr string `json:"stderr,omitempty" yaml:"stderr,omitempty"`
|
||||
|
||||
apiClient ContainerClient
|
||||
Resources data.ResourceMapper `json:"-" yaml:"-"`
|
||||
}
|
||||
@ -180,17 +189,22 @@ func (c *Container) Validate() error {
|
||||
}
|
||||
|
||||
func (c *Container) Notify(m *machine.EventMessage) {
|
||||
slog.Info("Container.Notify()", "destination_event", m.Dest, "uri", c.URI())
|
||||
ctx := context.Background()
|
||||
switch m.On {
|
||||
case machine.ENTERSTATEEVENT:
|
||||
switch m.Dest {
|
||||
case "start_stat":
|
||||
if statErr := c.ReadStat(); statErr == nil {
|
||||
slog.Info("Container.Notify() - ReadStat", "event", "start_stat", "error", statErr)
|
||||
if triggerErr := c.StateMachine().Trigger("exists"); triggerErr == nil {
|
||||
slog.Info("Container.Notify()", "event", "start_stat", "trigger", "exists")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
slog.Info("Container.Notify() - ReadStat", "event", "start_stat", "error", statErr)
|
||||
if triggerErr := c.StateMachine().Trigger("notexists"); triggerErr == nil {
|
||||
slog.Info("Container.Notify()", "event", "start_stat", "trigger", "notexists")
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -219,6 +233,7 @@ func (c *Container) Notify(m *machine.EventMessage) {
|
||||
panic(createErr)
|
||||
}
|
||||
case "start_delete":
|
||||
slog.Info("Container.Notify()", "event", "start_delete")
|
||||
if deleteErr := c.Delete(ctx); deleteErr == nil {
|
||||
if triggerErr := c.StateMachine().Trigger("deleted"); triggerErr == nil {
|
||||
return
|
||||
@ -242,6 +257,24 @@ func (c *Container) Notify(m *machine.EventMessage) {
|
||||
}
|
||||
|
||||
func (c *Container) ReadStat() (err error) {
|
||||
err = fmt.Errorf("%w: %s", ErrResourceStateAbsent, c.Name)
|
||||
filterArgs := filters.NewArgs()
|
||||
filterArgs.Add("name", "/"+c.Name)
|
||||
if containers, listErr := c.apiClient.ContainerList(context.Background(), container.ListOptions{
|
||||
All: true,
|
||||
Filters: filterArgs,
|
||||
}); listErr == nil {
|
||||
for _, container := range containers {
|
||||
for _, containerName := range container.Names {
|
||||
if containerName == "/"+c.Name {
|
||||
slog.Info("Container.ReadStat() exists", "container", c.Name)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err = fmt.Errorf("%w: %w", err, listErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -275,6 +308,61 @@ func (c *Container) LoadDecl(yamlResourceDeclaration string) error {
|
||||
return c.LoadString(yamlResourceDeclaration, codec.FormatYaml)
|
||||
}
|
||||
|
||||
func (c *Container) ReadFromContainer(ctx context.Context) (err error) {
|
||||
var buf bytes.Buffer
|
||||
var stdout, stderr []string
|
||||
|
||||
if stdoutReader, err := c.apiClient.ContainerLogs(ctx, c.Id, container.LogsOptions{ShowStdout: true, ShowStderr: true}); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer stdoutReader.Close()
|
||||
|
||||
if _, copyErr := io.Copy(&buf, stdoutReader); copyErr != nil {
|
||||
return copyErr
|
||||
}
|
||||
slog.Info("Container.ReadFromContainer() - ContainerLogs", "read", buf.String())
|
||||
|
||||
for {
|
||||
if streamType, message, extractErr := containerlog.Read(&buf); extractErr == nil {
|
||||
switch streamType {
|
||||
case containerlog.StreamStdout:
|
||||
stdout = append(stdout, message)
|
||||
case containerlog.StreamStderr:
|
||||
stderr = append(stderr, message)
|
||||
}
|
||||
} else {
|
||||
if extractErr == io.EOF {
|
||||
break
|
||||
}
|
||||
err = extractErr
|
||||
}
|
||||
/*
|
||||
if streamType, size, extractErr := c.ExtractLogHeader(&buf); extractErr == nil {
|
||||
slog.Info("Container.Create() - ContainerLogs", "streamtype", streamType, "size", size)
|
||||
var logMessage string
|
||||
if logMessage, err = c.ReadLogMessage(&buf, size); err == nil {
|
||||
switch streamType {
|
||||
case ContainerLogStreamStdout:
|
||||
stdout = append(stdout, logMessage)
|
||||
case ContainerLogStreamStderr:
|
||||
stderr = append(stderr, logMessage)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if extractErr == io.EOF {
|
||||
break
|
||||
}
|
||||
err = extractErr
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
c.Stdout = strings.Join(stdout, "")
|
||||
c.Stderr = strings.Join(stderr, "")
|
||||
slog.Info("Container.ReadFromContainer()", "stdout", c.Stdout, "stderr", c.Stderr, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Container) Create(ctx context.Context) error {
|
||||
numberOfEnvironmentVariables := len(c.Environment)
|
||||
|
||||
@ -288,6 +376,9 @@ func (c *Container) Create(ctx context.Context) error {
|
||||
Entrypoint: c.Entrypoint,
|
||||
Tty: false,
|
||||
ExposedPorts: portset,
|
||||
WorkingDir: c.WorkingDir,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
}
|
||||
|
||||
config.Env = make([]string, numberOfEnvironmentVariables)
|
||||
@ -319,20 +410,32 @@ func (c *Container) Create(ctx context.Context) error {
|
||||
}
|
||||
c.Id = resp.ID
|
||||
|
||||
/*
|
||||
statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
case <-statusCh:
|
||||
}
|
||||
*/
|
||||
|
||||
if startErr := c.apiClient.ContainerStart(ctx, c.Id, container.StartOptions{}); startErr != nil {
|
||||
return startErr
|
||||
}
|
||||
|
||||
if err = c.ReadFromContainer(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Wait {
|
||||
slog.Info("Container.Create() - waiting for container to stop", "id", c.Id, "name", c.Name)
|
||||
statusCh, errCh := c.apiClient.ContainerWait(ctx, c.Id, container.WaitConditionNotRunning)
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
case <-statusCh:
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.Stdout) == 0 && len(c.Stderr) == 0 {
|
||||
if err = c.ReadFromContainer(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@ -386,9 +489,6 @@ func (c *Container) Inspect(ctx context.Context, containerID string) error {
|
||||
}
|
||||
c.Common.Path = containerJSON.Path
|
||||
c.Image = containerJSON.Image
|
||||
if containerJSON.State != nil {
|
||||
c.ContainerState = *containerJSON.State
|
||||
}
|
||||
c.Created = containerJSON.Created
|
||||
c.ResolvConfPath = containerJSON.ResolvConfPath
|
||||
c.HostnamePath = containerJSON.HostnamePath
|
||||
@ -396,11 +496,18 @@ func (c *Container) Inspect(ctx context.Context, containerID string) error {
|
||||
c.LogPath = containerJSON.LogPath
|
||||
c.RestartCount = containerJSON.RestartCount
|
||||
c.Driver = containerJSON.Driver
|
||||
if containerJSON.State != nil {
|
||||
c.ContainerState = *containerJSON.State
|
||||
if c.ContainerState.ExitCode != 0 {
|
||||
return fmt.Errorf("%s", c.ContainerState.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Container) Delete(ctx context.Context) error {
|
||||
slog.Info("Container.Delete()", "id", c.Id, "name", c.Name)
|
||||
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
|
||||
|
@ -98,7 +98,7 @@ func (n *ContainerNetwork) SetResourceMapper(resources data.ResourceMapper) {
|
||||
|
||||
func (n *ContainerNetwork) Clone() data.Resource {
|
||||
return &ContainerNetwork {
|
||||
Common: n.Common,
|
||||
Common: n.Common.Clone(),
|
||||
Id: n.Id,
|
||||
Name: n.Name,
|
||||
apiClient: n.apiClient,
|
||||
|
@ -5,20 +5,21 @@ package resource
|
||||
import (
|
||||
"context"
|
||||
"decl/tests/mocks"
|
||||
_ "encoding/json"
|
||||
_ "fmt"
|
||||
_ "encoding/json"
|
||||
_ "fmt"
|
||||
"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/stretchr/testify/assert"
|
||||
_ "io"
|
||||
_ "net/http"
|
||||
_ "net/http/httptest"
|
||||
_ "net/url"
|
||||
_ "os"
|
||||
_ "strings"
|
||||
"io"
|
||||
_ "net/http"
|
||||
_ "net/http/httptest"
|
||||
_ "net/url"
|
||||
_ "os"
|
||||
"strings"
|
||||
"testing"
|
||||
"bytes"
|
||||
)
|
||||
|
||||
func TestNewContainerResource(t *testing.T) {
|
||||
@ -55,6 +56,9 @@ func TestReadContainer(t *testing.T) {
|
||||
go func() { resChan <- res }()
|
||||
return resChan, errChan
|
||||
},
|
||||
InjectContainerLogs: func(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("done.")), nil
|
||||
},
|
||||
}
|
||||
|
||||
c := NewContainer(m)
|
||||
@ -87,6 +91,9 @@ func TestCreateContainer(t *testing.T) {
|
||||
go func() { resChan <- res }()
|
||||
return resChan, errChan
|
||||
},
|
||||
InjectContainerLogs: func(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("done.")), nil
|
||||
},
|
||||
}
|
||||
|
||||
decl := `
|
||||
@ -107,3 +114,37 @@ func TestCreateContainer(t *testing.T) {
|
||||
applyDeleteErr := c.Apply()
|
||||
assert.Equal(t, nil, applyDeleteErr)
|
||||
}
|
||||
|
||||
// Detect the ContainerLog header for each entry
|
||||
func TestContainerLogOutput(t *testing.T) {
|
||||
logHeader := []byte{0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x05}
|
||||
logHeader = append(logHeader, []byte(string("done."))...)
|
||||
logs := bytes.NewReader(logHeader)
|
||||
|
||||
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
|
||||
},
|
||||
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
|
||||
},
|
||||
InjectContainerLogs: func(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
|
||||
return io.NopCloser(logs), nil
|
||||
},
|
||||
}
|
||||
|
||||
c := NewContainer(m)
|
||||
c.ReadFromContainer(context.Background())
|
||||
assert.Equal(t, "done.", c.Stdout)
|
||||
|
||||
}
|
||||
|
295
internal/resource/container_volume.go
Normal file
295
internal/resource/container_volume.go
Normal file
@ -0,0 +1,295 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
_ "github.com/docker/docker/api/types/strslice"
|
||||
"github.com/docker/docker/client"
|
||||
"gopkg.in/yaml.v3"
|
||||
_ "log/slog"
|
||||
"net/url"
|
||||
_ "os"
|
||||
_ "os/exec"
|
||||
_ "strings"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
"decl/internal/codec"
|
||||
"decl/internal/data"
|
||||
"decl/internal/folio"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
const (
|
||||
ContainerVolumeTypeName TypeName = "container-volume"
|
||||
)
|
||||
|
||||
type ContainerVolumeClient interface {
|
||||
ContainerClient
|
||||
VolumeCreate(ctx context.Context, options volume.CreateOptions) (volume.Volume, error)
|
||||
VolumeList(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error)
|
||||
VolumeInspect(ctx context.Context, volumeID string) (volume.Volume, error)
|
||||
VolumeRemove(ctx context.Context, volumeID string, force bool) (error)
|
||||
}
|
||||
|
||||
type ContainerVolume struct {
|
||||
*Common `json:",inline" yaml:",inline"`
|
||||
stater machine.Stater `json:"-" yaml:"-"`
|
||||
volume.Volume `json:",inline" yaml:",inline"`
|
||||
|
||||
apiClient ContainerVolumeClient
|
||||
Resources data.ResourceMapper `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
folio.DocumentRegistry.ResourceTypes.Register([]string{"container-volume"}, func(u *url.URL) (n data.Resource) {
|
||||
n = NewContainerVolume(nil)
|
||||
if u != nil {
|
||||
if err := folio.CastParsedURI(u).ConstructResource(n); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func NewContainerVolume(containerClientApi ContainerVolumeClient) (cn *ContainerVolume) {
|
||||
var apiClient ContainerVolumeClient = containerClientApi
|
||||
if apiClient == nil {
|
||||
var err error
|
||||
apiClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
cn = &ContainerVolume{
|
||||
apiClient: apiClient,
|
||||
}
|
||||
cn.Common = NewCommon(ContainerVolumeTypeName, true)
|
||||
cn.Common.NormalizePath = cn.NormalizePath
|
||||
return cn
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) Init(u data.URIParser) error {
|
||||
if u == nil {
|
||||
u = folio.URI(v.URI()).Parse()
|
||||
}
|
||||
return v.SetParsedURI(u)
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) NormalizePath() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) SetResourceMapper(resources data.ResourceMapper) {
|
||||
v.Resources = resources
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) Clone() data.Resource {
|
||||
return &ContainerVolume {
|
||||
Common: v.Common.Clone(),
|
||||
Volume: v.Volume,
|
||||
apiClient: v.apiClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) StateMachine() machine.Stater {
|
||||
if v.stater == nil {
|
||||
v.stater = StorageMachine(v)
|
||||
}
|
||||
return v.stater
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) Notify(m *machine.EventMessage) {
|
||||
ctx := context.Background()
|
||||
slog.Info("Notify()", "ContainerVolume", v, "m", m)
|
||||
switch m.On {
|
||||
case machine.ENTERSTATEEVENT:
|
||||
switch m.Dest {
|
||||
case "start_read":
|
||||
if _,readErr := v.Read(ctx); readErr == nil {
|
||||
if triggerErr := v.StateMachine().Trigger("state_read"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
v.Common.State = "absent"
|
||||
panic(triggerErr)
|
||||
}
|
||||
} else {
|
||||
v.Common.State = "absent"
|
||||
panic(readErr)
|
||||
}
|
||||
case "start_delete":
|
||||
if deleteErr := v.Delete(ctx); deleteErr == nil {
|
||||
if triggerErr := v.StateMachine().Trigger("deleted"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
v.Common.State = "present"
|
||||
panic(triggerErr)
|
||||
}
|
||||
} else {
|
||||
v.Common.State = "present"
|
||||
panic(deleteErr)
|
||||
}
|
||||
case "start_create":
|
||||
if e := v.Create(ctx); e == nil {
|
||||
if triggerErr := v.StateMachine().Trigger("created"); triggerErr == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
v.Common.State = "absent"
|
||||
case "absent":
|
||||
v.Common.State = "absent"
|
||||
case "present", "created", "read":
|
||||
v.Common.State = "present"
|
||||
}
|
||||
case machine.EXITSTATEEVENT:
|
||||
}
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) URI() string {
|
||||
return fmt.Sprintf("container-volume://%s", v.Name)
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) JSON() ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) Validate() error {
|
||||
return fmt.Errorf("failed")
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) Apply() error {
|
||||
ctx := context.Background()
|
||||
switch v.Common.State {
|
||||
case "absent":
|
||||
return v.Delete(ctx)
|
||||
case "present":
|
||||
return v.Create(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) Load(docData []byte, f codec.Format) (err error) {
|
||||
err = f.StringDecoder(string(docData)).Decode(v)
|
||||
return
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) LoadReader(r io.ReadCloser, f codec.Format) (err error) {
|
||||
err = f.Decoder(r).Decode(v)
|
||||
return
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) LoadString(docData string, f codec.Format) (err error) {
|
||||
err = f.StringDecoder(docData).Decode(v)
|
||||
return
|
||||
}
|
||||
|
||||
func (n *ContainerVolume) LoadDecl(yamlResourceDeclaration string) error {
|
||||
return n.LoadString(yamlResourceDeclaration, codec.FormatYaml)
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) Create(ctx context.Context) (err error) {
|
||||
var spec volume.ClusterVolumeSpec
|
||||
if v.ClusterVolume != nil {
|
||||
spec = v.ClusterVolume.Spec
|
||||
}
|
||||
v.Volume, err = v.apiClient.VolumeCreate(ctx, volume.CreateOptions{
|
||||
Name: v.Name,
|
||||
Driver: v.Driver,
|
||||
DriverOpts: v.Options,
|
||||
Labels: v.Labels,
|
||||
ClusterVolumeSpec: &spec,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) Inspect(ctx context.Context, volumeID string) error {
|
||||
volumeInspect, err := v.apiClient.VolumeInspect(ctx, volumeID)
|
||||
if client.IsErrNotFound(err) {
|
||||
v.Common.State = "absent"
|
||||
} else {
|
||||
v.Common.State = "present"
|
||||
v.Volume = volumeInspect
|
||||
if v.Name == "" {
|
||||
if volumeInspect.Name[0] == '/' {
|
||||
v.Name = volumeInspect.Name[1:]
|
||||
} else {
|
||||
v.Name = volumeInspect.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) Read(ctx context.Context) ([]byte, error) {
|
||||
var volumeID string
|
||||
filterArgs := filters.NewArgs()
|
||||
filterArgs.Add("name", v.Name)
|
||||
volumes, err := v.apiClient.VolumeList(ctx, volume.ListOptions{
|
||||
Filters: filterArgs,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s %s", err, v.Type(), v.Name)
|
||||
}
|
||||
|
||||
for _, vol := range volumes.Volumes {
|
||||
if vol.Name == v.Name {
|
||||
volumeID = vol.Name
|
||||
}
|
||||
}
|
||||
|
||||
if inspectErr := v.Inspect(ctx, volumeID); inspectErr != nil {
|
||||
return nil, fmt.Errorf("%w: volume %s", inspectErr, volumeID)
|
||||
}
|
||||
slog.Info("Read() ", "type", v.Type(), "name", v.Name)
|
||||
|
||||
return yaml.Marshal(v)
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) Update(ctx context.Context) error {
|
||||
return v.Create(ctx)
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) Delete(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *ContainerVolume) Type() string { return "container-volume" }
|
||||
|
||||
func (v *ContainerVolume) ResolveId(ctx context.Context) string {
|
||||
v.Inspect(ctx, v.Name)
|
||||
return v.Name
|
||||
/*
|
||||
volumes, err := n.apiClient.VolumeInspect(ctx, volume.ListOptions{
|
||||
|
||||
filterArgs := filters.NewArgs()
|
||||
filterArgs.Add("name", "/"+n.Name)
|
||||
volumes, err := n.apiClient.VolumeList(ctx, volume.ListOptions{
|
||||
All: true,
|
||||
Filters: filterArgs,
|
||||
})
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("%w: %s %s", err, n.Type(), n.Name))
|
||||
}
|
||||
|
||||
for _, volume := range volumes {
|
||||
for _, containerName := range volume.Name {
|
||||
if containerName == n.Name {
|
||||
if n.Id == "" {
|
||||
n.Id = container.ID
|
||||
}
|
||||
return container.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
*/
|
||||
}
|
50
internal/resource/container_volume_test.go
Normal file
50
internal/resource/container_volume_test.go
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"decl/tests/mocks"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewContainerVolumeResource(t *testing.T) {
|
||||
c := NewContainerVolume(&mocks.MockContainerClient{})
|
||||
assert.NotNil(t, c)
|
||||
}
|
||||
|
||||
func TestReadContainerVolume(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
decl := `
|
||||
name: "testcontainervolume"
|
||||
state: present
|
||||
`
|
||||
m := &mocks.MockContainerClient{
|
||||
InjectVolumeList: func(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) {
|
||||
return volume.ListResponse{
|
||||
Volumes: []*volume.Volume{},
|
||||
Warnings: []string{},
|
||||
}, nil
|
||||
},
|
||||
InjectVolumeInspect: func(ctx context.Context, volumeID string) (volume.Volume, error) {
|
||||
return volume.Volume{
|
||||
Name: "test",
|
||||
Driver: "local",
|
||||
Mountpoint: "/src",
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
v := NewContainerVolume(m)
|
||||
assert.NotNil(t, v)
|
||||
|
||||
e := v.LoadDecl(decl)
|
||||
assert.Nil(t, e)
|
||||
assert.Equal(t, "testcontainervolume", v.Name)
|
||||
|
||||
resourceYaml, readContainerVolumeErr := v.Read(ctx)
|
||||
assert.Equal(t, nil, readContainerVolumeErr)
|
||||
assert.Greater(t, len(resourceYaml), 0)
|
||||
}
|
29
internal/resource/containerlogstreamtype.go
Normal file
29
internal/resource/containerlogstreamtype.go
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
// Container resource
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type ContainerLogStreamType byte
|
||||
|
||||
const (
|
||||
ContainerLogStreamStdin ContainerLogStreamType = 0x0
|
||||
ContainerLogStreamStdout ContainerLogStreamType = 0x1
|
||||
ContainerLogStreamStderr ContainerLogStreamType = 0x2
|
||||
)
|
||||
|
||||
var (
|
||||
ErrContainerLogInvalidStreamType error = errors.New("Invalid container log stream type")
|
||||
)
|
||||
|
||||
func (s ContainerLogStreamType) Validate() error {
|
||||
switch s {
|
||||
case ContainerLogStreamStdin, ContainerLogStreamStdout, ContainerLogStreamStderr:
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%w: %d", ErrContainerLogInvalidStreamType, s)
|
||||
}
|
20
internal/resource/containerlogstreamtype_test.go
Normal file
20
internal/resource/containerlogstreamtype_test.go
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
package resource
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContainerLogStreamType(t *testing.T) {
|
||||
for _, v := range []struct{ expected error; value ContainerLogStreamType } {
|
||||
{ expected: nil, value: 0x0 },
|
||||
{ expected: nil, value: 0x1 },
|
||||
{ expected: nil, value: 0x2 },
|
||||
{ expected: ErrContainerLogInvalidStreamType, value: 0x3 },
|
||||
{ expected: ErrContainerLogInvalidStreamType, value: 0x4 },
|
||||
} {
|
||||
assert.ErrorIs(t, v.value.Validate(), v.expected)
|
||||
}
|
||||
}
|
@ -202,33 +202,57 @@ func (f *File) Notify(m *machine.EventMessage) {
|
||||
if triggerErr := f.StateMachine().Trigger("state_read"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
f.Common.State = "absent"
|
||||
panic(triggerErr)
|
||||
_ = f.AddError(triggerErr)
|
||||
}
|
||||
} else {
|
||||
f.Common.State = "absent"
|
||||
if ! errors.Is(readErr, ErrResourceStateAbsent) {
|
||||
panic(readErr)
|
||||
_ = f.AddError(readErr)
|
||||
if f.IsResourceInconsistent() {
|
||||
if triggerErr := f.StateMachine().Trigger("read-failed"); triggerErr == nil {
|
||||
panic(readErr)
|
||||
} else {
|
||||
_ = f.AddError(triggerErr)
|
||||
panic(fmt.Errorf("%w - %w", readErr, triggerErr))
|
||||
}
|
||||
}
|
||||
_ = f.AddError(f.StateMachine().Trigger("notexists"))
|
||||
}
|
||||
case "start_create":
|
||||
if e := f.Create(ctx); e == nil {
|
||||
if createErr := f.Create(ctx); createErr == nil {
|
||||
if triggerErr := f.StateMachine().Trigger("created"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
_ = f.AddError(triggerErr)
|
||||
}
|
||||
} else {
|
||||
f.Common.State = "absent"
|
||||
panic(e)
|
||||
_ = f.AddError(createErr)
|
||||
if f.IsResourceInconsistent() {
|
||||
if triggerErr := f.StateMachine().Trigger("create-failed"); triggerErr == nil {
|
||||
panic(createErr)
|
||||
} else {
|
||||
_ = f.AddError(triggerErr)
|
||||
panic(fmt.Errorf("%w - %w", createErr, triggerErr))
|
||||
}
|
||||
}
|
||||
_ = f.StateMachine().Trigger("notexists")
|
||||
panic(createErr)
|
||||
}
|
||||
case "start_update":
|
||||
if updateErr := f.Update(ctx); updateErr == nil {
|
||||
if triggerErr := f.stater.Trigger("updated"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
f.Common.State = "absent"
|
||||
_ = f.AddError(triggerErr)
|
||||
}
|
||||
} else {
|
||||
f.Common.State = "absent"
|
||||
_ = f.AddError(updateErr)
|
||||
if f.IsResourceInconsistent() {
|
||||
if triggerErr := f.StateMachine().Trigger("update-failed"); triggerErr == nil {
|
||||
panic(updateErr)
|
||||
} else {
|
||||
panic(fmt.Errorf("%w - %w", updateErr, triggerErr))
|
||||
}
|
||||
}
|
||||
_ = f.StateMachine().Trigger("notexists")
|
||||
panic(updateErr)
|
||||
}
|
||||
case "start_delete":
|
||||
@ -240,15 +264,21 @@ func (f *File) Notify(m *machine.EventMessage) {
|
||||
panic(triggerErr)
|
||||
}
|
||||
} else {
|
||||
f.Common.State = "present"
|
||||
_ = f.StateMachine().Trigger("exists")
|
||||
panic(deleteErr)
|
||||
}
|
||||
case "inconsistent":
|
||||
f.Common.State = "inconsistent"
|
||||
case "absent":
|
||||
f.Common.State = "absent"
|
||||
case "present", "created", "read":
|
||||
f.Common.State = "present"
|
||||
}
|
||||
case machine.EXITSTATEEVENT:
|
||||
switch m.Dest {
|
||||
case "start_create":
|
||||
slog.Info("File.Notify - EXITSTATE", "dest", m.Dest, "common.state", f.Common.State)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
427
internal/resource/openpgp_keyring.go
Normal file
427
internal/resource/openpgp_keyring.go
Normal file
@ -0,0 +1,427 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"gopkg.in/yaml.v3"
|
||||
"net/url"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
"decl/internal/codec"
|
||||
"decl/internal/transport"
|
||||
"decl/internal/data"
|
||||
"decl/internal/folio"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"io/fs"
|
||||
"os"
|
||||
"bytes"
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrOpenPGPEncryptionFailure error = errors.New("OpenPGP encryption failure")
|
||||
)
|
||||
|
||||
const (
|
||||
OpenPGPKeyRingTypeName TypeName = "openpgp-keyring"
|
||||
)
|
||||
|
||||
func init() {
|
||||
folio.DocumentRegistry.ResourceTypes.Register([]string{"openpgp-keyring"}, func(u *url.URL) (res data.Resource) {
|
||||
o := NewOpenPGPKeyRing()
|
||||
if u != nil {
|
||||
if err := folio.CastParsedURI(u).ConstructResource(o); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return o
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
type OpenPGPKeyRing struct {
|
||||
*Common `json:",inline" yaml:",inline"`
|
||||
stater machine.Stater `json:"-" yaml:"-"`
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Comment string `json:"comment,omitempty" yaml:"comment,omitempty"`
|
||||
Email string `json:"email,omitempty" yaml:"email,omitempty"`
|
||||
KeyRing string `json:"keyring,omitempty" yaml:"keyring,omitempty"`
|
||||
Bits int `json:"bits" yaml:"bits"`
|
||||
KeyRingRef folio.ResourceReference `json:"keyringref,omitempty" yaml:"keyringref,omitempty"`
|
||||
|
||||
entityList openpgp.EntityList
|
||||
}
|
||||
|
||||
func NewOpenPGPKeyRing() (o *OpenPGPKeyRing) {
|
||||
o = &OpenPGPKeyRing {
|
||||
Common: NewCommon(OpenPGPKeyRingTypeName, false),
|
||||
Bits: 2048,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) Init(uri data.URIParser) error {
|
||||
if uri == nil {
|
||||
uri = folio.URI(o.URI()).Parse()
|
||||
} else {
|
||||
// o.Name = uri.URL().Hostname()
|
||||
}
|
||||
return o.SetParsedURI(uri)
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) NormalizePath() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func (o *OpenPGPKeyRing) Validate() (err error) {
|
||||
var keyringJson []byte
|
||||
if keyringJson, err = o.JSON(); err == nil {
|
||||
s := NewSchema(o.Type())
|
||||
err = s.Validate(string(keyringJson))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) Config() *packet.Config {
|
||||
config := &packet.Config{
|
||||
RSABits: 2048,
|
||||
Algorithm: packet.PubKeyAlgoRSA,
|
||||
DefaultHash: crypto.SHA256,
|
||||
DefaultCompressionAlgo: packet.CompressionZLIB,
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) IsEncrypted(index int) (result bool) {
|
||||
if len(o.entityList) >= index {
|
||||
result = o.entityList[index].PrivateKey.Encrypted
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) EncryptPrivateKey(entity *openpgp.Entity) error {
|
||||
passphraseConfig, _ := o.config.GetValue("passphrase")
|
||||
passphrase := []byte(passphraseConfig.(string))
|
||||
if len(passphrase) > 0 {
|
||||
if encryptErr := entity.PrivateKey.Encrypt(passphrase); encryptErr != nil {
|
||||
return fmt.Errorf("%w private key: %w", ErrOpenPGPEncryptionFailure, encryptErr)
|
||||
}
|
||||
for _, subkey := range entity.Subkeys {
|
||||
if encryptErr := subkey.PrivateKey.Encrypt(passphrase); encryptErr != nil {
|
||||
return fmt.Errorf("%w subkey (private key): %w", ErrOpenPGPEncryptionFailure, encryptErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) Create(ctx context.Context) (err error) {
|
||||
var entity *openpgp.Entity
|
||||
cfg := o.Config()
|
||||
entity, err = openpgp.NewEntity(o.Name, o.Comment, o.Email, cfg)
|
||||
o.entityList = append(o.entityList, entity)
|
||||
|
||||
if entity.PrivateKey == nil {
|
||||
return fmt.Errorf("Failed creating new private key")
|
||||
}
|
||||
|
||||
if entity.PrimaryKey == nil {
|
||||
return fmt.Errorf("Failed creating new public key")
|
||||
}
|
||||
|
||||
if err = o.EncryptPrivateKey(entity); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(o.KeyRing) == 0 {
|
||||
var keyringBuffer bytes.Buffer
|
||||
if publicKeyWriter, err := armor.Encode(&keyringBuffer, openpgp.PublicKeyType, nil); err == nil {
|
||||
if err = entity.Serialize(publicKeyWriter); err == nil {
|
||||
|
||||
}
|
||||
publicKeyWriter.Close()
|
||||
}
|
||||
keyringBuffer.WriteString("\n")
|
||||
if privateKeyWriter, err := armor.Encode(&keyringBuffer, openpgp.PrivateKeyType, nil); err == nil {
|
||||
if err = entity.SerializePrivateWithoutSigning(privateKeyWriter, nil); err == nil {
|
||||
|
||||
}
|
||||
privateKeyWriter.Close()
|
||||
}
|
||||
keyringBuffer.WriteString("\n")
|
||||
|
||||
o.KeyRing = keyringBuffer.String()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) Clone() data.Resource {
|
||||
return &OpenPGPKeyRing {
|
||||
Common: o.Common.Clone(),
|
||||
Name: o.Name,
|
||||
Comment: o.Comment,
|
||||
Email: o.Email,
|
||||
Bits: o.Bits,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) StateMachine() machine.Stater {
|
||||
if o.stater == nil {
|
||||
o.stater = StorageMachine(o)
|
||||
}
|
||||
return o.stater
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) Notify(m *machine.EventMessage) {
|
||||
ctx := context.Background()
|
||||
switch m.On {
|
||||
case machine.ENTERSTATEEVENT:
|
||||
switch m.Dest {
|
||||
case "start_stat":
|
||||
if statErr := o.ReadStat(); statErr == nil {
|
||||
if triggerErr := o.StateMachine().Trigger("exists"); triggerErr == nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if triggerErr := o.StateMachine().Trigger("notexists"); triggerErr == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
case "start_read":
|
||||
if _,readErr := o.Read(ctx); readErr == nil {
|
||||
if triggerErr := o.StateMachine().Trigger("state_read"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
_ = o.AddError(triggerErr)
|
||||
}
|
||||
} else {
|
||||
_ = o.AddError(readErr)
|
||||
if o.IsResourceInconsistent() {
|
||||
if triggerErr := o.StateMachine().Trigger("read-failed"); triggerErr == nil {
|
||||
panic(readErr)
|
||||
} else {
|
||||
_ = o.AddError(triggerErr)
|
||||
panic(fmt.Errorf("%w - %w", readErr, triggerErr))
|
||||
}
|
||||
}
|
||||
_ = o.AddError(o.StateMachine().Trigger("notexists"))
|
||||
}
|
||||
case "start_create":
|
||||
if createErr := o.Create(ctx); createErr == nil {
|
||||
if triggerErr := o.StateMachine().Trigger("created"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
_ = o.AddError(triggerErr)
|
||||
}
|
||||
} else {
|
||||
_ = o.AddError(createErr)
|
||||
if o.IsResourceInconsistent() {
|
||||
if triggerErr := o.StateMachine().Trigger("create-failed"); triggerErr == nil {
|
||||
panic(createErr)
|
||||
} else {
|
||||
_ = o.AddError(triggerErr)
|
||||
panic(fmt.Errorf("%w - %w", createErr, triggerErr))
|
||||
}
|
||||
}
|
||||
_ = o.StateMachine().Trigger("notexists")
|
||||
panic(createErr)
|
||||
}
|
||||
case "start_update":
|
||||
if updateErr := o.Update(ctx); updateErr == nil {
|
||||
if triggerErr := o.stater.Trigger("updated"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
_ = o.AddError(triggerErr)
|
||||
}
|
||||
} else {
|
||||
_ = o.AddError(updateErr)
|
||||
if o.IsResourceInconsistent() {
|
||||
if triggerErr := o.StateMachine().Trigger("update-failed"); triggerErr == nil {
|
||||
panic(updateErr)
|
||||
} else {
|
||||
panic(fmt.Errorf("%w - %w", updateErr, triggerErr))
|
||||
}
|
||||
}
|
||||
_ = o.StateMachine().Trigger("notexists")
|
||||
panic(updateErr)
|
||||
}
|
||||
case "start_delete":
|
||||
if deleteErr := o.Delete(ctx); deleteErr == nil {
|
||||
if triggerErr := o.StateMachine().Trigger("deleted"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
o.Common.State = "present"
|
||||
panic(triggerErr)
|
||||
}
|
||||
} else {
|
||||
_ = o.StateMachine().Trigger("exists")
|
||||
panic(deleteErr)
|
||||
}
|
||||
case "inconsistent":
|
||||
o.Common.State = "inconsistent"
|
||||
case "absent":
|
||||
o.Common.State = "absent"
|
||||
case "present", "created", "read":
|
||||
o.Common.State = "present"
|
||||
}
|
||||
case machine.EXITSTATEEVENT:
|
||||
switch m.Dest {
|
||||
case "start_create":
|
||||
slog.Info("OpenPGP_Entity.Notify - EXITSTATE", "dest", m.Dest, "common.state", o.Common.State)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) KeyRingRefStat() (info fs.FileInfo, err error) {
|
||||
if len(o.KeyRingRef) > 0 {
|
||||
rs, _ := o.ContentReaderStream()
|
||||
defer rs.Close()
|
||||
info, err = rs.Stat()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) FilePath() string {
|
||||
return o.Common.Path
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) JSON() ([]byte, error) {
|
||||
return json.Marshal(o)
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) Apply() error {
|
||||
ctx := context.Background()
|
||||
switch o.Common.State {
|
||||
case "absent":
|
||||
return o.Delete(ctx)
|
||||
case "present":
|
||||
return o.Create(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) Load(docData []byte, format codec.Format) (err error) {
|
||||
err = format.StringDecoder(string(docData)).Decode(o)
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) LoadReader(r io.ReadCloser, format codec.Format) (err error) {
|
||||
err = format.Decoder(r).Decode(o)
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) LoadString(docData string, format codec.Format) (err error) {
|
||||
err = format.StringDecoder(docData).Decode(o)
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) LoadDecl(yamlResourceDeclaration string) (err error) {
|
||||
return o.LoadString(yamlResourceDeclaration, codec.FormatYaml)
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) ResolveId(ctx context.Context) string {
|
||||
if e := o.NormalizePath(); e != nil {
|
||||
panic(e)
|
||||
}
|
||||
return o.Common.Path
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) GetContentSourceRef() string {
|
||||
return string(o.KeyRingRef)
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) SetContentSourceRef(uri string) {
|
||||
o.KeyRingRef = folio.ResourceReference(uri)
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) Stat() (info fs.FileInfo, err error) {
|
||||
return o.KeyRingRefStat()
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) Update(ctx context.Context) error {
|
||||
return o.Create(ctx)
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) Delete(ctx context.Context) error {
|
||||
return os.Remove(o.Common.Path)
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) ReadStat() (err error) {
|
||||
if _, err = o.Stat(); err != nil {
|
||||
o.Common.State = "absent"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) Read(ctx context.Context) (yamlData []byte, err error) {
|
||||
var keyringReader io.ReadCloser
|
||||
statErr := o.ReadStat()
|
||||
if statErr != nil {
|
||||
return nil, fmt.Errorf("%w - %w: %s", ErrResourceStateAbsent, statErr, o.Path)
|
||||
}
|
||||
|
||||
if keyringReader, err = o.GetContent(nil); err == nil {
|
||||
if krData, krErr := io.ReadAll(keyringReader); krErr == nil {
|
||||
o.KeyRing = string(krData)
|
||||
o.entityList, err = openpgp.ReadArmoredKeyRing(strings.NewReader(o.KeyRing))
|
||||
} else {
|
||||
err = krErr
|
||||
}
|
||||
}
|
||||
|
||||
o.Common.State = "present"
|
||||
return yaml.Marshal(o)
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) GetContent(w io.Writer) (contentReader io.ReadCloser, err error) {
|
||||
contentReader, err = o.readThru()
|
||||
if w != nil {
|
||||
copyBuffer := make([]byte, 32 * 1024)
|
||||
_, writeErr := io.CopyBuffer(w, contentReader, copyBuffer)
|
||||
if writeErr != nil {
|
||||
return nil, fmt.Errorf("OpenPGPKeyRing.GetContent(): CopyBuffer failed %v %v: %w", w, contentReader, writeErr)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) readThru() (contentReader io.ReadCloser, err error) {
|
||||
if o.KeyRingRef.IsEmpty() {
|
||||
if len(o.KeyRing) != 0 {
|
||||
contentReader = io.NopCloser(strings.NewReader(o.KeyRing))
|
||||
}
|
||||
} else {
|
||||
contentReader, err = o.KeyRingRef.Lookup(nil).ContentReaderStream()
|
||||
contentReader.(*transport.Reader).SetGzip(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) URI() string { return string(o.Common.URI()) }
|
||||
|
||||
func (o *OpenPGPKeyRing) Type() string { return "openpgp-keyring" }
|
||||
|
||||
func (o *OpenPGPKeyRing) ContentReaderStream() (*transport.Reader, error) {
|
||||
if len(o.KeyRing) == 0 && ! o.KeyRingRef.IsEmpty() {
|
||||
return o.KeyRingRef.Lookup(nil).ContentReaderStream()
|
||||
}
|
||||
return nil, fmt.Errorf("Cannot provide transport reader for string content")
|
||||
}
|
||||
|
||||
func (o *OpenPGPKeyRing) ContentWriterStream() (*transport.Writer, error) {
|
||||
if len(o.KeyRing) == 0 && ! o.KeyRingRef.IsEmpty() {
|
||||
return o.KeyRingRef.Lookup(nil).ContentWriterStream()
|
||||
}
|
||||
return nil, fmt.Errorf("Cannot provide transport writer for string content")
|
||||
}
|
108
internal/resource/openpgp_keyring_test.go
Normal file
108
internal/resource/openpgp_keyring_test.go
Normal file
@ -0,0 +1,108 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"fmt"
|
||||
"decl/internal/data"
|
||||
)
|
||||
|
||||
func TestNewOpenPGPKeyRingResource(t *testing.T) {
|
||||
assert.NotNil(t, NewOpenPGPKeyRing())
|
||||
}
|
||||
|
||||
func TestCreateKeyRing(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
declarationAttributes := `
|
||||
name: TestUser1
|
||||
comment: TestUser1
|
||||
email: testuser@rosskeen.house
|
||||
`
|
||||
|
||||
testKeyRing := NewOpenPGPKeyRing()
|
||||
e := testKeyRing.LoadDecl(declarationAttributes)
|
||||
assert.Nil(t, e)
|
||||
|
||||
testKeyRing.UseConfig(MockConfigValueGetter(func(key string) (any, error) {
|
||||
switch key {
|
||||
case "passphrase":
|
||||
return "foo", nil
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key)
|
||||
}))
|
||||
|
||||
|
||||
err := testKeyRing.Create(ctx)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Greater(t, len(testKeyRing.entityList), 0)
|
||||
assert.Contains(t, testKeyRing.entityList[0].Identities, "TestUser1 (TestUser1) <testuser@rosskeen.house>")
|
||||
assert.Contains(t, testKeyRing.KeyRing, "-----END PGP PUBLIC KEY BLOCK-----")
|
||||
assert.Contains(t, testKeyRing.KeyRing, "-----END PGP PRIVATE KEY BLOCK-----")
|
||||
|
||||
assert.True(t, testKeyRing.IsEncrypted(0))
|
||||
}
|
||||
|
||||
func TestReadKeyRing(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
declarationAttributes := `
|
||||
keyring: |-
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
+aAQq8u60fWheEIG+xaV3T01zrNRUo6I7xu5kA==
|
||||
=yFbn
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
`
|
||||
|
||||
testKeyRing := NewOpenPGPKeyRing()
|
||||
e := testKeyRing.LoadDecl(declarationAttributes)
|
||||
assert.Nil(t, e)
|
||||
y, err := testKeyRing.Read(ctx)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, y)
|
||||
|
||||
assert.Greater(t, len(testKeyRing.entityList), 0)
|
||||
assert.Contains(t, testKeyRing.entityList[0].Identities, "TestUser (TestUser) <matthewrich.conf@gmail.com>")
|
||||
|
||||
}
|
365
internal/resource/openpgp_signature.go
Normal file
365
internal/resource/openpgp_signature.go
Normal file
@ -0,0 +1,365 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"gopkg.in/yaml.v3"
|
||||
"net/url"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
"decl/internal/codec"
|
||||
"decl/internal/ext"
|
||||
"decl/internal/transport"
|
||||
"decl/internal/data"
|
||||
"decl/internal/folio"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/fs"
|
||||
"bytes"
|
||||
"os"
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSignatureWriterFailed error = errors.New("Failed creating signature writer")
|
||||
ErrArmoredWriterFailed error = errors.New("Failed to create armored writer")
|
||||
)
|
||||
|
||||
const (
|
||||
OpenPGPSignatureTypeName TypeName = "openpgp-signature"
|
||||
)
|
||||
|
||||
func init() {
|
||||
folio.DocumentRegistry.ResourceTypes.Register([]string{"openpgp-signature"}, func(u *url.URL) (res data.Resource) {
|
||||
o := NewOpenPGPSignature()
|
||||
if u != nil {
|
||||
if err := folio.CastParsedURI(u).ConstructResource(o); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return o
|
||||
})
|
||||
}
|
||||
|
||||
type OpenPGPSignature struct {
|
||||
*Common `json:",inline" yaml:",inline"`
|
||||
stater machine.Stater `json:"-" yaml:"-"`
|
||||
Signature string `json:"signature,omitempty" yaml:"signature,omitempty"`
|
||||
KeyRingRef folio.ResourceReference `json:"keyringref,omitempty" yaml:"keyringref,omitempty"`
|
||||
SourceRef folio.ResourceReference `json:"soureref,omitempty" yaml:"sourceref,omitempty"`
|
||||
SignatureRef folio.ResourceReference `json:"signatureref,omitempty" yaml:"signatureref,omitempty"`
|
||||
|
||||
message *openpgp.MessageDetails
|
||||
entityList openpgp.EntityList
|
||||
}
|
||||
|
||||
|
||||
func NewOpenPGPSignature() *OpenPGPSignature {
|
||||
o := &OpenPGPSignature {
|
||||
Common: NewCommon(OpenPGPSignatureTypeName, false),
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) Type() string { return "openpgp-signature" }
|
||||
|
||||
func (o *OpenPGPSignature) Init(uri data.URIParser) error {
|
||||
if uri == nil {
|
||||
uri = folio.URI(o.URI()).Parse()
|
||||
} else {
|
||||
// o.Name = uri.URL().Hostname()
|
||||
}
|
||||
return o.SetParsedURI(uri)
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) NormalizePath() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) StateMachine() machine.Stater {
|
||||
if o.stater == nil {
|
||||
o.stater = StorageMachine(o)
|
||||
}
|
||||
return o.stater
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) URI() string {
|
||||
return string(o.Common.URI())
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) SigningEntity() (err error) {
|
||||
if o.KeyRingRef.IsEmpty() {
|
||||
var keyringConfig any
|
||||
if keyringConfig, err = o.config.GetValue("keyring"); err == nil {
|
||||
o.entityList = keyringConfig.(openpgp.EntityList)
|
||||
}
|
||||
} else {
|
||||
ringFileStream, _ := o.KeyRingRef.Lookup(o.Resources).ContentReaderStream()
|
||||
defer ringFileStream.Close()
|
||||
o.entityList, err = openpgp.ReadArmoredKeyRing(ringFileStream)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
func (o *OpenPGPSignature) Sign(message io.Reader, w io.Writer) (err error) {
|
||||
var writer io.WriteCloser
|
||||
entity := o.entityList[0]
|
||||
|
||||
if writer, err = openpgp.Sign(w, entity, nil, o.Config()); err == nil {
|
||||
defer writer.Close()
|
||||
_, err = io.Copy(writer, message)
|
||||
} else {
|
||||
err = fmt.Errorf("%w: %w", ErrSignatureWriterFailed, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) Create(ctx context.Context) (err error) {
|
||||
if err = o.SigningEntity(); err == nil {
|
||||
var sourceReadStream io.ReadCloser
|
||||
sourceReadStream, err = o.SourceRef.Lookup(o.Resources).ContentReaderStream()
|
||||
var signatureStream, armoredWriter io.WriteCloser
|
||||
|
||||
if o.SignatureRef.IsEmpty() {
|
||||
var signatureContent bytes.Buffer
|
||||
signatureStream = ext.WriteNopCloser(&signatureContent)
|
||||
defer func() { o.Signature = signatureContent.String() }()
|
||||
} else {
|
||||
signatureStream, _ = o.SignatureRef.Lookup(o.Resources).ContentWriterStream()
|
||||
}
|
||||
|
||||
if armoredWriter, err = armor.Encode(signatureStream, openpgp.SignatureType, nil); err != nil {
|
||||
err = fmt.Errorf("%w: %w", ErrArmoredWriterFailed, err)
|
||||
}
|
||||
defer armoredWriter.Close()
|
||||
|
||||
err = o.Sign(sourceReadStream, armoredWriter)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) Validate() (err error) {
|
||||
var signatureJson []byte
|
||||
if signatureJson, err = o.JSON(); err == nil {
|
||||
s := NewSchema(o.Type())
|
||||
err = s.Validate(string(signatureJson))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) Config() *packet.Config {
|
||||
config := &packet.Config{
|
||||
RSABits: 2048,
|
||||
Algorithm: packet.PubKeyAlgoRSA,
|
||||
DefaultHash: crypto.SHA256,
|
||||
DefaultCompressionAlgo: packet.CompressionZLIB,
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) Clone() data.Resource {
|
||||
return &OpenPGPSignature {
|
||||
Common: o.Common.Clone(),
|
||||
KeyRingRef: o.KeyRingRef,
|
||||
SourceRef: o.SourceRef,
|
||||
SignatureRef: o.SignatureRef,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) Notify(m *machine.EventMessage) {
|
||||
ctx := context.Background()
|
||||
switch m.On {
|
||||
case machine.ENTERSTATEEVENT:
|
||||
switch m.Dest {
|
||||
case "start_stat":
|
||||
if statErr := o.ReadStat(); statErr == nil {
|
||||
if triggerErr := o.StateMachine().Trigger("exists"); triggerErr == nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if triggerErr := o.StateMachine().Trigger("notexists"); triggerErr == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
case "start_read":
|
||||
if _,readErr := o.Read(ctx); readErr == nil {
|
||||
if triggerErr := o.StateMachine().Trigger("state_read"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
_ = o.AddError(triggerErr)
|
||||
}
|
||||
} else {
|
||||
_ = o.AddError(readErr)
|
||||
if o.IsResourceInconsistent() {
|
||||
if triggerErr := o.StateMachine().Trigger("read-failed"); triggerErr == nil {
|
||||
panic(readErr)
|
||||
} else {
|
||||
_ = o.AddError(triggerErr)
|
||||
panic(fmt.Errorf("%w - %w", readErr, triggerErr))
|
||||
}
|
||||
}
|
||||
_ = o.AddError(o.StateMachine().Trigger("notexists"))
|
||||
}
|
||||
case "start_create":
|
||||
if createErr := o.Create(ctx); createErr == nil {
|
||||
if triggerErr := o.StateMachine().Trigger("created"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
_ = o.AddError(triggerErr)
|
||||
}
|
||||
} else {
|
||||
_ = o.AddError(createErr)
|
||||
if o.IsResourceInconsistent() {
|
||||
if triggerErr := o.StateMachine().Trigger("create-failed"); triggerErr == nil {
|
||||
panic(createErr)
|
||||
} else {
|
||||
_ = o.AddError(triggerErr)
|
||||
panic(fmt.Errorf("%w - %w", createErr, triggerErr))
|
||||
}
|
||||
}
|
||||
_ = o.StateMachine().Trigger("notexists")
|
||||
panic(createErr)
|
||||
}
|
||||
case "start_update":
|
||||
if updateErr := o.Update(ctx); updateErr == nil {
|
||||
if triggerErr := o.stater.Trigger("updated"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
_ = o.AddError(triggerErr)
|
||||
}
|
||||
} else {
|
||||
_ = o.AddError(updateErr)
|
||||
if o.IsResourceInconsistent() {
|
||||
if triggerErr := o.StateMachine().Trigger("update-failed"); triggerErr == nil {
|
||||
panic(updateErr)
|
||||
} else {
|
||||
panic(fmt.Errorf("%w - %w", updateErr, triggerErr))
|
||||
}
|
||||
}
|
||||
_ = o.StateMachine().Trigger("notexists")
|
||||
panic(updateErr)
|
||||
}
|
||||
case "start_delete":
|
||||
if deleteErr := o.Delete(ctx); deleteErr == nil {
|
||||
if triggerErr := o.StateMachine().Trigger("deleted"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
o.Common.State = "present"
|
||||
panic(triggerErr)
|
||||
}
|
||||
} else {
|
||||
_ = o.StateMachine().Trigger("exists")
|
||||
panic(deleteErr)
|
||||
}
|
||||
case "inconsistent":
|
||||
o.Common.State = "inconsistent"
|
||||
case "absent":
|
||||
o.Common.State = "absent"
|
||||
case "present", "created", "read":
|
||||
o.Common.State = "present"
|
||||
}
|
||||
case machine.EXITSTATEEVENT:
|
||||
switch m.Dest {
|
||||
case "start_create":
|
||||
slog.Info("OpenPGPSignature.Notify - EXITSTATE", "dest", m.Dest, "common.state", o.Common.State)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) FilePath() string {
|
||||
return o.Common.Path
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) JSON() ([]byte, error) {
|
||||
return json.Marshal(o)
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) Apply() error {
|
||||
ctx := context.Background()
|
||||
switch o.Common.State {
|
||||
case "absent":
|
||||
return o.Delete(ctx)
|
||||
case "present":
|
||||
return o.Create(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) Load(docData []byte, format codec.Format) (err error) {
|
||||
err = format.StringDecoder(string(docData)).Decode(o)
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) LoadReader(r io.ReadCloser, format codec.Format) (err error) {
|
||||
err = format.Decoder(r).Decode(o)
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) LoadString(docData string, format codec.Format) (err error) {
|
||||
err = format.StringDecoder(docData).Decode(o)
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) LoadDecl(yamlResourceDeclaration string) (err error) {
|
||||
return o.LoadString(yamlResourceDeclaration, codec.FormatYaml)
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) ResolveId(ctx context.Context) string {
|
||||
if e := o.NormalizePath(); e != nil {
|
||||
panic(e)
|
||||
}
|
||||
return o.Common.Path
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) GetContentSourceRef() string {
|
||||
return string(o.SignatureRef)
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) SetContentSourceRef(uri string) {
|
||||
o.SignatureRef = folio.ResourceReference(uri)
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) SignatureRefStat() (info fs.FileInfo, err error) {
|
||||
err = fmt.Errorf("%w: %s", ErrResourceStateAbsent, o.SignatureRef)
|
||||
if len(o.SignatureRef) > 0 {
|
||||
rs, _ := o.ContentReaderStream()
|
||||
defer rs.Close()
|
||||
return rs.Stat()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) Stat() (info fs.FileInfo, err error) {
|
||||
return o.SignatureRefStat()
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) ReadStat() (err error) {
|
||||
_, err = o.SignatureRefStat()
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) Update(ctx context.Context) error {
|
||||
return o.Create(ctx)
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) Delete(ctx context.Context) error {
|
||||
return os.Remove(o.Common.Path)
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) Read(ctx context.Context) ([]byte, error) {
|
||||
return yaml.Marshal(o)
|
||||
}
|
||||
|
||||
func (o *OpenPGPSignature) ContentReaderStream() (*transport.Reader, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
142
internal/resource/openpgp_signature_test.go
Normal file
142
internal/resource/openpgp_signature_test.go
Normal file
@ -0,0 +1,142 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"fmt"
|
||||
"decl/internal/data"
|
||||
"decl/internal/folio"
|
||||
"decl/internal/codec"
|
||||
"log"
|
||||
)
|
||||
|
||||
func NewTestUserKeys() (data.ResourceMapper, folio.URI) {
|
||||
uri := "openpgp-keyring://TestUser1/TestUser1/testuser@rosskeen.house"
|
||||
keyRingDecl := folio.NewDeclaration()
|
||||
keyRingDecl.NewResource(&uri)
|
||||
|
||||
ctx := context.Background()
|
||||
declarationAttributes := fmt.Sprintf(`
|
||||
name: TestUser1
|
||||
comment: TestUser1
|
||||
email: testuser@rosskeen.house
|
||||
keyringref: file://%s/keyring.asc
|
||||
`, string(TempDir))
|
||||
|
||||
testKeyRing := keyRingDecl.Resource()
|
||||
if e := testKeyRing.LoadString(declarationAttributes, codec.FormatYaml); e != nil {
|
||||
log.Fatal(e)
|
||||
}
|
||||
|
||||
testKeyRing.UseConfig(MockConfigValueGetter(func(key string) (any, error) {
|
||||
switch key {
|
||||
case "passphrase":
|
||||
return "foo", nil
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key)
|
||||
}))
|
||||
|
||||
if err := testKeyRing.Create(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return TestResourceMapper(func(key string) (data.Declaration, bool) {
|
||||
return keyRingDecl, true
|
||||
}), folio.URI(uri)
|
||||
|
||||
}
|
||||
|
||||
func TestNewOpenPGPSignatureResource(t *testing.T) {
|
||||
assert.NotNil(t, NewOpenPGPSignature())
|
||||
}
|
||||
|
||||
func TestCreateSignature(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
m, keyRingUri := NewTestUserKeys()
|
||||
|
||||
assert.Nil(t, TempDir.CreateFile("test.txt", "test data"))
|
||||
|
||||
declarationAttributes := fmt.Sprintf(`
|
||||
keyringref: %s
|
||||
sourceref: file://%s/test.txt
|
||||
`, string(keyRingUri), string(TempDir))
|
||||
|
||||
testSignature := NewOpenPGPSignature()
|
||||
testSignature.Resources = m
|
||||
e := testSignature.LoadDecl(declarationAttributes)
|
||||
assert.Nil(t, e)
|
||||
|
||||
err := testSignature.Create(ctx)
|
||||
assert.Nil(t, err)
|
||||
/*
|
||||
assert.Greater(t, len(testSignature.entityList), 0)
|
||||
assert.Contains(t, testSignature.entityList[0].Identities, "TestUser1 (TestUser1) <testuser@rosskeen.house>")
|
||||
assert.Contains(t, testSignature.Signature, "-----END PGP PUBLIC KEY BLOCK-----")
|
||||
assert.Contains(t, testSignature.Signature, "-----END PGP PRIVATE KEY BLOCK-----")
|
||||
*/
|
||||
}
|
||||
|
||||
/*
|
||||
func TestReadSignature(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
declarationAttributes := `
|
||||
signature: |-
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
+aAQq8u60fWheEIG+xaV3T01zrNRUo6I7xu5kA==
|
||||
=yFbn
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
`
|
||||
|
||||
testSignature := NewOpenPGPSignature()
|
||||
e := testSignature.LoadDecl(declarationAttributes)
|
||||
assert.Nil(t, e)
|
||||
y, err := testSignature.Read(ctx)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, y)
|
||||
|
||||
assert.Greater(t, len(testSignature.entityList), 0)
|
||||
assert.Contains(t, testSignature.entityList[0].Identities, "TestUser (TestUser) <matthewrich.conf@gmail.com>")
|
||||
|
||||
}
|
||||
*/
|
@ -40,12 +40,12 @@ func ResourceConstructor(res data.Resource, uri data.URIParser) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func StorageMachine(sub machine.Subscriber) machine.Stater {
|
||||
// start_destroy -> absent -> start_create -> present -> start_destroy
|
||||
// Common resource states
|
||||
func ResourceMachine(sub machine.Subscriber) machine.Stater {
|
||||
stater := machine.New("unknown")
|
||||
stater.AddStates("unkonwn", "absent", "start_create", "present", "start_delete", "start_read", "start_update", "start_stat")
|
||||
stater.AddStates("unkonwn", "inconsistent", "absent", "start_create", "present", "start_delete", "start_read", "start_update", "start_stat", "created")
|
||||
|
||||
stater.AddTransition("create", machine.States("unknown", "absent"), "start_create")
|
||||
stater.AddTransition("create", machine.States("unknown", "inconsistent", "absent"), "start_create")
|
||||
if e := stater.AddSubscription("create", sub); e != nil {
|
||||
return nil
|
||||
}
|
||||
@ -54,7 +54,8 @@ func StorageMachine(sub machine.Subscriber) machine.Stater {
|
||||
if e := stater.AddSubscription("created", sub); e != nil {
|
||||
return nil
|
||||
}
|
||||
stater.AddTransition("exists", machine.States("unknown", "absent", "start_stat"), "present")
|
||||
|
||||
stater.AddTransition("exists", machine.States("unknown", "inconsistent", "absent", "start_stat"), "present")
|
||||
if e := stater.AddSubscription("exists", sub); e != nil {
|
||||
return nil
|
||||
}
|
||||
@ -64,6 +65,24 @@ func StorageMachine(sub machine.Subscriber) machine.Stater {
|
||||
return nil
|
||||
}
|
||||
|
||||
stater.AddTransition("update-failed", machine.States("start_update"), "inconsistent")
|
||||
if e := stater.AddSubscription("update-failed", sub); e != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
stater.AddTransition("create-failed", machine.States("start_create"), "inconsistent")
|
||||
if e := stater.AddSubscription("create-failed", sub); e != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return stater
|
||||
}
|
||||
|
||||
func StorageMachine(sub machine.Subscriber) machine.Stater {
|
||||
// start_destroy -> absent -> start_create -> present -> start_destroy
|
||||
|
||||
stater := ResourceMachine(sub)
|
||||
|
||||
stater.AddTransition("read", machine.States("*"), "start_read")
|
||||
if e := stater.AddSubscription("read", sub); e != nil {
|
||||
return nil
|
||||
@ -98,24 +117,12 @@ func StorageMachine(sub machine.Subscriber) machine.Stater {
|
||||
|
||||
func ProcessMachine(sub machine.Subscriber) machine.Stater {
|
||||
// "enum": [ "created", "restarting", "running", "paused", "exited", "dead" ]
|
||||
stater := machine.New("unknown")
|
||||
stater.AddStates("unkonwn", "absent", "start_create", "present", "created", "restarting", "running", "paused", "exited", "dead", "start_delete", "start_read", "start_update")
|
||||
stater.AddTransition("create", machine.States("unknown", "absent"), "start_create")
|
||||
if e := stater.AddSubscription("create", sub); e != nil {
|
||||
return nil
|
||||
}
|
||||
stater.AddTransition("created", machine.States("start_create"), "present")
|
||||
if e := stater.AddSubscription("created", sub); e != nil {
|
||||
return nil
|
||||
}
|
||||
stater.AddTransition("exists", machine.States("unknown", "absent"), "present")
|
||||
if e := stater.AddSubscription("exists", sub); e != nil {
|
||||
return nil
|
||||
}
|
||||
stater.AddTransition("notexists", machine.States("*"), "absent")
|
||||
if e := stater.AddSubscription("notexists", sub); e != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
stater := ResourceMachine(sub)
|
||||
|
||||
stater.AddStates("restarting", "running", "paused", "exited", "dead")
|
||||
|
||||
|
||||
stater.AddTransition("read", machine.States("*"), "start_read")
|
||||
if e := stater.AddSubscription("read", sub); e != nil {
|
||||
return nil
|
||||
|
@ -30,6 +30,7 @@
|
||||
{ "$ref": "http-declaration.schema.json" },
|
||||
{ "$ref": "iptable-declaration.schema.json" },
|
||||
{ "$ref": "network-route-declaration.schema.json" },
|
||||
{ "$ref": "openpgp-keyring-declaration.schema.json" },
|
||||
{ "$ref": "package-declaration.schema.json" },
|
||||
{ "$ref": "pki-declaration.schema.json" },
|
||||
{ "$ref": "user-declaration.schema.json" }
|
||||
|
@ -0,0 +1,21 @@
|
||||
{
|
||||
"$id": "openpgp-keyring-declaration.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "openpgp-keyring-declaration",
|
||||
"type": "object",
|
||||
"required": [ "type", "attributes" ],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Resource type name.",
|
||||
"enum": [ "file" ]
|
||||
},
|
||||
"config": {
|
||||
"type": "string",
|
||||
"description": "Config name"
|
||||
},
|
||||
"attributes": {
|
||||
"$ref": "openpgp-keyring.schema.json"
|
||||
}
|
||||
}
|
||||
}
|
17
internal/resource/schemas/openpgp-keyring.schema.json
Normal file
17
internal/resource/schemas/openpgp-keyring.schema.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"$id": "openpgp-keyring.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "openpgp-keyring",
|
||||
"type": "object",
|
||||
"required": [ ],
|
||||
"properties": {
|
||||
"armored": {
|
||||
"type": "string",
|
||||
"description": "ASCII-armored PGP keyring"
|
||||
},
|
||||
"sourceref": {
|
||||
"type": "string",
|
||||
"description": "file content source uri"
|
||||
}
|
||||
}
|
||||
}
|
@ -325,14 +325,21 @@ func (u *User) ReadGroups() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (u *User) Update(ctx context.Context) (error) {
|
||||
_, err := u.UpdateCommand.Execute(u)
|
||||
func (u *User) Update(ctx context.Context) (err error) {
|
||||
switch u.UserType {
|
||||
case UserTypeBusyBox:
|
||||
_, err = u.CreateCommand.Execute(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = u.UpdateCommand.Execute(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_,e := u.Read(ctx)
|
||||
return e
|
||||
_, err = u.Read(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
func (u *User) Delete(ctx context.Context) (error) {
|
||||
@ -489,11 +496,16 @@ func NewUserShadowCreateCommand() *command.Command {
|
||||
func NewUserBusyBoxCreateCommand() *command.Command {
|
||||
c := command.NewCommand()
|
||||
c.Path = "adduser"
|
||||
c.Split = false
|
||||
c.Args = []command.CommandArg{
|
||||
command.CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
|
||||
command.CommandArg("{{ if .Gecos }}-g {{ .Gecos }}{{ end }}"),
|
||||
command.CommandArg("{{ if .Group }}-G {{ .Group }}{{ end }}"),
|
||||
command.CommandArg("{{ if .Home }}-h {{ .Home }}{{ end }}"),
|
||||
command.CommandArg("{{ if .UID }}-u{{ end }}"),
|
||||
command.CommandArg("{{ if .UID }}{{ .UID }}{{ end }}"),
|
||||
command.CommandArg("{{ if .Gecos }}-g{{ end }}"),
|
||||
command.CommandArg("{{ if .Gecos }}{{ .Gecos }}{{ end }}"),
|
||||
command.CommandArg("{{ if .Group }}-G{{ end }}"),
|
||||
command.CommandArg("{{ if .Group }}{{ .Group }}{{ end }}"),
|
||||
command.CommandArg("{{ if .Home }}-h{{ end }}"),
|
||||
command.CommandArg("{{ if .Home }}{{ .Home }}{{ end }}"),
|
||||
command.CommandArg("{{ if not .CreateHome }}-H{{ end }}"),
|
||||
command.CommandArg("-D"),
|
||||
command.CommandArg("{{ .Name }}"),
|
||||
@ -544,8 +556,11 @@ func NewUserBusyBoxUpdateCommand() *command.Command {
|
||||
c.StdinAvailable = true
|
||||
c.Input = command.CommandInput("{{ if .Groups }}{{ range .Groups }}{{ . }}\n{{ end }}{{ end }}")
|
||||
c.Args = []command.CommandArg{
|
||||
command.CommandArg("-r"),
|
||||
command.CommandArg("-IGROUP"),
|
||||
command.CommandArg("adduser"),
|
||||
command.CommandArg("{{ .Name }}"),
|
||||
command.CommandArg("GROUP"),
|
||||
}
|
||||
c.Extractor = func(out []byte, target any) error {
|
||||
return nil
|
||||
|
Loading…
Reference in New Issue
Block a user