go fmt
All checks were successful
Declarative Tests / test (push) Successful in 48s

This commit is contained in:
Matthew Rich 2024-03-25 13:31:06 -07:00
parent e71d177984
commit e695278d0c
18 changed files with 930 additions and 943 deletions

View File

@ -1,260 +1,257 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved. // Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
// Container resource // Container resource
package resource package resource
import ( import (
"context" "context"
"fmt" "fmt"
_ "os" "github.com/docker/docker/api/types"
_ "gopkg.in/yaml.v3" "github.com/docker/docker/api/types/container"
_ "os/exec" "github.com/docker/docker/api/types/filters"
_ "strings" "github.com/docker/docker/api/types/mount"
"log/slog" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/strslice" "github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/api/types/mount" "github.com/docker/docker/client"
"github.com/docker/docker/api/types/container" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/docker/docker/api/types/filters" "gopkg.in/yaml.v3"
"github.com/docker/docker/api/types" _ "gopkg.in/yaml.v3"
"github.com/docker/docker/client" "log/slog"
"github.com/docker/docker/api/types/network" "net/url"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" _ "os"
"gopkg.in/yaml.v3" _ "os/exec"
"net/url" "path/filepath"
"path/filepath" _ "strings"
) )
type ContainerClient interface { type ContainerClient interface {
ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error)
ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error
ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error) ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error)
ContainerInspect(context.Context, string) (types.ContainerJSON, error) ContainerInspect(context.Context, string) (types.ContainerJSON, error)
ContainerRemove(context.Context, string, container.RemoveOptions) error ContainerRemove(context.Context, string, container.RemoveOptions) error
Close() error Close() error
} }
type Container struct { type Container struct {
loader YamlLoader loader YamlLoader
Id string `yaml:"ID",omitempty` Id string `yaml:"ID",omitempty`
Name string `yaml:"name"` Name string `yaml:"name"`
Path string `yaml:"path"` Path string `yaml:"path"`
Cmd []string `yaml:"cmd",omitempty` Cmd []string `yaml:"cmd",omitempty`
Entrypoint strslice.StrSlice `yaml:"entrypoint",omitempty` Entrypoint strslice.StrSlice `yaml:"entrypoint",omitempty`
Args []string `yaml:"args",omitempty` Args []string `yaml:"args",omitempty`
Environment map[string]string `yaml:"environment"` Environment map[string]string `yaml:"environment"`
Image string `yaml:"image"` Image string `yaml:"image"`
ResolvConfPath string `yaml:"resolvconfpath"` ResolvConfPath string `yaml:"resolvconfpath"`
HostnamePath string `yaml:"hostnamepath"` HostnamePath string `yaml:"hostnamepath"`
HostsPath string `yaml:"hostspath"` HostsPath string `yaml:"hostspath"`
LogPath string `yaml:"logpath"` LogPath string `yaml:"logpath"`
Created string `yaml:"created"` Created string `yaml:"created"`
ContainerState types.ContainerState `yaml:"containerstate"` ContainerState types.ContainerState `yaml:"containerstate"`
RestartCount int `yaml:"restartcount"` RestartCount int `yaml:"restartcount"`
Driver string `yaml:"driver"` Driver string `yaml:"driver"`
Platform string `yaml:"platform"` Platform string `yaml:"platform"`
MountLabel string `yaml:"mountlabel"` MountLabel string `yaml:"mountlabel"`
ProcessLabel string `yaml:"processlabel"` ProcessLabel string `yaml:"processlabel"`
AppArmorProfile string `yaml:"apparmorprofile"` AppArmorProfile string `yaml:"apparmorprofile"`
ExecIDs []string `yaml:"execids"` ExecIDs []string `yaml:"execids"`
HostConfig container.HostConfig `yaml:"hostconfig"` HostConfig container.HostConfig `yaml:"hostconfig"`
GraphDriver types.GraphDriverData `yaml:"graphdriver"` GraphDriver types.GraphDriverData `yaml:"graphdriver"`
SizeRw *int64 `json:",omitempty"` SizeRw *int64 `json:",omitempty"`
SizeRootFs *int64 `json:",omitempty"` SizeRootFs *int64 `json:",omitempty"`
/* /*
Mounts []MountPoint Mounts []MountPoint
Config *container.Config Config *container.Config
NetworkSettings *NetworkSettings NetworkSettings *NetworkSettings
*/ */
State string `yaml:"state"` State string `yaml:"state"`
apiClient ContainerClient apiClient ContainerClient
} }
func init() { func init() {
ResourceTypes.Register("container", func(u *url.URL) Resource { ResourceTypes.Register("container", func(u *url.URL) Resource {
c := NewContainer(nil) c := NewContainer(nil)
c.Name = filepath.Join(u.Hostname(), u.Path) c.Name = filepath.Join(u.Hostname(), u.Path)
return c return c
}) })
} }
func NewContainer(containerClientApi ContainerClient) *Container { func NewContainer(containerClientApi ContainerClient) *Container {
var apiClient ContainerClient = containerClientApi var apiClient ContainerClient = containerClientApi
if apiClient == nil { if apiClient == nil {
var err error var err error
apiClient, err = client.NewClientWithOpts(client.FromEnv) apiClient, err = client.NewClientWithOpts(client.FromEnv)
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
return &Container{ return &Container{
loader: YamlLoadDecl, loader: YamlLoadDecl,
apiClient: apiClient, apiClient: apiClient,
} }
} }
func (c *Container) URI() string { func (c *Container) URI() string {
return fmt.Sprintf("container://%s", c.Id) return fmt.Sprintf("container://%s", c.Id)
} }
func (c *Container) SetURI(uri string) error { func (c *Container) SetURI(uri string) error {
resourceUri, e := url.Parse(uri) resourceUri, e := url.Parse(uri)
if resourceUri.Scheme == c.Type() { if resourceUri.Scheme == c.Type() {
c.Name, e = filepath.Abs(filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI())) c.Name, e = filepath.Abs(filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI()))
} else { } else {
e = fmt.Errorf("%w: %s is not a %s", ErrInvalidResourceURI, uri, c.Type()) e = fmt.Errorf("%w: %s is not a %s", ErrInvalidResourceURI, uri, c.Type())
} }
return e return e
} }
func (c *Container) Apply() error { func (c *Container) Apply() error {
ctx := context.Background() ctx := context.Background()
switch c.State { switch c.State {
case "absent": case "absent":
return c.Delete(ctx) return c.Delete(ctx)
case "present": case "present":
return c.Create(ctx) return c.Create(ctx)
} }
return nil return nil
} }
func (c *Container) LoadDecl(yamlFileResourceDeclaration string) error { func (c *Container) LoadDecl(yamlFileResourceDeclaration string) error {
return c.loader(yamlFileResourceDeclaration, c) return c.loader(yamlFileResourceDeclaration, c)
} }
func (c *Container) Create(ctx context.Context) error { func (c *Container) Create(ctx context.Context) error {
numberOfEnvironmentVariables := len(c.Environment) numberOfEnvironmentVariables := len(c.Environment)
config := &container.Config { config := &container.Config{
Image: c.Image, Image: c.Image,
Cmd: c.Cmd, Cmd: c.Cmd,
Entrypoint: c.Entrypoint, Entrypoint: c.Entrypoint,
Tty: false, Tty: false,
} }
config.Env = make([]string, numberOfEnvironmentVariables) config.Env = make([]string, numberOfEnvironmentVariables)
index := 0 index := 0
for k,v := range c.Environment { for k, v := range c.Environment {
config.Env[index] = k + "=" + v config.Env[index] = k + "=" + v
index++ index++
} }
for i := range c.HostConfig.Mounts { for i := range c.HostConfig.Mounts {
if c.HostConfig.Mounts[i].Type == mount.TypeBind { if c.HostConfig.Mounts[i].Type == mount.TypeBind {
if mountSourceAbsolutePath,e := filepath.Abs(c.HostConfig.Mounts[i].Source); e == nil { if mountSourceAbsolutePath, e := filepath.Abs(c.HostConfig.Mounts[i].Source); e == nil {
c.HostConfig.Mounts[i].Source = mountSourceAbsolutePath c.HostConfig.Mounts[i].Source = mountSourceAbsolutePath
} }
} }
} }
resp, err := c.apiClient.ContainerCreate(ctx, config, &c.HostConfig, nil, nil, c.Name) resp, err := c.apiClient.ContainerCreate(ctx, config, &c.HostConfig, nil, nil, c.Name)
if err != nil { if err != nil {
panic(err) panic(err)
} }
c.Id = resp.ID c.Id = resp.ID
/* /*
statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
select { select {
case err := <-errCh: case err := <-errCh:
if err != nil { if err != nil {
panic(err) panic(err)
} }
case <-statusCh: case <-statusCh:
} }
*/ */
if startErr := c.apiClient.ContainerStart(ctx, c.Id, types.ContainerStartOptions{}); startErr != nil { if startErr := c.apiClient.ContainerStart(ctx, c.Id, types.ContainerStartOptions{}); startErr != nil {
return startErr return startErr
} }
return err return err
} }
// produce yaml representation of any resource // produce yaml representation of any resource
func (c *Container) Read(ctx context.Context) ([]byte, error) { func (c *Container) Read(ctx context.Context) ([]byte, error) {
var containerID string var containerID string
filterArgs := filters.NewArgs() filterArgs := filters.NewArgs()
filterArgs.Add("name", "/" + c.Name) filterArgs.Add("name", "/"+c.Name)
containers,err := c.apiClient.ContainerList(ctx, types.ContainerListOptions{ containers, err := c.apiClient.ContainerList(ctx, types.ContainerListOptions{
All: true, All: true,
Filters: filterArgs, Filters: filterArgs,
}) })
if err != nil { if err != nil {
panic(fmt.Errorf("%w: %s %s", err, c.Type(), c.Name)) panic(fmt.Errorf("%w: %s %s", err, c.Type(), c.Name))
} }
for _, container := range containers { for _, container := range containers {
for _, containerName := range container.Names { for _, containerName := range container.Names {
if containerName == "/" + c.Name { if containerName == "/"+c.Name {
containerID = container.ID containerID = container.ID
} }
} }
} }
containerJSON, err := c.apiClient.ContainerInspect(ctx, containerID) containerJSON, err := c.apiClient.ContainerInspect(ctx, containerID)
if client.IsErrNotFound(err) { if client.IsErrNotFound(err) {
c.State = "absent" c.State = "absent"
} else { } else {
c.State = "present" c.State = "present"
c.Id = containerJSON.ID c.Id = containerJSON.ID
if c.Name == "" { if c.Name == "" {
c.Name = containerJSON.Name c.Name = containerJSON.Name
} }
c.Path = containerJSON.Path c.Path = containerJSON.Path
c.Image = containerJSON.Image c.Image = containerJSON.Image
if containerJSON.State != nil { if containerJSON.State != nil {
c.ContainerState = *containerJSON.State 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
c.HostsPath = containerJSON.HostsPath c.HostsPath = containerJSON.HostsPath
c.LogPath = containerJSON.LogPath c.LogPath = containerJSON.LogPath
c.RestartCount = containerJSON.RestartCount c.RestartCount = containerJSON.RestartCount
c.Driver = containerJSON.Driver c.Driver = containerJSON.Driver
} }
slog.Info("Read() ", "type", c.Type(), "name", c.Name, "Id", c.Id) slog.Info("Read() ", "type", c.Type(), "name", c.Name, "Id", c.Id)
return yaml.Marshal(c) return yaml.Marshal(c)
} }
func (c *Container) Delete(ctx context.Context) error { func (c *Container) Delete(ctx context.Context) error {
err := c.apiClient.ContainerRemove(ctx, c.Id, types.ContainerRemoveOptions{ err := c.apiClient.ContainerRemove(ctx, c.Id, types.ContainerRemoveOptions{
RemoveVolumes: true, RemoveVolumes: true,
Force: false, Force: false,
}) })
if err != nil { if err != nil {
slog.Error("Failed to remove: ", "Id", c.Id) slog.Error("Failed to remove: ", "Id", c.Id)
panic(err) panic(err)
} }
return err return err
} }
func (c *Container) Type() string { return "container" } func (c *Container) Type() string { return "container" }
func (c *Container) ResolveId(ctx context.Context) string { func (c *Container) ResolveId(ctx context.Context) string {
filterArgs := filters.NewArgs() filterArgs := filters.NewArgs()
filterArgs.Add("name", "/" + c.Name) filterArgs.Add("name", "/"+c.Name)
containers,err := c.apiClient.ContainerList(ctx, types.ContainerListOptions{ containers, err := c.apiClient.ContainerList(ctx, types.ContainerListOptions{
All: true, All: true,
Filters: filterArgs, Filters: filterArgs,
}) })
if err != nil { if err != nil {
panic(fmt.Errorf("%w: %s %s", err, c.Type(), c.Name)) panic(fmt.Errorf("%w: %s %s", err, c.Type(), c.Name))
} }
for _, container := range containers { for _, container := range containers {
for _, containerName := range container.Names { for _, containerName := range container.Names {
if containerName == c.Name { if containerName == c.Name {
if c.Id == "" { if c.Id == "" {
c.Id = container.ID c.Id = container.ID
} }
return container.ID return container.ID
} }
} }
} }
return "" return ""
} }

View File

@ -1,93 +1,92 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved. // Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
//
package resource package resource
import ( import (
_ "fmt" "context"
"context" "decl/tests/mocks"
"testing" _ "encoding/json"
_ "net/http" _ "fmt"
_ "net/http/httptest" "github.com/docker/docker/api/types"
_ "net/url" "github.com/docker/docker/api/types/container"
_ "io" "github.com/docker/docker/api/types/network"
_ "os" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
_ "encoding/json" _ "io"
_ "strings" _ "net/http"
"decl/tests/mocks" _ "net/http/httptest"
"github.com/docker/docker/api/types" _ "net/url"
"github.com/docker/docker/api/types/container" _ "os"
"github.com/docker/docker/api/types/network" _ "strings"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" "testing"
) )
func TestNewContainerResource(t *testing.T) { func TestNewContainerResource(t *testing.T) {
c := NewContainer(&mocks.MockContainerClient{}) c := NewContainer(&mocks.MockContainerClient{})
assert.NotEqual(t, nil, c) assert.NotEqual(t, nil, c)
} }
func TestReadContainer(t *testing.T) { func TestReadContainer(t *testing.T) {
ctx := context.Background() ctx := context.Background()
decl := ` decl := `
name: "testcontainer" name: "testcontainer"
image: "alpine" image: "alpine"
state: present state: present
` `
m := &mocks.MockContainerClient { m := &mocks.MockContainerClient{
InjectContainerList: func(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) { InjectContainerList: func(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) {
return []types.Container{ return []types.Container{
{ ID: "123456789abc" }, {ID: "123456789abc"},
{ ID: "123456789def" }, {ID: "123456789def"},
}, nil }, nil
}, },
InjectContainerInspect: func(ctx context.Context, containerID string) (types.ContainerJSON, error) { InjectContainerInspect: func(ctx context.Context, containerID string) (types.ContainerJSON, error) {
return types.ContainerJSON{ return types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{ ContainerJSONBase: &types.ContainerJSONBase{
ID: "123456789abc", ID: "123456789abc",
Name: "test", Name: "test",
Image: "alpine", Image: "alpine",
} }, nil }}, nil
}, },
} }
c := NewContainer(m) c := NewContainer(m)
assert.NotEqual(t, nil, c) assert.NotEqual(t, nil, c)
e := c.LoadDecl(decl) e := c.LoadDecl(decl)
assert.Equal(t, nil, e) assert.Equal(t, nil, e)
assert.Equal(t, "testcontainer", c.Name) assert.Equal(t, "testcontainer", c.Name)
resourceYaml, readContainerErr := c.Read(ctx) resourceYaml, readContainerErr := c.Read(ctx)
assert.Equal(t, nil, readContainerErr) assert.Equal(t, nil, readContainerErr)
assert.Greater(t, len(resourceYaml), 0) assert.Greater(t, len(resourceYaml), 0)
} }
func TestCreateContainer(t *testing.T) { func TestCreateContainer(t *testing.T) {
m := &mocks.MockContainerClient { 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) { InjectContainerCreate: func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) {
return container.CreateResponse{ ID: "abcdef012", Warnings: []string{} }, nil return container.CreateResponse{ID: "abcdef012", Warnings: []string{}}, nil
}, },
InjectContainerRemove: func(context.Context, string, container.RemoveOptions) error { InjectContainerRemove: func(context.Context, string, container.RemoveOptions) error {
return nil return nil
}, },
} }
decl := ` decl := `
name: "testcontainer" name: "testcontainer"
image: "alpine" image: "alpine"
state: present state: present
` `
c := NewContainer(m) c := NewContainer(m)
e := c.LoadDecl(decl) e := c.LoadDecl(decl)
assert.Equal(t, nil, e) assert.Equal(t, nil, e)
assert.Equal(t, "testcontainer", c.Name) assert.Equal(t, "testcontainer", c.Name)
applyErr := c.Apply() applyErr := c.Apply()
assert.Equal(t, nil, applyErr) assert.Equal(t, nil, applyErr)
c.State = "absent" c.State = "absent"
applyDeleteErr := c.Apply() applyDeleteErr := c.Apply()
assert.Equal(t, nil, applyDeleteErr) assert.Equal(t, nil, applyDeleteErr)
} }

