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/data"
|
||||||
"decl/internal/folio"
|
"decl/internal/folio"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UriSchemeValidator func(scheme string) bool
|
type UriSchemeValidator func(scheme string) bool
|
||||||
@ -31,6 +32,7 @@ type Common struct {
|
|||||||
State string `json:"state,omitempty" yaml:"state,omitempty"`
|
State string `json:"state,omitempty" yaml:"state,omitempty"`
|
||||||
config data.ConfigurationValueGetter
|
config data.ConfigurationValueGetter
|
||||||
Resources data.ResourceMapper `json:"-" yaml:"-"`
|
Resources data.ResourceMapper `json:"-" yaml:"-"`
|
||||||
|
Errors []error `json:"-" yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommon(resourceType TypeName, includeQueryParams bool) *Common {
|
func NewCommon(resourceType TypeName, includeQueryParams bool) *Common {
|
||||||
@ -100,6 +102,11 @@ func (c *Common) SetParsedURI(u data.URIParser) (err error) {
|
|||||||
} else {
|
} else {
|
||||||
c.Path = filepath.Join(c.parsedURI.Hostname(), c.parsedURI.Path)
|
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 {
|
if c.absPath, err = filepath.Abs(c.Path); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -130,11 +137,6 @@ func (c *Common) ResolveId(ctx context.Context) string {
|
|||||||
|
|
||||||
// Common path normalization for a file resource.
|
// Common path normalization for a file resource.
|
||||||
func (c *Common) NormalizeFilePath() (err error) {
|
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 {
|
if c.normalizePath {
|
||||||
c.Path = c.absPath
|
c.Path = c.absPath
|
||||||
}
|
}
|
||||||
@ -142,3 +144,21 @@ func (c *Common) NormalizeFilePath() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Common) Type() string { return string(c.resourceType) }
|
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/codec"
|
||||||
"decl/internal/data"
|
"decl/internal/data"
|
||||||
"decl/internal/folio"
|
"decl/internal/folio"
|
||||||
|
"decl/internal/containerlog"
|
||||||
|
"bytes"
|
||||||
|
_ "encoding/base64"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -43,6 +46,7 @@ type ContainerClient interface {
|
|||||||
ContainerRemove(context.Context, string, container.RemoveOptions) error
|
ContainerRemove(context.Context, string, container.RemoveOptions) error
|
||||||
ContainerStop(context.Context, string, container.StopOptions) error
|
ContainerStop(context.Context, string, container.StopOptions) error
|
||||||
ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan 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
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,6 +56,7 @@ type Container struct {
|
|||||||
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"`
|
||||||
Cmd []string `json:"cmd,omitempty" yaml:"cmd,omitempty"`
|
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"`
|
Entrypoint strslice.StrSlice `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty"`
|
||||||
Args []string `json:"args,omitempty" yaml:"args,omitempty"`
|
Args []string `json:"args,omitempty" yaml:"args,omitempty"`
|
||||||
Ports []string `json:"ports,omitempty" yaml:"ports,omitempty"`
|
Ports []string `json:"ports,omitempty" yaml:"ports,omitempty"`
|
||||||
@ -81,6 +86,10 @@ type Container struct {
|
|||||||
NetworkSettings *NetworkSettings
|
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
|
apiClient ContainerClient
|
||||||
Resources data.ResourceMapper `json:"-" yaml:"-"`
|
Resources data.ResourceMapper `json:"-" yaml:"-"`
|
||||||
}
|
}
|
||||||
@ -180,17 +189,22 @@ func (c *Container) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) Notify(m *machine.EventMessage) {
|
func (c *Container) Notify(m *machine.EventMessage) {
|
||||||
|
slog.Info("Container.Notify()", "destination_event", m.Dest, "uri", c.URI())
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
switch m.On {
|
switch m.On {
|
||||||
case machine.ENTERSTATEEVENT:
|
case machine.ENTERSTATEEVENT:
|
||||||
switch m.Dest {
|
switch m.Dest {
|
||||||
case "start_stat":
|
case "start_stat":
|
||||||
if statErr := c.ReadStat(); statErr == nil {
|
if statErr := c.ReadStat(); statErr == nil {
|
||||||
|
slog.Info("Container.Notify() - ReadStat", "event", "start_stat", "error", statErr)
|
||||||
if triggerErr := c.StateMachine().Trigger("exists"); triggerErr == nil {
|
if triggerErr := c.StateMachine().Trigger("exists"); triggerErr == nil {
|
||||||
|
slog.Info("Container.Notify()", "event", "start_stat", "trigger", "exists")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
slog.Info("Container.Notify() - ReadStat", "event", "start_stat", "error", statErr)
|
||||||
if triggerErr := c.StateMachine().Trigger("notexists"); triggerErr == nil {
|
if triggerErr := c.StateMachine().Trigger("notexists"); triggerErr == nil {
|
||||||
|
slog.Info("Container.Notify()", "event", "start_stat", "trigger", "notexists")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -219,6 +233,7 @@ func (c *Container) Notify(m *machine.EventMessage) {
|
|||||||
panic(createErr)
|
panic(createErr)
|
||||||
}
|
}
|
||||||
case "start_delete":
|
case "start_delete":
|
||||||
|
slog.Info("Container.Notify()", "event", "start_delete")
|
||||||
if deleteErr := c.Delete(ctx); deleteErr == nil {
|
if deleteErr := c.Delete(ctx); deleteErr == nil {
|
||||||
if triggerErr := c.StateMachine().Trigger("deleted"); triggerErr == nil {
|
if triggerErr := c.StateMachine().Trigger("deleted"); triggerErr == nil {
|
||||||
return
|
return
|
||||||
@ -242,6 +257,24 @@ func (c *Container) Notify(m *machine.EventMessage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) ReadStat() (err error) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,6 +308,61 @@ func (c *Container) LoadDecl(yamlResourceDeclaration string) error {
|
|||||||
return c.LoadString(yamlResourceDeclaration, codec.FormatYaml)
|
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 {
|
func (c *Container) Create(ctx context.Context) error {
|
||||||
numberOfEnvironmentVariables := len(c.Environment)
|
numberOfEnvironmentVariables := len(c.Environment)
|
||||||
|
|
||||||
@ -288,6 +376,9 @@ func (c *Container) Create(ctx context.Context) error {
|
|||||||
Entrypoint: c.Entrypoint,
|
Entrypoint: c.Entrypoint,
|
||||||
Tty: false,
|
Tty: false,
|
||||||
ExposedPorts: portset,
|
ExposedPorts: portset,
|
||||||
|
WorkingDir: c.WorkingDir,
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Env = make([]string, numberOfEnvironmentVariables)
|
config.Env = make([]string, numberOfEnvironmentVariables)
|
||||||
@ -319,8 +410,17 @@ func (c *Container) Create(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
c.Id = resp.ID
|
c.Id = resp.ID
|
||||||
|
|
||||||
/*
|
if startErr := c.apiClient.ContainerStart(ctx, c.Id, container.StartOptions{}); startErr != nil {
|
||||||
statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
|
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 {
|
select {
|
||||||
case err := <-errCh:
|
case err := <-errCh:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -328,11 +428,14 @@ func (c *Container) Create(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
case <-statusCh:
|
case <-statusCh:
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
if startErr := c.apiClient.ContainerStart(ctx, c.Id, container.StartOptions{}); startErr != nil {
|
|
||||||
return startErr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(c.Stdout) == 0 && len(c.Stderr) == 0 {
|
||||||
|
if err = c.ReadFromContainer(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,9 +489,6 @@ func (c *Container) Inspect(ctx context.Context, containerID string) error {
|
|||||||
}
|
}
|
||||||
c.Common.Path = containerJSON.Path
|
c.Common.Path = containerJSON.Path
|
||||||
c.Image = containerJSON.Image
|
c.Image = containerJSON.Image
|
||||||
if containerJSON.State != nil {
|
|
||||||
c.ContainerState = *containerJSON.State
|
|
||||||
}
|
|
||||||
c.Created = containerJSON.Created
|
c.Created = containerJSON.Created
|
||||||
c.ResolvConfPath = containerJSON.ResolvConfPath
|
c.ResolvConfPath = containerJSON.ResolvConfPath
|
||||||
c.HostnamePath = containerJSON.HostnamePath
|
c.HostnamePath = containerJSON.HostnamePath
|
||||||
@ -396,11 +496,18 @@ func (c *Container) Inspect(ctx context.Context, containerID string) error {
|
|||||||
c.LogPath = containerJSON.LogPath
|
c.LogPath = containerJSON.LogPath
|
||||||
c.RestartCount = containerJSON.RestartCount
|
c.RestartCount = containerJSON.RestartCount
|
||||||
c.Driver = containerJSON.Driver
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) Delete(ctx context.Context) error {
|
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 {
|
if stopErr := c.apiClient.ContainerStop(ctx, c.Id, container.StopOptions{}); stopErr != nil {
|
||||||
slog.Error("Container.Delete() - failed to stop: ", "Id", c.Id, "error", stopErr)
|
slog.Error("Container.Delete() - failed to stop: ", "Id", c.Id, "error", stopErr)
|
||||||
return stopErr
|
return stopErr
|
||||||
|
@ -98,7 +98,7 @@ func (n *ContainerNetwork) SetResourceMapper(resources data.ResourceMapper) {
|
|||||||
|
|
||||||
func (n *ContainerNetwork) Clone() data.Resource {
|
func (n *ContainerNetwork) Clone() data.Resource {
|
||||||
return &ContainerNetwork {
|
return &ContainerNetwork {
|
||||||
Common: n.Common,
|
Common: n.Common.Clone(),
|
||||||
Id: n.Id,
|
Id: n.Id,
|
||||||
Name: n.Name,
|
Name: n.Name,
|
||||||
apiClient: n.apiClient,
|
apiClient: n.apiClient,
|
||||||
|
@ -5,20 +5,21 @@ package resource
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"decl/tests/mocks"
|
"decl/tests/mocks"
|
||||||
_ "encoding/json"
|
_ "encoding/json"
|
||||||
_ "fmt"
|
_ "fmt"
|
||||||
"github.com/docker/docker/api/types"
|
"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/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
_ "io"
|
"io"
|
||||||
_ "net/http"
|
_ "net/http"
|
||||||
_ "net/http/httptest"
|
_ "net/http/httptest"
|
||||||
_ "net/url"
|
_ "net/url"
|
||||||
_ "os"
|
_ "os"
|
||||||
_ "strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"bytes"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewContainerResource(t *testing.T) {
|
func TestNewContainerResource(t *testing.T) {
|
||||||
@ -55,6 +56,9 @@ func TestReadContainer(t *testing.T) {
|
|||||||
go func() { resChan <- res }()
|
go func() { resChan <- res }()
|
||||||
return resChan, errChan
|
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)
|
c := NewContainer(m)
|
||||||
@ -87,6 +91,9 @@ func TestCreateContainer(t *testing.T) {
|
|||||||
go func() { resChan <- res }()
|
go func() { resChan <- res }()
|
||||||
return resChan, errChan
|
return resChan, errChan
|
||||||
},
|
},
|
||||||
|
InjectContainerLogs: func(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
|
||||||
|
return io.NopCloser(strings.NewReader("done.")), nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
decl := `
|
decl := `
|
||||||
@ -107,3 +114,37 @@ func TestCreateContainer(t *testing.T) {
|
|||||||
applyDeleteErr := c.Apply()
|
applyDeleteErr := c.Apply()
|
||||||
assert.Equal(t, nil, applyDeleteErr)
|
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 {
|
if triggerErr := f.StateMachine().Trigger("state_read"); triggerErr == nil {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
f.Common.State = "absent"
|
_ = f.AddError(triggerErr)
|
||||||
panic(triggerErr)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
f.Common.State = "absent"
|
_ = f.AddError(readErr)
|
||||||
if ! errors.Is(readErr, ErrResourceStateAbsent) {
|
if f.IsResourceInconsistent() {
|
||||||
|
if triggerErr := f.StateMachine().Trigger("read-failed"); triggerErr == nil {
|
||||||
panic(readErr)
|
panic(readErr)
|
||||||
|
} else {
|
||||||
|
_ = f.AddError(triggerErr)
|
||||||
|
panic(fmt.Errorf("%w - %w", readErr, triggerErr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ = f.AddError(f.StateMachine().Trigger("notexists"))
|
||||||
|
}
|
||||||
case "start_create":
|
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 {
|
if triggerErr := f.StateMachine().Trigger("created"); triggerErr == nil {
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
_ = f.AddError(triggerErr)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
f.Common.State = "absent"
|
_ = f.AddError(createErr)
|
||||||
panic(e)
|
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":
|
case "start_update":
|
||||||
if updateErr := f.Update(ctx); updateErr == nil {
|
if updateErr := f.Update(ctx); updateErr == nil {
|
||||||
if triggerErr := f.stater.Trigger("updated"); triggerErr == nil {
|
if triggerErr := f.stater.Trigger("updated"); triggerErr == nil {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
f.Common.State = "absent"
|
_ = f.AddError(triggerErr)
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
panic(updateErr)
|
||||||
}
|
}
|
||||||
case "start_delete":
|
case "start_delete":
|
||||||
@ -240,15 +264,21 @@ func (f *File) Notify(m *machine.EventMessage) {
|
|||||||
panic(triggerErr)
|
panic(triggerErr)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
f.Common.State = "present"
|
_ = f.StateMachine().Trigger("exists")
|
||||||
panic(deleteErr)
|
panic(deleteErr)
|
||||||
}
|
}
|
||||||
|
case "inconsistent":
|
||||||
|
f.Common.State = "inconsistent"
|
||||||
case "absent":
|
case "absent":
|
||||||
f.Common.State = "absent"
|
f.Common.State = "absent"
|
||||||
case "present", "created", "read":
|
case "present", "created", "read":
|
||||||
f.Common.State = "present"
|
f.Common.State = "present"
|
||||||
}
|
}
|
||||||
case machine.EXITSTATEEVENT:
|
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-----
|
||||||
|
|
||||||
|
mQGNBGctCH8BDADGmdabVG6gDuSRk0razrEMEproTMT5w9zUMWH5uUeLY9eM9g5S
|
||||||
|
/5I062ume5jj6MIC1lq7tqJXh3Zwcv7Lf7ER1SWa1h6BGruHaF4o9GiR01FHfyVM
|
||||||
|
YTMTkMxFi1wnY87Mr0f+EIv1i9u2nD1o9moBXzEXT0JFFGyla8DvnblQhLhrwfNl
|
||||||
|
lN0L2LQJDTVnrPj4eMaFshqP2UdqNiYjR2qfLyCH/ZZhxg++G2KJhNzlkOzqZZJk
|
||||||
|
iYwfEUvGg/PzdCsSOYEvSureI0bF1hKBGK+RpOY0sKcvSY0xiY1YXEzJSau5cnFe
|
||||||
|
/mdwC7YleZiKsGOyBsbRFn7FUXM4eM7CkDISjZMmKDBzbvzgFXsUG2upgC+B7fvi
|
||||||
|
pTpgQxQk1Xi3+4bNQmgupJEUrP0XlIFoBVJdoTb0wcs8JUNDfc6ESZB+eA1OJdR+
|
||||||
|
xiag1XwN3PKcwzmoZoZ71oT/eqAOufwhWXFJ+StPqwd+BVpK1YwbG0jRagNlM/29
|
||||||
|
+Rzh2A70qwCcCXcAEQEAAbQwVGVzdFVzZXIgKFRlc3RVc2VyKSA8bWF0dGhld3Jp
|
||||||
|
Y2guY29uZkBnbWFpbC5jb20+iQHOBBMBCgA4FiEErhhqUPYtSfwcGHCb+/Evfjwu
|
||||||
|
gEkFAmctCH8CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ+/EvfjwugElu
|
||||||
|
cwv/ZB9xlMf8C9CBOVX8nvU1HiWvmJqlvcePobQBc7Y54pWuK+giv+/SE3bEx6x/
|
||||||
|
Vb0aWrJ52CBod6R1YfyPW+F58W9kADIPFRkH/bXExj+WMrXZU4J8Gz5nCxECK6PB
|
||||||
|
CR8xh/T9lbvDt1q7JeP4+ldzZJoSLxAK6D5EeYTC8OKXVMuTgHBmwtiTC+Hyja3+
|
||||||
|
HV1MZwx7SnnXmX5dRtPq8z1F1shoM4UTLEaolA6r3XQKwfsP9c6LS2VUc+Yft4eN
|
||||||
|
6JCz9+fa/N9bMgIS6Az23JDYJWynmmPx82Y/uqiSxXL9qljOUsgR/QK9OaLL8fFH
|
||||||
|
UD6Ob+TnjH/cPBoESXrslFcwKZWMsAxJ9w6K/HJT+Fm+8XcbN3awoXcEtLAeKirL
|
||||||
|
z7borUsseCXJqY4epHfbvhx7NjhxElspY2A51l6oX4OoVyFL3163anxwzEEXgMRk
|
||||||
|
+pPGlzw55cq/iN48qURetgs94Vdr4HCNJFY8+CLUyNqPQHaVXA6nUndL2wqfOqwj
|
||||||
|
82R0uQGNBGctCH8BDAC/uHoD/vw8dOQt/cHyObaAEunN3Xy2MOtpY7TRh9AdrNKY
|
||||||
|
O0hEFQvllf8iEzW4WjiIXCzNyWzY53AD6k1kWg5tW0/6hLxk9YMUnUhi6MSD17zj
|
||||||
|
QQMR8XRUNuadVh8G0INJnvXVhgJXSQmKCn+4e6e1/gYKvHq9uEYf4N1BSazlCH/e
|
||||||
|
ZEhHTzI8WLtZeG+rM1wBW/3KuRrLDP9WUHamzp+0bL5OKvEhetZQZQxPr9wYccAh
|
||||||
|
bPU9MeatkAn6CwbeCOxUGUbwC0rzMVL3CPvOjhPFWGJaqi4H4ZdSSKN/vceXyfWh
|
||||||
|
CvzzJR/v0jzwJaE6sxIdIu1ylRKXN+hZ7Eqn7ZDurWgVxAH9o0jXkBNGsmZlqdRx
|
||||||
|
J+86/aGpSlNXZZO6o4xznV9Xd8ssuvwMLKN3qwVYEcbFOTdgeRw8dJo8fx4Y14tZ
|
||||||
|
RQUVPLh2iI4ykjFnBJFfOExAEKHQauLgQ6iXRsetgTb5RvUevOvIOJJTZGrqrhxt
|
||||||
|
7lHYlDfxS7zJL9ygldMAEQEAAYkBtgQYAQoAIBYhBK4YalD2LUn8HBhwm/vxL348
|
||||||
|
LoBJBQJnLQh/AhsMAAoJEPvxL348LoBJ+5oMALOv9RIyhNvyeJ4y7TLpOervh/0C
|
||||||
|
EfvIxYEVtDTFZlqfkuovhF1Cbgu+PP9iG2JU0FYHsNisf+1XSMKHX0DIm9gWWZaZ
|
||||||
|
J1CElJ4vsQ0t/4ypSrP7cZB6FokrQBcglpB9mVg0meVzCmZOJfVL+s+gCycshSZR
|
||||||
|
msw9Y3tN72JMAGdxHXtr1DTL3uDbl12Bz+egYNrqmugX9Jc9HiWG51XO9SDtztG0
|
||||||
|
KtVLcBA6X4Avc940Q4d4BofmOT4ajAAnysnR84UvTTSaAr9m/xvyKNEuS4YLysaC
|
||||||
|
gOG8nDFxujEkfO5FW+N1r5hFd2owt8Ige4e59wPRu5RVycPF3+JnxM70wFxQPkO3
|
||||||
|
lDtVTMG9vZyRkxRyKeqFo0z4msbc9WHwdvI6l/h7h2v6E6VbMe2sX/k+CxNyTPBX
|
||||||
|
sn7sjApKUjVpdXtHbu81ELhAbVPJPpMlkTdUwUUUfPD7uBoyRQbEQwgpbPQrEqmE
|
||||||
|
+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-----
|
||||||
|
|
||||||
|
mQGNBGctCH8BDADGmdabVG6gDuSRk0razrEMEproTMT5w9zUMWH5uUeLY9eM9g5S
|
||||||
|
/5I062ume5jj6MIC1lq7tqJXh3Zwcv7Lf7ER1SWa1h6BGruHaF4o9GiR01FHfyVM
|
||||||
|
YTMTkMxFi1wnY87Mr0f+EIv1i9u2nD1o9moBXzEXT0JFFGyla8DvnblQhLhrwfNl
|
||||||
|
lN0L2LQJDTVnrPj4eMaFshqP2UdqNiYjR2qfLyCH/ZZhxg++G2KJhNzlkOzqZZJk
|
||||||
|
iYwfEUvGg/PzdCsSOYEvSureI0bF1hKBGK+RpOY0sKcvSY0xiY1YXEzJSau5cnFe
|
||||||
|
/mdwC7YleZiKsGOyBsbRFn7FUXM4eM7CkDISjZMmKDBzbvzgFXsUG2upgC+B7fvi
|
||||||
|
pTpgQxQk1Xi3+4bNQmgupJEUrP0XlIFoBVJdoTb0wcs8JUNDfc6ESZB+eA1OJdR+
|
||||||
|
xiag1XwN3PKcwzmoZoZ71oT/eqAOufwhWXFJ+StPqwd+BVpK1YwbG0jRagNlM/29
|
||||||
|
+Rzh2A70qwCcCXcAEQEAAbQwVGVzdFVzZXIgKFRlc3RVc2VyKSA8bWF0dGhld3Jp
|
||||||
|
Y2guY29uZkBnbWFpbC5jb20+iQHOBBMBCgA4FiEErhhqUPYtSfwcGHCb+/Evfjwu
|
||||||
|
gEkFAmctCH8CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ+/EvfjwugElu
|
||||||
|
cwv/ZB9xlMf8C9CBOVX8nvU1HiWvmJqlvcePobQBc7Y54pWuK+giv+/SE3bEx6x/
|
||||||
|
Vb0aWrJ52CBod6R1YfyPW+F58W9kADIPFRkH/bXExj+WMrXZU4J8Gz5nCxECK6PB
|
||||||
|
CR8xh/T9lbvDt1q7JeP4+ldzZJoSLxAK6D5EeYTC8OKXVMuTgHBmwtiTC+Hyja3+
|
||||||
|
HV1MZwx7SnnXmX5dRtPq8z1F1shoM4UTLEaolA6r3XQKwfsP9c6LS2VUc+Yft4eN
|
||||||
|
6JCz9+fa/N9bMgIS6Az23JDYJWynmmPx82Y/uqiSxXL9qljOUsgR/QK9OaLL8fFH
|
||||||
|
UD6Ob+TnjH/cPBoESXrslFcwKZWMsAxJ9w6K/HJT+Fm+8XcbN3awoXcEtLAeKirL
|
||||||
|
z7borUsseCXJqY4epHfbvhx7NjhxElspY2A51l6oX4OoVyFL3163anxwzEEXgMRk
|
||||||
|
+pPGlzw55cq/iN48qURetgs94Vdr4HCNJFY8+CLUyNqPQHaVXA6nUndL2wqfOqwj
|
||||||
|
82R0uQGNBGctCH8BDAC/uHoD/vw8dOQt/cHyObaAEunN3Xy2MOtpY7TRh9AdrNKY
|
||||||
|
O0hEFQvllf8iEzW4WjiIXCzNyWzY53AD6k1kWg5tW0/6hLxk9YMUnUhi6MSD17zj
|
||||||
|
QQMR8XRUNuadVh8G0INJnvXVhgJXSQmKCn+4e6e1/gYKvHq9uEYf4N1BSazlCH/e
|
||||||
|
ZEhHTzI8WLtZeG+rM1wBW/3KuRrLDP9WUHamzp+0bL5OKvEhetZQZQxPr9wYccAh
|
||||||
|
bPU9MeatkAn6CwbeCOxUGUbwC0rzMVL3CPvOjhPFWGJaqi4H4ZdSSKN/vceXyfWh
|
||||||
|
CvzzJR/v0jzwJaE6sxIdIu1ylRKXN+hZ7Eqn7ZDurWgVxAH9o0jXkBNGsmZlqdRx
|
||||||
|
J+86/aGpSlNXZZO6o4xznV9Xd8ssuvwMLKN3qwVYEcbFOTdgeRw8dJo8fx4Y14tZ
|
||||||
|
RQUVPLh2iI4ykjFnBJFfOExAEKHQauLgQ6iXRsetgTb5RvUevOvIOJJTZGrqrhxt
|
||||||
|
7lHYlDfxS7zJL9ygldMAEQEAAYkBtgQYAQoAIBYhBK4YalD2LUn8HBhwm/vxL348
|
||||||
|
LoBJBQJnLQh/AhsMAAoJEPvxL348LoBJ+5oMALOv9RIyhNvyeJ4y7TLpOervh/0C
|
||||||
|
EfvIxYEVtDTFZlqfkuovhF1Cbgu+PP9iG2JU0FYHsNisf+1XSMKHX0DIm9gWWZaZ
|
||||||
|
J1CElJ4vsQ0t/4ypSrP7cZB6FokrQBcglpB9mVg0meVzCmZOJfVL+s+gCycshSZR
|
||||||
|
msw9Y3tN72JMAGdxHXtr1DTL3uDbl12Bz+egYNrqmugX9Jc9HiWG51XO9SDtztG0
|
||||||
|
KtVLcBA6X4Avc940Q4d4BofmOT4ajAAnysnR84UvTTSaAr9m/xvyKNEuS4YLysaC
|
||||||
|
gOG8nDFxujEkfO5FW+N1r5hFd2owt8Ige4e59wPRu5RVycPF3+JnxM70wFxQPkO3
|
||||||
|
lDtVTMG9vZyRkxRyKeqFo0z4msbc9WHwdvI6l/h7h2v6E6VbMe2sX/k+CxNyTPBX
|
||||||
|
sn7sjApKUjVpdXtHbu81ELhAbVPJPpMlkTdUwUUUfPD7uBoyRQbEQwgpbPQrEqmE
|
||||||
|
+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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func StorageMachine(sub machine.Subscriber) machine.Stater {
|
// Common resource states
|
||||||
// start_destroy -> absent -> start_create -> present -> start_destroy
|
func ResourceMachine(sub machine.Subscriber) machine.Stater {
|
||||||
stater := machine.New("unknown")
|
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 {
|
if e := stater.AddSubscription("create", sub); e != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -54,7 +54,8 @@ func StorageMachine(sub machine.Subscriber) machine.Stater {
|
|||||||
if e := stater.AddSubscription("created", sub); e != nil {
|
if e := stater.AddSubscription("created", sub); e != nil {
|
||||||
return 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 {
|
if e := stater.AddSubscription("exists", sub); e != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -64,6 +65,24 @@ func StorageMachine(sub machine.Subscriber) machine.Stater {
|
|||||||
return nil
|
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")
|
stater.AddTransition("read", machine.States("*"), "start_read")
|
||||||
if e := stater.AddSubscription("read", sub); e != nil {
|
if e := stater.AddSubscription("read", sub); e != nil {
|
||||||
return nil
|
return nil
|
||||||
@ -98,24 +117,12 @@ func StorageMachine(sub machine.Subscriber) machine.Stater {
|
|||||||
|
|
||||||
func ProcessMachine(sub machine.Subscriber) machine.Stater {
|
func ProcessMachine(sub machine.Subscriber) machine.Stater {
|
||||||
// "enum": [ "created", "restarting", "running", "paused", "exited", "dead" ]
|
// "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 := ResourceMachine(sub)
|
||||||
stater.AddTransition("create", machine.States("unknown", "absent"), "start_create")
|
|
||||||
if e := stater.AddSubscription("create", sub); e != nil {
|
stater.AddStates("restarting", "running", "paused", "exited", "dead")
|
||||||
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.AddTransition("read", machine.States("*"), "start_read")
|
stater.AddTransition("read", machine.States("*"), "start_read")
|
||||||
if e := stater.AddSubscription("read", sub); e != nil {
|
if e := stater.AddSubscription("read", sub); e != nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
{ "$ref": "http-declaration.schema.json" },
|
{ "$ref": "http-declaration.schema.json" },
|
||||||
{ "$ref": "iptable-declaration.schema.json" },
|
{ "$ref": "iptable-declaration.schema.json" },
|
||||||
{ "$ref": "network-route-declaration.schema.json" },
|
{ "$ref": "network-route-declaration.schema.json" },
|
||||||
|
{ "$ref": "openpgp-keyring-declaration.schema.json" },
|
||||||
{ "$ref": "package-declaration.schema.json" },
|
{ "$ref": "package-declaration.schema.json" },
|
||||||
{ "$ref": "pki-declaration.schema.json" },
|
{ "$ref": "pki-declaration.schema.json" },
|
||||||
{ "$ref": "user-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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) Update(ctx context.Context) (error) {
|
func (u *User) Update(ctx context.Context) (err error) {
|
||||||
_, err := u.UpdateCommand.Execute(u)
|
switch u.UserType {
|
||||||
|
case UserTypeBusyBox:
|
||||||
|
_, err = u.CreateCommand.Execute(u)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = u.UpdateCommand.Execute(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_,e := u.Read(ctx)
|
_, err = u.Read(ctx)
|
||||||
return e
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) Delete(ctx context.Context) (error) {
|
func (u *User) Delete(ctx context.Context) (error) {
|
||||||
@ -489,11 +496,16 @@ func NewUserShadowCreateCommand() *command.Command {
|
|||||||
func NewUserBusyBoxCreateCommand() *command.Command {
|
func NewUserBusyBoxCreateCommand() *command.Command {
|
||||||
c := command.NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "adduser"
|
c.Path = "adduser"
|
||||||
|
c.Split = false
|
||||||
c.Args = []command.CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
command.CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
|
command.CommandArg("{{ if .UID }}-u{{ end }}"),
|
||||||
command.CommandArg("{{ if .Gecos }}-g {{ .Gecos }}{{ end }}"),
|
command.CommandArg("{{ if .UID }}{{ .UID }}{{ end }}"),
|
||||||
command.CommandArg("{{ if .Group }}-G {{ .Group }}{{ end }}"),
|
command.CommandArg("{{ if .Gecos }}-g{{ end }}"),
|
||||||
command.CommandArg("{{ if .Home }}-h {{ .Home }}{{ 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("{{ if not .CreateHome }}-H{{ end }}"),
|
||||||
command.CommandArg("-D"),
|
command.CommandArg("-D"),
|
||||||
command.CommandArg("{{ .Name }}"),
|
command.CommandArg("{{ .Name }}"),
|
||||||
@ -544,8 +556,11 @@ func NewUserBusyBoxUpdateCommand() *command.Command {
|
|||||||
c.StdinAvailable = true
|
c.StdinAvailable = true
|
||||||
c.Input = command.CommandInput("{{ if .Groups }}{{ range .Groups }}{{ . }}\n{{ end }}{{ end }}")
|
c.Input = command.CommandInput("{{ if .Groups }}{{ range .Groups }}{{ . }}\n{{ end }}{{ end }}")
|
||||||
c.Args = []command.CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("-r"),
|
||||||
|
command.CommandArg("-IGROUP"),
|
||||||
command.CommandArg("adduser"),
|
command.CommandArg("adduser"),
|
||||||
command.CommandArg("{{ .Name }}"),
|
command.CommandArg("{{ .Name }}"),
|
||||||
|
command.CommandArg("GROUP"),
|
||||||
}
|
}
|
||||||
c.Extractor = func(out []byte, target any) error {
|
c.Extractor = func(out []byte, target any) error {
|
||||||
return nil
|
return nil
|
||||||
|
Loading…
Reference in New Issue
Block a user