update container/iptables resources
Some checks failed
Lint / golangci-lint (push) Failing after 9m50s
Declarative Tests / test (push) Failing after 10s

This commit is contained in:
Matthew Rich 2024-05-05 17:48:54 -07:00
parent f25fa59449
commit 43a2274b7e
44 changed files with 1940 additions and 187 deletions

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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
} }

View File

@ -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)
} }

View 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 ""
}

View 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)
}

View File

@ -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)

View File

@ -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)
} }

View File

@ -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"

View File

@ -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

View File

@ -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

View 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)
} }

View File

@ -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) {

View File

@ -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)
}

View File

@ -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
} }

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 },
} }
} }

View File

@ -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
} }

View File

@ -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)
} }

View File

@ -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) {

View File

@ -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) {

View File

@ -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)
} }

View File

@ -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
} }
*/

View File

@ -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 {

View 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"
}
}
}

View 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})$"
}
}
}

View File

@ -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"
}
}
}

View 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})$"
}
}
}

View File

@ -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" }
] ]
} }
} }

View File

@ -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"
} }

View File

@ -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"

View File

@ -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
} }

View File

@ -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"

View 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
View 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
}

View 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
View 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
}

View 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
View 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)
}

View 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))
}

View File

@ -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)
}