View File

@ -1,84 +1,83 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved. // Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
//
package resource package resource
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog" "gopkg.in/yaml.v3"
"gopkg.in/yaml.v3" "log/slog"
) )
type Declaration struct { type Declaration struct {
Type string `yaml:"type"` Type string `yaml:"type"`
Attributes yaml.Node `yaml:"attributes"` Attributes yaml.Node `yaml:"attributes"`
Implementation Resource `-` Implementation Resource `-`
} }
type ResourceLoader interface { type ResourceLoader interface {
LoadDecl(string) error LoadDecl(string) error
} }
type StateTransformer interface { type StateTransformer interface {
Apply() error Apply() error
} }
type YamlLoader func(string, any) error type YamlLoader func(string, any) error
func YamlLoadDecl(yamlFileResourceDeclaration string, resource any) error { func YamlLoadDecl(yamlFileResourceDeclaration string, resource any) error {
if err := yaml.Unmarshal([]byte(yamlFileResourceDeclaration), resource); err != nil { if err := yaml.Unmarshal([]byte(yamlFileResourceDeclaration), resource); err != nil {
return err return err
} }
return nil return nil
} }
func NewDeclaration() *Declaration { func NewDeclaration() *Declaration {
return &Declaration{} return &Declaration{}
} }
func (d *Declaration) LoadDecl(yamlResourceDeclaration string) error { func (d *Declaration) LoadDecl(yamlResourceDeclaration string) error {
return YamlLoadDecl(yamlResourceDeclaration, d) return YamlLoadDecl(yamlResourceDeclaration, d)
} }
func (d *Declaration) NewResource() error { func (d *Declaration) NewResource() error {
uri := fmt.Sprintf("%s://", d.Type) uri := fmt.Sprintf("%s://", d.Type)
newResource, err := ResourceTypes.New(uri) newResource, err := ResourceTypes.New(uri)
d.Implementation = newResource d.Implementation = newResource
return err return err
} }
func (d *Declaration) LoadResourceFromYaml() (Resource, error) { func (d *Declaration) LoadResourceFromYaml() (Resource, error) {
var errResource error var errResource error
if d.Implementation == nil { if d.Implementation == nil {
errResource = d.NewResource() errResource = d.NewResource()
if errResource != nil { if errResource != nil {
return nil, errResource return nil, errResource
} }
} }
d.Attributes.Decode(d.Implementation) d.Attributes.Decode(d.Implementation)
d.Implementation.ResolveId(context.Background()) d.Implementation.ResolveId(context.Background())
return d.Implementation, errResource return d.Implementation, errResource
} }
func (d *Declaration) UpdateYamlFromResource() error { func (d *Declaration) UpdateYamlFromResource() error {
if d.Implementation != nil { if d.Implementation != nil {
return d.Attributes.Encode(d.Implementation) return d.Attributes.Encode(d.Implementation)
} }
return nil return nil
} }
func (d *Declaration) Resource() Resource { func (d *Declaration) Resource() Resource {
return d.Implementation return d.Implementation
} }
func (d *Declaration) SetURI(uri string) error { func (d *Declaration) SetURI(uri string) error {
slog.Info("SetURI()", "uri", uri) slog.Info("SetURI()", "uri", uri)
d.Implementation = NewResource(uri) d.Implementation = NewResource(uri)
if d.Implementation == nil { if d.Implementation == nil {
panic("unknown resource") panic("unknown resource")
} }
d.Type = d.Implementation.Type() d.Type = d.Implementation.Type()
d.Implementation.Read(context.Background()) // fix context d.Implementation.Read(context.Background()) // fix context
return nil return nil
} }

