update container/iptables resources
This commit is contained in:
parent
f25fa59449
commit
43a2274b7e
@ -15,4 +15,4 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "decl"
|
||||
artifacts: "jx"
|
||||
|
3
Makefile
3
Makefile
@ -9,4 +9,5 @@ jx-cli:
|
||||
go build -o jx $(LDFLAGS) ./cmd/cli/main.go
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
git clone https://gitea.rosskeen.house/Declarative/decl.git
|
||||
git clone https://gitea.rosskeen.house/doublejynx/jx.git
|
||||
|
||||
make test
|
||||
|
||||
|
@ -217,24 +217,27 @@ func DiffSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
||||
rightDocuments := make([]*resource.Document, 0, 100)
|
||||
|
||||
slog.Info("jx diff subcommand", "left", leftSource, "right", rightSource, "flagset", cmd)
|
||||
leftDocuments = append(leftDocuments, LoadSourceURI(leftSource)...)
|
||||
|
||||
if rightSource == "" {
|
||||
slog.Info("jx diff clone", "docs", leftDocuments)
|
||||
for i, doc := range leftDocuments {
|
||||
rightDocuments = append(rightDocuments, LoadSourceURI(leftSource)...)
|
||||
slog.Info("jx diff clone", "docs", rightDocuments)
|
||||
for i, doc := range rightDocuments {
|
||||
if doc != nil {
|
||||
rightDocuments = append(rightDocuments, doc.Clone())
|
||||
leftDocuments = append(leftDocuments, doc.Clone())
|
||||
for _,resourceDeclaration := range leftDocuments[i].Resources() {
|
||||
if _, e := resourceDeclaration.Resource().Read(ctx); e != nil {
|
||||
return e
|
||||
slog.Info("jx diff ", "err", e)
|
||||
//return e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
leftDocuments = append(leftDocuments, LoadSourceURI(leftSource)...)
|
||||
rightDocuments = append(rightDocuments, LoadSourceURI(rightSource)...)
|
||||
}
|
||||
|
||||
slog.Info("jx diff ", "right", rightDocuments, "left", leftDocuments)
|
||||
index := 0
|
||||
for {
|
||||
if index >= len(rightDocuments) && index >= len(leftDocuments) {
|
||||
|
@ -36,15 +36,35 @@ func NewCommand() *Command {
|
||||
return nil, err
|
||||
}
|
||||
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()
|
||||
if pipeErr != nil {
|
||||
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)
|
||||
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
|
||||
}
|
||||
@ -60,15 +80,21 @@ func (c *Command) LoadDecl(yamlResourceDeclaration 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 {
|
||||
var commandLineArg strings.Builder
|
||||
err := template.Must(template.New(fmt.Sprintf("arg%d", i)).Parse(string(arg))).Execute(&commandLineArg, value)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -13,17 +13,19 @@ import (
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/strslice"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/nat"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"gopkg.in/yaml.v3"
|
||||
_ "gopkg.in/yaml.v3"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
_ "os"
|
||||
_ "os/exec"
|
||||
_ "os"
|
||||
_ "os/exec"
|
||||
"path/filepath"
|
||||
_ "strings"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
)
|
||||
|
||||
type ContainerClient interface {
|
||||
@ -42,6 +44,7 @@ type Container struct {
|
||||
Cmd []string `json:"cmd,omitempty" yaml:"cmd,omitempty"`
|
||||
Entrypoint strslice.StrSlice `json:"entrypoint,omitempty" yaml:"entrypoint,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"`
|
||||
Image string `json:"image" yaml:"image"`
|
||||
ResolvConfPath string `json:"resolvconfpath" yaml:"resolvconfpath"`
|
||||
@ -61,13 +64,14 @@ type Container struct {
|
||||
GraphDriver types.GraphDriverData `json:"graphdriver" yaml:"graphdriver"`
|
||||
SizeRw *int64 `json:",omitempty" yaml:",omitempty"`
|
||||
SizeRootFs *int64 `json:",omitempty" yaml:",omitempty"`
|
||||
Networks []string `json:"networks,omitempty" yaml:"networks,omitempty"`
|
||||
/*
|
||||
Mounts []MountPoint
|
||||
Config *container.Config
|
||||
NetworkSettings *NetworkSettings
|
||||
*/
|
||||
|
||||
State string `yaml:"state"`
|
||||
State string `yaml:"state,omitempty" json:"state,omitempty"`
|
||||
|
||||
apiClient ContainerClient
|
||||
}
|
||||
@ -121,11 +125,16 @@ func (c *Container) Clone() Resource {
|
||||
GraphDriver: c.GraphDriver,
|
||||
SizeRw: c.SizeRw,
|
||||
SizeRootFs: c.SizeRootFs,
|
||||
Networks: c.Networks,
|
||||
State: c.State,
|
||||
apiClient: c.apiClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) StateMachine() machine.Stater {
|
||||
return ProcessMachine()
|
||||
}
|
||||
|
||||
func (c *Container) URI() string {
|
||||
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 {
|
||||
numberOfEnvironmentVariables := len(c.Environment)
|
||||
|
||||
portset := nat.PortSet {}
|
||||
for _, port := range c.Ports {
|
||||
portset[nat.Port(port)] = struct{}{}
|
||||
}
|
||||
config := &container.Config{
|
||||
Image: c.Image,
|
||||
Cmd: c.Cmd,
|
||||
Entrypoint: c.Entrypoint,
|
||||
Tty: false,
|
||||
ExposedPorts: portset,
|
||||
}
|
||||
|
||||
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 {
|
||||
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 TypeName `json:"type" yaml:"type"`
|
||||
Transition string `json:"transition" yaml:"transition"`
|
||||
Transition string `json:"transition,omitempty" yaml:"transition,omitempty"`
|
||||
}
|
||||
|
||||
type Declaration struct {
|
||||
@ -64,6 +64,11 @@ func (d *Declaration) Resource() Resource {
|
||||
return d.Attributes
|
||||
}
|
||||
|
||||
func (d *Declaration) Apply() error {
|
||||
stater := d.Attributes.StateMachine()
|
||||
stater.Trigger(d.Transition)
|
||||
}
|
||||
|
||||
func (d *Declaration) SetURI(uri string) error {
|
||||
slog.Info("Declaration.SetURI()", "uri", uri, "declaration", d)
|
||||
d.Attributes = NewResource(uri)
|
||||
|
@ -1,4 +1,6 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
|
||||
package resource
|
||||
|
||||
import (
|
||||
@ -97,7 +99,7 @@ func TestDeclarationJson(t *testing.T) {
|
||||
"type": "user",
|
||||
"attributes": {
|
||||
"name": "testuser",
|
||||
"uid": 10012
|
||||
"uid": "10012"
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -106,6 +108,6 @@ func TestDeclarationJson(t *testing.T) {
|
||||
assert.Nil(t, ue)
|
||||
assert.Equal(t, TypeName("user"), userResourceDeclaration.Type)
|
||||
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) {
|
||||
decl := `{
|
||||
"name": "testuser",
|
||||
"uid": 12001,
|
||||
"uid": "12001",
|
||||
"group": "12001",
|
||||
"home": "/home/testuser",
|
||||
"state": "present"
|
||||
@ -41,7 +41,7 @@ func TestNewDecoderDecodeJSON(t *testing.T) {
|
||||
func TestNewJSONStringDecoder(t *testing.T) {
|
||||
decl := `{
|
||||
"name": "testuser",
|
||||
"uid": 12001,
|
||||
"uid": "12001",
|
||||
"group": "12001",
|
||||
"home": "/home/testuser",
|
||||
"state": "present"
|
||||
|
@ -4,7 +4,7 @@ package resource
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
_ "fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io"
|
||||
"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...) {
|
||||
slog.Info("Diff()", "diff", diff)
|
||||
fmt.Printf("yaml %#v with %#v\n", yamlDiff, withDiff)
|
||||
_,e := output.Write([]byte(diff.Dump()))
|
||||
if e != nil {
|
||||
return "", e
|
||||
|
@ -45,9 +45,10 @@ resources:
|
||||
- type: user
|
||||
attributes:
|
||||
name: "testuser"
|
||||
uid: 10022
|
||||
uid: "10022"
|
||||
group: "10022"
|
||||
home: "/home/testuser"
|
||||
createhome: true
|
||||
state: present
|
||||
`, file)
|
||||
d := NewDocument()
|
||||
@ -138,9 +139,10 @@ resources:
|
||||
- type: user
|
||||
attributes:
|
||||
name: "testuser"
|
||||
uid: 10022
|
||||
uid: "10022"
|
||||
group: "10022"
|
||||
home: "/home/testuser"
|
||||
createhome: true
|
||||
state: present
|
||||
`
|
||||
d := NewDocument()
|
||||
@ -169,9 +171,10 @@ resources:
|
||||
- type: user
|
||||
attributes:
|
||||
name: "testuser"
|
||||
uid: 10022
|
||||
uid: "10022"
|
||||
group: "10022"
|
||||
home: "/home/testuser"
|
||||
createhome: true
|
||||
state: present
|
||||
`
|
||||
d := NewDocument()
|
||||
@ -194,7 +197,7 @@ resources:
|
||||
- type: user
|
||||
attributes:
|
||||
name: "testuser"
|
||||
uid: 10022
|
||||
uid: "10022"
|
||||
home: "/home/testuser"
|
||||
state: present
|
||||
- type: file
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"path/filepath"
|
||||
_ "strings"
|
||||
"io"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
)
|
||||
|
||||
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 {
|
||||
return fmt.Sprintf("exec://%s", x.Id)
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
"crypto/sha256"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
)
|
||||
|
||||
type FileType string
|
||||
@ -34,6 +35,9 @@ const (
|
||||
|
||||
var ErrInvalidResourceURI error = errors.New("Invalid resource URI")
|
||||
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() {
|
||||
ResourceTypes.Register("file", func(u *url.URL) Resource {
|
||||
@ -60,7 +64,7 @@ type File struct {
|
||||
Size int64 `json:"size,omitempty" yaml:"size,omitempty"`
|
||||
Target string `json:"target,omitempty" yaml:"target,omitempty"`
|
||||
FileType FileType `json:"filetype" yaml:"filetype"`
|
||||
State string `json:"state" yaml:"state"`
|
||||
State string `json:"state,omitempty" yaml:"state,omitempty"`
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Sprintf("file://%s", f.Path)
|
||||
}
|
||||
@ -134,7 +142,7 @@ func (f *File) Apply() error {
|
||||
{
|
||||
uid, uidErr := LookupUID(f.Owner)
|
||||
if uidErr != nil {
|
||||
return uidErr
|
||||
return fmt.Errorf("%w: unkwnon user %d", ErrInvalidFileOwner, uid)
|
||||
}
|
||||
|
||||
gid, gidErr := LookupGID(f.Group)
|
||||
@ -145,9 +153,8 @@ func (f *File) Apply() error {
|
||||
slog.Info("File.Mode", "mode", f.Mode)
|
||||
mode, modeErr := strconv.ParseInt(f.Mode, 8, 64)
|
||||
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)
|
||||
//if os.IsNotExist(e) {
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
"os/user"
|
||||
)
|
||||
|
||||
func TestNewFileResource(t *testing.T) {
|
||||
@ -26,6 +27,20 @@ func TestNewFileResource(t *testing.T) {
|
||||
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) {
|
||||
f := NewFile()
|
||||
assert.NotEqual(t, nil, f)
|
||||
@ -280,3 +295,64 @@ func TestFileResourceFileInfo(t *testing.T) {
|
||||
fi := f.FileInfo()
|
||||
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"
|
||||
"strings"
|
||||
"log/slog"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -38,7 +39,7 @@ type HTTP struct {
|
||||
Endpoint string `yaml:"endpoint" json:"endpoint"`
|
||||
Headers []HTTPHeader `yaml:"headers,omitempty" json:"headers,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 {
|
||||
@ -55,6 +56,10 @@ func (h *HTTP) Clone() Resource {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTTP) StateMachine() machine.Stater {
|
||||
return StorageMachine()
|
||||
}
|
||||
|
||||
func (h *HTTP) URI() string {
|
||||
return h.Endpoint
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"context"
|
||||
_ "encoding/hex"
|
||||
"encoding/json"
|
||||
_ "errors"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io"
|
||||
@ -16,17 +16,26 @@ _ "os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"log/slog"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ResourceTypes.Register("iptable", func(u *url.URL) Resource {
|
||||
i := NewIptable()
|
||||
i.Table = IptableName(u.Hostname())
|
||||
if len(u.Path) > 0 {
|
||||
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])
|
||||
if len(fields) < 3 {
|
||||
i.ResourceType = IptableTypeChain
|
||||
} else {
|
||||
i.ResourceType = IptableTypeRule
|
||||
id, _ := strconv.ParseUint(fields[2], 10, 32)
|
||||
i.Id = uint(id)
|
||||
}
|
||||
i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD()
|
||||
}
|
||||
return i
|
||||
})
|
||||
}
|
||||
@ -84,10 +93,17 @@ type ExtensionFlag struct {
|
||||
|
||||
type IptablePort uint16
|
||||
|
||||
type IptableType string
|
||||
|
||||
const (
|
||||
IptableTypeRule = "rule"
|
||||
IptableTypeChain = "chain"
|
||||
)
|
||||
|
||||
// Manage the state of iptables rules
|
||||
// iptable://filter/INPUT/0
|
||||
type Iptable struct {
|
||||
Id uint `json:"id" yaml:"id"`
|
||||
Id uint `json:"id,omitempty" yaml:"id,omitempty"`
|
||||
Table IptableName `json:"table" yaml:"table"`
|
||||
Chain IptableChain `json:"chain" yaml:"chain"`
|
||||
Destination IptableCIDR `json:"destination,omitempty" yaml:"destination,omitempty"`
|
||||
@ -99,9 +115,11 @@ type Iptable struct {
|
||||
Match []string `json:"match,omitempty" yaml:"match,omitempty"`
|
||||
Flags []ExtensionFlag `json:"extension_flags,omitempty" yaml:"extension_flags,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"`
|
||||
ChainLength uint `json:"-" yaml:"-"`
|
||||
|
||||
ResourceType IptableType `json:"resourcetype,omitempty" yaml:"resourcetype,omitempty"`
|
||||
CreateCommand *Command `yaml:"-" json:"-"`
|
||||
ReadCommand *Command `yaml:"-" json:"-"`
|
||||
UpdateCommand *Command `yaml:"-" json:"-"`
|
||||
@ -109,13 +127,13 @@ type Iptable struct {
|
||||
}
|
||||
|
||||
func NewIptable() *Iptable {
|
||||
i := &Iptable{}
|
||||
i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.NewCRUD()
|
||||
i := &Iptable{ ResourceType: IptableTypeRule }
|
||||
i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD()
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *Iptable) Clone() Resource {
|
||||
return &Iptable {
|
||||
newIpt := &Iptable {
|
||||
Id: i.Id,
|
||||
Table: i.Table,
|
||||
Chain: i.Chain,
|
||||
@ -125,8 +143,15 @@ func (i *Iptable) Clone() Resource {
|
||||
Out: i.Out,
|
||||
Match: i.Match,
|
||||
Proto: i.Proto,
|
||||
ResourceType: i.ResourceType,
|
||||
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 {
|
||||
@ -140,8 +165,13 @@ func (i *Iptable) SetURI(uri string) error {
|
||||
i.Table = IptableName(resourceUri.Hostname())
|
||||
fields := strings.Split(resourceUri.Path, "/")
|
||||
i.Chain = IptableChain(fields[1])
|
||||
if len(fields) < 3 {
|
||||
i.ResourceType = IptableTypeChain
|
||||
} else {
|
||||
i.ResourceType = IptableTypeRule
|
||||
id, _ := strconv.ParseUint(fields[2], 10, 32)
|
||||
i.Id = uint(id)
|
||||
}
|
||||
} else {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@ -175,7 +205,7 @@ func (i *Iptable) UnmarshalYAML(value *yaml.Node) error {
|
||||
if unmarshalErr := value.Decode((*decodeIptable)(i)); unmarshalErr != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@ -184,12 +214,17 @@ func (i *Iptable) NewCRUD() (create *Command, read *Command, update *Command, de
|
||||
}
|
||||
|
||||
func (i *Iptable) Apply() error {
|
||||
|
||||
ctx := context.Background()
|
||||
switch i.State {
|
||||
case "absent":
|
||||
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 {
|
||||
@ -208,6 +243,177 @@ func (i *Iptable) ResolveId(ctx context.Context) string {
|
||||
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) {
|
||||
out, err := i.ReadCommand.Execute(i)
|
||||
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 *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 {
|
||||
c := NewCommand()
|
||||
c.Path = "iptables"
|
||||
c.Args = []CommandArg{
|
||||
CommandArg("-t"),
|
||||
CommandArg("{{ .Table }}"),
|
||||
CommandArg("-R"),
|
||||
CommandArg("{{ if le .Id .ChainLength }}-R{{ else }}-A{{ end }}"),
|
||||
CommandArg("{{ .Chain }}"),
|
||||
CommandArg("{{ .Id }}"),
|
||||
CommandArg("{{ if le .Id .ChainLength }}{{ .Id }}{{ 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 .Sport }}--sport {{ .Sport }}{{ 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 }}"),
|
||||
}
|
||||
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 {
|
||||
c := NewCommand()
|
||||
c.Path = "iptables"
|
||||
@ -248,60 +523,240 @@ func NewIptableReadCommand() *Command {
|
||||
CommandArg("{{ .Table }}"),
|
||||
CommandArg("-S"),
|
||||
CommandArg("{{ .Chain }}"),
|
||||
CommandArg("{{ .Id }}"),
|
||||
CommandArg("{{ if .Id }}{{ .Id }}{{ end }}"),
|
||||
}
|
||||
c.Extractor = func(out []byte, target any) error {
|
||||
i := target.(*Iptable)
|
||||
ruleFields := strings.Split(strings.TrimSpace(string(out)), " ")
|
||||
switch ruleFields[0] {
|
||||
case "-A":
|
||||
//chain := ruleFields[1]
|
||||
flags := ruleFields[2:]
|
||||
for optind,opt := range flags {
|
||||
if optind > len(flags) - 2 {
|
||||
if i.Id > 0 {
|
||||
return RuleExtractor(out, target)
|
||||
}
|
||||
|
||||
state := "absent"
|
||||
var lineNumber uint = 1
|
||||
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
|
||||
}
|
||||
optValue := flags[optind + 1]
|
||||
switch opt {
|
||||
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)})
|
||||
if err == nil {
|
||||
lineNumber++
|
||||
}
|
||||
}
|
||||
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"
|
||||
default:
|
||||
} else {
|
||||
i.State = "absent"
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func NewIptableUpdateCommand() *Command {
|
||||
return nil
|
||||
return NewIptableCreateCommand()
|
||||
}
|
||||
|
||||
func NewIptableDeleteCommand() *Command {
|
||||
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.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 (
|
||||
"context"
|
||||
_ "gopkg.in/yaml.v3"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
)
|
||||
|
||||
func NewFooResource() *MockResource {
|
||||
@ -14,5 +15,6 @@ func NewFooResource() *MockResource {
|
||||
InjectRead: func(ctx context.Context) ([]byte, error) { return nil,nil },
|
||||
InjectLoadDecl: func(string) error { return nil },
|
||||
InjectApply: func() error { return nil },
|
||||
InjectStateMachine: func() machine.Stater { return nil },
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
_ "gopkg.in/yaml.v3"
|
||||
"encoding/json"
|
||||
_ "fmt"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
)
|
||||
|
||||
type MockResource struct {
|
||||
@ -17,12 +18,17 @@ type MockResource struct {
|
||||
InjectValidate func() error
|
||||
InjectApply func() error
|
||||
InjectRead func(context.Context) ([]byte, error)
|
||||
InjectStateMachine func() machine.Stater
|
||||
}
|
||||
|
||||
func (m *MockResource) Clone() Resource {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockResource) StateMachine() machine.Stater {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockResource) SetURI(uri string) error {
|
||||
return nil
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"regexp"
|
||||
_ "strconv"
|
||||
"strings"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -138,6 +139,10 @@ func (n *NetworkRoute) Clone() Resource {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NetworkRoute) StateMachine() machine.Stater {
|
||||
return StorageMachine()
|
||||
}
|
||||
|
||||
func (n *NetworkRoute) URI() string {
|
||||
return fmt.Sprintf("route://%s", n.Id)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"os/user"
|
||||
"strconv"
|
||||
"regexp"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
var MatchId *regexp.Regexp = regexp.MustCompile(`^[0-9]+$`)
|
||||
@ -19,29 +20,36 @@ func LookupUIDString(userName string) string {
|
||||
}
|
||||
|
||||
func LookupUID(userName string) (int, error) {
|
||||
var userLookupErr error
|
||||
var UID string
|
||||
if MatchId.MatchString(userName) {
|
||||
user, userLookupErr := user.LookupId(userName)
|
||||
if userLookupErr != nil {
|
||||
//return -1, userLookupErr
|
||||
user, err := user.LookupId(userName)
|
||||
slog.Info("LookupUID() numeric", "user", user, "userLookupErr", err)
|
||||
if err != nil {
|
||||
userLookupErr = err
|
||||
UID = userName
|
||||
} else {
|
||||
UID = user.Uid
|
||||
}
|
||||
} else {
|
||||
user, userLookupErr := user.Lookup(userName)
|
||||
if userLookupErr != nil {
|
||||
return -1, userLookupErr
|
||||
}
|
||||
if user, err := user.Lookup(userName); err != nil {
|
||||
return -1, err
|
||||
} else {
|
||||
UID = user.Uid
|
||||
}
|
||||
}
|
||||
|
||||
uid, uidErr := strconv.Atoi(UID)
|
||||
slog.Info("LookupUID()", "uid", uid, "uidErr", uidErr)
|
||||
if uidErr != nil {
|
||||
if userLookupErr != nil {
|
||||
return -1, userLookupErr
|
||||
} else {
|
||||
return -1, uidErr
|
||||
}
|
||||
}
|
||||
|
||||
return uid, nil
|
||||
return uid, userLookupErr
|
||||
}
|
||||
|
||||
func LookupGID(groupName string) (int, error) {
|
||||
|
@ -4,7 +4,6 @@ package resource
|
||||
import (
|
||||
_ "context"
|
||||
_ "encoding/json"
|
||||
_ "fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
_ "io"
|
||||
_ "net/http"
|
||||
@ -20,9 +19,9 @@ func TestLookupUID(t *testing.T) {
|
||||
assert.Nil(t, e)
|
||||
assert.Equal(t, 65534, uid)
|
||||
|
||||
nuid, ne := LookupUID("1001")
|
||||
assert.Nil(t, ne)
|
||||
assert.Equal(t, 1001, nuid)
|
||||
nuid, ne := LookupUID("10101")
|
||||
assert.Error(t, ne, "user: unknonwn userid ", ne)
|
||||
assert.Equal(t, 10101, nuid)
|
||||
}
|
||||
|
||||
func TestLookupGID(t *testing.T) {
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
_ "os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
)
|
||||
|
||||
type PackageType string
|
||||
@ -94,6 +95,10 @@ func (p *Package) Clone() Resource {
|
||||
return newp
|
||||
}
|
||||
|
||||
func (p *Package) StateMachine() machine.Stater {
|
||||
return StorageMachine()
|
||||
}
|
||||
|
||||
func (p *Package) URI() string {
|
||||
return fmt.Sprintf("package://%s?version=%s&type=%s", p.Name, p.Version, p.PackageType)
|
||||
}
|
||||
|
@ -9,12 +9,14 @@ import (
|
||||
_ "fmt"
|
||||
_ "gopkg.in/yaml.v3"
|
||||
_ "net/url"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
)
|
||||
|
||||
type ResourceSelector func(r *Declaration) bool
|
||||
|
||||
type Resource interface {
|
||||
Type() string
|
||||
StateMachine() machine.Stater
|
||||
URI() string
|
||||
SetURI(string) error
|
||||
ResolveId(context.Context) string
|
||||
@ -55,14 +57,22 @@ func NewResource(uri string) Resource {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
/*
|
||||
func Machine() {
|
||||
|
||||
func StorageMachine() machine.Stater {
|
||||
// start_destroy -> absent -> start_create -> present -> start_destroy
|
||||
stater := machine.New("absent")
|
||||
stater.AddStates("absent", "start_create", "present", "start_delete")
|
||||
stater.AddTransition("creating", "absent", "start_create")
|
||||
stater.AddStates("absent", "start_create", "present", "start_delete", "start_read", "start_update")
|
||||
stater.AddTransition("create", "absent", "start_create")
|
||||
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")
|
||||
return stater
|
||||
}
|
||||
|
||||
func ProcessMachine() machine.Stater {
|
||||
return nil
|
||||
}
|
||||
*/
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
)
|
||||
|
||||
//go:embed schemas/*.jsonschema
|
||||
//go:embed schemas/*.schema.json
|
||||
var schemaFiles embed.FS
|
||||
|
||||
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": "exec-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.",
|
||||
"enum": [ "user" ]
|
||||
},
|
||||
"transition": {
|
||||
"$ref": "storagetransition.schema.json"
|
||||
},
|
||||
"attributes": {
|
||||
"$ref": "user.jsonschema"
|
||||
}
|
||||
|
@ -11,9 +11,8 @@
|
||||
"pattern": "^[a-z]([-_a-z0-9]{0,31})$"
|
||||
},
|
||||
"uid": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 65535
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]*$"
|
||||
},
|
||||
"group": {
|
||||
"type": "string"
|
||||
|
@ -6,44 +6,67 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"log/slog"
|
||||
_ "log/slog"
|
||||
"net/url"
|
||||
_ "os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
)
|
||||
|
||||
type decodeUser User
|
||||
|
||||
type UserType string
|
||||
|
||||
const (
|
||||
UserTypeAddUser = "adduser"
|
||||
UserTypeUserAdd = "useradd"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
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"`
|
||||
Groups []string `json:"groups,omitempty" yaml:"groups,omitempty"`
|
||||
Gecos string `json:"gecos,omitempty" yaml:"gecos,omitempty"`
|
||||
Home string `json:"home" yaml:"home"`
|
||||
CreateHome bool `json:"createhome,omitempty" yaml:"createhome,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 {
|
||||
return &User{}
|
||||
return &User{ CreateHome: true }
|
||||
}
|
||||
|
||||
func init() {
|
||||
ResourceTypes.Register("user", func(u *url.URL) Resource {
|
||||
user := NewUser()
|
||||
user.Name = u.Path
|
||||
user.UID, _ = LookupUID(u.Path)
|
||||
user.Name = u.Hostname()
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
func (u *User) Clone() Resource {
|
||||
return &User {
|
||||
newu := &User {
|
||||
Name: u.Name,
|
||||
UID: u.UID,
|
||||
Group: u.Group,
|
||||
@ -53,7 +76,14 @@ func (u *User) Clone() Resource {
|
||||
CreateHome: u.CreateHome,
|
||||
Shell: u.Shell,
|
||||
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 {
|
||||
@ -85,43 +115,11 @@ func (u *User) Apply() error {
|
||||
case "present":
|
||||
_, NoUserExists := LookupUID(u.Name)
|
||||
if NoUserExists != nil {
|
||||
var userCommandName string = "useradd"
|
||||
args := make([]string, 0, 7)
|
||||
if u.UID >= 0 {
|
||||
args = append(args, "-u", fmt.Sprintf("%d", u.UID))
|
||||
}
|
||||
|
||||
if _, pathErr := exec.LookPath("useradd"); pathErr != nil {
|
||||
if _, addUserPathErr := exec.LookPath("adduser"); addUserPathErr == nil {
|
||||
userCommandName = "adduser"
|
||||
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))
|
||||
cmdErr := u.Create(context.Background())
|
||||
return cmdErr
|
||||
}
|
||||
case "absent":
|
||||
var userDelCommandName string = "userdel"
|
||||
args := make([]string, 0, 7)
|
||||
|
||||
if _, pathErr := exec.LookPath("userdel"); pathErr != nil {
|
||||
if _, delUserPathErr := exec.LookPath("deluser"); delUserPathErr == nil {
|
||||
userDelCommandName = "deluser"
|
||||
}
|
||||
}
|
||||
args = append(args, u.Name)
|
||||
cmd := exec.Command(userDelCommandName, args...)
|
||||
cmdOutput, cmdErr := cmd.CombinedOutput()
|
||||
slog.Info("user command", "command", cmd.String(), "output", string(cmdOutput))
|
||||
cmdErr := u.Delete()
|
||||
return cmdErr
|
||||
}
|
||||
return nil
|
||||
@ -166,29 +164,205 @@ func (u *User) UserAddCommand(args *[]string) error {
|
||||
|
||||
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) {
|
||||
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 e error
|
||||
if 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.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 {
|
||||
u.Group = readGroup.Name
|
||||
} else {
|
||||
panic(groupErr)
|
||||
return groupErr
|
||||
}
|
||||
u.Home = readUser.HomeDir
|
||||
u.Gecos = readUser.Name
|
||||
|
||||
return yaml.Marshal(u)
|
||||
if u.UID != "" {
|
||||
u.State = "present"
|
||||
}
|
||||
}
|
||||
return e
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
_ "context"
|
||||
"context"
|
||||
_ "encoding/json"
|
||||
_ "fmt"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
_ "io"
|
||||
_ "net/http"
|
||||
@ -20,7 +20,28 @@ func TestNewUserResource(t *testing.T) {
|
||||
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) {
|
||||
|
||||
decl := `
|
||||
name: "testuser"
|
||||
uid: 12001
|
||||
@ -28,16 +49,28 @@ func TestCreateUser(t *testing.T) {
|
||||
home: "/home/testuser"
|
||||
state: present
|
||||
`
|
||||
|
||||
u := NewUser()
|
||||
e := u.LoadDecl(decl)
|
||||
assert.Equal(t, nil, e)
|
||||
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()
|
||||
assert.Equal(t, nil, applyErr)
|
||||
uid, uidErr := LookupUID(u.Name)
|
||||
assert.Equal(t, nil, uidErr)
|
||||
assert.Equal(t, 12001, uid)
|
||||
assert.Nil(t, applyErr)
|
||||
|
||||
assert.Equal(t, "12001", u.UID)
|
||||
|
||||
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 {
|
||||
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)
|
||||
InjectNetworkCreate func(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
|
||||
InjectContainerList func(context.Context, container.ListOptions) ([]types.Container, error)
|
||||
InjectContainerInspect func(context.Context, string) (types.ContainerJSON, error)
|
||||
InjectContainerRemove func(context.Context, string, container.RemoveOptions) error
|
||||
@ -47,3 +48,7 @@ func (m *MockContainerClient) Close() error {
|
||||
}
|
||||
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