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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,65 +1,63 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
//
package resource
import (
"context"
"fmt"
_ "os"
"gopkg.in/yaml.v3"
_ "os/exec"
_ "strings"
_ "log"
"net/url"
"context"
"fmt"
"gopkg.in/yaml.v3"
_ "log"
"net/url"
_ "os"
_ "os/exec"
_ "strings"
)
type Exec struct {
loader YamlLoader
Id string `yaml:"id"`
// create command
// read command
// update command
// delete command
loader YamlLoader
Id string `yaml:"id"`
// create command
// read command
// update command
// delete command
// state attributes
State string `yaml:"state"`
// state attributes
State string `yaml:"state"`
}
func init() {
ResourceTypes.Register("exec", func(u *url.URL) Resource {
x := NewExec()
return x
})
ResourceTypes.Register("exec", func(u *url.URL) Resource {
x := NewExec()
return x
})
}
func NewExec() *Exec {
return &Exec { loader: YamlLoadDecl }
return &Exec{loader: YamlLoadDecl}
}
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 {
return nil
return nil
}
func (x *Exec) ResolveId(ctx context.Context) string {
return ""
return ""
}
func (x *Exec) Apply() error {
return nil
return nil
}
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) 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) {
x := NewExec()
assert.NotNil(t, x)
x.SetURI("exec://" + "12345_key")
assert.Equal(t, "exec", x.Type())
assert.Equal(t, "12345_key", x.Id)
x := NewExec()
assert.NotNil(t, x)
x.SetURI("exec://" + "12345_key")
assert.Equal(t, "exec", x.Type())
assert.Equal(t, "12345_key", x.Id)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,52 +2,52 @@
package resource
import (
_ "context"
"testing"
"github.com/stretchr/testify/assert"
"decl/tests/mocks"
"net/url"
_ "context"
"decl/tests/mocks"
"github.com/stretchr/testify/assert"
"net/url"
"testing"
)
func TestNewResourceTypes(t *testing.T) {
resourceTypes := NewTypes()
assert.NotEqual(t, nil, resourceTypes)
resourceTypes := NewTypes()
assert.NotEqual(t, nil, resourceTypes)
}
func TestNewResourceTypesRegister(t *testing.T) {
m := mocks.NewFooResource()
m := mocks.NewFooResource()
resourceTypes := NewTypes()
assert.NotEqual(t, nil, resourceTypes)
resourceTypes := NewTypes()
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://")
assert.Equal(t, nil, e)
assert.Equal(t, m, r)
r, e := resourceTypes.New("foo://")
assert.Equal(t, nil, e)
assert.Equal(t, m, r)
}
func TestResourceTypesFromURI(t *testing.T) {
m := mocks.NewFooResource()
m := mocks.NewFooResource()
resourceTypes := NewTypes()
assert.NotEqual(t, nil, resourceTypes)
resourceTypes := NewTypes()
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")
assert.Equal(t, nil, e)
assert.Equal(t, m, r)
r, e := resourceTypes.New("foo://bar")
assert.Equal(t, nil, e)
assert.Equal(t, m, r)
}
func TestResourceTypesHasType(t *testing.T) {
m := mocks.NewFooResource()
m := mocks.NewFooResource()
resourceTypes := NewTypes()
assert.NotNil(t, resourceTypes)
resourceTypes := NewTypes()
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.
//
package resource
import (
"context"
"fmt"
_ "os"
"gopkg.in/yaml.v3"
"os/exec"
"strings"
"log"
"net/url"
"os/user"
"strconv"
"context"
"fmt"
"gopkg.in/yaml.v3"
"log"
"net/url"
_ "os"
"os/exec"
"os/user"
"strconv"
"strings"
)
type User struct {
loader YamlLoader
Name string `yaml:"name"`
UID int `yaml:"uid"`
Group string `yaml:"group"`
Groups []string `yaml:"groups",omitempty`
Gecos string `yaml:"gecos"`
Home string `yaml:"home"`
CreateHome bool `yaml:"createhome"omitempty`
Shell string `yaml:"shell"`
loader YamlLoader
Name string `yaml:"name"`
UID int `yaml:"uid"`
Group string `yaml:"group"`
Groups []string `yaml:"groups",omitempty`
Gecos string `yaml:"gecos"`
Home string `yaml:"home"`
CreateHome bool `yaml:"createhome"omitempty`
Shell string `yaml:"shell"`
State string `yaml:"state"`
State string `yaml:"state"`
}
func NewUser() *User {
return &User{ loader: YamlLoadDecl }
return &User{loader: YamlLoadDecl}
}
func init() {
ResourceTypes.Register("user", func(u *url.URL) Resource {
user := NewUser()
user.Name = u.Path
user.UID, _ = LookupUID(u.Path)
return user
})
ResourceTypes.Register("user", func(u *url.URL) Resource {
user := NewUser()
user.Name = u.Path
user.UID, _ = LookupUID(u.Path)
return user
})
}
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 {
return LookupUIDString(u.Name)
return LookupUIDString(u.Name)
}
func (u *User) Apply() error {
switch u.State {
case "present":
_, NoUserExists := LookupUID(u.Name)
if NoUserExists != nil {
var userCommandName string = "useradd"
args := make([]string, 0, 7)
if u.UID >= 0 {
args = append(args, "-u", fmt.Sprintf("%d", u.UID))
}
switch u.State {
case "present":
_, NoUserExists := LookupUID(u.Name)
if NoUserExists != nil {
var userCommandName string = "useradd"
args := make([]string, 0, 7)
if u.UID >= 0 {
args = append(args, "-u", fmt.Sprintf("%d", u.UID))
}
if _,pathErr := exec.LookPath("useradd"); pathErr != nil {
if _,addUserPathErr := exec.LookPath("adduser"); addUserPathErr == nil {
userCommandName = "adduser"
u.AddUserCommand(&args)
}
} else {
u.UserAddCommand(&args)
}
args = append(args, u.Name)
cmd := exec.Command(userCommandName, args...)
cmdOutput, cmdErr := cmd.CombinedOutput()
log.Printf("%s\n", cmdOutput)
return cmdErr
}
case "absent":
var userDelCommandName string = "userdel"
args := make([]string, 0, 7)
if _, pathErr := exec.LookPath("useradd"); pathErr != nil {
if _, addUserPathErr := exec.LookPath("adduser"); addUserPathErr == nil {
userCommandName = "adduser"
u.AddUserCommand(&args)
}
} else {
u.UserAddCommand(&args)
}
args = append(args, u.Name)
cmd := exec.Command(userCommandName, args...)
cmdOutput, cmdErr := cmd.CombinedOutput()
log.Printf("%s\n", cmdOutput)
return cmdErr
}
case "absent":
var userDelCommandName string = "userdel"
args := make([]string, 0, 7)
if _,pathErr := exec.LookPath("userdel"); pathErr != nil {
if _,delUserPathErr := exec.LookPath("deluser"); delUserPathErr == nil {
userDelCommandName = "deluser"
}
}
args = append(args, u.Name)
cmd := exec.Command(userDelCommandName, args...)
cmdOutput, cmdErr := cmd.CombinedOutput()
log.Printf("%s\n", cmdOutput)
return cmdErr
}
return nil
if _, pathErr := exec.LookPath("userdel"); pathErr != nil {
if _, delUserPathErr := exec.LookPath("deluser"); delUserPathErr == nil {
userDelCommandName = "deluser"
}
}
args = append(args, u.Name)
cmd := exec.Command(userDelCommandName, args...)
cmdOutput, cmdErr := cmd.CombinedOutput()
log.Printf("%s\n", cmdOutput)
return cmdErr
}
return nil
}
func (u *User) LoadDecl(yamlFileResourceDeclaration string) error {
return u.loader(yamlFileResourceDeclaration, u)
return u.loader(yamlFileResourceDeclaration, u)
}
func (u *User) AddUserCommand(args *[]string) error {
*args = append(*args, "-D")
if u.Group != "" {
*args = append(*args, "-G", u.Group)
}
if u.Home != "" {
*args = append(*args, "-h", u.Home)
}
return nil
*args = append(*args, "-D")
if u.Group != "" {
*args = append(*args, "-G", u.Group)
}
if u.Home != "" {
*args = append(*args, "-h", u.Home)
}
return nil
}
func (u *User) UserAddCommand(args *[]string) error {
if u.Group != "" {
*args = append(*args, "-g", u.Group)
}
if len(u.Groups) > 0 {
*args = append(*args, "-G", strings.Join(u.Groups, ","))
}
if u.Home != "" {
*args = append(*args, "-d", u.Home)
}
if u.CreateHome {
*args = append(*args, "-m")
}
return nil
if u.Group != "" {
*args = append(*args, "-g", u.Group)
}
if len(u.Groups) > 0 {
*args = append(*args, "-G", strings.Join(u.Groups, ","))
}
if u.Home != "" {
*args = append(*args, "-d", u.Home)
}
if u.CreateHome {
*args = append(*args, "-m")
}
return nil
}
func (u *User) Type() string { return "user" }
func (u *User) Read(ctx context.Context) ([]byte, error) {
var readUser *user.User
var e error
if u.Name != "" {
readUser,e = user.Lookup(u.Name)
}
if u.UID >= 0 {
readUser,e = user.LookupId(strconv.Itoa(u.UID))
}
var readUser *user.User
var e error
if u.Name != "" {
readUser, e = user.Lookup(u.Name)
}
if u.UID >= 0 {
readUser, e = user.LookupId(strconv.Itoa(u.UID))
}
if e != nil {
panic(e)
}
if e != nil {
panic(e)
}
u.Name = readUser.Username
u.UID,_ = strconv.Atoi(readUser.Uid)
if readGroup, groupErr := user.LookupGroupId(readUser.Gid); groupErr == nil {
u.Group = readGroup.Name
} else {
panic(groupErr)
}
u.Home = readUser.HomeDir
u.Gecos = readUser.Name
u.Name = readUser.Username
u.UID, _ = strconv.Atoi(readUser.Uid)
if readGroup, groupErr := user.LookupGroupId(readUser.Gid); groupErr == nil {
u.Group = readGroup.Name
} else {
panic(groupErr)
}
u.Home = readUser.HomeDir
u.Gecos = readUser.Name
return yaml.Marshal(u)
return yaml.Marshal(u)
}

View File

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