View File

@ -2,20 +2,20 @@
package resource package resource
import ( import (
_ "os" "fmt"
"path/filepath" "github.com/stretchr/testify/assert"
"fmt" _ "log"
_ "log" _ "os"
"testing" "path/filepath"
"github.com/stretchr/testify/assert" "testing"
) )
func TestYamlLoadDecl(t *testing.T) { func TestYamlLoadDecl(t *testing.T) {
file := filepath.Join(TempDir, "fooread.txt") file := filepath.Join(TempDir, "fooread.txt")
resourceAttributes := make(map[string]any) resourceAttributes := make(map[string]any)
decl := fmt.Sprintf(` decl := fmt.Sprintf(`
path: "%s" path: "%s"
owner: "nobody" owner: "nobody"
group: "nobody" group: "nobody"
@ -25,21 +25,21 @@ func TestYamlLoadDecl(t *testing.T) {
test line 2 test line 2
`, file) `, file)
e := YamlLoadDecl(decl, &resourceAttributes) e := YamlLoadDecl(decl, &resourceAttributes)
assert.Equal(t, nil, e) assert.Equal(t, nil, e)
assert.Equal(t, "nobody", resourceAttributes["group"]) assert.Equal(t, "nobody", resourceAttributes["group"])
} }
func TestNewResourceDeclaration(t *testing.T) { func TestNewResourceDeclaration(t *testing.T) {
resourceDeclaration := NewDeclaration() resourceDeclaration := NewDeclaration()
assert.NotEqual(t, nil, resourceDeclaration) assert.NotEqual(t, nil, resourceDeclaration)
} }
func TestNewResourceDeclarationType(t *testing.T) { func TestNewResourceDeclarationType(t *testing.T) {
file := filepath.Join(TempDir, "fooread.txt") file := filepath.Join(TempDir, "fooread.txt")
decl := fmt.Sprintf(` decl := fmt.Sprintf(`
type: file type: file
attributes: attributes:
path: "%s" path: "%s"
@ -51,25 +51,25 @@ func TestNewResourceDeclarationType(t *testing.T) {
test line 2 test line 2
`, file) `, file)
resourceDeclaration := NewDeclaration() resourceDeclaration := NewDeclaration()
assert.NotEqual(t, nil, resourceDeclaration) assert.NotEqual(t, nil, resourceDeclaration)
resourceDeclaration.LoadDecl(decl) resourceDeclaration.LoadDecl(decl)
assert.Equal(t, "file", resourceDeclaration.Type) assert.Equal(t, "file", resourceDeclaration.Type)
assert.NotEqual(t, nil, resourceDeclaration.Attributes) assert.NotEqual(t, nil, resourceDeclaration.Attributes)
} }
func TestDeclarationNewResource(t *testing.T) { func TestDeclarationNewResource(t *testing.T) {
resourceDeclaration := NewDeclaration() resourceDeclaration := NewDeclaration()
assert.NotNil(t, resourceDeclaration) assert.NotNil(t, resourceDeclaration)
errNewUnknownResource := resourceDeclaration.NewResource() errNewUnknownResource := resourceDeclaration.NewResource()
assert.ErrorIs(t, errNewUnknownResource, ErrUnknownResourceType) assert.ErrorIs(t, errNewUnknownResource, ErrUnknownResourceType)
resourceDeclaration.Type = "file" resourceDeclaration.Type = "file"
errNewFileResource := resourceDeclaration.NewResource() errNewFileResource := resourceDeclaration.NewResource()
assert.Nil(t, errNewFileResource) assert.Nil(t, errNewFileResource)
//assert.NotNil(t, resourceDeclaration.Implementation) //assert.NotNil(t, resourceDeclaration.Implementation)
assert.NotNil(t, resourceDeclaration.Attributes) assert.NotNil(t, resourceDeclaration.Attributes)
} }

View File

@ -1,69 +1,68 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved. // Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
//
package resource package resource
import ( import (
_ "fmt" _ "fmt"
_ "log" "gopkg.in/yaml.v3"
"io" "io"
"gopkg.in/yaml.v3" _ "log"
_ "net/url" _ "net/url"
) )
type Document struct { type Document struct {
ResourceDecls []Declaration `yaml:"resources"` ResourceDecls []Declaration `yaml:"resources"`
} }
func NewDocument() *Document { func NewDocument() *Document {
return &Document {} return &Document{}
} }
func (d *Document) Load(r io.Reader) error { func (d *Document) Load(r io.Reader) error {
yamlDecoder := yaml.NewDecoder(r) yamlDecoder := yaml.NewDecoder(r)
yamlDecoder.Decode(d) yamlDecoder.Decode(d)
for i := range(d.ResourceDecls) { for i := range d.ResourceDecls {
if _,e := d.ResourceDecls[i].LoadResourceFromYaml(); e != nil { if _, e := d.ResourceDecls[i].LoadResourceFromYaml(); e != nil {
return e return e
} }
} }
return nil return nil
} }
func (d *Document) Resources() []Declaration { func (d *Document) Resources() []Declaration {
return d.ResourceDecls return d.ResourceDecls
} }
func (d *Document) Apply() error { func (d *Document) Apply() error {
for i := range(d.ResourceDecls) { for i := range d.ResourceDecls {
if e := d.ResourceDecls[i].Resource().Apply(); e != nil { if e := d.ResourceDecls[i].Resource().Apply(); e != nil {
return e return e
} }
} }
return nil return nil
} }
func (d *Document) Generate(w io.Writer) (error) { func (d *Document) Generate(w io.Writer) error {
yamlEncoder := yaml.NewEncoder(w) yamlEncoder := yaml.NewEncoder(w)
yamlEncoder.Encode(d) yamlEncoder.Encode(d)
return yamlEncoder.Close() return yamlEncoder.Close()
} }
func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration Resource) { func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration Resource) {
decl := NewDeclaration() decl := NewDeclaration()
decl.Type = resourceType decl.Type = resourceType
decl.Implementation = resourceDeclaration decl.Implementation = resourceDeclaration
decl.UpdateYamlFromResource() decl.UpdateYamlFromResource()
d.ResourceDecls = append(d.ResourceDecls, *decl) d.ResourceDecls = append(d.ResourceDecls, *decl)
} }
func (d *Document) AddResource(uri string) error { func (d *Document) AddResource(uri string) error {
//parsedResourceURI, e := url.Parse(uri) //parsedResourceURI, e := url.Parse(uri)
//if e == nil { //if e == nil {
decl := NewDeclaration() decl := NewDeclaration()
decl.SetURI(uri) decl.SetURI(uri)
decl.UpdateYamlFromResource() decl.UpdateYamlFromResource()
d.ResourceDecls = append(d.ResourceDecls, *decl) d.ResourceDecls = append(d.ResourceDecls, *decl)
//} //}
return nil return nil
} }

