Compare commits

..

No commits in common. "main" and "v0.3.0" have entirely different histories.
main ... v0.3.0

115 changed files with 1405 additions and 5673 deletions

View File

@ -32,8 +32,6 @@ run:
docker run -it -v $(HOME)/.git-credentials:/root/.git-credentials -v $(HOME)/.gitconfig:/root/.gitconfig -v /var/run/docker.sock:/var/run/docker.sock -v $(shell pwd):/src $(IMAGE) sh docker run -it -v $(HOME)/.git-credentials:/root/.git-credentials -v $(HOME)/.gitconfig:/root/.gitconfig -v /var/run/docker.sock:/var/run/docker.sock -v $(shell pwd):/src $(IMAGE) sh
run-alpine: run-alpine:
docker run -it -v $(HOME)/.git-credentials:/root/.git-credentials -v $(HOME)/.gitconfig:/root/.gitconfig -v /var/run/docker.sock:/var/run/docker.sock -v $(shell pwd):/src golang:1.22.6-alpine sh docker run -it -v $(HOME)/.git-credentials:/root/.git-credentials -v $(HOME)/.gitconfig:/root/.gitconfig -v /var/run/docker.sock:/var/run/docker.sock -v $(shell pwd):/src golang:1.22.6-alpine sh
build-container:
docker run -it -v $(HOME)/.git-credentials:/root/.git-credentials -v /tmp:/tmp -v $(HOME)/.gitconfig:/root/.gitconfig -v /var/run/docker.sock:/var/run/docker.sock -e WORKSPACE_PATH=$(shell pwd) -v $(shell pwd):/src -w /src rosskeenhouse/build-golang:1.22.6-alpine sh
clean: clean:
go clean -modcache go clean -modcache
rm jx rm jx

View File

@ -10,9 +10,7 @@ These tools work with YAML descriptions of resources (E.g. files, users, contain
# Releases # Releases
**<span style="color:red">v0 releases are unstable and changes may be made to interfaces and specifications.</span>** **v0 releases are unstable and changes may be made to interfaces and specifications.**
Use at your own risk.
# JX Documents # JX Documents
@ -43,14 +41,14 @@ resources:
# Testing # Testing
Testing the current version involves checking out main and building inside of the alpine go build container. Testing the current version involves checking out main and building.
``` ```
git clone https://gitea.rosskeen.house/doublejynx/jx.git git clone https://gitea.rosskeen.house/doublejynx/jx.git
make build-container
make test make test
make build
``` ```
# Command-line # Command-line

View File

@ -1,16 +0,0 @@
---
imports:
- /etc/jx/dockerhub.jx.yaml
resources:
- type: container-image
config: dockerhub
transition: update
attributes:
name: rosskeenhouse/build-golang:1.22.6-alpine
push: true
dockerfile: |-
FROM golang:1.22.6-alpine
COPY . /opt/build
WORKDIR /opt/build
RUN ./jx apply ./alpine.jx.yaml
contextref: file://build/docker/golang/build

View File

@ -1,36 +0,0 @@
imports:
- file://common.jx.yaml
resources:
- type: package
transition: create
attributes:
name: musl-dev
- type: package
transition: create
attributes:
name: luajit
verion: =~2.2
- type: package
transition: create
attributes:
name: luajit-dev
- type: package
transition: create
attributes:
name: protobuf
- type: package
transition: create
attributes:
name: openjdk8
- type: package
transition: create
attributes:
name: docker
- type: package
transition: create
attributes:
name: openssh-client
- type: package
transition: create
attributes:
name: golangci-lint

View File

@ -1,45 +0,0 @@
resources:
- type: file
transition: create
attributes:
path: /usr/local/bin/antlr-4.10-complete.jar
sourceref: https://www.antlr.org/download/antlr-4.10-complete.jar
owner: root
group: root
mode: 0755
- type: package
transition: create
attributes:
name: make
- type: package
transition: create
attributes:
name: openssl
- type: package
transition: create
attributes:
name: curl
- type: package
transition: create
attributes:
name: git
- type: package
transition: create
attributes:
name: gcc
- type: exec
transition: create
attributes:
create:
path: go
args:
- install
- google.golang.org/protobuf/cmd/protoc-gen-go@latest
- type: exec
transition: create
attributes:
create:
path: go
args:
- install
- golang.org/x/vuln/cmd/govulncheck@latest

View File

@ -184,25 +184,6 @@ func TestResourcesRead(t *testing.T) {
t.Skip("cli not built") t.Skip("cli not built")
} }
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
assert.Equal(t, req.URL.String(), "/resource/user")
_, err := io.ReadAll(req.Body)
assert.Nil(t, err)
userdecl := []byte(`
type: "user"
attributes:
name: "foo"
gecos: "foo user"
`)
_, writeErr := rw.Write(userdecl)
assert.Nil(t, writeErr)
}))
defer server.Close()
assert.Nil(t, TempDir.CreateFile("testread", "data")) assert.Nil(t, TempDir.CreateFile("testread", "data"))
resources := fmt.Sprintf(` resources := fmt.Sprintf(`
@ -234,7 +215,7 @@ resources:
- type: http - type: http
transition: read transition: read
attributes: attributes:
endpoint: %s/resource/user endpoint: https://gitea.rosskeen.house
- type: route - type: route
transition: read transition: read
attributes: attributes:
@ -246,7 +227,7 @@ resources:
rtid: all rtid: all
routetype: local routetype: local
metric: 100 metric: 100
`, TempDir.FilePath("testread"), server.URL) `, TempDir.FilePath("testread"))
assert.Nil(t, TempDir.CreateFile("resources.jx.yaml", resources)) assert.Nil(t, TempDir.CreateFile("resources.jx.yaml", resources))

View File

