update container/iptables resources
This commit is contained in:
parent
f25fa59449
commit
43a2274b7e
@ -15,4 +15,4 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ncipollo/release-action@v1
|
- uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
artifacts: "decl"
|
artifacts: "jx"
|
||||||
|
3
Makefile
3
Makefile
@ -9,4 +9,5 @@ jx-cli:
|
|||||||
go build -o jx $(LDFLAGS) ./cmd/cli/main.go
|
go build -o jx $(LDFLAGS) ./cmd/cli/main.go
|
||||||
|
|
||||||
test: jx-cli
|
test: jx-cli
|
||||||
go test ./...
|
go test -coverprofile=artifacts/coverage.profile ./...
|
||||||
|
go tool cover -html=artifacts/coverage.profile -o artifacts/code-coverage.html
|
||||||
|
@ -13,7 +13,7 @@ These tools work with YAML descriptions of resources (E.g. files, users, contain
|
|||||||
Testing the current version involves checking out main and building.
|
Testing the current version involves checking out main and building.
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://gitea.rosskeen.house/Declarative/decl.git
|
git clone https://gitea.rosskeen.house/doublejynx/jx.git
|
||||||
|
|
||||||
make test
|
make test
|
||||||
|
|
||||||
|
@ -217,24 +217,27 @@ func DiffSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
|||||||
rightDocuments := make([]*resource.Document, 0, 100)
|
rightDocuments := make([]*resource.Document, 0, 100)
|
||||||
|
|
||||||
slog.Info("jx diff subcommand", "left", leftSource, "right", rightSource, "flagset", cmd)
|
slog.Info("jx diff subcommand", "left", leftSource, "right", rightSource, "flagset", cmd)
|
||||||
leftDocuments = append(leftDocuments, LoadSourceURI(leftSource)...)
|
|
||||||
|
|
||||||
if rightSource == "" {
|
if rightSource == "" {
|
||||||
slog.Info("jx diff clone", "docs", leftDocuments)
|
rightDocuments = append(rightDocuments, LoadSourceURI(leftSource)...)
|
||||||
for i, doc := range leftDocuments {
|
slog.Info("jx diff clone", "docs", rightDocuments)
|
||||||
|
for i, doc := range rightDocuments {
|
||||||
if doc != nil {
|
if doc != nil {
|
||||||
rightDocuments = append(rightDocuments, doc.Clone())
|
leftDocuments = append(leftDocuments, doc.Clone())
|
||||||
for _,resourceDeclaration := range leftDocuments[i].Resources() {
|
for _,resourceDeclaration := range leftDocuments[i].Resources() {
|
||||||
if _, e := resourceDeclaration.Resource().Read(ctx); e != nil {
|
if _, e := resourceDeclaration.Resource().Read(ctx); e != nil {
|
||||||
return e
|
slog.Info("jx diff ", "err", e)
|
||||||
|
//return e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
leftDocuments = append(leftDocuments, LoadSourceURI(leftSource)...)
|
||||||
rightDocuments = append(rightDocuments, LoadSourceURI(rightSource)...)
|
rightDocuments = append(rightDocuments, LoadSourceURI(rightSource)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slog.Info("jx diff ", "right", rightDocuments, "left", leftDocuments)
|
||||||
index := 0
|
index := 0
|
||||||
for {
|
for {
|
||||||
if index >= len(rightDocuments) && index >= len(leftDocuments) {
|
if index >= len(rightDocuments) && index >= len(leftDocuments) {
|
||||||
|
@ -36,15 +36,35 @@ func NewCommand() *Command {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
cmd := exec.Command(c.Path, args...)
|
cmd := exec.Command(c.Path, args...)
|
||||||
|
|
||||||
|
slog.Info("execute() - cmd", "path", c.Path, "args", args)
|
||||||
|
output, stdoutPipeErr := cmd.StdoutPipe()
|
||||||
|
if stdoutPipeErr != nil {
|
||||||
|
return nil, stdoutPipeErr
|
||||||
|
}
|
||||||
|
|
||||||
stderr, pipeErr := cmd.StderrPipe()
|
stderr, pipeErr := cmd.StderrPipe()
|
||||||
if pipeErr != nil {
|
if pipeErr != nil {
|
||||||
return nil, pipeErr
|
return nil, pipeErr
|
||||||
}
|
}
|
||||||
output, err := cmd.Output()
|
|
||||||
|
|
||||||
|
if startErr := cmd.Start(); startErr != nil {
|
||||||
|
return nil, startErr
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("execute() - start", "cmd", cmd)
|
||||||
|
stdOutOutput, _ := io.ReadAll(output)
|
||||||
stdErrOutput, _ := io.ReadAll(stderr)
|
stdErrOutput, _ := io.ReadAll(stderr)
|
||||||
slog.Info("execute()", "path", c.Path, "args", args, "output", output, "error", stdErrOutput)
|
|
||||||
return output, err
|
slog.Info("execute() - io", "stdout", string(stdOutOutput), "stderr", string(stdErrOutput))
|
||||||
|
waitErr := cmd.Wait()
|
||||||
|
|
||||||
|
slog.Info("execute()", "path", c.Path, "args", args, "output", string(stdOutOutput), "error", string(stdErrOutput))
|
||||||
|
|
||||||
|
if len(stdErrOutput) > 0 {
|
||||||
|
return stdOutOutput, fmt.Errorf(string(stdErrOutput))
|
||||||
|
}
|
||||||
|
return stdOutOutput, waitErr
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@ -60,15 +80,21 @@ func (c *Command) LoadDecl(yamlResourceDeclaration string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Command) Template(value any) ([]string, error) {
|
func (c *Command) Template(value any) ([]string, error) {
|
||||||
var args []string = make([]string, len(c.Args))
|
var args []string = make([]string, 0, len(c.Args) * 2)
|
||||||
for i, arg := range c.Args {
|
for i, arg := range c.Args {
|
||||||
var commandLineArg strings.Builder
|
var commandLineArg strings.Builder
|
||||||
err := template.Must(template.New(fmt.Sprintf("arg%d", i)).Parse(string(arg))).Execute(&commandLineArg, value)
|
err := template.Must(template.New(fmt.Sprintf("arg%d", i)).Parse(string(arg))).Execute(&commandLineArg, value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
args[i] = commandLineArg.String()
|
if commandLineArg.Len() > 0 {
|
||||||
|
splitArg := strings.Split(commandLineArg.String(), " ")
|
||||||
|
slog.Info("Template()", "split", splitArg, "len", len(splitArg))
|
||||||
|
args = append(args, splitArg...)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Template()", "Args", c.Args, "lencargs", len(c.Args), "args", args, "lenargs", len(args), "value", value)
|
||||||
return args, nil
|
return args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/api/types/strslice"
|
"github.com/docker/docker/api/types/strslice"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
_ "gopkg.in/yaml.v3"
|
_ "gopkg.in/yaml.v3"
|
||||||
@ -24,6 +25,7 @@ import (
|
|||||||
_ "strings"
|
_ "strings"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContainerClient interface {
|
type ContainerClient interface {
|
||||||
@ -42,6 +44,7 @@ type Container struct {
|
|||||||
Cmd []string `json:"cmd,omitempty" yaml:"cmd,omitempty"`
|
Cmd []string `json:"cmd,omitempty" yaml:"cmd,omitempty"`
|
||||||
Entrypoint strslice.StrSlice `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty"`
|
Entrypoint strslice.StrSlice `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty"`
|
||||||
Args []string `json:"args,omitempty" yaml:"args,omitempty"`
|
Args []string `json:"args,omitempty" yaml:"args,omitempty"`
|
||||||
|
Ports []string `json:"ports,omitempty" yaml:"ports,omitempty"`
|
||||||
Environment map[string]string `json:"environment" yaml:"environment"`
|
Environment map[string]string `json:"environment" yaml:"environment"`
|
||||||
Image string `json:"image" yaml:"image"`
|
Image string `json:"image" yaml:"image"`
|
||||||
ResolvConfPath string `json:"resolvconfpath" yaml:"resolvconfpath"`
|
ResolvConfPath string `json:"resolvconfpath" yaml:"resolvconfpath"`
|
||||||
@ -61,13 +64,14 @@ type Container struct {
|
|||||||
GraphDriver types.GraphDriverData `json:"graphdriver" yaml:"graphdriver"`
|
GraphDriver types.GraphDriverData `json:"graphdriver" yaml:"graphdriver"`
|
||||||
SizeRw *int64 `json:",omitempty" yaml:",omitempty"`
|
SizeRw *int64 `json:",omitempty" yaml:",omitempty"`
|
||||||
SizeRootFs *int64 `json:",omitempty" yaml:",omitempty"`
|
SizeRootFs *int64 `json:",omitempty" yaml:",omitempty"`
|
||||||
|
Networks []string `json:"networks,omitempty" yaml:"networks,omitempty"`
|
||||||
/*
|
/*
|
||||||
Mounts []MountPoint
|
Mounts []MountPoint
|
||||||
Config *container.Config
|
Config *container.Config
|
||||||
NetworkSettings *NetworkSettings
|
NetworkSettings *NetworkSettings
|
||||||
*/
|
*/
|
||||||
|
|
||||||
State string `yaml:"state"`
|
State string `yaml:"state,omitempty" json:"state,omitempty"`
|
||||||
|
|
||||||
apiClient ContainerClient
|
apiClient ContainerClient
|
||||||
}
|
}
|
||||||
@ -121,11 +125,16 @@ func (c *Container) Clone() Resource {
|
|||||||
GraphDriver: c.GraphDriver,
|
GraphDriver: c.GraphDriver,
|
||||||
SizeRw: c.SizeRw,
|
SizeRw: c.SizeRw,
|
||||||
SizeRootFs: c.SizeRootFs,
|
SizeRootFs: c.SizeRootFs,
|
||||||
|
Networks: c.Networks,
|
||||||
State: c.State,
|
State: c.State,
|
||||||
apiClient: c.apiClient,
|
apiClient: c.apiClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Container) StateMachine() machine.Stater {
|
||||||
|
return ProcessMachine()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Container) URI() string {
|
func (c *Container) URI() string {
|
||||||
return fmt.Sprintf("container://%s", c.Id)
|
return fmt.Sprintf("container://%s", c.Id)
|
||||||
}
|
}
|
||||||
@ -173,11 +182,17 @@ func (c *Container) LoadDecl(yamlResourceDeclaration string) error {
|
|||||||
|
|
||||||
func (c *Container) Create(ctx context.Context) error {
|
func (c *Container) Create(ctx context.Context) error {
|
||||||
numberOfEnvironmentVariables := len(c.Environment)
|
numberOfEnvironmentVariables := len(c.Environment)
|
||||||
|
|
||||||
|
portset := nat.PortSet {}
|
||||||
|
for _, port := range c.Ports {
|
||||||
|
portset[nat.Port(port)] = struct{}{}
|
||||||
|
}
|
||||||
config := &container.Config{
|
config := &container.Config{
|
||||||
Image: c.Image,
|
Image: c.Image,
|
||||||
Cmd: c.Cmd,
|
Cmd: c.Cmd,
|
||||||
Entrypoint: c.Entrypoint,
|
Entrypoint: c.Entrypoint,
|
||||||
Tty: false,
|
Tty: false,
|
||||||
|
ExposedPorts: portset,
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Env = make([]string, numberOfEnvironmentVariables)
|
config.Env = make([]string, numberOfEnvironmentVariables)
|
||||||
@ -194,7 +209,16 @@ func (c *Container) Create(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := c.apiClient.ContainerCreate(ctx, config, &c.HostConfig, nil, nil, c.Name)
|
networkConfig := &network.NetworkingConfig{
|
||||||
|
EndpointsConfig: map[string]*network.EndpointSettings{},
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := &network.EndpointSettings{}
|
||||||
|
for _, network := range c.Networks {
|
||||||
|
networkConfig.EndpointsConfig[network] = settings
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.apiClient.ContainerCreate(ctx, config, &c.HostConfig, networkConfig, nil, c.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
168
internal/resource/container_network.go
Normal file
168
internal/resource/container_network.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
// Container resource
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
_ "log/slog"
|
||||||
|
"net/url"
|
||||||
|
_ "os"
|
||||||
|
_ "os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
_ "strings"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContainerNetworkClient interface {
|
||||||
|
ContainerClient
|
||||||
|
NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContainerNetwork struct {
|
||||||
|
Id string `json:"ID,omitempty" yaml:"ID,omitempty"`
|
||||||
|
Name string `json:"name" yaml:"name"`
|
||||||
|
|
||||||
|
State string `yaml:"state"`
|
||||||
|
|
||||||
|
apiClient ContainerNetworkClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ResourceTypes.Register("container_network", func(u *url.URL) Resource {
|
||||||
|
n := NewContainerNetwork(nil)
|
||||||
|
n.Name = filepath.Join(u.Hostname(), u.Path)
|
||||||
|
return n
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewContainerNetwork(containerClientApi ContainerNetworkClient) *ContainerNetwork {
|
||||||
|
var apiClient ContainerNetworkClient = containerClientApi
|
||||||
|
if apiClient == nil {
|
||||||
|
var err error
|
||||||
|
apiClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &ContainerNetwork{
|
||||||
|
apiClient: apiClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ContainerNetwork) Clone() Resource {
|
||||||
|
return &ContainerNetwork {
|
||||||
|
Id: n.Id,
|
||||||
|
Name: n.Name,
|
||||||
|
State: n.State,
|
||||||
|
apiClient: n.apiClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ContainerNetwork) StateMachine() machine.Stater {
|
||||||
|
return StorageMachine()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ContainerNetwork) URI() string {
|
||||||
|
return fmt.Sprintf("container_network://%s", n.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ContainerNetwork) SetURI(uri string) error {
|
||||||
|
resourceUri, e := url.Parse(uri)
|
||||||
|
if e == nil {
|
||||||
|
if resourceUri.Scheme == n.Type() {
|
||||||
|
n.Name, e = filepath.Abs(filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI()))
|
||||||
|
} else {
|
||||||
|
e = fmt.Errorf("%w: %s is not a %s", ErrInvalidResourceURI, uri, n.Type())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ContainerNetwork) JSON() ([]byte, error) {
|
||||||
|
return json.Marshal(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ContainerNetwork) Validate() error {
|
||||||
|
return fmt.Errorf("failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ContainerNetwork) Apply() error {
|
||||||
|
ctx := context.Background()
|
||||||
|
switch n.State {
|
||||||
|
case "absent":
|
||||||
|
return n.Delete(ctx)
|
||||||
|
case "present":
|
||||||
|
return n.Create(ctx)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ContainerNetwork) Load(r io.Reader) error {
|
||||||
|
d := NewYAMLDecoder(r)
|
||||||
|
return d.Decode(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ContainerNetwork) LoadDecl(yamlResourceDeclaration string) error {
|
||||||
|
d := NewYAMLStringDecoder(yamlResourceDeclaration)
|
||||||
|
return d.Decode(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ContainerNetwork) Create(ctx context.Context) error {
|
||||||
|
networkResp, err := n.apiClient.NetworkCreate(ctx, n.Name, types.NetworkCreate{
|
||||||
|
Driver: "bridge",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
n.Id = networkResp.ID
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// produce yaml representation of any resource
|
||||||
|
|
||||||
|
func (n *ContainerNetwork) Read(ctx context.Context) ([]byte, error) {
|
||||||
|
return yaml.Marshal(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ContainerNetwork) Delete(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *ContainerNetwork) Type() string { return "container_network" }
|
||||||
|
|
||||||
|
func (n *ContainerNetwork) ResolveId(ctx context.Context) string {
|
||||||
|
filterArgs := filters.NewArgs()
|
||||||
|
filterArgs.Add("name", "/"+n.Name)
|
||||||
|
containers, err := n.apiClient.ContainerList(ctx, container.ListOptions{
|
||||||
|
All: true,
|
||||||
|
Filters: filterArgs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("%w: %s %s", err, n.Type(), n.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, container := range containers {
|
||||||
|
for _, containerName := range container.Names {
|
||||||
|
if containerName == n.Name {
|
||||||
|
if n.Id == "" {
|
||||||
|
n.Id = container.ID
|
||||||
|
}
|
||||||
|
return container.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
47
internal/resource/container_network_test.go
Normal file
47
internal/resource/container_network_test.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
_ "io"
|
||||||
|
_ "net/http"
|
||||||
|
_ "net/http/httptest"
|
||||||
|
_ "net/url"
|
||||||
|
_ "os"
|
||||||
|
_ "strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewContainerNetworkResource(t *testing.T) {
|
||||||
|
c := NewContainerNetwork(&mocks.MockContainerClient{})
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadContainerNetwork(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
decl := `
|
||||||
|
name: "testcontainernetwork"
|
||||||
|
state: present
|
||||||
|
`
|
||||||
|
m := &mocks.MockContainerClient{
|
||||||
|
}
|
||||||
|
|
||||||
|
n := NewContainerNetwork(m)
|
||||||
|
assert.NotNil(t, n)
|
||||||
|
|
||||||
|
e := n.LoadDecl(decl)
|
||||||
|
assert.Nil(t, e)
|
||||||
|
assert.Equal(t, "testcontainernetwork", n.Name)
|
||||||
|
|
||||||
|
resourceYaml, readContainerErr := n.Read(ctx)
|
||||||
|
assert.Equal(t, nil, readContainerErr)
|
||||||
|
assert.Greater(t, len(resourceYaml), 0)
|
||||||
|
}
|
@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
type DeclarationType struct {
|
type DeclarationType struct {
|
||||||
Type TypeName `json:"type" yaml:"type"`
|
Type TypeName `json:"type" yaml:"type"`
|
||||||
Transition string `json:"transition" yaml:"transition"`
|
Transition string `json:"transition,omitempty" yaml:"transition,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Declaration struct {
|
type Declaration struct {
|
||||||
@ -64,6 +64,11 @@ func (d *Declaration) Resource() Resource {
|
|||||||
return d.Attributes
|
return d.Attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Declaration) Apply() error {
|
||||||
|
stater := d.Attributes.StateMachine()
|
||||||
|
stater.Trigger(d.Transition)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Declaration) SetURI(uri string) error {
|
func (d *Declaration) SetURI(uri string) error {
|
||||||
slog.Info("Declaration.SetURI()", "uri", uri, "declaration", d)
|
slog.Info("Declaration.SetURI()", "uri", uri, "declaration", d)
|
||||||
d.Attributes = NewResource(uri)
|
d.Attributes = NewResource(uri)
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
|
||||||
package resource
|
package resource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -97,7 +99,7 @@ func TestDeclarationJson(t *testing.T) {
|
|||||||
"type": "user",
|
"type": "user",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"name": "testuser",
|
"name": "testuser",
|
||||||
"uid": 10012
|
"uid": "10012"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@ -106,6 +108,6 @@ func TestDeclarationJson(t *testing.T) {
|
|||||||
assert.Nil(t, ue)
|
assert.Nil(t, ue)
|
||||||
assert.Equal(t, TypeName("user"), userResourceDeclaration.Type)
|
assert.Equal(t, TypeName("user"), userResourceDeclaration.Type)
|
||||||
assert.Equal(t, "testuser", userResourceDeclaration.Attributes.(*User).Name)
|
assert.Equal(t, "testuser", userResourceDeclaration.Attributes.(*User).Name)
|
||||||
assert.Equal(t, 10012, userResourceDeclaration.Attributes.(*User).UID)
|
assert.Equal(t, "10012", userResourceDeclaration.Attributes.(*User).UID)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ func TestNewYAMLDecoder(t *testing.T) {
|
|||||||
func TestNewDecoderDecodeJSON(t *testing.T) {
|
func TestNewDecoderDecodeJSON(t *testing.T) {
|
||||||
decl := `{
|
decl := `{
|
||||||
"name": "testuser",
|
"name": "testuser",
|
||||||
"uid": 12001,
|
"uid": "12001",
|
||||||
"group": "12001",
|
"group": "12001",
|
||||||
"home": "/home/testuser",
|
"home": "/home/testuser",
|
||||||
"state": "present"
|
"state": "present"
|
||||||
@ -41,7 +41,7 @@ func TestNewDecoderDecodeJSON(t *testing.T) {
|
|||||||
func TestNewJSONStringDecoder(t *testing.T) {
|
func TestNewJSONStringDecoder(t *testing.T) {
|
||||||
decl := `{
|
decl := `{
|
||||||
"name": "testuser",
|
"name": "testuser",
|
||||||
"uid": 12001,
|
"uid": "12001",
|
||||||
"group": "12001",
|
"group": "12001",
|
||||||
"home": "/home/testuser",
|
"home": "/home/testuser",
|
||||||
"state": "present"
|
"state": "present"
|
||||||
|
@ -4,7 +4,7 @@ package resource
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
_ "fmt"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@ -147,7 +147,6 @@ func (d *Document) Diff(with *Document, output io.Writer) (string, error) {
|
|||||||
|
|
||||||
for _,diff := range yamldiff.Do(yamlDiff, withDiff, opts...) {
|
for _,diff := range yamldiff.Do(yamlDiff, withDiff, opts...) {
|
||||||
slog.Info("Diff()", "diff", diff)
|
slog.Info("Diff()", "diff", diff)
|
||||||
fmt.Printf("yaml %#v with %#v\n", yamlDiff, withDiff)
|
|
||||||
_,e := output.Write([]byte(diff.Dump()))
|
_,e := output.Write([]byte(diff.Dump()))
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return "", e
|
return "", e
|
||||||
|
@ -45,9 +45,10 @@ resources:
|
|||||||
- type: user
|
- type: user
|
||||||
attributes:
|
attributes:
|
||||||
name: "testuser"
|
name: "testuser"
|
||||||
uid: 10022
|
uid: "10022"
|
||||||
group: "10022"
|
group: "10022"
|
||||||
home: "/home/testuser"
|
home: "/home/testuser"
|
||||||
|
createhome: true
|
||||||
state: present
|
state: present
|
||||||
`, file)
|
`, file)
|
||||||
d := NewDocument()
|
d := NewDocument()
|
||||||
@ -138,9 +139,10 @@ resources:
|
|||||||
- type: user
|
- type: user
|
||||||
attributes:
|
attributes:
|
||||||
name: "testuser"
|
name: "testuser"
|
||||||
uid: 10022
|
uid: "10022"
|
||||||
group: "10022"
|
group: "10022"
|
||||||
home: "/home/testuser"
|
home: "/home/testuser"
|
||||||
|
createhome: true
|
||||||
state: present
|
state: present
|
||||||
`
|
`
|
||||||
d := NewDocument()
|
d := NewDocument()
|
||||||
@ -169,9 +171,10 @@ resources:
|
|||||||
- type: user
|
- type: user
|
||||||
attributes:
|
attributes:
|
||||||
name: "testuser"
|
name: "testuser"
|
||||||
uid: 10022
|
uid: "10022"
|
||||||
group: "10022"
|
group: "10022"
|
||||||
home: "/home/testuser"
|
home: "/home/testuser"
|
||||||
|
createhome: true
|
||||||
state: present
|
state: present
|
||||||
`
|
`
|
||||||
d := NewDocument()
|
d := NewDocument()
|
||||||
@ -194,7 +197,7 @@ resources:
|
|||||||
- type: user
|
- type: user
|
||||||
attributes:
|
attributes:
|
||||||
name: "testuser"
|
name: "testuser"
|
||||||
uid: 10022
|
uid: "10022"
|
||||||
home: "/home/testuser"
|
home: "/home/testuser"
|
||||||
state: present
|
state: present
|
||||||
- type: file
|
- type: file
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
_ "strings"
|
_ "strings"
|
||||||
"io"
|
"io"
|
||||||
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Exec struct {
|
type Exec struct {
|
||||||
@ -48,6 +49,10 @@ func (x *Exec) Clone() Resource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *Exec) StateMachine() machine.Stater {
|
||||||
|
return ProcessMachine()
|
||||||
|
}
|
||||||
|
|
||||||
func (x *Exec) URI() string {
|
func (x *Exec) URI() string {
|
||||||
return fmt.Sprintf("exec://%s", x.Id)
|
return fmt.Sprintf("exec://%s", x.Id)
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileType string
|
type FileType string
|
||||||
@ -34,6 +35,9 @@ const (
|
|||||||
|
|
||||||
var ErrInvalidResourceURI error = errors.New("Invalid resource URI")
|
var ErrInvalidResourceURI error = errors.New("Invalid resource URI")
|
||||||
var ErrInvalidFileInfo error = errors.New("Invalid FileInfo")
|
var ErrInvalidFileInfo error = errors.New("Invalid FileInfo")
|
||||||
|
var ErrInvalidFileMode error = errors.New("Invalid Mode")
|
||||||
|
var ErrInvalidFileOwner error = errors.New("Unknown User")
|
||||||
|
var ErrInvalidFileGroup error = errors.New("Unknown Group")
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ResourceTypes.Register("file", func(u *url.URL) Resource {
|
ResourceTypes.Register("file", func(u *url.URL) Resource {
|
||||||
@ -60,7 +64,7 @@ type File struct {
|
|||||||
Size int64 `json:"size,omitempty" yaml:"size,omitempty"`
|
Size int64 `json:"size,omitempty" yaml:"size,omitempty"`
|
||||||
Target string `json:"target,omitempty" yaml:"target,omitempty"`
|
Target string `json:"target,omitempty" yaml:"target,omitempty"`
|
||||||
FileType FileType `json:"filetype" yaml:"filetype"`
|
FileType FileType `json:"filetype" yaml:"filetype"`
|
||||||
State string `json:"state" yaml:"state"`
|
State string `json:"state,omitempty" yaml:"state,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourceFileInfo struct {
|
type ResourceFileInfo struct {
|
||||||
@ -100,6 +104,10 @@ func (f *File) Clone() Resource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *File) StateMachine() machine.Stater {
|
||||||
|
return StorageMachine()
|
||||||
|
}
|
||||||
|
|
||||||
func (f *File) URI() string {
|
func (f *File) URI() string {
|
||||||
return fmt.Sprintf("file://%s", f.Path)
|
return fmt.Sprintf("file://%s", f.Path)
|
||||||
}
|
}
|
||||||
@ -134,7 +142,7 @@ func (f *File) Apply() error {
|
|||||||
{
|
{
|
||||||
uid, uidErr := LookupUID(f.Owner)
|
uid, uidErr := LookupUID(f.Owner)
|
||||||
if uidErr != nil {
|
if uidErr != nil {
|
||||||
return uidErr
|
return fmt.Errorf("%w: unkwnon user %d", ErrInvalidFileOwner, uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
gid, gidErr := LookupGID(f.Group)
|
gid, gidErr := LookupGID(f.Group)
|
||||||
@ -145,9 +153,8 @@ func (f *File) Apply() error {
|
|||||||
slog.Info("File.Mode", "mode", f.Mode)
|
slog.Info("File.Mode", "mode", f.Mode)
|
||||||
mode, modeErr := strconv.ParseInt(f.Mode, 8, 64)
|
mode, modeErr := strconv.ParseInt(f.Mode, 8, 64)
|
||||||
if modeErr != nil {
|
if modeErr != nil {
|
||||||
return modeErr
|
return fmt.Errorf("%w: invalid mode %d", ErrInvalidFileMode, mode)
|
||||||
}
|
}
|
||||||
slog.Info("File.Mode Parse", "mode", mode, "err", modeErr)
|
|
||||||
|
|
||||||
//e := os.Stat(f.path)
|
//e := os.Stat(f.path)
|
||||||
//if os.IsNotExist(e) {
|
//if os.IsNotExist(e) {
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
"os/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewFileResource(t *testing.T) {
|
func TestNewFileResource(t *testing.T) {
|
||||||
@ -26,6 +27,20 @@ func TestNewFileResource(t *testing.T) {
|
|||||||
assert.NotEqual(t, nil, f)
|
assert.NotEqual(t, nil, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewFileNormalized(t *testing.T) {
|
||||||
|
file := fmt.Sprintf("%s/%s", TempDir, "bar/../fooread.txt")
|
||||||
|
absFilePath,_ := filepath.Abs(file)
|
||||||
|
f := NewNormalizedFile()
|
||||||
|
assert.NotNil(t, f)
|
||||||
|
f.SetURI("file://" + file)
|
||||||
|
|
||||||
|
assert.NotEqual(t, file, f.Path)
|
||||||
|
assert.Equal(t, absFilePath, f.Path)
|
||||||
|
|
||||||
|
assert.NotEqual(t, "file://" + file, f.URI())
|
||||||
|
assert.Equal(t, "file://" + absFilePath, f.URI())
|
||||||
|
}
|
||||||
|
|
||||||
func TestApplyResourceTransformation(t *testing.T) {
|
func TestApplyResourceTransformation(t *testing.T) {
|
||||||
f := NewFile()
|
f := NewFile()
|
||||||
assert.NotEqual(t, nil, f)
|
assert.NotEqual(t, nil, f)
|
||||||
@ -280,3 +295,64 @@ func TestFileResourceFileInfo(t *testing.T) {
|
|||||||
fi := f.FileInfo()
|
fi := f.FileInfo()
|
||||||
assert.Equal(t, os.FileMode(0600), fi.Mode().Perm())
|
assert.Equal(t, os.FileMode(0600), fi.Mode().Perm())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFileClone(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
testFile := filepath.Join(TempDir, "testorig.txt")
|
||||||
|
testCloneFile := filepath.Join(TempDir, "testclone.txt")
|
||||||
|
|
||||||
|
f := NewFile()
|
||||||
|
assert.NotNil(t, f)
|
||||||
|
|
||||||
|
f.Path = testFile
|
||||||
|
f.Mode = "0600"
|
||||||
|
f.State = "present"
|
||||||
|
assert.Nil(t, f.Apply())
|
||||||
|
|
||||||
|
f.Read(ctx)
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
clone := f.Clone().(*File)
|
||||||
|
assert.Equal(t, f, clone)
|
||||||
|
clone.Mtime = time.Time{}
|
||||||
|
clone.Path = testCloneFile
|
||||||
|
assert.Nil(t, clone.Apply())
|
||||||
|
|
||||||
|
f.Read(ctx)
|
||||||
|
clone.Read(ctx)
|
||||||
|
|
||||||
|
fmt.Printf("file %#v\nclone %#v\n", f, clone)
|
||||||
|
assert.NotEqual(t, f.Mtime, clone.Mtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileErrors(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
testFile := filepath.Join(TempDir, "testerr.txt")
|
||||||
|
|
||||||
|
f := NewFile()
|
||||||
|
assert.NotNil(t, f)
|
||||||
|
|
||||||
|
f.Path = testFile
|
||||||
|
f.Mode = "631"
|
||||||
|
f.State = "present"
|
||||||
|
assert.Nil(t, f.Apply())
|
||||||
|
|
||||||
|
read := NewFile()
|
||||||
|
read.Path = testFile
|
||||||
|
read.Read(ctx)
|
||||||
|
assert.Equal(t, "0631", read.Mode)
|
||||||
|
|
||||||
|
f.Mode = "900"
|
||||||
|
assert.ErrorAs(t, f.Apply(), &ErrInvalidFileMode, "Apply should fail with NumError when converting invalid octal")
|
||||||
|
|
||||||
|
read.Read(ctx)
|
||||||
|
assert.Equal(t, "0631", read.Mode)
|
||||||
|
|
||||||
|
f.Mode = "0631"
|
||||||
|
f.Owner = "bar"
|
||||||
|
uidErr := f.Apply()
|
||||||
|
var UnknownUser user.UnknownUserError
|
||||||
|
assert.Error(t, uidErr, UnknownUser)
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -14,6 +14,7 @@ _ "os"
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -38,7 +39,7 @@ type HTTP struct {
|
|||||||
Endpoint string `yaml:"endpoint" json:"endpoint"`
|
Endpoint string `yaml:"endpoint" json:"endpoint"`
|
||||||
Headers []HTTPHeader `yaml:"headers,omitempty" json:"headers,omitempty"`
|
Headers []HTTPHeader `yaml:"headers,omitempty" json:"headers,omitempty"`
|
||||||
Body string `yaml:"body,omitempty" json:"body,omitempty"`
|
Body string `yaml:"body,omitempty" json:"body,omitempty"`
|
||||||
State string `yaml:"state" json:"state"`
|
State string `yaml:"state,omitempty" json:"state,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTP() *HTTP {
|
func NewHTTP() *HTTP {
|
||||||
@ -55,6 +56,10 @@ func (h *HTTP) Clone() Resource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) StateMachine() machine.Stater {
|
||||||
|
return StorageMachine()
|
||||||
|
}
|
||||||
|
|
||||||
func (h *HTTP) URI() string {
|
func (h *HTTP) URI() string {
|
||||||
return h.Endpoint
|
return h.Endpoint
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
_ "encoding/hex"
|
_ "encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
_ "errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"io"
|
"io"
|
||||||
@ -16,17 +16,26 @@ _ "os/exec"
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ResourceTypes.Register("iptable", func(u *url.URL) Resource {
|
ResourceTypes.Register("iptable", func(u *url.URL) Resource {
|
||||||
i := NewIptable()
|
i := NewIptable()
|
||||||
i.Table = IptableName(u.Hostname())
|
i.Table = IptableName(u.Hostname())
|
||||||
|
if len(u.Path) > 0 {
|
||||||
fields := strings.Split(u.Path, "/")
|
fields := strings.Split(u.Path, "/")
|
||||||
slog.Info("iptables factory", "iptable", i, "uri", u, "field", fields)
|
slog.Info("iptables factory", "iptable", i, "uri", u, "fields", fields, "number_fields", len(fields))
|
||||||
i.Chain = IptableChain(fields[1])
|
i.Chain = IptableChain(fields[1])
|
||||||
|
if len(fields) < 3 {
|
||||||
|
i.ResourceType = IptableTypeChain
|
||||||
|
} else {
|
||||||
|
i.ResourceType = IptableTypeRule
|
||||||
id, _ := strconv.ParseUint(fields[2], 10, 32)
|
id, _ := strconv.ParseUint(fields[2], 10, 32)
|
||||||
i.Id = uint(id)
|
i.Id = uint(id)
|
||||||
|
}
|
||||||
|
i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD()
|
||||||
|
}
|
||||||
return i
|
return i
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -84,10 +93,17 @@ type ExtensionFlag struct {
|
|||||||
|
|
||||||
type IptablePort uint16
|
type IptablePort uint16
|
||||||
|
|
||||||
|
type IptableType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
IptableTypeRule = "rule"
|
||||||
|
IptableTypeChain = "chain"
|
||||||
|
)
|
||||||
|
|
||||||
// Manage the state of iptables rules
|
// Manage the state of iptables rules
|
||||||
// iptable://filter/INPUT/0
|
// iptable://filter/INPUT/0
|
||||||
type Iptable struct {
|
type Iptable struct {
|
||||||
Id uint `json:"id" yaml:"id"`
|
Id uint `json:"id,omitempty" yaml:"id,omitempty"`
|
||||||
Table IptableName `json:"table" yaml:"table"`
|
Table IptableName `json:"table" yaml:"table"`
|
||||||
Chain IptableChain `json:"chain" yaml:"chain"`
|
Chain IptableChain `json:"chain" yaml:"chain"`
|
||||||
Destination IptableCIDR `json:"destination,omitempty" yaml:"destination,omitempty"`
|
Destination IptableCIDR `json:"destination,omitempty" yaml:"destination,omitempty"`
|
||||||
@ -99,9 +115,11 @@ type Iptable struct {
|
|||||||
Match []string `json:"match,omitempty" yaml:"match,omitempty"`
|
Match []string `json:"match,omitempty" yaml:"match,omitempty"`
|
||||||
Flags []ExtensionFlag `json:"extension_flags,omitempty" yaml:"extension_flags,omitempty"`
|
Flags []ExtensionFlag `json:"extension_flags,omitempty" yaml:"extension_flags,omitempty"`
|
||||||
Proto IptableProto `json:"proto,omitempty" yaml:"proto,omitempty"`
|
Proto IptableProto `json:"proto,omitempty" yaml:"proto,omitempty"`
|
||||||
Jump string `json:"jump" yaml:"jump"`
|
Jump string `json:"jump,omitempty" yaml:"jump,omitempty"`
|
||||||
State string `json:"state" yaml:"state"`
|
State string `json:"state" yaml:"state"`
|
||||||
|
ChainLength uint `json:"-" yaml:"-"`
|
||||||
|
|
||||||
|
ResourceType IptableType `json:"resourcetype,omitempty" yaml:"resourcetype,omitempty"`
|
||||||
CreateCommand *Command `yaml:"-" json:"-"`
|
CreateCommand *Command `yaml:"-" json:"-"`
|
||||||
ReadCommand *Command `yaml:"-" json:"-"`
|
ReadCommand *Command `yaml:"-" json:"-"`
|
||||||
UpdateCommand *Command `yaml:"-" json:"-"`
|
UpdateCommand *Command `yaml:"-" json:"-"`
|
||||||
@ -109,13 +127,13 @@ type Iptable struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewIptable() *Iptable {
|
func NewIptable() *Iptable {
|
||||||
i := &Iptable{}
|
i := &Iptable{ ResourceType: IptableTypeRule }
|
||||||
i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.NewCRUD()
|
i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD()
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Iptable) Clone() Resource {
|
func (i *Iptable) Clone() Resource {
|
||||||
return &Iptable {
|
newIpt := &Iptable {
|
||||||
Id: i.Id,
|
Id: i.Id,
|
||||||
Table: i.Table,
|
Table: i.Table,
|
||||||
Chain: i.Chain,
|
Chain: i.Chain,
|
||||||
@ -125,8 +143,15 @@ func (i *Iptable) Clone() Resource {
|
|||||||
Out: i.Out,
|
Out: i.Out,
|
||||||
Match: i.Match,
|
Match: i.Match,
|
||||||
Proto: i.Proto,
|
Proto: i.Proto,
|
||||||
|
ResourceType: i.ResourceType,
|
||||||
State: i.State,
|
State: i.State,
|
||||||
}
|
}
|
||||||
|
newIpt.CreateCommand, newIpt.ReadCommand, newIpt.UpdateCommand, newIpt.DeleteCommand = newIpt.ResourceType.NewCRUD()
|
||||||
|
return newIpt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iptable) StateMachine() machine.Stater {
|
||||||
|
return StorageMachine()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Iptable) URI() string {
|
func (i *Iptable) URI() string {
|
||||||
@ -140,8 +165,13 @@ func (i *Iptable) SetURI(uri string) error {
|
|||||||
i.Table = IptableName(resourceUri.Hostname())
|
i.Table = IptableName(resourceUri.Hostname())
|
||||||
fields := strings.Split(resourceUri.Path, "/")
|
fields := strings.Split(resourceUri.Path, "/")
|
||||||
i.Chain = IptableChain(fields[1])
|
i.Chain = IptableChain(fields[1])
|
||||||
|
if len(fields) < 3 {
|
||||||
|
i.ResourceType = IptableTypeChain
|
||||||
|
} else {
|
||||||
|
i.ResourceType = IptableTypeRule
|
||||||
id, _ := strconv.ParseUint(fields[2], 10, 32)
|
id, _ := strconv.ParseUint(fields[2], 10, 32)
|
||||||
i.Id = uint(id)
|
i.Id = uint(id)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
e = fmt.Errorf("%w: %s is not an iptable rule", ErrInvalidResourceURI, uri)
|
e = fmt.Errorf("%w: %s is not an iptable rule", ErrInvalidResourceURI, uri)
|
||||||
}
|
}
|
||||||
@ -166,7 +196,7 @@ func (i *Iptable) UnmarshalJSON(data []byte) error {
|
|||||||
if unmarshalErr := json.Unmarshal(data, i); unmarshalErr != nil {
|
if unmarshalErr := json.Unmarshal(data, i); unmarshalErr != nil {
|
||||||
return unmarshalErr
|
return unmarshalErr
|
||||||
}
|
}
|
||||||
i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.NewCRUD()
|
i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +205,7 @@ func (i *Iptable) UnmarshalYAML(value *yaml.Node) error {
|
|||||||
if unmarshalErr := value.Decode((*decodeIptable)(i)); unmarshalErr != nil {
|
if unmarshalErr := value.Decode((*decodeIptable)(i)); unmarshalErr != nil {
|
||||||
return unmarshalErr
|
return unmarshalErr
|
||||||
}
|
}
|
||||||
i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.NewCRUD()
|
i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,12 +214,17 @@ func (i *Iptable) NewCRUD() (create *Command, read *Command, update *Command, de
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *Iptable) Apply() error {
|
func (i *Iptable) Apply() error {
|
||||||
|
ctx := context.Background()
|
||||||
switch i.State {
|
switch i.State {
|
||||||
case "absent":
|
case "absent":
|
||||||
case "present":
|
case "present":
|
||||||
|
err := i.Create(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
}
|
||||||
|
_,e := i.Read(context.Background())
|
||||||
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Iptable) Load(r io.Reader) error {
|
func (i *Iptable) Load(r io.Reader) error {
|
||||||
@ -208,6 +243,177 @@ func (i *Iptable) ResolveId(ctx context.Context) string {
|
|||||||
return fmt.Sprintf("%d", i.Id)
|
return fmt.Sprintf("%d", i.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Iptable) SetFlagValue(opt, value string) bool {
|
||||||
|
switch opt {
|
||||||
|
case "-i":
|
||||||
|
i.In = value
|
||||||
|
return true
|
||||||
|
case "-o":
|
||||||
|
i.Out = value
|
||||||
|
return true
|
||||||
|
case "-m":
|
||||||
|
for _,search := range i.Match {
|
||||||
|
if search == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i.Match = append(i.Match, value)
|
||||||
|
return true
|
||||||
|
case "-s":
|
||||||
|
i.Source = IptableCIDR(value)
|
||||||
|
return true
|
||||||
|
case "-d":
|
||||||
|
i.Destination = IptableCIDR(value)
|
||||||
|
return true
|
||||||
|
case "-p":
|
||||||
|
i.Proto = IptableProto(value)
|
||||||
|
return true
|
||||||
|
case "-j":
|
||||||
|
i.Jump = value
|
||||||
|
return true
|
||||||
|
case "--dport":
|
||||||
|
port,_ := strconv.ParseUint(value, 10, 16)
|
||||||
|
i.Dport = IptablePort(port)
|
||||||
|
return true
|
||||||
|
case "--sport":
|
||||||
|
port,_ := strconv.ParseUint(value, 10, 16)
|
||||||
|
i.Sport = IptablePort(port)
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
if opt[0] == '-' {
|
||||||
|
i.Flags = append(i.Flags, ExtensionFlag{ Name: strings.Trim(opt, "-"), Value: strings.TrimSpace(value)})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iptable) GetFlagValue(opt string) any {
|
||||||
|
switch opt {
|
||||||
|
case "-i":
|
||||||
|
return i.In
|
||||||
|
case "-o":
|
||||||
|
return i.Out
|
||||||
|
case "-m":
|
||||||
|
return i.Match
|
||||||
|
case "-s":
|
||||||
|
return i.Source
|
||||||
|
case "-d":
|
||||||
|
return i.Destination
|
||||||
|
case "-p":
|
||||||
|
return i.Proto
|
||||||
|
case "-j":
|
||||||
|
return i.Jump
|
||||||
|
case "--dport":
|
||||||
|
return strconv.Itoa(int(i.Dport))
|
||||||
|
case "--sport":
|
||||||
|
return strconv.Itoa(int(i.Sport))
|
||||||
|
default:
|
||||||
|
if opt[0] == '-' {
|
||||||
|
return i.Flags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iptable) SetRule(flags []string) (assigned bool) {
|
||||||
|
assigned = true
|
||||||
|
for index, flag := range flags {
|
||||||
|
if flag[0] == '-' {
|
||||||
|
flag := flags[index]
|
||||||
|
value := flags[index + 1]
|
||||||
|
if value[0] != '-' {
|
||||||
|
if ! i.SetFlagValue(flag, value) {
|
||||||
|
assigned = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iptable) MatchRule(flags []string) (match bool) {
|
||||||
|
match = true
|
||||||
|
for index, flag := range flags {
|
||||||
|
if flag[0] == '-' {
|
||||||
|
value := flags[index + 1]
|
||||||
|
switch v := i.GetFlagValue(flag).(type) {
|
||||||
|
case []string:
|
||||||
|
for _,element := range v {
|
||||||
|
if element == value {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match = false
|
||||||
|
case []ExtensionFlag:
|
||||||
|
for _,element := range v {
|
||||||
|
if element.Name == flag && element.Value == value {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match = false
|
||||||
|
case IptableCIDR:
|
||||||
|
if v == IptableCIDR(value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
match = false
|
||||||
|
case IptableName:
|
||||||
|
if v == IptableName(value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
match = false
|
||||||
|
case IptableChain:
|
||||||
|
if v == IptableChain(value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
match = false
|
||||||
|
default:
|
||||||
|
if v.(string) == value {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
match = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iptable) ReadChainLength() error {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Path = "iptables"
|
||||||
|
c.Args = []CommandArg{
|
||||||
|
CommandArg("-S"),
|
||||||
|
CommandArg("{{ .Chain }}"),
|
||||||
|
}
|
||||||
|
output,err := c.Execute(i)
|
||||||
|
if err == nil {
|
||||||
|
linesCount := strings.Count(string(output), "\n")
|
||||||
|
if linesCount > 0 {
|
||||||
|
i.ChainLength = uint(linesCount) - 1
|
||||||
|
} else {
|
||||||
|
i.ChainLength = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Iptable) Create(ctx context.Context) error {
|
||||||
|
if i.Id > 0 {
|
||||||
|
if lenErr := i.ReadChainLength(); lenErr != nil {
|
||||||
|
return lenErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := i.CreateCommand.Execute(i)
|
||||||
|
//slog.Info("IptableChain Create()", "err", err, "errstr", err.Error(), "iptable", i, "createcommand", i.CreateCommand)
|
||||||
|
// TODO add Command status/error handler rather than using the read extractor
|
||||||
|
if i.CreateCommand.Extractor != nil {
|
||||||
|
if err != nil {
|
||||||
|
return i.CreateCommand.Extractor([]byte(err.Error()), i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Iptable) Read(ctx context.Context) ([]byte, error) {
|
func (i *Iptable) Read(ctx context.Context) ([]byte, error) {
|
||||||
out, err := i.ReadCommand.Execute(i)
|
out, err := i.ReadCommand.Execute(i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -222,24 +428,93 @@ func (i *Iptable) Read(ctx context.Context) ([]byte, error) {
|
|||||||
|
|
||||||
func (i *Iptable) Type() string { return "iptable" }
|
func (i *Iptable) Type() string { return "iptable" }
|
||||||
|
|
||||||
|
func (i *IptableType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) {
|
||||||
|
switch *i {
|
||||||
|
case IptableTypeRule:
|
||||||
|
return NewIptableCreateCommand(), NewIptableReadCommand(), NewIptableUpdateCommand(), NewIptableDeleteCommand()
|
||||||
|
case IptableTypeChain:
|
||||||
|
return NewIptableChainCreateCommand(), NewIptableChainReadCommand(), NewIptableChainUpdateCommand(), NewIptableChainDeleteCommand()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return nil, nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IptableType) UnmarshalValue(value string) error {
|
||||||
|
switch value {
|
||||||
|
case string(IptableTypeRule), string(IptableTypeChain):
|
||||||
|
*i = IptableType(value)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errors.New("invalid IptableType value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IptableType) UnmarshalJSON(data []byte) error {
|
||||||
|
var s string
|
||||||
|
if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
|
||||||
|
return unmarshalRouteTypeErr
|
||||||
|
}
|
||||||
|
return i.UnmarshalValue(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IptableType) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
var s string
|
||||||
|
if err := value.Decode(&s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return i.UnmarshalValue(s)
|
||||||
|
}
|
||||||
|
|
||||||
func NewIptableCreateCommand() *Command {
|
func NewIptableCreateCommand() *Command {
|
||||||
c := NewCommand()
|
c := NewCommand()
|
||||||
c.Path = "iptables"
|
c.Path = "iptables"
|
||||||
c.Args = []CommandArg{
|
c.Args = []CommandArg{
|
||||||
CommandArg("-t"),
|
CommandArg("-t"),
|
||||||
CommandArg("{{ .Table }}"),
|
CommandArg("{{ .Table }}"),
|
||||||
CommandArg("-R"),
|
CommandArg("{{ if le .Id .ChainLength }}-R{{ else }}-A{{ end }}"),
|
||||||
CommandArg("{{ .Chain }}"),
|
CommandArg("{{ .Chain }}"),
|
||||||
CommandArg("{{ .Id }}"),
|
CommandArg("{{ if le .Id .ChainLength }}{{ .Id }}{{ end }}"),
|
||||||
CommandArg("{{ if .In }}-i {{ .In }}{{ else if .Out }}-o {{ .Out }}{{ end }}"),
|
CommandArg("{{ if .In }}-i {{ .In }}{{ else if .Out }}-o {{ .Out }}{{ end }}"),
|
||||||
CommandArg("{{ range .Match }}-m {{ . }} {{ end }}"),
|
CommandArg("{{ range .Match }}-m {{ . }} {{- end }}"),
|
||||||
CommandArg("{{ if .Source }}-s {{ .Source }}{{ end }}"),
|
CommandArg("{{ if .Source }}-s {{ .Source }}{{ end }}"),
|
||||||
|
CommandArg("{{ if .Sport }}--sport {{ .Sport }}{{ end }}"),
|
||||||
CommandArg("{{ if .Destination }}-d {{ .Destination }}{{ end }}"),
|
CommandArg("{{ if .Destination }}-d {{ .Destination }}{{ end }}"),
|
||||||
|
CommandArg("{{ if .Dport }}--dport {{ .Dport }}{{ end }}"),
|
||||||
|
CommandArg("{{ if .Proto }}-p {{ .Proto }}{{ end }}"),
|
||||||
|
CommandArg("{{ range .Flags }} --{{ .Name }} {{ .Value }} {{ end }}"),
|
||||||
CommandArg("{{ if .Jump }}-j {{ .Jump }}{{ end }}"),
|
CommandArg("{{ if .Jump }}-j {{ .Jump }}{{ end }}"),
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IptableExtractRule(lineNumber uint, ruleLine string, target *Iptable) (state string, err error) {
|
||||||
|
state = "absent"
|
||||||
|
ruleFields := strings.Split(strings.TrimSpace(ruleLine), " ")
|
||||||
|
slog.Info("IptableExtractRule()", "lineNumber", lineNumber, "ruleLine", ruleLine, "target", target)
|
||||||
|
if ruleFields[0] == "-A" {
|
||||||
|
flags := ruleFields[2:]
|
||||||
|
if target.Id > 0 {
|
||||||
|
if target.Id == lineNumber {
|
||||||
|
slog.Info("IptableExtractRule() SetRule", "lineNumber", lineNumber, "flags", flags, "target", target)
|
||||||
|
if target.SetRule(flags) {
|
||||||
|
state = "present"
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if target.MatchRule(flags) {
|
||||||
|
target.Id = lineNumber
|
||||||
|
state = "present"
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("Invalid rule %d %s", lineNumber, ruleLine)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func NewIptableReadCommand() *Command {
|
func NewIptableReadCommand() *Command {
|
||||||
c := NewCommand()
|
c := NewCommand()
|
||||||
c.Path = "iptables"
|
c.Path = "iptables"
|
||||||
@ -248,60 +523,240 @@ func NewIptableReadCommand() *Command {
|
|||||||
CommandArg("{{ .Table }}"),
|
CommandArg("{{ .Table }}"),
|
||||||
CommandArg("-S"),
|
CommandArg("-S"),
|
||||||
CommandArg("{{ .Chain }}"),
|
CommandArg("{{ .Chain }}"),
|
||||||
CommandArg("{{ .Id }}"),
|
CommandArg("{{ if .Id }}{{ .Id }}{{ end }}"),
|
||||||
}
|
}
|
||||||
c.Extractor = func(out []byte, target any) error {
|
c.Extractor = func(out []byte, target any) error {
|
||||||
i := target.(*Iptable)
|
i := target.(*Iptable)
|
||||||
ruleFields := strings.Split(strings.TrimSpace(string(out)), " ")
|
if i.Id > 0 {
|
||||||
switch ruleFields[0] {
|
return RuleExtractor(out, target)
|
||||||
case "-A":
|
}
|
||||||
//chain := ruleFields[1]
|
|
||||||
flags := ruleFields[2:]
|
state := "absent"
|
||||||
for optind,opt := range flags {
|
var lineNumber uint = 1
|
||||||
if optind > len(flags) - 2 {
|
lines := strings.Split(string(out), "\n")
|
||||||
|
numberOfLines := len(lines)
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
matchState, err := IptableExtractRule(lineNumber, line, i)
|
||||||
|
if matchState == "present" {
|
||||||
|
state = matchState
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
optValue := flags[optind + 1]
|
if err == nil {
|
||||||
switch opt {
|
lineNumber++
|
||||||
case "-i":
|
|
||||||
i.In = optValue
|
|
||||||
case "-o":
|
|
||||||
i.Out = optValue
|
|
||||||
case "-m":
|
|
||||||
i.Match = append(i.Match, optValue)
|
|
||||||
case "-s":
|
|
||||||
i.Source = IptableCIDR(optValue)
|
|
||||||
case "-d":
|
|
||||||
i.Destination = IptableCIDR(optValue)
|
|
||||||
case "-p":
|
|
||||||
i.Proto = IptableProto(optValue)
|
|
||||||
case "-j":
|
|
||||||
i.Jump = optValue
|
|
||||||
case "--dport":
|
|
||||||
port,_ := strconv.ParseUint(optValue, 10, 16)
|
|
||||||
i.Dport = IptablePort(port)
|
|
||||||
case "--sport":
|
|
||||||
port,_ := strconv.ParseUint(optValue, 10, 16)
|
|
||||||
i.Sport = IptablePort(port)
|
|
||||||
default:
|
|
||||||
if opt[0] == '-' {
|
|
||||||
i.Flags = append(i.Flags, ExtensionFlag{ Name: strings.Trim(opt, "-"), Value: strings.TrimSpace(optValue)})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
i.State = state
|
||||||
|
if numberOfLines > 0 {
|
||||||
|
i.ChainLength = uint(numberOfLines) - 1
|
||||||
|
} else {
|
||||||
|
i.ChainLength = 0
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIptableReadChainCommand() *Command {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Path = "iptables"
|
||||||
|
c.Args = []CommandArg{
|
||||||
|
CommandArg("-t"),
|
||||||
|
CommandArg("{{ .Table }}"),
|
||||||
|
CommandArg("-S"),
|
||||||
|
CommandArg("{{ .Chain }}"),
|
||||||
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
IptableChainRules := target.(*[]*Iptable)
|
||||||
|
numberOfChainRules := len(*IptableChainRules)
|
||||||
|
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||||
|
numberOfLines := len(lines)
|
||||||
|
diff := (numberOfLines - 1) - numberOfChainRules
|
||||||
|
if diff > 0 {
|
||||||
|
for i := 0; i < diff; i++ {
|
||||||
|
*IptableChainRules = append(*IptableChainRules, NewIptable())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for lineIndex, line := range lines[1:] {
|
||||||
|
i := (*IptableChainRules)[lineIndex]
|
||||||
|
i.Id = uint(lineIndex + 1)
|
||||||
|
ruleFields := strings.Split(strings.TrimSpace(line), " ")
|
||||||
|
if ruleFields[0] == "-A" {
|
||||||
|
flags := ruleFields[2:]
|
||||||
|
if i.SetRule(flags) {
|
||||||
i.State = "present"
|
i.State = "present"
|
||||||
default:
|
} else {
|
||||||
i.State = "absent"
|
i.State = "absent"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIptableUpdateCommand() *Command {
|
func NewIptableUpdateCommand() *Command {
|
||||||
return nil
|
return NewIptableCreateCommand()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIptableDeleteCommand() *Command {
|
func NewIptableDeleteCommand() *Command {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewIptableChainCreateCommand() *Command {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Path = "iptables"
|
||||||
|
c.Args = []CommandArg{
|
||||||
|
CommandArg("-t"),
|
||||||
|
CommandArg("{{ .Table }}"),
|
||||||
|
CommandArg("-N"),
|
||||||
|
CommandArg("{{ .Chain }}"),
|
||||||
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
slog.Info("IptableChain Extractor", "output", out, "command", c)
|
||||||
|
for _,line := range strings.Split(string(out), "\n") {
|
||||||
|
if line == "iptables: Chain already exists." {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf(string(out))
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChainExtractor(out []byte, target any) error {
|
||||||
|
i := target.(*Iptable)
|
||||||
|
rules := strings.Split(string(out), "\n")
|
||||||
|
for _,rule := range rules {
|
||||||
|
ruleFields := strings.Split(strings.TrimSpace(string(rule)), " ")
|
||||||
|
switch ruleFields[0] {
|
||||||
|
case "-N", "-A":
|
||||||
|
chain := ruleFields[1]
|
||||||
|
if chain == string(i.Chain) {
|
||||||
|
i.State = "present"
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
i.State = "absent"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
i.State = "absent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RuleExtractor(out []byte, target any) (err error) {
|
||||||
|
ipt := target.(*Iptable)
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||||
|
err = fmt.Errorf("Failed to extract rule by Id: %d", ipt.Id)
|
||||||
|
ipt.State = "absent"
|
||||||
|
var lineIndex uint = 1
|
||||||
|
if uint(len(lines)) >= ipt.Id {
|
||||||
|
lineIndex = ipt.Id
|
||||||
|
} else if len(lines) > 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ruleFields := strings.Split(strings.TrimSpace(lines[lineIndex]), " ")
|
||||||
|
slog.Info("RuleExtractor()", "lines", lines, "line", lines[lineIndex], "fields", ruleFields, "index", lineIndex)
|
||||||
|
if ruleFields[0] == "-A" {
|
||||||
|
if ipt.SetRule(ruleFields[2:]) {
|
||||||
|
ipt.State = "present"
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func RuleExtractorMatchFlags(out []byte, target any) (err error) {
|
||||||
|
ipt := target.(*Iptable)
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||||
|
var linesCount uint = uint(len(lines))
|
||||||
|
err = fmt.Errorf("Failed to extract rule")
|
||||||
|
if linesCount > 0 {
|
||||||
|
ipt.ChainLength = linesCount - 1
|
||||||
|
ipt.State = "absent"
|
||||||
|
for linesIndex, line := range lines {
|
||||||
|
ruleFields := strings.Split(strings.TrimSpace(line), " ")
|
||||||
|
slog.Info("RuleExtractorMatchFlags()", "lines", lines, "line", line, "fields", ruleFields, "index", linesIndex)
|
||||||
|
if ruleFields[0] == "-A" {
|
||||||
|
flags := ruleFields[2:]
|
||||||
|
if ipt.MatchRule(flags) {
|
||||||
|
slog.Info("RuleExtractorMatchFlags()", "flags", flags, "ipt", ipt)
|
||||||
|
err = nil
|
||||||
|
ipt.State = "present"
|
||||||
|
ipt.Id = uint(linesIndex)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func RuleExtractorById(out []byte, target any) (err error) {
|
||||||
|
ipt := target.(*Iptable)
|
||||||
|
state := "absent"
|
||||||
|
lines := strings.Split(string(out), "\n")
|
||||||
|
err = fmt.Errorf("Failed to extract rule by Id: %d", ipt.Id)
|
||||||
|
ipt.ChainLength = 0
|
||||||
|
for _, line := range lines {
|
||||||
|
ruleFields := strings.Split(strings.TrimSpace(line), " ")
|
||||||
|
if ruleFields[0] == "-A" {
|
||||||
|
ipt.ChainLength++
|
||||||
|
flags := ruleFields[2:]
|
||||||
|
slog.Info("RuleExtractorById()", "target", ipt)
|
||||||
|
if ipt.Id == ipt.ChainLength {
|
||||||
|
if ipt.SetRule(flags) {
|
||||||
|
slog.Info("RuleExtractorById() SetRule", "flags", flags, "target", ipt)
|
||||||
|
state = "present"
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ipt.State = state
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIptableChainReadCommand() *Command {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Path = "iptables"
|
||||||
|
c.Args = []CommandArg{
|
||||||
|
CommandArg("-t"),
|
||||||
|
CommandArg("{{ .Table }}"),
|
||||||
|
CommandArg("-S"),
|
||||||
|
CommandArg("{{ .Chain }}"),
|
||||||
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
i := target.(*Iptable)
|
||||||
|
rules := strings.Split(string(out), "\n")
|
||||||
|
for _,rule := range rules {
|
||||||
|
ruleFields := strings.Split(strings.TrimSpace(string(rule)), " ")
|
||||||
|
slog.Info("IptableChain Extract()", "fields", ruleFields)
|
||||||
|
switch ruleFields[0] {
|
||||||
|
case "-N", "-A":
|
||||||
|
chain := ruleFields[1]
|
||||||
|
if chain == string(i.Chain) {
|
||||||
|
i.State = "present"
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
i.State = "absent"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
i.State = "absent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIptableChainUpdateCommand() *Command {
|
||||||
|
return NewIptableChainCreateCommand()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIptableChainDeleteCommand() *Command {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -74,3 +74,61 @@ func TestReadIptable(t *testing.T) {
|
|||||||
assert.NotNil(t, r)
|
assert.NotNil(t, r)
|
||||||
assert.Equal(t, "eth0", testRule.In)
|
assert.Equal(t, "eth0", testRule.In)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateIptable(t *testing.T) {
|
||||||
|
testRule := NewIptable()
|
||||||
|
assert.NotNil(t, testRule)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIptableSetFlagValue(t *testing.T) {
|
||||||
|
i := NewIptable()
|
||||||
|
assert.NotNil(t, i)
|
||||||
|
i.SetFlagValue("-i", "eth0")
|
||||||
|
assert.Equal(t, "eth0", i.In)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func TestIptableChainExtractor(t *testing.T) {
|
||||||
|
ipt := NewIptable()
|
||||||
|
assert.NotNil(t, ipt)
|
||||||
|
ipt.Chain = IptableChain("FOO")
|
||||||
|
assert.Nil(t, ChainExtractor([]byte("-N FOO\n"), ipt))
|
||||||
|
assert.Equal(t, IptableChain("FOO"), ipt.Chain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIptableRuleExtractorById(t *testing.T) {
|
||||||
|
ipt := NewIptable()
|
||||||
|
assert.NotNil(t, ipt)
|
||||||
|
ipt.Table = IptableName("filter")
|
||||||
|
ipt.Chain = IptableChain("FOO")
|
||||||
|
ipt.Id = 1
|
||||||
|
|
||||||
|
data := []byte(`
|
||||||
|
-N FOO
|
||||||
|
-A FOO -s 192.168.0.1/32 -j ACCEPT
|
||||||
|
`)
|
||||||
|
assert.Nil(t, RuleExtractor(data, ipt))
|
||||||
|
assert.Equal(t, IptableChain("FOO"), ipt.Chain)
|
||||||
|
assert.Equal(t, IptableCIDR("192.168.0.1/32"), ipt.Source)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIptableRuleExtractorByFlags(t *testing.T) {
|
||||||
|
ipt := NewIptable()
|
||||||
|
assert.NotNil(t, ipt)
|
||||||
|
ipt.Table = IptableName("filter")
|
||||||
|
ipt.Chain = IptableChain("FOO")
|
||||||
|
ipt.Source = IptableCIDR("192.168.0.1/32")
|
||||||
|
ipt.Jump = "ACCEPT"
|
||||||
|
data := []byte(`
|
||||||
|
-N FOO
|
||||||
|
-A FOO -d 192.168.0.3/32 -j ACCEPT
|
||||||
|
-A FOO -s 192.168.0.3/32 -j ACCEPT
|
||||||
|
-A FOO -s 192.168.0.1/32 -j ACCEPT
|
||||||
|
`)
|
||||||
|
assert.Nil(t, RuleExtractorMatchFlags(data, ipt))
|
||||||
|
assert.Equal(t, uint(3), ipt.Id, ipt.Chain)
|
||||||
|
assert.Equal(t, IptableChain("FOO"), ipt.Chain)
|
||||||
|
assert.Equal(t, IptableCIDR("192.168.0.1/32"), ipt.Source)
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ package resource
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
_ "gopkg.in/yaml.v3"
|
_ "gopkg.in/yaml.v3"
|
||||||
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewFooResource() *MockResource {
|
func NewFooResource() *MockResource {
|
||||||
@ -14,5 +15,6 @@ func NewFooResource() *MockResource {
|
|||||||
InjectRead: func(ctx context.Context) ([]byte, error) { return nil,nil },
|
InjectRead: func(ctx context.Context) ([]byte, error) { return nil,nil },
|
||||||
InjectLoadDecl: func(string) error { return nil },
|
InjectLoadDecl: func(string) error { return nil },
|
||||||
InjectApply: func() error { return nil },
|
InjectApply: func() error { return nil },
|
||||||
|
InjectStateMachine: func() machine.Stater { return nil },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
_ "gopkg.in/yaml.v3"
|
_ "gopkg.in/yaml.v3"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
_ "fmt"
|
_ "fmt"
|
||||||
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MockResource struct {
|
type MockResource struct {
|
||||||
@ -17,12 +18,17 @@ type MockResource struct {
|
|||||||
InjectValidate func() error
|
InjectValidate func() error
|
||||||
InjectApply func() error
|
InjectApply func() error
|
||||||
InjectRead func(context.Context) ([]byte, error)
|
InjectRead func(context.Context) ([]byte, error)
|
||||||
|
InjectStateMachine func() machine.Stater
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockResource) Clone() Resource {
|
func (m *MockResource) Clone() Resource {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockResource) StateMachine() machine.Stater {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MockResource) SetURI(uri string) error {
|
func (m *MockResource) SetURI(uri string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
_ "strconv"
|
_ "strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -138,6 +139,10 @@ func (n *NetworkRoute) Clone() Resource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NetworkRoute) StateMachine() machine.Stater {
|
||||||
|
return StorageMachine()
|
||||||
|
}
|
||||||
|
|
||||||
func (n *NetworkRoute) URI() string {
|
func (n *NetworkRoute) URI() string {
|
||||||
return fmt.Sprintf("route://%s", n.Id)
|
return fmt.Sprintf("route://%s", n.Id)
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"os/user"
|
"os/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"log/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
var MatchId *regexp.Regexp = regexp.MustCompile(`^[0-9]+$`)
|
var MatchId *regexp.Regexp = regexp.MustCompile(`^[0-9]+$`)
|
||||||
@ -19,29 +20,36 @@ func LookupUIDString(userName string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LookupUID(userName string) (int, error) {
|
func LookupUID(userName string) (int, error) {
|
||||||
|
var userLookupErr error
|
||||||
var UID string
|
var UID string
|
||||||
if MatchId.MatchString(userName) {
|
if MatchId.MatchString(userName) {
|
||||||
user, userLookupErr := user.LookupId(userName)
|
user, err := user.LookupId(userName)
|
||||||
if userLookupErr != nil {
|
slog.Info("LookupUID() numeric", "user", user, "userLookupErr", err)
|
||||||
//return -1, userLookupErr
|
if err != nil {
|
||||||
|
userLookupErr = err
|
||||||
UID = userName
|
UID = userName
|
||||||
} else {
|
} else {
|
||||||
UID = user.Uid
|
UID = user.Uid
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
user, userLookupErr := user.Lookup(userName)
|
if user, err := user.Lookup(userName); err != nil {
|
||||||
if userLookupErr != nil {
|
return -1, err
|
||||||
return -1, userLookupErr
|
} else {
|
||||||
}
|
|
||||||
UID = user.Uid
|
UID = user.Uid
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uid, uidErr := strconv.Atoi(UID)
|
uid, uidErr := strconv.Atoi(UID)
|
||||||
|
slog.Info("LookupUID()", "uid", uid, "uidErr", uidErr)
|
||||||
if uidErr != nil {
|
if uidErr != nil {
|
||||||
|
if userLookupErr != nil {
|
||||||
|
return -1, userLookupErr
|
||||||
|
} else {
|
||||||
return -1, uidErr
|
return -1, uidErr
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return uid, nil
|
return uid, userLookupErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func LookupGID(groupName string) (int, error) {
|
func LookupGID(groupName string) (int, error) {
|
||||||
|
@ -4,7 +4,6 @@ package resource
|
|||||||
import (
|
import (
|
||||||
_ "context"
|
_ "context"
|
||||||
_ "encoding/json"
|
_ "encoding/json"
|
||||||
_ "fmt"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
_ "io"
|
_ "io"
|
||||||
_ "net/http"
|
_ "net/http"
|
||||||
@ -20,9 +19,9 @@ func TestLookupUID(t *testing.T) {
|
|||||||
assert.Nil(t, e)
|
assert.Nil(t, e)
|
||||||
assert.Equal(t, 65534, uid)
|
assert.Equal(t, 65534, uid)
|
||||||
|
|
||||||
nuid, ne := LookupUID("1001")
|
nuid, ne := LookupUID("10101")
|
||||||
assert.Nil(t, ne)
|
assert.Error(t, ne, "user: unknonwn userid ", ne)
|
||||||
assert.Equal(t, 1001, nuid)
|
assert.Equal(t, 10101, nuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLookupGID(t *testing.T) {
|
func TestLookupGID(t *testing.T) {
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
_ "os/exec"
|
_ "os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PackageType string
|
type PackageType string
|
||||||
@ -94,6 +95,10 @@ func (p *Package) Clone() Resource {
|
|||||||
return newp
|
return newp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Package) StateMachine() machine.Stater {
|
||||||
|
return StorageMachine()
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Package) URI() string {
|
func (p *Package) URI() string {
|
||||||
return fmt.Sprintf("package://%s?version=%s&type=%s", p.Name, p.Version, p.PackageType)
|
return fmt.Sprintf("package://%s?version=%s&type=%s", p.Name, p.Version, p.PackageType)
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,14 @@ import (
|
|||||||
_ "fmt"
|
_ "fmt"
|
||||||
_ "gopkg.in/yaml.v3"
|
_ "gopkg.in/yaml.v3"
|
||||||
_ "net/url"
|
_ "net/url"
|
||||||
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResourceSelector func(r *Declaration) bool
|
type ResourceSelector func(r *Declaration) bool
|
||||||
|
|
||||||
type Resource interface {
|
type Resource interface {
|
||||||
Type() string
|
Type() string
|
||||||
|
StateMachine() machine.Stater
|
||||||
URI() string
|
URI() string
|
||||||
SetURI(string) error
|
SetURI(string) error
|
||||||
ResolveId(context.Context) string
|
ResolveId(context.Context) string
|
||||||
@ -55,14 +57,22 @@ func NewResource(uri string) Resource {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
func Machine() {
|
func StorageMachine() machine.Stater {
|
||||||
// start_destroy -> absent -> start_create -> present -> start_destroy
|
// start_destroy -> absent -> start_create -> present -> start_destroy
|
||||||
stater := machine.New("absent")
|
stater := machine.New("absent")
|
||||||
stater.AddStates("absent", "start_create", "present", "start_delete")
|
stater.AddStates("absent", "start_create", "present", "start_delete", "start_read", "start_update")
|
||||||
stater.AddTransition("creating", "absent", "start_create")
|
stater.AddTransition("create", "absent", "start_create")
|
||||||
stater.AddTransition("created", "start_create", "present")
|
stater.AddTransition("created", "start_create", "present")
|
||||||
stater.AddTransition("deleting", "present", "start_delete")
|
stater.AddTransition("read", "*", "start_read")
|
||||||
|
stater.AddTransition("state_read", "start_read", "present")
|
||||||
|
stater.AddTransition("update", "*", "start_update")
|
||||||
|
stater.AddTransition("updated", "start_update", "present")
|
||||||
|
stater.AddTransition("delete", "*", "start_delete")
|
||||||
stater.AddTransition("deleted", "start_delete", "absent")
|
stater.AddTransition("deleted", "start_delete", "absent")
|
||||||
|
return stater
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProcessMachine() machine.Stater {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
//go:embed schemas/*.jsonschema
|
//go:embed schemas/*.jsonschema
|
||||||
|
//go:embed schemas/*.schema.json
|
||||||
var schemaFiles embed.FS
|
var schemaFiles embed.FS
|
||||||
|
|
||||||
type Schema struct {
|
type Schema struct {
|
||||||
|
17
internal/resource/schemas/container-declaration.jsonschema
Normal file
17
internal/resource/schemas/container-declaration.jsonschema
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$id": "container-declaration.jsonschema",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "declaration",
|
||||||
|
"type": "object",
|
||||||
|
"required": [ "type", "attributes" ],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Resource type name.",
|
||||||
|
"enum": [ "container" ]
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"$ref": "container.jsonschema"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
internal/resource/schemas/container.jsonschema
Normal file
14
internal/resource/schemas/container.jsonschema
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$id": "container.jsonschema",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "container",
|
||||||
|
"description": "A docker container",
|
||||||
|
"type": "object",
|
||||||
|
"required": [ "name" ],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z]([-_a-z0-9]{0,31})$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$id": "container_network-declaration.jsonschema",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "declaration",
|
||||||
|
"type": "object",
|
||||||
|
"required": [ "type", "attributes" ],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Resource type name.",
|
||||||
|
"enum": [ "container_network" ]
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"$ref": "container_network.jsonschema"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
internal/resource/schemas/container_network.jsonschema
Normal file
14
internal/resource/schemas/container_network.jsonschema
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$id": "container_network.jsonschema",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "container_network",
|
||||||
|
"description": "A docker container network",
|
||||||
|
"type": "object",
|
||||||
|
"required": [ "name" ],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z]([-_a-z0-9]{0,31})$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,7 +16,9 @@
|
|||||||
{ "$ref": "user-declaration.jsonschema" },
|
{ "$ref": "user-declaration.jsonschema" },
|
||||||
{ "$ref": "exec-declaration.jsonschema" },
|
{ "$ref": "exec-declaration.jsonschema" },
|
||||||
{ "$ref": "network_route-declaration.jsonschema" },
|
{ "$ref": "network_route-declaration.jsonschema" },
|
||||||
{ "$ref": "iptable-declaration.jsonschema" }
|
{ "$ref": "iptable-declaration.jsonschema" },
|
||||||
|
{ "$ref": "container-declaration.jsonschema" },
|
||||||
|
{ "$ref": "container_network-declaration.jsonschema" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,9 @@
|
|||||||
"description": "Resource type name.",
|
"description": "Resource type name.",
|
||||||
"enum": [ "user" ]
|
"enum": [ "user" ]
|
||||||
},
|
},
|
||||||
|
"transition": {
|
||||||
|
"$ref": "storagetransition.schema.json"
|
||||||
|
},
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"$ref": "user.jsonschema"
|
"$ref": "user.jsonschema"
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,8 @@
|
|||||||
"pattern": "^[a-z]([-_a-z0-9]{0,31})$"
|
"pattern": "^[a-z]([-_a-z0-9]{0,31})$"
|
||||||
},
|
},
|
||||||
"uid": {
|
"uid": {
|
||||||
"type": "integer",
|
"type": "string",
|
||||||
"minimum": 0,
|
"pattern": "^[0-9]*$"
|
||||||
"maximum": 65535
|
|
||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
@ -6,44 +6,67 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"log/slog"
|
_ "log/slog"
|
||||||
"net/url"
|
"net/url"
|
||||||
_ "os"
|
_ "os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
|
)
|
||||||
|
|
||||||
|
type decodeUser User
|
||||||
|
|
||||||
|
type UserType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserTypeAddUser = "adduser"
|
||||||
|
UserTypeUserAdd = "useradd"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
Name string `json:"name" yaml:"name"`
|
Name string `json:"name" yaml:"name"`
|
||||||
UID int `json:"uid,omitempty" yaml:"uid,omitempty"`
|
UID string `json:"uid,omitempty" yaml:"uid,omitempty"`
|
||||||
Group string `json:"group,omitempty" yaml:"group,omitempty"`
|
Group string `json:"group,omitempty" yaml:"group,omitempty"`
|
||||||
Groups []string `json:"groups,omitempty" yaml:"groups,omitempty"`
|
Groups []string `json:"groups,omitempty" yaml:"groups,omitempty"`
|
||||||
Gecos string `json:"gecos,omitempty" yaml:"gecos,omitempty"`
|
Gecos string `json:"gecos,omitempty" yaml:"gecos,omitempty"`
|
||||||
Home string `json:"home" yaml:"home"`
|
Home string `json:"home" yaml:"home"`
|
||||||
CreateHome bool `json:"createhome,omitempty" yaml:"createhome,omitempty"`
|
CreateHome bool `json:"createhome,omitempty" yaml:"createhome,omitempty"`
|
||||||
Shell string `json:"shell,omitempty" yaml:"shell,omitempty"`
|
Shell string `json:"shell,omitempty" yaml:"shell,omitempty"`
|
||||||
|
UserType UserType `json:"-" yaml:"-"`
|
||||||
|
|
||||||
State string `json:"state" yaml:"state"`
|
CreateCommand *Command `json:"-" yaml:"-"`
|
||||||
|
ReadCommand *Command `json:"-" yaml:"-"`
|
||||||
|
UpdateCommand *Command `json:"-" yaml:"-"`
|
||||||
|
DeleteCommand *Command `json:"-" yaml:"-"`
|
||||||
|
State string `json:"state,omitempty" yaml:"state,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUser() *User {
|
func NewUser() *User {
|
||||||
return &User{}
|
return &User{ CreateHome: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ResourceTypes.Register("user", func(u *url.URL) Resource {
|
ResourceTypes.Register("user", func(u *url.URL) Resource {
|
||||||
user := NewUser()
|
user := NewUser()
|
||||||
user.Name = u.Path
|
user.Name = u.Hostname()
|
||||||
user.UID, _ = LookupUID(u.Path)
|
user.UID = LookupUIDString(u.Hostname())
|
||||||
|
if _, addUserPathErr := exec.LookPath("adduser"); addUserPathErr == nil {
|
||||||
|
user.UserType = UserTypeAddUser
|
||||||
|
}
|
||||||
|
if _, pathErr := exec.LookPath("useradd"); pathErr == nil {
|
||||||
|
user.UserType = UserTypeUserAdd
|
||||||
|
}
|
||||||
|
user.CreateCommand, user.ReadCommand, user.UpdateCommand, user.DeleteCommand = user.UserType.NewCRUD()
|
||||||
return user
|
return user
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) Clone() Resource {
|
func (u *User) Clone() Resource {
|
||||||
return &User {
|
newu := &User {
|
||||||
Name: u.Name,
|
Name: u.Name,
|
||||||
UID: u.UID,
|
UID: u.UID,
|
||||||
Group: u.Group,
|
Group: u.Group,
|
||||||
@ -53,7 +76,14 @@ func (u *User) Clone() Resource {
|
|||||||
CreateHome: u.CreateHome,
|
CreateHome: u.CreateHome,
|
||||||
Shell: u.Shell,
|
Shell: u.Shell,
|
||||||
State: u.State,
|
State: u.State,
|
||||||
|
UserType: u.UserType,
|
||||||
}
|
}
|
||||||
|
newu.CreateCommand, newu.ReadCommand, newu.UpdateCommand, newu.DeleteCommand = u.UserType.NewCRUD()
|
||||||
|
return newu
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) StateMachine() machine.Stater {
|
||||||
|
return StorageMachine()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) SetURI(uri string) error {
|
func (u *User) SetURI(uri string) error {
|
||||||
@ -85,43 +115,11 @@ func (u *User) Apply() error {
|
|||||||
case "present":
|
case "present":
|
||||||
_, NoUserExists := LookupUID(u.Name)
|
_, NoUserExists := LookupUID(u.Name)
|
||||||
if NoUserExists != nil {
|
if NoUserExists != nil {
|
||||||
var userCommandName string = "useradd"
|
cmdErr := u.Create(context.Background())
|
||||||
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"
|
|
||||||
if addUserCommandErr := u.AddUserCommand(&args); addUserCommandErr != nil {
|
|
||||||
return addUserCommandErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if userAddCommandErr := u.UserAddCommand(&args); userAddCommandErr != nil {
|
|
||||||
return userAddCommandErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
args = append(args, u.Name)
|
|
||||||
cmd := exec.Command(userCommandName, args...)
|
|
||||||
cmdOutput, cmdErr := cmd.CombinedOutput()
|
|
||||||
slog.Info("user command", "command", cmd.String(), "output", string(cmdOutput))
|
|
||||||
return cmdErr
|
return cmdErr
|
||||||
}
|
}
|
||||||
case "absent":
|
case "absent":
|
||||||
var userDelCommandName string = "userdel"
|
cmdErr := u.Delete()
|
||||||
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()
|
|
||||||
slog.Info("user command", "command", cmd.String(), "output", string(cmdOutput))
|
|
||||||
return cmdErr
|
return cmdErr
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -166,29 +164,205 @@ func (u *User) UserAddCommand(args *[]string) error {
|
|||||||
|
|
||||||
func (u *User) Type() string { return "user" }
|
func (u *User) Type() string { return "user" }
|
||||||
|
|
||||||
|
func (u *User) Create(ctx context.Context) (error) {
|
||||||
|
_, err := u.CreateCommand.Execute(u)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_,e := u.Read(ctx)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
func (u *User) Read(ctx context.Context) ([]byte, error) {
|
func (u *User) Read(ctx context.Context) ([]byte, error) {
|
||||||
|
exErr := u.ReadCommand.Extractor(nil, u)
|
||||||
|
if exErr != nil {
|
||||||
|
u.State = "absent"
|
||||||
|
}
|
||||||
|
if yaml, yamlErr := yaml.Marshal(u); yamlErr != nil {
|
||||||
|
return yaml, yamlErr
|
||||||
|
} else {
|
||||||
|
return yaml, exErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Delete() (error) {
|
||||||
|
_, err := u.DeleteCommand.Execute(u)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) UnmarshalJSON(data []byte) error {
|
||||||
|
if unmarshalErr := json.Unmarshal(data, (*decodeUser)(u)); unmarshalErr != nil {
|
||||||
|
return unmarshalErr
|
||||||
|
}
|
||||||
|
u.CreateCommand, u.ReadCommand, u.UpdateCommand, u.DeleteCommand = u.UserType.NewCRUD()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
if unmarshalErr := value.Decode((*decodeUser)(u)); unmarshalErr != nil {
|
||||||
|
return unmarshalErr
|
||||||
|
}
|
||||||
|
u.CreateCommand, u.ReadCommand, u.UpdateCommand, u.DeleteCommand = u.UserType.NewCRUD()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) {
|
||||||
|
switch *u {
|
||||||
|
case UserTypeUserAdd:
|
||||||
|
return NewUserAddCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewUserDelDeleteCommand()
|
||||||
|
case UserTypeAddUser:
|
||||||
|
return NewAddUserCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewDelUserDeleteCommand()
|
||||||
|
default:
|
||||||
|
if _, addUserPathErr := exec.LookPath("adduser"); addUserPathErr == nil {
|
||||||
|
*u = UserTypeAddUser
|
||||||
|
return NewAddUserCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewDelUserDeleteCommand()
|
||||||
|
}
|
||||||
|
if _, pathErr := exec.LookPath("useradd"); pathErr == nil {
|
||||||
|
*u = UserTypeUserAdd
|
||||||
|
return NewUserAddCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewUserDelDeleteCommand()
|
||||||
|
}
|
||||||
|
return NewUserAddCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewUserDelDeleteCommand()
|
||||||
|
}
|
||||||
|
return nil, nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserType) UnmarshalValue(value string) error {
|
||||||
|
switch value {
|
||||||
|
case string(UserTypeUserAdd), string(UserTypeAddUser):
|
||||||
|
*u = UserType(value)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errors.New("invalid UserType value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserType) UnmarshalJSON(data []byte) error {
|
||||||
|
var s string
|
||||||
|
if unmarshalUserTypeErr := json.Unmarshal(data, &s); unmarshalUserTypeErr != nil {
|
||||||
|
return unmarshalUserTypeErr
|
||||||
|
}
|
||||||
|
return u.UnmarshalValue(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UserType) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
var s string
|
||||||
|
if err := value.Decode(&s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return u.UnmarshalValue(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserAddCreateCommand() *Command {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Path = "useradd"
|
||||||
|
c.Args = []CommandArg{
|
||||||
|
CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
|
||||||
|
CommandArg("{{ if .Gecos }}-c {{ .Gecos }}{{ end }}"),
|
||||||
|
CommandArg("{{ if .Group }}-g {{ .Group }}{{ end }}"),
|
||||||
|
CommandArg("{{ if .Groups }}-G {{ range .Groups }}{{ . }},{{- end }}{{ end }}"),
|
||||||
|
CommandArg("{{ if .Home }}-d {{ .Home }}{{ end }}"),
|
||||||
|
CommandArg("{{ if .CreateHome }}-m{{ else }}-M{{ end }}"),
|
||||||
|
CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
return nil
|
||||||
|
for _,line := range strings.Split(string(out), "\n") {
|
||||||
|
if line == "iptables: Chain already exists." {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf(string(out))
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAddUserCreateCommand() *Command {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Path = "adduser"
|
||||||
|
c.Args = []CommandArg{
|
||||||
|
CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
|
||||||
|
CommandArg("{{ if .Gecos }}-g {{ .Gecos }}{{ end }}"),
|
||||||
|
CommandArg("{{ if .Group }}-G {{ .Group }}{{ end }}"),
|
||||||
|
CommandArg("{{ if .Home }}-h {{ .Home }}{{ end }}"),
|
||||||
|
CommandArg("{{ if not .CreateHome }}-H{{ end }}"),
|
||||||
|
CommandArg("-D"),
|
||||||
|
CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
return nil
|
||||||
|
for _,line := range strings.Split(string(out), "\n") {
|
||||||
|
if line == "iptables: Chain already exists." {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf(string(out))
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserReadCommand() *Command {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
u := target.(*User)
|
||||||
|
u.State = "absent"
|
||||||
var readUser *user.User
|
var readUser *user.User
|
||||||
var e error
|
var e error
|
||||||
if u.Name != "" {
|
if u.Name != "" {
|
||||||
readUser, e = user.Lookup(u.Name)
|
readUser, e = user.Lookup(u.Name)
|
||||||
|
} else {
|
||||||
|
if u.UID != "" {
|
||||||
|
readUser, e = user.LookupId(u.UID)
|
||||||
}
|
}
|
||||||
if u.UID >= 0 {
|
|
||||||
readUser, e = user.LookupId(strconv.Itoa(u.UID))
|
|
||||||
}
|
|
||||||
|
|
||||||
if e != nil {
|
|
||||||
panic(e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e == nil {
|
||||||
u.Name = readUser.Username
|
u.Name = readUser.Username
|
||||||
u.UID, _ = strconv.Atoi(readUser.Uid)
|
u.UID = readUser.Uid
|
||||||
|
u.Home = readUser.HomeDir
|
||||||
|
u.Gecos = readUser.Name
|
||||||
if readGroup, groupErr := user.LookupGroupId(readUser.Gid); groupErr == nil {
|
if readGroup, groupErr := user.LookupGroupId(readUser.Gid); groupErr == nil {
|
||||||
u.Group = readGroup.Name
|
u.Group = readGroup.Name
|
||||||
} else {
|
} else {
|
||||||
panic(groupErr)
|
return groupErr
|
||||||
|
}
|
||||||
|
if u.UID != "" {
|
||||||
|
u.State = "present"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
u.Home = readUser.HomeDir
|
|
||||||
u.Gecos = readUser.Name
|
|
||||||
|
|
||||||
return yaml.Marshal(u)
|
func NewUserUpdateCommand() *Command {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserDelDeleteCommand() *Command {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Path = "userdel"
|
||||||
|
c.Args = []CommandArg{
|
||||||
|
CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDelUserDeleteCommand() *Command {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Path = "deluser"
|
||||||
|
c.Args = []CommandArg{
|
||||||
|
CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
package resource
|
package resource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "context"
|
"context"
|
||||||
_ "encoding/json"
|
_ "encoding/json"
|
||||||
_ "fmt"
|
"fmt"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
_ "io"
|
_ "io"
|
||||||
_ "net/http"
|
_ "net/http"
|
||||||
@ -20,7 +20,28 @@ func TestNewUserResource(t *testing.T) {
|
|||||||
assert.NotEqual(t, nil, u)
|
assert.NotEqual(t, nil, u)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReadUser(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
decl := `
|
||||||
|
name: "nobody"
|
||||||
|
`
|
||||||
|
|
||||||
|
u := NewUser()
|
||||||
|
e := u.LoadDecl(decl)
|
||||||
|
assert.Nil(t, e)
|
||||||
|
assert.Equal(t, "nobody", u.Name)
|
||||||
|
|
||||||
|
fmt.Printf("%#v\n", u)
|
||||||
|
_, readErr := u.Read(ctx)
|
||||||
|
assert.Nil(t, readErr)
|
||||||
|
|
||||||
|
fmt.Printf("%#v\n", u)
|
||||||
|
assert.Equal(t, "65534", u.UID)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateUser(t *testing.T) {
|
func TestCreateUser(t *testing.T) {
|
||||||
|
|
||||||
decl := `
|
decl := `
|
||||||
name: "testuser"
|
name: "testuser"
|
||||||
uid: 12001
|
uid: 12001
|
||||||
@ -28,16 +49,28 @@ func TestCreateUser(t *testing.T) {
|
|||||||
home: "/home/testuser"
|
home: "/home/testuser"
|
||||||
state: present
|
state: present
|
||||||
`
|
`
|
||||||
|
|
||||||
u := NewUser()
|
u := NewUser()
|
||||||
e := u.LoadDecl(decl)
|
e := u.LoadDecl(decl)
|
||||||
assert.Equal(t, nil, e)
|
assert.Equal(t, nil, e)
|
||||||
assert.Equal(t, "testuser", u.Name)
|
assert.Equal(t, "testuser", u.Name)
|
||||||
|
|
||||||
|
u.CreateCommand.Executor = func(value any) ([]byte, error) {
|
||||||
|
return []byte(``), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u.ReadCommand.Extractor = func(out []byte, target any) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u.DeleteCommand.Executor = func(value any) ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
applyErr := u.Apply()
|
applyErr := u.Apply()
|
||||||
assert.Equal(t, nil, applyErr)
|
assert.Nil(t, applyErr)
|
||||||
uid, uidErr := LookupUID(u.Name)
|
|
||||||
assert.Equal(t, nil, uidErr)
|
assert.Equal(t, "12001", u.UID)
|
||||||
assert.Equal(t, 12001, uid)
|
|
||||||
|
|
||||||
u.State = "absent"
|
u.State = "absent"
|
||||||
|
|
||||||
|
69
internal/source/iptable.go
Normal file
69
internal/source/iptable.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "context"
|
||||||
|
_ "encoding/json"
|
||||||
|
_ "fmt"
|
||||||
|
_ "gopkg.in/yaml.v3"
|
||||||
|
"net/url"
|
||||||
|
_ "path/filepath"
|
||||||
|
"decl/internal/resource"
|
||||||
|
_ "os"
|
||||||
|
_ "io"
|
||||||
|
"strings"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Iptable struct {
|
||||||
|
Table string `yaml:"table" json:"table"`
|
||||||
|
Chain string `yaml:"chain" json:"chain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIptable() *Iptable {
|
||||||
|
return &Iptable{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SourceTypes.Register([]string{"iptable"}, func(u *url.URL) DocSource {
|
||||||
|
t := NewIptable()
|
||||||
|
t.Table = u.Hostname()
|
||||||
|
t.Chain = strings.Split(u.RequestURI(), "/")[1]
|
||||||
|
slog.Info("iptable chain source factory", "table", t, "uri", u, "table", u.Hostname())
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (i *Iptable) Type() string { return "iptable" }
|
||||||
|
|
||||||
|
func (i *Iptable) ExtractResources(filter ResourceSelector) ([]*resource.Document, error) {
|
||||||
|
documents := make([]*resource.Document, 0, 100)
|
||||||
|
|
||||||
|
slog.Info("iptable chain source ExtractResources()", "table", i)
|
||||||
|
iptRules := make([]*resource.Iptable, 0, 100)
|
||||||
|
cmd := resource.NewIptableReadChainCommand()
|
||||||
|
if out, err := cmd.Execute(i); err == nil {
|
||||||
|
slog.Info("iptable chain source ExtractResources()", "output", out)
|
||||||
|
if exErr := cmd.Extractor(out, &iptRules); exErr != nil {
|
||||||
|
return documents, exErr
|
||||||
|
}
|
||||||
|
for _, rule := range iptRules {
|
||||||
|
document := resource.NewDocument()
|
||||||
|
if rule == nil {
|
||||||
|
rule = resource.NewIptable()
|
||||||
|
}
|
||||||
|
rule.Table = resource.IptableName(i.Table)
|
||||||
|
rule.Chain = resource.IptableChain(i.Chain)
|
||||||
|
|
||||||
|
document.AddResourceDeclaration("iptable", rule)
|
||||||
|
documents = append(documents, document)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
slog.Info("iptable chain source ExtractResources()", "output", out, "error", err)
|
||||||
|
return documents, err
|
||||||
|
}
|
||||||
|
return documents, nil
|
||||||
|
}
|
154
internal/target/decl.go
Normal file
154
internal/target/decl.go
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "context"
|
||||||
|
_ "encoding/json"
|
||||||
|
_ "fmt"
|
||||||
|
_ "gopkg.in/yaml.v3"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"decl/internal/resource"
|
||||||
|
"os"
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
_ "errors"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormatYaml = "yaml"
|
||||||
|
FormatJson = "json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeclFile struct {
|
||||||
|
Path string `yaml:"path" json:"path"`
|
||||||
|
Gzip bool `yaml:"gzip,omitempty" json:"gzip,omitempty"`
|
||||||
|
Format string `yaml:"format,omitempty" json:"format,omitempty"`
|
||||||
|
encoder resource.Encoder `yaml:"-" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDeclFile() *DeclFile {
|
||||||
|
return &DeclFile{ Gzip: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileDocTarget(u *url.URL, format string, gzip bool, fileUri bool) DocTarget {
|
||||||
|
t := NewDeclFile()
|
||||||
|
t.Format = format
|
||||||
|
t.Gzip = gzip
|
||||||
|
if fileUri {
|
||||||
|
fileAbsolutePath, _ := filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI()))
|
||||||
|
t.Path = fileAbsolutePath
|
||||||
|
} else {
|
||||||
|
t.Path = filepath.Join(u.Hostname(), u.Path)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TargetTypes.Register([]string{"decl", "file"}, func(u *url.URL) DocTarget {
|
||||||
|
t := NewDeclFile()
|
||||||
|
if u.Path != "-" {
|
||||||
|
t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.Path))
|
||||||
|
} else {
|
||||||
|
t.Path = "-"
|
||||||
|
}
|
||||||
|
if _,ok := u.Query()["gzip"]; ok {
|
||||||
|
t.Gzip = true
|
||||||
|
}
|
||||||
|
if format,ok := u.Query()["format"]; ok {
|
||||||
|
switch format[0] {
|
||||||
|
case string(FormatYaml):
|
||||||
|
t.Format = FormatYaml
|
||||||
|
case string(FormatJson):
|
||||||
|
t.Format = FormatJson
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
|
||||||
|
TargetTypes.Register([]string{"yaml.gz","yml.gz"}, func(u *url.URL) DocTarget {
|
||||||
|
switch u.Scheme {
|
||||||
|
case "yaml", "yml", "file":
|
||||||
|
return NewFileDocTarget(u, FormatYaml, true, false)
|
||||||
|
}
|
||||||
|
return NewFileDocTarget(u, FormatYaml, true, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
TargetTypes.Register([]string{"json.gz"}, func(u *url.URL) DocTarget {
|
||||||
|
switch u.Scheme {
|
||||||
|
case "json", "file":
|
||||||
|
return NewFileDocTarget(u, FormatJson, true, false)
|
||||||
|
}
|
||||||
|
return NewFileDocTarget(u, FormatJson, true, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
TargetTypes.Register([]string{"yaml","yml"}, func(u *url.URL) DocTarget {
|
||||||
|
switch u.Scheme {
|
||||||
|
case "yaml", "yml", "file":
|
||||||
|
return NewFileDocTarget(u, FormatYaml, false, false)
|
||||||
|
}
|
||||||
|
return NewFileDocTarget(u, FormatYaml, false, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
TargetTypes.Register([]string{"json"}, func(u *url.URL) DocTarget {
|
||||||
|
switch u.Scheme {
|
||||||
|
case "json", "file":
|
||||||
|
return NewFileDocTarget(u, FormatJson, false, false)
|
||||||
|
}
|
||||||
|
return NewFileDocTarget(u, FormatJson, false, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (d *DeclFile) Type() string { return "decl" }
|
||||||
|
|
||||||
|
func (d *DeclFile) EmitResources(documents []*resource.Document, filter resource.ResourceSelector) (error) {
|
||||||
|
var file *os.File
|
||||||
|
var fileErr error
|
||||||
|
var fileWriter io.Writer
|
||||||
|
if d.Path == "" || d.Path == "-" {
|
||||||
|
file = os.Stdout
|
||||||
|
} else {
|
||||||
|
file, fileErr = os.Open(d.Path)
|
||||||
|
if fileErr != nil {
|
||||||
|
return fileErr
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
file.Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Gzip {
|
||||||
|
fileWriter = gzip.NewWriter(file)
|
||||||
|
} else {
|
||||||
|
fileWriter = file
|
||||||
|
}
|
||||||
|
|
||||||
|
switch d.Format {
|
||||||
|
case FormatJson:
|
||||||
|
d.encoder = resource.NewJSONEncoder(fileWriter)
|
||||||
|
case FormatYaml:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
d.encoder = resource.NewYAMLEncoder(fileWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, doc := range documents {
|
||||||
|
emitDoc := resource.NewDocument()
|
||||||
|
if validationErr := doc.Validate(); validationErr != nil {
|
||||||
|
return validationErr
|
||||||
|
}
|
||||||
|
for _, declaration := range doc.Filter(filter) {
|
||||||
|
emitDoc.ResourceDecls = append(emitDoc.ResourceDecls, *declaration)
|
||||||
|
}
|
||||||
|
slog.Info("EmitResources", "doctarget", d, "encoder", d.encoder, "emit", emitDoc)
|
||||||
|
if documentErr := d.encoder.Encode(emitDoc); documentErr != nil {
|
||||||
|
slog.Info("EmitResources", "err", documentErr)
|
||||||
|
return documentErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
35
internal/target/doctarget.go
Normal file
35
internal/target/doctarget.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "context"
|
||||||
|
_ "encoding/json"
|
||||||
|
_ "fmt"
|
||||||
|
_ "gopkg.in/yaml.v3"
|
||||||
|
_ "net/url"
|
||||||
|
_ "regexp"
|
||||||
|
_ "strings"
|
||||||
|
_ "os"
|
||||||
|
_ "io"
|
||||||
|
"decl/internal/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
// convert a document into some other container type
|
||||||
|
|
||||||
|
// move selector to resource pkg
|
||||||
|
// type ResourceSelector func(r resource.Resource) bool
|
||||||
|
|
||||||
|
type DocTarget interface {
|
||||||
|
Type() string
|
||||||
|
|
||||||
|
EmitResources(documents []*resource.Document, filter resource.ResourceSelector) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocTarget(uri string) DocTarget {
|
||||||
|
s, e := TargetTypes.New(uri)
|
||||||
|
if e == nil {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
94
internal/target/tar.go
Normal file
94
internal/target/tar.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "context"
|
||||||
|
_ "encoding/json"
|
||||||
|
_ "fmt"
|
||||||
|
_ "gopkg.in/yaml.v3"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"decl/internal/resource"
|
||||||
|
"compress/gzip"
|
||||||
|
"archive/tar"
|
||||||
|
_ "regexp"
|
||||||
|
"os"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Tar struct {
|
||||||
|
Path string `yaml:"path" json:"path"`
|
||||||
|
Gzip bool `yaml:"gzip" json:"gzip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTar() *Tar {
|
||||||
|
return &Tar{ Gzip: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TargetTypes.Register([]string{"tar"}, func(u *url.URL) DocTarget {
|
||||||
|
t := NewTar()
|
||||||
|
t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.Path))
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
|
||||||
|
TargetTypes.Register([]string{"tar.gz", "tgz"}, func(u *url.URL) DocTarget {
|
||||||
|
t := NewTar()
|
||||||
|
if u.Scheme == "file" {
|
||||||
|
fileAbsolutePath, _ := filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI()))
|
||||||
|
t.Path = fileAbsolutePath
|
||||||
|
} else {
|
||||||
|
t.Path = filepath.Join(u.Hostname(), u.Path)
|
||||||
|
}
|
||||||
|
t.Gzip = true
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (t *Tar) Type() string { return "tar" }
|
||||||
|
|
||||||
|
func (t *Tar) EmitResources(documents []*resource.Document, filter resource.ResourceSelector) error {
|
||||||
|
file, fileErr := os.Create(t.Path)
|
||||||
|
if fileErr != nil {
|
||||||
|
return fileErr
|
||||||
|
}
|
||||||
|
var fileWriter io.WriteCloser
|
||||||
|
if t.Gzip {
|
||||||
|
fileWriter = gzip.NewWriter(file)
|
||||||
|
} else {
|
||||||
|
fileWriter = file
|
||||||
|
}
|
||||||
|
|
||||||
|
tarWriter := tar.NewWriter(fileWriter)
|
||||||
|
defer func() {
|
||||||
|
tarWriter.Close()
|
||||||
|
fileWriter.Close()
|
||||||
|
file.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _,document := range documents {
|
||||||
|
for _,res := range document.Filter(func(d *resource.Declaration) bool {
|
||||||
|
if d.Type == "file" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}) {
|
||||||
|
var f *resource.File = res.Attributes.(*resource.File)
|
||||||
|
slog.Info("Tar.EmitResources", "file", f)
|
||||||
|
hdr, fiErr := tar.FileInfoHeader(f.FileInfo(), "")
|
||||||
|
slog.Info("Tar.EmitResources", "header", hdr, "err", fiErr)
|
||||||
|
if err := tarWriter.WriteHeader(hdr); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := tarWriter.Write([]byte(f.Content)); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
14
internal/target/tar_test.go
Normal file
14
internal/target/tar_test.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewTarSource(t *testing.T) {
|
||||||
|
s := NewTar()
|
||||||
|
assert.NotNil(t, s)
|
||||||
|
}
|
||||||
|
|
100
internal/target/types.go
Normal file
100
internal/target/types.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"path/filepath"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnknownTargetType = errors.New("Unknown target type")
|
||||||
|
TargetTypes *Types = NewTypes()
|
||||||
|
)
|
||||||
|
|
||||||
|
type TypeName string //`json:"type"`
|
||||||
|
|
||||||
|
type TypeFactory func(*url.URL) DocTarget
|
||||||
|
|
||||||
|
type Types struct {
|
||||||
|
registry map[string]TypeFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTypes() *Types {
|
||||||
|
return &Types{registry: make(map[string]TypeFactory)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Types) Register(names []string, factory TypeFactory) {
|
||||||
|
for _,name := range names {
|
||||||
|
t.registry[name] = factory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Types) FromExtension(path string) (TypeFactory, error) {
|
||||||
|
elements := strings.Split(path, ".")
|
||||||
|
numberOfElements := len(elements)
|
||||||
|
if numberOfElements > 2 {
|
||||||
|
if src := t.Get(strings.Join(elements[numberOfElements - 2: numberOfElements - 1], ".")); src != nil {
|
||||||
|
return src, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if src := t.Get(elements[numberOfElements - 1]); src != nil {
|
||||||
|
return src, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrUnknownTargetType, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Types) New(uri string) (DocTarget, error) {
|
||||||
|
if uri == "" {
|
||||||
|
uri = "file://-"
|
||||||
|
}
|
||||||
|
|
||||||
|
u, e := url.Parse(uri)
|
||||||
|
if u == nil || e != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrUnknownTargetType, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Scheme == "" {
|
||||||
|
u.Scheme = "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(u.Hostname(), u.Path)
|
||||||
|
if d, lookupErr := t.FromExtension(path); d != nil {
|
||||||
|
slog.Info("Target.New", "target", t, "err", lookupErr)
|
||||||
|
return d(u), lookupErr
|
||||||
|
} else {
|
||||||
|
slog.Info("Target.New", "target", t, "err", lookupErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r, ok := t.registry[u.Scheme]; ok {
|
||||||
|
return r(u), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrUnknownTargetType, u.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Types) Has(typename string) bool {
|
||||||
|
if _, ok := t.registry[typename]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Types) Get(typename string) TypeFactory {
|
||||||
|
if d, ok := t.registry[typename]; ok {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *TypeName) UnmarshalJSON(b []byte) error {
|
||||||
|
TargetTypeName := strings.Trim(string(b), "\"")
|
||||||
|
if TargetTypes.Has(TargetTypeName) {
|
||||||
|
*n = TypeName(TargetTypeName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%w: %s", ErrUnknownTargetType, TargetTypeName)
|
||||||
|
}
|
89
internal/target/types_test.go
Normal file
89
internal/target/types_test.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package target
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "context"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"decl/internal/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockDocTarget struct {
|
||||||
|
InjectType func() string
|
||||||
|
InjectEmitResources func(documents []*resource.Document, filter resource.ResourceSelector) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDocTarget) Type() string { return m.InjectType() }
|
||||||
|
func (m *MockDocTarget) EmitResources(documents []*resource.Document, filter resource.ResourceSelector) error { return m.InjectEmitResources(documents, filter) }
|
||||||
|
|
||||||
|
func NewFooDocTarget() DocTarget {
|
||||||
|
return &MockDocTarget{
|
||||||
|
InjectType: func() string { return "foo" },
|
||||||
|
InjectEmitResources: func(documents []*resource.Document, filter resource.ResourceSelector) error { return nil },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockFileDocTarget() DocTarget {
|
||||||
|
return &MockDocTarget{
|
||||||
|
InjectType: func() string { return "file" },
|
||||||
|
InjectEmitResources: func(documents []*resource.Document, filter resource.ResourceSelector) error { return nil },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewTargetTypes(t *testing.T) {
|
||||||
|
targetTypes := NewTypes()
|
||||||
|
assert.NotNil(t, targetTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewTargetTypesRegister(t *testing.T) {
|
||||||
|
m := NewFooDocTarget()
|
||||||
|
|
||||||
|
targetTypes := NewTypes()
|
||||||
|
assert.NotNil(t, targetTypes)
|
||||||
|
|
||||||
|
targetTypes.Register([]string{"foo"}, func(*url.URL) DocTarget { return m })
|
||||||
|
|
||||||
|
r, e := targetTypes.New("foo://")
|
||||||
|
assert.Nil(t, e)
|
||||||
|
assert.Equal(t, m, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceTypesFromURI(t *testing.T) {
|
||||||
|
m := NewFooDocTarget()
|
||||||
|
|
||||||
|
targetTypes := NewTypes()
|
||||||
|
assert.NotNil(t, targetTypes)
|
||||||
|
|
||||||
|
targetTypes.Register([]string{"foo"}, func(*url.URL) DocTarget { return m })
|
||||||
|
|
||||||
|
r, e := targetTypes.New("foo://bar")
|
||||||
|
assert.Nil(t, e)
|
||||||
|
assert.Equal(t, m, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceTypesHasType(t *testing.T) {
|
||||||
|
m := NewFooDocTarget()
|
||||||
|
|
||||||
|
targetTypes := NewTypes()
|
||||||
|
assert.NotNil(t, targetTypes)
|
||||||
|
|
||||||
|
targetTypes.Register([]string{"foo"}, func(*url.URL) DocTarget { return m })
|
||||||
|
|
||||||
|
assert.True(t, targetTypes.Has("foo"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocTargetTypeName(t *testing.T) {
|
||||||
|
TargetTypes.Register([]string{"file"}, func(*url.URL) DocTarget { return NewMockFileDocTarget() })
|
||||||
|
|
||||||
|
type fDocTargetName struct {
|
||||||
|
Name TypeName `json:"type"`
|
||||||
|
}
|
||||||
|
fTypeName := &fDocTargetName{}
|
||||||
|
jsonType := `{ "type": "file" }`
|
||||||
|
e := json.Unmarshal([]byte(jsonType), &fTypeName)
|
||||||
|
assert.Nil(t, e)
|
||||||
|
assert.Equal(t, "file", string(fTypeName.Name))
|
||||||
|
}
|
@ -12,6 +12,7 @@ import (
|
|||||||
type MockContainerClient struct {
|
type MockContainerClient struct {
|
||||||
InjectContainerStart func(ctx context.Context, containerID string, options container.StartOptions) error
|
InjectContainerStart func(ctx context.Context, containerID string, options container.StartOptions) error
|
||||||
InjectContainerCreate func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error)
|
InjectContainerCreate func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error)
|
||||||
|
InjectNetworkCreate func(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
|
||||||
InjectContainerList func(context.Context, container.ListOptions) ([]types.Container, error)
|
InjectContainerList func(context.Context, container.ListOptions) ([]types.Container, error)
|
||||||
InjectContainerInspect func(context.Context, string) (types.ContainerJSON, error)
|
InjectContainerInspect func(context.Context, string) (types.ContainerJSON, error)
|
||||||
InjectContainerRemove func(context.Context, string, container.RemoveOptions) error
|
InjectContainerRemove func(context.Context, string, container.RemoveOptions) error
|
||||||
@ -47,3 +48,7 @@ func (m *MockContainerClient) Close() error {
|
|||||||
}
|
}
|
||||||
return m.InjectClose()
|
return m.InjectClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockContainerClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
|
||||||
|
return m.InjectNetworkCreate(ctx, name, options)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user