View File

@ -2,33 +2,33 @@
package resource package resource
import ( import (
"context" "context"
"os" "fmt"
"fmt" "github.com/stretchr/testify/assert"
"log" "log"
"strings" "os"
"path/filepath" "path/filepath"
"testing" "strings"
"github.com/stretchr/testify/assert" "syscall"
"time" "testing"
"syscall" "time"
) )
func TestNewDocumentLoader(t *testing.T) { func TestNewDocumentLoader(t *testing.T) {
d := NewDocument() d := NewDocument()
assert.NotEqual(t, nil, d) assert.NotEqual(t, nil, d)
} }
func TestDocumentLoader(t *testing.T) { func TestDocumentLoader(t *testing.T) {
dir, err := os.MkdirTemp("", "testdocumentloader") dir, err := os.MkdirTemp("", "testdocumentloader")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
file,_ := filepath.Abs(filepath.Join(dir, "foo.txt")) file, _ := filepath.Abs(filepath.Join(dir, "foo.txt"))
document := fmt.Sprintf(` document := fmt.Sprintf(`
--- ---
resources: resources:
- type: file - type: file
@ -50,39 +50,39 @@ resources:
state: present state: present
`, file) `, file)
d := NewDocument() d := NewDocument()
assert.NotEqual(t, nil, d) assert.NotEqual(t, nil, d)
docReader := strings.NewReader(document) docReader := strings.NewReader(document)
e := d.Load(docReader) e := d.Load(docReader)
assert.Equal(t, nil, e) assert.Equal(t, nil, e)
resources := d.Resources() resources := d.Resources()
assert.Equal(t, 2, len(resources)) assert.Equal(t, 2, len(resources))
} }
func TestDocumentGenerator(t *testing.T) { func TestDocumentGenerator(t *testing.T) {
ctx := context.Background() ctx := context.Background()
fileContent := `// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved. fileContent := `// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
` `
file,_ := filepath.Abs(filepath.Join(TempDir, "foo.txt")) file, _ := filepath.Abs(filepath.Join(TempDir, "foo.txt"))
err := os.WriteFile(file, []byte(fileContent), 0644) err := os.WriteFile(file, []byte(fileContent), 0644)
assert.Nil(t, err) assert.Nil(t, err)
info,statErr := os.Stat(file) info, statErr := os.Stat(file)
assert.Nil(t, statErr) assert.Nil(t, statErr)
mTime := info.ModTime() mTime := info.ModTime()
stat, ok := info.Sys().(*syscall.Stat_t) stat, ok := info.Sys().(*syscall.Stat_t)
assert.True(t, ok) assert.True(t, ok)
aTime := time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) aTime := time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec))
cTime := time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec)) cTime := time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))
expected := fmt.Sprintf(` expected := fmt.Sprintf(`
resources: resources:
- type: file - type: file
attributes: attributes:
@ -99,31 +99,31 @@ resources:
state: present state: present
`, file, fileContent, aTime.Format(time.RFC3339Nano), cTime.Format(time.RFC3339Nano), mTime.Format(time.RFC3339Nano)) `, file, fileContent, aTime.Format(time.RFC3339Nano), cTime.Format(time.RFC3339Nano), mTime.Format(time.RFC3339Nano))
var documentYaml strings.Builder var documentYaml strings.Builder
d := NewDocument() d := NewDocument()
assert.NotEqual(t, nil, d) assert.NotEqual(t, nil, d)
f,e := ResourceTypes.New("file://") f, e := ResourceTypes.New("file://")
assert.Nil(t, e) assert.Nil(t, e)
assert.NotNil(t, f) assert.NotNil(t, f)
f.(*File).Path = filepath.Join(TempDir, "foo.txt") f.(*File).Path = filepath.Join(TempDir, "foo.txt")
f.(*File).Read(ctx) f.(*File).Read(ctx)
d.AddResourceDeclaration("file", f) d.AddResourceDeclaration("file", f)
ey := d.Generate(&documentYaml) ey := d.Generate(&documentYaml)
assert.Equal(t, nil, ey) assert.Equal(t, nil, ey)
assert.Greater(t, documentYaml.Len(), 0) assert.Greater(t, documentYaml.Len(), 0)
assert.YAMLEq(t, expected, documentYaml.String()) assert.YAMLEq(t, expected, documentYaml.String())
} }
func TestDocumentAddResource(t *testing.T) { func TestDocumentAddResource(t *testing.T) {
file,_ := filepath.Abs(filepath.Join(TempDir, "foo.txt")) file, _ := filepath.Abs(filepath.Join(TempDir, "foo.txt"))
err := os.WriteFile(file, []byte(""), 0644) err := os.WriteFile(file, []byte(""), 0644)
assert.Nil(t, err) assert.Nil(t, err)
d := NewDocument() d := NewDocument()
assert.NotNil(t, d) assert.NotNil(t, d)
d.AddResource(fmt.Sprintf("file://%s", file)) d.AddResource(fmt.Sprintf("file://%s", file))
} }

View File

@ -1,65 +1,63 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved. // Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
//
package resource package resource
import ( import (
"context" "context"
"fmt" "fmt"
_ "os" "gopkg.in/yaml.v3"
"gopkg.in/yaml.v3" _ "log"
_ "os/exec" "net/url"
_ "strings" _ "os"
_ "log" _ "os/exec"
"net/url" _ "strings"
) )
type Exec struct { type Exec struct {
loader YamlLoader loader YamlLoader
Id string `yaml:"id"` Id string `yaml:"id"`
// create command // create command
// read command // read command
// update command // update command
// delete command // delete command
// state attributes // state attributes
State string `yaml:"state"` State string `yaml:"state"`
} }
func init() { func init() {
ResourceTypes.Register("exec", func(u *url.URL) Resource { ResourceTypes.Register("exec", func(u *url.URL) Resource {
x := NewExec() x := NewExec()
return x return x
}) })
} }
func NewExec() *Exec { func NewExec() *Exec {
return &Exec { loader: YamlLoadDecl } return &Exec{loader: YamlLoadDecl}
} }
func (x *Exec) URI() string { func (x *Exec) URI() string {
return fmt.Sprintf("exec://%s", x.Id) return fmt.Sprintf("exec://%s", x.Id)
} }
func (x *Exec) SetURI(uri string) error { func (x *Exec) SetURI(uri string) error {
return nil return nil
} }
func (x *Exec) ResolveId(ctx context.Context) string { func (x *Exec) ResolveId(ctx context.Context) string {
return "" return ""
} }
func (x *Exec) Apply() error { func (x *Exec) Apply() error {
return nil return nil
} }
func (x *Exec) LoadDecl(yamlFileResourceDeclaration string) error { func (x *Exec) LoadDecl(yamlFileResourceDeclaration string) error {
return x.loader(yamlFileResourceDeclaration, x) return x.loader(yamlFileResourceDeclaration, x)
} }
func (x *Exec) Type() string { return "exec" } func (x *Exec) Type() string { return "exec" }
func (x *Exec) Read(ctx context.Context) ([]byte, error) { func (x *Exec) Read(ctx context.Context) ([]byte, error) {
return yaml.Marshal(x) return yaml.Marshal(x)
} }

