initial version
This commit is contained in:
parent
4b5382d7aa
commit
13aacbe28d
1
COPYRIGHT
Normal file
1
COPYRIGHT
Normal file
@ -0,0 +1 @@
|
||||
Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
53
cmd/cli/main.go
Normal file
53
cmd/cli/main.go
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"flag"
|
||||
"log"
|
||||
_ "fmt"
|
||||
_ "gopkg.in/yaml.v3"
|
||||
"decl/internal/resource"
|
||||
)
|
||||
|
||||
|
||||
func main() {
|
||||
|
||||
file := flag.String("resource-file", "", "Resource file path")
|
||||
resourceUri := flag.String("import-resource", "", "Add an existing resource")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
var resourceFile *os.File
|
||||
var inputFileErr error
|
||||
|
||||
if *file != "" {
|
||||
resourceFile,inputFileErr = os.Open(*file)
|
||||
} else {
|
||||
if stdinInfo, stdinErr := os.Stdin.Stat(); stdinErr == nil {
|
||||
if (stdinInfo.Mode() & os.ModeCharDevice) == 0 {
|
||||
resourceFile = os.Stdin
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if inputFileErr != nil {
|
||||
log.Fatal(inputFileErr)
|
||||
}
|
||||
|
||||
d := resource.NewDocument()
|
||||
if e := d.Load(resourceFile); e != nil {
|
||||
log.Fatal(e)
|
||||
}
|
||||
if applyErr := d.Apply(); applyErr != nil {
|
||||
log.Fatal(applyErr)
|
||||
}
|
||||
|
||||
if *resourceUri != "" {
|
||||
d.AddResource(*resourceUri)
|
||||
}
|
||||
|
||||
d.Generate(os.Stdout)
|
||||
}
|
244
internal/resource/container.go
Normal file
244
internal/resource/container.go
Normal file
@ -0,0 +1,244 @@
|
||||
// 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"
|
||||
"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"
|
||||
)
|
||||
|
||||
type ContainerClient interface {
|
||||
ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, 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`
|
||||
Args []string `yaml:"args",omitempty`
|
||||
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"`
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) URI() string {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (c *Container) LoadDecl(yamlFileResourceDeclaration string) error {
|
||||
return c.loader(yamlFileResourceDeclaration, c)
|
||||
}
|
||||
|
||||
/*
|
||||
apiClient, err := client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer apiClient.Close()
|
||||
|
||||
containers, err := apiClient.ContainerList(context.Background(), container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, ctr := range containers {
|
||||
fmt.Printf("%s %s (status: %s)\n", ctr.ID, ctr.Image, ctr.Status)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func (c *Container) Create(ctx context.Context) error {
|
||||
config := &container.Config {
|
||||
Image: c.Image,
|
||||
Cmd: c.Cmd,
|
||||
Tty: false,
|
||||
}
|
||||
resp, err := c.apiClient.ContainerCreate(ctx, config, nil, 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:
|
||||
}
|
||||
*/
|
||||
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,
|
||||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
containerJSON, err := c.apiClient.ContainerInspect(ctx, containerID)
|
||||
if client.IsErrNotFound(err) {
|
||||
c.State = "absent"
|
||||
} else {
|
||||
c.State = "present"
|
||||
c.Id = containerJSON.ID
|
||||
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
|
||||
}
|
||||
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 {
|
||||
log.Printf("Failed to remove: %s\n", 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))
|
||||
}
|
||||
|
||||
for _, container := range containers {
|
||||
for _, containerName := range container.Names {
|
||||
if containerName == c.Name {
|
||||
if c.Id == "" {
|
||||
c.Id = container.ID
|
||||
}
|
||||
return container.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
95
internal/resource/container_test.go
Normal file
95
internal/resource/container_test.go
Normal file
@ -0,0 +1,95 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
func TestNewContainerResource(t *testing.T) {
|
||||
c := NewContainer(&mocks.MockContainerClient{})
|
||||
assert.NotEqual(t, nil, c)
|
||||
}
|
||||
|
||||
func TestReadContainer(t *testing.T) {
|
||||
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
|
||||
},
|
||||
}
|
||||
|
||||
c := NewContainer(m)
|
||||
assert.NotEqual(t, nil, c)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
applyErr := c.Apply()
|
||||
assert.Equal(t, nil, applyErr)
|
||||
|
||||
c.State = "absent"
|
||||
|
||||
applyDeleteErr := c.Apply()
|
||||
assert.Equal(t, nil, applyDeleteErr)
|
||||
}
|
||||
|
||||
func TestContainerResolveId(t *testing.T) {
|
||||
|
||||
}
|
81
internal/resource/declaration.go
Normal file
81
internal/resource/declaration.go
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
_ "log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Declaration struct {
|
||||
Type string `yaml:"type"`
|
||||
Attributes yaml.Node `yaml:"attributes"`
|
||||
Implementation Resource `-`
|
||||
}
|
||||
|
||||
type ResourceLoader interface {
|
||||
LoadDecl(string) error
|
||||
}
|
||||
|
||||
type StateTransformer interface {
|
||||
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
|
||||
}
|
||||
|
||||
func NewDeclaration() *Declaration {
|
||||
return &Declaration{}
|
||||
}
|
||||
|
||||
func (d *Declaration) LoadDecl(yamlResourceDeclaration string) error {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (d *Declaration) UpdateYamlFromResource() error {
|
||||
if d.Implementation != nil {
|
||||
return d.Attributes.Encode(d.Implementation)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Declaration) Resource() Resource {
|
||||
return d.Implementation
|
||||
}
|
||||
|
||||
func (d *Declaration) SetURI(uri string) error {
|
||||
d.Implementation = NewResource(uri)
|
||||
if d.Implementation == nil {
|
||||
panic("unknown resource")
|
||||
}
|
||||
d.Type = d.Implementation.Type()
|
||||
d.Implementation.Read(context.Background()) // fix
|
||||
return nil
|
||||
}
|
@ -1,14 +1,16 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
package resource
|
||||
|
||||
import (
|
||||
_ "fmt"
|
||||
_ "log"
|
||||
"io"
|
||||
"gopkg.in/yaml.v3"
|
||||
_ "net/url"
|
||||
)
|
||||
|
||||
type Document struct {
|
||||
Nodes []yaml.Node `yaml:"resources"`
|
||||
ResourceDecls []Resource `-`
|
||||
ResourceDecls []Declaration `yaml:"resources"`
|
||||
}
|
||||
|
||||
func NewDocument() *Document {
|
||||
@ -18,18 +20,48 @@ func NewDocument() *Document {
|
||||
func (d *Document) Load(r io.Reader) error {
|
||||
yamlDecoder := yaml.NewDecoder(r)
|
||||
yamlDecoder.Decode(d)
|
||||
d.ResourceDecls = make([]Resource, len(d.Nodes))
|
||||
for i,node := range(d.Nodes) {
|
||||
resourceDecl := NewDeclaration()
|
||||
node.Decode(resourceDecl)
|
||||
if r,e := ResourceTypes.New(resourceDecl.Type); e == nil {
|
||||
resourceDecl.Attributes.Decode(r)
|
||||
d.ResourceDecls[i] = r
|
||||
for i := range(d.ResourceDecls) {
|
||||
if _,e := d.ResourceDecls[i].LoadResourceFromYaml(); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Document) Resources() []Resource {
|
||||
func (d *Document) Resources() []Declaration {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"fmt"
|
||||
"log"
|
||||
@ -22,7 +24,7 @@ func TestDocumentLoader(t *testing.T) {
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
file := filepath.Join(dir, "foo.txt")
|
||||
file,_ := filepath.Abs(filepath.Join(dir, "foo.txt"))
|
||||
|
||||
document := fmt.Sprintf(`
|
||||
---
|
||||
@ -57,3 +59,57 @@ resources:
|
||||
resources := d.Resources()
|
||||
assert.Equal(t, 2, len(resources))
|
||||
}
|
||||
|
||||
func TestDocumentGenerator(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
fileContent := `// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
`
|
||||
|
||||
file,_ := filepath.Abs(filepath.Join(TempDir, "foo.txt"))
|
||||
|
||||
err := os.WriteFile(file, []byte(fileContent), 0644)
|
||||
assert.Nil(t, err)
|
||||
|
||||
expected := fmt.Sprintf(`
|
||||
resources:
|
||||
- type: file
|
||||
attributes:
|
||||
path: %s
|
||||
owner: "root"
|
||||
group: "root"
|
||||
mode: "0644"
|
||||
content: |
|
||||
%s
|
||||
filetype: "regular"
|
||||
state: present
|
||||
`, file, fileContent)
|
||||
|
||||
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.(*File).Path = filepath.Join(TempDir, "foo.txt")
|
||||
f.(*File).Read(ctx)
|
||||
d.AddResourceDeclaration("file", f)
|
||||
|
||||
ey := d.Generate(&documentYaml)
|
||||
assert.Equal(t, nil, ey)
|
||||
|
||||
assert.Greater(t, documentYaml.Len(), 0)
|
||||
assert.YAMLEq(t, documentYaml.String(), expected)
|
||||
}
|
||||
|
||||
func TestDocumentAddResource(t *testing.T) {
|
||||
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))
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
@ -8,10 +11,30 @@ import (
|
||||
"syscall"
|
||||
"gopkg.in/yaml.v3"
|
||||
"strconv"
|
||||
"path/filepath"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
var ErrInvalidResourceURI error = errors.New("Invalid resource URI")
|
||||
|
||||
func init() {
|
||||
ResourceTypes.Register("file", func() Resource { return NewFile() })
|
||||
ResourceTypes.Register("file", func(u *url.URL) Resource {
|
||||
f := NewFile()
|
||||
f.Path = filepath.Join(u.Hostname(), u.Path)
|
||||
return f
|
||||
})
|
||||
}
|
||||
|
||||
type File struct {
|
||||
@ -20,12 +43,27 @@ type File struct {
|
||||
Owner string `yaml:"owner"`
|
||||
Group string `yaml:"group"`
|
||||
Mode string `yaml:"mode"`
|
||||
Content string `yaml:"content"`
|
||||
Content string `yaml:"content",omitempty`
|
||||
FileType FileType `yaml:"filetype"`
|
||||
State string `yaml:"state"`
|
||||
}
|
||||
|
||||
func NewFile() *File {
|
||||
return &File{ loader: YamlLoadDecl }
|
||||
return &File{ loader: YamlLoadDecl, FileType: RegularFile }
|
||||
}
|
||||
|
||||
func (f *File) URI() string {
|
||||
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
|
||||
}
|
||||
|
||||
func (f *File) Apply() error {
|
||||
@ -47,32 +85,39 @@ func (f *File) Apply() error {
|
||||
return gidErr
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
if chownErr := createdFile.Chown(uid, gid); chownErr != nil {
|
||||
return chownErr
|
||||
}
|
||||
|
||||
mode,modeErr := strconv.ParseInt(f.Mode, 8, 64)
|
||||
if modeErr != nil {
|
||||
return modeErr
|
||||
}
|
||||
|
||||
if chmodErr := createdFile.Chmod(os.FileMode(mode)); chmodErr != nil {
|
||||
return chmodErr
|
||||
}
|
||||
|
||||
_,writeErr := createdFile.Write([]byte(f.Content))
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
}
|
||||
|
||||
if chownErr := os.Chown(f.Path, uid, gid); chownErr != nil {
|
||||
return chownErr
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -82,7 +127,21 @@ func (f *File) LoadDecl(yamlFileResourceDeclaration string) error {
|
||||
return f.loader(yamlFileResourceDeclaration, f)
|
||||
}
|
||||
|
||||
func (f *File) Read() ([]byte, error) {
|
||||
func (f *File) ResolveId(ctx context.Context) string {
|
||||
filePath, fileAbsErr := filepath.Abs(f.Path)
|
||||
if fileAbsErr != nil {
|
||||
panic(fileAbsErr)
|
||||
}
|
||||
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
|
||||
|
||||
info, e := os.Stat(f.Path)
|
||||
|
||||
if e != nil {
|
||||
@ -90,17 +149,23 @@ func (f *File) Read() ([]byte, error) {
|
||||
}
|
||||
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
fileUser, userErr := user.LookupId(strconv.Itoa(int(stat.Uid)))
|
||||
userId := strconv.Itoa(int(stat.Uid))
|
||||
groupId := strconv.Itoa(int(stat.Gid))
|
||||
fileUser, userErr := user.LookupId(userId)
|
||||
if userErr != nil { //UnknownUserIdError
|
||||
panic(userErr)
|
||||
}
|
||||
fileGroup, groupErr := user.LookupGroupId(strconv.Itoa(int(stat.Gid)))
|
||||
if groupErr != nil {
|
||||
panic(groupErr)
|
||||
}
|
||||
//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())
|
||||
|
||||
@ -117,3 +182,20 @@ func (f *File) Read() ([]byte, error) {
|
||||
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
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,21 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "encoding/json"
|
||||
"fmt"
|
||||
_ "context"
|
||||
"testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
_ "io"
|
||||
_ "log"
|
||||
_ "net/http"
|
||||
_ "net/http/httptest"
|
||||
_ "net/url"
|
||||
_ "io"
|
||||
"os"
|
||||
_ "log"
|
||||
"path/filepath"
|
||||
"github.com/stretchr/testify/assert"
|
||||
_ "encoding/json"
|
||||
_ "strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewFileResource(t *testing.T) {
|
||||
@ -30,7 +32,8 @@ func TestApplyResourceTransformation(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReadFile(t *testing.T) {
|
||||
file := filepath.Join(TempDir, "fooread.txt")
|
||||
ctx := context.Background()
|
||||
file, _ := filepath.Abs(filepath.Join(TempDir, "fooread.txt"))
|
||||
|
||||
decl := fmt.Sprintf(`
|
||||
path: "%s"
|
||||
@ -40,6 +43,7 @@ func TestReadFile(t *testing.T) {
|
||||
content: |-
|
||||
test line 1
|
||||
test line 2
|
||||
filetype: "regular"
|
||||
state: present
|
||||
`, file)
|
||||
|
||||
@ -52,14 +56,14 @@ func TestReadFile(t *testing.T) {
|
||||
assert.NotEqual(t, nil, f)
|
||||
|
||||
f.Path = file
|
||||
r,e := f.Read()
|
||||
r, e := f.Read(ctx)
|
||||
assert.Equal(t, nil, e)
|
||||
assert.Equal(t, "nobody", f.Owner)
|
||||
assert.YAMLEq(t, decl, string(r))
|
||||
}
|
||||
|
||||
func TestCreateFile(t *testing.T) {
|
||||
file := filepath.Join(TempDir, "foo.txt")
|
||||
file, _ := filepath.Abs(filepath.Join(TempDir, "foo.txt"))
|
||||
|
||||
decl := fmt.Sprintf(`
|
||||
path: "%s"
|
||||
@ -89,3 +93,40 @@ func TestCreateFile(t *testing.T) {
|
||||
assert.Equal(t, nil, f.Apply())
|
||||
assert.NoFileExists(t, file, nil)
|
||||
}
|
||||
|
||||
func TestFileType(t *testing.T) {
|
||||
fileType := []byte(`
|
||||
filetype: "directory"
|
||||
`)
|
||||
var testFile File
|
||||
err := yaml.Unmarshal(fileType, &testFile)
|
||||
assert.Nil(t, err)
|
||||
|
||||
}
|
||||
|
||||
func TestFileDirectory(t *testing.T) {
|
||||
file, _ := filepath.Abs(filepath.Join(TempDir, "testdir"))
|
||||
|
||||
decl := fmt.Sprintf(`
|
||||
path: "%s"
|
||||
owner: "nobody"
|
||||
group: "nobody"
|
||||
mode: "0700"
|
||||
filetype: "directory"
|
||||
state: present
|
||||
`, file)
|
||||
|
||||
f := NewFile()
|
||||
e := f.LoadDecl(decl)
|
||||
assert.Equal(t, nil, e)
|
||||
assert.Equal(t, "nobody", f.Owner)
|
||||
|
||||
applyErr := f.Apply()
|
||||
assert.Equal(t, nil, applyErr)
|
||||
assert.DirExists(t, file)
|
||||
|
||||
f.State = "absent"
|
||||
deleteErr := f.Apply()
|
||||
assert.Nil(t, deleteErr)
|
||||
assert.NoDirExists(t, file)
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
package resource
|
||||
|
||||
import (
|
||||
@ -5,6 +6,14 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func LookupUIDString(userName string) string {
|
||||
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 {
|
||||
|
@ -1,3 +1,4 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
package resource
|
||||
|
||||
import (
|
||||
|
@ -1,42 +1,34 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
package resource
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v3"
|
||||
"context"
|
||||
_ "fmt"
|
||||
_ "gopkg.in/yaml.v3"
|
||||
_ "net/url"
|
||||
)
|
||||
|
||||
type Declaration struct {
|
||||
Type string `yaml:"type"`
|
||||
Attributes yaml.Node `yaml:"attributes"`
|
||||
}
|
||||
|
||||
type Resource interface {
|
||||
Type() string
|
||||
URI() string
|
||||
//SetURI(string) error
|
||||
ResolveId(context.Context) string
|
||||
ResourceLoader
|
||||
StateTransformer
|
||||
ResourceReader
|
||||
}
|
||||
|
||||
type ResourceLoader interface {
|
||||
LoadDecl(string) error
|
||||
}
|
||||
|
||||
type StateTransformer interface {
|
||||
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
|
||||
// validate the type/uri
|
||||
type ResourceValidator interface {
|
||||
Validate() error
|
||||
}
|
||||
|
||||
type ResourceCreator interface {
|
||||
Create() error
|
||||
Create(context.Context) error
|
||||
}
|
||||
|
||||
type ResourceReader interface {
|
||||
Read() ([]byte, error)
|
||||
Read(context.Context) ([]byte, error)
|
||||
}
|
||||
|
||||
type ResourceUpdater interface {
|
||||
@ -47,11 +39,14 @@ type ResourceDeleter interface {
|
||||
Delete() error
|
||||
}
|
||||
|
||||
type ResourceDecoder struct {
|
||||
|
||||
func NewDeclaration() *Declaration {
|
||||
return &Declaration{}
|
||||
}
|
||||
|
||||
func (d *Declaration) LoadDecl(yamlResourceDeclaration string) error {
|
||||
return YamlLoadDecl(yamlResourceDeclaration, d)
|
||||
func NewResource(uri string) Resource {
|
||||
r,e := ResourceTypes.New(uri)
|
||||
if e == nil {
|
||||
return r
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -1,14 +1,18 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
package resource
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownResourceType = errors.New("Unknown resource type")
|
||||
ResourceTypes *Types = NewTypes()
|
||||
)
|
||||
|
||||
type TypeFactory func() Resource
|
||||
type TypeFactory func(*url.URL) Resource
|
||||
|
||||
type Types struct {
|
||||
registry map[string]TypeFactory
|
||||
@ -22,9 +26,21 @@ func (t *Types) Register(name string, factory TypeFactory) {
|
||||
t.registry[name] = factory
|
||||
}
|
||||
|
||||
func (t *Types) New(name string) (Resource, error) {
|
||||
if r,ok := t.registry[name]; ok {
|
||||
return r(), nil
|
||||
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)
|
||||
}
|
||||
return nil, fmt.Errorf("Unknown type: %s", name)
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"decl/tests/mocks"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func TestNewResourceTypes(t *testing.T) {
|
||||
@ -13,6 +16,8 @@ func TestNewResourceTypes(t *testing.T) {
|
||||
|
||||
func TestNewResourceTypesRegister(t *testing.T) {
|
||||
m := &mocks.MockResource {
|
||||
InjectType: func() string { return "foo" },
|
||||
InjectRead: func(ctx context.Context) ([]byte, error) { return nil,nil },
|
||||
InjectLoadDecl: func(string) error { return nil },
|
||||
InjectApply: func() error { return nil },
|
||||
}
|
||||
@ -20,13 +25,39 @@ func TestNewResourceTypesRegister(t *testing.T) {
|
||||
resourceTypes := NewTypes()
|
||||
assert.NotEqual(t, nil, resourceTypes)
|
||||
|
||||
resourceTypes.Register("foo", func() 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, m, r)
|
||||
}
|
||||
|
||||
func TestResourceTypesLoadResource(t *testing.T) {
|
||||
func TestResourceTypesFromURI(t *testing.T) {
|
||||
m := &mocks.MockResource {
|
||||
InjectType: func() string { return "foo" },
|
||||
InjectRead: func(ctx context.Context) ([]byte, error) { return nil,nil },
|
||||
InjectLoadDecl: func(string) error { return nil },
|
||||
InjectApply: func() error { return nil },
|
||||
}
|
||||
|
||||
resourceTypes := NewTypes()
|
||||
assert.NotEqual(t, nil, resourceTypes)
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
func TestResourceTypesHasType(t *testing.T) {
|
||||
m := mocks.NewFooResource()
|
||||
|
||||
resourceTypes := NewTypes()
|
||||
assert.NotNil(t, resourceTypes)
|
||||
|
||||
resourceTypes.Register("foo", func(*url.URL) Resource { return m })
|
||||
|
||||
assert.True(t, resourceTypes.Has("foo"))
|
||||
}
|
||||
|
@ -1,15 +1,41 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
package mocks
|
||||
|
||||
/*
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"context"
|
||||
"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/docker/docker/api/types"
|
||||
)
|
||||
|
||||
func newMockClient(doer func(*http.Request) (*http.Response, error)) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: transportEnsureBody(transportFunc(doer)),
|
||||
}
|
||||
type MockContainerClient struct {
|
||||
InjectContainerCreate func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error)
|
||||
InjectContainerList func(context.Context, types.ContainerListOptions) ([]types.Container, error)
|
||||
InjectContainerInspect func(context.Context, string) (types.ContainerJSON, error)
|
||||
InjectContainerRemove func(context.Context, string, container.RemoveOptions) error
|
||||
InjectClose func() error
|
||||
}
|
||||
|
||||
*/
|
||||
func (m *MockContainerClient) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) {
|
||||
return m.InjectContainerCreate(ctx, config, hostConfig, networkingConfig, platform, containerName)
|
||||
}
|
||||
|
||||
func (m *MockContainerClient) ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) {
|
||||
return m.InjectContainerList(ctx, options)
|
||||
}
|
||||
|
||||
func (m *MockContainerClient) ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error) {
|
||||
return m.InjectContainerInspect(ctx, containerID)
|
||||
}
|
||||
|
||||
func (m *MockContainerClient) ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error {
|
||||
return m.InjectContainerRemove(ctx, containerID, options)
|
||||
}
|
||||
|
||||
func (m *MockContainerClient) Close() error {
|
||||
if m.InjectClose == nil {
|
||||
return nil
|
||||
}
|
||||
return m.InjectClose()
|
||||
}
|
||||
|
16
tests/mocks/fooresource.go
Normal file
16
tests/mocks/fooresource.go
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func NewFooResource() *MockResource {
|
||||
return &MockResource {
|
||||
InjectType: func() string { return "foo" },
|
||||
InjectRead: func(ctx context.Context) ([]byte, error) { return nil,nil },
|
||||
InjectLoadDecl: func(string) error { return nil },
|
||||
InjectApply: func() error { return nil },
|
||||
}
|
||||
}
|
@ -1,8 +1,21 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type MockResource struct {
|
||||
InjectURI func() string
|
||||
InjectType func() string
|
||||
InjectLoadDecl func(string) error
|
||||
InjectApply func() error
|
||||
InjectRead func(context.Context) ([]byte, error)
|
||||
}
|
||||
|
||||
func (m *MockResource) URI() string {
|
||||
return m.InjectURI()
|
||||
}
|
||||
|
||||
func (m *MockResource) LoadDecl(yamlResourceDeclaration string) error {
|
||||
@ -12,3 +25,11 @@ func (m *MockResource) LoadDecl(yamlResourceDeclaration string) error {
|
||||
func (m *MockResource) Apply() error {
|
||||
return m.InjectApply()
|
||||
}
|
||||
|
||||
func (m *MockResource) Read(ctx context.Context) ([]byte, error) {
|
||||
return m.InjectRead(ctx)
|
||||
}
|
||||
|
||||
func (m *MockResource) Type() string {
|
||||
return m.InjectType()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user