@ -10,10 +10,10 @@ _ "decl/internal/config"
_ "decl/internal/resource" _ "decl/internal/resource"
_ "decl/internal/fan" _ "decl/internal/fan"
"decl/internal/builtin" "decl/internal/builtin"
_ "errors" _ "errors"
"flag" "flag"
"fmt" "fmt"
_ "gopkg.in/yaml.v3" _ "gopkg.in/yaml.v3"
"io" "io"
"log/slog" "log/slog"
"os" "os"
@ -159,7 +159,7 @@ func main() {
DefaultConfigurations, configErr := builtin.BuiltInDocuments() DefaultConfigurations, configErr := builtin.BuiltInDocuments()
if configErr != nil { if configErr != nil {
slog.Warn("Failed loading default configuration", "error", configErr) slog.Error("Failed loading default configuration", "error", configErr)
} }
ConfigDoc.AppendConfigurations(DefaultConfigurations) ConfigDoc.AppendConfigurations(DefaultConfigurations)
@ -167,8 +167,8 @@ func main() {
for _, subCmd := range jxSubCommands { for _, subCmd := range jxSubCommands {
cmdFlagSet := flag.NewFlagSet(subCmd.Name, flag.ExitOnError) cmdFlagSet := flag.NewFlagSet(subCmd.Name, flag.ExitOnError)
cmdFlagSet.StringVar(&ConfigPath, "config", "/etc/jx/conf.d", "Config file path") cmdFlagSet.StringVar(&ConfigPath, "config", "/etc/jx", "Config file path")
cmdFlagSet.StringVar(&ConfigPath, "c", "/etc/jx/conf.d", "Config file path") cmdFlagSet.StringVar(&ConfigPath, "c", "/etc/jx", "Config file path")
GlobalOformat = cmdFlagSet.String("oformat", "yaml", "Output serialization format") GlobalOformat = cmdFlagSet.String("oformat", "yaml", "Output serialization format")
cmdFlagSet.StringVar(&GlobalOutput, "output", "-", "Output target (default stdout)") cmdFlagSet.StringVar(&GlobalOutput, "output", "-", "Output target (default stdout)")

View File

@ -1,26 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package main
import (
_ "github.com/stretchr/testify/assert"
"testing"
_ "decl/internal/folio"
_ "decl/internal/data"
_ "log/slog"
)
func TestLoadSourceURIConverter(t *testing.T) {
/*
var uri folio.URI = "file://../../examples/file.jx.yaml"
docs, err := LoadSourceURIConverter(uri)
assert.Nil(t, err)
assert.Greater(t, len(docs), 0)
slog.Info("TestLoadSourceURIConverter", "doc", docs[0], "resource", docs[0].(*folio.Document).ResourceDeclarations[0].Attributes)
resDecl := docs[0].(*folio.Document).ResourceDeclarations[0]
assert.Equal(t, "file", resDecl.Attributes.Type())
v, ok := docs[0].Get("file:///tmp/foo.txt")
assert.True(t, ok)
assert.Equal(t, "/tmp/foo.txt", v.(data.Declaration).Resource().(data.FileResource).FilePath())
*/
}

View File

@ -1,8 +1,8 @@
resources: resources:
- type: file - type: file
transition: create
attributes: attributes:
path: /tmp/foo.txt path: /tmp/foo.txt
owner: nobody owner: nobody
group: nobody
mode: 0644 mode: 0644
state: present state: present

View File

@ -1,3 +0,0 @@
# Import the built-in install document which install the jx binary.
imports:
- file://documents/install.jx.yaml

View File

@ -1,7 +1,7 @@
resources: resources:
- type: user - type: user
transition: create
attributes: attributes:
name: "testuser" name: "testuser"
uid: "12001" uid: "12001"
home: "/home/testuser" home: "/home/testuser"
state: present

View File

@ -1,54 +0,0 @@
configurations:
- name: confdir
values:
prefix: /etc/jx
resources:
- type: group
transition: create
onerror: stop
attributes:
name: "jx"
- type: file
transition: update
attributes:
path: "/etc/jx"
owner: "root"
group: "root"
mode: "0755"
filetype: directory
- type: file
transition: update
config: confdir
attributes:
path: "conf.d"
owner: "root"
group: "jx"
mode: "0770"
filetype: directory
- type: file
transition: update
config: confdir
attributes:
path: "lib"
owner: "root"
group: "jx"
mode: "0770"
filetype: directory
- type: file
transition: update
config: confdir
attributes:
path: "pki"
owner: "root"
group: "jx"
mode: "0770"
filetype: directory
- type: file
transition: update
config: confdir
attributes:
path: "pki/ca"
owner: "root"
group: "jx"
mode: "0770"
filetype: directory

View File

@ -1,16 +0,0 @@
imports:
- file://documents/config.jx.yaml
configurations:
- name: bindir
values:
prefix: /usr/local/bin
resources:
- type: file
transition: update
config: bindir
attributes:
path: "jx"
owner: "root"
group: "root"
mode: "0755"
sourceref: file://jx

View File

@ -100,10 +100,8 @@ func (a *App) SetOutput(uri string) (err error) {
// Each document has an `imports` keyword which can be used to load dependencies // Each document has an `imports` keyword which can be used to load dependencies
func (a *App) LoadDocumentImports() error { func (a *App) LoadDocumentImports() error {
slog.Info("Client.LoadDocumentImports()", "documents", a.Documents)
for i, d := range a.Documents { for i, d := range a.Documents {
importedDocs := d.ImportedDocuments() importedDocs := d.ImportedDocuments()
slog.Info("Client.LoadDocumentImports()", "imported", importedDocs)
for _, importedDocument := range importedDocs { for _, importedDocument := range importedDocs {
docURI := folio.URI(importedDocument.GetURI()) docURI := folio.URI(importedDocument.GetURI())
if _, ok := a.ImportedMap[docURI]; !ok { if _, ok := a.ImportedMap[docURI]; !ok {
@ -128,7 +126,7 @@ func (a *App) ImportResource(ctx context.Context, uri string) (err error) {
a.Documents = append(a.Documents, folio.DocumentRegistry.NewDocument("")) a.Documents = append(a.Documents, folio.DocumentRegistry.NewDocument(""))
} }
resourceURI := folio.URI(uri) resourceURI := folio.URI(uri)
u := resourceURI.Parse().URL() u := resourceURI.Parse()
if u == nil { if u == nil {
return fmt.Errorf("Failed adding resource: %s", uri) return fmt.Errorf("Failed adding resource: %s", uri)
} }
@ -149,19 +147,9 @@ func (a *App) ImportResource(ctx context.Context, uri string) (err error) {
} }
func (a *App) ImportSource(uri string) (loadedDocuments []data.Document, err error) { func (a *App) ImportSource(uri string) (loadedDocuments []data.Document, err error) {
if source := folio.URI(uri).Parse().URL(); source != nil { if loadedDocuments, err = folio.DocumentRegistry.Load(folio.URI(uri)); err == nil && loadedDocuments != nil {
if source.Scheme == "" { a.Documents = append(a.Documents, loadedDocuments...)
source.Scheme = "file"
}
slog.Info("Client.ImportSource()", "uri", uri, "source", source, "error", err)
if loadedDocuments, err = folio.DocumentRegistry.LoadFromParsedURI(source); err == nil && loadedDocuments != nil {
a.Documents = append(a.Documents, loadedDocuments...)
}
} else {
err = folio.ErrInvalidURI
} }
slog.Info("Client.ImportSource()", "uri", uri, "error", err) slog.Info("Client.ImportSource()", "uri", uri, "error", err)
return return
} }
@ -195,7 +183,7 @@ func (a *App) Apply(ctx context.Context, deleteResources bool) (err error) {
continue continue
} }
slog.Info("Client.Apply()", "uri", d.GetURI(), "document", d, "state", overrideState, "error", err) slog.Info("Client.Apply()", "document", d, "state", overrideState, "error", err)
if e := d.(*folio.Document).Apply(overrideState); e != nil { if e := d.(*folio.Document).Apply(overrideState); e != nil {
slog.Info("Client.Apply() error", "error", e) slog.Info("Client.Apply() error", "error", e)
return e return e

View File

@ -1,162 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package client
import (
"github.com/stretchr/testify/assert"
"testing"
"fmt"
"context"
"decl/internal/folio"
"decl/internal/resource"
"decl/internal/codec"
"log/slog"
"os"
"io"
"strings"
)
var containerDoc string = `
imports:
- %s
resources:
- type: container
transition: create
attributes:
image: rosskeenhouse/build-golang:1.22.6-alpine
name: jx-client-resources-test
hostconfig:
autoremove: false
mounts:
- type: "bind"
source: "%s"
target: "/src"
- type: "bind"
source: "%s"
target: "%s"
workingdir: "/src"
entrypoint:
- "/src/jx"
cmd:
- apply
- %s
wait: true
---
resources:
- type: container
transition: delete
attributes:
name: jx-client-resources-test
`
// create a container
// run a test inside the container
func TestUserResource(t *testing.T) {
ctx := context.Background()
c := NewClient()
assert.NotNil(t, c)
TempDir.Mkdir("testresources", 0700)
tmpresourcespath := TempDir.FilePath("testresources")
configurations := fmt.Sprintf(`
configurations:
- name: tmpdir
values:
prefix: %s
`, tmpresourcespath)
assert.Nil(t, TempDir.CreateFile("config.jx.yaml", configurations))
configURI := TempDir.URIPath("config.jx.yaml")
//assert.Nil(t, c.Import([]string{configURI}))
testUserFile := fmt.Sprintf(`
imports:
- %s
resources:
- type: file
config: tmpdir
transition: update
attributes:
path: testdir
mode: 0600
state: present
- type: group
transition: update
attributes:
name: testuser
- type: group
transition: update
attributes:
name: testgroup
- type: user
transition: update
attributes:
name: testuser
gecos: "my test account"
home: "/home/testuser"
createhome: true
group: testuser
groups:
- testgroup
- testuser
appendgroups: true
state: present
`, configURI)
assert.Nil(t, TempDir.CreateFile("test_userfile.jx.yaml", testUserFile))
for _, resourceTestDoc := range []string{
TempDir.FilePath("test_userfile.jx.yaml"),
} {
content := fmt.Sprintf(containerDoc, configURI, os.Getenv("WORKSPACE_PATH"), TempDir, TempDir, resourceTestDoc)
assert.Nil(t, TempDir.CreateFile("run-tests.jx.yaml", content))
runTestsDocument := TempDir.URIPath("run-tests.jx.yaml")
assert.Nil(t, c.Import([]string{runTestsDocument}))
assert.Nil(t, c.LoadDocumentImports())
assert.Nil(t, c.Apply(ctx, false))
applied, ok := folio.DocumentRegistry.GetDocument(folio.URI(runTestsDocument))
assert.True(t, ok)
cont := applied.ResourceDeclarations[0].Resource().(*resource.Container)
slog.Info("TestUserResources", "stdout", cont.Stdout, "stderr", cont.Stderr)
slog.Info("TestUserResources", "doc", applied, "container", applied.ResourceDeclarations[0])
assert.Equal(t, 0, len(applied.Errors))
assert.Greater(t, len(cont.Stdout), 0)
resultReader := io.NopCloser(strings.NewReader(cont.Stdout))
decoder := codec.NewDecoder(resultReader, codec.FormatYaml)
result := folio.NewDocument(nil)
assert.Nil(t, decoder.Decode(folio.NewDocument(nil)))
assert.Nil(t, decoder.Decode(result))
uri := fmt.Sprintf("file://%s", resourceTestDoc)
//testDoc := folio.DocumentRegistry.NewDocument(folio.URI(uri))
docs, loadErr := folio.DocumentRegistry.Load(folio.URI(uri))
assert.Nil(t, loadErr)
testDoc := docs[0]
var added int = 0
diffs, diffsErr := testDoc.(*folio.Document).Diff(result, nil)
assert.Nil(t, diffsErr)
assert.Greater(t, len(diffs), 1)
for _, line := range strings.Split(diffs, "\n") {
if len(line) > 0 {
switch line[0] {
case '+':
slog.Info("TestUserResources Diff", "line", line, "added", added)
added++
case '-':
assert.Fail(t, "resource attribute missing", line)
}
}
}
assert.Equal(t, 4, added)
}
}

View File

@ -3,56 +3,40 @@
package command package command
import ( import (
_ "context" _ "context"
"encoding/json"
"fmt" "fmt"
"errors" "errors"
"gopkg.in/yaml.v3"
"io" "io"
"log/slog" "log/slog"
_ "net/url" _ "net/url"
"os"
"os/exec" "os/exec"
"strings" "strings"
"text/template" "text/template"
"decl/internal/codec" "decl/internal/codec"
"syscall"
) )
// A resource that implements the ExecProvider interface can be used as an exec target.
type CommandProvider interface {
Start() error
Wait() error
SetCmdEnv([]string)
SetStdin(io.Reader)
StdinPipe() (io.WriteCloser, error)
StdoutPipe() (io.ReadCloser, error)
StderrPipe() (io.ReadCloser, error)
}
var ErrUnknownCommand error = errors.New("Unable to find command in path") var ErrUnknownCommand error = errors.New("Unable to find command in path")
type CommandExecutor func(value any) ([]byte, error) type CommandExecutor func(value any) ([]byte, error)
type CommandExtractAttributes func(output []byte, target any) error type CommandExtractAttributes func(output []byte, target any) error
type CommandExists func() error type CommandExists func() error
type CommandInput string type CommandArg string
type Command struct { type Command struct {
Path string `json:"path" yaml:"path"` Path string `json:"path" yaml:"path"`
Args []CommandArg `json:"args" yaml:"args"` Args []CommandArg `json:"args" yaml:"args"`
Env []string `json:"env" yaml:"env"` Env []string `json:"env" yaml:"env"`
Split bool `json:"split" yaml:"split"` Split bool `json:"split" yaml:"split"`
FailOnError bool `json:"failonerror" yaml:"failonerror"` FailOnError bool `json:"failonerror" yaml:"failonerror"`
StdinAvailable bool `json:"stdinavailable,omitempty" yaml:"stdinavailable,omitempty"` StdinAvailable bool `json:"stdinavailable,omitempty" yaml:"stdinavailable,omitempty"`
ExitCode int `json:"exitcode,omitempty" yaml:"exitcode,omitempty"` Executor CommandExecutor `json:"-" yaml:"-"`
Stdout string `json:"stdout,omitempty" yaml:"stdout,omitempty"` Extractor CommandExtractAttributes `json:"-" yaml:"-"`
Stderr string `json:"stderr,omitempty" yaml:"stderr,omitempty"` CommandExists CommandExists `json:"-" yaml:"-"`
Executor CommandExecutor `json:"-" yaml:"-"` stdin io.Reader `json:"-" yaml:"-"`
Extractor CommandExtractAttributes `json:"-" yaml:"-"`
CommandExists CommandExists `json:"-" yaml:"-"`
Input CommandInput `json:"-" yaml:"-"`
stdin io.Reader `json:"-" yaml:"-"`
TargetRef CommandTargetRef `json:"targetref,omitempty" yaml:"targetref,omitempty"`
execHandle CommandProvider `json:"-" yaml:"-"`
} }
func NewCommand() *Command { func NewCommand() *Command {
@ -61,14 +45,7 @@ func NewCommand() *Command {
return c return c
} }
func (c *Command) ClearOutput() {
c.Stdout = ""
c.Stderr = ""
c.ExitCode = 0
}
func (c *Command) Defaults() { func (c *Command) Defaults() {
c.ClearOutput()
c.Split = true c.Split = true
c.FailOnError = true c.FailOnError = true
c.CommandExists = func() error { c.CommandExists = func() error {
@ -78,21 +55,18 @@ func (c *Command) Defaults() {
return nil return nil
} }
c.Executor = func(value any) ([]byte, error) { c.Executor = func(value any) ([]byte, error) {
c.ClearOutput() args, err := c.Template(value)
if err != nil {
c.execHandle = c.TargetRef.Provider(c, value) return nil, err
if inputErr := c.SetInput(value); inputErr != nil {
return nil, inputErr
} }
cmd := exec.Command(c.Path, args...)
c.SetCmdEnv(cmd)
c.SetCmdEnv()
cmd := c.execHandle
if c.stdin != nil { if c.stdin != nil {
cmd.SetStdin(c.stdin) cmd.Stdin = c.stdin
} }
slog.Info("execute() - cmd", "path", c.Path, "args", args)
output, stdoutPipeErr := cmd.StdoutPipe() output, stdoutPipeErr := cmd.StdoutPipe()
if stdoutPipeErr != nil { if stdoutPipeErr != nil {
return nil, stdoutPipeErr return nil, stdoutPipeErr
@ -117,17 +91,11 @@ func (c *Command) Defaults() {
} }
waitErr := cmd.Wait() waitErr := cmd.Wait()
c.Stdout = string(stdOutOutput)
c.Stderr = string(stdErrOutput)
c.ExitCode = c.GetExitCodeFromError(waitErr)
/*
if len(stdOutOutput) > 100 { if len(stdOutOutput) > 100 {
slog.Info("execute()", "path", c.Path, "args", args, "output", string(stdOutOutput[:100]), "error", string(stdErrOutput)) slog.Info("execute()", "path", c.Path, "args", args, "output", string(stdOutOutput[:100]), "error", string(stdErrOutput))
} else { } else {
slog.Info("execute()", "path", c.Path, "args", args, "output", string(stdOutOutput), "error", string(stdErrOutput)) slog.Info("execute()", "path", c.Path, "args", args, "output", string(stdOutOutput), "error", string(stdErrOutput))
} }
*/
if len(stdErrOutput) > 0 && c.FailOnError { if len(stdErrOutput) > 0 && c.FailOnError {
return stdOutOutput, fmt.Errorf("%w %s", waitErr, string(stdErrOutput)) return stdOutOutput, fmt.Errorf("%w %s", waitErr, string(stdErrOutput))
@ -144,8 +112,8 @@ func (c *Command) LoadDecl(yamlResourceDeclaration string) error {
return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(c) return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(c)
} }
func (c *Command) SetCmdEnv() { func (c *Command) SetCmdEnv(cmd *exec.Cmd) {
c.execHandle.SetCmdEnv(c.Env) cmd.Env = append(os.Environ(), c.Env...)
} }
func (c *Command) SetStdinReader(r io.Reader) { func (c *Command) SetStdinReader(r io.Reader) {
@ -158,15 +126,6 @@ func (c *Command) Exists() bool {
return c.CommandExists() == nil return c.CommandExists() == nil
} }
func (c *Command) GetExitCodeFromError(err error) (ec int) {
if exitErr, ok := err.(*exec.ExitError); ok {
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
return status.ExitStatus()
}
}
return
}
func (c *Command) Template(value any) ([]string, error) { func (c *Command) Template(value any) ([]string, error) {
var args []string = make([]string, 0, len(c.Args) * 2) var args []string = make([]string, 0, len(c.Args) * 2)
for i, arg := range c.Args { for i, arg := range c.Args {
@ -195,20 +154,23 @@ func (c *Command) Execute(value any) ([]byte, error) {
return c.Executor(value) return c.Executor(value)
} }
func (c *Command) SetInput(value any) error { func (c *CommandArg) UnmarshalValue(value string) error {
if len(c.Input) > 0 { *c = CommandArg(value)
if r, err := c.Input.Template(value); err != nil {
slog.Info("Command.SetInput", "input", r.String(), "error", err)
return err
} else {
slog.Info("Command.SetInput", "input", r.String())
c.SetStdinReader(strings.NewReader(r.String()))
}
}
return nil return nil
} }
func (c *CommandInput) Template(value any) (result strings.Builder, err error) { func (c *CommandArg) UnmarshalJSON(data []byte) error {
err = template.Must(template.New("commandInput").Parse(string(*c))).Execute(&result, value) var s string
return if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
return unmarshalRouteTypeErr
}
return c.UnmarshalValue(s)
}
func (c *CommandArg) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
return c.UnmarshalValue(s)
} }

View File

@ -81,22 +81,3 @@ stdinavailable: true
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, expected, string(out)) assert.Equal(t, expected, string(out))
} }
func TestCommandExitCode(t *testing.T) {
c := NewCommand()
assert.NotNil(t, c)
decl := `
path: ls
args:
- "amissingfile"
`
assert.Nil(t, c.LoadDecl(decl))
assert.Equal(t, "ls", c.Path)
out, err := c.Execute(nil)
assert.NotNil(t, err)
assert.Greater(t, c.ExitCode, 0)
assert.Equal(t, string(out), c.Stdout)
assert.Equal(t, string("ls: amissingfile: No such file or directory\n"), c.Stderr)
}

View File

@ -1,31 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package command
import (
"encoding/json"
"gopkg.in/yaml.v3"
)
type CommandArg string
func (c *CommandArg) UnmarshalValue(value string) error {
*c = CommandArg(value)
return nil
}
func (c *CommandArg) UnmarshalJSON(data []byte) error {
var s string
if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
return unmarshalRouteTypeErr
}
return c.UnmarshalValue(s)
}
func (c *CommandArg) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
return c.UnmarshalValue(s)
}

View File

@ -1,30 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package command
import (
"path/filepath"
"decl/internal/identifier"
)
type CommandTargetRef identifier.ID
func (r CommandTargetRef) GetType() (CommandType) {
uri := identifier.ID(r).Parse()
if uri != nil {
var ct CommandType = CommandType(uri.Scheme)
if ct.Validate() == nil {
return ct
}
}
return ""
}
func (r CommandTargetRef) Provider(cmd *Command, value any) CommandProvider {
return r.GetType().Provider(cmd, value)
}
func (r CommandTargetRef) Name() string {
u := identifier.ID(r).Parse()
return filepath.Join(u.Hostname(), u.Path)
}

View File

@ -1,44 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package command
import (
"errors"
"fmt"
)
var (
ErrInvalidCommandType error = errors.New("Invalid command type")
)
type CommandType string
const (
ExecCommand CommandType = "exec"
ContainerCommand CommandType = "container"
SSHCommand CommandType = "ssh"
)
func (t CommandType) Validate() error {
switch t {
case ExecCommand, ContainerCommand, SSHCommand:
return nil
}
return fmt.Errorf("%w: %s", ErrInvalidCommandType, t)
}
func (t CommandType) Provider(cmd *Command, value any) CommandProvider {
switch t {
case ContainerCommand:
return NewContainerProvider(cmd, value)
case SSHCommand:
return NewSSHProvider(cmd, value)
default:
fallthrough
case ExecCommand:
return NewExecProvider(cmd, value)
}
return nil
}

View File

@ -1,18 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package command
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestCommandType(t *testing.T) {
var cmdType CommandType = "container"
assert.Nil(t, cmdType.Validate())
}

View File

@ -1,104 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package command
import (
_ "fmt"
"github.com/stretchr/testify/assert"
_ "os"
_ "strings"
"testing"
"bytes"
)
/*
func TestNewContainerProvider(t *testing.T) {
c := NewContainerProvider(nil, nil)
assert.NotNil(t, c)
}
*/
func TestContainerCommandLoad(t *testing.T) {
c := NewCommand()
assert.NotNil(t, c)
decl := `
path: find
args:
- "{{ .Path }}"
`
assert.Nil(t, c.LoadDecl(decl))
assert.Equal(t, "find", c.Path)
}
func TestContainerCommandTemplate(t *testing.T) {
c := NewCommand()
assert.NotNil(t, c)
decl := `
path: find
args:
- "{{ .Path }}"
`
assert.Nil(t, c.LoadDecl(decl))
assert.Equal(t, "find", c.Path)
assert.Equal(t, 1, len(c.Args))
f := struct { Path string } {
Path: "./",
}
args, templateErr := c.Template(f)
assert.Nil(t, templateErr)
assert.Equal(t, 1, len(args))
assert.Equal(t, "./", string(args[0]))
out, err := c.Execute(f)
assert.Nil(t, err)
assert.Greater(t, len(out), 0)
}
func TestContainerCommandStdin(t *testing.T) {
var expected string = "stdin test data"
var stdinBuffer bytes.Buffer
stdinBuffer.WriteString(expected)
c := NewCommand()
assert.NotNil(t, c)
decl := `
path: cat
stdinavailable: true
`
assert.Nil(t, c.LoadDecl(decl))
assert.Equal(t, "cat", c.Path)
c.SetStdinReader(&stdinBuffer)
out, err := c.Execute(nil)
assert.Nil(t, err)
assert.Equal(t, expected, string(out))
}
func TestContainerCommandExitCode(t *testing.T) {
c := NewCommand()
assert.NotNil(t, c)
decl := `
path: ls
args:
- "amissingfile"
`
assert.Nil(t, c.LoadDecl(decl))
assert.Equal(t, "ls", c.Path)
out, err := c.Execute(nil)
assert.NotNil(t, err)
assert.Greater(t, c.ExitCode, 0)
assert.Equal(t, string(out), c.Stdout)
assert.Equal(t, string("ls: amissingfile: No such file or directory\n"), c.Stderr)
}

View File

@ -1,142 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package command
import (
"context"
"fmt"
"io"
"os"
"decl/internal/containerlog"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"time"
"log/slog"
)
type ContainerExecClient interface {
ContainerExecAttach(ctx context.Context, execID string, config container.ExecAttachOptions) (types.HijackedResponse, error)
ContainerExecCreate(ctx context.Context, containerID string, options container.ExecOptions) (types.IDResponse, error)
ContainerExecInspect(ctx context.Context, execID string) (container.ExecInspect, error)
ContainerExecStart(ctx context.Context, execID string, config container.ExecStartOptions) (error)
ContainerList(context.Context, container.ListOptions) ([]types.Container, error)
Close() error
}
type ContainerCommandProvider struct {
containerID string
execID string
ExitCode int
container.ExecOptions
response types.HijackedResponse
pipes *containerlog.StreamReader
Stdin io.Reader
apiClient ContainerExecClient `json:"-" yaml:"-"`
}
// ref could be a resource, but it just needs to implement the ExecResource interface
func NewContainerProvider(cmd *Command, value any) (p *ContainerCommandProvider) {
if args, err := cmd.Template(value); err == nil {
p = &ContainerCommandProvider {
ExecOptions: container.ExecOptions {
Cmd: strslice.StrSlice(append([]string{cmd.Path}, args...)),
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
},
}
p.containerID = p.ResolveId(context.Background(), cmd.TargetRef)
slog.Info("command.NewContainerProvider", "command", cmd.Path, "args", args, "target", cmd.TargetRef, "container", p.containerID)
}
return
}
func (c *ContainerCommandProvider) ResolveId(ctx context.Context, ref CommandTargetRef) (containerID string) {
name := ref.Name()
filterArgs := filters.NewArgs()
filterArgs.Add("name", "/"+name)
containers, listErr := c.apiClient.ContainerList(ctx, container.ListOptions{
All: true,
Filters: filterArgs,
})
if listErr != nil {
panic(listErr)
}
for _, container := range containers {
for _, containerName := range container.Names {
if containerName == "/"+name {
containerID = container.ID
}
}
}
return
}
func (c *ContainerCommandProvider) Start() (err error) {
var execIDResponse types.IDResponse
ctx := context.Background()
if execIDResponse, err = c.apiClient.ContainerExecCreate(ctx, c.containerID, c.ExecOptions); err == nil {
c.execID = execIDResponse.ID
if c.execID == "" {
return fmt.Errorf("Failed creating a container exec ID")
}
execStartCheck := types.ExecStartCheck{
Tty: false,
}
if c.response, err = c.apiClient.ContainerExecAttach(ctx, c.execID, execStartCheck); err == nil {
c.pipes = containerlog.NewStreamReader(c.response.Conn)
}
}
return
}
func (c *ContainerCommandProvider) Wait() (err error) {
var containerDetails container.ExecInspect
ctx := context.Background()
for {
// copy Stdin to the connection
if c.Stdin != nil {
io.Copy(c.response.Conn, c.Stdin)
}
if containerDetails, err = c.apiClient.ContainerExecInspect(ctx, c.execID); err != nil || ! containerDetails.Running {
c.ExitCode = containerDetails.ExitCode
break
} else {
time.Sleep(500 * time.Millisecond)
}
}
return
}
func (c *ContainerCommandProvider) SetCmdEnv(env []string) {
c.Env = append(os.Environ(), env...)
}
func (c *ContainerCommandProvider) SetStdin(r io.Reader) {
c.Stdin = r
}
func (c *ContainerCommandProvider) StdinPipe() (io.WriteCloser, error) {
return c.response.Conn, nil
}
func (c *ContainerCommandProvider) StdoutPipe() (io.ReadCloser, error) {
return c.pipes.StdoutPipe(), nil
}
func (c *ContainerCommandProvider) StderrPipe() (io.ReadCloser, error) {
return c.pipes.StderrPipe(), nil
}
func (c *ContainerCommandProvider) Close() (err error) {
err = c.response.CloseWrite()
c.response.Close()
return
}

View File

@ -1,34 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package command
import (
"os"
"os/exec"
"io"
"log/slog"
)
var (
)
type ExecCommandProvider struct {
*exec.Cmd
}
// Consturct a new exec
func NewExecProvider(c *Command, value any) *ExecCommandProvider {
if args, err := c.Template(value); err == nil {
slog.Info("command.NewExecProvider", "command", c.Path, "args", args, "target", c.TargetRef)
return &ExecCommandProvider{exec.Command(c.Path, args...)}
}
return nil
}
func (e *ExecCommandProvider) SetCmdEnv(env []string) {
e.Cmd.Env = append(os.Environ(), env...)
}
func (e *ExecCommandProvider) SetStdin(r io.Reader) {
e.Cmd.Stdin = r
}

View File

@ -1,54 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package command
import (
"io"
"os"
"log/slog"
)
var (
)
type SSHCommandProvider struct {
Env []string
Stdin io.Reader
}
// Consturct a new ssh exec
func NewSSHProvider(c *Command, value any) (p *SSHCommandProvider) {
if args, err := c.Template(value); err == nil {
slog.Info("command.NewSSHProvider", "command", c.Path, "args", args, "target", c.TargetRef)
}
return nil
}
func (s *SSHCommandProvider) Start() error {
return nil
}
func (s *SSHCommandProvider) Wait() error {
return nil
}
func (s *SSHCommandProvider) StdinPipe() (w io.WriteCloser, err error) {
return
}
func (s *SSHCommandProvider) StdoutPipe() (r io.ReadCloser, err error) {
return
}
func (s *SSHCommandProvider) StderrPipe() (r io.ReadCloser, err error) {
return
}
func (s *SSHCommandProvider) SetCmdEnv(env []string) {
s.Env = append(os.Environ(), env...)
}
func (s *SSHCommandProvider) SetStdin(r io.Reader) {
s.Stdin = r
}

View File

@ -37,7 +37,7 @@ func (c *Certificate) SetURI(uri string) error {
return nil return nil
} }
func (c *Certificate) SetParsedURI(uri data.URIParser) error { func (c *Certificate) SetParsedURI(uri *url.URL) error {
return nil return nil
} }

View File

@ -39,7 +39,7 @@ func (x *Exec) SetURI(uri string) error {
return nil return nil
} }
func (x *Exec) SetParsedURI(uri data.URIParser) error { func (x *Exec) SetParsedURI(uri *url.URL) error {
return nil return nil
} }

View File

@ -35,7 +35,7 @@ func (g *Generic[Value]) SetURI(uri string) error {
return nil return nil
} }
func (g *Generic[Value]) SetParsedURI(uri data.URIParser) error { func (g *Generic[Value]) SetParsedURI(uri *url.URL) error {
return nil return nil
} }

View File

@ -1,90 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"crypto/x509"
"crypto/x509/pkix"
"crypto/rsa"
"crypto/rand"
"encoding/pem"
"encoding/json"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/packet"
)
type OpenPGP struct {
Armored string
entities openpgp.EntityList
}
func (o *OpenPGP) Read() (yamlData []byte, err error) {
pemReader := io.NopCloser(strings.NewReader(o.Armored))
o.entities, err = openpgp.ReadArmoredKeyRing(pemReader)
return
}
func (o *OpenPGP) UnmarshalJSON(data []byte) error {
if unmarshalErr := json.Unmarshal(data, o); unmarshalErr != nil {
return unmarshalErr
}
return nil
}
func (o *OpenPGP) UnmarshalYAML(value *yaml.Node) error {
type decodeOpenPGP OpenPGP
if unmarshalErr := value.Decode((*decodeOpenPGP)(o)); unmarshalErr != nil {
return unmarshalErr
}
return nil
}
func (o *OpenPGP) Clone() data.Configuration {
jsonGeneric, _ := json.Marshal(c)
clone := NewOpenPGP()
if unmarshalErr := json.Unmarshal(jsonGeneric, &clone); unmarshalErr != nil {
panic(unmarshalErr)
}
return clone
}
func (o *OpenPGP) Type() string {
return "openpgp"
}
func (o *OpenPGP) GetEntityIndex(key string) (index int, field string, err error) {
values := strings.SplitN(key, ".", 2)
if len(values) == 2 {
if index, err = strconv.Atoi(values[0]); err == nil {
field = values[1]
}
} else {
err = data.ErrUnknownConfigurationKey
}
return
}
func (o *OpenPGP) GetValue(name string) (result any, err error) {
var ok bool
if result, ok = (*c)[name]; !ok {
err = data.ErrUnknownConfigurationKey
}
return
}
// Expected key: 0.PrivateKey
func (o *OpenPGP) Has(key string) (ok bool) {
index, field, err := o.GetEntityIndex(key)
if len(o.entities) > index && err == nil {
switch key {
case PublicKey:
ok = o.entities[index].PrimaryKey != nil
case PrivateKey:
ok = o.entities[index].PrimaryKey != nil
}
}
return
}

View File

@ -1,31 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"github.com/stretchr/testify/assert"
"testing"
"crypto/x509"
)
func TestNewOpenPGPConfig(t *testing.T) {
p := NewOpenPGP()
assert.NotNil(t, p)
}
func TestNewOpenPGPConfigYAML(t *testing.T) {
p := NewOpenPGP()
assert.NotNil(t, p)
config := `
openpgp:
publickey:
`
yamlErr := c.LoadYAML(config)
assert.Nil(t, yamlErr)
crt, err := c.GetValue("catemplate")
assert.Nil(t, err)
assert.Equal(t, []string{"RKH"}, crt.(*x509.Certificate).Subject.Organization)
}

View File

@ -10,7 +10,6 @@ import (
"decl/internal/data" "decl/internal/data"
"decl/internal/folio" "decl/internal/folio"
"runtime" "runtime"
"decl/internal/system"
) )
// Collects facts about the system // Collects facts about the system
@ -36,24 +35,9 @@ func NewSystem() *System {
for k, v := range buildValues { for k, v := range buildValues {
s[k] = v s[k] = v
} }
s.CurrentUser()
s["importpath"] = []string {
"/etc/jx/lib",
}
return &s return &s
} }
func (s *System) CurrentUser() {
processUser := system.ProcessUser()
processGroup := system.ProcessGroup(processUser)
(*s)["user"] = processUser.Username
(*s)["gecos"] = processUser.Name
(*s)["home"] = processUser.HomeDir
(*s)["uid"] = processUser.Uid
(*s)["group"] = processGroup.Name
(*s)["gid"] = processUser.Gid
}
func (s *System) URI() string { func (s *System) URI() string {
return fmt.Sprintf("%s://%s", s.Type(), "") return fmt.Sprintf("%s://%s", s.Type(), "")
} }
@ -62,7 +46,7 @@ func (s *System) SetURI(uri string) error {
return nil return nil
} }
func (s *System) SetParsedURI(uri data.URIParser) error { func (s *System) SetParsedURI(uri *url.URL) error {
return nil return nil
} }

View File

@ -1,39 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package containerlog
import (
"io"
"encoding/binary"
)
func Header(r io.Reader) (s StreamType, size uint64, err error) {
var header []byte = make([]byte, 8)
if _, err = io.ReadFull(r, header); err == nil {
s = StreamType(header[0])
if err = s.Validate(); err == nil {
header[0] = 0x0
size = binary.BigEndian.Uint64(header)
}
}
return
}
func ReadMessage(r io.Reader, size uint64) (message string, err error) {
var messageData []byte = make([]byte, size)
var bytesRead int
if bytesRead, err = r.Read(messageData); err == nil && uint64(bytesRead) == size {
message = string(messageData)
}
return
}
func Read(r io.Reader) (s StreamType, message string, err error) {
var messageSize uint64
if s, messageSize, err = Header(r); err == nil {
if message, err = ReadMessage(r, messageSize); err == nil {
return
}
}
return
}

View File

@ -1,28 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package containerlog
import (
"github.com/stretchr/testify/assert"
"testing"
"bytes"
)
func TestLogHeader(t *testing.T) {
for _, v := range []struct{ expected error; value []byte } {
{ expected: nil, value: StreamStdout.Log("test message") },
{ expected: nil, value: StreamStderr.Log("test error") },
{ expected: ErrInvalidStreamType, value: StreamType(0x3).Log("fail") },
{ expected: ErrInvalidStreamType, value: StreamType(0x4).Log("fail") },
} {
var buf bytes.Buffer
_, e := buf.Write(v.value)
assert.Nil(t, e)
logType, logSize, err := Header(&buf)
assert.ErrorIs(t, err, v.expected)
assert.ErrorIs(t, logType.Validate(), v.expected)
if err == nil {
assert.Equal(t, uint64(len(v.value) - 8), logSize)
}
}
}

View File

@ -1,74 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package containerlog
import (
"bytes"
"io"
)
type StreamReader struct {
source io.Reader
streams []*ReadBuffer
}
type ReadBuffer struct {
streamtype StreamType
reader *StreamReader
bytes.Buffer
}
func NewStreamReader(source io.Reader) (s *StreamReader) {
s = &StreamReader{
source: source,
streams: make([]*ReadBuffer, 3),
}
s.streams[StreamStdout] = &ReadBuffer{
streamtype: StreamStdout,
reader: s,
}
s.streams[StreamStderr] = &ReadBuffer{
streamtype: StreamStderr,
reader: s,
}
return
}
func (s *StreamReader) StdoutPipe() io.ReadCloser {
return s.streams[StreamStdout]
}
func (s *StreamReader) StderrPipe() io.ReadCloser {
return s.streams[StreamStderr]
}
func (b *ReadBuffer) Read(p []byte) (n int, e error) {
for {
if b.reader.streams[b.streamtype].Len() >= len(p) {
break
}
streamtype, message, err := Read(b.reader.source)
if err != nil {
break
}
if b.reader.streams[streamtype] == nil {
b.reader.streams[streamtype] = &ReadBuffer{
streamtype: streamtype,
reader: b.reader,
}
}
if bytesRead, bufferErr := b.reader.streams[streamtype].WriteString(message); bytesRead != len(message) || bufferErr != nil {
break
}
}
return b.Buffer.Read(p)
}
func (b *ReadBuffer) Close() error {
return nil
}

View File

@ -1,35 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package containerlog
import (
"github.com/stretchr/testify/assert"
"testing"
"bytes"
"io"
)
func TestStreamReader(t *testing.T) {
var logs bytes.Buffer
logs.Write(StreamStdout.Log("stdout log message"))
logs.Write(StreamStderr.Log("stderr log message"))
logs.Write(StreamStdout.Log("stdout log message - line 2"))
logs.Write(StreamStderr.Log("stderr log message - line 2"))
logs.Write(StreamStderr.Log("stderr log message - line 3"))
sr := NewStreamReader(&logs)
outpipe := sr.StdoutPipe()
errpipe := sr.StderrPipe()
var message []byte = make([]byte, 20)
n, ee := errpipe.Read(message)
assert.Nil(t, ee)
assert.Equal(t, 20, n)
assert.Equal(t, "stderr log messagest", string(message))
ov, oe := io.ReadAll(outpipe)
assert.Nil(t, oe)
assert.Equal(t, "stdout log messagestdout log message - line 2", string(ov))
}

View File

@ -1,38 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package containerlog
import (
"fmt"
"errors"
"encoding/binary"
)
type StreamType byte
const (
StreamStdin StreamType = 0x0
StreamStdout StreamType = 0x1
StreamStderr StreamType = 0x2
)
var (
ErrInvalidStreamType error = errors.New("Invalid container log stream type")
)
func (s StreamType) Validate() error {
switch s {
case StreamStdin, StreamStdout, StreamStderr:
return nil
}
return fmt.Errorf("%w: %d", ErrInvalidStreamType, s)
}
func (s StreamType) Log(msg string) (log []byte) {
msgLen := len(msg)
log = make([]byte, 8 + msgLen)
binary.BigEndian.PutUint64(log, uint64(msgLen))
log[0] = byte(s)
copy(log[8:], []byte(msg))
return
}

View File

@ -1,20 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package containerlog
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestStreamType(t *testing.T) {
for _, v := range []struct{ expected error; value StreamType } {
{ expected: nil, value: 0x0 },
{ expected: nil, value: 0x1 },
{ expected: nil, value: 0x2 },
{ expected: ErrInvalidStreamType, value: 0x3 },
{ expected: ErrInvalidStreamType, value: 0x4 },
} {
assert.ErrorIs(t, v.value.Validate(), v.expected)
}
}

View File

@ -1,27 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package data
import (
)
var (
)
type CommandExecutor interface {
Execute(value any) ([]byte, error)
}
type CommandOutputExtractor interface {
Extract(output []byte, target any) error
}
type CommandChecker interface {
Exists() error
}
type Commander interface {
CommandExecutor
CommandOutputExtractor
CommandChecker
}

View File

@ -8,6 +8,7 @@ import (
"decl/internal/codec" "decl/internal/codec"
"io" "io"
"decl/internal/mapper" "decl/internal/mapper"
"net/url"
) )
var ( var (
@ -43,7 +44,7 @@ type Document interface {
mapper.Mapper mapper.Mapper
NewResource(uri string) (Resource, error) NewResource(uri string) (Resource, error)
NewResourceFromParsedURI(uri URIParser) (Resource, error) NewResourceFromParsedURI(uri *url.URL) (Resource, error)
AddDeclaration(Declaration) AddDeclaration(Declaration)
AddResourceDeclaration(resourceType string, resourceDeclaration Resource) AddResourceDeclaration(resourceType string, resourceDeclaration Resource)

View File

@ -5,34 +5,16 @@ package data
import ( import (
"errors" "errors"
"net/url" "net/url"
"decl/internal/transport"
) )
var ( var (
ErrInvalidURI error = errors.New("Invalid URI") ErrInvalidURI error = errors.New("Invalid URI")
) )
type URIParser interface {
URL() *url.URL
NewResource(document Document) (newResource Resource, err error)
ConstructResource(res Resource) (err error)
Converter() (converter Converter, err error)
Exists() bool
ContentReaderStream() (*transport.Reader, error)
ContentWriterStream() (*transport.Writer, error)
String() string
SetURL(url *url.URL)
Extension() (string, string)
ContentType() string
IsEmpty() bool
}
type Identifier interface { type Identifier interface {
URI() string URI() string
SetParsedURI(URIParser) error SetURI(string) error
SetParsedURI(*url.URL) error
} }
type DocumentElement interface { type DocumentElement interface {

View File

@ -22,11 +22,6 @@ type StateTransformer interface {
Apply() error Apply() error
} }
// Used by the resource factory to initialize new resources.
type ResourceInitializer interface {
Init(uri URIParser) error
}
type Resource interface { type Resource interface {
Identifier Identifier
Type() string Type() string
@ -106,13 +101,6 @@ type FileResource interface {
SetGzipContent(bool) SetGzipContent(bool)
} }
type ExecResource interface {
Start() error
Wait() error
StdoutPipe() (io.ReadCloser, error)
StderrPipe() (io.ReadCloser, error)
}
type Signed interface { type Signed interface {
Signature() Signature Signature() Signature
} }

View File

@ -11,7 +11,6 @@ type Factory[Product comparable] func(*url.URL) Product
type TypesRegistry[Product comparable] interface { type TypesRegistry[Product comparable] interface {
New(uri string) (result Product, err error) New(uri string) (result Product, err error)
NewFromParsedURI(uri *url.URL) (result Product, err error) NewFromParsedURI(uri *url.URL) (result Product, err error)
NewFromType(typename string) (result Product, err error)
Has(typename string) bool Has(typename string) bool
//Get(string) Factory[Product] //Get(string) Factory[Product]
} }

View File

@ -1,63 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package ds
import (
"log/slog"
)
type OrderedSet[Value comparable] struct {
Values []*Value
elements map[Value]int
}
func NewOrderedSet[Value comparable]() *OrderedSet[Value] {
return &OrderedSet[Value]{ elements: make(map[Value]int), Values: make([]*Value, 0, 10) }
}
func (s *OrderedSet[Value]) Add(value Value) {
slog.Info("OrderedSet.Add", "key", value, "s", s)
s.Values = append(s.Values, &value)
s.elements[value] = len(s.Values)
slog.Info("OrderedSet.Add", "key", value, "s", s, "v", &s.Values)
}
func (s *OrderedSet[Value]) Delete(key Value) {
slog.Info("OrderedSet.Delete", "key", key, "s", s, "size", len(s.Values))
if i, ok := s.elements[key]; ok {
i--
s.Values[i] = nil
delete(s.elements, key)
}
}
func (s *OrderedSet[Value]) Contains(value Value) (result bool) {
slog.Info("OrderedSet.Contains", "key", value, "s", s, "size", len(s.Values), "v", &s.Values)
_, result = s.elements[value]
return
}
func (s *OrderedSet[Value]) Len() int {
return len(s.elements)
}
func (s *OrderedSet[Value]) AddItems(value []Value) {
for _, v := range value {
s.Add(v)
}
}
func (s *OrderedSet[Value]) Items() []*Value {
slog.Info("OrderedSet.Items - start", "s", s)
result := make([]*Value, 0, len(s.elements) - 1)
for _, v := range s.Values {
slog.Info("OrderedSet.Items", "value", v)
if v != nil {
result = append(result, v)
s.elements[*v] = len(result)
}
}
slog.Info("OrderedSet.Items", "s", s, "result", result)
s.Values = result
return result
}

View File

@ -1,51 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package ds
import (
"github.com/stretchr/testify/assert"
"testing"
"log/slog"
)
var (
)
func TestNewOrderedSet(t *testing.T) {
s := NewOrderedSet[string]()
assert.NotNil(t, s)
testValues := []string{
"foo",
"bar",
"baz",
"quuz",
}
for _,value := range testValues {
s.Add(value)
slog.Info("TestNewOrderedSet - ADD", "item", value, "s", s)
assert.True(t, s.Contains(value))
slog.Info("TestNewOrderedSet - CONTAINS", "s", s)
for x, item := range s.Items() {
slog.Info("TestNewOrderedSet", "item", item, "s", s)
assert.Equal(t, testValues[x], *item)
}
}
s.Delete("bar")
expectedValues := []string {
"foo",
"baz",
"quuz",
}
for x, item := range s.Items() {
assert.Equal(t, expectedValues[x], *item)
}
}

View File

@ -1,34 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package ds
import (
)
type Set[Value comparable] map[Value]bool
func NewSet[Value comparable]() Set[Value] {
return make(map[Value]bool)
}
func (s Set[Value]) Add(value Value) {
s[value] = true
}
func (s Set[Value]) Delete(value Value) {
delete(s, value)
}
func (s Set[Value]) Contains(value Value) bool {
return s[value]
}
func (s Set[Value]) Len() int {
return len(s)
}
func (s Set[Value]) AddSlice(value []Value) {
for _, v := range value {
s.Add(v)
}
}

View File

@ -1,24 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package ds
import (
"github.com/stretchr/testify/assert"
"testing"
)
var (
)
func TestNewSet(t *testing.T) {
s := NewSet[string]()
assert.NotNil(t, s)
s["foo"] = true
assert.True(t, s.Contains("foo"))
s.Add("bar")
assert.True(t, s.Contains("bar"))
}

View File

@ -1,26 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package ext
import (
"os"
"path/filepath"
)
type FilePath string
func (f *FilePath) Exists() bool {
_, err := os.Stat(string(*f))
return !os.IsNotExist(err)
}
func (f *FilePath) Add(relative string) {
newPath := filepath.Join(string(*f), relative)
*f = FilePath(newPath)
}
func (f FilePath) Abs() FilePath {
result, _ := filepath.Abs(string(f))
return FilePath(result)
}

View File

@ -105,6 +105,12 @@ func (d *Dir) Emit(document data.Document, filter data.ElementSelector) (resourc
return nil, ErrEmptyDocument return nil, ErrEmptyDocument
} }
dirFileDeclaration := folio.NewDeclaration()
dirFileDeclaration.Type = "file"
if err = dirFileDeclaration.NewResource(nil); err != nil {
return
}
parentPaths := make(map[string]int) parentPaths := make(map[string]int)
var containingDirectoryPath string var containingDirectoryPath string
for _,res := range document.Filter(func(d data.Declaration) bool { for _,res := range document.Filter(func(d data.Declaration) bool {
@ -124,12 +130,8 @@ func (d *Dir) Emit(document data.Document, filter data.ElementSelector) (resourc
containingDirectoryPath, _ = d.isParent(&parentPaths, parent, containingDirectoryPath) containingDirectoryPath, _ = d.isParent(&parentPaths, parent, containingDirectoryPath)
} }
uri := fmt.Sprintf("file://%s", containingDirectoryPath) uri := fmt.Sprintf("file://%s", containingDirectoryPath)
if err = dirFileDeclaration.SetURI(uri); err != nil {
dirFileDeclaration := folio.NewDeclaration()
dirFileDeclaration.Type = "file"
if err = dirFileDeclaration.NewResource(&uri); err != nil {
return return
} }

View File

@ -120,10 +120,8 @@ func (j *JxFile) setdecoder(source data.ContentIdentifier) {
for _,v := range strings.Split(source.ContentType(), ".") { for _,v := range strings.Split(source.ContentType(), ".") {
_ = j.Format.Set(v) _ = j.Format.Set(v)
} }
slog.Info("JxFile.setdecoder()", "type", source.ContentType(), "format", j.Format)
j.decoder = codec.NewDecoder(j.reader, j.Format) j.decoder = codec.NewDecoder(j.reader, j.Format)
} }
slog.Info("JxFile.setdecoder()", "decoder", j.decoder)
} }
func (j *JxFile) Type() data.TypeName { return "jx" } func (j *JxFile) Type() data.TypeName { return "jx" }
@ -181,7 +179,6 @@ func (j *JxFile) ExtractMany(resourceSource data.Resource, filter data.ElementSe
} }
break break
} }
slog.Info("JxFile.ExtractMany() loading", "document", j.index)
} }
slog.Info("JxFile.ExtractMany()", "jxfile", j, "error", err) slog.Info("JxFile.ExtractMany()", "jxfile", j, "error", err)
return return
@ -189,7 +186,7 @@ func (j *JxFile) ExtractMany(resourceSource data.Resource, filter data.ElementSe
func (j *JxFile) targetResource() (target data.Resource, err error) { func (j *JxFile) targetResource() (target data.Resource, err error) {
if j.emitResource == nil { if j.emitResource == nil {
targetUrl := j.Uri.Parse().URL() targetUrl := j.Uri.Parse()
targetUrl.Scheme = "file" targetUrl.Scheme = "file"
q := targetUrl.Query() q := targetUrl.Query()
q.Set("format", string(j.Format)) q.Set("format", string(j.Format))

View File

@ -15,32 +15,6 @@ import (
"decl/internal/schema" "decl/internal/schema"
) )
type ConfigKey string
// Lookup a config value using block.key identifier (E.g. system.GOOS)
func (c ConfigKey) GetValue() (value any, err error) {
fields := strings.SplitN(string(c), ".", 2)
if configBlock, ok := DocumentRegistry.ConfigNameMap.Get(fields[0]); ok && len(fields) > 1 {
return configBlock.GetValue(fields[1])
} else {
return nil, fmt.Errorf("%w - %s", data.ErrUnknownConfigurationType, c)
}
}
func (c ConfigKey) Get() any {
if v, err := c.GetValue(); err == nil {
return v
}
return nil
}
func (c ConfigKey) GetStringSlice() []string {
if v, err := c.GetValue(); err == nil {
return v.([]string)
}
return nil
}
type BlockType struct { type BlockType struct {
Name string `json:"name" yaml:"name"` Name string `json:"name" yaml:"name"`
Type TypeName `json:"type" yaml:"type"` Type TypeName `json:"type" yaml:"type"`
@ -161,9 +135,9 @@ func (b *Block) SetURI(uri string) (err error) {
return return
} }
func (b *Block) SetParsedURI(uri data.URIParser) (err error) { func (b *Block) SetParsedURI(uri *url.URL) (err error) {
if b.Values == nil { if b.Values == nil {
if err = b.NewConfigurationFromParsedURI(uri.URL()); err != nil { if err = b.NewConfigurationFromParsedURI(uri); err != nil {
return return
} }
} }

View File

@ -11,10 +11,12 @@ _ "errors"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"log/slog" "log/slog"
_ "gitea.rosskeen.house/rosskeen.house/machine" _ "gitea.rosskeen.house/rosskeen.house/machine"
"gitea.rosskeen.house/pylon/luaruntime" //_ "gitea.rosskeen.house/pylon/luaruntime"
"decl/internal/codec" "decl/internal/codec"
"decl/internal/data" "decl/internal/data"
"decl/internal/schema" "decl/internal/schema"
"net/url"
"runtime/debug"
"errors" "errors"
) )
@ -31,7 +33,6 @@ type DeclarationType struct {
OnError OnError `json:"onerror,omitempty" yaml:"onerror,omitempty"` OnError OnError `json:"onerror,omitempty" yaml:"onerror,omitempty"`
Error string `json:"error,omitempty" yaml:"error,omitempty"` Error string `json:"error,omitempty" yaml:"error,omitempty"`
Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"` Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"`
On *Events `json:"on,omitempty" yaml:"on,omitempty"`
} }
type Declaration struct { type Declaration struct {
@ -42,15 +43,14 @@ type Declaration struct {
OnError OnError `json:"onerror,omitempty" yaml:"onerror,omitempty"` OnError OnError `json:"onerror,omitempty" yaml:"onerror,omitempty"`
Error string `json:"error,omitempty" yaml:"error,omitempty"` Error string `json:"error,omitempty" yaml:"error,omitempty"`
Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"` Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"`
On *Events `json:"on,omitempty" yaml:"on,omitempty"` // runtime luaruntime.LuaRunner
runtime luaruntime.LuaRunner
document *Document document *Document
configBlock data.Block configBlock data.Block
ResourceTypes data.TypesRegistry[data.Resource] `json:"-" yaml:"-"` ResourceTypes data.TypesRegistry[data.Resource] `json:"-" yaml:"-"`
} }
func NewDeclaration() *Declaration { func NewDeclaration() *Declaration {
return &Declaration{ ResourceTypes: DocumentRegistry.ResourceTypes, runtime: luaruntime.New() } return &Declaration{ ResourceTypes: DocumentRegistry.ResourceTypes }
} }
func NewDeclarationFromDocument(document *Document) *Declaration { func NewDeclarationFromDocument(document *Document) *Declaration {
@ -98,7 +98,6 @@ func (d *Declaration) Clone() data.Declaration {
//runtime: luaruntime.New(), //runtime: luaruntime.New(),
Config: d.Config, Config: d.Config,
Requires: d.Requires, Requires: d.Requires,
On: d.On,
} }
} }
@ -138,12 +137,11 @@ func (d *Declaration) Validate() (err error) {
return err return err
} }
func (d *Declaration) NewResourceFromParsedURI(u data.URIParser) (err error) { func (d *Declaration) NewResourceFromParsedURI(u *url.URL) (err error) {
if u == nil { if u == nil {
d.Attributes, err = d.ResourceTypes.NewFromType(string(d.Type)) d.Attributes, err = d.ResourceTypes.NewFromParsedURI(&url.URL{ Scheme: string(d.Type) })
} else { } else {
parsed := u.URL() if d.Attributes, err = d.ResourceTypes.NewFromParsedURI(u); err == nil {
if d.Attributes, err = d.ResourceTypes.NewFromParsedURI(parsed); err == nil {
err = d.Attributes.SetParsedURI(u) err = d.Attributes.SetParsedURI(u)
} }
} }
@ -151,17 +149,15 @@ func (d *Declaration) NewResourceFromParsedURI(u data.URIParser) (err error) {
} }
func (d *Declaration) NewResource(uri *string) (err error) { func (d *Declaration) NewResource(uri *string) (err error) {
slog.Info("Declaration.NewResource()")
if d.ResourceTypes == nil { if d.ResourceTypes == nil {
panic(fmt.Errorf("Undefined type registry: unable to create new resource %s", *uri)) panic(fmt.Errorf("Undefined type registry: unable to create new resource %s", *uri))
} }
if uri == nil { if uri == nil {
d.Attributes, err = d.ResourceTypes.NewFromType(string(d.Type)) d.Attributes, err = d.ResourceTypes.New(fmt.Sprintf("%s://", d.Type))
} else { } else {
slog.Info("Declaration.NewResource()", "uri", *uri) if d.Attributes, err = d.ResourceTypes.New(*uri); err == nil {
parsedURI := URI(*uri).Parse() err = d.Attributes.SetURI(*uri)
d.Attributes, err = d.ResourceTypes.NewFromParsedURI(parsedURI.URL()) }
} }
return return
} }
@ -173,6 +169,7 @@ func (d *Declaration) Resource() data.Resource {
func (d *Declaration) Apply(stateTransition string) (result error) { func (d *Declaration) Apply(stateTransition string) (result error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
slog.Debug("Declaration.Apply()", "stacktrace", string(debug.Stack()))
slog.Info("Declaration.Apply()", "error", r, "resourceerror", d.Error) slog.Info("Declaration.Apply()", "error", r, "resourceerror", d.Error)
if d.Error != "" { if d.Error != "" {
result = fmt.Errorf("%s - %s", r, d.Error) result = fmt.Errorf("%s - %s", r, d.Error)
@ -219,7 +216,6 @@ func (d *Declaration) Apply(stateTransition string) (result error) {
} }
} }
slog.Info("Declaration.Apply() - read", "state", stater.CurrentState(), "declaration", d)
result = stater.Trigger("read") result = stater.Trigger("read")
currentState := stater.CurrentState() currentState := stater.CurrentState()
switch currentState { switch currentState {
@ -249,7 +245,7 @@ func (d *Declaration) SetConfig(configDoc data.Document) {
return return
} }
if d.Config != "" { // XXX if d.Config != "" { // XXX
panic(fmt.Sprintf("failed setting config: %s", d.Config)) panic("failed setting config")
} }
} }
@ -258,7 +254,7 @@ func (d *Declaration) SetURI(uri string) (err error) {
if d.Attributes == nil { if d.Attributes == nil {
err = d.NewResource(&uri) err = d.NewResource(&uri)
} else { } else {
err = d.Attributes.SetParsedURI(URI(uri).Parse()) err = d.Attributes.SetURI(uri)
} }
if err != nil { if err != nil {
return err return err
@ -272,7 +268,7 @@ func (d *Declaration) SetURI(uri string) (err error) {
return return
} }
func (d *Declaration) SetParsedURI(uri data.URIParser) (err error) { func (d *Declaration) SetParsedURI(uri *url.URL) (err error) {
slog.Info("Declaration.SetParsedURI()", "uri", uri, "declaration", d) slog.Info("Declaration.SetParsedURI()", "uri", uri, "declaration", d)
if d.Attributes == nil { if d.Attributes == nil {
err = d.NewResourceFromParsedURI(uri) err = d.NewResourceFromParsedURI(uri)
@ -302,9 +298,7 @@ func (d *Declaration) UnmarshalValue(value *DeclarationType) error {
d.OnError = value.OnError d.OnError = value.OnError
d.Error = value.Error d.Error = value.Error
d.Requires = value.Requires d.Requires = value.Requires
d.On = value.On newResource, resourceErr := d.ResourceTypes.New(fmt.Sprintf("%s://", value.Type))
newResource, resourceErr := d.ResourceTypes.NewFromType(string(value.Type))
slog.Info("Declaration.UnmarshalValue", "value", value, "error", resourceErr, "type", value.Type, "resource", newResource, "resourcetypes", d.ResourceTypes) slog.Info("Declaration.UnmarshalValue", "value", value, "error", resourceErr, "type", value.Type, "resource", newResource, "resourcetypes", d.ResourceTypes)
if resourceErr != nil { if resourceErr != nil {
slog.Info("Declaration.UnmarshalValue", "value", value, "error", resourceErr) slog.Info("Declaration.UnmarshalValue", "value", value, "error", resourceErr)
@ -312,30 +306,10 @@ func (d *Declaration) UnmarshalValue(value *DeclarationType) error {
} }
d.Attributes = newResource d.Attributes = newResource
d.configBlock = d.Config.GetBlock() d.configBlock = d.Config.GetBlock()
if d.On != nil {
if handler, ok := (*d.On)[EventTypeLoad]; ok {
stackSize := d.runtime.Api().GetTop()
if e := d.runtime.LoadScriptFromString(string(handler)); e != nil {
d.Error = e.Error()
}
returnsCount := d.runtime.Api().GetTop() - stackSize
if ! d.runtime.Api().IsNil(-1) {
if returnsCount == 0 {
// return nil
} else {
if lr,le := d.runtime.CopyReturnValuesFromCall(int(returnsCount)); le == nil {
slog.Info("Event.Load", "result", lr, "error", le)
}
}
}
}
}
return nil return nil
} }
func (d *Declaration) UnmarshalYAML(value *yaml.Node) (err error) { func (d *Declaration) UnmarshalYAML(value *yaml.Node) error {
if d.ResourceTypes == nil { if d.ResourceTypes == nil {
d.ResourceTypes = DocumentRegistry.ResourceTypes d.ResourceTypes = DocumentRegistry.ResourceTypes
} }
@ -357,17 +331,10 @@ func (d *Declaration) UnmarshalYAML(value *yaml.Node) (err error) {
if unmarshalResourceErr := resourceAttrs.Attributes.Decode(d.Attributes); unmarshalResourceErr != nil { if unmarshalResourceErr := resourceAttrs.Attributes.Decode(d.Attributes); unmarshalResourceErr != nil {
return unmarshalResourceErr return unmarshalResourceErr
} }
return nil
if i, ok := d.Attributes.(data.ResourceInitializer); ok {
err = i.Init(nil)
} else {
err = fmt.Errorf("failed to execute init")
}
return
} }
func (d *Declaration) UnmarshalJSON(jsonData []byte) (err error) { func (d *Declaration) UnmarshalJSON(jsonData []byte) error {
if d.ResourceTypes == nil { if d.ResourceTypes == nil {
d.ResourceTypes = DocumentRegistry.ResourceTypes d.ResourceTypes = DocumentRegistry.ResourceTypes
} }
@ -387,15 +354,25 @@ func (d *Declaration) UnmarshalJSON(jsonData []byte) (err error) {
return unmarshalAttributesErr return unmarshalAttributesErr
} }
if i, ok := d.Attributes.(data.ResourceInitializer); ok { return nil
err = i.Init(nil)
} else {
err = fmt.Errorf("failed to execute init")
}
return
} }
/*
func (d *Declaration) MarshalJSON() ([]byte, error) {
buf := new(bytes.Buffer)
buf.WriteByte('"')
buf.WriteString("value"))
buf.WriteByte('"')
return buf.Bytes(), nil
}
*/
/*
func (d *Declaration) MarshalYAML() (any, error) {
return d, nil
}
*/
/* /*
func (l *LuaWorker) Receive(m message.Envelope) { func (l *LuaWorker) Receive(m message.Envelope) {
s := m.Sender() s := m.Sender()

View File

@ -57,7 +57,6 @@ func TestNewResourceDeclarationType(t *testing.T) {
e := resourceDeclaration.LoadString(decl, codec.FormatYaml) e := resourceDeclaration.LoadString(decl, codec.FormatYaml)
assert.Nil(t, e) assert.Nil(t, e)
assert.Equal(t, TypeName("foo"), resourceDeclaration.Type) assert.Equal(t, TypeName("foo"), resourceDeclaration.Type)
assert.NotNil(t, resourceDeclaration.Attributes) assert.NotNil(t, resourceDeclaration.Attributes)
} }

View File

@ -10,6 +10,7 @@ import (
"io/fs" "io/fs"
_ "os" _ "os"
"log/slog" "log/slog"
"net/url"
"github.com/sters/yaml-diff/yamldiff" "github.com/sters/yaml-diff/yamldiff"
"strings" "strings"
"decl/internal/codec" "decl/internal/codec"
@ -18,7 +19,6 @@ _ "decl/internal/types"
"decl/internal/data" "decl/internal/data"
"decl/internal/schema" "decl/internal/schema"
"context" "context"
"path/filepath"
) )
type DocumentType struct { type DocumentType struct {
@ -44,23 +44,13 @@ type Document struct {
config data.Document config data.Document
Registry *Registry `json:"-" yaml:"-"` Registry *Registry `json:"-" yaml:"-"`
failedResources int `json:"-" yaml:"-"` failedResources int `json:"-" yaml:"-"`
importPaths *SearchPath `json:"-" yaml:"-"`
} }
func NewDocument(r *Registry) *Document { func NewDocument(r *Registry) *Document {
if r == nil { if r == nil {
r = DocumentRegistry r = DocumentRegistry
} }
return &Document{ Registry: r, Format: codec.FormatYaml, uris: mapper.New[string, data.Declaration](), configNames: mapper.New[string, data.Block]() }
var configImportPath ConfigKey = "system.importpath"
return &Document{
Registry: r,
Format: codec.FormatYaml,
uris: mapper.New[string, data.Declaration](),
configNames: mapper.New[string, data.Block](),
importPaths: NewSearchPath(configImportPath.GetStringSlice()),
}
} }
func (d *Document) GetURI() string { func (d *Document) GetURI() string {
@ -69,19 +59,6 @@ func (d *Document) GetURI() string {
func (d *Document) SetURI(uri string) { func (d *Document) SetURI(uri string) {
d.URI = URI(uri) d.URI = URI(uri)
d.AddProjectPath()
}
func (d *Document) AddProjectPath() {
exists := d.URI.Exists()
if exists {
if u := d.URI.Parse().(*ParsedURI); u != nil {
projectPath := filepath.Dir(filepath.Join(u.Hostname(), u.Path))
if err := d.importPaths.AddPath(projectPath); err != nil {
panic(err)
}
}
}
} }
func (d *Document) Types() data.TypesRegistry[data.Resource] { func (d *Document) Types() data.TypesRegistry[data.Resource] {
@ -122,10 +99,6 @@ func (d *Document) Set(key string, value any) {
d.uris.Set(key, value.(data.Declaration)) d.uris.Set(key, value.(data.Declaration))
} }
func (d *Document) Delete(key string) {
d.uris.Delete(key)
}
func (d *Document) GetResource(uri string) *Declaration { func (d *Document) GetResource(uri string) *Declaration {
if decl, ok := d.uris[uri]; ok { if decl, ok := d.uris[uri]; ok {
return decl.(*Declaration) return decl.(*Declaration)
@ -169,15 +142,7 @@ func (d *Document) ImportedDocuments() (documents []data.Document) {
func (d *Document) loadImports() (err error) { func (d *Document) loadImports() (err error) {
for _, uri := range d.Imports { for _, uri := range d.Imports {
if ! DocumentRegistry.HasDocument(uri) { if ! DocumentRegistry.HasDocument(uri) {
var load URI = uri if _, err = DocumentRegistry.Load(uri); err != nil {
if ! load.Exists() {
foundURI := d.importPaths.FindURI(load)
if foundURI != "" {
load = foundURI
}
}
slog.Info("Document.loadImports()", "load", load, "uri", uri, "importpaths", d.importPaths, "doc", d.URI)
if _, err = DocumentRegistry.Load(load); err != nil {
return return
} }
} }
@ -236,17 +201,15 @@ func (d *Document) GetSchemaFiles() (schemaFs fs.FS) {
return return
} }
schemaFs, _ = d.Registry.Schemas.Get(d.Registry.DefaultSchema) schemaFs, _ = d.Registry.Schemas.Get(d.Registry.DefaultSchema)
slog.Info("Document.GetSchemaFiles()", "schemaFs", schemaFs)
return return
} }
func (d *Document) Validate() error { func (d *Document) Validate() error {
jsonDocument, jsonErr := d.JSON() jsonDocument, jsonErr := d.JSON()
slog.Info("Document.Validate() json", "err", jsonErr) slog.Info("document.Validate() json", "err", jsonErr)
if jsonErr == nil { if jsonErr == nil {
s := schema.New("document", d.GetSchemaFiles()) s := schema.New("document", d.GetSchemaFiles())
err := s.Validate(string(jsonDocument)) err := s.Validate(string(jsonDocument))
slog.Info("Document.Validate()", "error", err)
if err != nil { if err != nil {
return err return err
} }
@ -360,10 +323,6 @@ func (d *Document) MapResourceURI(uri string, declaration data.Declaration) {
d.uris[uri] = declaration d.uris[uri] = declaration
} }
func (d *Document) UnMapResourceURI(uri string) {
d.uris.Delete(uri)
}
func (d *Document) AddDeclaration(declaration data.Declaration) { func (d *Document) AddDeclaration(declaration data.Declaration) {
uri := declaration.URI() uri := declaration.URI()
decl := declaration.(*Declaration) decl := declaration.(*Declaration)
@ -423,7 +382,7 @@ func (d *Document) NewResourceFromURI(uri URI) (newResource data.Resource, err e
return d.NewResourceFromParsedURI(uri.Parse()) return d.NewResourceFromParsedURI(uri.Parse())
} }
func (d *Document) NewResourceFromParsedURI(uri data.URIParser) (newResource data.Resource, err error) { func (d *Document) NewResourceFromParsedURI(uri *url.URL) (newResource data.Resource, err error) {
if uri == nil { if uri == nil {
return nil, fmt.Errorf("%w: %s", ErrUnknownResourceType, uri) return nil, fmt.Errorf("%w: %s", ErrUnknownResourceType, uri)
} }
@ -537,35 +496,6 @@ func (d *Document) DiffState(output io.Writer) (returnOutput string, diffErr err
return d.Diff(clone, output) return d.Diff(clone, output)
} }
func (d *Document) YamlDiff(with data.Document) (diffs []*yamldiff.YamlDiff, diffErr error) {
defer func() {
if r := recover(); r != nil {
diffErr = fmt.Errorf("%s", r)
}
}()
opts := []yamldiff.DoOptionFunc{}
ydata, yerr := d.YAML()
if yerr != nil {
return nil, yerr
}
yamlDiff,yamlDiffErr := yamldiff.Load(string(ydata))
if yamlDiffErr != nil {
return nil, yamlDiffErr
}
wdata,werr := with.YAML()
if werr != nil {
return nil, werr
}
withDiff,withDiffErr := yamldiff.Load(string(wdata))
if withDiffErr != nil {
return nil, withDiffErr
}
slog.Info("Document.Diff() ", "document.yaml", ydata, "with.yaml", wdata)
return yamldiff.Do(yamlDiff, withDiff, opts...), nil
}
func (d *Document) Diff(with data.Document, output io.Writer) (returnOutput string, diffErr error) { func (d *Document) Diff(with data.Document, output io.Writer) (returnOutput string, diffErr error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -573,21 +503,37 @@ func (d *Document) Diff(with data.Document, output io.Writer) (returnOutput stri
diffErr = fmt.Errorf("%s", r) diffErr = fmt.Errorf("%s", r)
} }
}() }()
slog.Info("Document.Diff()")
opts := []yamldiff.DoOptionFunc{}
if output == nil { if output == nil {
output = &strings.Builder{} output = &strings.Builder{}
} }
ydata, yerr := d.YAML()
if yerr != nil {
return "", yerr
}
yamlDiff,yamlDiffErr := yamldiff.Load(string(ydata))
if yamlDiffErr != nil {
return "", yamlDiffErr
}
var diffs []*yamldiff.YamlDiff wdata,werr := with.YAML()
diffs, diffErr = d.YamlDiff(with) if werr != nil {
return "", werr
}
withDiff,withDiffErr := yamldiff.Load(string(wdata))
if withDiffErr != nil {
return "", withDiffErr
}
for _,docDiffResults := range diffs { for _,docDiffResults := range yamldiff.Do(yamlDiff, withDiff, opts...) {
slog.Info("Diff()", "diff", docDiffResults, "dump", docDiffResults.Dump()) slog.Info("Diff()", "diff", docDiffResults, "dump", docDiffResults.Dump())
_,e := output.Write([]byte(docDiffResults.Dump())) _,e := output.Write([]byte(docDiffResults.Dump()))
if e != nil { if e != nil {
return "", e return "", e
} }
} }
slog.Info("Document.Diff() ", "document.yaml", ydata, "with.yaml", wdata)
if stringOutput, ok := output.(*strings.Builder); ok { if stringOutput, ok := output.(*strings.Builder); ok {
return stringOutput.String(), nil return stringOutput.String(), nil
} }
@ -600,7 +546,7 @@ func (d *Document) UnmarshalValue(value *DocumentType) error {
} }
*/ */
func (d *Document) UnmarshalYAML(value *yaml.Node) (err error) { func (d *Document) UnmarshalYAML(value *yaml.Node) error {
type decodeDocument Document type decodeDocument Document
t := &DocumentType{} t := &DocumentType{}
if unmarshalDocumentErr := value.Decode(t); unmarshalDocumentErr != nil { if unmarshalDocumentErr := value.Decode(t); unmarshalDocumentErr != nil {
@ -610,22 +556,20 @@ func (d *Document) UnmarshalYAML(value *yaml.Node) (err error) {
if unmarshalResourcesErr := value.Decode((*decodeDocument)(d)); unmarshalResourcesErr != nil { if unmarshalResourcesErr := value.Decode((*decodeDocument)(d)); unmarshalResourcesErr != nil {
return unmarshalResourcesErr return unmarshalResourcesErr
} }
err = d.loadImports()
d.assignConfigurationsDocument() d.assignConfigurationsDocument()
d.assignResourcesDocument() d.assignResourcesDocument()
return return d.loadImports()
} }
func (d *Document) UnmarshalJSON(data []byte) (err error) { func (d *Document) UnmarshalJSON(data []byte) error {
type decodeDocument Document type decodeDocument Document
t := (*decodeDocument)(d) t := (*decodeDocument)(d)
if unmarshalDocumentErr := json.Unmarshal(data, t); unmarshalDocumentErr != nil { if unmarshalDocumentErr := json.Unmarshal(data, t); unmarshalDocumentErr != nil {
return unmarshalDocumentErr return unmarshalDocumentErr
} }
err = d.loadImports()
d.assignConfigurationsDocument() d.assignConfigurationsDocument()
d.assignResourcesDocument() d.assignResourcesDocument()
return return d.loadImports()
} }
func (d *Document) AddError(e error) { func (d *Document) AddError(e error) {

View File

@ -84,9 +84,6 @@ resources:
f.Name = "mytestresource" f.Name = "mytestresource"
f.Size = 3 f.Size = 3
assert.Nil(t, f.Init(nil))
d.AddResourceDeclaration("foo", f) d.AddResourceDeclaration("foo", f)
ey := d.Generate(&documentYaml) ey := d.Generate(&documentYaml)

View File

@ -1,31 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"errors"
_ "gitea.rosskeen.house/pylon/luaruntime"
_ "fmt"
)
type EventHandler string
type Events map[EventType]EventHandler
var (
ErrInvalidHandler error = errors.New("Invalid event handler")
)
func NewEvents() *Events {
e := make(Events)
return &e
}
func (e *Events) Set(t EventType, h EventHandler) (err error) {
if err = t.Validate(); err == nil {
(*e)[t] = h
}
return
}

View File

@ -1,22 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestNewEvent(t *testing.T) {
var et EventType
events := NewEvents()
assert.NotNil(t, events)
assert.Nil(t, et.Set(EventTypeLoad))
assert.Nil(t, events.Set(et, EventHandler(`
print('hello world')
`)))
}

View File

@ -1,39 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"errors"
"fmt"
)
type EventType string
const (
EventTypeLoad EventType = "load"
EventTypeCreate EventType = "create"
EventTypeRead EventType = "read"
EventTypeUpdate EventType = "update"
EventTypeDelete EventType = "delete"
EventTypeError EventType = "error"
)
var (
ErrUnknownEventType error = errors.New("Unknown EventType")
)
func (e EventType) Validate() (err error) {
switch e {
case EventTypeLoad, EventTypeCreate, EventTypeRead, EventTypeUpdate, EventTypeDelete, EventTypeError:
default:
return fmt.Errorf("%w: %s", ErrUnknownEventType, e)
}
return
}
func (e *EventType) Set(v EventType) (err error) {
if err = v.Validate(); err == nil {
(*e) = v
}
return
}

View File

@ -1,29 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestEventType(t *testing.T) {
for _, v := range []struct{ et EventType; expected error } {
{ et: EventType("load"), expected: nil },
{ et: EventType("create"), expected: nil },
{ et: EventType("read"), expected: nil },
{ et: EventType("update"), expected: nil },
{ et: EventType("delete"), expected: nil },
{ et: EventType("error"), expected: nil },
{ et: EventType("foo"), expected: ErrUnknownEventType },
} {
assert.ErrorIs(t, v.et.Validate(), v.expected)
}
}
func TestEventTypeSet(t *testing.T) {
var et EventType
assert.Nil(t, et.Set(EventTypeLoad))
}

View File

@ -3,13 +3,14 @@
package folio package folio
import ( import (
"context" "context"
_ "gopkg.in/yaml.v3" _ "gopkg.in/yaml.v3"
"encoding/json" "encoding/json"
_ "fmt" _ "fmt"
"decl/internal/data" "decl/internal/data"
"decl/internal/codec" "decl/internal/codec"
"io" "io"
"net/url"
) )
type MockConfiguration struct { type MockConfiguration struct {
@ -37,7 +38,7 @@ func (m *MockConfiguration) SetURI(uri string) error {
return nil return nil
} }
func (m *MockConfiguration) SetParsedURI(uri data.URIParser) error { func (m *MockConfiguration) SetParsedURI(uri *url.URL) error {
return nil return nil
} }

View File

@ -17,30 +17,22 @@ _ "gopkg.in/yaml.v3"
func RegisterMocks() { func RegisterMocks() {
TestResourceTypes.Register([]string{"foo"}, func(u *url.URL) data.Resource { TestResourceTypes.Register([]string{"foo"}, func(u *url.URL) data.Resource {
f := NewFooResource() f := NewFooResource()
if u != nil { f.Name = filepath.Join(u.Hostname(), u.Path)
f.Name = filepath.Join(u.Hostname(), u.Path)
}
return f return f
}) })
TestResourceTypes.Register([]string{"bar"}, func(u *url.URL) data.Resource { TestResourceTypes.Register([]string{"bar"}, func(u *url.URL) data.Resource {
f := NewBarResource() f := NewBarResource()
if u != nil { f.Name = filepath.Join(u.Hostname(), u.Path)
f.Name = filepath.Join(u.Hostname(), u.Path)
}
return f return f
}) })
TestResourceTypes.Register([]string{"testuser"}, func(u *url.URL) data.Resource { TestResourceTypes.Register([]string{"testuser"}, func(u *url.URL) data.Resource {
f := NewTestuserResource() f := NewTestuserResource()
if u != nil { f.Name = filepath.Join(u.Hostname(), u.Path)
f.Name = filepath.Join(u.Hostname(), u.Path)
}
return f return f
}) })
TestResourceTypes.Register([]string{"file"}, func(u *url.URL) data.Resource { TestResourceTypes.Register([]string{"file"}, func(u *url.URL) data.Resource {
f := NewFileResource() f := NewFileResource()
if u != nil { f.Name = filepath.Join(u.Hostname(), u.Path)
f.Name = filepath.Join(u.Hostname(), u.Path)
}
return f return f
}) })
} }
@ -48,20 +40,23 @@ func RegisterMocks() {
type MockFoo struct { type MockFoo struct {
stater machine.Stater `json:"-" yaml:"-"` stater machine.Stater `json:"-" yaml:"-"`
*MockResource `json:",inline" yaml:",inline"` *MockResource `json:"-" yaml:"-"`
Name string `json:"name" yaml:"name"`
Size int `json:"size" yaml:"size"` Size int `json:"size" yaml:"size"`
} }
type MockBar struct { type MockBar struct {
stater machine.Stater `json:"-" yaml:"-"` stater machine.Stater `json:"-" yaml:"-"`
*MockResource `json:",inline" yaml:",inline"` *MockResource `json:"-" yaml:"-"`
Name string `json:"name" yaml:"name"`
Size int `json:"size,omitempty" yaml:"size,omitempty"` Size int `json:"size,omitempty" yaml:"size,omitempty"`
Owner string `json:"owner,omitempty" yaml:"owner,omitempty"` Owner string `json:"owner,omitempty" yaml:"owner,omitempty"`
} }
type MockTestuser struct { type MockTestuser struct {
stater machine.Stater `json:"-" yaml:"-"` stater machine.Stater `json:"-" yaml:"-"`
*MockResource `json:",inline" yaml:",inline"` *MockResource `json:"-" yaml:"-"`
Name string `json:"name" yaml:"name"`
Uid string `json:"uid" yaml:"uid"` Uid string `json:"uid" yaml:"uid"`
Group string `json:"group" yaml:"group"` Group string `json:"group" yaml:"group"`
Home string `json:"home" yaml:"home"` Home string `json:"home" yaml:"home"`
@ -69,12 +64,13 @@ type MockTestuser struct {
type MockFile struct { type MockFile struct {
stater machine.Stater `json:"-" yaml:"-"` stater machine.Stater `json:"-" yaml:"-"`
*MockResource `json:",inline" yaml:",inline"` *MockResource `json:"-" yaml:"-"`
Name string `json:"name" yaml:"name"`
Size int `json:"size" yaml:"size"` Size int `json:"size" yaml:"size"`
} }
func NewMockResource(typename string, stater machine.Stater) (m *MockResource) { func NewMockResource(typename string, stater machine.Stater) *MockResource {
m = &MockResource { return &MockResource {
InjectType: func() string { return typename }, InjectType: func() string { return typename },
InjectResolveId: func(ctx context.Context) string { return "bar" }, InjectResolveId: func(ctx context.Context) string { return "bar" },
InjectLoadDecl: func(string) error { return nil }, InjectLoadDecl: func(string) error { return nil },
@ -93,13 +89,6 @@ func NewMockResource(typename string, stater machine.Stater) (m *MockResource) {
InjectURI: func() string { return fmt.Sprintf("%s://bar", typename) }, InjectURI: func() string { return fmt.Sprintf("%s://bar", typename) },
InjectNotify: func(*machine.EventMessage) {}, InjectNotify: func(*machine.EventMessage) {},
} }
m.InjectInit = func(u data.URIParser) error {
if u != nil {
m.Name = filepath.Join(u.URL().Hostname(), u.URL().Path)
}
return nil
}
return m
} }
func NewFooResource() *MockFoo { func NewFooResource() *MockFoo {

View File

@ -3,19 +3,18 @@
package folio package folio
import ( import (
"context" "context"
_ "gopkg.in/yaml.v3" _ "gopkg.in/yaml.v3"
"encoding/json" "encoding/json"
_ "fmt" _ "fmt"
"gitea.rosskeen.house/rosskeen.house/machine" "gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/data" "decl/internal/data"
"decl/internal/codec" "decl/internal/codec"
"io" "io"
"net/url"
) )
type MockResource struct { type MockResource struct {
Name string `json:"name" yaml:"name"`
InjectInit func(data.URIParser) error `json:"-" yaml:"-"`
InjectURI func() string `json:"-" yaml:"-"` InjectURI func() string `json:"-" yaml:"-"`
InjectType func() string `json:"-" yaml:"-"` InjectType func() string `json:"-" yaml:"-"`
InjectResolveId func(ctx context.Context) string `json:"-" yaml:"-"` InjectResolveId func(ctx context.Context) string `json:"-" yaml:"-"`
@ -55,7 +54,7 @@ func (m *MockResource) SetURI(uri string) error {
return nil return nil
} }
func (m *MockResource) SetParsedURI(uri data.URIParser) error { func (m *MockResource) SetParsedURI(uri *url.URL) error {
return nil return nil
} }
@ -127,10 +126,6 @@ func (m *MockResource) Apply() error {
return m.InjectApply() return m.InjectApply()
} }
func (m *MockResource) Init(u data.URIParser) (error) {
return m.InjectInit(u)
}
func (m *MockResource) Type() string { func (m *MockResource) Type() string {
return m.InjectType() return m.InjectType()
} }

View File

@ -1,102 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"net/url"
"decl/internal/transport"
"decl/internal/data"
"decl/internal/identifier"
"strings"
)
var (
)
type ParsedURI url.URL
func NewParsedURI(uri URI) *ParsedURI {
return uri.Parse().(*ParsedURI)
}
func CastParsedURI(u *url.URL) *ParsedURI {
return (*ParsedURI)(u)
}
func (uri *ParsedURI) URL() *url.URL {
return (*url.URL)(uri)
}
func (uri *ParsedURI) Hostname() string {
return (*url.URL)(uri).Hostname()
}
func (uri *ParsedURI) NewResource(document data.Document) (newResource data.Resource, err error) {
if document == nil {
declaration := NewDeclaration()
if err = declaration.NewResourceFromParsedURI(uri); err == nil {
return declaration.Attributes, err
}
} else {
newResource, err = document.NewResourceFromParsedURI(uri)
return
}
return
}
func (uri *ParsedURI) ConstructResource(res data.Resource) (err error) {
if ri, ok := res.(data.ResourceInitializer); ok {
err = ri.Init(uri)
} else {
err = res.SetParsedURI(uri)
}
return
}
func (uri *ParsedURI) Converter() (converter data.Converter, err error) {
return DocumentRegistry.ConverterTypes.NewFromParsedURI(uri.URL())
}
func (uri *ParsedURI) Exists() bool {
return transport.Exists(uri.URL())
}
func (uri *ParsedURI) ContentReaderStream() (*transport.Reader, error) {
return transport.NewReader(uri.URL())
}
func (uri *ParsedURI) ContentWriterStream() (*transport.Writer, error) {
return transport.NewWriter(uri.URL())
}
func (uri *ParsedURI) String() string {
return uri.URL().String()
}
func (uri *ParsedURI) SetURL(url *url.URL) {
*uri = *(*ParsedURI)(url)
}
func (uri *ParsedURI) Extension() (string, string) {
return (identifier.ID)(uri.Path).Extension()
}
func (uri *ParsedURI) ContentType() string {
var ext strings.Builder
exttype, fileext := uri.Extension()
if fileext == "" {
return exttype
}
ext.WriteString(exttype)
ext.WriteRune('.')
ext.WriteString(fileext)
return ext.String()
}
func (uri *ParsedURI) IsEmpty() bool {
if uri == nil || (identifier.ID)(uri.String()).IsEmpty() {
return true
}
return false
}

View File

@ -69,7 +69,7 @@ func (r *Registry) SetDocument(key URI, value *Document) {
func (r *Registry) NewDocument(uri URI) (doc *Document) { func (r *Registry) NewDocument(uri URI) (doc *Document) {
doc = NewDocument(r) doc = NewDocument(r)
doc.SetURI(string(uri)) doc.URI = uri
r.Documents = append(r.Documents, doc) r.Documents = append(r.Documents, doc)
if uri != "" { if uri != "" {
r.UriMap[uri] = doc r.UriMap[uri] = doc
@ -88,17 +88,17 @@ func (r *Registry) AppendParsedURI(uri *url.URL, documents []data.Document) (add
case data.ManyExtractor: case data.ManyExtractor:
var docs []data.Document var docs []data.Document
docs, err = extractor.ExtractMany(sourceResource, nil) docs, err = extractor.ExtractMany(sourceResource, nil)
slog.Info("folio.Registry.AppendParsedURI() - ExtractMany", "uri", uri, "source", sourceResource, "docs", docs, "error", err) slog.Info("folio.Registry.Append() - ExtractMany", "uri", uri, "source", sourceResource, "docs", docs, "error", err)
documents = append(documents, docs...) documents = append(documents, docs...)
case data.Extractor: case data.Extractor:
var singleDocument data.Document var singleDocument data.Document
singleDocument, err = extractor.Extract(sourceResource, nil) singleDocument, err = extractor.Extract(sourceResource, nil)
slog.Info("folio.Registry.AppendParsedURI() - Extract", "uri", uri, "source", sourceResource, "doc", singleDocument, "error", err) slog.Info("folio.Registry.Append() - Extract", "uri", uri, "source", sourceResource, "doc", singleDocument, "error", err)
documents = append(documents, singleDocument) documents = append(documents, singleDocument)
} }
} }
} }
slog.Info("folio.Registry.AppendParsedURI()", "uri", uri, "converter", r.ConverterTypes, "error", err) slog.Info("folio.Registry.Append()", "uri", uri, "converter", r.ConverterTypes, "error", err)
addedDocuments = documents addedDocuments = documents
return return
} }
@ -130,8 +130,6 @@ func (r *Registry) Append(uri URI, documents []data.Document) (addedDocuments []
slog.Info("folio.Registry.Append() - Extract", "uri", uri, "source", sourceResource, "doc", singleDocument, "error", err) slog.Info("folio.Registry.Append() - Extract", "uri", uri, "source", sourceResource, "doc", singleDocument, "error", err)
documents = append(documents, singleDocument) documents = append(documents, singleDocument)
} }
} else {
slog.Warn("folio.Registry.Append(): failed loading extractor as resource")
} }
} }
slog.Info("folio.Registry.Append()", "uri", uri, "converter", r.ConverterTypes, "error", err) slog.Info("folio.Registry.Append()", "uri", uri, "converter", r.ConverterTypes, "error", err)
@ -148,8 +146,3 @@ func (r *Registry) LoadFromURL(uri *url.URL) (documents []data.Document, err err
documents = make([]data.Document, 0, 10) documents = make([]data.Document, 0, 10)
return r.AppendParsedURI(uri, documents) return r.AppendParsedURI(uri, documents)
} }
func (r *Registry) LoadFromParsedURI(uri *url.URL) (documents []data.Document, err error) {
documents = make([]data.Document, 0, 10)
return r.AppendParsedURI(uri, documents)
}

View File

@ -15,11 +15,11 @@ var (
func NewResourceFromParsedURI(u *url.URL, document data.Document) (newResource data.Resource, err error) { func NewResourceFromParsedURI(u *url.URL, document data.Document) (newResource data.Resource, err error) {
if document == nil { if document == nil {
declaration := NewDeclaration() declaration := NewDeclaration()
if err = declaration.NewResourceFromParsedURI((*ParsedURI)(u)); err == nil { if err = declaration.NewResourceFromParsedURI(u); err == nil {
return declaration.Attributes, err return declaration.Attributes, err
} }
} else { } else {
newResource, err = document.NewResourceFromParsedURI((*ParsedURI)(u)) newResource, err = document.NewResourceFromParsedURI(u)
return return
} }
return return

View File

@ -38,7 +38,7 @@ func (r ResourceReference) Lookup(look data.ResourceMapper) ContentReadWriter {
slog.Info("ResourceReference.Lookup()", "resourcereference", r, "resourcemapper", look) slog.Info("ResourceReference.Lookup()", "resourcereference", r, "resourcemapper", look)
if look != nil { if look != nil {
if v,ok := look.Get(string(r)); ok { if v,ok := look.Get(string(r)); ok {
return v.Resource().(ContentReadWriter) return v.(ContentReadWriter)
} }
} }
return r return r
@ -56,7 +56,7 @@ func (r ResourceReference) Dereference(look data.ResourceMapper) data.Resource {
} }
func (r ResourceReference) Parse() *url.URL { func (r ResourceReference) Parse() *url.URL {
return URI(r).Parse().URL() return URI(r).Parse()
} }
func (r ResourceReference) Exists() bool { func (r ResourceReference) Exists() bool {
@ -70,7 +70,3 @@ func (r ResourceReference) ContentReaderStream() (*transport.Reader, error) {
func (r ResourceReference) ContentWriterStream() (*transport.Writer, error) { func (r ResourceReference) ContentWriterStream() (*transport.Writer, error) {
return URI(r).ContentWriterStream() return URI(r).ContentWriterStream()
} }
func (r ResourceReference) IsEmpty() bool {
return URI(r).IsEmpty()
}

View File

@ -1,65 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"decl/internal/ext"
"decl/internal/ds"
"errors"
"fmt"
"log/slog"
"path/filepath"
"decl/internal/data"
)
var (
ErrSearchPathNotExist error = errors.New("Search path does not exist.")
)
// A search path should find a file in list of paths
type SearchPath struct {
paths *ds.OrderedSet[string]
}
func NewSearchPath(paths []string) (s *SearchPath) {
s = &SearchPath{ paths: ds.NewOrderedSet[string]() }
s.paths.AddItems(paths)
return
}
func (s *SearchPath) AddPath(path string) error {
if newPath := ext.FilePath(path).Abs(); newPath.Exists() {
s.paths.Add(string(newPath))
} else {
return fmt.Errorf("%w: %s", ErrSearchPathNotExist, path)
}
return nil
}
func (s *SearchPath) Find(relative string) string {
pathValues := s.paths.Items()
for i := len(pathValues) - 1; i >= 0; i-- {
p := *pathValues[i]
searchPath := ext.FilePath(p)
searchPath.Add(relative)
slog.Info("SearchPath.Find()", "searchpath", p, "file", relative, "target", searchPath)
if searchPath.Exists() {
return string(searchPath)
}
}
return ""
}
func (s *SearchPath) FindParsedURI(uri data.URIParser) (result URI) {
u := uri.URL()
filePath := filepath.Join(u.Hostname(), u.Path)
if absPath := s.Find(filePath); absPath != "" {
result = URI(fmt.Sprintf("%s://%s", u.Scheme, string(absPath)))
}
return
}
func (s *SearchPath) FindURI(uri URI) (result URI) {
return s.FindParsedURI(uri.Parse())
}

View File

@ -1,24 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"github.com/stretchr/testify/assert"
"testing"
"os"
)
func TestSearchPath(t *testing.T) {
assert.Nil(t, TempDir.CreateFile("test.jx.yaml", ""))
sp := NewSearchPath(nil)
assert.NotNil(t, sp)
assert.Nil(t, sp.AddPath(string(TempDir)))
absPath := sp.Find("test.jx.yaml")
_, err := os.Stat(absPath)
assert.True(t, !os.IsNotExist(err))
}

View File

@ -35,24 +35,14 @@ func (u URI) NewResource(document data.Document) (newResource data.Resource, err
return return
} }
func (u URI) ConstructResource(res data.Resource) (err error) {
parsedURI := u.Parse()
if ri, ok := res.(data.ResourceInitializer); ok {
err = ri.Init(parsedURI)
} else {
err = res.SetParsedURI(parsedURI)
}
return
}
func (u URI) Converter() (converter data.Converter, err error) { func (u URI) Converter() (converter data.Converter, err error) {
return DocumentRegistry.ConverterTypes.New(string(u)) return DocumentRegistry.ConverterTypes.New(string(u))
} }
func (u URI) Parse() data.URIParser { func (u URI) Parse() *url.URL {
url, e := url.Parse(string(u)) url, e := url.Parse(string(u))
if e == nil { if e == nil {
return (*ParsedURI)(url) return url
} }
return nil return nil
} }

View File

@ -10,17 +10,17 @@ import (
func TestURI(t *testing.T) { func TestURI(t *testing.T) {
var file URI = URI(fmt.Sprintf("file://%s", TempDir)) var file URI = URI(fmt.Sprintf("file://%s", TempDir))
u := file.Parse().URL() u := file.Parse()
assert.Equal(t, "file", u.Scheme) assert.Equal(t, "file", u.Scheme)
assert.True(t, file.Exists()) assert.True(t, file.Exists())
file = URI(fmt.Sprintf("0file:_/%s", TempDir)) file = URI(fmt.Sprintf("0file:_/%s", TempDir))
x := file.Parse() u = file.Parse()
assert.Nil(t, x) assert.Nil(t, u)
} }
func TestURISetURL(t *testing.T) { func TestURISetURL(t *testing.T) {
var file URI = URI(fmt.Sprintf("file://%s", TempDir)) var file URI = URI(fmt.Sprintf("file://%s", TempDir))
u := file.Parse().URL() u := file.Parse()
var fileFromURL URI var fileFromURL URI
fileFromURL.SetURL(u) fileFromURL.SetURL(u)
assert.Equal(t, fileFromURL, file) assert.Equal(t, fileFromURL, file)

View File

@ -18,15 +18,10 @@ func (m Store[Key, Value]) Set(key Key, value Value) {
m[key] = value m[key] = value
} }
func (m Store[Key, Value]) Delete(key Key) {
delete(m, key)
}
type Mapper interface { type Mapper interface {
Get(key string) (any, bool) Get(key string) (any, bool)
Has(key string) (bool) Has(key string) (bool)
Set(key string, value any) Set(key string, value any)
Delete(key string)
} }
type Getter[Key comparable, Value comparable] interface { type Getter[Key comparable, Value comparable] interface {
@ -38,7 +33,6 @@ type Map[Key comparable, Value comparable] interface {
Get(key Key) (Value, bool) Get(key Key) (Value, bool)
Has(key Key) (bool) Has(key Key) (bool)
Set(key Key, value Value) Set(key Key, value Value)
Delete(key Key)
} }
func New[Key comparable, Value comparable]() Store[Key, Value] { func New[Key comparable, Value comparable]() Store[Key, Value] {

View File

@ -0,0 +1,129 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
_ "context"
"encoding/json"
"fmt"
"gopkg.in/yaml.v3"
"io"
"log/slog"
_ "net/url"
_ "os"
"os/exec"
"strings"
"text/template"
"decl/internal/codec"
)
type CommandExecutor func(value any) ([]byte, error)
type CommandExtractAttributes func(output []byte, target any) error
type CommandArg string
type Command struct {
Path string `json:"path" yaml:"path"`
Args []CommandArg `json:"args" yaml:"args"`
Split bool `json:"split" yaml:"split"`
Executor CommandExecutor `json:"-" yaml:"-"`
Extractor CommandExtractAttributes `json:"-" yaml:"-"`
}
func NewCommand() *Command {
c := &Command{ Split: true }
c.Executor = func(value any) ([]byte, error) {
args, err := c.Template(value)
if err != nil {
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
}
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() - 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
}
func (c *Command) Load(r io.Reader) error {
return codec.NewYAMLDecoder(r).Decode(c)
}
func (c *Command) LoadDecl(yamlResourceDeclaration string) error {
return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(c)
}
func (c *Command) Template(value any) ([]string, error) {
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
}
if commandLineArg.Len() > 0 {
var splitArg []string
if c.Split {
splitArg = strings.Split(commandLineArg.String(), " ")
} else {
splitArg = []string{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
}
func (c *Command) Execute(value any) ([]byte, error) {
return c.Executor(value)
}
func (c *CommandArg) UnmarshalValue(value string) error {
*c = CommandArg(value)
return nil
}
func (c *CommandArg) UnmarshalJSON(data []byte) error {
var s string
if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
return unmarshalRouteTypeErr
}
return c.UnmarshalValue(s)
}
func (c *CommandArg) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
return c.UnmarshalValue(s)
}

View File

@ -0,0 +1,58 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
_ "fmt"
"github.com/stretchr/testify/assert"
_ "os"
_ "strings"
"testing"
)
func TestNewCommand(t *testing.T) {
c := NewCommand()
assert.NotNil(t, c)
}
func TestCommandLoad(t *testing.T) {
c := NewCommand()
assert.NotNil(t, c)
decl := `
path: find
args:
- "{{ .Path }}"
`
assert.Nil(t, c.LoadDecl(decl))
assert.Equal(t, "find", c.Path)
}
func TestCommandTemplate(t *testing.T) {
c := NewCommand()
assert.NotNil(t, c)
decl := `
path: find
args:
- "{{ .Path }}"
`
assert.Nil(t, c.LoadDecl(decl))
assert.Equal(t, "find", c.Path)
assert.Equal(t, 1, len(c.Args))
f := NewFile()
f.Path = "./"
args, templateErr := c.Template(f)
assert.Nil(t, templateErr)
assert.Equal(t, 1, len(args))
assert.Equal(t, "./", string(args[0]))
out, err := c.Execute(f)
assert.Nil(t, err)
assert.Greater(t, len(out), 0)
}

View File

@ -10,20 +10,17 @@ import (
"decl/internal/data" "decl/internal/data"
"decl/internal/folio" "decl/internal/folio"
"log/slog" "log/slog"
"errors"
) )
type UriSchemeValidator func(scheme string) bool type UriSchemeValidator func(scheme string) bool
type UriNormalize func() error
type Common struct { type Common struct {
SchemeCheck UriSchemeValidator `json:"-" yaml:"-"` SchemeCheck UriSchemeValidator `json:"-" yaml:"-"`
NormalizePath UriNormalize `json:"-" yaml:"-"`
includeQueryParamsInURI bool `json:"-" yaml:"-"` includeQueryParamsInURI bool `json:"-" yaml:"-"`
resourceType TypeName `json:"-" yaml:"-"` resourceType TypeName `json:"-" yaml:"-"`
Uri folio.URI `json:"uri,omitempty" yaml:"uri,omitempty"`
parsedURI *url.URL `json:"-" yaml:"-"` parsedURI *url.URL `json:"-" yaml:"-"`
Path string `json:"path,omitempty" yaml:"path,omitempty"` Path string `json:"path,omitempty" yaml:"path,omitempty"`
absPath string `json:"-" yaml:"-"`
exttype string `json:"-" yaml:"-"` exttype string `json:"-" yaml:"-"`
fileext string `json:"-" yaml:"-"` fileext string `json:"-" yaml:"-"`
@ -32,13 +29,11 @@ type Common struct {
State string `json:"state,omitempty" yaml:"state,omitempty"` State string `json:"state,omitempty" yaml:"state,omitempty"`
config data.ConfigurationValueGetter config data.ConfigurationValueGetter
Resources data.ResourceMapper `json:"-" yaml:"-"` Resources data.ResourceMapper `json:"-" yaml:"-"`
Errors []error `json:"-" yaml:"-"`
} }
func NewCommon(resourceType TypeName, includeQueryParams bool) *Common { func NewCommon(resourceType TypeName, includeQueryParams bool) *Common {
c := &Common{ includeQueryParamsInURI: includeQueryParams, resourceType: resourceType } c := &Common{ includeQueryParamsInURI: includeQueryParams, resourceType: resourceType }
c.SchemeCheck = c.IsValidResourceScheme c.SchemeCheck = c.IsValidResourceScheme
c.NormalizePath = c.NormalizeFilePath
return c return c
} }
@ -59,13 +54,9 @@ func (c *Common) SetResourceMapper(resources data.ResourceMapper) {
func (c *Common) Clone() *Common { func (c *Common) Clone() *Common {
return &Common { return &Common {
SchemeCheck: c.SchemeCheck, Uri: c.Uri,
NormalizePath: c.NormalizePath,
includeQueryParamsInURI: c.includeQueryParamsInURI,
resourceType: c.resourceType,
parsedURI: c.parsedURI, parsedURI: c.parsedURI,
Path: c.Path, Path: c.Path,
absPath: c.absPath,
exttype: c.exttype, exttype: c.exttype,
fileext: c.fileext, fileext: c.fileext,
normalizePath: c.normalizePath, normalizePath: c.normalizePath,
@ -83,40 +74,41 @@ func (c *Common) URIPath() string {
return c.Path return c.Path
} }
func (c *Common) URI() folio.URI { func (c *Common) URI() string {
slog.Info("Common.URI", "parsed", c.parsedURI) return string(c.Uri)
return folio.URI(c.parsedURI.String())
} }
func (c *Common) SetParsedURI(u data.URIParser) (err error) { func (c *Common) SetURI(uri string) (err error) {
c.SetURIFromString(uri)
err = c.SetParsedURI(c.Uri.Parse())
return
}
func (c *Common) SetURIFromString(uri string) {
c.Uri = folio.URI(uri)
c.exttype, c.fileext = c.Uri.Extension()
}
func (c *Common) SetParsedURI(u *url.URL) (err error) {
if u != nil { if u != nil {
slog.Info("Common.SetParsedURI()", "parsed", u, "uri", c.Uri)
slog.Info("Common.SetParsedURI()", "parsed", u, "uri", c) if c.Uri.IsEmpty() {
c.SetURIFromString(u.String())
c.parsedURI = u.URL() }
c.parsedURI = u
c.exttype, c.fileext = u.Extension()
if c.SchemeCheck(c.parsedURI.Scheme) { if c.SchemeCheck(c.parsedURI.Scheme) {
if c.includeQueryParamsInURI { if c.includeQueryParamsInURI {
c.Path = filepath.Join(c.parsedURI.Hostname(), c.parsedURI.RequestURI()) c.Path = filepath.Join(c.parsedURI.Hostname(), c.parsedURI.RequestURI())
} else { } else {
c.Path = filepath.Join(c.parsedURI.Hostname(), c.parsedURI.Path) c.Path = filepath.Join(c.parsedURI.Hostname(), c.parsedURI.Path)
} }
if c.config != nil {
if prefixPath, configErr := c.config.GetValue("prefix"); configErr == nil {
c.Path = filepath.Join(prefixPath.(string), c.Path)
}
}
if c.absPath, err = filepath.Abs(c.Path); err != nil {
return
}
if err = c.NormalizePath(); err != nil { if err = c.NormalizePath(); err != nil {
return return
} }
return return
} }
} }
err = fmt.Errorf("%w: %s is not a %s resource, parsed: %t", ErrInvalidResourceURI, c.URI(), c.Type(), (u != nil)) err = fmt.Errorf("%w: %s is not a %s resource, parsed: %t", ErrInvalidResourceURI, c.Uri, c.Type(), (u != nil))
return return
} }
@ -125,40 +117,26 @@ func (c *Common) UseConfig(config data.ConfigurationValueGetter) {
} }
func (c *Common) ResolveId(ctx context.Context) string { func (c *Common) ResolveId(ctx context.Context) string {
var err error if e := c.NormalizePath(); e != nil {
if c.absPath, err = filepath.Abs(c.Path); err != nil { panic(e)
panic(err)
}
if err = c.NormalizePath(); err != nil {
panic(err)
} }
return c.Path return c.Path
} }
// Common path normalization for a file resource. func (c *Common) NormalizePath() error {
func (c *Common) NormalizeFilePath() (err error) { if c.config != nil {
if c.normalizePath { if prefixPath, configErr := c.config.GetValue("prefix"); configErr == nil {
c.Path = c.absPath c.Path = filepath.Join(prefixPath.(string), c.Path)
}
} }
return if c.normalizePath {
filePath, fileAbsErr := filepath.Abs(c.Path)
if fileAbsErr == nil {
c.Path = filePath
}
return fileAbsErr
}
return nil
} }
func (c *Common) Type() string { return string(c.resourceType) } func (c *Common) Type() string { return string(c.resourceType) }
// If a resource update has errors but the resource is not actually absent
func (c *Common) IsResourceInconsistent() (result bool) {
for _, err := range c.Errors {
if ! errors.Is(err, ErrResourceStateAbsent) {
result = true
}
}
return
}
func (c *Common) AddError(err error) (error) {
if err != nil {
c.Errors = append(c.Errors, err)
}
return err
}

View File

@ -6,7 +6,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"testing" "testing"
"net/url" "net/url"
"decl/internal/folio"
) )
func TestNewCommon(t *testing.T) { func TestNewCommon(t *testing.T) {
@ -17,15 +16,16 @@ func TestNewCommon(t *testing.T) {
func TestCommon(t *testing.T) { func TestCommon(t *testing.T) {
expectedCommon := NewCommon(FileTypeName, false) expectedCommon := NewCommon(FileTypeName, false)
expectedCommon.resourceType = "file" expectedCommon.resourceType = "file"
expectedCommon.Uri = "file:///tmp/foo"
expectedCommon.parsedURI = &url.URL{ Scheme: "file", Path: "/tmp/foo"} expectedCommon.parsedURI = &url.URL{ Scheme: "file", Path: "/tmp/foo"}
expectedCommon.Path = "/tmp/foo" expectedCommon.Path = "/tmp/foo"
for _, v := range []struct{ uri folio.URI; expected *Common }{ for _, v := range []struct{ uri string; expected *Common }{
{ uri: "file:///tmp/foo", expected: expectedCommon }, { uri: "file:///tmp/foo", expected: expectedCommon },
}{ }{
c := NewCommon(FileTypeName, false) c := NewCommon(FileTypeName, false)
c.resourceType = "file" c.resourceType = "file"
assert.Nil(t, c.SetParsedURI(v.uri.Parse())) assert.Nil(t, c.SetURI(v.uri))
assert.Equal(t, v.expected.resourceType , c.resourceType) assert.Equal(t, v.expected.resourceType , c.resourceType)
assert.Equal(t, v.expected.Path, c.Path) assert.Equal(t, v.expected.Path, c.Path)
assert.Equal(t, v.expected.parsedURI.Scheme, c.parsedURI.Scheme) assert.Equal(t, v.expected.parsedURI.Scheme, c.parsedURI.Scheme)

View File

@ -16,7 +16,7 @@ import (
"github.com/docker/go-connections/nat" "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"
"log/slog" "log/slog"
"net/url" "net/url"
_ "os" _ "os"
@ -28,10 +28,6 @@ _ "os/exec"
"gitea.rosskeen.house/rosskeen.house/machine" "gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec" "decl/internal/codec"
"decl/internal/data" "decl/internal/data"
"decl/internal/folio"
"decl/internal/containerlog"
"bytes"
_ "encoding/base64"
) )
const ( const (
@ -46,63 +42,60 @@ type ContainerClient interface {
ContainerRemove(context.Context, string, container.RemoveOptions) error ContainerRemove(context.Context, string, container.RemoveOptions) error
ContainerStop(context.Context, string, container.StopOptions) error ContainerStop(context.Context, string, container.StopOptions) error
ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error)
ContainerLogs(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error)
Close() error Close() error
} }
type Container struct { type Container struct {
*Common `yaml:",inline" json:",inline"` *Common `yaml:",inline" json:",inline"`
stater machine.Stater `yaml:"-" json:"-"` stater machine.Stater `yaml:"-" json:"-"`
Id string `json:"ID,omitempty" yaml:"ID,omitempty"` Id string `json:"ID,omitempty" yaml:"ID,omitempty"`
Name string `json:"name" yaml:"name"` Name string `json:"name" yaml:"name"`
Cmd []string `json:"cmd,omitempty" yaml:"cmd,omitempty"` // Path string `json:"path" yaml:"path"`
WorkingDir string `json:"workingdir,omitempty" yaml:"workingdir,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"` 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"`
HostnamePath string `json:"hostnamepath" yaml:"hostnamepath"` HostnamePath string `json:"hostnamepath" yaml:"hostnamepath"`
HostsPath string `json:"hostpath" yaml:"hostspath"` HostsPath string `json:"hostpath" yaml:"hostspath"`
LogPath string `json:"logpath" yaml:"logpath"` LogPath string `json:"logpath" yaml:"logpath"`
Created string `json:"created" yaml:"created"` Created string `json:"created" yaml:"created"`
ContainerState types.ContainerState `json:"containerstate" yaml:"containerstate"` ContainerState types.ContainerState `json:"containerstate" yaml:"containerstate"`
RestartCount int `json:"restartcount" yaml:"restartcount"` RestartCount int `json:"restartcount" yaml:"restartcount"`
Driver string `json:"driver" yaml:"driver"` Driver string `json:"driver" yaml:"driver"`
Platform string `json:"platform" yaml:"platform"` Platform string `json:"platform" yaml:"platform"`
MountLabel string `json:"mountlabel" yaml:"mountlabel"` MountLabel string `json:"mountlabel" yaml:"mountlabel"`
ProcessLabel string `json:"processlabel" yaml:"processlabel"` ProcessLabel string `json:"processlabel" yaml:"processlabel"`
AppArmorProfile string `json:"apparmorprofile" yaml:"apparmorprofile"` AppArmorProfile string `json:"apparmorprofile" yaml:"apparmorprofile"`
ExecIDs []string `json:"execids" yaml:"execids"` ExecIDs []string `json:"execids" yaml:"execids"`
HostConfig container.HostConfig `json:"hostconfig" yaml:"hostconfig"` HostConfig container.HostConfig `json:"hostconfig" yaml:"hostconfig"`
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"` Networks []string `json:"networks,omitempty" yaml:"networks,omitempty"`
/* /*
Mounts []MountPoint Mounts []MountPoint
Config *container.Config Config *container.Config
NetworkSettings *NetworkSettings NetworkSettings *NetworkSettings
*/ */
Wait bool `json:"wait,omitempty" yaml:"wait,omitempty"` // State string `yaml:"state,omitempty" json:"state,omitempty"`
Stdout string `json:"stdout,omitempty" yaml:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty" yaml:"stderr,omitempty"`
// config ConfigurationValueGetter
apiClient ContainerClient apiClient ContainerClient
Resources data.ResourceMapper `json:"-" yaml:"-"` // Resources data.ResourceMapper `json:"-" yaml:"-"`
} }
func init() { func init() {
ResourceTypes.Register([]string{"container"}, func(u *url.URL) (c data.Resource) { ResourceTypes.Register([]string{"container"}, func(u *url.URL) data.Resource {
c = NewContainer(nil) c := NewContainer(nil)
if u != nil { c.Name = filepath.Join(u.Hostname(), u.Path)
if err := folio.CastParsedURI(u).ConstructResource(c); err != nil { if err := c.Common.SetParsedURI(u); err == nil {
panic(err) return c
}
} }
return return nil
}) })
} }
@ -121,22 +114,6 @@ func NewContainer(containerClientApi ContainerClient) *Container {
} }
} }
func (c *Container) Init(u data.URIParser) error {
if u == nil {
u = folio.URI(c.URI()).Parse()
}
uri := u.URL()
c.Name = filepath.Join(uri.Hostname(), uri.Path)
return c.SetParsedURI(u)
}
func (c *Container) SetParsedURI(u data.URIParser) (err error) {
if err = c.Common.SetParsedURI(u); err == nil {
c.Name = filepath.Join(c.Common.parsedURI.Hostname(), c.Common.parsedURI.Path)
}
return
}
func (c *Container) SetResourceMapper(resources data.ResourceMapper) { func (c *Container) SetResourceMapper(resources data.ResourceMapper) {
c.Resources = resources c.Resources = resources
} }
@ -145,7 +122,7 @@ func (c *Container) Clone() data.Resource {
return &Container { return &Container {
Id: c.Id, Id: c.Id,
Name: c.Name, Name: c.Name,
Common: c.Common.Clone(), Common: c.Common,
Cmd: c.Cmd, Cmd: c.Cmd,
Entrypoint: c.Entrypoint, Entrypoint: c.Entrypoint,
Args: c.Args, Args: c.Args,
@ -169,6 +146,7 @@ func (c *Container) Clone() data.Resource {
SizeRw: c.SizeRw, SizeRw: c.SizeRw,
SizeRootFs: c.SizeRootFs, SizeRootFs: c.SizeRootFs,
Networks: c.Networks, Networks: c.Networks,
// State: c.State,
apiClient: c.apiClient, apiClient: c.apiClient,
} }
} }
@ -180,6 +158,10 @@ func (c *Container) StateMachine() machine.Stater {
return c.stater return c.stater
} }
func (c *Container) UseConfig(config data.ConfigurationValueGetter) {
c.config = config
}
func (c *Container) JSON() ([]byte, error) { func (c *Container) JSON() ([]byte, error) {
return json.Marshal(c) return json.Marshal(c)
} }
@ -189,22 +171,17 @@ func (c *Container) Validate() error {
} }
func (c *Container) Notify(m *machine.EventMessage) { func (c *Container) Notify(m *machine.EventMessage) {
slog.Info("Container.Notify()", "destination_event", m.Dest, "uri", c.URI())
ctx := context.Background() ctx := context.Background()
switch m.On { switch m.On {
case machine.ENTERSTATEEVENT: case machine.ENTERSTATEEVENT:
switch m.Dest { switch m.Dest {
case "start_stat": case "start_stat":
if statErr := c.ReadStat(); statErr == nil { if statErr := c.ReadStat(); statErr == nil {
slog.Info("Container.Notify() - ReadStat", "event", "start_stat", "error", statErr)
if triggerErr := c.StateMachine().Trigger("exists"); triggerErr == nil { if triggerErr := c.StateMachine().Trigger("exists"); triggerErr == nil {
slog.Info("Container.Notify()", "event", "start_stat", "trigger", "exists")
return return
} }
} else { } else {
slog.Info("Container.Notify() - ReadStat", "event", "start_stat", "error", statErr)
if triggerErr := c.StateMachine().Trigger("notexists"); triggerErr == nil { if triggerErr := c.StateMachine().Trigger("notexists"); triggerErr == nil {
slog.Info("Container.Notify()", "event", "start_stat", "trigger", "notexists")
return return
} }
} }
@ -233,7 +210,6 @@ func (c *Container) Notify(m *machine.EventMessage) {
panic(createErr) panic(createErr)
} }
case "start_delete": case "start_delete":
slog.Info("Container.Notify()", "event", "start_delete")
if deleteErr := c.Delete(ctx); deleteErr == nil { if deleteErr := c.Delete(ctx); deleteErr == nil {
if triggerErr := c.StateMachine().Trigger("deleted"); triggerErr == nil { if triggerErr := c.StateMachine().Trigger("deleted"); triggerErr == nil {
return return
@ -257,24 +233,6 @@ func (c *Container) Notify(m *machine.EventMessage) {
} }
func (c *Container) ReadStat() (err error) { func (c *Container) ReadStat() (err error) {
err = fmt.Errorf("%w: %s", ErrResourceStateAbsent, c.Name)
filterArgs := filters.NewArgs()
filterArgs.Add("name", "/"+c.Name)
if containers, listErr := c.apiClient.ContainerList(context.Background(), container.ListOptions{
All: true,
Filters: filterArgs,
}); listErr == nil {
for _, container := range containers {
for _, containerName := range container.Names {
if containerName == "/"+c.Name {
slog.Info("Container.ReadStat() exists", "container", c.Name)
return nil
}
}
}
} else {
err = fmt.Errorf("%w: %w", err, listErr)
}
return return
} }
@ -308,61 +266,6 @@ func (c *Container) LoadDecl(yamlResourceDeclaration string) error {
return c.LoadString(yamlResourceDeclaration, codec.FormatYaml) return c.LoadString(yamlResourceDeclaration, codec.FormatYaml)
} }
func (c *Container) ReadFromContainer(ctx context.Context) (err error) {
var buf bytes.Buffer
var stdout, stderr []string
if stdoutReader, err := c.apiClient.ContainerLogs(ctx, c.Id, container.LogsOptions{ShowStdout: true, ShowStderr: true}); err != nil {
return err
} else {
defer stdoutReader.Close()
if _, copyErr := io.Copy(&buf, stdoutReader); copyErr != nil {
return copyErr
}
slog.Info("Container.ReadFromContainer() - ContainerLogs", "read", buf.String())
for {
if streamType, message, extractErr := containerlog.Read(&buf); extractErr == nil {
switch streamType {
case containerlog.StreamStdout:
stdout = append(stdout, message)
case containerlog.StreamStderr:
stderr = append(stderr, message)
}
} else {
if extractErr == io.EOF {
break
}
err = extractErr
}
/*
if streamType, size, extractErr := c.ExtractLogHeader(&buf); extractErr == nil {
slog.Info("Container.Create() - ContainerLogs", "streamtype", streamType, "size", size)
var logMessage string
if logMessage, err = c.ReadLogMessage(&buf, size); err == nil {
switch streamType {
case ContainerLogStreamStdout:
stdout = append(stdout, logMessage)
case ContainerLogStreamStderr:
stderr = append(stderr, logMessage)
}
}
} else {
if extractErr == io.EOF {
break
}
err = extractErr
}
*/
}
}
c.Stdout = strings.Join(stdout, "")
c.Stderr = strings.Join(stderr, "")
slog.Info("Container.ReadFromContainer()", "stdout", c.Stdout, "stderr", c.Stderr, "error", err)
return
}
func (c *Container) Create(ctx context.Context) error { func (c *Container) Create(ctx context.Context) error {
numberOfEnvironmentVariables := len(c.Environment) numberOfEnvironmentVariables := len(c.Environment)
@ -376,9 +279,6 @@ func (c *Container) Create(ctx context.Context) error {
Entrypoint: c.Entrypoint, Entrypoint: c.Entrypoint,
Tty: false, Tty: false,
ExposedPorts: portset, ExposedPorts: portset,
WorkingDir: c.WorkingDir,
AttachStdout: true,
AttachStderr: true,
} }
config.Env = make([]string, numberOfEnvironmentVariables) config.Env = make([]string, numberOfEnvironmentVariables)
@ -410,32 +310,20 @@ func (c *Container) Create(ctx context.Context) error {
} }
c.Id = resp.ID c.Id = resp.ID
/*
statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
select {
case err := <-errCh:
if err != nil {
panic(err)
}
case <-statusCh:
}
*/
if startErr := c.apiClient.ContainerStart(ctx, c.Id, container.StartOptions{}); startErr != nil { if startErr := c.apiClient.ContainerStart(ctx, c.Id, container.StartOptions{}); startErr != nil {
return startErr return startErr
} }
if err = c.ReadFromContainer(ctx); err != nil {
return err
}
if c.Wait {
slog.Info("Container.Create() - waiting for container to stop", "id", c.Id, "name", c.Name)
statusCh, errCh := c.apiClient.ContainerWait(ctx, c.Id, container.WaitConditionNotRunning)
select {
case err := <-errCh:
if err != nil {
panic(err)
}
case <-statusCh:
}
}
if len(c.Stdout) == 0 && len(c.Stderr) == 0 {
if err = c.ReadFromContainer(ctx); err != nil {
return err
}
}
return err return err
} }
@ -489,6 +377,9 @@ func (c *Container) Inspect(ctx context.Context, containerID string) error {
} }
c.Common.Path = containerJSON.Path c.Common.Path = containerJSON.Path
c.Image = containerJSON.Image c.Image = containerJSON.Image
if containerJSON.State != nil {
c.ContainerState = *containerJSON.State
}
c.Created = containerJSON.Created c.Created = containerJSON.Created
c.ResolvConfPath = containerJSON.ResolvConfPath c.ResolvConfPath = containerJSON.ResolvConfPath
c.HostnamePath = containerJSON.HostnamePath c.HostnamePath = containerJSON.HostnamePath
@ -496,18 +387,11 @@ func (c *Container) Inspect(ctx context.Context, containerID string) error {
c.LogPath = containerJSON.LogPath c.LogPath = containerJSON.LogPath
c.RestartCount = containerJSON.RestartCount c.RestartCount = containerJSON.RestartCount
c.Driver = containerJSON.Driver c.Driver = containerJSON.Driver
if containerJSON.State != nil {
c.ContainerState = *containerJSON.State
if c.ContainerState.ExitCode != 0 {
return fmt.Errorf("%s", c.ContainerState.Error)
}
}
} }
return nil return nil
} }
func (c *Container) Delete(ctx context.Context) error { func (c *Container) Delete(ctx context.Context) error {
slog.Info("Container.Delete()", "id", c.Id, "name", c.Name)
if stopErr := c.apiClient.ContainerStop(ctx, c.Id, container.StopOptions{}); stopErr != nil { if stopErr := c.apiClient.ContainerStop(ctx, c.Id, container.StopOptions{}); stopErr != nil {
slog.Error("Container.Delete() - failed to stop: ", "Id", c.Id, "error", stopErr) slog.Error("Container.Delete() - failed to stop: ", "Id", c.Id, "error", stopErr)
return stopErr return stopErr
@ -545,7 +429,7 @@ func (c *Container) Type() string { return "container" }
func (c *Container) ResolveId(ctx context.Context) string { func (c *Container) ResolveId(ctx context.Context) string {
var err error var err error
if err = c.Common.SetParsedURI(folio.URI(c.URI()).Parse()); err != nil { if err = c.Common.SetURI(c.URI()); err != nil {
triggerErr := c.StateMachine().Trigger("notexists") triggerErr := c.StateMachine().Trigger("notexists")
panic(fmt.Errorf("%w: %s %s, %w", err, c.Type(), c.Name, triggerErr)) panic(fmt.Errorf("%w: %s %s, %w", err, c.Type(), c.Name, triggerErr))
} }

View File

@ -101,14 +101,11 @@ type ContainerImage struct {
} }
func init() { func init() {
folio.DocumentRegistry.ResourceTypes.Register([]string{"container-image"}, func(u *url.URL) (c data.Resource) { folio.DocumentRegistry.ResourceTypes.Register([]string{"container-image"}, func(u *url.URL) data.Resource {
c = NewContainerImage(nil) c := NewContainerImage(nil)
if u != nil { c.Name = ContainerImageNameFromURI(u)
if err := folio.CastParsedURI(u).ConstructResource(c); err != nil { slog.Info("NewContainerImage", "container", c)
panic(err) return c
}
}
return
}) })
} }
@ -121,24 +118,13 @@ func NewContainerImage(containerClientApi ContainerImageClient) *ContainerImage
panic(err) panic(err)
} }
} }
c := &ContainerImage{ return &ContainerImage{
Common: NewCommon(ContainerImageTypeName, true), Common: &Common{ includeQueryParamsInURI: true, resourceType: ContainerImageTypeName },
apiClient: apiClient, apiClient: apiClient,
InjectJX: true, InjectJX: true,
PushImage: false, PushImage: false,
ConverterTypes: folio.DocumentRegistry.ConverterTypes, ConverterTypes: folio.DocumentRegistry.ConverterTypes,
} }
c.Common.NormalizePath = c.NormalizePath
return c
}
func (c *ContainerImage) Init(u data.URIParser) (err error) {
if u == nil {
u = folio.URI(c.URI()).Parse()
}
err = c.SetParsedURI(u)
c.Name = ContainerImageNameFromURI(u.URL())
return
} }
func (c *ContainerImage) RegistryAuthConfig() (authConfig registry.AuthConfig, err error) { func (c *ContainerImage) RegistryAuthConfig() (authConfig registry.AuthConfig, err error) {
@ -189,10 +175,6 @@ func (c *ContainerImage) RegistryAuth() (string, error) {
} }
} }
func (c *ContainerImage) NormalizePath() error {
return nil
}
func (c *ContainerImage) SetResourceMapper(resources data.ResourceMapper) { func (c *ContainerImage) SetResourceMapper(resources data.ResourceMapper) {
c.Resources = resources c.Resources = resources
} }
@ -267,6 +249,24 @@ func (c *ContainerImage) URI() string {
return URIFromContainerImageName(c.Name) return URIFromContainerImageName(c.Name)
} }
/*
func (c *ContainerImage) SetURI(uri string) error {
resourceUri, e := url.Parse(uri)
if e == nil {
if resourceUri.Scheme == c.Type() {
c.Name = strings.Join([]string{resourceUri.Hostname(), resourceUri.RequestURI()}, ":")
} else {
e = fmt.Errorf("%w: %s is not a %s", ErrInvalidResourceURI, uri, c.Type())
}
}
return e
}
func (c *ContainerImage) UseConfig(config data.ConfigurationValueGetter) {
c.config = config
}
*/
func (c *ContainerImage) JSON() ([]byte, error) { func (c *ContainerImage) JSON() ([]byte, error) {
return json.Marshal(c) return json.Marshal(c)
} }
@ -789,6 +789,12 @@ func (c *ContainerImage) Delete(ctx context.Context) error {
_, err := c.apiClient.ImageRemove(ctx, c.Id, options) _, err := c.apiClient.ImageRemove(ctx, c.Id, options)
return err return err
/*
for _, img := range deletedImages {
fmt.Printf("Deleted image: %s\n", img.Deleted)
fmt.Printf("Untagged image: %s\n", img.Untagged)
}
*/
} }
func (c *ContainerImage) Type() string { return "container-image" } func (c *ContainerImage) Type() string { return "container-image" }
@ -815,3 +821,5 @@ func (c *ContainerImage) ResolveId(ctx context.Context) string {
} }
return c.Id return c.Id
} }

View File

@ -15,6 +15,7 @@ _ "log/slog"
"net/url" "net/url"
_ "os" _ "os"
_ "os/exec" _ "os/exec"
"path/filepath"
_ "strings" _ "strings"
"encoding/json" "encoding/json"
"io" "io"
@ -38,33 +39,30 @@ type ContainerNetworkClient interface {
} }
type ContainerNetwork struct { type ContainerNetwork struct {
*Common `json:",inline" yaml:",inline"` *Common `json:",inline" yaml:",inline"`
stater machine.Stater `json:"-" yaml:"-"` stater machine.Stater `json:"-" yaml:"-"`
Id string `json:"ID,omitempty" yaml:"ID,omitempty"` Id string `json:"ID,omitempty" yaml:"ID,omitempty"`
Name string `json:"name" yaml:"name"` Name string `json:"name" yaml:"name"`
Driver string `json:"driver,omitempty" yaml:"driver,omitempty"` Driver string `json:"driver,omitempty" yaml:"driver,omitempty"`
EnableIPv6 bool `json:"enableipv6,omitempty" yaml:"enableipv6,omitempty"` EnableIPv6 bool `json:"enableipv6,omitempty" yaml:"enableipv6,omitempty"`
Internal bool `json:"internal,omitempty" yaml:"internal,omitempty"` Internal bool `json:"internal,omitempty" yaml:"internal,omitempty"`
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
Created time.Time `json:"created" yaml:"created"` Created time.Time `json:"created" yaml:"created"`
//State string `yaml:"state"`
apiClient ContainerNetworkClient apiClient ContainerNetworkClient
Resources data.ResourceMapper `json:"-" yaml:"-"` Resources data.ResourceMapper `json:"-" yaml:"-"`
} }
func init() { func init() {
folio.DocumentRegistry.ResourceTypes.Register([]string{"container-network"}, func(u *url.URL) (n data.Resource) { folio.DocumentRegistry.ResourceTypes.Register([]string{"container-network"}, func(u *url.URL) data.Resource {
n = NewContainerNetwork(nil) n := NewContainerNetwork(nil)
if u != nil { n.Name = filepath.Join(u.Hostname(), u.Path)
if err := folio.CastParsedURI(u).ConstructResource(n); err != nil { return n
panic(err)
}
}
return
}) })
} }
func NewContainerNetwork(containerClientApi ContainerNetworkClient) (cn *ContainerNetwork) { func NewContainerNetwork(containerClientApi ContainerNetworkClient) *ContainerNetwork {
var apiClient ContainerNetworkClient = containerClientApi var apiClient ContainerNetworkClient = containerClientApi
if apiClient == nil { if apiClient == nil {
var err error var err error
@ -73,23 +71,10 @@ func NewContainerNetwork(containerClientApi ContainerNetworkClient) (cn *Contain
panic(err) panic(err)
} }
} }
cn = &ContainerNetwork{ return &ContainerNetwork{
Common: &Common{ includeQueryParamsInURI: true, resourceType: ContainerNetworkTypeName },
apiClient: apiClient, apiClient: apiClient,
} }
cn.Common = NewCommon(ContainerNetworkTypeName, true)
cn.Common.NormalizePath = cn.NormalizePath
return cn
}
func (n *ContainerNetwork) Init(u data.URIParser) error {
if u == nil {
u = folio.URI(n.URI()).Parse()
}
return n.SetParsedURI(u)
}
func (n *ContainerNetwork) NormalizePath() error {
return nil
} }
func (n *ContainerNetwork) SetResourceMapper(resources data.ResourceMapper) { func (n *ContainerNetwork) SetResourceMapper(resources data.ResourceMapper) {
@ -98,9 +83,10 @@ func (n *ContainerNetwork) SetResourceMapper(resources data.ResourceMapper) {
func (n *ContainerNetwork) Clone() data.Resource { func (n *ContainerNetwork) Clone() data.Resource {
return &ContainerNetwork { return &ContainerNetwork {
Common: n.Common.Clone(), Common: n.Common,
Id: n.Id, Id: n.Id,
Name: n.Name, Name: n.Name,
//State: n.State,
apiClient: n.apiClient, apiClient: n.apiClient,
} }
} }
@ -162,6 +148,24 @@ func (n *ContainerNetwork) URI() string {
return fmt.Sprintf("container-network://%s", n.Name) 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) UseConfig(config data.ConfigurationValueGetter) {
n.config = config
}
*/
func (n *ContainerNetwork) JSON() ([]byte, error) { func (n *ContainerNetwork) JSON() ([]byte, error) {
return json.Marshal(n) return json.Marshal(n)
} }

View File

@ -5,21 +5,20 @@ package resource
import ( import (
"context" "context"
"decl/tests/mocks" "decl/tests/mocks"
_ "encoding/json" _ "encoding/json"
_ "fmt" _ "fmt"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"io" _ "io"
_ "net/http" _ "net/http"
_ "net/http/httptest" _ "net/http/httptest"
_ "net/url" _ "net/url"
_ "os" _ "os"
"strings" _ "strings"
"testing" "testing"
"bytes"
) )
func TestNewContainerResource(t *testing.T) { func TestNewContainerResource(t *testing.T) {
@ -56,9 +55,6 @@ func TestReadContainer(t *testing.T) {
go func() { resChan <- res }() go func() { resChan <- res }()
return resChan, errChan return resChan, errChan
}, },
InjectContainerLogs: func(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("done.")), nil
},
} }
c := NewContainer(m) c := NewContainer(m)
@ -91,9 +87,6 @@ func TestCreateContainer(t *testing.T) {
go func() { resChan <- res }() go func() { resChan <- res }()
return resChan, errChan return resChan, errChan
}, },
InjectContainerLogs: func(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("done.")), nil
},
} }
decl := ` decl := `
@ -114,37 +107,3 @@ func TestCreateContainer(t *testing.T) {
applyDeleteErr := c.Apply() applyDeleteErr := c.Apply()
assert.Equal(t, nil, applyDeleteErr) assert.Equal(t, nil, applyDeleteErr)
} }
// Detect the ContainerLog header for each entry
func TestContainerLogOutput(t *testing.T) {
logHeader := []byte{0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x05}
logHeader = append(logHeader, []byte(string("done."))...)
logs := bytes.NewReader(logHeader)
m := &mocks.MockContainerClient{
InjectContainerCreate: func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) {
return container.CreateResponse{ID: "abcdef012", Warnings: []string{}}, nil
},
InjectContainerStop: func(context.Context, string, container.StopOptions) error {
return nil
},
InjectContainerRemove: func(context.Context, string, container.RemoveOptions) error {
return nil
},
InjectContainerWait: func(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
var res container.WaitResponse
resChan := make(chan container.WaitResponse)
errChan := make(chan error, 1)
go func() { resChan <- res }()
return resChan, errChan
},
InjectContainerLogs: func(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
return io.NopCloser(logs), nil
},
}
c := NewContainer(m)
c.ReadFromContainer(context.Background())
assert.Equal(t, "done.", c.Stdout)
}

View File

@ -1,295 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
"context"
"fmt"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume"
_ "github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/client"
"gopkg.in/yaml.v3"
_ "log/slog"
"net/url"
_ "os"
_ "os/exec"
_ "strings"
"encoding/json"
"io"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
"decl/internal/data"
"decl/internal/folio"
"log/slog"
)
const (
ContainerVolumeTypeName TypeName = "container-volume"
)
type ContainerVolumeClient interface {
ContainerClient
VolumeCreate(ctx context.Context, options volume.CreateOptions) (volume.Volume, error)
VolumeList(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error)
VolumeInspect(ctx context.Context, volumeID string) (volume.Volume, error)
VolumeRemove(ctx context.Context, volumeID string, force bool) (error)
}
type ContainerVolume struct {
*Common `json:",inline" yaml:",inline"`
stater machine.Stater `json:"-" yaml:"-"`
volume.Volume `json:",inline" yaml:",inline"`
apiClient ContainerVolumeClient
Resources data.ResourceMapper `json:"-" yaml:"-"`
}
func init() {
folio.DocumentRegistry.ResourceTypes.Register([]string{"container-volume"}, func(u *url.URL) (n data.Resource) {
n = NewContainerVolume(nil)
if u != nil {
if err := folio.CastParsedURI(u).ConstructResource(n); err != nil {
panic(err)
}
}
return
})
}
func NewContainerVolume(containerClientApi ContainerVolumeClient) (cn *ContainerVolume) {
var apiClient ContainerVolumeClient = containerClientApi
if apiClient == nil {
var err error
apiClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}
}
cn = &ContainerVolume{
apiClient: apiClient,
}
cn.Common = NewCommon(ContainerVolumeTypeName, true)
cn.Common.NormalizePath = cn.NormalizePath
return cn
}
func (v *ContainerVolume) Init(u data.URIParser) error {
if u == nil {
u = folio.URI(v.URI()).Parse()
}
return v.SetParsedURI(u)
}
func (v *ContainerVolume) NormalizePath() error {
return nil
}
func (v *ContainerVolume) SetResourceMapper(resources data.ResourceMapper) {
v.Resources = resources
}
func (v *ContainerVolume) Clone() data.Resource {
return &ContainerVolume {
Common: v.Common.Clone(),
Volume: v.Volume,
apiClient: v.apiClient,
}
}
func (v *ContainerVolume) StateMachine() machine.Stater {
if v.stater == nil {
v.stater = StorageMachine(v)
}
return v.stater
}
func (v *ContainerVolume) Notify(m *machine.EventMessage) {
ctx := context.Background()
slog.Info("Notify()", "ContainerVolume", v, "m", m)
switch m.On {
case machine.ENTERSTATEEVENT:
switch m.Dest {
case "start_read":
if _,readErr := v.Read(ctx); readErr == nil {
if triggerErr := v.StateMachine().Trigger("state_read"); triggerErr == nil {
return
} else {
v.Common.State = "absent"
panic(triggerErr)
}
} else {
v.Common.State = "absent"
panic(readErr)
}
case "start_delete":
if deleteErr := v.Delete(ctx); deleteErr == nil {
if triggerErr := v.StateMachine().Trigger("deleted"); triggerErr == nil {
return
} else {
v.Common.State = "present"
panic(triggerErr)
}
} else {
v.Common.State = "present"
panic(deleteErr)
}
case "start_create":
if e := v.Create(ctx); e == nil {
if triggerErr := v.StateMachine().Trigger("created"); triggerErr == nil {
return
}
}
v.Common.State = "absent"
case "absent":
v.Common.State = "absent"
case "present", "created", "read":
v.Common.State = "present"
}
case machine.EXITSTATEEVENT:
}
}
func (v *ContainerVolume) URI() string {
return fmt.Sprintf("container-volume://%s", v.Name)
}
func (v *ContainerVolume) JSON() ([]byte, error) {
return json.Marshal(v)
}
func (v *ContainerVolume) Validate() error {
return fmt.Errorf("failed")
}
func (v *ContainerVolume) Apply() error {
ctx := context.Background()
switch v.Common.State {
case "absent":
return v.Delete(ctx)
case "present":
return v.Create(ctx)
}
return nil
}
func (v *ContainerVolume) Load(docData []byte, f codec.Format) (err error) {
err = f.StringDecoder(string(docData)).Decode(v)
return
}
func (v *ContainerVolume) LoadReader(r io.ReadCloser, f codec.Format) (err error) {
err = f.Decoder(r).Decode(v)
return
}
func (v *ContainerVolume) LoadString(docData string, f codec.Format) (err error) {
err = f.StringDecoder(docData).Decode(v)
return
}
func (n *ContainerVolume) LoadDecl(yamlResourceDeclaration string) error {
return n.LoadString(yamlResourceDeclaration, codec.FormatYaml)
}
func (v *ContainerVolume) Create(ctx context.Context) (err error) {
var spec volume.ClusterVolumeSpec
if v.ClusterVolume != nil {
spec = v.ClusterVolume.Spec
}
v.Volume, err = v.apiClient.VolumeCreate(ctx, volume.CreateOptions{
Name: v.Name,
Driver: v.Driver,
DriverOpts: v.Options,
Labels: v.Labels,
ClusterVolumeSpec: &spec,
})
if err != nil {
panic(err)
}
return nil
}
func (v *ContainerVolume) Inspect(ctx context.Context, volumeID string) error {
volumeInspect, err := v.apiClient.VolumeInspect(ctx, volumeID)
if client.IsErrNotFound(err) {
v.Common.State = "absent"
} else {
v.Common.State = "present"
v.Volume = volumeInspect
if v.Name == "" {
if volumeInspect.Name[0] == '/' {
v.Name = volumeInspect.Name[1:]
} else {
v.Name = volumeInspect.Name
}
}
}
return nil
}
func (v *ContainerVolume) Read(ctx context.Context) ([]byte, error) {
var volumeID string
filterArgs := filters.NewArgs()
filterArgs.Add("name", v.Name)
volumes, err := v.apiClient.VolumeList(ctx, volume.ListOptions{
Filters: filterArgs,
})
if err != nil {
return nil, fmt.Errorf("%w: %s %s", err, v.Type(), v.Name)
}
for _, vol := range volumes.Volumes {
if vol.Name == v.Name {
volumeID = vol.Name
}
}
if inspectErr := v.Inspect(ctx, volumeID); inspectErr != nil {
return nil, fmt.Errorf("%w: volume %s", inspectErr, volumeID)
}
slog.Info("Read() ", "type", v.Type(), "name", v.Name)
return yaml.Marshal(v)
}
func (v *ContainerVolume) Update(ctx context.Context) error {
return v.Create(ctx)
}
func (v *ContainerVolume) Delete(ctx context.Context) error {
return nil
}
func (v *ContainerVolume) Type() string { return "container-volume" }
func (v *ContainerVolume) ResolveId(ctx context.Context) string {
v.Inspect(ctx, v.Name)
return v.Name
/*
volumes, err := n.apiClient.VolumeInspect(ctx, volume.ListOptions{
filterArgs := filters.NewArgs()
filterArgs.Add("name", "/"+n.Name)
volumes, err := n.apiClient.VolumeList(ctx, volume.ListOptions{
All: true,
Filters: filterArgs,
})
if err != nil {
panic(fmt.Errorf("%w: %s %s", err, n.Type(), n.Name))
}
for _, volume := range volumes {
for _, containerName := range volume.Name {
if containerName == n.Name {
if n.Id == "" {
n.Id = container.ID
}
return container.ID
}
}
}
return ""
*/
}

View File

@ -1,50 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
"context"
"decl/tests/mocks"
"github.com/docker/docker/api/types/volume"
"github.com/stretchr/testify/assert"
"testing"
)
func TestNewContainerVolumeResource(t *testing.T) {
c := NewContainerVolume(&mocks.MockContainerClient{})
assert.NotNil(t, c)
}
func TestReadContainerVolume(t *testing.T) {
ctx := context.Background()
decl := `
name: "testcontainervolume"
state: present
`
m := &mocks.MockContainerClient{
InjectVolumeList: func(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) {
return volume.ListResponse{
Volumes: []*volume.Volume{},
Warnings: []string{},
}, nil
},
InjectVolumeInspect: func(ctx context.Context, volumeID string) (volume.Volume, error) {
return volume.Volume{
Name: "test",
Driver: "local",
Mountpoint: "/src",
}, nil
},
}
v := NewContainerVolume(m)
assert.NotNil(t, v)
e := v.LoadDecl(decl)
assert.Nil(t, e)
assert.Equal(t, "testcontainervolume", v.Name)
resourceYaml, readContainerVolumeErr := v.Read(ctx)
assert.Equal(t, nil, readContainerVolumeErr)
assert.Greater(t, len(resourceYaml), 0)
}

View File

@ -1,29 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
// Container resource
package resource
import (
"fmt"
"errors"
)
type ContainerLogStreamType byte
const (
ContainerLogStreamStdin ContainerLogStreamType = 0x0
ContainerLogStreamStdout ContainerLogStreamType = 0x1
ContainerLogStreamStderr ContainerLogStreamType = 0x2
)
var (
ErrContainerLogInvalidStreamType error = errors.New("Invalid container log stream type")
)
func (s ContainerLogStreamType) Validate() error {
switch s {
case ContainerLogStreamStdin, ContainerLogStreamStdout, ContainerLogStreamStderr:
return nil
}
return fmt.Errorf("%w: %d", ErrContainerLogInvalidStreamType, s)
}

View File

@ -1,20 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestContainerLogStreamType(t *testing.T) {
for _, v := range []struct{ expected error; value ContainerLogStreamType } {
{ expected: nil, value: 0x0 },
{ expected: nil, value: 0x1 },
{ expected: nil, value: 0x2 },
{ expected: ErrContainerLogInvalidStreamType, value: 0x3 },
{ expected: ErrContainerLogInvalidStreamType, value: 0x4 },
} {
assert.ErrorIs(t, v.value.Validate(), v.expected)
}
}

View File

@ -39,22 +39,9 @@ type Exec struct {
} }
func init() { func init() {
folio.DocumentRegistry.ResourceTypes.Register([]string{"exec"}, func(u *url.URL) (res data.Resource) { folio.DocumentRegistry.ResourceTypes.Register([]string{"exec"}, func(u *url.URL) data.Resource {
x := NewExec() x := NewExec()
res = x return x
if u != nil {
uri := folio.CastParsedURI(u)
if ri, ok := res.(data.ResourceInitializer); ok {
if err := ri.Init(uri); err != nil {
panic(err)
}
} else {
if err := x.SetParsedURI(uri); err != nil {
panic(err)
}
}
}
return
}) })
} }
@ -69,7 +56,7 @@ func (x *Exec) SetResourceMapper(resources data.ResourceMapper) {
func (x *Exec) Clone() data.Resource { func (x *Exec) Clone() data.Resource {
return &Exec { return &Exec {
Common: x.Common.Clone(), Common: x.Common,
Id: x.Id, Id: x.Id,
CreateTemplate: x.CreateTemplate, CreateTemplate: x.CreateTemplate,
ReadTemplate: x.ReadTemplate, ReadTemplate: x.ReadTemplate,
@ -89,16 +76,13 @@ func (x *Exec) URI() string {
return fmt.Sprintf("exec://%s", x.Id) return fmt.Sprintf("exec://%s", x.Id)
} }
func (x *Exec) Init(u data.URIParser) (err error) { func (x *Exec) SetURI(uri string) (err error) {
if u == nil { err = x.Common.SetURI(uri)
u = folio.URI(x.URI()).Parse()
}
err = x.SetParsedURI(u)
x.Id = x.Common.Path x.Id = x.Common.Path
return return
} }
func (x *Exec) SetParsedURI(uri data.URIParser) (err error) { func (x *Exec) SetParsedURI(uri *url.URL) (err error) {
err = x.Common.SetParsedURI(uri) err = x.Common.SetParsedURI(uri)
x.Id = x.Common.Path x.Id = x.Common.Path
return return

View File

@ -4,21 +4,20 @@
package resource package resource
import ( import (
_ "context" _ "context"
_ "encoding/json" _ "encoding/json"
_ "fmt" _ "fmt"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
_ "gopkg.in/yaml.v3" _ "gopkg.in/yaml.v3"
_ "io" _ "io"
_ "log" _ "log"
_ "net/http" _ "net/http"
_ "net/http/httptest" _ "net/http/httptest"
_ "net/url" _ "net/url"
_ "os" _ "os"
_ "strings" _ "strings"
"testing" "testing"
"decl/internal/command" "decl/internal/command"
"decl/internal/folio"
) )
func TestNewExecResource(t *testing.T) { func TestNewExecResource(t *testing.T) {
@ -56,10 +55,9 @@ func TestCreateExec(t *testing.T) {
} }
func TestExecSetURI(t *testing.T) { func TestExecSetURI(t *testing.T) {
var uri folio.URI = "exec://12345_key"
x := NewExec() x := NewExec()
assert.NotNil(t, x) assert.NotNil(t, x)
e := x.SetParsedURI(uri.Parse()) e := x.SetURI("exec://" + "12345_key")
assert.Nil(t, e) assert.Nil(t, e)
assert.Equal(t, "exec", x.Type()) assert.Equal(t, "exec", x.Type())
assert.Equal(t, "12345_key", x.Path) assert.Equal(t, "12345_key", x.Path)

View File

@ -14,6 +14,7 @@ import (
"net/url" "net/url"
"os" "os"
"os/user" "os/user"
"path/filepath"
"strconv" "strconv"
"syscall" "syscall"
"time" "time"
@ -55,14 +56,19 @@ var ErrInvalidFileGroup error = errors.New("Unknown Group")
type FileMode string type FileMode string
func init() { func init() {
folio.DocumentRegistry.ResourceTypes.Register([]string{"file"}, func(u *url.URL) (res data.Resource) { folio.DocumentRegistry.ResourceTypes.Register([]string{"file"}, func(u *url.URL) data.Resource {
f := NewFile() f := NewFile()
slog.Info("FileFactory", "uri", u) f.parsedURI = u
if u != nil { //f.Uri.SetURL(u)
if err := folio.CastParsedURI(u).ConstructResource(f); err != nil { f.Path = filepath.Join(u.Hostname(), u.Path)
panic(err) f.exttype, f.fileext = f.Uri.Extension()
}
slog.Info("folio.DocumentRegistry.ResourceTypes.Register()()", "url", u, "file", f)
/*
if absPath, err := filepath.Abs(f.Path); err == nil {
f.Filesystem = os.DirFS(filepath.Dir(absPath))
} }
*/
return f return f
}) })
} }
@ -77,12 +83,18 @@ The `SerializeContent` the flag allows forcing the content to be serialized in t
*/ */
type File struct { type File struct {
*Common `json:",inline" yaml:",inline"` Uri folio.URI `json:"uri,omitempty" yaml:"uri,omitempty"`
parsedURI *url.URL `json:"-" yaml:"-"`
Filesystem fs.FS `json:"-" yaml:"-"` Filesystem fs.FS `json:"-" yaml:"-"`
exttype string `json:"-" yaml:"-"`
fileext string `json:"-" yaml:"-"`
stater machine.Stater `json:"-" yaml:"-"` stater machine.Stater `json:"-" yaml:"-"`
normalizePath bool `json:"-" yaml:"-"`
absPath string `json:"-" yaml:"-"`
basePath int `json:"-" yaml:"-"` basePath int `json:"-" yaml:"-"`
Path string `json:"path" yaml:"path"`
Owner string `json:"owner" yaml:"owner"` Owner string `json:"owner" yaml:"owner"`
Group string `json:"group" yaml:"group"` Group string `json:"group" yaml:"group"`
Mode FileMode `json:"mode" yaml:"mode"` Mode FileMode `json:"mode" yaml:"mode"`
@ -97,8 +109,10 @@ 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,omitempty" yaml:"state,omitempty"`
SerializeContent bool `json:"serializecontent,omitempty" yaml:"serializecontent,omitempty"` SerializeContent bool `json:"serializecontent,omitempty" yaml:"serializecontent,omitempty"`
GzipContent bool `json:"gzipcontent,omitempty" yaml:"gzipcontent,omitempty"` GzipContent bool `json:"gzipcontent,omitempty" yaml:"gzipcontent,omitempty"`
config data.ConfigurationValueGetter
Resources data.ResourceMapper `json:"-" yaml:"-"` Resources data.ResourceMapper `json:"-" yaml:"-"`
} }
@ -109,48 +123,22 @@ type ResourceFileInfo struct {
func NewFile() *File { func NewFile() *File {
currentUser, _ := user.Current() currentUser, _ := user.Current()
group, _ := user.LookupGroupId(currentUser.Gid) group, _ := user.LookupGroupId(currentUser.Gid)
f := &File{ f := &File{ normalizePath: false, Owner: currentUser.Username, Group: group.Name, Mode: "0644", FileType: RegularFile, SerializeContent: false }
Common: NewCommon(FileTypeName, true),
Owner: currentUser.Username,
Group: group.Name,
Mode: "0644",
FileType: RegularFile,
SerializeContent: false,
}
f.PathNormalization(false)
slog.Info("NewFile()", "file", f) slog.Info("NewFile()", "file", f)
return f return f
} }
func NewNormalizedFile() *File { func NewNormalizedFile() *File {
f := NewFile() f := NewFile()
f.PathNormalization(true) f.normalizePath = true
return f return f
} }
func (f *File) Init(u data.URIParser) error {
if u == nil {
u = folio.URI(f.URI()).Parse()
}
return f.SetParsedURI(u)
}
func (f *File) NormalizePath() error {
return f.Common.NormalizePath()
}
func (f *File) ContentType() string { func (f *File) ContentType() string {
var ext strings.Builder
if f.parsedURI.Scheme != "file" { if f.parsedURI.Scheme != "file" {
return f.parsedURI.Scheme return f.parsedURI.Scheme
} }
if f.fileext == "" { return f.exttype
return f.exttype
}
ext.WriteString(f.exttype)
ext.WriteRune('.')
ext.WriteString(f.fileext)
return ext.String()
} }
func (f *File) SetResourceMapper(resources data.ResourceMapper) { func (f *File) SetResourceMapper(resources data.ResourceMapper) {
@ -159,7 +147,13 @@ func (f *File) SetResourceMapper(resources data.ResourceMapper) {
func (f *File) Clone() data.Resource { func (f *File) Clone() data.Resource {
return &File { return &File {
Common: f.Common.Clone(), Uri: f.Uri,
parsedURI: f.parsedURI,
exttype: f.exttype,
fileext: f.fileext,
normalizePath: f.normalizePath,
absPath: f.absPath,
Path: f.Path,
Owner: f.Owner, Owner: f.Owner,
Group: f.Group, Group: f.Group,
Mode: f.Mode, Mode: f.Mode,
@ -171,6 +165,7 @@ func (f *File) Clone() data.Resource {
Size: f.Size, Size: f.Size,
Target: f.Target, Target: f.Target,
FileType: f.FileType, FileType: f.FileType,
State: f.State,
} }
} }
@ -187,98 +182,57 @@ func (f *File) Notify(m *machine.EventMessage) {
switch m.On { switch m.On {
case machine.ENTERSTATEEVENT: case machine.ENTERSTATEEVENT:
switch m.Dest { switch m.Dest {
case "start_stat": case "start_stat":
if statErr := f.ReadStat(); statErr == nil { if statErr := f.ReadStat(); statErr == nil {
if triggerErr := f.StateMachine().Trigger("exists"); triggerErr == nil { if triggerErr := f.StateMachine().Trigger("exists"); triggerErr == nil {
return return
} }
} else {
if triggerErr := f.StateMachine().Trigger("notexists"); triggerErr == nil {
return
}
}
case "start_read":
if _,readErr := f.Read(ctx); readErr == nil {
if triggerErr := f.StateMachine().Trigger("state_read"); triggerErr == nil {
return
} else { } else {
_ = f.AddError(triggerErr) if triggerErr := f.StateMachine().Trigger("notexists"); triggerErr == nil {
} return
} else {
_ = f.AddError(readErr)
if f.IsResourceInconsistent() {
if triggerErr := f.StateMachine().Trigger("read-failed"); triggerErr == nil {
panic(readErr)
} else {
_ = f.AddError(triggerErr)
panic(fmt.Errorf("%w - %w", readErr, triggerErr))
} }
} }
_ = f.AddError(f.StateMachine().Trigger("notexists")) case "start_read":
if _,readErr := f.Read(ctx); readErr == nil {
if triggerErr := f.StateMachine().Trigger("state_read"); triggerErr == nil {
return
} else {
f.State = "absent"
panic(triggerErr)
}
} else {
f.State = "absent"
if ! errors.Is(readErr, ErrResourceStateAbsent) {
panic(readErr)
}
} }
case "start_create": case "start_create":
if createErr := f.Create(ctx); createErr == nil { if e := f.Create(ctx); e == nil {
if triggerErr := f.StateMachine().Trigger("created"); triggerErr == nil { if triggerErr := f.StateMachine().Trigger("created"); triggerErr == nil {
return return
} else {
_ = f.AddError(triggerErr)
} }
} else { } else {
_ = f.AddError(createErr) f.State = "absent"
if f.IsResourceInconsistent() { panic(e)
if triggerErr := f.StateMachine().Trigger("create-failed"); triggerErr == nil {
panic(createErr)
} else {
_ = f.AddError(triggerErr)
panic(fmt.Errorf("%w - %w", createErr, triggerErr))
}
}
_ = f.StateMachine().Trigger("notexists")
panic(createErr)
}
case "start_update":
if updateErr := f.Update(ctx); updateErr == nil {
if triggerErr := f.stater.Trigger("updated"); triggerErr == nil {
return
} else {
_ = f.AddError(triggerErr)
}
} else {
_ = f.AddError(updateErr)
if f.IsResourceInconsistent() {
if triggerErr := f.StateMachine().Trigger("update-failed"); triggerErr == nil {
panic(updateErr)
} else {
panic(fmt.Errorf("%w - %w", updateErr, triggerErr))
}
}
_ = f.StateMachine().Trigger("notexists")
panic(updateErr)
} }
case "start_delete": case "start_delete":
if deleteErr := f.Delete(ctx); deleteErr == nil { if deleteErr := f.Delete(ctx); deleteErr == nil {
if triggerErr := f.StateMachine().Trigger("deleted"); triggerErr == nil { if triggerErr := f.StateMachine().Trigger("deleted"); triggerErr == nil {
return return
} else { } else {
f.Common.State = "present" f.State = "present"
panic(triggerErr) panic(triggerErr)
} }
} else { } else {
_ = f.StateMachine().Trigger("exists") f.State = "present"
panic(deleteErr) panic(deleteErr)
} }
case "inconsistent":
f.Common.State = "inconsistent"
case "absent": case "absent":
f.Common.State = "absent" f.State = "absent"
case "present", "created", "read": case "present", "created", "read":
f.Common.State = "present" f.State = "present"
} }
case machine.EXITSTATEEVENT: case machine.EXITSTATEEVENT:
switch m.Dest {
case "start_create":
slog.Info("File.Notify - EXITSTATE", "dest", m.Dest, "common.state", f.Common.State)
}
} }
} }
@ -286,8 +240,12 @@ func (f *File) SetGzipContent(flag bool) {
f.GzipContent = flag f.GzipContent = flag
} }
func (f *File) PathNormalization(flag bool) {
f.normalizePath = flag
}
func (f *File) FilePath() string { func (f *File) FilePath() string {
return f.Common.Path return f.Path
} }
func (f *File) SetFS(fsys fs.FS) { func (f *File) SetFS(fsys fs.FS) {
@ -295,23 +253,63 @@ func (f *File) SetFS(fsys fs.FS) {
} }
func (f *File) URI() string { func (f *File) URI() string {
return fmt.Sprintf("file://%s", f.Common.Path) return fmt.Sprintf("file://%s", f.Path)
} }
func (f *File) RelativePath() string { func (f *File) RelativePath() string {
return f.Common.Path[f.basePath:] return f.Path[f.basePath:]
} }
func (f *File) SetBasePath(index int) { func (f *File) SetBasePath(index int) {
if index < len(f.Common.Path) { if index < len(f.Path) {
f.basePath = index f.basePath = index
} }
} }
func (f *File) SetURI(uri string) (err error) {
slog.Info("File.SetURI()", "uri", uri, "file", f, "parsed", f.parsedURI)
f.SetURIFromString(uri)
err = f.SetParsedURI(f.Uri.Parse())
return
}
func (f *File) DetectGzip() bool { func (f *File) DetectGzip() bool {
return (f.parsedURI.Query().Get("gzip") == "true" || f.fileext == "gz" || f.exttype == "tgz" || f.exttype == "gz" || f.fileext == "tgz" ) return (f.parsedURI.Query().Get("gzip") == "true" || f.fileext == "gz" || f.exttype == "tgz" || f.exttype == "gz" || f.fileext == "tgz" )
} }
func (f *File) SetURIFromString(uri string) {
f.Uri = folio.URI(uri)
f.exttype, f.fileext = f.Uri.Extension()
}
func (f *File) SetParsedURI(u *url.URL) (err error) {
if u != nil {
if u.Scheme == "" {
u.Scheme = "file"
f.Uri = ""
}
if f.Uri.IsEmpty() {
f.SetURIFromString(u.String())
}
slog.Info("File.SetParsedURI()", "parsed", u, "path", f.Path)
f.parsedURI = u
if f.parsedURI.Scheme == "file" {
f.Path = filepath.Join(f.parsedURI.Hostname(), f.parsedURI.Path)
slog.Info("File.SetParsedURI()", "path", f.Path)
if err = f.NormalizePath(); err != nil {
return
}
return
}
}
err = fmt.Errorf("%w: %s is not a file", ErrInvalidResourceURI, f.Uri)
return
}
func (f *File) UseConfig(config data.ConfigurationValueGetter) {
f.config = config
}
func (f *File) JSON() ([]byte, error) { func (f *File) JSON() ([]byte, error) {
return json.Marshal(f) return json.Marshal(f)
} }
@ -327,7 +325,7 @@ func (f *File) Validate() (err error) {
func (f *File) Apply() error { func (f *File) Apply() error {
ctx := context.Background() ctx := context.Background()
switch f.Common.State { switch f.State {
case "absent": case "absent":
return f.Delete(ctx) return f.Delete(ctx)
case "present": case "present":
@ -369,10 +367,9 @@ func (f *File) ResolveId(ctx context.Context) string {
if e := f.NormalizePath(); e != nil { if e := f.NormalizePath(); e != nil {
panic(e) panic(e)
} }
return f.Common.Path return f.Path
} }
/*
func (f *File) NormalizePath() (err error) { func (f *File) NormalizePath() (err error) {
if f.config != nil { if f.config != nil {
if prefixPath, configErr := f.config.GetValue("prefix"); configErr == nil { if prefixPath, configErr := f.config.GetValue("prefix"); configErr == nil {
@ -384,7 +381,6 @@ func (f *File) NormalizePath() (err error) {
} }
return return
} }
*/
func (f *File) GetContentSourceRef() string { func (f *File) GetContentSourceRef() string {
return string(f.ContentSourceRef) return string(f.ContentSourceRef)
@ -397,7 +393,7 @@ func (f *File) SetContentSourceRef(uri string) {
func (f *File) Stat() (info fs.FileInfo, err error) { func (f *File) Stat() (info fs.FileInfo, err error) {
if _, ok := f.Filesystem.(embed.FS); ok { if _, ok := f.Filesystem.(embed.FS); ok {
info, err = fs.Stat(f.Filesystem, f.Common.Path) info, err = fs.Stat(f.Filesystem, f.Path)
} else { } else {
info, err = os.Lstat(f.absPath) info, err = os.Lstat(f.absPath)
} }
@ -457,12 +453,12 @@ func (f *File) Create(ctx context.Context) error {
switch f.FileType { switch f.FileType {
case SymbolicLinkFile: case SymbolicLinkFile:
linkErr := os.Symlink(f.Target, f.Common.Path) linkErr := os.Symlink(f.Target, f.Path)
if linkErr != nil { if linkErr != nil {
return linkErr return linkErr
} }
case DirectoryFile: case DirectoryFile:
if mkdirErr := os.MkdirAll(f.Common.Path, mode); mkdirErr != nil { if mkdirErr := os.MkdirAll(f.Path, mode); mkdirErr != nil {
return mkdirErr return mkdirErr
} }
default: default:
@ -490,8 +486,7 @@ func (f *File) Create(ctx context.Context) error {
}) })
var createdFileWriter io.WriteCloser var createdFileWriter io.WriteCloser
createdFile, fileErr := os.Create(f.Common.Path) createdFile, fileErr := os.Create(f.Path)
slog.Info("File.Create(): os.Create()", "path", f.Common.Path, "error", fileErr)
if fileErr != nil { if fileErr != nil {
return fileErr return fileErr
} }
@ -505,7 +500,6 @@ func (f *File) Create(ctx context.Context) error {
defer createdFile.Close() defer createdFile.Close()
slog.Info("File.Create(): Chmod()", "path", f.Common.Path, "mode", mode)
if chmodErr := createdFile.Chmod(mode); chmodErr != nil { if chmodErr := createdFile.Chmod(mode); chmodErr != nil {
return chmodErr return chmodErr
} }
@ -517,20 +511,16 @@ func (f *File) Create(ctx context.Context) error {
f.Sha256 = fmt.Sprintf("%x", hash.Sum(nil)) f.Sha256 = fmt.Sprintf("%x", hash.Sum(nil))
if !f.Mtime.IsZero() && !f.Atime.IsZero() { if !f.Mtime.IsZero() && !f.Atime.IsZero() {
slog.Info("File.Create(): Chtimes()", "path", f.Common.Path, "atime", f.Atime, "mtime", f.Mtime) if chtimesErr := os.Chtimes(f.Path, f.Atime, f.Mtime); chtimesErr != nil {
if chtimesErr := os.Chtimes(f.Common.Path, f.Atime, f.Mtime); chtimesErr != nil {
return chtimesErr return chtimesErr
} }
} else {
slog.Info("File.Create(): Chtimes() SKIPPED", "path", f.Common.Path, "atime", f.Atime, "mtime", f.Mtime)
} }
} }
slog.Info("File.Create(): Chown()", "path", f.Common.Path, "uid", uid, "gid", gid)
if chownErr := os.Chown(f.Common.Path, uid, gid); chownErr != nil { if chownErr := os.Chown(f.Path, uid, gid); chownErr != nil {
return chownErr return chownErr
} }
f.Common.State = "present" f.State = "present"
return nil return nil
} }
@ -539,7 +529,7 @@ func (f *File) Update(ctx context.Context) error {
} }
func (f *File) Delete(ctx context.Context) error { func (f *File) Delete(ctx context.Context) error {
return os.Remove(f.Common.Path) return os.Remove(f.Path)
} }
func (f *File) UpdateContentAttributes() { func (f *File) UpdateContentAttributes() {
@ -595,11 +585,11 @@ func (f *File) ContentSourceRefStat() (info fs.FileInfo) {
func (f *File) ReadStat() (err error) { func (f *File) ReadStat() (err error) {
var info fs.FileInfo var info fs.FileInfo
slog.Info("ReadStat()", "filesystem", f.Filesystem, "path", f.Common.Path) slog.Info("ReadStat()", "filesystem", f.Filesystem, "path", f.Path)
info, err = f.Stat() info, err = f.Stat()
slog.Info("ReadStat()", "filesystem", f.Filesystem, "path", f.Common.Path, "info", info, "error", err) slog.Info("ReadStat()", "filesystem", f.Filesystem, "path", f.Path, "info", info, "error", err)
if err == nil { if err == nil {
_ = f.SetFileInfo(info) _ = f.SetFileInfo(info)
@ -613,9 +603,9 @@ func (f *File) ReadStat() (err error) {
} }
slog.Info("ReadStat()", "stat", info, "path", f.Common.Path) slog.Info("ReadStat()", "stat", info, "path", f.Path)
if err != nil { if err != nil {
f.Common.State = "absent" f.State = "absent"
return return
} }
@ -623,29 +613,27 @@ func (f *File) ReadStat() (err error) {
} }
func (f *File) open() (file io.ReadCloser, err error) { func (f *File) open() (file io.ReadCloser, err error) {
slog.Info("open()", "file", f.Common.Path, "fs", f.Filesystem) slog.Info("open()", "file", f.Path, "fs", f.Filesystem)
if _, ok := f.Filesystem.(embed.FS); ok { if _, ok := f.Filesystem.(embed.FS); ok {
file, err = f.Filesystem.Open(f.Common.Path) file, err = f.Filesystem.Open(f.Path)
} else { } else {
file, err = os.Open(f.Common.Path) file, err = os.Open(f.Path)
} }
if f.GzipContent && f.DetectGzip() { if f.GzipContent && f.DetectGzip() {
file, err = gzip.NewReader(file) file, err = gzip.NewReader(file)
} }
slog.Info("open()", "file", f.Common.Path, "error", err) slog.Info("open()", "file", f.Path, "error", err)
return return
} }
func (f *File) Read(ctx context.Context) ([]byte, error) { func (f *File) Read(ctx context.Context) ([]byte, error) {
/*
if normalizePathErr := f.NormalizePath(); normalizePathErr != nil { if normalizePathErr := f.NormalizePath(); normalizePathErr != nil {
return nil, normalizePathErr return nil, normalizePathErr
} }
*/
statErr := f.ReadStat() statErr := f.ReadStat()
if statErr != nil { if statErr != nil {
return nil, fmt.Errorf("%w - %w: %s", ErrResourceStateAbsent, statErr, f.Path) return nil, fmt.Errorf("%w - %w", ErrResourceStateAbsent, statErr)
} }
switch f.FileType { switch f.FileType {
@ -665,13 +653,13 @@ func (f *File) Read(ctx context.Context) ([]byte, error) {
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fileContent)) f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fileContent))
} }
case SymbolicLinkFile: case SymbolicLinkFile:
linkTarget, pathErr := os.Readlink(f.Common.Path) linkTarget, pathErr := os.Readlink(f.Path)
if pathErr != nil { if pathErr != nil {
return nil, pathErr return nil, pathErr
} }
f.Target = linkTarget f.Target = linkTarget
} }
f.Common.State = "present" f.State = "present"
return yaml.Marshal(f) return yaml.Marshal(f)
} }

View File

@ -4,15 +4,15 @@ package resource
import ( import (
"context" "context"
_ "encoding/json" _ "encoding/json"
"fmt" "fmt"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"io" "io"
_ "log" _ "log"
_ "net/http" _ "net/http"
_ "net/http/httptest" _ "net/http/httptest"
_ "net/url" _ "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -23,7 +23,6 @@ _ "net/url"
"io/fs" "io/fs"
"decl/internal/codec" "decl/internal/codec"
"decl/internal/data" "decl/internal/data"
"decl/internal/folio"
"log/slog" "log/slog"
) )
@ -38,8 +37,7 @@ func TestNewFileNormalized(t *testing.T) {
f := NewNormalizedFile() f := NewNormalizedFile()
assert.NotNil(t, f) assert.NotNil(t, f)
f.Path = indirectFile assert.Nil(t, f.SetURI("file://" + indirectFile))
assert.Nil(t, f.Init(nil))
assert.NotEqual(t, indirectFile, f.Path) assert.NotEqual(t, indirectFile, f.Path)
assert.Equal(t, absFilePath, f.Path) assert.Equal(t, absFilePath, f.Path)
@ -86,21 +84,13 @@ func TestReadFile(t *testing.T) {
testFile := NewFile() testFile := NewFile()
e := testFile.LoadDecl(decl) e := testFile.LoadDecl(decl)
assert.Nil(t, e) assert.Nil(t, e)
assert.Equal(t, "present", testFile.Common.State)
assert.Equal(t, file, testFile.Common.Path)
applyErr := testFile.Apply() applyErr := testFile.Apply()
assert.Nil(t, applyErr) assert.Nil(t, applyErr)
assert.FileExists(t, file)
f := NewFile() f := NewFile()
assert.NotNil(t, f) assert.NotEqual(t, nil, f)
f.Path = file f.Path = file
assert.Nil(t, f.Init(nil))
r, e := f.Read(ctx) r, e := f.Read(ctx)
assert.Nil(t, e) assert.Nil(t, e)
assert.Equal(t, ProcessTestUserName, f.Owner) assert.Equal(t, ProcessTestUserName, f.Owner)
@ -128,8 +118,7 @@ func TestUseConfig(t *testing.T) {
return nil, data.ErrUnknownConfigurationKey return nil, data.ErrUnknownConfigurationKey
})) }))
uri := folio.URI(fmt.Sprintf("file://%s", file)) assert.Nil(t, f.SetURI(fmt.Sprintf("file://%s", file)))
assert.Nil(t, f.Init(uri.Parse()))
assert.Equal(t, filepath.Join("/tmp", file), f.FilePath()) assert.Equal(t, filepath.Join("/tmp", file), f.FilePath())
} }
@ -274,8 +263,8 @@ func TestFileSetURI(t *testing.T) {
file, _ := filepath.Abs(TempDir.FilePath("testuri.txt")) file, _ := filepath.Abs(TempDir.FilePath("testuri.txt"))
f := NewFile() f := NewFile()
assert.NotNil(t, f) assert.NotNil(t, f)
uri := folio.URI("file://" + file).Parse() e := f.SetURI("file://" + file)
assert.Nil(t, f.Init(uri)) assert.Nil(t, e)
assert.Equal(t, "file", f.Type()) assert.Equal(t, "file", f.Type())
assert.Equal(t, file, f.Path) assert.Equal(t, file, f.Path)
} }
@ -313,7 +302,6 @@ func TestFileUpdateAttributesFromFileInfo(t *testing.T) {
func TestFileReadStat(t *testing.T) { func TestFileReadStat(t *testing.T) {
ctx := context.Background() ctx := context.Background()
link := TempDir.FilePath("link.txt") link := TempDir.FilePath("link.txt")
linkTargetFile := TempDir.FilePath("testuri.txt") linkTargetFile := TempDir.FilePath("testuri.txt")
@ -321,8 +309,8 @@ func TestFileReadStat(t *testing.T) {
assert.NotNil(t, f) assert.NotNil(t, f)
f.Path = linkTargetFile f.Path = linkTargetFile
f.PathNormalization(true) e := f.NormalizePath()
assert.Nil(t, f.Init(nil)) assert.Nil(t, e)
statErr := f.ReadStat() statErr := f.ReadStat()
assert.Error(t, statErr) assert.Error(t, statErr)
@ -336,9 +324,8 @@ func TestFileReadStat(t *testing.T) {
l := NewFile() l := NewFile()
assert.NotNil(t, l) assert.NotNil(t, l)
l.PathNormalization(true)
assert.Nil(t, l.Init(nil))
assert.Nil(t, l.NormalizePath())
l.FileType = SymbolicLinkFile l.FileType = SymbolicLinkFile
l.Path = link l.Path = link
l.Target = linkTargetFile l.Target = linkTargetFile
@ -353,9 +340,6 @@ func TestFileReadStat(t *testing.T) {
testRead := NewFile() testRead := NewFile()
testRead.Path = link testRead.Path = link
assert.Nil(t, testRead.Init(nil))
_,testReadErr := testRead.Read(ctx) _,testReadErr := testRead.Read(ctx)
assert.Nil(t, testReadErr) assert.Nil(t, testReadErr)
assert.Equal(t, linkTargetFile, testRead.Target) assert.Equal(t, linkTargetFile, testRead.Target)
@ -371,8 +355,6 @@ func TestFileResourceFileInfo(t *testing.T) {
f.Mode = "0600" f.Mode = "0600"
f.Content = "some test data" f.Content = "some test data"
f.State = "present" f.State = "present"
assert.Nil(t, f.Init(nil))
assert.Nil(t, f.Apply()) assert.Nil(t, f.Apply())
_, readErr := f.Read(context.Background()) _, readErr := f.Read(context.Background())
@ -396,45 +378,20 @@ func TestFileClone(t *testing.T) {
assert.NotNil(t, f) assert.NotNil(t, f)
f.Path = testFile f.Path = testFile
assert.Nil(t, f.Init(nil))
f.Mode = "0600" f.Mode = "0600"
f.State = "present" f.State = "present"
assert.Nil(t, f.Apply()) assert.Nil(t, f.Apply())
origin := time.Now()
_,readErr := f.Read(ctx) _,readErr := f.Read(ctx)
assert.Nil(t, readErr) assert.Nil(t, readErr)
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
assert.Greater(t, origin, f.Mtime)
clone := f.Clone().(*File) clone := f.Clone().(*File)
assert.Equal(t, f.Common.Path, clone.Common.Path) assert.Equal(t, f, clone)
assert.Equal(t, f.Common.absPath, clone.Common.absPath) clone.Mtime = time.Time{}
assert.Equal(t, f.Common.parsedURI, clone.Common.parsedURI)
assert.Equal(t, f.Common.exttype, clone.Common.exttype)
assert.Equal(t, f.Common.fileext, clone.Common.fileext)
assert.Equal(t, f.Common.State, clone.Common.State)
assert.Equal(t, f.Size, clone.Size)
assert.Equal(t, f.Owner, clone.Owner)
assert.Equal(t, f.Group, clone.Group)
assert.Equal(t, f.Mode, clone.Mode)
assert.Equal(t, f.Atime, clone.Atime)
assert.Equal(t, f.Mtime, clone.Mtime)
assert.Equal(t, f.Ctime, clone.Ctime)
assert.Equal(t, f.Content, clone.Content)
assert.Equal(t, f.Sha256, clone.Sha256)
clone.Mtime = time.Now()
clone.Path = testCloneFile clone.Path = testCloneFile
assert.Nil(t, clone.Init(nil))
assert.NotEqual(t, f.absPath, clone.absPath)
slog.Info("TestFileClone", "clone", clone)
assert.Nil(t, clone.Apply()) assert.Nil(t, clone.Apply())
slog.Info("TestFileClone - applied mtime change", "clone", clone)
_,updateReadErr := f.Read(ctx) _,updateReadErr := f.Read(ctx)
assert.Nil(t, updateReadErr) assert.Nil(t, updateReadErr)
@ -442,8 +399,7 @@ func TestFileClone(t *testing.T) {
_, cloneReadErr := clone.Read(ctx) _, cloneReadErr := clone.Read(ctx)
assert.Nil(t, cloneReadErr) assert.Nil(t, cloneReadErr)
slog.Info("TestFileClone - read mtime change", "orig", f.Mtime, "clone", clone.Mtime) fmt.Printf("file %#v\nclone %#v\n", f, clone)
fmt.Printf("file %#v\n %#v\nclone %#v\n %#v\n", f, f.Common, clone, clone.Common)
assert.NotEqual(t, f.Mtime, clone.Mtime) assert.NotEqual(t, f.Mtime, clone.Mtime)
} }
@ -456,16 +412,12 @@ func TestFileErrors(t *testing.T) {
stater := f.StateMachine() stater := f.StateMachine()
f.Path = testFile f.Path = testFile
assert.Nil(t, f.Init(nil))
f.Mode = "631" f.Mode = "631"
assert.Nil(t, stater.Trigger("create")) assert.Nil(t, stater.Trigger("create"))
assert.FileExists(t, f.Path)
read := NewFile() read := NewFile()
readStater := read.StateMachine() readStater := read.StateMachine()
read.Path = testFile read.Path = testFile
assert.Nil(t, read.Init(nil))
assert.Nil(t, readStater.Trigger("read")) assert.Nil(t, readStater.Trigger("read"))
assert.Equal(t, FileMode("0631"), read.Mode) assert.Equal(t, FileMode("0631"), read.Mode)
@ -582,8 +534,7 @@ func TestFilePathURI(t *testing.T) {
e := f.LoadDecl(decl) e := f.LoadDecl(decl)
assert.Nil(t, e) assert.Nil(t, e)
assert.Equal(t, "", f.FilePath()) assert.Equal(t, "", f.FilePath())
// assert.ErrorContains(t, f.Validate(), "path: String length must be greater than or equal to 1") assert.ErrorContains(t, f.Validate(), "path: String length must be greater than or equal to 1")
assert.ErrorContains(t, f.Validate(), "path is required")
} }
func TestFileAbsent(t *testing.T) { func TestFileAbsent(t *testing.T) {
@ -642,8 +593,8 @@ func TestFileSetURIError(t *testing.T) {
file := TempDir.FilePath("fooread.txt") file := TempDir.FilePath("fooread.txt")
f := NewFile() f := NewFile()
assert.NotNil(t, f) assert.NotNil(t, f)
uri := folio.URI("foo://" + file).Parse() e := f.SetURI("foo://" + file)
e := f.Init(uri) assert.NotNil(t, e)
assert.ErrorIs(t, e, ErrInvalidResourceURI) assert.ErrorIs(t, e, ErrInvalidResourceURI)
} }
@ -651,8 +602,8 @@ func TestFileContentType(t *testing.T) {
file := TempDir.FilePath("fooread.txt") file := TempDir.FilePath("fooread.txt")
f := NewFile() f := NewFile()
assert.NotNil(t, f) assert.NotNil(t, f)
uri := folio.URI("file://" + file).Parse() e := f.SetURI("file://" + file)
assert.Nil(t, f.Init(uri)) assert.Nil(t, e)
assert.Equal(t, "txt", f.ContentType()) assert.Equal(t, "txt", f.ContentType())
} }

View File

@ -27,9 +27,8 @@ type decodeGroup Group
type GroupType string type GroupType string
const ( const (
GroupTypeName TypeName = "group" GroupTypeAddGroup = "addgroup"
GroupTypeAddGroup GroupType = "addgroup" GroupTypeGroupAdd = "groupadd"
GroupTypeGroupAdd GroupType = "groupadd"
) )
var ErrUnsupportedGroupType error = errors.New("The GroupType is not supported on this system") var ErrUnsupportedGroupType error = errors.New("The GroupType is not supported on this system")
@ -48,26 +47,28 @@ type Group struct {
ReadCommand *command.Command `json:"-" yaml:"-"` ReadCommand *command.Command `json:"-" yaml:"-"`
UpdateCommand *command.Command `json:"-" yaml:"-"` UpdateCommand *command.Command `json:"-" yaml:"-"`
DeleteCommand *command.Command `json:"-" yaml:"-"` DeleteCommand *command.Command `json:"-" yaml:"-"`
State string `json:"state,omitempty" yaml:"state,omitempty"`
config data.ConfigurationValueGetter
Resources data.ResourceMapper `json:"-" yaml:"-"` Resources data.ResourceMapper `json:"-" yaml:"-"`
groupStatus *user.Group `json:"-" yaml:"-"`
} }
func NewGroup() (g *Group) { func NewGroup() *Group {
g = &Group{} return &Group{}
g.Common = NewCommon(GroupTypeName, true)
g.Common.NormalizePath = g.NormalizePath
return
} }
func init() { func init() {
folio.DocumentRegistry.ResourceTypes.Register([]string{"group"}, func(u *url.URL) (group data.Resource) { folio.DocumentRegistry.ResourceTypes.Register([]string{"group"}, func(u *url.URL) data.Resource {
group = NewGroup() group := NewGroup()
if u != nil { group.Name = u.Hostname()
if err := folio.CastParsedURI(u).ConstructResource(group); err != nil { group.GID = LookupGIDString(u.Hostname())
panic(err) if _, addGroupPathErr := exec.LookPath("addgroup"); addGroupPathErr == nil {
} group.GroupType = GroupTypeAddGroup
} }
return if _, pathErr := exec.LookPath("groupadd"); pathErr == nil {
group.GroupType = GroupTypeGroupAdd
}
group.CreateCommand, group.ReadCommand, group.UpdateCommand, group.DeleteCommand = group.GroupType.NewCRUD()
return group
}) })
} }
@ -81,36 +82,15 @@ func FindSystemGroupType() GroupType {
return GroupTypeAddGroup return GroupTypeAddGroup
} }
func (g *Group) Init(u data.URIParser) error {
if u == nil {
u = folio.URI(g.URI()).Parse()
}
uri := u.URL()
g.Name = uri.Hostname()
g.GID = LookupGIDString(uri.Hostname())
if _, addGroupPathErr := exec.LookPath("addgroup"); addGroupPathErr == nil {
g.GroupType = GroupTypeAddGroup
}
if _, pathErr := exec.LookPath("groupadd"); pathErr == nil {
g.GroupType = GroupTypeGroupAdd
}
g.CreateCommand, g.ReadCommand, g.UpdateCommand, g.DeleteCommand = g.GroupType.NewCRUD()
return g.SetParsedURI(u)
}
func (g *Group) NormalizePath() error {
return nil
}
func (g *Group) SetResourceMapper(resources data.ResourceMapper) { func (g *Group) SetResourceMapper(resources data.ResourceMapper) {
g.Resources = resources g.Resources = resources
} }
func (g *Group) Clone() data.Resource { func (g *Group) Clone() data.Resource {
newg := &Group { newg := &Group {
Common: g.Common,
Name: g.Name, Name: g.Name,
GID: g.GID, GID: g.GID,
State: g.State,
GroupType: g.GroupType, GroupType: g.GroupType,
} }
newg.CreateCommand, newg.ReadCommand, newg.UpdateCommand, newg.DeleteCommand = g.GroupType.NewCRUD() newg.CreateCommand, newg.ReadCommand, newg.UpdateCommand, newg.DeleteCommand = g.GroupType.NewCRUD()
@ -129,62 +109,40 @@ func (g *Group) Notify(m *machine.EventMessage) {
switch m.On { switch m.On {
case machine.ENTERSTATEEVENT: case machine.ENTERSTATEEVENT:
switch m.Dest { switch m.Dest {
case "start_stat":
if statErr := g.ReadStat(); statErr == nil {
if triggerErr := g.StateMachine().Trigger("exists"); triggerErr == nil {
return
}
} else {
if triggerErr := g.StateMachine().Trigger("notexists"); triggerErr == nil {
return
}
}
case "start_read":
if _,readErr := g.Read(ctx); readErr == nil {
if triggerErr := g.StateMachine().Trigger("state_read"); triggerErr == nil {
return
} else {
g.Common.State = "absent"
panic(triggerErr)
}
} else {
g.Common.State = "absent"
panic(readErr)
}
case "start_create": case "start_create":
if e := g.Create(ctx); e == nil { if e := g.Create(ctx); e == nil {
if triggerErr := g.stater.Trigger("created"); triggerErr == nil { if triggerErr := g.stater.Trigger("created"); triggerErr == nil {
return return
} }
} }
g.Common.State = "absent" g.State = "absent"
case "present":
g.State = "present"
case "start_update":
if updateErr := g.Update(ctx); updateErr == nil {
if triggerErr := g.stater.Trigger("updated"); triggerErr == nil {
return
} else {
g.Common.State = "absent"
}
} else {
g.Common.State = "absent"
panic(updateErr)
}
case "absent":
g.Common.State = "absent"
case "present", "created", "read":
g.Common.State = "present"
} }
case machine.EXITSTATEEVENT: case machine.EXITSTATEEVENT:
} }
} }
func (g *Group) SetURI(uri string) error {
resourceUri, e := url.Parse(uri)
if e == nil {
if resourceUri.Scheme == "group" {
g.Name = resourceUri.Hostname()
} else {
e = fmt.Errorf("%w: %s is not a group", ErrInvalidResourceURI, uri)
}
}
return e
}
func (g *Group) URI() string { func (g *Group) URI() string {
return fmt.Sprintf("group://%s", g.Name) return fmt.Sprintf("group://%s", g.Name)
} }
func (g *Group) UseConfig(config data.ConfigurationValueGetter) {
g.config = config
}
func (g *Group) ResolveId(ctx context.Context) string { func (g *Group) ResolveId(ctx context.Context) string {
return LookupUIDString(g.Name) return LookupUIDString(g.Name)
} }
@ -195,7 +153,7 @@ func (g *Group) Validate() error {
func (g *Group) Apply() error { func (g *Group) Apply() error {
ctx := context.Background() ctx := context.Background()
switch g.Common.State { switch g.State {
case "present": case "present":
_, NoGroupExists := LookupGID(g.Name) _, NoGroupExists := LookupGID(g.Name)
if NoGroupExists != nil { if NoGroupExists != nil {
@ -240,26 +198,10 @@ func (g *Group) Create(ctx context.Context) (error) {
return e return e
} }
func (g *Group) ReadStat() (err error) {
if g.groupStatus == nil {
if g.groupStatus, err = user.LookupGroup(g.Name); err != nil {
g.Common.State = "absent"
return err
}
}
if len(g.groupStatus.Gid) < 1 {
g.Common.State = "absent"
return ErrResourceStateAbsent
}
g.GID = g.groupStatus.Gid
return
}
func (g *Group) Read(ctx context.Context) ([]byte, error) { func (g *Group) Read(ctx context.Context) ([]byte, error) {
exErr := g.ReadCommand.Extractor(nil, g) exErr := g.ReadCommand.Extractor(nil, g)
if exErr != nil { if exErr != nil {
g.Common.State = "absent" g.State = "absent"
} }
if yaml, yamlErr := yaml.Marshal(g); yamlErr != nil { if yaml, yamlErr := yaml.Marshal(g); yamlErr != nil {
return yaml, yamlErr return yaml, yamlErr
@ -268,9 +210,8 @@ func (g *Group) Read(ctx context.Context) ([]byte, error) {
} }
} }
func (g *Group) Update(ctx context.Context) (err error) { func (g *Group) Update(ctx context.Context) (error) {
_, err = g.UpdateCommand.Execute(g) return g.Create(ctx)
return
} }
func (g *Group) Delete(ctx context.Context) (error) { func (g *Group) Delete(ctx context.Context) (error) {
@ -404,7 +345,7 @@ func NewGroupReadCommand() *command.Command {
c := command.NewCommand() c := command.NewCommand()
c.Extractor = func(out []byte, target any) error { c.Extractor = func(out []byte, target any) error {
g := target.(*Group) g := target.(*Group)
g.Common.State = "absent" g.State = "absent"
var readGroup *user.Group var readGroup *user.Group
var e error var e error
if g.Name != "" { if g.Name != "" {
@ -419,7 +360,7 @@ func NewGroupReadCommand() *command.Command {
g.Name = readGroup.Name g.Name = readGroup.Name
g.GID = readGroup.Gid g.GID = readGroup.Gid
if g.GID != "" { if g.GID != "" {
g.Common.State = "present" g.State = "present"
} }
} }
return e return e
@ -428,17 +369,7 @@ func NewGroupReadCommand() *command.Command {
} }
func NewGroupUpdateCommand() *command.Command { func NewGroupUpdateCommand() *command.Command {
c := command.NewCommand() return nil
c.Path = "addgroup"
c.FailOnError = false
c.Args = []command.CommandArg{
command.CommandArg("{{ if .GID }}-g {{ .GID }}{{ end }}"),
command.CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
return nil
}
return c
} }
func NewGroupDelDeleteCommand() *command.Command { func NewGroupDelDeleteCommand() *command.Command {
@ -485,7 +416,7 @@ func NewReadGroupsCommand() *command.Command {
g := (*Groups)[lineIndex] g := (*Groups)[lineIndex]
g.Name = groupRecord[0] g.Name = groupRecord[0]
g.GID = groupRecord[2] g.GID = groupRecord[2]
g.Common.State = "present" g.State = "present"
g.GroupType = SystemGroupType g.GroupType = SystemGroupType
lineIndex++ lineIndex++
} }

View File

@ -60,11 +60,16 @@ func init() {
} }
func HTTPFactory(u *url.URL) data.Resource { func HTTPFactory(u *url.URL) data.Resource {
var err error
h := NewHTTP() h := NewHTTP()
if u != nil {
if err := folio.CastParsedURI(u).ConstructResource(h); err != nil { slog.Info("HTTP.Factory", "http", h, "url", u)
panic(err) if err = h.SetParsedURI(u); err != nil {
} panic(err)
}
if err = h.Open(); err != nil {
panic(err)
} }
return h return h
} }
@ -97,36 +102,17 @@ type HTTP struct {
} }
func NewHTTP() *HTTP { func NewHTTP() *HTTP {
h := &HTTP{ client: &http.Client{} } h := &HTTP{ client: &http.Client{}, Common: &Common{ includeQueryParamsInURI: true, resourceType: HTTPTypeName, SchemeCheck: func(scheme string) bool {
h.Common = NewCommon(HTTPTypeName, true) switch scheme {
h.Common.SchemeCheck = h.SchemeCheck case "http", "https":
h.Common.NormalizePath = h.NormalizePath return true
}
return false
} } }
slog.Info("NewHTTP()", "http", h) slog.Info("NewHTTP()", "http", h)
return h return h
} }
func (h *HTTP) SchemeCheck(scheme string) bool {
switch scheme {
case "http", "https":
return true
}
return false
}
func (h *HTTP) Init(u data.URIParser) (err error) {
if u == nil {
u = folio.URI(h.URI()).Parse()
}
if err = h.SetParsedURI(u); err == nil {
err = h.Open()
}
return
}
func (h *HTTP) NormalizePath() error {
return nil
}
func (h *HTTP) SetResourceMapper(resources data.ResourceMapper) { func (h *HTTP) SetResourceMapper(resources data.ResourceMapper) {
h.Resources = resources h.Resources = resources
} }
@ -233,9 +219,16 @@ func (h *HTTP) URI() string {
return h.Endpoint.String() return h.Endpoint.String()
} }
func (h *HTTP) SetParsedURI(u data.URIParser) (err error) { func (h *HTTP) SetURI(uri string) (err error) {
if err = h.Common.SetURI(uri); err == nil {
h.Endpoint = h.Common.Uri
}
return
}
func (h *HTTP) SetParsedURI(u *url.URL) (err error) {
if err = h.Common.SetParsedURI(u); err == nil { if err = h.Common.SetParsedURI(u); err == nil {
h.Endpoint = h.Common.URI() h.Endpoint = h.Common.Uri
} }
return return
} }
@ -273,13 +266,11 @@ func (h *HTTP) ContentSourceRefStat() (info fs.FileInfo) {
} }
func (h *HTTP) ReadStat() (err error) { func (h *HTTP) ReadStat() (err error) {
if h.reader == nil { if h.reader == nil {
if err = h.OpenGetter(); err != nil { if err = h.OpenGetter(); err != nil {
return return
} }
} }
var info fs.FileInfo var info fs.FileInfo
info, err = h.reader.Stat() info, err = h.reader.Stat()
@ -341,14 +332,14 @@ func (h *HTTP) Apply() error {
func (h *HTTP) Load(docData []byte, f codec.Format) (err error) { func (h *HTTP) Load(docData []byte, f codec.Format) (err error) {
if err = f.StringDecoder(string(docData)).Decode(h); err == nil { if err = f.StringDecoder(string(docData)).Decode(h); err == nil {
err = h.Common.SetParsedURI(folio.URI(h.Endpoint).Parse()) err = h.Common.SetURI(string(h.Endpoint))
} }
return return
} }
func (h *HTTP) LoadReader(r io.ReadCloser, f codec.Format) (err error) { func (h *HTTP) LoadReader(r io.ReadCloser, f codec.Format) (err error) {
if err = f.Decoder(r).Decode(h); err == nil { if err = f.Decoder(r).Decode(h); err == nil {
err = h.Common.SetParsedURI(folio.URI(h.Endpoint).Parse()) err = h.Common.SetURI(string(h.Endpoint))
//err = h.setParsedURI(h.Endpoint) //err = h.setParsedURI(h.Endpoint)
} }
return return
@ -356,7 +347,7 @@ func (h *HTTP) LoadReader(r io.ReadCloser, f codec.Format) (err error) {
func (h *HTTP) LoadString(docData string, f codec.Format) (err error) { func (h *HTTP) LoadString(docData string, f codec.Format) (err error) {
if err = f.StringDecoder(docData).Decode(h); err == nil { if err = f.StringDecoder(docData).Decode(h); err == nil {
err = h.Common.SetParsedURI(folio.URI(h.Endpoint).Parse()) err = h.Common.SetURI(string(h.Endpoint))
//err = h.setParsedURI(h.Endpoint) //err = h.setParsedURI(h.Endpoint)
} }
return return
@ -367,7 +358,7 @@ func (h *HTTP) LoadDecl(yamlResourceDeclaration string) error {
} }
func (h *HTTP) ResolveId(ctx context.Context) string { func (h *HTTP) ResolveId(ctx context.Context) string {
_ = h.Common.SetParsedURI(folio.URI(h.Endpoint).Parse()) _ = h.Common.SetURI(h.Endpoint.String())
slog.Info("HTTP.ResolveId()", "uri", h.Endpoint.String()) slog.Info("HTTP.ResolveId()", "uri", h.Endpoint.String())
return h.Endpoint.String() return h.Endpoint.String()
} }

View File

@ -30,11 +30,20 @@ const (
func init() { func init() {
folio.DocumentRegistry.ResourceTypes.Register([]string{"iptable"}, func(u *url.URL) data.Resource { folio.DocumentRegistry.ResourceTypes.Register([]string{"iptable"}, func(u *url.URL) data.Resource {
i := NewIptable() i := NewIptable()
if u != nil { i.Table = IptableName(u.Hostname())
if err := folio.CastParsedURI(u).ConstructResource(i); err != nil { if len(u.Path) > 0 {
panic(err) fields := strings.FieldsFunc(u.Path, func(c rune) bool { return c == '/' })
slog.Info("iptables factory", "iptable", i, "uri", u, "fields", fields, "number_fields", len(fields))
if len(fields) > 0 {
i.Chain = IptableChain(fields[0])
if len(fields) < 3 {
i.ResourceType = IptableTypeChain
} else {
i.ResourceType = IptableTypeRule
id, _ := strconv.ParseUint(fields[1], 10, 32)
i.Id = uint(id)
}
} }
i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD() i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD()
} }
return i return i
@ -63,9 +72,9 @@ var IptableNumber = regexp.MustCompile(`^[0-9]+$`)
type IptableChain string type IptableChain string
const ( const (
IptableChainInput = "INPUT" IptableChainInput = "INPUT"
IptableChainOutput = "OUTPUT" IptableChainOutput = "OUTPUT"
IptableChainForward = "FORWARD" IptableChainForward = "FORWARD"
IptableChainPreRouting = "PREROUTING" IptableChainPreRouting = "PREROUTING"
IptableChainPostRouting = "POSTROUTING" IptableChainPostRouting = "POSTROUTING"
) )
@ -92,8 +101,6 @@ type ExtensionFlag struct {
Value string `json:"value" yaml:"value"` Value string `json:"value" yaml:"value"`
} }
type TargetFlag ExtensionFlag
type IptablePort uint16 type IptablePort uint16
type IptableType string type IptableType string
@ -103,8 +110,6 @@ const (
IptableTypeChain = "chain" IptableTypeChain = "chain"
) )
type IptableRule string
var ( var (
ErrInvalidIptableName error = errors.New("The IptableName is not a valid table") ErrInvalidIptableName error = errors.New("The IptableName is not a valid table")
) )
@ -112,32 +117,32 @@ var (
// 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 {
*Common `json:",inline" yaml:",inline"` *Common `json:",inline" yaml:",inline"`
stater machine.Stater `json:"-" yaml:"-"` stater machine.Stater `json:"-" yaml:"-"`
Id uint `json:"id,omitempty" yaml:"id,omitempty"` parsedURI *url.URL `json:"-" yaml:"-"`
Table IptableName `json:"table" yaml:"table"` Id uint `json:"id,omitempty" yaml:"id,omitempty"`
Chain IptableChain `json:"chain" yaml:"chain"` Table IptableName `json:"table" yaml:"table"`
Destination IptableCIDR `json:"destination,omitempty" yaml:"destination,omitempty"` Chain IptableChain `json:"chain" yaml:"chain"`
Source IptableCIDR `json:"source,omitempty" yaml:"source,omitempty"` Destination IptableCIDR `json:"destination,omitempty" yaml:"destination,omitempty"`
Dport IptablePort `json:"dport,omitempty" yaml:"dport,omitempty"` Source IptableCIDR `json:"source,omitempty" yaml:"source,omitempty"`
Sport IptablePort `json:"sport,omitempty" yaml:"sport,omitempty"` Dport IptablePort `json:"dport,omitempty" yaml:"dport,omitempty"`
In string `json:"in,omitempty" yaml:"in,omitempty"` Sport IptablePort `json:"sport,omitempty" yaml:"sport,omitempty"`
Out string `json:"out,omitempty" yaml:"out,omitempty"` In string `json:"in,omitempty" yaml:"in,omitempty"`
Match []string `json:"match,omitempty" yaml:"match,omitempty"` Out string `json:"out,omitempty" yaml:"out,omitempty"`
Flags []ExtensionFlag `json:"extension_flags,omitempty" yaml:"extension_flags,omitempty"` Match []string `json:"match,omitempty" yaml:"match,omitempty"`
Proto IptableProto `json:"proto,omitempty" yaml:"proto,omitempty"` Flags []ExtensionFlag `json:"extension_flags,omitempty" yaml:"extension_flags,omitempty"`
Jump string `json:"jump,omitempty" yaml:"jump,omitempty"` Proto IptableProto `json:"proto,omitempty" yaml:"proto,omitempty"`
TargetFlags []TargetFlag `json:"target_flags,omitempty" yaml:"target_flags,omitempty"` Jump string `json:"jump,omitempty" yaml:"jump,omitempty"`
ChainLength uint `json:"-" yaml:"-"` ChainLength uint `json:"-" yaml:"-"`
ResourceType IptableType `json:"resourcetype,omitempty" yaml:"resourcetype,omitempty"` ResourceType IptableType `json:"resourcetype,omitempty" yaml:"resourcetype,omitempty"`
CreateCommand *command.Command `yaml:"-" json:"-"` CreateCommand *command.Command `yaml:"-" json:"-"`
ReadCommand *command.Command `yaml:"-" json:"-"` ReadCommand *command.Command `yaml:"-" json:"-"`
ReadChainCommand *command.Command `yaml:"-" json:"-"` UpdateCommand *command.Command `yaml:"-" json:"-"`
UpdateCommand *command.Command `yaml:"-" json:"-"` DeleteCommand *command.Command `yaml:"-" json:"-"`
DeleteCommand *command.Command `yaml:"-" json:"-"`
Resources data.ResourceMapper `yaml:"-" json:"-"` config data.ConfigurationValueGetter
Resources data.ResourceMapper `yaml:"-" json:"-"`
} }
@ -150,39 +155,10 @@ func (n IptableName) Validate() error {
} }
} }
func NewIptable() (i *Iptable) { func NewIptable() *Iptable {
i = &Iptable{ ResourceType: IptableTypeRule } i := &Iptable{ ResourceType: IptableTypeRule, Common: &Common{ resourceType: IptableTypeName } }
i.Common = NewCommon(IptableTypeName, false)
i.Common.NormalizePath = i.NormalizePath
i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD() i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD()
i.ReadChainCommand = NewIptableChainReadCommand() return i
return
}
func (i *Iptable) Init(u data.URIParser) (err error) {
if u == nil {
u = folio.URI(i.URI()).Parse()
}
uri := u.URL()
err = i.SetParsedURI(u)
i.Table = IptableName(uri.Hostname())
if len(uri.Path) > 0 {
fields := strings.FieldsFunc(uri.Path, func(c rune) bool { return c == '/' })
slog.Info("iptables factory", "iptable", i, "uri", uri, "fields", fields, "number_fields", len(fields))
if len(fields) > 0 {
i.Chain = IptableChain(fields[0])
if len(fields) < 3 {
i.ResourceType = IptableTypeChain
} else {
i.ResourceType = IptableTypeRule
id, _ := strconv.ParseUint(fields[1], 10, 32)
i.SetId(uint(id))
}
}
i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD()
}
return
} }
func (i *Iptable) SetResourceMapper(resources data.ResourceMapper) { func (i *Iptable) SetResourceMapper(resources data.ResourceMapper) {
@ -219,121 +195,53 @@ func (i *Iptable) Notify(m *machine.EventMessage) {
switch m.On { switch m.On {
case machine.ENTERSTATEEVENT: case machine.ENTERSTATEEVENT:
switch m.Dest { switch m.Dest {
case "start_stat":
if statErr := i.ReadStat(ctx); statErr == nil {
if triggerErr := i.StateMachine().Trigger("exists"); triggerErr == nil {
return
}
} else {
if triggerErr := i.StateMachine().Trigger("notexists"); triggerErr == nil {
return
}
}
case "start_read":
if _,readErr := i.Read(ctx); readErr == nil {
if triggerErr := i.stater.Trigger("state_read"); triggerErr == nil {
return
} else {
i.Common.State = "absent"
panic(triggerErr)
}
} else {
i.Common.State = "absent"
panic(readErr)
}
case "start_create": case "start_create":
if createErr := i.Create(ctx); createErr == nil { if e := i.Create(ctx); e == nil {
if triggerErr := i.stater.Trigger("created"); triggerErr == nil { if triggerErr := i.stater.Trigger("created"); triggerErr == nil {
slog.Info("ContainerImage.Notify()", "created", i, "error", triggerErr)
return
} else {
slog.Info("ContainerImage.Notify()", "created", i, "error", triggerErr)
i.Common.State = "absent"
panic(triggerErr)
}
} else {
i.Common.State = "absent"
panic(createErr)
}
case "start_update":
if createErr := i.Update(ctx); createErr == nil {
if triggerErr := i.stater.Trigger("updated"); triggerErr == nil {
return
} else {
i.Common.State = "absent"
}
} else {
i.Common.State = "absent"
panic(createErr)
}
case "start_delete":
if deleteErr := i.Delete(ctx); deleteErr == nil {
if triggerErr := i.stater.Trigger("deleted"); triggerErr == nil {
return return
} }
} else {
panic(deleteErr)
} }
case "present", "created", "read":
i.Common.State = "present"
case "absent":
i.Common.State = "absent" i.Common.State = "absent"
case "present":
i.Common.State = "present"
} }
case machine.EXITSTATEEVENT: case machine.EXITSTATEEVENT:
} }
} }
// Set the chain ID and update the mapped URI
func (i *Iptable) SetId(id uint) {
if i.Id != id {
uri := i.URI()
i.Id = id
decl, ok := i.Resources.Get(uri)
if ok {
i.Resources.Delete(uri)
}
i.Resources.Set(i.URI(), decl)
}
}
func (i *Iptable) URI() string { func (i *Iptable) URI() string {
return fmt.Sprintf("iptable://%s/%s/%d", i.Table, i.Chain, i.Id) return fmt.Sprintf("iptable://%s/%s/%d", i.Table, i.Chain, i.Id)
} }
func (i *Iptable) SetURI(uri string) (err error) {
func (i *Iptable) SetParsedURI(uri data.URIParser) (err error) { i.parsedURI, err = url.Parse(uri)
if err = i.Common.SetParsedURI(uri); err == nil { if err == nil {
err = i.setFieldsFromPath() fields := strings.FieldsFunc(i.parsedURI.Path, func(c rune) bool { return c == '/' })
} fieldsLen := len(fields)
return if i.parsedURI.Scheme == "iptable" && fieldsLen > 0 {
} i.Table = IptableName(i.parsedURI.Hostname())
if err = i.Table.Validate(); err != nil {
func (i *Iptable) NormalizePath() error { return err
return nil }
} i.Chain = IptableChain(fields[0])
if fieldsLen < 2 {
func (i *Iptable) setFieldsFromPath() (err error) { i.ResourceType = IptableTypeChain
fields := strings.FieldsFunc(i.Common.Path, func(c rune) bool { return c == '/' }) } else {
fieldsLen := len(fields) i.ResourceType = IptableTypeRule
if fieldsLen > 0 { id, _ := strconv.ParseUint(fields[1], 10, 32)
i.Table = IptableName(fields[0]) i.Id = uint(id)
if err = i.Table.Validate(); err != nil { }
return err
}
i.Chain = IptableChain(fields[1])
if fieldsLen < 3 {
i.ResourceType = IptableTypeChain
} else { } else {
i.ResourceType = IptableTypeRule err = fmt.Errorf("%w: %s is not an iptable rule", ErrInvalidResourceURI, uri)
id, _ := strconv.ParseUint(fields[1], 10, 32)
i.Id = uint(id)
} }
} else {
err = fmt.Errorf("%w: %s is not an iptable rule", ErrInvalidResourceURI, i.Common.URI())
} }
return return
} }
func (i *Iptable) UseConfig(config data.ConfigurationValueGetter) {
i.config = config
}
func (i *Iptable) Validate() error { func (i *Iptable) Validate() error {
s := NewSchema(i.Type()) s := NewSchema(i.Type())
jsonDoc, jsonErr := i.JSON() jsonDoc, jsonErr := i.JSON()
@ -408,46 +316,6 @@ func (i *Iptable) ResolveId(ctx context.Context) string {
return fmt.Sprintf("%d", i.Id) return fmt.Sprintf("%d", i.Id)
} }
func (f *ExtensionFlag) Match(name string, value string) bool {
start := 0
if name[1] == '-' {
start = 2
} else if name[0] == '-' {
start = 1
}
return f.Name == name[start:] && f.Value == value
}
func (i *Iptable) HasExtensionFlag(name string) bool {
optName := strings.Trim(name, "-")
for _, ext := range i.Flags {
if ext.Name == optName {
return true
}
}
return false
}
func (i *Iptable) HasTargetFlag(name string) bool {
optName := strings.Trim(name, "-")
for _, ext := range i.TargetFlags {
if ext.Name == optName {
return true
}
}
return false
}
func (f *TargetFlag) Match(name string, value string) bool {
start := 0
if name[1] == '-' {
start = 2
} else if name[0] == '-' {
start = 1
}
return f.Name == name[start:] && f.Value == value
}
func (i *Iptable) SetFlagValue(opt, value string) bool { func (i *Iptable) SetFlagValue(opt, value string) bool {
switch opt { switch opt {
case "-i": case "-i":
@ -485,14 +353,10 @@ func (i *Iptable) SetFlagValue(opt, value string) bool {
i.Sport = IptablePort(port) i.Sport = IptablePort(port)
return true return true
default: default:
if opt[0] == '-' { if opt[0] == '-' {
if len(i.Jump) > 0 { i.Flags = append(i.Flags, ExtensionFlag{ Name: strings.Trim(opt, "-"), Value: strings.TrimSpace(value)})
i.TargetFlags = append(i.TargetFlags, TargetFlag{ Name: strings.Trim(opt, "-"), Value: strings.TrimSpace(value)})
} else {
i.Flags = append(i.Flags, ExtensionFlag{ Name: strings.Trim(opt, "-"), Value: strings.TrimSpace(value)})
}
return true return true
} }
} }
return false return false
} }
@ -518,15 +382,9 @@ func (i *Iptable) GetFlagValue(opt string) any {
case "--sport": case "--sport":
return strconv.Itoa(int(i.Sport)) return strconv.Itoa(int(i.Sport))
default: default:
if opt[0] == '-' { if opt[0] == '-' {
slog.Info("Iptable.GetFlagValue()", "opt", opt, "iptable", i) return i.Flags
if i.HasExtensionFlag(opt) { }
return i.Flags
}
if i.HasTargetFlag(opt) {
return i.TargetFlags
}
}
} }
return nil return nil
} }
@ -549,11 +407,9 @@ func (i *Iptable) SetRule(flags []string) (assigned bool) {
func (i *Iptable) MatchRule(flags []string) (match bool) { func (i *Iptable) MatchRule(flags []string) (match bool) {
match = true match = true
next:
for index, flag := range flags { for index, flag := range flags {
if flag[0] == '-' { if flag[0] == '-' {
value := flags[index + 1] value := flags[index + 1]
slog.Info("Iptable.MatchRule()", "flag", flag, "value", value)
switch v := i.GetFlagValue(flag).(type) { switch v := i.GetFlagValue(flag).(type) {
case []string: case []string:
for _,element := range v { for _,element := range v {
@ -561,57 +417,48 @@ func (i *Iptable) MatchRule(flags []string) (match bool) {
continue continue
} }
} }
slog.Info("Iptable.MatchRule() - FAILED", "flag", flag, "value", v)
match = false match = false
case []ExtensionFlag: case []ExtensionFlag:
for _,element := range v { for _,element := range v {
if element.Match(flag, value) { if element.Name == flag && element.Value == value {
continue next continue
} }
} }
slog.Info("Iptable.MatchRule() - FAILED", "flag", flag, "value", v)
match = false
case []TargetFlag:
for _,element := range v {
if element.Match(flag, value) {
continue next
}
}
slog.Info("Iptable.MatchRule() - FAILED", "flag", flag, "value", v)
match = false match = false
case IptableCIDR: case IptableCIDR:
if v == IptableCIDR(value) { if v == IptableCIDR(value) {
continue continue
} }
slog.Info("Iptable.MatchRule() - FAILED", "flag", flag, "value", v)
match = false match = false
case IptableName: case IptableName:
if v == IptableName(value) { if v == IptableName(value) {
continue continue
} }
slog.Info("Iptable.MatchRule() - FAILED", "flag", flag, "value", v)
match = false match = false
case IptableChain: case IptableChain:
if v == IptableChain(value) { if v == IptableChain(value) {
continue continue
} }
slog.Info("Iptable.MatchRule() - FAILED", "flag", flag, "value", v)
match = false match = false
default: default:
if v.(string) == value { if v.(string) == value {
continue continue
} }
slog.Info("Iptable.MatchRule() - FAILED", "flag", flag, "value", v)
match = false match = false
} }
} }
} }
slog.Info("Iptable.MatchRule()", "flags", flags, "match", match)
return return
} }
func (i *Iptable) ReadChainLength() error { func (i *Iptable) ReadChainLength() error {
output,err := i.ReadChainCommand.Execute(i) c := command.NewCommand()
c.Path = "iptables"
c.Args = []command.CommandArg{
command.CommandArg("-S"),
command.CommandArg("{{ .Chain }}"),
}
output,err := c.Execute(i)
if err == nil { if err == nil {
linesCount := strings.Count(string(output), "\n") linesCount := strings.Count(string(output), "\n")
if linesCount > 0 { if linesCount > 0 {
@ -623,16 +470,13 @@ func (i *Iptable) ReadChainLength() error {
return err return err
} }
func (i *Iptable) Create(ctx context.Context) (err error) { func (i *Iptable) Create(ctx context.Context) error {
slog.Info("Iptable.Create()", "iptable", i)
if i.Id > 0 { if i.Id > 0 {
if lenErr := i.ReadChainLength(); lenErr != nil { if lenErr := i.ReadChainLength(); lenErr != nil {
return lenErr return lenErr
} }
} }
_, err := i.CreateCommand.Execute(i)
_, err = i.CreateCommand.Execute(i)
slog.Info("Iptable.Create()", "err", err, "iptable", i, "createcommand", i.CreateCommand)
//slog.Info("IptableChain Create()", "err", err, "errstr", err.Error(), "iptable", i, "createcommand", i.CreateCommand) //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 // TODO add Command status/error handler rather than using the read extractor
if i.CreateCommand.Extractor != nil { if i.CreateCommand.Extractor != nil {
@ -640,20 +484,7 @@ func (i *Iptable) Create(ctx context.Context) (err error) {
return i.CreateCommand.Extractor([]byte(err.Error()), i) return i.CreateCommand.Extractor([]byte(err.Error()), i)
} }
} }
return err return nil
}
func (i *Iptable) ReadStat(ctx context.Context) (err error) {
if i.ReadCommand.Exists() {
var out []byte
if out, err = i.ReadCommand.Execute(i); err == nil {
err = i.ReadCommand.Extractor(out, i)
}
}
if i.Id == 0 {
return ErrResourceStateAbsent
}
return err
} }
func (i *Iptable) Read(ctx context.Context) ([]byte, error) { func (i *Iptable) Read(ctx context.Context) ([]byte, error) {
@ -672,15 +503,8 @@ func (i *Iptable) Update(ctx context.Context) error {
return i.Create(ctx) return i.Create(ctx)
} }
func (i *Iptable) Delete(ctx context.Context) (err error) { func (i *Iptable) Delete(ctx context.Context) error {
if i.Id < 1 { return nil
return fmt.Errorf("Failed to find rule to delete")
}
var out []byte
if out, err = i.DeleteCommand.Execute(i); err == nil {
err = i.DeleteCommand.Extractor(out, i)
}
return
} }
func (i *Iptable) Type() string { return "iptable" } func (i *Iptable) Type() string { return "iptable" }
@ -728,7 +552,9 @@ func NewIptableCreateCommand() *command.Command {
c.Args = []command.CommandArg{ c.Args = []command.CommandArg{
command.CommandArg("-t"), command.CommandArg("-t"),
command.CommandArg("{{ .Table }}"), command.CommandArg("{{ .Table }}"),
command.CommandArg("{{ if and (le .Id .ChainLength) (gt .Id 0) }}-R {{ .Chain }} {{ .Id }}{{ else }}-A {{ .Chain }}{{ end }}"), command.CommandArg("{{ if le .Id .ChainLength }}-R{{ else }}-A{{ end }}"),
command.CommandArg("{{ .Chain }}"),
command.CommandArg("{{ if le .Id .ChainLength }}{{ .Id }}{{ end }}"),
command.CommandArg("{{ if .In }}-i {{ .In }}{{ else if .Out }}-o {{ .Out }}{{ end }}"), command.CommandArg("{{ if .In }}-i {{ .In }}{{ else if .Out }}-o {{ .Out }}{{ end }}"),
command.CommandArg("{{ range .Match }}-m {{ . }} {{- end }}"), command.CommandArg("{{ range .Match }}-m {{ . }} {{- end }}"),
command.CommandArg("{{ if .Source }}-s {{ .Source }}{{ end }}"), command.CommandArg("{{ if .Source }}-s {{ .Source }}{{ end }}"),
@ -736,9 +562,8 @@ func NewIptableCreateCommand() *command.Command {
command.CommandArg("{{ if .Destination }}-d {{ .Destination }}{{ end }}"), command.CommandArg("{{ if .Destination }}-d {{ .Destination }}{{ end }}"),
command.CommandArg("{{ if .Dport }}--dport {{ .Dport }}{{ end }}"), command.CommandArg("{{ if .Dport }}--dport {{ .Dport }}{{ end }}"),
command.CommandArg("{{ if .Proto }}-p {{ .Proto }}{{ end }}"), command.CommandArg("{{ if .Proto }}-p {{ .Proto }}{{ end }}"),
command.CommandArg("{{ range .Flags -}} --{{ .Name }} {{ .Value }} {{- end }}"), command.CommandArg("{{ range .Flags }} --{{ .Name }} {{ .Value }} {{ end }}"),
command.CommandArg("{{ if .Jump }}-j {{ .Jump }}{{ end }}"), command.CommandArg("{{ if .Jump }}-j {{ .Jump }}{{ end }}"),
command.CommandArg("{{ range .TargetFlags -}} --{{ .Name }} {{ .Value }}{{- end }}"),
} }
return c return c
} }
@ -759,7 +584,7 @@ func IptableExtractRule(lineNumber uint, ruleLine string, target *Iptable) (stat
} }
} else { } else {
if target.MatchRule(flags) { if target.MatchRule(flags) {
target.SetId(lineNumber) target.Id = lineNumber
state = "present" state = "present"
err = nil err = nil
} }
@ -836,7 +661,7 @@ func NewIptableReadChainCommand() *command.Command {
} }
for lineIndex, line := range lines[1:] { for lineIndex, line := range lines[1:] {
i := (*IptableChainRules)[lineIndex] i := (*IptableChainRules)[lineIndex]
i.SetId(uint(lineIndex + 1)) i.Id = uint(lineIndex + 1)
ruleFields := strings.Split(strings.TrimSpace(line), " ") ruleFields := strings.Split(strings.TrimSpace(line), " ")
if ruleFields[0] == "-A" { if ruleFields[0] == "-A" {
flags := ruleFields[2:] flags := ruleFields[2:]
@ -857,16 +682,7 @@ func NewIptableUpdateCommand() *command.Command {
} }
func NewIptableDeleteCommand() *command.Command { func NewIptableDeleteCommand() *command.Command {
c := command.NewCommand() return nil
c.Path = "iptables"
c.Args = []command.CommandArg{
command.CommandArg("-t"),
command.CommandArg("{{ .Table }}"),
command.CommandArg("-D"),
command.CommandArg("{{ .Chain }}"),
command.CommandArg("{{ .Id }}"),
}
return c
} }
func NewIptableChainCreateCommand() *command.Command { func NewIptableChainCreateCommand() *command.Command {
@ -950,7 +766,7 @@ func RuleExtractorMatchFlags(out []byte, target any) (err error) {
slog.Info("RuleExtractorMatchFlags()", "flags", flags, "ipt", ipt) slog.Info("RuleExtractorMatchFlags()", "flags", flags, "ipt", ipt)
err = nil err = nil
ipt.Common.State = "present" ipt.Common.State = "present"
ipt.SetId(uint(linesIndex)) ipt.Id = uint(linesIndex)
return return
} }
} }
@ -1022,14 +838,6 @@ func NewIptableChainUpdateCommand() *command.Command {
} }
func NewIptableChainDeleteCommand() *command.Command { func NewIptableChainDeleteCommand() *command.Command {
c := command.NewCommand() return nil
c.Path = "iptables"
c.Args = []command.CommandArg{
command.CommandArg("-t"),
command.CommandArg("{{ .Table }}"),
command.CommandArg("-X"),
command.CommandArg("{{ .Chain }}"),
}
return c
} }

View File

@ -20,7 +20,6 @@ _ "syscall"
"testing" "testing"
_ "time" _ "time"
"decl/internal/command" "decl/internal/command"
"decl/internal/data"
) )
func TestNewIptableResource(t *testing.T) { func TestNewIptableResource(t *testing.T) {
@ -78,59 +77,10 @@ func TestReadIptable(t *testing.T) {
} }
func TestCreateIptable(t *testing.T) { func TestCreateIptable(t *testing.T) {
ctx := context.Background()
testRule := NewIptable() testRule := NewIptable()
assert.NotNil(t, testRule) assert.NotNil(t, testRule)
declarationAttributes := `
table: "filter"
id: 5
chain: "INPUT"
source: "192.168.0.0/24"
destination: "192.168.0.1"
jump: "ACCEPT"
state: present
`
m := &MockCommand{
Executor: func(value any) ([]byte, error) {
return nil, nil
},
Extractor: func(output []byte, target any) error {
testRule.Table = "filter"
testRule.Id = 3
testRule.Chain = "INPUT"
testRule.In = "eth0"
testRule.Source = "192.168.0.0/24"
testRule.State = "present"
return nil
},
}
mockReadChain := &MockCommand{
Executor: func(value any) ([]byte, error) {
return []byte(`
-P INPUT ACCEPT
-A INPUT -j LIBVIRT_INP
`), nil
},
}
e := testRule.LoadDecl(declarationAttributes)
assert.Nil(t, e)
testRule.ReadChainCommand = (*command.Command)(mockReadChain)
testRule.ReadCommand = (*command.Command)(m)
testRule.CreateCommand = (*command.Command)(m)
assert.Nil(t, testRule.Create(ctx))
assert.Equal(t, uint(2), testRule.ChainLength)
_, err := testRule.Read(ctx)
assert.Nil(t, err)
//assert.Equal(t, uint(3), testRule.ChainLength)
assert.Equal(t, uint(3), testRule.Id)
} }
func TestIptableSetFlagValue(t *testing.T) { func TestIptableSetFlagValue(t *testing.T) {
@ -167,7 +117,6 @@ func TestIptableRuleExtractorById(t *testing.T) {
func TestIptableRuleExtractorByFlags(t *testing.T) { func TestIptableRuleExtractorByFlags(t *testing.T) {
ipt := NewIptable() ipt := NewIptable()
ipt.Resources = data.NewResourceMapper()
assert.NotNil(t, ipt) assert.NotNil(t, ipt)
ipt.Table = IptableName("filter") ipt.Table = IptableName("filter")
ipt.Chain = IptableChain("FOO") ipt.Chain = IptableChain("FOO")

View File

@ -18,7 +18,6 @@ _ "strconv"
"decl/internal/codec" "decl/internal/codec"
"decl/internal/data" "decl/internal/data"
"decl/internal/folio" "decl/internal/folio"
"decl/internal/command"
) )
const ( const (
@ -26,17 +25,52 @@ const (
) )
func init() { func init() {
folio.DocumentRegistry.ResourceTypes.Register([]string{"route"}, func(u *url.URL) (n data.Resource) { folio.DocumentRegistry.ResourceTypes.Register([]string{"route"}, func(u *url.URL) data.Resource {
n = NewNetworkRoute() n := NewNetworkRoute()
if u != nil {
if err := folio.CastParsedURI(u).ConstructResource(n); err != nil {
panic(err)
}
}
return n return n
}) })
} }
/*
ROUTE := NODE_SPEC [ INFO_SPEC ]
NODE_SPEC := [ TYPE ] PREFIX [ tos TOS ]
[ table TABLE_ID ] [ proto RTPROTO ]
[ scope SCOPE ] [ metric METRIC ]
[ ttl-propagate { enabled | disabled } ]
INFO_SPEC := { NH | nhid ID } OPTIONS FLAGS [ nexthop NH ]...
NH := [ encap ENCAPTYPE ENCAPHDR ] [ via [ FAMILY ] ADDRESS ]
[ dev STRING ] [ weight NUMBER ] NHFLAGS
FAMILY := [ inet | inet6 | mpls | bridge | link ]
OPTIONS := FLAGS [ mtu NUMBER ] [ advmss NUMBER ] [ as [ to ] ADDRESS ]
[ rtt TIME ] [ rttvar TIME ] [ reordering NUMBER ]
[ window NUMBER ] [ cwnd NUMBER ] [ initcwnd NUMBER ]
[ ssthresh NUMBER ] [ realms REALM ] [ src ADDRESS ]
[ rto_min TIME ] [ hoplimit NUMBER ] [ initrwnd NUMBER ]
[ features FEATURES ] [ quickack BOOL ] [ congctl NAME ]
[ pref PREF ] [ expires TIME ] [ fastopen_no_cookie BOOL ]
NHFLAGS := [ onlink | pervasive ]
PREF := [ low | medium | high ]
TIME := NUMBER[s|ms]
BOOL := [1|0]
FEATURES := ecn
ENCAPTYPE := [ mpls | ip | ip6 | seg6 | seg6local | rpl | ioam6 ]
ENCAPHDR := [ MPLSLABEL | SEG6HDR | SEG6LOCAL | IOAM6HDR ]
SEG6HDR := [ mode SEGMODE ] segs ADDR1,ADDRi,ADDRn [hmac HMACKEYID] [cleanup]
SEGMODE := [ encap | inline ]
SEG6LOCAL := action ACTION [ OPTIONS ] [ count ]
ACTION := { End | End.X | End.T | End.DX2 | End.DX6 | End.DX4 |
End.DT6 | End.DT4 | End.DT46 | End.B6 | End.B6.Encaps |
End.BM | End.S | End.AS | End.AM | End.BPF }
OPTIONS := OPTION [ OPTIONS ]
OPTION := { srh SEG6HDR | nh4 ADDR | nh6 ADDR | iif DEV | oif DEV |
table TABLEID | vrftable TABLEID | endpoint PROGNAME }
IOAM6HDR := trace prealloc type IOAM6_TRACE_TYPE ns IOAM6_NAMESPACE size IOAM6_TRACE_SIZE
ROUTE_GET_FLAGS := [ fibmatch ]
*/
type NetworkRouteType string type NetworkRouteType string
const ( const (
@ -80,45 +114,33 @@ const (
// Manage the state of network routes // Manage the state of network routes
type NetworkRoute struct { type NetworkRoute struct {
*Common `json:",inline" yaml:",inline"` *Common `json:",inline" yaml:",inline"`
stater machine.Stater `json:"-" yaml:"-"` stater machine.Stater `json:"-" yaml:"-"`
Id string Id string
To string `json:"to" yaml:"to"` To string `json:"to" yaml:"to"`
Interface string `json:"interface" yaml:"interface"` Interface string `json:"interface" yaml:"interface"`
Gateway string `json:"gateway" yaml:"gateway"` Gateway string `json:"gateway" yaml:"gateway"`
Metric uint `json:"metric" yaml:"metric"` Metric uint `json:"metric" yaml:"metric"`
Rtid NetworkRouteTableId `json:"rtid" yaml:"rtid"` Rtid NetworkRouteTableId `json:"rtid" yaml:"rtid"`
RouteType NetworkRouteType `json:"routetype" yaml:"routetype"` RouteType NetworkRouteType `json:"routetype" yaml:"routetype"`
Scope NetworkRouteScope `json:"scope" yaml:"scope"` Scope NetworkRouteScope `json:"scope" yaml:"scope"`
Proto NetworkRouteProto `json:"proto" yaml:"proto"` Proto NetworkRouteProto `json:"proto" yaml:"proto"`
CreateCommand *command.Command `yaml:"-" json:"-"` CreateCommand *Command `yaml:"-" json:"-"`
ReadCommand *command.Command `yaml:"-" json:"-"` ReadCommand *Command `yaml:"-" json:"-"`
UpdateCommand *command.Command `yaml:"-" json:"-"` UpdateCommand *Command `yaml:"-" json:"-"`
DeleteCommand *command.Command `yaml:"-" json:"-"` DeleteCommand *Command `yaml:"-" json:"-"`
Resources data.ResourceMapper `json:"-" yaml:"-"` config data.ConfigurationValueGetter
Resources data.ResourceMapper `json:"-" yaml:"-"`
} }
func NewNetworkRoute() *NetworkRoute { func NewNetworkRoute() *NetworkRoute {
n := &NetworkRoute{Rtid: NetworkRouteTableMain} n := &NetworkRoute{Rtid: NetworkRouteTableMain, Common: &Common{ resourceType: NetworkRouteTypeName } }
n.Common = NewCommon(NetworkRouteTypeName, false)
n.Common.NormalizePath = n.NormalizePath
n.CreateCommand, n.ReadCommand, n.UpdateCommand, n.DeleteCommand = n.NewCRUD() n.CreateCommand, n.ReadCommand, n.UpdateCommand, n.DeleteCommand = n.NewCRUD()
return n return n
} }
func (n *NetworkRoute) Init(u data.URIParser) (err error) {
if u == nil {
u = folio.URI(n.URI()).Parse()
}
return n.SetParsedURI(u)
}
func (n *NetworkRoute) NormalizePath() error {
return nil
}
func (n *NetworkRoute) SetResourceMapper(resources data.ResourceMapper) { func (n *NetworkRoute) SetResourceMapper(resources data.ResourceMapper) {
n.Resources = resources n.Resources = resources
} }
@ -188,6 +210,14 @@ func (n *NetworkRoute) URI() string {
return fmt.Sprintf("route://%s", n.Id) return fmt.Sprintf("route://%s", n.Id)
} }
func (n *NetworkRoute) SetURI(uri string) error {
return nil
}
func (n *NetworkRoute) UseConfig(config data.ConfigurationValueGetter) {
n.config = config
}
func (n *NetworkRoute) Validate() error { func (n *NetworkRoute) Validate() error {
return fmt.Errorf("failed") return fmt.Errorf("failed")
} }
@ -442,41 +472,41 @@ func (n *NetworkRoute) UnmarshalJSON(data []byte) error {
} }
func (n *NetworkRoute) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) { func (n *NetworkRoute) NewCRUD() (create *Command, read *Command, update *Command, del *Command) {
return NewNetworkRouteCreateCommand(), NewNetworkRouteReadCommand(), NewNetworkRouteUpdateCommand(), NewNetworkRouteDeleteCommand() return NewNetworkRouteCreateCommand(), NewNetworkRouteReadCommand(), NewNetworkRouteUpdateCommand(), NewNetworkRouteDeleteCommand()
} }
func NewNetworkRouteCreateCommand() *command.Command { func NewNetworkRouteCreateCommand() *Command {
c := command.NewCommand() c := NewCommand()
c.Path = "ip" c.Path = "ip"
c.Args = []command.CommandArg{ c.Args = []CommandArg{
command.CommandArg("route"), CommandArg("route"),
command.CommandArg("add"), CommandArg("add"),
command.CommandArg("{{ if .To }}to {{ .To }}{{ end }}"), CommandArg("{{ if .To }}to {{ .To }}{{ end }}"),
command.CommandArg("{{ if .Rtid }}table {{ .Rtid }}{{ end }}"), CommandArg("{{ if .Rtid }}table {{ .Rtid }}{{ end }}"),
command.CommandArg("{{ if .Gateway }}via {{ .Gateway }}{{ end }}"), CommandArg("{{ if .Gateway }}via {{ .Gateway }}{{ end }}"),
command.CommandArg("{{ if .Proto }}protocol {{ .Proto }}{{ end }}"), CommandArg("{{ if .Proto }}protocol {{ .Proto }}{{ end }}"),
command.CommandArg("{{ if .Scope }}scope {{ .Scope }}{{ end }}"), CommandArg("{{ if .Scope }}scope {{ .Scope }}{{ end }}"),
command.CommandArg("{{ if .RouteType }}type {{ .RouteType }}{{ end }}"), CommandArg("{{ if .RouteType }}type {{ .RouteType }}{{ end }}"),
command.CommandArg("{{ if .Interface }}dev {{ .Interface }}{{ end }}"), CommandArg("{{ if .Interface }}dev {{ .Interface }}{{ end }}"),
command.CommandArg("{{ if .Metric }}metric {{ .Metric }}{{ end }}"), CommandArg("{{ if .Metric }}metric {{ .Metric }}{{ end }}"),
} }
return c return c
} }
func NewNetworkRouteReadCommand() *command.Command { func NewNetworkRouteReadCommand() *Command {
c := command.NewCommand() c := NewCommand()
c.Path = "ip" c.Path = "ip"
c.Args = []command.CommandArg{ c.Args = []CommandArg{
command.CommandArg("route"), CommandArg("route"),
command.CommandArg("show"), CommandArg("show"),
command.CommandArg("{{ if .To }}to {{ .To }}{{ end }}"), CommandArg("{{ if .To }}to {{ .To }}{{ end }}"),
command.CommandArg("{{ if .Rtid }}table {{ .Rtid }}{{ end }}"), CommandArg("{{ if .Rtid }}table {{ .Rtid }}{{ end }}"),
command.CommandArg("{{ if .Gateway }}via {{ .Gateway }}{{ end }}"), CommandArg("{{ if .Gateway }}via {{ .Gateway }}{{ end }}"),
command.CommandArg("{{ if .Proto }}protocol {{ .Proto }}{{ end }}"), CommandArg("{{ if .Proto }}protocol {{ .Proto }}{{ end }}"),
command.CommandArg("{{ if .Scope }}scope {{ .Scope }}{{ end }}"), CommandArg("{{ if .Scope }}scope {{ .Scope }}{{ end }}"),
command.CommandArg("{{ if .RouteType }}type {{ .RouteType }}{{ end }}"), CommandArg("{{ if .RouteType }}type {{ .RouteType }}{{ end }}"),
command.CommandArg("{{ if .Interface }}dev {{ .Interface }}{{ end }}"), CommandArg("{{ if .Interface }}dev {{ .Interface }}{{ end }}"),
} }
c.Extractor = func(out []byte, target any) error { c.Extractor = func(out []byte, target any) error {
n := target.(*NetworkRoute) n := target.(*NetworkRoute)
@ -499,16 +529,16 @@ func NewNetworkRouteReadCommand() *command.Command {
return c return c
} }
func NewNetworkRouteUpdateCommand() *command.Command { func NewNetworkRouteUpdateCommand() *Command {
c := command.NewCommand() c := NewCommand()
c.Path = "ip" c.Path = "ip"
c.Args = []command.CommandArg{ c.Args = []CommandArg{
command.CommandArg("del"), CommandArg("del"),
command.CommandArg("{{ .Name }}"), CommandArg("{{ .Name }}"),
} }
return c return c
} }
func NewNetworkRouteDeleteCommand() *command.Command { func NewNetworkRouteDeleteCommand() *Command {
return nil return nil
} }

View File

@ -1,427 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
"context"
"errors"
"fmt"
"log/slog"
"gopkg.in/yaml.v3"
"net/url"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
"decl/internal/transport"
"decl/internal/data"
"decl/internal/folio"
"crypto"
"encoding/json"
"io"
"strings"
"io/fs"
"os"
"bytes"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/ProtonMail/go-crypto/openpgp/armor"
)
var (
ErrOpenPGPEncryptionFailure error = errors.New("OpenPGP encryption failure")
)
const (
OpenPGPKeyRingTypeName TypeName = "openpgp-keyring"
)
func init() {
folio.DocumentRegistry.ResourceTypes.Register([]string{"openpgp-keyring"}, func(u *url.URL) (res data.Resource) {
o := NewOpenPGPKeyRing()
if u != nil {
if err := folio.CastParsedURI(u).ConstructResource(o); err != nil {
panic(err)
}
}
return o
})
}
type OpenPGPKeyRing struct {
*Common `json:",inline" yaml:",inline"`
stater machine.Stater `json:"-" yaml:"-"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Comment string `json:"comment,omitempty" yaml:"comment,omitempty"`
Email string `json:"email,omitempty" yaml:"email,omitempty"`
KeyRing string `json:"keyring,omitempty" yaml:"keyring,omitempty"`
Bits int `json:"bits" yaml:"bits"`
KeyRingRef folio.ResourceReference `json:"keyringref,omitempty" yaml:"keyringref,omitempty"`
entityList openpgp.EntityList
}
func NewOpenPGPKeyRing() (o *OpenPGPKeyRing) {
o = &OpenPGPKeyRing {
Common: NewCommon(OpenPGPKeyRingTypeName, false),
Bits: 2048,
}
return
}
func (o *OpenPGPKeyRing) Init(uri data.URIParser) error {
if uri == nil {
uri = folio.URI(o.URI()).Parse()
} else {
// o.Name = uri.URL().Hostname()
}
return o.SetParsedURI(uri)
}
func (o *OpenPGPKeyRing) NormalizePath() error {
return nil
}
func (o *OpenPGPKeyRing) Validate() (err error) {
var keyringJson []byte
if keyringJson, err = o.JSON(); err == nil {
s := NewSchema(o.Type())
err = s.Validate(string(keyringJson))
}
return err
}
func (o *OpenPGPKeyRing) Config() *packet.Config {
config := &packet.Config{
RSABits: 2048,
Algorithm: packet.PubKeyAlgoRSA,
DefaultHash: crypto.SHA256,
DefaultCompressionAlgo: packet.CompressionZLIB,
}
return config
}
func (o *OpenPGPKeyRing) IsEncrypted(index int) (result bool) {
if len(o.entityList) >= index {
result = o.entityList[index].PrivateKey.Encrypted
}
return
}
func (o *OpenPGPKeyRing) EncryptPrivateKey(entity *openpgp.Entity) error {
passphraseConfig, _ := o.config.GetValue("passphrase")
passphrase := []byte(passphraseConfig.(string))
if len(passphrase) > 0 {
if encryptErr := entity.PrivateKey.Encrypt(passphrase); encryptErr != nil {
return fmt.Errorf("%w private key: %w", ErrOpenPGPEncryptionFailure, encryptErr)
}
for _, subkey := range entity.Subkeys {
if encryptErr := subkey.PrivateKey.Encrypt(passphrase); encryptErr != nil {
return fmt.Errorf("%w subkey (private key): %w", ErrOpenPGPEncryptionFailure, encryptErr)
}
}
}
return nil
}
func (o *OpenPGPKeyRing) Create(ctx context.Context) (err error) {
var entity *openpgp.Entity
cfg := o.Config()
entity, err = openpgp.NewEntity(o.Name, o.Comment, o.Email, cfg)
o.entityList = append(o.entityList, entity)
if entity.PrivateKey == nil {
return fmt.Errorf("Failed creating new private key")
}
if entity.PrimaryKey == nil {
return fmt.Errorf("Failed creating new public key")
}
if err = o.EncryptPrivateKey(entity); err != nil {
return
}
if len(o.KeyRing) == 0 {
var keyringBuffer bytes.Buffer
if publicKeyWriter, err := armor.Encode(&keyringBuffer, openpgp.PublicKeyType, nil); err == nil {
if err = entity.Serialize(publicKeyWriter); err == nil {
}
publicKeyWriter.Close()
}
keyringBuffer.WriteString("\n")
if privateKeyWriter, err := armor.Encode(&keyringBuffer, openpgp.PrivateKeyType, nil); err == nil {
if err = entity.SerializePrivateWithoutSigning(privateKeyWriter, nil); err == nil {
}
privateKeyWriter.Close()
}
keyringBuffer.WriteString("\n")
o.KeyRing = keyringBuffer.String()
}
return
}
func (o *OpenPGPKeyRing) Clone() data.Resource {
return &OpenPGPKeyRing {
Common: o.Common.Clone(),
Name: o.Name,
Comment: o.Comment,
Email: o.Email,
Bits: o.Bits,
}
}
func (o *OpenPGPKeyRing) StateMachine() machine.Stater {
if o.stater == nil {
o.stater = StorageMachine(o)
}
return o.stater
}
func (o *OpenPGPKeyRing) Notify(m *machine.EventMessage) {
ctx := context.Background()
switch m.On {
case machine.ENTERSTATEEVENT:
switch m.Dest {
case "start_stat":
if statErr := o.ReadStat(); statErr == nil {
if triggerErr := o.StateMachine().Trigger("exists"); triggerErr == nil {
return
}
} else {
if triggerErr := o.StateMachine().Trigger("notexists"); triggerErr == nil {
return
}
}
case "start_read":
if _,readErr := o.Read(ctx); readErr == nil {
if triggerErr := o.StateMachine().Trigger("state_read"); triggerErr == nil {
return
} else {
_ = o.AddError(triggerErr)
}
} else {
_ = o.AddError(readErr)
if o.IsResourceInconsistent() {
if triggerErr := o.StateMachine().Trigger("read-failed"); triggerErr == nil {
panic(readErr)
} else {
_ = o.AddError(triggerErr)
panic(fmt.Errorf("%w - %w", readErr, triggerErr))
}
}
_ = o.AddError(o.StateMachine().Trigger("notexists"))
}
case "start_create":
if createErr := o.Create(ctx); createErr == nil {
if triggerErr := o.StateMachine().Trigger("created"); triggerErr == nil {
return
} else {
_ = o.AddError(triggerErr)
}
} else {
_ = o.AddError(createErr)
if o.IsResourceInconsistent() {
if triggerErr := o.StateMachine().Trigger("create-failed"); triggerErr == nil {
panic(createErr)
} else {
_ = o.AddError(triggerErr)
panic(fmt.Errorf("%w - %w", createErr, triggerErr))
}
}
_ = o.StateMachine().Trigger("notexists")
panic(createErr)
}
case "start_update":
if updateErr := o.Update(ctx); updateErr == nil {
if triggerErr := o.stater.Trigger("updated"); triggerErr == nil {
return
} else {
_ = o.AddError(triggerErr)
}
} else {
_ = o.AddError(updateErr)
if o.IsResourceInconsistent() {
if triggerErr := o.StateMachine().Trigger("update-failed"); triggerErr == nil {
panic(updateErr)
} else {
panic(fmt.Errorf("%w - %w", updateErr, triggerErr))
}
}
_ = o.StateMachine().Trigger("notexists")
panic(updateErr)
}
case "start_delete":
if deleteErr := o.Delete(ctx); deleteErr == nil {
if triggerErr := o.StateMachine().Trigger("deleted"); triggerErr == nil {
return
} else {
o.Common.State = "present"
panic(triggerErr)
}
} else {
_ = o.StateMachine().Trigger("exists")
panic(deleteErr)
}
case "inconsistent":
o.Common.State = "inconsistent"
case "absent":
o.Common.State = "absent"
case "present", "created", "read":
o.Common.State = "present"
}
case machine.EXITSTATEEVENT:
switch m.Dest {
case "start_create":
slog.Info("OpenPGP_Entity.Notify - EXITSTATE", "dest", m.Dest, "common.state", o.Common.State)
}
}
}
func (o *OpenPGPKeyRing) KeyRingRefStat() (info fs.FileInfo, err error) {
if len(o.KeyRingRef) > 0 {
rs, _ := o.ContentReaderStream()
defer rs.Close()
info, err = rs.Stat()
}
return
}
func (o *OpenPGPKeyRing) FilePath() string {
return o.Common.Path
}
func (o *OpenPGPKeyRing) JSON() ([]byte, error) {
return json.Marshal(o)
}
func (o *OpenPGPKeyRing) Apply() error {
ctx := context.Background()
switch o.Common.State {
case "absent":
return o.Delete(ctx)
case "present":
return o.Create(ctx)
}
return nil
}
func (o *OpenPGPKeyRing) Load(docData []byte, format codec.Format) (err error) {
err = format.StringDecoder(string(docData)).Decode(o)
return
}
func (o *OpenPGPKeyRing) LoadReader(r io.ReadCloser, format codec.Format) (err error) {
err = format.Decoder(r).Decode(o)
return
}
func (o *OpenPGPKeyRing) LoadString(docData string, format codec.Format) (err error) {
err = format.StringDecoder(docData).Decode(o)
return
}
func (o *OpenPGPKeyRing) LoadDecl(yamlResourceDeclaration string) (err error) {
return o.LoadString(yamlResourceDeclaration, codec.FormatYaml)
}
func (o *OpenPGPKeyRing) ResolveId(ctx context.Context) string {
if e := o.NormalizePath(); e != nil {
panic(e)
}
return o.Common.Path
}
func (o *OpenPGPKeyRing) GetContentSourceRef() string {
return string(o.KeyRingRef)
}
func (o *OpenPGPKeyRing) SetContentSourceRef(uri string) {
o.KeyRingRef = folio.ResourceReference(uri)
}
func (o *OpenPGPKeyRing) Stat() (info fs.FileInfo, err error) {
return o.KeyRingRefStat()
}
func (o *OpenPGPKeyRing) Update(ctx context.Context) error {
return o.Create(ctx)
}
func (o *OpenPGPKeyRing) Delete(ctx context.Context) error {
return os.Remove(o.Common.Path)
}
func (o *OpenPGPKeyRing) ReadStat() (err error) {
if _, err = o.Stat(); err != nil {
o.Common.State = "absent"
}
return
}
func (o *OpenPGPKeyRing) Read(ctx context.Context) (yamlData []byte, err error) {
var keyringReader io.ReadCloser
statErr := o.ReadStat()
if statErr != nil {
return nil, fmt.Errorf("%w - %w: %s", ErrResourceStateAbsent, statErr, o.Path)
}
if keyringReader, err = o.GetContent(nil); err == nil {
if krData, krErr := io.ReadAll(keyringReader); krErr == nil {
o.KeyRing = string(krData)
o.entityList, err = openpgp.ReadArmoredKeyRing(strings.NewReader(o.KeyRing))
} else {
err = krErr
}
}
o.Common.State = "present"
return yaml.Marshal(o)
}
func (o *OpenPGPKeyRing) GetContent(w io.Writer) (contentReader io.ReadCloser, err error) {
contentReader, err = o.readThru()
if w != nil {
copyBuffer := make([]byte, 32 * 1024)
_, writeErr := io.CopyBuffer(w, contentReader, copyBuffer)
if writeErr != nil {
return nil, fmt.Errorf("OpenPGPKeyRing.GetContent(): CopyBuffer failed %v %v: %w", w, contentReader, writeErr)
}
return nil, nil
}
return
}
func (o *OpenPGPKeyRing) readThru() (contentReader io.ReadCloser, err error) {
if o.KeyRingRef.IsEmpty() {
if len(o.KeyRing) != 0 {
contentReader = io.NopCloser(strings.NewReader(o.KeyRing))
}
} else {
contentReader, err = o.KeyRingRef.Lookup(nil).ContentReaderStream()
contentReader.(*transport.Reader).SetGzip(false)
}
return
}
func (o *OpenPGPKeyRing) URI() string { return string(o.Common.URI()) }
func (o *OpenPGPKeyRing) Type() string { return "openpgp-keyring" }
func (o *OpenPGPKeyRing) ContentReaderStream() (*transport.Reader, error) {
if len(o.KeyRing) == 0 && ! o.KeyRingRef.IsEmpty() {
return o.KeyRingRef.Lookup(nil).ContentReaderStream()
}
return nil, fmt.Errorf("Cannot provide transport reader for string content")
}
func (o *OpenPGPKeyRing) ContentWriterStream() (*transport.Writer, error) {
if len(o.KeyRing) == 0 && ! o.KeyRingRef.IsEmpty() {
return o.KeyRingRef.Lookup(nil).ContentWriterStream()
}
return nil, fmt.Errorf("Cannot provide transport writer for string content")
}

View File

@ -1,108 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
"context"
"github.com/stretchr/testify/assert"
"testing"
"fmt"
"decl/internal/data"
)
func TestNewOpenPGPKeyRingResource(t *testing.T) {
assert.NotNil(t, NewOpenPGPKeyRing())
}
func TestCreateKeyRing(t *testing.T) {
ctx := context.Background()
declarationAttributes := `
name: TestUser1
comment: TestUser1
email: testuser@rosskeen.house
`
testKeyRing := NewOpenPGPKeyRing()
e := testKeyRing.LoadDecl(declarationAttributes)
assert.Nil(t, e)
testKeyRing.UseConfig(MockConfigValueGetter(func(key string) (any, error) {
switch key {
case "passphrase":
return "foo", nil
}
return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key)
}))
err := testKeyRing.Create(ctx)
assert.Nil(t, err)
assert.Greater(t, len(testKeyRing.entityList), 0)
assert.Contains(t, testKeyRing.entityList[0].Identities, "TestUser1 (TestUser1) <testuser@rosskeen.house>")
assert.Contains(t, testKeyRing.KeyRing, "-----END PGP PUBLIC KEY BLOCK-----")
assert.Contains(t, testKeyRing.KeyRing, "-----END PGP PRIVATE KEY BLOCK-----")
assert.True(t, testKeyRing.IsEncrypted(0))
}
func TestReadKeyRing(t *testing.T) {
ctx := context.Background()
declarationAttributes := `
keyring: |-
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGNBGctCH8BDADGmdabVG6gDuSRk0razrEMEproTMT5w9zUMWH5uUeLY9eM9g5S
/5I062ume5jj6MIC1lq7tqJXh3Zwcv7Lf7ER1SWa1h6BGruHaF4o9GiR01FHfyVM
YTMTkMxFi1wnY87Mr0f+EIv1i9u2nD1o9moBXzEXT0JFFGyla8DvnblQhLhrwfNl
lN0L2LQJDTVnrPj4eMaFshqP2UdqNiYjR2qfLyCH/ZZhxg++G2KJhNzlkOzqZZJk
iYwfEUvGg/PzdCsSOYEvSureI0bF1hKBGK+RpOY0sKcvSY0xiY1YXEzJSau5cnFe
/mdwC7YleZiKsGOyBsbRFn7FUXM4eM7CkDISjZMmKDBzbvzgFXsUG2upgC+B7fvi
pTpgQxQk1Xi3+4bNQmgupJEUrP0XlIFoBVJdoTb0wcs8JUNDfc6ESZB+eA1OJdR+
xiag1XwN3PKcwzmoZoZ71oT/eqAOufwhWXFJ+StPqwd+BVpK1YwbG0jRagNlM/29
+Rzh2A70qwCcCXcAEQEAAbQwVGVzdFVzZXIgKFRlc3RVc2VyKSA8bWF0dGhld3Jp
Y2guY29uZkBnbWFpbC5jb20+iQHOBBMBCgA4FiEErhhqUPYtSfwcGHCb+/Evfjwu
gEkFAmctCH8CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ+/EvfjwugElu
cwv/ZB9xlMf8C9CBOVX8nvU1HiWvmJqlvcePobQBc7Y54pWuK+giv+/SE3bEx6x/
Vb0aWrJ52CBod6R1YfyPW+F58W9kADIPFRkH/bXExj+WMrXZU4J8Gz5nCxECK6PB
CR8xh/T9lbvDt1q7JeP4+ldzZJoSLxAK6D5EeYTC8OKXVMuTgHBmwtiTC+Hyja3+
HV1MZwx7SnnXmX5dRtPq8z1F1shoM4UTLEaolA6r3XQKwfsP9c6LS2VUc+Yft4eN
6JCz9+fa/N9bMgIS6Az23JDYJWynmmPx82Y/uqiSxXL9qljOUsgR/QK9OaLL8fFH
UD6Ob+TnjH/cPBoESXrslFcwKZWMsAxJ9w6K/HJT+Fm+8XcbN3awoXcEtLAeKirL
z7borUsseCXJqY4epHfbvhx7NjhxElspY2A51l6oX4OoVyFL3163anxwzEEXgMRk
+pPGlzw55cq/iN48qURetgs94Vdr4HCNJFY8+CLUyNqPQHaVXA6nUndL2wqfOqwj
82R0uQGNBGctCH8BDAC/uHoD/vw8dOQt/cHyObaAEunN3Xy2MOtpY7TRh9AdrNKY
O0hEFQvllf8iEzW4WjiIXCzNyWzY53AD6k1kWg5tW0/6hLxk9YMUnUhi6MSD17zj
QQMR8XRUNuadVh8G0INJnvXVhgJXSQmKCn+4e6e1/gYKvHq9uEYf4N1BSazlCH/e
ZEhHTzI8WLtZeG+rM1wBW/3KuRrLDP9WUHamzp+0bL5OKvEhetZQZQxPr9wYccAh
bPU9MeatkAn6CwbeCOxUGUbwC0rzMVL3CPvOjhPFWGJaqi4H4ZdSSKN/vceXyfWh
CvzzJR/v0jzwJaE6sxIdIu1ylRKXN+hZ7Eqn7ZDurWgVxAH9o0jXkBNGsmZlqdRx
J+86/aGpSlNXZZO6o4xznV9Xd8ssuvwMLKN3qwVYEcbFOTdgeRw8dJo8fx4Y14tZ
RQUVPLh2iI4ykjFnBJFfOExAEKHQauLgQ6iXRsetgTb5RvUevOvIOJJTZGrqrhxt
7lHYlDfxS7zJL9ygldMAEQEAAYkBtgQYAQoAIBYhBK4YalD2LUn8HBhwm/vxL348
LoBJBQJnLQh/AhsMAAoJEPvxL348LoBJ+5oMALOv9RIyhNvyeJ4y7TLpOervh/0C
EfvIxYEVtDTFZlqfkuovhF1Cbgu+PP9iG2JU0FYHsNisf+1XSMKHX0DIm9gWWZaZ
J1CElJ4vsQ0t/4ypSrP7cZB6FokrQBcglpB9mVg0meVzCmZOJfVL+s+gCycshSZR
msw9Y3tN72JMAGdxHXtr1DTL3uDbl12Bz+egYNrqmugX9Jc9HiWG51XO9SDtztG0
KtVLcBA6X4Avc940Q4d4BofmOT4ajAAnysnR84UvTTSaAr9m/xvyKNEuS4YLysaC
gOG8nDFxujEkfO5FW+N1r5hFd2owt8Ige4e59wPRu5RVycPF3+JnxM70wFxQPkO3
lDtVTMG9vZyRkxRyKeqFo0z4msbc9WHwdvI6l/h7h2v6E6VbMe2sX/k+CxNyTPBX
sn7sjApKUjVpdXtHbu81ELhAbVPJPpMlkTdUwUUUfPD7uBoyRQbEQwgpbPQrEqmE
+aAQq8u60fWheEIG+xaV3T01zrNRUo6I7xu5kA==
=yFbn
-----END PGP PUBLIC KEY BLOCK-----
`
testKeyRing := NewOpenPGPKeyRing()
e := testKeyRing.LoadDecl(declarationAttributes)
assert.Nil(t, e)
y, err := testKeyRing.Read(ctx)
assert.Nil(t, err)
assert.NotNil(t, y)
assert.Greater(t, len(testKeyRing.entityList), 0)
assert.Contains(t, testKeyRing.entityList[0].Identities, "TestUser (TestUser) <matthewrich.conf@gmail.com>")
}

View File

@ -1,365 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
"context"
"errors"
"fmt"
"log/slog"
"gopkg.in/yaml.v3"
"net/url"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
"decl/internal/ext"
"decl/internal/transport"
"decl/internal/data"
"decl/internal/folio"
"crypto"
"encoding/json"
"io"
"io/fs"
"bytes"
"os"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/ProtonMail/go-crypto/openpgp/armor"
)
var (
ErrSignatureWriterFailed error = errors.New("Failed creating signature writer")
ErrArmoredWriterFailed error = errors.New("Failed to create armored writer")
)
const (
OpenPGPSignatureTypeName TypeName = "openpgp-signature"
)
func init() {
folio.DocumentRegistry.ResourceTypes.Register([]string{"openpgp-signature"}, func(u *url.URL) (res data.Resource) {
o := NewOpenPGPSignature()
if u != nil {
if err := folio.CastParsedURI(u).ConstructResource(o); err != nil {
panic(err)
}
}
return o
})
}
type OpenPGPSignature struct {
*Common `json:",inline" yaml:",inline"`
stater machine.Stater `json:"-" yaml:"-"`
Signature string `json:"signature,omitempty" yaml:"signature,omitempty"`
KeyRingRef folio.ResourceReference `json:"keyringref,omitempty" yaml:"keyringref,omitempty"`
SourceRef folio.ResourceReference `json:"soureref,omitempty" yaml:"sourceref,omitempty"`
SignatureRef folio.ResourceReference `json:"signatureref,omitempty" yaml:"signatureref,omitempty"`
message *openpgp.MessageDetails
entityList openpgp.EntityList
}
func NewOpenPGPSignature() *OpenPGPSignature {
o := &OpenPGPSignature {
Common: NewCommon(OpenPGPSignatureTypeName, false),
}
return o
}
func (o *OpenPGPSignature) Type() string { return "openpgp-signature" }
func (o *OpenPGPSignature) Init(uri data.URIParser) error {
if uri == nil {
uri = folio.URI(o.URI()).Parse()
} else {
// o.Name = uri.URL().Hostname()
}
return o.SetParsedURI(uri)
}
func (o *OpenPGPSignature) NormalizePath() error {
return nil
}
func (o *OpenPGPSignature) StateMachine() machine.Stater {
if o.stater == nil {
o.stater = StorageMachine(o)
}
return o.stater
}
func (o *OpenPGPSignature) URI() string {
return string(o.Common.URI())
}
func (o *OpenPGPSignature) SigningEntity() (err error) {
if o.KeyRingRef.IsEmpty() {
var keyringConfig any
if keyringConfig, err = o.config.GetValue("keyring"); err == nil {
o.entityList = keyringConfig.(openpgp.EntityList)
}
} else {
ringFileStream, _ := o.KeyRingRef.Lookup(o.Resources).ContentReaderStream()
defer ringFileStream.Close()
o.entityList, err = openpgp.ReadArmoredKeyRing(ringFileStream)
}
return
}
func (o *OpenPGPSignature) Sign(message io.Reader, w io.Writer) (err error) {
var writer io.WriteCloser
entity := o.entityList[0]
if writer, err = openpgp.Sign(w, entity, nil, o.Config()); err == nil {
defer writer.Close()
_, err = io.Copy(writer, message)
} else {
err = fmt.Errorf("%w: %w", ErrSignatureWriterFailed, err)
}
return
}
func (o *OpenPGPSignature) Create(ctx context.Context) (err error) {
if err = o.SigningEntity(); err == nil {
var sourceReadStream io.ReadCloser
sourceReadStream, err = o.SourceRef.Lookup(o.Resources).ContentReaderStream()
var signatureStream, armoredWriter io.WriteCloser
if o.SignatureRef.IsEmpty() {
var signatureContent bytes.Buffer
signatureStream = ext.WriteNopCloser(&signatureContent)
defer func() { o.Signature = signatureContent.String() }()
} else {
signatureStream, _ = o.SignatureRef.Lookup(o.Resources).ContentWriterStream()
}
if armoredWriter, err = armor.Encode(signatureStream, openpgp.SignatureType, nil); err != nil {
err = fmt.Errorf("%w: %w", ErrArmoredWriterFailed, err)
}
defer armoredWriter.Close()
err = o.Sign(sourceReadStream, armoredWriter)
}
return
}
func (o *OpenPGPSignature) Validate() (err error) {
var signatureJson []byte
if signatureJson, err = o.JSON(); err == nil {
s := NewSchema(o.Type())
err = s.Validate(string(signatureJson))
}
return err
}
func (o *OpenPGPSignature) Config() *packet.Config {
config := &packet.Config{
RSABits: 2048,
Algorithm: packet.PubKeyAlgoRSA,
DefaultHash: crypto.SHA256,
DefaultCompressionAlgo: packet.CompressionZLIB,
}
return config
}
func (o *OpenPGPSignature) Clone() data.Resource {
return &OpenPGPSignature {
Common: o.Common.Clone(),
KeyRingRef: o.KeyRingRef,
SourceRef: o.SourceRef,
SignatureRef: o.SignatureRef,
}
}
func (o *OpenPGPSignature) Notify(m *machine.EventMessage) {
ctx := context.Background()
switch m.On {
case machine.ENTERSTATEEVENT:
switch m.Dest {
case "start_stat":
if statErr := o.ReadStat(); statErr == nil {
if triggerErr := o.StateMachine().Trigger("exists"); triggerErr == nil {
return
}
} else {
if triggerErr := o.StateMachine().Trigger("notexists"); triggerErr == nil {
return
}
}
case "start_read":
if _,readErr := o.Read(ctx); readErr == nil {
if triggerErr := o.StateMachine().Trigger("state_read"); triggerErr == nil {
return
} else {
_ = o.AddError(triggerErr)
}
} else {
_ = o.AddError(readErr)
if o.IsResourceInconsistent() {
if triggerErr := o.StateMachine().Trigger("read-failed"); triggerErr == nil {
panic(readErr)
} else {
_ = o.AddError(triggerErr)
panic(fmt.Errorf("%w - %w", readErr, triggerErr))
}
}
_ = o.AddError(o.StateMachine().Trigger("notexists"))
}
case "start_create":
if createErr := o.Create(ctx); createErr == nil {
if triggerErr := o.StateMachine().Trigger("created"); triggerErr == nil {
return
} else {
_ = o.AddError(triggerErr)
}
} else {
_ = o.AddError(createErr)
if o.IsResourceInconsistent() {
if triggerErr := o.StateMachine().Trigger("create-failed"); triggerErr == nil {
panic(createErr)
} else {
_ = o.AddError(triggerErr)
panic(fmt.Errorf("%w - %w", createErr, triggerErr))
}
}
_ = o.StateMachine().Trigger("notexists")
panic(createErr)
}
case "start_update":
if updateErr := o.Update(ctx); updateErr == nil {
if triggerErr := o.stater.Trigger("updated"); triggerErr == nil {
return
} else {
_ = o.AddError(triggerErr)
}
} else {
_ = o.AddError(updateErr)
if o.IsResourceInconsistent() {
if triggerErr := o.StateMachine().Trigger("update-failed"); triggerErr == nil {
panic(updateErr)
} else {
panic(fmt.Errorf("%w - %w", updateErr, triggerErr))
}
}
_ = o.StateMachine().Trigger("notexists")
panic(updateErr)
}
case "start_delete":
if deleteErr := o.Delete(ctx); deleteErr == nil {
if triggerErr := o.StateMachine().Trigger("deleted"); triggerErr == nil {
return
} else {
o.Common.State = "present"
panic(triggerErr)
}
} else {
_ = o.StateMachine().Trigger("exists")
panic(deleteErr)
}
case "inconsistent":
o.Common.State = "inconsistent"
case "absent":
o.Common.State = "absent"
case "present", "created", "read":
o.Common.State = "present"
}
case machine.EXITSTATEEVENT:
switch m.Dest {
case "start_create":
slog.Info("OpenPGPSignature.Notify - EXITSTATE", "dest", m.Dest, "common.state", o.Common.State)
}
}
}
func (o *OpenPGPSignature) FilePath() string {
return o.Common.Path
}
func (o *OpenPGPSignature) JSON() ([]byte, error) {
return json.Marshal(o)
}
func (o *OpenPGPSignature) Apply() error {
ctx := context.Background()
switch o.Common.State {
case "absent":
return o.Delete(ctx)
case "present":
return o.Create(ctx)
}
return nil
}
func (o *OpenPGPSignature) Load(docData []byte, format codec.Format) (err error) {
err = format.StringDecoder(string(docData)).Decode(o)
return
}
func (o *OpenPGPSignature) LoadReader(r io.ReadCloser, format codec.Format) (err error) {
err = format.Decoder(r).Decode(o)
return
}
func (o *OpenPGPSignature) LoadString(docData string, format codec.Format) (err error) {
err = format.StringDecoder(docData).Decode(o)
return
}
func (o *OpenPGPSignature) LoadDecl(yamlResourceDeclaration string) (err error) {
return o.LoadString(yamlResourceDeclaration, codec.FormatYaml)
}
func (o *OpenPGPSignature) ResolveId(ctx context.Context) string {
if e := o.NormalizePath(); e != nil {
panic(e)
}
return o.Common.Path
}
func (o *OpenPGPSignature) GetContentSourceRef() string {
return string(o.SignatureRef)
}
func (o *OpenPGPSignature) SetContentSourceRef(uri string) {
o.SignatureRef = folio.ResourceReference(uri)
}
func (o *OpenPGPSignature) SignatureRefStat() (info fs.FileInfo, err error) {
err = fmt.Errorf("%w: %s", ErrResourceStateAbsent, o.SignatureRef)
if len(o.SignatureRef) > 0 {
rs, _ := o.ContentReaderStream()
defer rs.Close()
return rs.Stat()
}
return
}
func (o *OpenPGPSignature) Stat() (info fs.FileInfo, err error) {
return o.SignatureRefStat()
}
func (o *OpenPGPSignature) ReadStat() (err error) {
_, err = o.SignatureRefStat()
return err
}
func (o *OpenPGPSignature) Update(ctx context.Context) error {
return o.Create(ctx)
}
func (o *OpenPGPSignature) Delete(ctx context.Context) error {
return os.Remove(o.Common.Path)
}
func (o *OpenPGPSignature) Read(ctx context.Context) ([]byte, error) {
return yaml.Marshal(o)
}
func (o *OpenPGPSignature) ContentReaderStream() (*transport.Reader, error) {
return nil, nil
}

View File

@ -1,142 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
"context"
"github.com/stretchr/testify/assert"
"testing"
"fmt"
"decl/internal/data"
"decl/internal/folio"
"decl/internal/codec"
"log"
)
func NewTestUserKeys() (data.ResourceMapper, folio.URI) {
uri := "openpgp-keyring://TestUser1/TestUser1/testuser@rosskeen.house"
keyRingDecl := folio.NewDeclaration()
keyRingDecl.NewResource(&uri)
ctx := context.Background()
declarationAttributes := fmt.Sprintf(`
name: TestUser1
comment: TestUser1
email: testuser@rosskeen.house
keyringref: file://%s/keyring.asc
`, string(TempDir))
testKeyRing := keyRingDecl.Resource()
if e := testKeyRing.LoadString(declarationAttributes, codec.FormatYaml); e != nil {
log.Fatal(e)
}
testKeyRing.UseConfig(MockConfigValueGetter(func(key string) (any, error) {
switch key {
case "passphrase":
return "foo", nil
}
return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key)
}))
if err := testKeyRing.Create(ctx); err != nil {
log.Fatal(err)
}
return TestResourceMapper(func(key string) (data.Declaration, bool) {
return keyRingDecl, true
}), folio.URI(uri)
}
func TestNewOpenPGPSignatureResource(t *testing.T) {
assert.NotNil(t, NewOpenPGPSignature())
}
func TestCreateSignature(t *testing.T) {
ctx := context.Background()
m, keyRingUri := NewTestUserKeys()
assert.Nil(t, TempDir.CreateFile("test.txt", "test data"))
declarationAttributes := fmt.Sprintf(`
keyringref: %s
sourceref: file://%s/test.txt
`, string(keyRingUri), string(TempDir))
testSignature := NewOpenPGPSignature()
testSignature.Resources = m
e := testSignature.LoadDecl(declarationAttributes)
assert.Nil(t, e)
err := testSignature.Create(ctx)
assert.Nil(t, err)
/*
assert.Greater(t, len(testSignature.entityList), 0)
assert.Contains(t, testSignature.entityList[0].Identities, "TestUser1 (TestUser1) <testuser@rosskeen.house>")
assert.Contains(t, testSignature.Signature, "-----END PGP PUBLIC KEY BLOCK-----")
assert.Contains(t, testSignature.Signature, "-----END PGP PRIVATE KEY BLOCK-----")
*/
}
/*
func TestReadSignature(t *testing.T) {
ctx := context.Background()
declarationAttributes := `
signature: |-
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGNBGctCH8BDADGmdabVG6gDuSRk0razrEMEproTMT5w9zUMWH5uUeLY9eM9g5S
/5I062ume5jj6MIC1lq7tqJXh3Zwcv7Lf7ER1SWa1h6BGruHaF4o9GiR01FHfyVM
YTMTkMxFi1wnY87Mr0f+EIv1i9u2nD1o9moBXzEXT0JFFGyla8DvnblQhLhrwfNl
lN0L2LQJDTVnrPj4eMaFshqP2UdqNiYjR2qfLyCH/ZZhxg++G2KJhNzlkOzqZZJk
iYwfEUvGg/PzdCsSOYEvSureI0bF1hKBGK+RpOY0sKcvSY0xiY1YXEzJSau5cnFe
/mdwC7YleZiKsGOyBsbRFn7FUXM4eM7CkDISjZMmKDBzbvzgFXsUG2upgC+B7fvi
pTpgQxQk1Xi3+4bNQmgupJEUrP0XlIFoBVJdoTb0wcs8JUNDfc6ESZB+eA1OJdR+
xiag1XwN3PKcwzmoZoZ71oT/eqAOufwhWXFJ+StPqwd+BVpK1YwbG0jRagNlM/29
+Rzh2A70qwCcCXcAEQEAAbQwVGVzdFVzZXIgKFRlc3RVc2VyKSA8bWF0dGhld3Jp
Y2guY29uZkBnbWFpbC5jb20+iQHOBBMBCgA4FiEErhhqUPYtSfwcGHCb+/Evfjwu
gEkFAmctCH8CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ+/EvfjwugElu
cwv/ZB9xlMf8C9CBOVX8nvU1HiWvmJqlvcePobQBc7Y54pWuK+giv+/SE3bEx6x/
Vb0aWrJ52CBod6R1YfyPW+F58W9kADIPFRkH/bXExj+WMrXZU4J8Gz5nCxECK6PB
CR8xh/T9lbvDt1q7JeP4+ldzZJoSLxAK6D5EeYTC8OKXVMuTgHBmwtiTC+Hyja3+
HV1MZwx7SnnXmX5dRtPq8z1F1shoM4UTLEaolA6r3XQKwfsP9c6LS2VUc+Yft4eN
6JCz9+fa/N9bMgIS6Az23JDYJWynmmPx82Y/uqiSxXL9qljOUsgR/QK9OaLL8fFH
UD6Ob+TnjH/cPBoESXrslFcwKZWMsAxJ9w6K/HJT+Fm+8XcbN3awoXcEtLAeKirL
z7borUsseCXJqY4epHfbvhx7NjhxElspY2A51l6oX4OoVyFL3163anxwzEEXgMRk
+pPGlzw55cq/iN48qURetgs94Vdr4HCNJFY8+CLUyNqPQHaVXA6nUndL2wqfOqwj
82R0uQGNBGctCH8BDAC/uHoD/vw8dOQt/cHyObaAEunN3Xy2MOtpY7TRh9AdrNKY
O0hEFQvllf8iEzW4WjiIXCzNyWzY53AD6k1kWg5tW0/6hLxk9YMUnUhi6MSD17zj
QQMR8XRUNuadVh8G0INJnvXVhgJXSQmKCn+4e6e1/gYKvHq9uEYf4N1BSazlCH/e
ZEhHTzI8WLtZeG+rM1wBW/3KuRrLDP9WUHamzp+0bL5OKvEhetZQZQxPr9wYccAh
bPU9MeatkAn6CwbeCOxUGUbwC0rzMVL3CPvOjhPFWGJaqi4H4ZdSSKN/vceXyfWh
CvzzJR/v0jzwJaE6sxIdIu1ylRKXN+hZ7Eqn7ZDurWgVxAH9o0jXkBNGsmZlqdRx
J+86/aGpSlNXZZO6o4xznV9Xd8ssuvwMLKN3qwVYEcbFOTdgeRw8dJo8fx4Y14tZ
RQUVPLh2iI4ykjFnBJFfOExAEKHQauLgQ6iXRsetgTb5RvUevOvIOJJTZGrqrhxt
7lHYlDfxS7zJL9ygldMAEQEAAYkBtgQYAQoAIBYhBK4YalD2LUn8HBhwm/vxL348
LoBJBQJnLQh/AhsMAAoJEPvxL348LoBJ+5oMALOv9RIyhNvyeJ4y7TLpOervh/0C
EfvIxYEVtDTFZlqfkuovhF1Cbgu+PP9iG2JU0FYHsNisf+1XSMKHX0DIm9gWWZaZ
J1CElJ4vsQ0t/4ypSrP7cZB6FokrQBcglpB9mVg0meVzCmZOJfVL+s+gCycshSZR
msw9Y3tN72JMAGdxHXtr1DTL3uDbl12Bz+egYNrqmugX9Jc9HiWG51XO9SDtztG0
KtVLcBA6X4Avc940Q4d4BofmOT4ajAAnysnR84UvTTSaAr9m/xvyKNEuS4YLysaC
gOG8nDFxujEkfO5FW+N1r5hFd2owt8Ige4e59wPRu5RVycPF3+JnxM70wFxQPkO3
lDtVTMG9vZyRkxRyKeqFo0z4msbc9WHwdvI6l/h7h2v6E6VbMe2sX/k+CxNyTPBX
sn7sjApKUjVpdXtHbu81ELhAbVPJPpMlkTdUwUUUfPD7uBoyRQbEQwgpbPQrEqmE
+aAQq8u60fWheEIG+xaV3T01zrNRUo6I7xu5kA==
=yFbn
-----END PGP PUBLIC KEY BLOCK-----
`
testSignature := NewOpenPGPSignature()
e := testSignature.LoadDecl(declarationAttributes)
assert.Nil(t, e)
y, err := testSignature.Read(ctx)
assert.Nil(t, err)
assert.NotNil(t, y)
assert.Greater(t, len(testSignature.entityList), 0)
assert.Contains(t, testSignature.entityList[0].Identities, "TestUser (TestUser) <matthewrich.conf@gmail.com>")
}
*/

View File

@ -23,9 +23,6 @@ import (
"decl/internal/tempdir" "decl/internal/tempdir"
) )
const (
PackageTypeName TypeName = "package"
)
var ( var (
PackageTempDir tempdir.Path = "jx_package_resource" PackageTempDir tempdir.Path = "jx_package_resource"
) )
@ -53,8 +50,7 @@ var SupportedPackageTypes []PackageType = []PackageType{PackageTypeApk, PackageT
var SystemPackageType PackageType = FindSystemPackageType() var SystemPackageType PackageType = FindSystemPackageType()
type Package struct { type Package struct {
*Common `yaml:",inline" json:",inline"` stater machine.Stater `yaml:"-" json:"-"`
stater machine.Stater `yaml:"-" json:"-"`
Source string `yaml:"source,omitempty" json:"source,omitempty"` Source string `yaml:"source,omitempty" json:"source,omitempty"`
Name string `yaml:"name" json:"name"` Name string `yaml:"name" json:"name"`
Required string `json:"required,omitempty" yaml:"required,omitempty"` Required string `json:"required,omitempty" yaml:"required,omitempty"`
@ -66,12 +62,18 @@ type Package struct {
ReadCommand *command.Command `yaml:"-" json:"-"` ReadCommand *command.Command `yaml:"-" json:"-"`
UpdateCommand *command.Command `yaml:"-" json:"-"` UpdateCommand *command.Command `yaml:"-" json:"-"`
DeleteCommand *command.Command `yaml:"-" json:"-"` DeleteCommand *command.Command `yaml:"-" json:"-"`
// state attributes
State string `yaml:"state,omitempty" json:"state,omitempty"`
config data.ConfigurationValueGetter
Resources data.ResourceMapper `yaml:"-" json:"-"` Resources data.ResourceMapper `yaml:"-" json:"-"`
} }
func init() { func init() {
folio.DocumentRegistry.ResourceTypes.Register([]string{"package", string(PackageTypeApk), string(PackageTypeApt), string(PackageTypeDeb), string(PackageTypeDnf), string(PackageTypeRpm), string(PackageTypePip), string(PackageTypeYum)}, func(u *url.URL) data.Resource { folio.DocumentRegistry.ResourceTypes.Register([]string{"package", string(PackageTypeApk), string(PackageTypeApt), string(PackageTypeDeb), string(PackageTypeDnf), string(PackageTypeRpm), string(PackageTypePip), string(PackageTypeYum)}, func(u *url.URL) data.Resource {
return ConstructNewPackage(u) p := NewPackage()
e := p.SetParsedURI(u)
slog.Info("PackageFactory SetParsedURI()", "error", e)
return p
}) })
} }
@ -86,41 +88,7 @@ func FindSystemPackageType() PackageType {
} }
func NewPackage() *Package { func NewPackage() *Package {
return &Package{ return &Package{ PackageType: SystemPackageType }
Common: NewCommon(PackageTypeName, true),
PackageType: SystemPackageType,
}
}
func ConstructNewPackage(uri *url.URL) (p *Package) {
p = NewPackage()
if uri != nil {
if err := folio.CastParsedURI(uri).ConstructResource(p); err != nil {
panic(err)
}
}
return
}
func (p *Package) Init(u data.URIParser) error {
if u == nil {
u = folio.URI(p.URI()).Parse()
}
uri := u.URL()
p.Name = filepath.Join(uri.Hostname(), uri.Path)
p.Version = uri.Query().Get("version")
if p.Version == "" {
p.Version = "latest"
}
indicatedPackageType := PackageType(uri.Query().Get("type"))
if indicatedPackageType.Validate() != nil {
p.PackageType = SystemPackageType
}
p.CreateCommand, p.ReadCommand, p.UpdateCommand, p.DeleteCommand = p.PackageType.NewCRUD()
return p.SetParsedURI(u)
} }
func (p *Package) SetResourceMapper(resources data.ResourceMapper) { func (p *Package) SetResourceMapper(resources data.ResourceMapper) {
@ -129,11 +97,11 @@ func (p *Package) SetResourceMapper(resources data.ResourceMapper) {
func (p *Package) Clone() data.Resource { func (p *Package) Clone() data.Resource {
newp := &Package { newp := &Package {
Common: p.Common.Clone(),
Name: p.Name, Name: p.Name,
Required: p.Required, Required: p.Required,
Version: p.Version, Version: p.Version,
PackageType: p.PackageType, PackageType: p.PackageType,
State: p.State,
} }
newp.CreateCommand, newp.ReadCommand, newp.UpdateCommand, newp.DeleteCommand = newp.PackageType.NewCRUD() newp.CreateCommand, newp.ReadCommand, newp.UpdateCommand, newp.DeleteCommand = newp.PackageType.NewCRUD()
return newp return newp
@ -167,11 +135,11 @@ func (p *Package) Notify(m *machine.EventMessage) {
if triggerErr := p.StateMachine().Trigger("state_read"); triggerErr == nil { if triggerErr := p.StateMachine().Trigger("state_read"); triggerErr == nil {
return return
} else { } else {
p.Common.State = "absent" p.State = "absent"
panic(triggerErr) panic(triggerErr)
} }
} else { } else {
p.Common.State = "absent" p.State = "absent"
if ! errors.Is(readErr, ErrResourceStateAbsent) { if ! errors.Is(readErr, ErrResourceStateAbsent) {
panic(readErr) panic(readErr)
} }
@ -196,17 +164,17 @@ func (p *Package) Notify(m *machine.EventMessage) {
if triggerErr := p.StateMachine().Trigger("deleted"); triggerErr == nil { if triggerErr := p.StateMachine().Trigger("deleted"); triggerErr == nil {
return return
} else { } else {
p.Common.State = "present" p.State = "present"
panic(triggerErr) panic(triggerErr)
} }
} else { } else {
p.Common.State = "present" p.State = "present"
panic(deleteErr) panic(deleteErr)
} }
case "absent": case "absent":
p.Common.State = "absent" p.State = "absent"
case "present", "created", "updated", "read": case "present", "created", "updated", "read":
p.Common.State = "present" p.State = "present"
} }
case machine.EXITSTATEEVENT: case machine.EXITSTATEEVENT:
} }
@ -224,25 +192,36 @@ func (p *Package) URI() string {
} }
func (p *Package) SetParsedURI(uri data.URIParser) (err error) { func (p *Package) SetURI(uri string) error {
u := uri.URL() resourceUri, e := url.Parse(uri)
if u.Scheme == "package" { if e == nil {
p.Name = filepath.Join(u.Hostname(), u.Path) e = p.SetParsedURI(resourceUri)
p.Version = u.Query().Get("version") }
return e
}
func (p *Package) SetParsedURI(uri *url.URL) (err error) {
if uri.Scheme == "package" {
p.Name = filepath.Join(uri.Hostname(), uri.Path)
p.Version = uri.Query().Get("version")
if p.Version == "" { if p.Version == "" {
p.Version = "latest" p.Version = "latest"
} }
indicatedPackageType := PackageType(u.Query().Get("type")) indicatedPackageType := PackageType(uri.Query().Get("type"))
if indicatedPackageType.Validate() != nil { if indicatedPackageType.Validate() != nil {
p.PackageType = SystemPackageType p.PackageType = SystemPackageType
} }
} else { } else {
err = fmt.Errorf("%w: %s is not a package resource ", ErrInvalidResourceURI, u.String()) err = fmt.Errorf("%w: %s is not a package resource ", ErrInvalidResourceURI, uri.String())
} }
p.CreateCommand, p.ReadCommand, p.UpdateCommand, p.DeleteCommand = p.PackageType.NewCRUD() p.CreateCommand, p.ReadCommand, p.UpdateCommand, p.DeleteCommand = p.PackageType.NewCRUD()
return return
} }
func (p *Package) UseConfig(config data.ConfigurationValueGetter) {
p.config = config
}
func (p *Package) JSON() ([]byte, error) { func (p *Package) JSON() ([]byte, error) {
return json.Marshal(p) return json.Marshal(p)
} }
@ -352,9 +331,6 @@ func (p *Package) LoadDecl(yamlResourceDeclaration string) error {
func (p *Package) Type() string { return "package" } func (p *Package) Type() string { return "package" }
func (p *Package) Read(ctx context.Context) (resourceYaml []byte, err error) { func (p *Package) Read(ctx context.Context) (resourceYaml []byte, err error) {
if p.Version == "latest" {
p.Version = ""
}
if p.ReadCommand.Exists() { if p.ReadCommand.Exists() {
var out []byte var out []byte
out, err = p.ReadCommand.Execute(p) out, err = p.ReadCommand.Execute(p)
@ -519,10 +495,10 @@ func NewApkReadCommand() *command.Command {
if packageName == p.Name { if packageName == p.Name {
p.Name = packageName p.Name = packageName
p.Version = packageVersion p.Version = packageVersion
p.Common.State = "present" p.State = "present"
} else { } else {
slog.Info("NewApkReadCommand().Extrctor() mismatch", "name", p.Name, "parsed", packageName) slog.Info("NewApkReadCommand().Extrctor() mismatch", "name", p.Name, "parsed", packageName)
p.Common.State = "absent" p.State = "absent"
} }
return nil return nil
} }
@ -578,7 +554,7 @@ func NewApkReadPackagesCommand() *command.Command {
packageName := strings.Join(packageFields[0:numberOfFields - 2], "-") packageName := strings.Join(packageFields[0:numberOfFields - 2], "-")
packageVersion := strings.Join(packageFields[numberOfFields - 2:numberOfFields - 1], "-") packageVersion := strings.Join(packageFields[numberOfFields - 2:numberOfFields - 1], "-")
p.Name = packageName p.Name = packageName
p.Common.State = "present" p.State = "present"
p.Version = packageVersion p.Version = packageVersion
} }
} }
@ -619,16 +595,16 @@ func NewAptReadCommand() *command.Command {
switch key { switch key {
case "Package": case "Package":
if value != p.Name { if value != p.Name {
p.Common.State = "absent" p.State = "absent"
return nil return nil
} }
case "Status": case "Status":
statusFields := strings.SplitN(value, " ", 3) statusFields := strings.SplitN(value, " ", 3)
if len(statusFields) > 1 { if len(statusFields) > 1 {
if statusFields[2] == "installed" { if statusFields[2] == "installed" {
p.Common.State = "present" p.State = "present"
} else { } else {
p.Common.State = "absent" p.State = "absent"
} }
} }
case "Version": case "Version":
@ -695,7 +671,7 @@ func NewAptReadPackagesCommand() *command.Command {
packageName := packageFields[0] packageName := packageFields[0]
packageVersion := installedPackage[1] packageVersion := installedPackage[1]
p.Name = packageName p.Name = packageName
p.Common.State = "present" p.State = "present"
p.Version = packageVersion p.Version = packageVersion
p.PackageType = PackageTypeApt p.PackageType = PackageTypeApt
} }
@ -735,16 +711,16 @@ func NewDebReadCommand() *command.Command {
switch key { switch key {
case "Package": case "Package":
if value != p.Name { if value != p.Name {
p.Common.State = "absent" p.State = "absent"
return nil return nil
} }
case "Status": case "Status":
statusFields := strings.SplitN(value, " ", 3) statusFields := strings.SplitN(value, " ", 3)
if len(statusFields) > 1 { if len(statusFields) > 1 {
if statusFields[2] == "installed" { if statusFields[2] == "installed" {
p.Common.State = "present" p.State = "present"
} else { } else {
p.Common.State = "absent" p.State = "absent"
} }
} }
case "Version": case "Version":
@ -811,7 +787,7 @@ func NewDebReadPackagesCommand() *command.Command {
p.Version = packageVersionFields[0] p.Version = packageVersionFields[0]
} }
p.Name = packageName p.Name = packageName
p.Common.State = "present" p.State = "present"
p.PackageType = PackageTypeDeb p.PackageType = PackageTypeDeb
lineIndex++ lineIndex++
} }
@ -857,7 +833,7 @@ func NewDnfReadCommand() *command.Command {
packageName := strings.TrimSpace(strings.Join(packageNameField[0:lenName - 1], ".")) packageName := strings.TrimSpace(strings.Join(packageNameField[0:lenName - 1], "."))
if packageName == p.Name { if packageName == p.Name {
p.Common.State = "present" p.State = "present"
packageVersionField := strings.Split(fields[1], ":") packageVersionField := strings.Split(fields[1], ":")
if len(packageVersionField) > 1 { if len(packageVersionField) > 1 {
//packageEpoch := strings.TrimSpace(packageVersionField[0]) //packageEpoch := strings.TrimSpace(packageVersionField[0])
@ -870,7 +846,7 @@ func NewDnfReadCommand() *command.Command {
slog.Info("DnfReadCommaond.Extract()", "package", packageName, "package", p) slog.Info("DnfReadCommaond.Extract()", "package", packageName, "package", p)
} }
} }
p.Common.State = "absent" p.State = "absent"
slog.Info("Extract()", "package", p) slog.Info("Extract()", "package", p)
return nil return nil
} }
@ -931,7 +907,7 @@ func NewDnfReadPackagesCommand() *command.Command {
p.Version = packageVersionFields[0] p.Version = packageVersionFields[0]
} }
p.Name = packageName p.Name = packageName
p.Common.State = "present" p.State = "present"
p.PackageType = PackageTypeDnf p.PackageType = PackageTypeDnf
lineIndex++ lineIndex++
} }
@ -973,13 +949,13 @@ func NewRpmReadCommand() *command.Command {
packageVersion := strings.Join(packageFields[numberOfFields - 2:numberOfFields - 1], "-") packageVersion := strings.Join(packageFields[numberOfFields - 2:numberOfFields - 1], "-")
slog.Info("Package[RPM].Extract()", "name", packageName, "version", packageVersion, "package", p) slog.Info("Package[RPM].Extract()", "name", packageName, "version", packageVersion, "package", p)
if packageName == p.Name { if packageName == p.Name {
p.Common.State = "present" p.State = "present"
p.Version = packageVersion p.Version = packageVersion
return nil return nil
} }
} }
} }
p.Common.State = "absent" p.State = "absent"
slog.Info("Extract()", "package", p) slog.Info("Extract()", "package", p)
return nil return nil
} }
@ -1037,13 +1013,13 @@ func NewPipReadCommand() *command.Command {
packageName := packageFields[0] packageName := packageFields[0]
packageVersion := packageFields[1] packageVersion := packageFields[1]
if packageName == p.Name { if packageName == p.Name {
p.Common.State = "present" p.State = "present"
p.Version = packageVersion p.Version = packageVersion
return nil return nil
} }
} }
} }
p.Common.State = "absent" p.State = "absent"
slog.Info("Extract()", "package", p) slog.Info("Extract()", "package", p)
return nil return nil
} }
@ -1107,7 +1083,7 @@ func NewYumReadCommand() *command.Command {
//packageArch := strings.TrimSpace(packageNameField[1]) //packageArch := strings.TrimSpace(packageNameField[1])
if packageName == p.Name { if packageName == p.Name {
p.Common.State = "present" p.State = "present"
packageVersionField := strings.Split(fields[1], ":") packageVersionField := strings.Split(fields[1], ":")
//packageEpoch := strings.TrimSpace(packageVersionField[0]) //packageEpoch := strings.TrimSpace(packageVersionField[0])
packageVersion := strings.TrimSpace(packageVersionField[1]) packageVersion := strings.TrimSpace(packageVersionField[1])
@ -1115,7 +1091,7 @@ func NewYumReadCommand() *command.Command {
return nil return nil
} }
} }
p.Common.State = "absent" p.State = "absent"
slog.Info("Extract()", "package", p) slog.Info("Extract()", "package", p)
return nil return nil
} }

View File

@ -4,12 +4,19 @@ package resource
import ( import (
"context" "context"
_ "encoding/json"
"fmt" "fmt"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
_ "gopkg.in/yaml.v3"
_ "io"
"log/slog" "log/slog"
_ "net/http"
_ "net/http/httptest"
_ "net/url"
_ "os"
_ "strings"
"testing" "testing"
"decl/internal/command" "decl/internal/command"
"decl/internal/folio"
) )
func TestNewPackageResource(t *testing.T) { func TestNewPackageResource(t *testing.T) {
@ -347,8 +354,6 @@ type: %s
assert.NotNil(t, p) assert.NotNil(t, p)
loadErr := p.LoadDecl(decl) loadErr := p.LoadDecl(decl)
assert.Nil(t, loadErr) assert.Nil(t, loadErr)
assert.Nil(t, p.Init(nil))
p.ReadCommand = SystemPackageType.NewReadCommand() p.ReadCommand = SystemPackageType.NewReadCommand()
/* /*
p.ReadCommand.Executor = func(value any) ([]byte, error) { p.ReadCommand.Executor = func(value any) ([]byte, error) {
@ -358,7 +363,6 @@ type: %s
p.ResolveId(ctx) p.ResolveId(ctx)
yaml, readErr := p.Read(ctx) yaml, readErr := p.Read(ctx)
assert.ErrorIs(t, readErr, ErrResourceStateAbsent) assert.ErrorIs(t, readErr, ErrResourceStateAbsent)
slog.Info("TestReadPackageError", "package", p, "common", p.Common, "yaml", yaml)
assert.YAMLEq(t, expected, string(yaml)) assert.YAMLEq(t, expected, string(yaml))
slog.Info("read()", "yaml", yaml) slog.Info("read()", "yaml", yaml)
assert.Equal(t, "", p.Version) assert.Equal(t, "", p.Version)
@ -371,9 +375,7 @@ func TestCreatePackage(t *testing.T) {
func TestPackageSetURI(t *testing.T) { func TestPackageSetURI(t *testing.T) {
p := NewPackage() p := NewPackage()
assert.NotNil(t, p) assert.NotNil(t, p)
e := p.SetURI("package://" + "12345_key?type=apk")
uri := folio.URI("package://" + "12345_key?type=apk").Parse()
e := p.Init(uri)
assert.Nil(t, e) assert.Nil(t, e)
assert.Equal(t, "package", p.Type()) assert.Equal(t, "package", p.Type())
assert.Equal(t, "12345_key", p.Name) assert.Equal(t, "12345_key", p.Name)

View File

@ -44,16 +44,11 @@ var ErrPKIInvalidEncodingType error = errors.New("Invalid EncodingType")
var ErrPKIFailedDecodingPemBlock error = errors.New("Failed decoding pem block") var ErrPKIFailedDecodingPemBlock error = errors.New("Failed decoding pem block")
func init() { func init() {
ResourceTypes.Register([]string{"pki"}, func(u *url.URL) (data.Resource) { ResourceTypes.Register([]string{"pki"}, func(u *url.URL) data.Resource {
k := NewPKI() k := NewPKI()
if u != nil { ref := folio.ResourceReference(filepath.Join(u.Hostname(), u.Path))
if err := folio.CastParsedURI(u).ConstructResource(k); err != nil { if len(ref) > 0 {
panic(err) k.PrivateKeyRef = ref
}
ref := folio.ResourceReference(filepath.Join(u.Hostname(), u.Path))
if len(ref) > 0 {
k.PrivateKeyRef = ref
}
} }
return k return k
}) })
@ -88,6 +83,8 @@ type PKI struct {
Bits int `json:"bits" yaml:"bits"` Bits int `json:"bits" yaml:"bits"`
EncodingType EncodingType `json:"type" yaml:"type"` EncodingType EncodingType `json:"type" yaml:"type"`
//State string `json:"state,omitempty" yaml:"state,omitempty"`
config data.ConfigurationValueGetter
Resources data.ResourceMapper `json:"-" yaml:"-"` Resources data.ResourceMapper `json:"-" yaml:"-"`
} }
@ -105,6 +102,7 @@ func (k *PKI) Clone() data.Resource {
return &PKI { return &PKI {
Common: k.Common.Clone(), Common: k.Common.Clone(),
EncodingType: k.EncodingType, EncodingType: k.EncodingType,
//State: k.State,
} }
} }
@ -121,16 +119,6 @@ func (k *PKI) Notify(m *machine.EventMessage) {
switch m.On { switch m.On {
case machine.ENTERSTATEEVENT: case machine.ENTERSTATEEVENT:
switch m.Dest { switch m.Dest {
case "start_stat":
if statErr := k.ReadStat(); statErr == nil {
if triggerErr := k.StateMachine().Trigger("exists"); triggerErr == nil {
return
}
} else {
if triggerErr := k.StateMachine().Trigger("notexists"); triggerErr == nil {
return
}
}
case "start_read": case "start_read":
if _,readErr := k.Read(ctx); readErr == nil { if _,readErr := k.Read(ctx); readErr == nil {
if triggerErr := k.StateMachine().Trigger("state_read"); triggerErr == nil { if triggerErr := k.StateMachine().Trigger("state_read"); triggerErr == nil {
@ -187,6 +175,22 @@ func (k *PKI) URI() string {
return fmt.Sprintf("pki://%s", filepath.Join(u.Hostname(), u.RequestURI())) return fmt.Sprintf("pki://%s", filepath.Join(u.Hostname(), u.RequestURI()))
} }
func (k *PKI) SetURI(uri string) error {
resourceUri, e := url.Parse(uri)
if e == nil {
if resourceUri.Scheme == "pki" {
k.PrivateKeyRef = folio.ResourceReference(fmt.Sprintf("pki://%s", filepath.Join(resourceUri.Hostname(), resourceUri.Path)))
} else {
e = fmt.Errorf("%w: %s is not a cert", ErrInvalidResourceURI, uri)
}
}
return e
}
func (k *PKI) UseConfig(config data.ConfigurationValueGetter) {
k.config = config
}
func (k *PKI) Validate() error { func (k *PKI) Validate() error {
return fmt.Errorf("failed") return fmt.Errorf("failed")
} }
@ -224,27 +228,6 @@ func (k *PKI) LoadDecl(yamlResourceDeclaration string) error {
return k.LoadString(yamlResourceDeclaration, codec.FormatYaml) return k.LoadString(yamlResourceDeclaration, codec.FormatYaml)
} }
func (k *PKI) ReadStat() (err error) {
var resourcesErr []string
if ! k.PrivateKeyRef.Exists() {
resourcesErr = append(resourcesErr, string(k.PrivateKeyRef))
}
if ! k.PublicKeyRef.Exists() {
resourcesErr = append(resourcesErr, string(k.PublicKeyRef))
}
if ! k.CertificateRef.Exists() {
resourcesErr = append(resourcesErr, string(k.CertificateRef))
}
if len(resourcesErr) > 0 {
err = fmt.Errorf("PKI resources missing: %s", strings.Join(resourcesErr, ","))
// k.State = "absent"
}
return
}
func (k *PKI) ResolveId(ctx context.Context) string { func (k *PKI) ResolveId(ctx context.Context) string {
return string(k.PrivateKeyRef) return string(k.PrivateKeyRef)
} }
@ -480,6 +463,62 @@ func (k *PKI) Update(ctx context.Context) (err error) {
func (k *PKI) Type() string { return "pki" } func (k *PKI) Type() string { return "pki" }
/*
func (k *PKI) UnmarshalValue(value *PKIContent) error {
d.Type = value.Type
d.Transition = value.Transition
d.Config = value.Config
newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", value.Type))
if resourceErr != nil {
slog.Info("Declaration.UnmarshalValue", "value", value, "error", resourceErr)
return resourceErr
}
d.Attributes = newResource
return nil
}
func (k *PKI) UnmarshalYAML(value *yaml.Node) error {
t := &DeclarationType{}
if unmarshalResourceTypeErr := value.Decode(t); unmarshalResourceTypeErr != nil {
return unmarshalResourceTypeErr
}
if err := d.UnmarshalValue(t); err != nil {
return err
}
resourceAttrs := struct {
Attributes yaml.Node `json:"attributes"`
}{}
if unmarshalAttributesErr := value.Decode(&resourceAttrs); unmarshalAttributesErr != nil {
return unmarshalAttributesErr
}
if unmarshalResourceErr := resourceAttrs.Attributes.Decode(d.Attributes); unmarshalResourceErr != nil {
return unmarshalResourceErr
}
return nil
}
func (d *Declaration) UnmarshalJSON(data []byte) error {
t := &DeclarationType{}
if unmarshalResourceTypeErr := json.Unmarshal(data, t); unmarshalResourceTypeErr != nil {
return unmarshalResourceTypeErr
}
if err := d.UnmarshalValue(t); err != nil {
return err
}
resourceAttrs := struct {
Attributes Resource `json:"attributes"`
}{Attributes: d.Attributes}
if unmarshalAttributesErr := json.Unmarshal(data, &resourceAttrs); unmarshalAttributesErr != nil {
return unmarshalAttributesErr
}
return nil
}
*/
func (t *EncodingType) UnmarshalValue(value string) error { func (t *EncodingType) UnmarshalValue(value string) error {
switch value { switch value {
case string(EncodingTypePem): case string(EncodingTypePem):

View File

@ -4,12 +4,12 @@ package resource
import ( import (
"context" "context"
_ "encoding/json" _ "encoding/json"
"fmt" "fmt"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
_ "gopkg.in/yaml.v3" _ "gopkg.in/yaml.v3"
"io" "io"
_ "log" _ "log"
_ "os" _ "os"
"decl/internal/transport" "decl/internal/transport"
"decl/internal/ext" "decl/internal/ext"
@ -19,6 +19,7 @@ _ "os"
"strings" "strings"
"testing" "testing"
"path/filepath" "path/filepath"
"net/url"
) )
type TestResourceMapper func(key string) (data.Declaration, bool) type TestResourceMapper func(key string) (data.Declaration, bool)
@ -35,9 +36,6 @@ func (rm TestResourceMapper) Has(key string) (ok bool) {
func (rm TestResourceMapper) Set(key string, value data.Declaration) { func (rm TestResourceMapper) Set(key string, value data.Declaration) {
} }
func (rm TestResourceMapper) Delete(key string) {
}
type StringContentReadWriter func() (any, error) type StringContentReadWriter func() (any, error)
func (s StringContentReadWriter) ContentWriterStream() (*transport.Writer, error) { func (s StringContentReadWriter) ContentWriterStream() (*transport.Writer, error) {
@ -58,7 +56,7 @@ func (s StringContentReadWriter) LoadString(docData string, f codec.Format) (err
func (s StringContentReadWriter) LoadDecl(yamlResourceDeclaration string) error { return nil } func (s StringContentReadWriter) LoadDecl(yamlResourceDeclaration string) error { return nil }
func (s StringContentReadWriter) ResolveId(ctx context.Context) (string) { return "" } func (s StringContentReadWriter) ResolveId(ctx context.Context) (string) { return "" }
func (s StringContentReadWriter) SetURI(uri string) (error) { return nil } func (s StringContentReadWriter) SetURI(uri string) (error) { return nil }
func (s StringContentReadWriter) SetParsedURI(uri data.URIParser) (error) { return nil } func (s StringContentReadWriter) SetParsedURI(uri *url.URL) (error) { return nil }
func (s StringContentReadWriter) URI() (string) { return "" } func (s StringContentReadWriter) URI() (string) { return "" }
func (s StringContentReadWriter) Validate() (error) { return nil } func (s StringContentReadWriter) Validate() (error) { return nil }
func (s StringContentReadWriter) ResourceType() data.TypeName { return "" } func (s StringContentReadWriter) ResourceType() data.TypeName { return "" }

View File

@ -20,6 +20,67 @@ var (
) )
type ResourceReference string type ResourceReference string
/*
type ResourceSelector func(r *Declaration) bool
type Resource interface {
Type() string
StateMachine() machine.Stater
URI() string
SetURI(string) error
UseConfig(config ConfigurationValueGetter)
ResolveId(context.Context) string
ResourceLoader
StateTransformer
ResourceReader
ResourceValidator
Clone() Resource
SetResourceMapper(resources ResourceMapper)
}
type ContentReader interface {
ContentReaderStream() (*transport.Reader, error)
}
type ContentWriter interface {
ContentWriterStream() (*transport.Writer, error)
}
type ContentReadWriter interface {
ContentReader
ContentWriter
}
type ResourceValidator interface {
Validate() error
}
type ResourceCreator interface {
Create(context.Context) error
}
type ResourceReader interface {
Read(context.Context) ([]byte, error)
}
type ResourceUpdater interface {
Update(context.Context) error
}
type ResourceDeleter interface {
Delete(context.Context) error
}
type ResourceDecoder struct {
}
type ResourceCrudder struct {
ResourceCreator
ResourceReader
ResourceUpdater
ResourceDeleter
}
*/
func NewResource(uri string) data.Resource { func NewResource(uri string) data.Resource {
r, e := ResourceTypes.New(uri) r, e := ResourceTypes.New(uri)
@ -29,23 +90,56 @@ func NewResource(uri string) data.Resource {
return nil return nil
} }
func ResourceConstructor(res data.Resource, uri data.URIParser) (err error) { /*
if uri != nil {
return uri.ConstructResource(res) // Return a Content ReadWriter for the resource referred to.
} else { func (r ResourceReference) Lookup(look data.ResourceMapper) data.ContentReadWriter {
if ri, ok := res.(data.ResourceInitializer); ok { slog.Info("ResourceReference.Lookup()", "resourcereference", r, "resourcemapper", look)
return ri.Init(uri) if look != nil {
if v,ok := look.Get(string(r)); ok {
return v.(data.ContentReadWriter)
} }
} }
return return r
} }
// Common resource states func (r ResourceReference) Dereference(look data.ResourceMapper) data.Resource {
func ResourceMachine(sub machine.Subscriber) machine.Stater { slog.Info("ResourceReference.Dereference()", "resourcereference", r, "resourcemapper", look)
stater := machine.New("unknown") if look != nil {
stater.AddStates("unkonwn", "inconsistent", "absent", "start_create", "present", "start_delete", "start_read", "start_update", "start_stat", "created") if v,ok := look.Get(string(r)); ok {
return v.(*Declaration).Attributes
}
}
return nil
}
stater.AddTransition("create", machine.States("unknown", "inconsistent", "absent"), "start_create") func (r ResourceReference) Parse() *url.URL {
u, e := url.Parse(string(r))
if e == nil {
return u
}
return nil
}
func (r ResourceReference) Exists() bool {
return transport.ExistsURI(string(r))
}
func (r ResourceReference) ContentReaderStream() (*transport.Reader, error) {
return transport.NewReaderURI(string(r))
}
func (r ResourceReference) ContentWriterStream() (*transport.Writer, error) {
return transport.NewWriterURI(string(r))
}
*/
func StorageMachine(sub machine.Subscriber) machine.Stater {
// start_destroy -> absent -> start_create -> present -> start_destroy
stater := machine.New("unknown")
stater.AddStates("unkonwn", "absent", "start_create", "present", "start_delete", "start_read", "start_update", "start_stat")
stater.AddTransition("create", machine.States("unknown", "absent"), "start_create")
if e := stater.AddSubscription("create", sub); e != nil { if e := stater.AddSubscription("create", sub); e != nil {
return nil return nil
} }
@ -54,8 +148,7 @@ func ResourceMachine(sub machine.Subscriber) machine.Stater {
if e := stater.AddSubscription("created", sub); e != nil { if e := stater.AddSubscription("created", sub); e != nil {
return nil return nil
} }
stater.AddTransition("exists", machine.States("unknown", "absent", "start_stat"), "present")
stater.AddTransition("exists", machine.States("unknown", "inconsistent", "absent", "start_stat"), "present")
if e := stater.AddSubscription("exists", sub); e != nil { if e := stater.AddSubscription("exists", sub); e != nil {
return nil return nil
} }
@ -65,24 +158,6 @@ func ResourceMachine(sub machine.Subscriber) machine.Stater {
return nil return nil
} }
stater.AddTransition("update-failed", machine.States("start_update"), "inconsistent")
if e := stater.AddSubscription("update-failed", sub); e != nil {
return nil
}
stater.AddTransition("create-failed", machine.States("start_create"), "inconsistent")
if e := stater.AddSubscription("create-failed", sub); e != nil {
return nil
}
return stater
}
func StorageMachine(sub machine.Subscriber) machine.Stater {
// start_destroy -> absent -> start_create -> present -> start_destroy
stater := ResourceMachine(sub)
stater.AddTransition("read", machine.States("*"), "start_read") stater.AddTransition("read", machine.States("*"), "start_read")
if e := stater.AddSubscription("read", sub); e != nil { if e := stater.AddSubscription("read", sub); e != nil {
return nil return nil
@ -117,12 +192,24 @@ func StorageMachine(sub machine.Subscriber) machine.Stater {
func ProcessMachine(sub machine.Subscriber) machine.Stater { func ProcessMachine(sub machine.Subscriber) machine.Stater {
// "enum": [ "created", "restarting", "running", "paused", "exited", "dead" ] // "enum": [ "created", "restarting", "running", "paused", "exited", "dead" ]
stater := machine.New("unknown")
stater := ResourceMachine(sub) stater.AddStates("unkonwn", "absent", "start_create", "present", "created", "restarting", "running", "paused", "exited", "dead", "start_delete", "start_read", "start_update")
stater.AddTransition("create", machine.States("unknown", "absent"), "start_create")
stater.AddStates("restarting", "running", "paused", "exited", "dead") if e := stater.AddSubscription("create", sub); e != nil {
return nil
}
stater.AddTransition("created", machine.States("start_create"), "present")
if e := stater.AddSubscription("created", sub); e != nil {
return nil
}
stater.AddTransition("exists", machine.States("unknown", "absent"), "present")
if e := stater.AddSubscription("exists", sub); e != nil {
return nil
}
stater.AddTransition("notexists", machine.States("*"), "absent")
if e := stater.AddSubscription("notexists", sub); e != nil {
return nil
}
stater.AddTransition("read", machine.States("*"), "start_read") stater.AddTransition("read", machine.States("*"), "start_read")
if e := stater.AddSubscription("read", sub); e != nil { if e := stater.AddSubscription("read", sub); e != nil {
return nil return nil

Some files were not shown because too many files have changed in this diff Show More