View File

@ -41,9 +41,9 @@ func TestCreateExec(t *testing.T) {
} }
func TestExecSetURI(t *testing.T) { func TestExecSetURI(t *testing.T) {
x := NewExec() x := NewExec()
assert.NotNil(t, x) assert.NotNil(t, x)
x.SetURI("exec://" + "12345_key") x.SetURI("exec://" + "12345_key")
assert.Equal(t, "exec", x.Type()) assert.Equal(t, "exec", x.Type())
assert.Equal(t, "12345_key", x.Id) assert.Equal(t, "12345_key", x.Id)
} }

View File

@ -1,222 +1,222 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved. // Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
//
package resource package resource
import ( import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"os" "gopkg.in/yaml.v3"
"os/user" "io"
"io" "net/url"
"syscall" "os"
"gopkg.in/yaml.v3" "os/user"
"strconv" "path/filepath"
"path/filepath" "strconv"
"net/url" "syscall"
"time" "time"
) )
type FileType string type FileType string
const ( const (
RegularFile FileType = "regular" RegularFile FileType = "regular"
DirectoryFile FileType = "directory" DirectoryFile FileType = "directory"
BlockDeviceFile FileType = "block" BlockDeviceFile FileType = "block"
CharacterDeviceFile FileType = "char" CharacterDeviceFile FileType = "char"
NamedPipeFile FileType = "pipe" NamedPipeFile FileType = "pipe"
SymbolicLinkFile FileType = "symlink" SymbolicLinkFile FileType = "symlink"
SocketFile FileType = "socket" SocketFile FileType = "socket"
) )
var ErrInvalidResourceURI error = errors.New("Invalid resource URI") var ErrInvalidResourceURI error = errors.New("Invalid resource URI")
func init() { func init() {
ResourceTypes.Register("file", func(u *url.URL) Resource { ResourceTypes.Register("file", func(u *url.URL) Resource {
f := NewFile() f := NewFile()
f.Path = filepath.Join(u.Hostname(), u.Path) f.Path = filepath.Join(u.Hostname(), u.Path)
return f return f
}) })
} }
// Manage the state of file system objects // Manage the state of file system objects
type File struct { type File struct {
loader YamlLoader loader YamlLoader
Path string `yaml:"path"` Path string `yaml:"path"`
Owner string `yaml:"owner"` Owner string `yaml:"owner"`
Group string `yaml:"group"` Group string `yaml:"group"`
Mode string `yaml:"mode"` Mode string `yaml:"mode"`
Atime time.Time `yaml:"atime",omitempty` Atime time.Time `yaml:"atime",omitempty`
Ctime time.Time `yaml:"ctime",omitempty` Ctime time.Time `yaml:"ctime",omitempty`
Mtime time.Time `yaml:"mtime",omitempty` Mtime time.Time `yaml:"mtime",omitempty`
Content string `yaml:"content",omitempty` Content string `yaml:"content",omitempty`
FileType FileType `yaml:"filetype"` FileType FileType `yaml:"filetype"`
State string `yaml:"state"` State string `yaml:"state"`
} }
func NewFile() *File { func NewFile() *File {
return &File{ loader: YamlLoadDecl, FileType: RegularFile } return &File{loader: YamlLoadDecl, FileType: RegularFile}
} }
func (f *File) URI() string { func (f *File) URI() string {
return fmt.Sprintf("file://%s", f.Path) return fmt.Sprintf("file://%s", f.Path)
} }
func (f *File) SetURI(uri string) error { func (f *File) SetURI(uri string) error {
resourceUri, e := url.Parse(uri) resourceUri, e := url.Parse(uri)
if resourceUri.Scheme == "file" { if resourceUri.Scheme == "file" {
f.Path, e = filepath.Abs(filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI())) f.Path, e = filepath.Abs(filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI()))
} else { } else {
e = fmt.Errorf("%w: %s is not a file", ErrInvalidResourceURI, uri) e = fmt.Errorf("%w: %s is not a file", ErrInvalidResourceURI, uri)
} }
return e return e
} }
func (f *File) Apply() error { func (f *File) Apply() error {
switch f.State { switch f.State {
case "absent": case "absent":
removeErr := os.Remove(f.Path) removeErr := os.Remove(f.Path)
if removeErr != nil { if removeErr != nil {
return removeErr return removeErr
} }
case "present": { case "present":
uid,uidErr := LookupUID(f.Owner) {
if uidErr != nil { uid, uidErr := LookupUID(f.Owner)
return uidErr if uidErr != nil {
} return uidErr
}
gid,gidErr := LookupGID(f.Group) gid, gidErr := LookupGID(f.Group)
if gidErr != nil { if gidErr != nil {
return gidErr return gidErr
} }
mode,modeErr := strconv.ParseInt(f.Mode, 8, 64) mode, modeErr := strconv.ParseInt(f.Mode, 8, 64)
if modeErr != nil { if modeErr != nil {
return modeErr return modeErr
} }
//e := os.Stat(f.path) //e := os.Stat(f.path)
//if os.IsNotExist(e) { //if os.IsNotExist(e) {
switch f.FileType { switch f.FileType {
case DirectoryFile: case DirectoryFile:
os.MkdirAll(f.Path, os.FileMode(mode)) os.MkdirAll(f.Path, os.FileMode(mode))
default: default:
fallthrough fallthrough
case RegularFile: case RegularFile:
createdFile,e := os.Create(f.Path) createdFile, e := os.Create(f.Path)
if e != nil { if e != nil {
return e return e
} }
defer createdFile.Close() defer createdFile.Close()
if chmodErr := createdFile.Chmod(os.FileMode(mode)); chmodErr != nil { if chmodErr := createdFile.Chmod(os.FileMode(mode)); chmodErr != nil {
return chmodErr return chmodErr
} }
_,writeErr := createdFile.Write([]byte(f.Content)) _, writeErr := createdFile.Write([]byte(f.Content))
if writeErr != nil { if writeErr != nil {
return writeErr return writeErr
} }
if ! f.Mtime.IsZero() && ! f.Atime.IsZero() { if !f.Mtime.IsZero() && !f.Atime.IsZero() {
if chtimesErr := os.Chtimes(f.Path, f.Atime, f.Mtime); chtimesErr != nil { if chtimesErr := os.Chtimes(f.Path, f.Atime, f.Mtime); chtimesErr != nil {
return chtimesErr return chtimesErr
} }
} }
} }
if chownErr := os.Chown(f.Path, uid, gid); chownErr != nil { if chownErr := os.Chown(f.Path, uid, gid); chownErr != nil {
return chownErr return chownErr
} }
} }
} }
return nil return nil
} }
func (f *File) LoadDecl(yamlFileResourceDeclaration string) error { func (f *File) LoadDecl(yamlFileResourceDeclaration string) error {
return f.loader(yamlFileResourceDeclaration, f) return f.loader(yamlFileResourceDeclaration, f)
} }
func (f *File) ResolveId(ctx context.Context) string { func (f *File) ResolveId(ctx context.Context) string {
filePath, fileAbsErr := filepath.Abs(f.Path) filePath, fileAbsErr := filepath.Abs(f.Path)
if fileAbsErr != nil { if fileAbsErr != nil {
panic(fileAbsErr) panic(fileAbsErr)
} }
f.Path = filePath f.Path = filePath
return filePath return filePath
} }
func (f *File) Read(ctx context.Context) ([]byte, error) { func (f *File) Read(ctx context.Context) ([]byte, error) {
filePath, fileAbsErr := filepath.Abs(f.Path) filePath, fileAbsErr := filepath.Abs(f.Path)
if fileAbsErr != nil { if fileAbsErr != nil {
panic(fileAbsErr) panic(fileAbsErr)
} }
f.Path = filePath f.Path = filePath
info, e := os.Stat(f.Path) info, e := os.Stat(f.Path)
if e != nil { if e != nil {
f.State = "absent" f.State = "absent"
return nil, e return nil, e
} }
f.Mtime = info.ModTime() f.Mtime = info.ModTime()
if stat, ok := info.Sys().(*syscall.Stat_t); ok { if stat, ok := info.Sys().(*syscall.Stat_t); ok {
f.Atime = time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) f.Atime = time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec))
f.Ctime = time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec)) f.Ctime = time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))
userId := strconv.Itoa(int(stat.Uid)) userId := strconv.Itoa(int(stat.Uid))
groupId := strconv.Itoa(int(stat.Gid)) groupId := strconv.Itoa(int(stat.Gid))
fileUser, userErr := user.LookupId(userId) fileUser, userErr := user.LookupId(userId)
if userErr != nil { //UnknownUserIdError if userErr != nil { //UnknownUserIdError
//panic(userErr) //panic(userErr)
f.Owner = userId f.Owner = userId
} else { } else {
f.Owner = fileUser.Name f.Owner = fileUser.Name
} }
fileGroup, groupErr := user.LookupGroupId(groupId) fileGroup, groupErr := user.LookupGroupId(groupId)
if groupErr != nil { if groupErr != nil {
//panic(groupErr) //panic(groupErr)
f.Group = groupId f.Group = groupId
} else { } else {
f.Group = fileGroup.Name f.Group = fileGroup.Name
} }
} }
f.Mode = fmt.Sprintf("%04o", info.Mode().Perm()) f.Mode = fmt.Sprintf("%04o", info.Mode().Perm())
file, fileErr := os.Open(f.Path) file, fileErr := os.Open(f.Path)
if fileErr != nil { if fileErr != nil {
panic(fileErr) panic(fileErr)
} }
fileContent, ioErr := io.ReadAll(file) fileContent, ioErr := io.ReadAll(file)
if ioErr != nil { if ioErr != nil {
panic(ioErr) panic(ioErr)
} }
f.Content = string(fileContent) f.Content = string(fileContent)
f.State = "present" f.State = "present"
return yaml.Marshal(f) return yaml.Marshal(f)
} }
func (f *File) Type() string { return "file" } func (f *File) Type() string { return "file" }
func (f *FileType) UnmarshalYAML(value *yaml.Node) error { func (f *FileType) UnmarshalYAML(value *yaml.Node) error {
var s string var s string
if err := value.Decode(&s); err != nil { if err := value.Decode(&s); err != nil {
return err return err
} }
switch s { switch s {
case string(RegularFile), string(DirectoryFile), string(BlockDeviceFile), string(CharacterDeviceFile), string(NamedPipeFile), string(SymbolicLinkFile), string(SocketFile): case string(RegularFile), string(DirectoryFile), string(BlockDeviceFile), string(CharacterDeviceFile), string(NamedPipeFile), string(SymbolicLinkFile), string(SocketFile):
*f = FileType(s) *f = FileType(s)
return nil return nil
default: default:
return errors.New("invalid FileType value") return errors.New("invalid FileType value")
} }
} }

View File

@ -15,9 +15,9 @@ import (
"os" "os"
"path/filepath" "path/filepath"
_ "strings" _ "strings"
"syscall"
"testing" "testing"
"time" "time"
"syscall"
) )
func TestNewFileResource(t *testing.T) { func TestNewFileResource(t *testing.T) {
@ -67,26 +67,26 @@ func TestReadFile(t *testing.T) {
assert.Equal(t, nil, e) assert.Equal(t, nil, e)
assert.Equal(t, "nobody", f.Owner) assert.Equal(t, "nobody", f.Owner)
info,statErr := os.Stat(file) info, statErr := os.Stat(file)
assert.Nil(t, statErr) assert.Nil(t, statErr)
stat, ok := info.Sys().(*syscall.Stat_t) stat, ok := info.Sys().(*syscall.Stat_t)
assert.True(t, ok) assert.True(t, ok)
cTime := time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec)) cTime := time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))
expected := fmt.Sprintf(declarationAttributes, file, cTime.Format(time.RFC3339Nano)) expected := fmt.Sprintf(declarationAttributes, file, cTime.Format(time.RFC3339Nano))
assert.YAMLEq(t, expected, string(r)) assert.YAMLEq(t, expected, string(r))
} }
func TestReadFileError(t *testing.T) { func TestReadFileError(t *testing.T) {
ctx := context.Background() ctx := context.Background()
file, _ := filepath.Abs(filepath.Join(TempDir, "missingfile.txt")) file, _ := filepath.Abs(filepath.Join(TempDir, "missingfile.txt"))
f := NewFile() f := NewFile()
assert.NotEqual(t, nil, f) assert.NotEqual(t, nil, f)
f.Path = file f.Path = file
_, e := f.Read(ctx) _, e := f.Read(ctx)
assert.True(t, os.IsNotExist(e)) assert.True(t, os.IsNotExist(e))
assert.Equal(t, "absent", f.State) assert.Equal(t, "absent", f.State)
} }
func TestCreateFile(t *testing.T) { func TestCreateFile(t *testing.T) {
@ -159,8 +159,8 @@ func TestFileDirectory(t *testing.T) {
} }
func TestFileTimes(t *testing.T) { func TestFileTimes(t *testing.T) {
file, _ := filepath.Abs(filepath.Join(TempDir, "testtimes.txt")) file, _ := filepath.Abs(filepath.Join(TempDir, "testtimes.txt"))
decl := fmt.Sprintf(` decl := fmt.Sprintf(`
path: "%s" path: "%s"
owner: "nobody" owner: "nobody"
group: "nobody" group: "nobody"
@ -170,21 +170,21 @@ func TestFileTimes(t *testing.T) {
state: "present" state: "present"
`, file) `, file)
expectedTime, timeErr := time.Parse(time.RFC3339, "2001-12-15T01:01:01.1Z") expectedTime, timeErr := time.Parse(time.RFC3339, "2001-12-15T01:01:01.1Z")
assert.Nil(t, timeErr) assert.Nil(t, timeErr)
f := NewFile() f := NewFile()
e := f.LoadDecl(decl) e := f.LoadDecl(decl)
assert.Nil(t, e) assert.Nil(t, e)
assert.Equal(t, "nobody", f.Owner) assert.Equal(t, "nobody", f.Owner)
assert.True(t, f.Mtime.Equal(expectedTime)) assert.True(t, f.Mtime.Equal(expectedTime))
} }
func TestFileSetURI(t *testing.T) { func TestFileSetURI(t *testing.T) {
file, _ := filepath.Abs(filepath.Join(TempDir, "testuri.txt")) file, _ := filepath.Abs(filepath.Join(TempDir, "testuri.txt"))
f := NewFile() f := NewFile()
assert.NotNil(t, f) assert.NotNil(t, f)
f.SetURI("file://" + file) f.SetURI("file://" + file)
assert.Equal(t, "file", f.Type()) assert.Equal(t, "file", f.Type())
assert.Equal(t, file, f.Path) assert.Equal(t, file, f.Path)
} }

View File

@ -1,45 +1,44 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved. // Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
//
package resource package resource
import ( import (
"os/user" "os/user"
"strconv" "strconv"
) )
func LookupUIDString(userName string) string { func LookupUIDString(userName string) string {
user, userLookupErr := user.Lookup(userName) user, userLookupErr := user.Lookup(userName)
if userLookupErr != nil { if userLookupErr != nil {
return "" return ""
} }
return user.Uid return user.Uid
} }
func LookupUID(userName string) (int,error) { func LookupUID(userName string) (int, error) {
user, userLookupErr := user.Lookup(userName) user, userLookupErr := user.Lookup(userName)
if userLookupErr != nil { if userLookupErr != nil {
return -1,userLookupErr return -1, userLookupErr
} }
uid, uidErr := strconv.Atoi(user.Uid) uid, uidErr := strconv.Atoi(user.Uid)
if uidErr != nil { if uidErr != nil {
return -1,uidErr return -1, uidErr
} }
return uid,nil return uid, nil
} }
func LookupGID(groupName string) (int,error) { func LookupGID(groupName string) (int, error) {
group, groupLookupErr := user.LookupGroup(groupName) group, groupLookupErr := user.LookupGroup(groupName)
if groupLookupErr != nil { if groupLookupErr != nil {
return -1,groupLookupErr return -1, groupLookupErr
} }
gid, gidErr := strconv.Atoi(group.Gid) gid, gidErr := strconv.Atoi(group.Gid)
if gidErr != nil { if gidErr != nil {
return -1,gidErr return -1, gidErr
} }
return gid, nil return gid, nil
} }

View File

@ -2,30 +2,30 @@
package resource package resource
import ( import (
_ "fmt" _ "context"
_ "context" _ "encoding/json"
"testing" _ "fmt"
_ "net/http" "github.com/stretchr/testify/assert"
_ "net/http/httptest" _ "io"
_ "net/url" _ "net/http"
_ "io" _ "net/http/httptest"
"github.com/stretchr/testify/assert" _ "net/url"
_ "encoding/json" _ "strings"
_ "strings" "testing"
) )
func TestLookupUID(t *testing.T) { func TestLookupUID(t *testing.T) {
uid,e := LookupUID("nobody") uid, e := LookupUID("nobody")
assert.Equal(t, nil, e) assert.Equal(t, nil, e)
assert.Equal(t, 65534, uid) assert.Equal(t, 65534, uid)
} }
func TestLookupGID(t *testing.T) { func TestLookupGID(t *testing.T) {
gid,e := LookupGID("nobody") gid, e := LookupGID("nobody")
assert.Equal(t, nil, e) assert.Equal(t, nil, e)
assert.Equal(t, 65534, gid) assert.Equal(t, 65534, gid)
} }
func TestExecCommand(t *testing.T) { func TestExecCommand(t *testing.T) {

View File

@ -4,51 +4,50 @@
package resource package resource
import ( import (
"context" "context"
_ "fmt" _ "fmt"
_ "gopkg.in/yaml.v3" _ "gopkg.in/yaml.v3"
_ "net/url" _ "net/url"
) )
type Resource interface { type Resource interface {
Type() string Type() string
URI() string URI() string
//SetURI(string) error //SetURI(string) error
ResolveId(context.Context) string ResolveId(context.Context) string
ResourceLoader ResourceLoader
StateTransformer StateTransformer
ResourceReader ResourceReader
} }
// validate the type/uri // validate the type/uri
type ResourceValidator interface { type ResourceValidator interface {
Validate() error Validate() error
} }
type ResourceCreator interface { type ResourceCreator interface {
Create(context.Context) error Create(context.Context) error
} }
type ResourceReader interface { type ResourceReader interface {
Read(context.Context) ([]byte, error) Read(context.Context) ([]byte, error)
} }
type ResourceUpdater interface { type ResourceUpdater interface {
Update() error Update() error
} }
type ResourceDeleter interface { type ResourceDeleter interface {
Delete() error Delete() error
} }
type ResourceDecoder struct { type ResourceDecoder struct {
} }
func NewResource(uri string) Resource { func NewResource(uri string) Resource {
r,e := ResourceTypes.New(uri) r, e := ResourceTypes.New(uri)
if e == nil { if e == nil {
return r return r
} }
return nil return nil
} }

View File

@ -2,45 +2,45 @@
package resource package resource
import ( import (
"context" "context"
"os" _ "fmt"
"path/filepath" "github.com/stretchr/testify/assert"
_ "fmt" "log"
"log" "os"
"testing" "path/filepath"
"github.com/stretchr/testify/assert" "testing"
) )
var TempDir string var TempDir string
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
var err error var err error
TempDir, err = os.MkdirTemp("", "testresourcefile") TempDir, err = os.MkdirTemp("", "testresourcefile")
if err != nil || TempDir == "" { if err != nil || TempDir == "" {
log.Fatal(err) log.Fatal(err)
} }
rc := m.Run() rc := m.Run()
os.RemoveAll(TempDir) os.RemoveAll(TempDir)
os.Exit(rc) os.Exit(rc)
} }
func TestNewResource(t *testing.T) { func TestNewResource(t *testing.T) {
resourceUri := "file://foo" resourceUri := "file://foo"
testFile := NewResource(resourceUri) testFile := NewResource(resourceUri)
assert.NotNil(t, testFile) assert.NotNil(t, testFile)
assert.Equal(t, "foo", testFile.(*File).Path) assert.Equal(t, "foo", testFile.(*File).Path)
} }
func TestResolveId(t *testing.T) { func TestResolveId(t *testing.T) {
testFile := NewResource("file://../../README.md") testFile := NewResource("file://../../README.md")
assert.NotNil(t, testFile) assert.NotNil(t, testFile)
absolutePath,e := filepath.Abs("../../README.md") absolutePath, e := filepath.Abs("../../README.md")
assert.Nil(t, e) assert.Nil(t, e)
testFile.ResolveId(context.Background()) testFile.ResolveId(context.Background())
assert.Equal(t, absolutePath, testFile.(*File).Path) assert.Equal(t, absolutePath, testFile.(*File).Path)
} }

View File

@ -1,48 +1,47 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved. // Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
//
package resource package resource
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
) )
var ( var (
ErrUnknownResourceType = errors.New("Unknown resource type") ErrUnknownResourceType = errors.New("Unknown resource type")
ResourceTypes *Types = NewTypes() ResourceTypes *Types = NewTypes()
) )
type TypeFactory func(*url.URL) Resource type TypeFactory func(*url.URL) Resource
type Types struct { type Types struct {
registry map[string]TypeFactory registry map[string]TypeFactory
} }
func NewTypes() *Types { func NewTypes() *Types {
return &Types{ registry: make(map[string]TypeFactory) } return &Types{registry: make(map[string]TypeFactory)}
} }
func (t *Types) Register(name string, factory TypeFactory) { func (t *Types) Register(name string, factory TypeFactory) {
t.registry[name] = factory t.registry[name] = factory
} }
func (t *Types) New(uri string) (Resource, error) { func (t *Types) New(uri string) (Resource, error) {
u,e := url.Parse(uri) u, e := url.Parse(uri)
if u == nil || e != nil { if u == nil || e != nil {
return nil, fmt.Errorf("%w: %s", ErrUnknownResourceType, e) return nil, fmt.Errorf("%w: %s", ErrUnknownResourceType, e)
} }
if r,ok := t.registry[u.Scheme]; ok { if r, ok := t.registry[u.Scheme]; ok {
return r(u), nil return r(u), nil
} }
return nil, fmt.Errorf("%w: %s", ErrUnknownResourceType, u.Scheme) return nil, fmt.Errorf("%w: %s", ErrUnknownResourceType, u.Scheme)
} }
func (t *Types) Has(typename string) bool { func (t *Types) Has(typename string) bool {
if _,ok := t.registry[typename]; ok { if _, ok := t.registry[typename]; ok {
return true return true
} }
return false return false
} }

View File

@ -2,52 +2,52 @@
package resource package resource
import ( import (
_ "context" _ "context"
"testing" "decl/tests/mocks"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"decl/tests/mocks" "net/url"
"net/url" "testing"
) )
func TestNewResourceTypes(t *testing.T) { func TestNewResourceTypes(t *testing.T) {
resourceTypes := NewTypes() resourceTypes := NewTypes()
assert.NotEqual(t, nil, resourceTypes) assert.NotEqual(t, nil, resourceTypes)
} }
func TestNewResourceTypesRegister(t *testing.T) { func TestNewResourceTypesRegister(t *testing.T) {
m := mocks.NewFooResource() m := mocks.NewFooResource()
resourceTypes := NewTypes() resourceTypes := NewTypes()
assert.NotEqual(t, nil, resourceTypes) assert.NotEqual(t, nil, resourceTypes)
resourceTypes.Register("foo", func(*url.URL) Resource { return m }) resourceTypes.Register("foo", func(*url.URL) Resource { return m })
r,e := resourceTypes.New("foo://") r, e := resourceTypes.New("foo://")
assert.Equal(t, nil, e) assert.Equal(t, nil, e)
assert.Equal(t, m, r) assert.Equal(t, m, r)
} }
func TestResourceTypesFromURI(t *testing.T) { func TestResourceTypesFromURI(t *testing.T) {
m := mocks.NewFooResource() m := mocks.NewFooResource()
resourceTypes := NewTypes() resourceTypes := NewTypes()
assert.NotEqual(t, nil, resourceTypes) assert.NotEqual(t, nil, resourceTypes)
resourceTypes.Register("foo", func(*url.URL) Resource { return m }) resourceTypes.Register("foo", func(*url.URL) Resource { return m })
r,e := resourceTypes.New("foo://bar") r, e := resourceTypes.New("foo://bar")
assert.Equal(t, nil, e) assert.Equal(t, nil, e)
assert.Equal(t, m, r) assert.Equal(t, m, r)
} }
func TestResourceTypesHasType(t *testing.T) { func TestResourceTypesHasType(t *testing.T) {
m := mocks.NewFooResource() m := mocks.NewFooResource()
resourceTypes := NewTypes() resourceTypes := NewTypes()
assert.NotNil(t, resourceTypes) assert.NotNil(t, resourceTypes)
resourceTypes.Register("foo", func(*url.URL) Resource { return m }) resourceTypes.Register("foo", func(*url.URL) Resource { return m })
assert.True(t, resourceTypes.Has("foo")) assert.True(t, resourceTypes.Has("foo"))
} }

View File

@ -1,156 +1,154 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved. // Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
//
package resource package resource
import ( import (
"context" "context"
"fmt" "fmt"
_ "os" "gopkg.in/yaml.v3"
"gopkg.in/yaml.v3" "log"
"os/exec" "net/url"
"strings" _ "os"
"log" "os/exec"
"net/url" "os/user"
"os/user" "strconv"
"strconv" "strings"
) )
type User struct { type User struct {
loader YamlLoader loader YamlLoader
Name string `yaml:"name"` Name string `yaml:"name"`
UID int `yaml:"uid"` UID int `yaml:"uid"`
Group string `yaml:"group"` Group string `yaml:"group"`
Groups []string `yaml:"groups",omitempty` Groups []string `yaml:"groups",omitempty`
Gecos string `yaml:"gecos"` Gecos string `yaml:"gecos"`
Home string `yaml:"home"` Home string `yaml:"home"`
CreateHome bool `yaml:"createhome"omitempty` CreateHome bool `yaml:"createhome"omitempty`
Shell string `yaml:"shell"` Shell string `yaml:"shell"`
State string `yaml:"state"` State string `yaml:"state"`
} }
func NewUser() *User { func NewUser() *User {
return &User{ loader: YamlLoadDecl } return &User{loader: YamlLoadDecl}
} }
func init() { func init() {
ResourceTypes.Register("user", func(u *url.URL) Resource { ResourceTypes.Register("user", func(u *url.URL) Resource {
user := NewUser() user := NewUser()
user.Name = u.Path user.Name = u.Path
user.UID, _ = LookupUID(u.Path) user.UID, _ = LookupUID(u.Path)
return user return user
}) })
} }
func (u *User) URI() string { func (u *User) URI() string {
return fmt.Sprintf("user://%s", u.Name) return fmt.Sprintf("user://%s", u.Name)
} }
func (u *User) ResolveId(ctx context.Context) string { func (u *User) ResolveId(ctx context.Context) string {
return LookupUIDString(u.Name) return LookupUIDString(u.Name)
} }
func (u *User) Apply() error { func (u *User) Apply() error {
switch u.State { switch u.State {
case "present": case "present":
_, NoUserExists := LookupUID(u.Name) _, NoUserExists := LookupUID(u.Name)
if NoUserExists != nil { if NoUserExists != nil {
var userCommandName string = "useradd" var userCommandName string = "useradd"
args := make([]string, 0, 7) args := make([]string, 0, 7)
if u.UID >= 0 { if u.UID >= 0 {
args = append(args, "-u", fmt.Sprintf("%d", u.UID)) args = append(args, "-u", fmt.Sprintf("%d", u.UID))
} }
if _,pathErr := exec.LookPath("useradd"); pathErr != nil { if _, pathErr := exec.LookPath("useradd"); pathErr != nil {
if _,addUserPathErr := exec.LookPath("adduser"); addUserPathErr == nil { if _, addUserPathErr := exec.LookPath("adduser"); addUserPathErr == nil {
userCommandName = "adduser" userCommandName = "adduser"
u.AddUserCommand(&args) u.AddUserCommand(&args)
} }
} else { } else {
u.UserAddCommand(&args) u.UserAddCommand(&args)
} }
args = append(args, u.Name) args = append(args, u.Name)
cmd := exec.Command(userCommandName, args...) cmd := exec.Command(userCommandName, args...)
cmdOutput, cmdErr := cmd.CombinedOutput() cmdOutput, cmdErr := cmd.CombinedOutput()
log.Printf("%s\n", cmdOutput) log.Printf("%s\n", cmdOutput)
return cmdErr return cmdErr
} }
case "absent": case "absent":
var userDelCommandName string = "userdel" var userDelCommandName string = "userdel"
args := make([]string, 0, 7) args := make([]string, 0, 7)
if _,pathErr := exec.LookPath("userdel"); pathErr != nil { if _, pathErr := exec.LookPath("userdel"); pathErr != nil {
if _,delUserPathErr := exec.LookPath("deluser"); delUserPathErr == nil { if _, delUserPathErr := exec.LookPath("deluser"); delUserPathErr == nil {
userDelCommandName = "deluser" userDelCommandName = "deluser"
} }
} }
args = append(args, u.Name) args = append(args, u.Name)
cmd := exec.Command(userDelCommandName, args...) cmd := exec.Command(userDelCommandName, args...)
cmdOutput, cmdErr := cmd.CombinedOutput() cmdOutput, cmdErr := cmd.CombinedOutput()
log.Printf("%s\n", cmdOutput) log.Printf("%s\n", cmdOutput)
return cmdErr return cmdErr
} }
return nil return nil
} }
func (u *User) LoadDecl(yamlFileResourceDeclaration string) error { func (u *User) LoadDecl(yamlFileResourceDeclaration string) error {
return u.loader(yamlFileResourceDeclaration, u) return u.loader(yamlFileResourceDeclaration, u)
} }
func (u *User) AddUserCommand(args *[]string) error { func (u *User) AddUserCommand(args *[]string) error {
*args = append(*args, "-D") *args = append(*args, "-D")
if u.Group != "" { if u.Group != "" {
*args = append(*args, "-G", u.Group) *args = append(*args, "-G", u.Group)
} }
if u.Home != "" { if u.Home != "" {
*args = append(*args, "-h", u.Home) *args = append(*args, "-h", u.Home)
} }
return nil return nil
} }
func (u *User) UserAddCommand(args *[]string) error { func (u *User) UserAddCommand(args *[]string) error {
if u.Group != "" { if u.Group != "" {
*args = append(*args, "-g", u.Group) *args = append(*args, "-g", u.Group)
} }
if len(u.Groups) > 0 { if len(u.Groups) > 0 {
*args = append(*args, "-G", strings.Join(u.Groups, ",")) *args = append(*args, "-G", strings.Join(u.Groups, ","))
} }
if u.Home != "" { if u.Home != "" {
*args = append(*args, "-d", u.Home) *args = append(*args, "-d", u.Home)
} }
if u.CreateHome { if u.CreateHome {
*args = append(*args, "-m") *args = append(*args, "-m")
} }
return nil return nil
} }
func (u *User) Type() string { return "user" } func (u *User) Type() string { return "user" }
func (u *User) Read(ctx context.Context) ([]byte, error) { func (u *User) Read(ctx context.Context) ([]byte, error) {
var readUser *user.User var readUser *user.User
var e error var e error
if u.Name != "" { if u.Name != "" {
readUser,e = user.Lookup(u.Name) readUser, e = user.Lookup(u.Name)
} }
if u.UID >= 0 { if u.UID >= 0 {
readUser,e = user.LookupId(strconv.Itoa(u.UID)) readUser, e = user.LookupId(strconv.Itoa(u.UID))
} }
if e != nil { if e != nil {
panic(e) panic(e)
} }
u.Name = readUser.Username u.Name = readUser.Username
u.UID,_ = strconv.Atoi(readUser.Uid) u.UID, _ = strconv.Atoi(readUser.Uid)
if readGroup, groupErr := user.LookupGroupId(readUser.Gid); groupErr == nil { if readGroup, groupErr := user.LookupGroupId(readUser.Gid); groupErr == nil {
u.Group = readGroup.Name u.Group = readGroup.Name
} else { } else {
panic(groupErr) panic(groupErr)
} }
u.Home = readUser.HomeDir u.Home = readUser.HomeDir
u.Gecos = readUser.Name u.Gecos = readUser.Name
return yaml.Marshal(u) return yaml.Marshal(u)
} }

View File

@ -2,45 +2,45 @@
package resource package resource
import ( import (
_ "fmt" _ "context"
_ "context" _ "encoding/json"
"testing" _ "fmt"
_ "net/http" "github.com/stretchr/testify/assert"
_ "net/http/httptest" _ "io"
_ "net/url" _ "net/http"
_ "io" _ "net/http/httptest"
_ "os" _ "net/url"
"github.com/stretchr/testify/assert" _ "os"
_ "encoding/json" _ "strings"
_ "strings" "testing"
) )
func TestNewUserResource(t *testing.T) { func TestNewUserResource(t *testing.T) {
u := NewUser() u := NewUser()
assert.NotEqual(t, nil, u) assert.NotEqual(t, nil, u)
} }
func TestCreateUser(t *testing.T) { func TestCreateUser(t *testing.T) {
decl := ` decl := `
name: "testuser" name: "testuser"
uid: 12001 uid: 12001
gid: 12001 gid: 12001
home: "/home/testuser" home: "/home/testuser"
state: present state: present
` `
u := NewUser() u := NewUser()
e := u.LoadDecl(decl) e := u.LoadDecl(decl)
assert.Equal(t, nil, e) assert.Equal(t, nil, e)
assert.Equal(t, "testuser", u.Name) assert.Equal(t, "testuser", u.Name)
applyErr := u.Apply() applyErr := u.Apply()
assert.Equal(t, nil, applyErr) assert.Equal(t, nil, applyErr)
uid, uidErr := LookupUID(u.Name) uid, uidErr := LookupUID(u.Name)
assert.Equal(t, nil, uidErr) assert.Equal(t, nil, uidErr)
assert.Equal(t, 12001, uid) assert.Equal(t, 12001, uid)
u.State = "absent" u.State = "absent"
applyDeleteErr := u.Apply() applyDeleteErr := u.Apply()
assert.Equal(t, nil, applyDeleteErr) assert.Equal(t, nil, applyDeleteErr)
} }