Compare commits

..

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

139 changed files with 1563 additions and 8029 deletions

View File

@ -15,12 +15,6 @@ jx-cli:
test: jx-cli test: jx-cli
go test -coverprofile=artifacts/coverage.profile ./... go test -coverprofile=artifacts/coverage.profile ./...
go tool cover -html=artifacts/coverage.profile -o artifacts/code-coverage.html go tool cover -html=artifacts/coverage.profile -o artifacts/code-coverage.html
alpine-deps:
apk add make
apk add golangci-lint
apk add git
apk add luajit luajit-dev
apk add gpg gpg-agent
fedora-deps: fedora-deps:
./jx apply build/rpm.jx.yaml ./jx apply build/rpm.jx.yaml
spectool -g -R build/jx.spec spectool -g -R build/jx.spec
@ -38,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,134 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"context"
"encoding/json"
"github.com/ProtonMail/go-crypto/openpgp"
"io"
"strings"
"strconv"
"gopkg.in/yaml.v3"
"decl/internal/data"
"decl/internal/codec"
"fmt"
"log/slog"
)
type OpenPGPKeyRing struct {
Keyring string `json:"keyring,omitempty" yaml:"keyring,omitempty"`
entities openpgp.EntityList
}
func NewOpenPGPKeyRing() *OpenPGPKeyRing {
o := &OpenPGPKeyRing{}
return o
}
func (o *OpenPGPKeyRing) URI() string {
return fmt.Sprintf("%s://%s", o.Type(), "")
}
func (o *OpenPGPKeyRing) SetURI(uri string) error {
return nil
}
func (o *OpenPGPKeyRing) SetParsedURI(uri data.URIParser) error {
return nil
}
func (o *OpenPGPKeyRing) Read(ctx context.Context) (yamlData []byte, err error) {
pemReader := io.NopCloser(strings.NewReader(o.Keyring))
o.entities, err = openpgp.ReadArmoredKeyRing(pemReader)
return
}
func (o *OpenPGPKeyRing) Load(r io.Reader) (err error) {
err = codec.NewYAMLDecoder(r).Decode(o)
if err == nil {
_, err = o.Read(context.Background())
}
return err
}
func (o *OpenPGPKeyRing) LoadYAML(yamlData string) (err error) {
err = codec.NewYAMLStringDecoder(yamlData).Decode(o)
slog.Info("OpenPGP.LoadYAML()", "keyring", len(o.Keyring), "err", err)
if err == nil {
_, err = o.Read(context.Background())
}
return err
}
func (o *OpenPGPKeyRing) UnmarshalJSON(data []byte) error {
if unmarshalErr := json.Unmarshal(data, o); unmarshalErr != nil {
return unmarshalErr
}
//o.NewReadConfigCommand()
return nil
}
func (o *OpenPGPKeyRing) UnmarshalYAML(value *yaml.Node) error {
type decodeOpenPGP OpenPGPKeyRing
if unmarshalErr := value.Decode((*decodeOpenPGP)(o)); unmarshalErr != nil {
return unmarshalErr
}
return nil
}
func (o *OpenPGPKeyRing) Clone() data.Configuration {
jsonGeneric, _ := json.Marshal(o)
clone := NewOpenPGPKeyRing()
if unmarshalErr := json.Unmarshal(jsonGeneric, &clone); unmarshalErr != nil {
panic(unmarshalErr)
}
return clone
}
func (o *OpenPGPKeyRing) Type() string {
return "openpgp"
}
func (o *OpenPGPKeyRing) 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 *OpenPGPKeyRing) GetValue(name string) (result any, err error) {
index, field, err := o.GetEntityIndex(name)
if len(o.entities) > index && err == nil {
switch field {
case "PublicKey":
return o.entities[index].PrimaryKey, err
case "PrivateKey":
return o.entities[index].PrimaryKey, err
}
}
err = data.ErrUnknownConfigurationKey
return
}
// Expected key: 0.PrivateKey
func (o *OpenPGPKeyRing) Has(key string) (ok bool) {
index, field, err := o.GetEntityIndex(key)
if len(o.entities) > index && err == nil {
switch field {
case "PublicKey":
ok = o.entities[index].PrimaryKey != nil
case "PrivateKey":
ok = o.entities[index].PrimaryKey != nil
}
}
return
}

View File

@ -1,131 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"github.com/stretchr/testify/assert"
"testing"
"fmt"
)
var (
privateKey = `
-----BEGIN PGP PRIVATE KEY BLOCK-----
xcMGBGhI/VUBCADDVSm3mKY5JsncMMJFV0zELMrmip7dkK3vvMvVWVmMHiC4akDH
WPxUdWNQPjE2e5HGF9Ebg0c7gu634VG470MnTzFdPV6f5zA+yJfdKrLq7gpl9QGW
jFLIeK/l4xc+MIpOE1rD9WqYUUw2IYY8YANYq4yB36rq41VuZps/adI9Go5IhfcU
3SVb7o7pa/gWE0FVu9ze31j2agC8FIKHgB++7bmYgbAQz5Qi1qgtG0Kn25QUacJ6
Akm2+h4w3SQCR6HLRV2BO29x9mFBszf2KQ7DW2VNiGyUuQQ3m8v2ZidG/11ff6U6
ad5tvr/8sYr5jOnKEJyDP9v9yQ04cU94GmsPABEBAAH+CQMIljE6AMIbuXNgXPhS
/aEINY2LCOvNUhTUGcepN5zlRJSqGmHCZJ4sI5TWvOzNM4ZCdjQsYYbZhXz5i+SW
R+YeoJKrI/c3jCsazgCUaBqjdHvTi/rHXT77SEQ2c1wBfXmYUbWPpyKeWu31nSnj
3vZCLtwoyWtCuR2lWbHtYu6hJu+wTm6chGxiBdCKEOKXCx9ZIiVKYvZE93tSITDX
R47rVUpMIt46m0tOr4CbsLjpsbAo6izviqFCMQblHr8kk31IF6yhAnwIfcGr0y3j
zzlEY5ntyUqBD6Gwth1wAboWSD4nupq7wRh/TJXes++udR2rPR05lg1HYVbmBvSt
03VGk5WQGhFjixU1LxKir7KMJOnDyMxGShTrx/GIhPpG0srWHLJhQtQ+yP0PrlVk
ho7JrhBNUbf9uCjSPSVCclgk1JrYNEDcwtitBnwR7QU2bkRQU3VhYjiesRcmTeSg
PQttdZoB8aCNfiXlLXb2GnacI49XbH+W4B0HgwqZ4dYSuri37BOm9Gvt9hoZGgsE
fdPt//Oox1N0tkwN+j3aLaOkmJSLlzarVlV3A+j3mkY336WalCRd6HFe3RrEgkVH
53M2dAdbhNlZAlKOwpsiUGwDFX4AiuWJuqXUpoVt4KTRuoYdVg9B0aXW67WM9eai
T9oyur9hZnRy7QANhzuU6m3FBy2EOWHn3c87axK+o48mGDxDYm9PlhIXGbZ7Vb3g
diCE2SkiPerZ0Cx0yO3egUy8BIWHdNWdyDYtcGiup8A7WOyF8ftUCynQkdldtYWx
5HFlcpiV3o/5C5lkUMMHF72fNOyWwz3PCpLO1uOn+T+jtylkrEY8061SlBF91HA/
jaLF6U136VTS1hIj9fjzBhk5a6/43Bk41hhgD0JrVFRFM+S9JIdmwQO1FN41QL1i
xKvQOWOE2s1bzS9UZXN0VXNlcjEgKFRlc3RVc2VyMSkgPHRlc3R1c2VyQHJvc3Nr
ZWVuLmhvdXNlPsLAigQTAQgAPgUCaEj9VQkQ3weSNhbTZdcWIQQAzpwPAGodxV8p
f5vfB5I2FtNl1wIbAwIeAQIZAQILBwIVCAMWAAIDJwcCAABiiwf/cIcIHy5KE2CP
kNE3trmV5exT8ltLeLW1EqCNWolcPspjQ1fsLqmLI2A6G9jMiv0wBZEgqmYQSEWx
i0SbcM0MJBf2phrnpR0kgV9y3cSv7KdlFPs7/zQRD9S5VDSnIbsaXQT2Iyh3Wziz
6CqhX3Qro0saVkyHigsL5w7bj/j5bI1IHGn8TMfnFcHu+wGdRYuQgQ/Q6+5zbC6L
8sK0orM8lYeXk9KW3LvBJX+aGEDU9at5rGqq8PebIZRkIsFYK3070Qg3mZymnx8D
FKIePKLbDSPWktB6QoYviTXfjy3v1pbI+cfqtLB3Puf9uM0LzdFsfXzxOP+8/D6/
glIQMYyO4cfDBgRoSP1VAQgAn2oaIthkcTnzqieGTfE3vuGyXjkHc06HwIrHwlO9
+qvjLxCrQLmO9r81nREVqZ+1wAZrZHyCPF8smvIhyrhBghE8tGKGdeRiwYEh4S55
TdlP+AZK1Ixr1I2VqrlttoHxQdavGXUhsKYPIa7KWP2/p5wBnWsKvoXxOmEUE5hu
bZAa3LcX8YJQZys3s5i9Wt2q0x1n9kkZS3gFjSOLAAq8j/+aZQ+vvWfWF0yng5V5
4GugcwfNMuxdoNHD3bV9tHVgBseIq6tiNksZQAb1jGa3ZtlKa+sixw9s06W39RZC
hVMPY9Jay9XzKtP4/c2yibE4egrTpRdOLAHqBFfUmd+hrwARAQAB/gkDCAAuxoQ+
wVtrYFWspVOMEjwr+2KBmNGJhv6lmsR7C8oauG3W2tz5EUbNz40k+hR+Plft5CuD
s5OwMsKJIRcnFOqTqGf9KhF74yDAzOem0cmxR+XKzhBhgcnj2fGoOMQqN4XnAVFG
B39p4JK+9IkkHCDefHdXZ6EOpjpmaPL41EmO/l02WOhgW9x69waSLpNlDK1YI7gH
72Zhr5BACkv3QWizzU3DP//XQaFyzpjKI01q6f+IXonFkaOiPJXP8Ym4ZAA5FXMF
xZl4V0qpsPyvy1PXx7O6NWG0CqV0LpJwsTf2HFXwnnEniZGB3MZqCq1ORoKHsIQe
Q27iFhqSM0iYHPL/iRt/TRwYgW3NZwpUh/OtiSMRy32BeQ2SMKocHPrqQsZvPYgF
KdZVvpu3n/n8Lj8Wtx+89vz28kd5HG6M01HmE8PDdRp4lryH/pPJb0I/W4TRzQgv
ZrWxP8BZPvLiyOxv+74lvV0gr+0zar8jU9RvhsbN/Nt/PU0dl4794K38Xo/vppAQ
GaGFjlQ3he3Vnb5wNA3hUIaBlOGihd28t6Jf3T+oqmfhYtZ95G7Q/8zvCOVadfUf
5j4xb3LCQYfXNTwDgbGzivpAkje33nX22r38uJg+yeGb8BskMzZWeZMztkw8ia44
F94D9dxtSa++6VQ97uKxzTza37876YDr4I6LVKu+JVIj4pp8FLS1ebuZC1HngJCB
RO2Ipx9zLIV9Pf4AqH0JW93WomTBnc927EdIeA7EZlybdif4kiRF4hONUIjMcGbt
PBbQbpDlc+ZWJcz7UtJTn9TyUwE0B7oMogV4sGM89jrtlH+BqOwLM1QvpuDTmn3b
eXZn+lZpHm174kN/VMaztxkvuxsmZRemMiHs7k1mAD7umDphep+h0aCkWj5G9miW
r0ypWrjjoGDFOp53AZuJ1sLAdgQYAQgAKgUCaEj9VQkQ3weSNhbTZdcWIQQAzpwP
AGodxV8pf5vfB5I2FtNl1wIbDAAAXFYH/0B/uqlgqV0PPmI1hCW4Wg9/IcBWU6mJ
qR+G+e7uGKNMSAHV+sHIq7ab6TibuAA0GB9aV2xiLV95YFWfp+yle4JzSTuimZCC
iXu55Ouwac0HtTSXXgdwpJMtnZz8m3tlLppdePGlg+rT/mW3z7mxGRfEcj0eEDHq
Ar9EkI0hNG39X+BPhhZZvPxUeCQ8h8cWyOvxutWGbhX/4kpkAbh5I1CnhCtOl0iN
19rWt33Wp9/KtaE81NASsTaMU5dJT86iN0OMYnunCvSqPgUcAJUvf+ipiiDY2tRZ
/3ZBzDBasRk1lSkkQxQHmr606hnjSWXQDyWy8AFQtkOpa95ilFAX6tA=
=BAfD
-----END PGP PRIVATE KEY BLOCK-----
`
publicKey = `
-----BEGIN PGP PUBLIC KEY BLOCK-----
xsBNBGhI/VUBCADDVSm3mKY5JsncMMJFV0zELMrmip7dkK3vvMvVWVmMHiC4akDH
WPxUdWNQPjE2e5HGF9Ebg0c7gu634VG470MnTzFdPV6f5zA+yJfdKrLq7gpl9QGW
jFLIeK/l4xc+MIpOE1rD9WqYUUw2IYY8YANYq4yB36rq41VuZps/adI9Go5IhfcU
3SVb7o7pa/gWE0FVu9ze31j2agC8FIKHgB++7bmYgbAQz5Qi1qgtG0Kn25QUacJ6
Akm2+h4w3SQCR6HLRV2BO29x9mFBszf2KQ7DW2VNiGyUuQQ3m8v2ZidG/11ff6U6
ad5tvr/8sYr5jOnKEJyDP9v9yQ04cU94GmsPABEBAAHNL1Rlc3RVc2VyMSAoVGVz
dFVzZXIxKSA8dGVzdHVzZXJAcm9zc2tlZW4uaG91c2U+wsCKBBMBCAA+BQJoSP1V
CRDfB5I2FtNl1xYhBADOnA8Aah3FXyl/m98HkjYW02XXAhsDAh4BAhkBAgsHAhUI
AxYAAgMnBwIAAGKLB/9whwgfLkoTYI+Q0Te2uZXl7FPyW0t4tbUSoI1aiVw+ymND
V+wuqYsjYDob2MyK/TAFkSCqZhBIRbGLRJtwzQwkF/amGuelHSSBX3LdxK/sp2UU
+zv/NBEP1LlUNKchuxpdBPYjKHdbOLPoKqFfdCujSxpWTIeKCwvnDtuP+PlsjUgc
afxMx+cVwe77AZ1Fi5CBD9Dr7nNsLovywrSiszyVh5eT0pbcu8Elf5oYQNT1q3ms
aqrw95shlGQiwVgrfTvRCDeZnKafHwMUoh48otsNI9aS0HpChi+JNd+PLe/Wlsj5
x+q0sHc+5/24zQvN0Wx9fPE4/7z8Pr+CUhAxjI7hzsBNBGhI/VUBCACfahoi2GRx
OfOqJ4ZN8Te+4bJeOQdzTofAisfCU736q+MvEKtAuY72vzWdERWpn7XABmtkfII8
Xyya8iHKuEGCETy0YoZ15GLBgSHhLnlN2U/4BkrUjGvUjZWquW22gfFB1q8ZdSGw
pg8hrspY/b+nnAGdawq+hfE6YRQTmG5tkBrctxfxglBnKzezmL1a3arTHWf2SRlL
eAWNI4sACryP/5plD6+9Z9YXTKeDlXnga6BzB80y7F2g0cPdtX20dWAGx4irq2I2
SxlABvWMZrdm2Upr6yLHD2zTpbf1FkKFUw9j0lrL1fMq0/j9zbKJsTh6CtOlF04s
AeoEV9SZ36GvABEBAAHCwHYEGAEIACoFAmhI/VUJEN8HkjYW02XXFiEEAM6cDwBq
HcVfKX+b3weSNhbTZdcCGwwAAFxWB/9Af7qpYKldDz5iNYQluFoPfyHAVlOpiakf
hvnu7hijTEgB1frByKu2m+k4m7gANBgfWldsYi1feWBVn6fspXuCc0k7opmQgol7
ueTrsGnNB7U0l14HcKSTLZ2c/Jt7ZS6aXXjxpYPq0/5lt8+5sRkXxHI9HhAx6gK/
RJCNITRt/V/gT4YWWbz8VHgkPIfHFsjr8brVhm4V/+JKZAG4eSNQp4QrTpdIjdfa
1rd91qffyrWhPNTQErE2jFOXSU/OojdDjGJ7pwr0qj4FHACVL3/oqYog2NrUWf92
QcwwWrEZNZUpJEMUB5q+tOoZ40ll0A8lsvABULZDqWveYpRQF+rQ
=jBvZ
-----END PGP PUBLIC KEY BLOCK-----
`
)
func TestNewOpenPGPKeyRingConfig(t *testing.T) {
p := NewOpenPGPKeyRing()
assert.NotNil(t, p)
}
func TestNewOpenPGPKeyRingConfigYAML(t *testing.T) {
p := NewOpenPGPKeyRing()
assert.NotNil(t, p)
config := fmt.Sprintf(`
keyring: |-
%s
%s
`, publicKey, privateKey)
yamlErr := p.LoadYAML(config)
assert.Nil(t, yamlErr)
/*
crt, err := p.GetValue("catemplate")
assert.Nil(t, err)
assert.Equal(t, []string{"RKH"}, crt.(*x509.Certificate).Subject.Organization)
*/
}

View File

@ -1,135 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"context"
"encoding/json"
"github.com/ProtonMail/go-crypto/openpgp"
"io"
"strings"
"strconv"
"gopkg.in/yaml.v3"
"decl/internal/data"
"decl/internal/codec"
"fmt"
"log/slog"
)
type OpenPGPSignature struct {
KeyRing string `json:"keyring,omitempty" yaml:"keyring,omitempty"`
Signature string `json:"signature,omitempty" yaml:"signature,omitempty"`
entities openpgp.EntityList
}
func NewOpenPGPSignature() *OpenPGPSignature {
o := &OpenPGPSignature{}
return o
}
func (o *OpenPGPSignature) URI() string {
return fmt.Sprintf("%s://%s", o.Type(), "")
}
func (o *OpenPGPSignature) SetURI(uri string) error {
return nil
}
func (o *OpenPGPSignature) SetParsedURI(uri data.URIParser) error {
return nil
}
func (o *OpenPGPSignature) Read(ctx context.Context) (yamlData []byte, err error) {
pemReader := io.NopCloser(strings.NewReader(o.KeyRing))
o.entities, err = openpgp.ReadArmoredKeyRing(pemReader)
return
}
func (o *OpenPGPSignature) Load(r io.Reader) (err error) {
err = codec.NewYAMLDecoder(r).Decode(o)
if err == nil {
_, err = o.Read(context.Background())
}
return err
}
func (o *OpenPGPSignature) LoadYAML(yamlData string) (err error) {
err = codec.NewYAMLStringDecoder(yamlData).Decode(o)
slog.Info("OpenPGP.LoadYAML()", "keyring", len(o.KeyRing), "err", err)
if err == nil {
_, err = o.Read(context.Background())
}
return err
}
func (o *OpenPGPSignature) UnmarshalJSON(data []byte) error {
if unmarshalErr := json.Unmarshal(data, o); unmarshalErr != nil {
return unmarshalErr
}
//o.NewReadConfigCommand()
return nil
}
func (o *OpenPGPSignature) UnmarshalYAML(value *yaml.Node) error {
type decodeOpenPGP OpenPGPSignature
if unmarshalErr := value.Decode((*decodeOpenPGP)(o)); unmarshalErr != nil {
return unmarshalErr
}
return nil
}
func (o *OpenPGPSignature) Clone() data.Configuration {
jsonGeneric, _ := json.Marshal(o)
clone := NewOpenPGPSignature()
if unmarshalErr := json.Unmarshal(jsonGeneric, &clone); unmarshalErr != nil {
panic(unmarshalErr)
}
return clone
}
func (o *OpenPGPSignature) Type() string {
return "openpgp"
}
func (o *OpenPGPSignature) 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 *OpenPGPSignature) GetValue(name string) (result any, err error) {
index, field, err := o.GetEntityIndex(name)
if len(o.entities) > index && err == nil {
switch field {
case "PublicKey":
return o.entities[index].PrimaryKey, err
case "PrivateKey":
return o.entities[index].PrimaryKey, err
}
}
err = data.ErrUnknownConfigurationKey
return
}
// Expected key: 0.PrivateKey
func (o *OpenPGPSignature) Has(key string) (ok bool) {
index, field, err := o.GetEntityIndex(key)
if len(o.entities) > index && err == nil {
switch field {
case "PublicKey":
ok = o.entities[index].PrimaryKey != nil
case "PrivateKey":
ok = o.entities[index].PrimaryKey != nil
}
}
return
}

View File

@ -1,33 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"context"
"github.com/stretchr/testify/assert"
"testing"
"fmt"
)
func TestNewOpenPGPSignatureConfig(t *testing.T) {
p := NewOpenPGPSignature()
assert.NotNil(t, p)
}
func TestNewOpenPGPSignatureConfigYAML(t *testing.T) {
p := NewOpenPGPSignature()
assert.NotNil(t, p)
config := fmt.Sprintf(`
keyring: |-
%s
%s
`, publicKey, privateKey)
yamlErr := p.LoadYAML(config)
assert.Nil(t, yamlErr)
p.Read(context.Background())
}

View File

@ -1,9 +0,0 @@
{
"$id": "openpgp.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "openpgp",
"type": "object",
"required": [ "path", "filetype" ],
"properties": {
}
}

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,25 +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",
}
s["keyringpath"] = "/etc/jx/pgp/keyring.asc"
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(), "")
} }
@ -63,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

@ -8,7 +8,6 @@ import (
var ( var (
ErrConfigUndefinedName = errors.New("Config block is missing a defined name") ErrConfigUndefinedName = errors.New("Config block is missing a defined name")
ErrConfigUndefined = errors.New("Config block is missing")
) )

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

@ -1,13 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package data
import (
)
var (
)
type Originator interface {
GetContentReadWriter() ContentReadWriter
}

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
@ -38,6 +33,7 @@ type Resource interface {
Crudder Crudder
Validator Validator
Clone() Resource Clone() Resource
SetResourceMapper(ResourceMapper)
} }
type Declaration interface { type Declaration interface {
@ -105,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" }
@ -149,16 +147,12 @@ func (j *JxFile) Extract(resourceSource data.Resource, filter data.ElementSelect
} }
uri := resourceSource.URI() uri := resourceSource.URI()
documentIndexUri := fmt.Sprintf("%s?index=%d", uri, j.index) if folio.DocumentRegistry.HasDocument(folio.URI(uri)) {
uri = fmt.Sprintf("%s?index=%d", uri, j.index)
doc = folio.DocumentRegistry.NewDocument(folio.URI(documentIndexUri))
if ! folio.DocumentRegistry.HasDocument(folio.URI(uri)) {
folio.DocumentRegistry.SetDocument(folio.URI(uri), doc.(*folio.Document))
doc.(*folio.Document).SetURI(uri)
} }
doc = folio.DocumentRegistry.NewDocument(folio.URI(uri))
err = j.decoder.Decode(doc) err = j.decoder.Decode(doc)
slog.Info("JxFile.Extract()", "uri", uri, "doc", doc, "jxfile", j, "error", err) slog.Info("JxFile.Extract()", "doc", doc, "jxfile", j, "error", err)
j.index++ j.index++
if err != nil { if err != nil {
return return
@ -185,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
@ -193,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 {
@ -73,7 +73,7 @@ func (d *Declaration) SetDocument(newDocument *Document) {
d.document = newDocument d.document = newDocument
d.SetConfig(d.document.config) d.SetConfig(d.document.config)
d.ResourceTypes = d.document.Types() d.ResourceTypes = d.document.Types()
d.Attributes.(ResourceMapSetter).SetResourceMapper(d.document.uris) d.Attributes.SetResourceMapper(d.document.uris)
} }
func (d *Declaration) ResolveId(ctx context.Context) string { func (d *Declaration) ResolveId(ctx context.Context) string {
@ -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)
@ -209,11 +206,6 @@ func (d *Declaration) Apply(stateTransition string) (result error) {
return result return result
} }
result = stater.Trigger("read") result = stater.Trigger("read")
case "restart": // XXX should only work for a process type resource
if result = stater.Trigger("restart"); result != nil {
return result
}
result = stater.Trigger("read")
default: default:
return fmt.Errorf("%w: %s on %s", ErrUnknownStateTransition, stateTransition, d.Attributes.URI()) return fmt.Errorf("%w: %s on %s", ErrUnknownStateTransition, stateTransition, d.Attributes.URI())
case "create", "present": case "create", "present":
@ -224,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 {
@ -254,7 +245,7 @@ func (d *Declaration) SetConfig(configDoc data.Document) {
return return
} }
if d.Config != "" { // XXX if d.Config != "" { // XXX
panic(fmt.Errorf("%w: failed setting config: %s", data.ErrConfigUndefined, d.Config)) panic("failed setting config")
} }
} }
@ -263,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
@ -277,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)
@ -307,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)
@ -317,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
} }
@ -362,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
} }
@ -392,18 +354,24 @@ 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) GetContentReadWriter() data.ContentReadWriter { /*
return d.Resource().(data.ContentReadWriter) 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) {

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 {
@ -37,30 +37,20 @@ type Document struct {
Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"` Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"`
Imports []URI `json:"imports,omitempty" yaml:"imports,omitempty"` Imports []URI `json:"imports,omitempty" yaml:"imports,omitempty"`
Errors []string `json:"error,omitempty" yaml:"error,omitempty"` Errors []string `json:"error,omitempty" yaml:"error,omitempty"`
uris mapper.Store[URI, *Declaration] uris mapper.Store[string, data.Declaration]
ResourceDeclarations []*Declaration `json:"resources,omitempty" yaml:"resources,omitempty"` ResourceDeclarations []*Declaration `json:"resources,omitempty" yaml:"resources,omitempty"`
configNames mapper.Store[string, data.Block] `json:"-" yaml:"-"` configNames mapper.Store[string, data.Block] `json:"-" yaml:"-"`
Configurations []*Block `json:"configurations,omitempty" yaml:"configurations,omitempty"` Configurations []*Block `json:"configurations,omitempty" yaml:"configurations,omitempty"`
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[URI, *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] {
@ -111,24 +88,20 @@ func (d *Document) Filter(filter data.DeclarationSelector) []data.Declaration {
} }
func (d *Document) Has(key string) bool { func (d *Document) Has(key string) bool {
return d.uris.Has(URI(key)) return d.uris.Has(key)
} }
func (d *Document) Get(key string) (any, bool) { func (d *Document) Get(key string) (any, bool) {
return d.uris.Get(URI(key)) return d.uris.Get(key)
} }
func (d *Document) Set(key string, value any) { func (d *Document) Set(key string, value any) {
d.uris.Set(URI(key), value.(*Declaration)) d.uris.Set(key, value.(data.Declaration))
}
func (d *Document) Delete(key string) {
d.uris.Delete(URI(key))
} }
func (d *Document) GetResource(uri string) *Declaration { func (d *Document) GetResource(uri string) *Declaration {
if decl, ok := d.uris[URI(uri)]; ok { if decl, ok := d.uris[uri]; ok {
return decl return decl.(*Declaration)
} }
return nil return nil
} }
@ -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() default schema", "schema", d.Registry.DefaultSchema, "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() convert to 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() validate schema", "error", err)
if err != nil { if err != nil {
return err return err
} }
@ -356,12 +319,8 @@ func (d *Document) MapConfigurationURI(uri string, block data.Block) {
} }
*/ */
func (d *Document) MapResourceURI(uri string, declaration *Declaration) { func (d *Document) MapResourceURI(uri string, declaration data.Declaration) {
d.uris[URI(uri)] = declaration d.uris[uri] = declaration
}
func (d *Document) UnMapResourceURI(uri string) {
d.uris.Delete(URI(uri))
} }
func (d *Document) AddDeclaration(declaration data.Declaration) { func (d *Document) AddDeclaration(declaration data.Declaration) {
@ -371,7 +330,7 @@ func (d *Document) AddDeclaration(declaration data.Declaration) {
d.ResourceDeclarations = append(d.ResourceDeclarations, decl) d.ResourceDeclarations = append(d.ResourceDeclarations, decl)
d.MapResourceURI(uri, decl) d.MapResourceURI(uri, declaration)
decl.SetDocument(d) decl.SetDocument(d)
d.Registry.DeclarationMap[decl] = d d.Registry.DeclarationMap[decl] = d
} }
@ -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
} }
@ -597,80 +543,35 @@ func (d *Document) Diff(with data.Document, output io.Writer) (returnOutput stri
/* /*
func (d *Document) UnmarshalValue(value *DocumentType) error { func (d *Document) UnmarshalValue(value *DocumentType) error {
d.Requires = value.Requires d.Requires = value.Requires
} }
*/ */
func (d *Document) UnmarshalLoadDependencies() (err error) { func (d *Document) UnmarshalYAML(value *yaml.Node) error {
defer func() {
if r := recover(); r != nil {
if err != nil {
err = fmt.Errorf("%s - %w", r, err)
} else {
err = fmt.Errorf("%s", r)
}
}
}()
err = d.loadImports()
d.assignConfigurationsDocument()
d.assignResourcesDocument()
return
}
func (d *Document) UnmarshalYAML(value *yaml.Node) (err 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 {
return unmarshalDocumentErr return unmarshalDocumentErr
} }
*/
if unmarshalResourcesErr := value.Decode((*decodeDocument)(d)); unmarshalResourcesErr != nil { if unmarshalResourcesErr := value.Decode((*decodeDocument)(d)); unmarshalResourcesErr != nil {
return unmarshalResourcesErr return unmarshalResourcesErr
} }
err = d.UnmarshalLoadDependencies() d.assignConfigurationsDocument()
return d.assignResourcesDocument()
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.UnmarshalLoadDependencies() d.assignConfigurationsDocument()
return d.assignResourcesDocument()
return d.loadImports()
} }
func (d *Document) AddError(e error) { func (d *Document) AddError(e error) {
d.Errors = append(d.Errors, e.Error()) d.Errors = append(d.Errors, e.Error())
} }
func (d *Document) GetContentReadWriter() data.ContentReadWriter {
return d.URI
}
func (d *Document) GetContent(w io.Writer) (contentReader io.ReadCloser, err error) {
slog.Info("Document.GetContent()")
var buf strings.Builder
err = codec.FormatYaml.Serialize(d, &buf)
contentReader = io.NopCloser(strings.NewReader(buf.String()))
if w != nil {
copyBuffer := make([]byte, 32 * 1024)
_, writeErr := io.CopyBuffer(w, contentReader, copyBuffer)
if writeErr != nil {
return nil, fmt.Errorf("File.GetContent(): CopyBuffer failed %v %v: %w", w, contentReader, writeErr)
}
return nil, nil
}
return
}
func (d *Document) SetContent(r io.Reader) error {
return d.LoadReader(io.NopCloser(r), codec.FormatYaml)
}

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

@ -8,7 +8,6 @@ _ "gopkg.in/yaml.v3"
"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/transport"
"io" "io"
"net/url" "net/url"
"path/filepath" "path/filepath"
@ -18,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
}) })
} }
@ -49,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"`
@ -70,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 },
@ -90,18 +85,10 @@ func NewMockResource(typename string, stater machine.Stater) (m *MockResource) {
InjectUpdate: func(context.Context) error { return nil }, InjectUpdate: func(context.Context) error { return nil },
InjectDelete: func(context.Context) error { return nil }, InjectDelete: func(context.Context) error { return nil },
InjectUseConfig: func(data.ConfigurationValueGetter) {}, InjectUseConfig: func(data.ConfigurationValueGetter) {},
InjectSetResourceMapper: func(ResourceMapper) {}, InjectSetResourceMapper: func(data.ResourceMapper) {},
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) {},
InjectContentReaderStream: func() (*transport.Reader, error) { return nil, nil },
} }
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,20 +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"
"decl/internal/transport" "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:"-"`
@ -35,11 +33,9 @@ type MockResource struct {
InjectUpdate func(context.Context) error `json:"-" yaml:"-"` InjectUpdate func(context.Context) error `json:"-" yaml:"-"`
InjectDelete func(context.Context) error `json:"-" yaml:"-"` InjectDelete func(context.Context) error `json:"-" yaml:"-"`
InjectStateMachine func() machine.Stater `json:"-" yaml:"-"` InjectStateMachine func() machine.Stater `json:"-" yaml:"-"`
InjectSetResourceMapper func(ResourceMapper) `json:"-" yaml:"-"` InjectSetResourceMapper func(data.ResourceMapper) `json:"-" yaml:"-"`
InjectUseConfig func(data.ConfigurationValueGetter) `json:"-" yaml:"-"` InjectUseConfig func(data.ConfigurationValueGetter) `json:"-" yaml:"-"`
InjectNotify func(*machine.EventMessage) `json:"-" yaml:"-"` InjectNotify func(*machine.EventMessage) `json:"-" yaml:"-"`
InjectContentReaderStream func() (*transport.Reader, error) `json:"-" yaml:"-"`
InjectContentWriterStream func() (*transport.Writer, error) `json:"-" yaml:"-"`
} }
func (m *MockResource) Clone() data.Resource { func (m *MockResource) Clone() data.Resource {
@ -58,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
} }
@ -70,7 +66,7 @@ func (m *MockResource) ResolveId(ctx context.Context) string {
return m.InjectResolveId(ctx) return m.InjectResolveId(ctx)
} }
func (m *MockResource) SetResourceMapper(rm ResourceMapper) { func (m *MockResource) SetResourceMapper(rm data.ResourceMapper) {
m.InjectSetResourceMapper(rm) m.InjectSetResourceMapper(rm)
} }
@ -130,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()
} }
@ -142,14 +134,6 @@ func (m *MockResource) Notify(em *machine.EventMessage) {
m.InjectNotify(em) m.InjectNotify(em)
} }
func (m *MockResource) ContentReaderStream() (*transport.Reader, error) {
return m.InjectContentReaderStream()
}
func (m *MockResource) ContentWriterStream() (*transport.Writer, error) {
return m.InjectContentWriterStream()
}
func (m *MockResource) UnmarshalJSON(data []byte) error { func (m *MockResource) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, m); err != nil { if err := json.Unmarshal(data, m); err != nil {
return err return err

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

@ -1,229 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"encoding/json"
"gopkg.in/yaml.v3"
"decl/internal/mapper"
"decl/internal/data"
"decl/internal/transport"
"net/url"
"log/slog"
"errors"
"fmt"
"io"
)
var (
ErrRefMapperMismatch = errors.New("Ref type does not the provided mapper")
)
type Ref struct {
Uri URI `json:"uri" yaml:"uri"`
RefType ReferenceType `json:"type,omitempty" yaml:"type,omitempty"`
documentMapper mapper.Store[URI, *Document]
resourceMapper mapper.Store[URI, *Declaration]
}
type decodeRef Ref
func NewRef() *Ref {
return &Ref {
}
}
func (r *ReferenceType) UnmarshalValue(value string) error {
switch value {
case string(ReferenceTypeResource), string(ReferenceTypeDocument):
*r = ReferenceType(value)
default:
*r = ReferenceTypeResource
//return ErrInvalidReferenceType
}
return nil
}
func (r *ReferenceType) UnmarshalJSON(jsonData []byte) error {
var s string
if unmarshalReferenceTypeErr := json.Unmarshal(jsonData, &s); unmarshalReferenceTypeErr != nil {
return unmarshalReferenceTypeErr
}
return r.UnmarshalValue(s)
}
func (r *ReferenceType) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
return r.UnmarshalValue(s)
}
func (r *Ref) UnmarshalValue(value *decodeRef) error {
slog.Info("Ref.UnmarshalValue", "decode", value)
r.Uri = value.Uri
switch value.RefType {
case ReferenceTypeResource:
r.RefType = value.RefType
case ReferenceTypeDocument:
r.RefType = value.RefType
r.SetMapper(DocumentRegistry.UriMap)
default:
r.RefType = ReferenceTypeResource
//return ErrInvalidReferenceType
}
slog.Info("Ref.UnmarshalValue", "stored", *r)
return nil
}
func (r *Ref) UnmarshalJSON(jsonData []byte) error {
decodeJsonToRef := &decodeRef{}
if unmarshalReferenceTypeErr := json.Unmarshal(jsonData, decodeJsonToRef); unmarshalReferenceTypeErr != nil {
return unmarshalReferenceTypeErr
}
return r.UnmarshalValue(decodeJsonToRef)
}
func (r *Ref) UnmarshalYAML(value *yaml.Node) error {
decodeYamlToRef := &decodeRef{}
if err := value.Decode(decodeYamlToRef); err != nil {
return err
}
return r.UnmarshalValue(decodeYamlToRef)
}
func (r *Ref) SetMapper(m any) error {
var ok bool
switch r.RefType {
case ReferenceTypeResource:
if r.resourceMapper, ok = m.(mapper.Store[URI, *Declaration]); ! ok {
return fmt.Errorf("%w - %T is not a %s", ErrRefMapperMismatch, m, r.RefType)
}
case ReferenceTypeDocument:
if r.documentMapper, ok = m.(mapper.Store[URI, *Document]); ! ok {
return fmt.Errorf("%w - %T is not a %s", ErrRefMapperMismatch, m, r.RefType)
}
}
return nil
}
func Dereference[T ReferenceTypes](uri URI, look mapper.Map[URI, T]) T {
if uri != "" && look != nil {
if v, ok := look.Get(uri); ok {
slog.Info("Ref::Dereference()", "value", v, "mapper", look)
return v
}
}
return nil
}
func (r *Ref) Lookup(look any) ContentReadWriter {
slog.Info("Ref.Lookup()", "ref", r, "mapper", look)
switch r.RefType {
case ReferenceTypeResource:
if resourceDeclaration := Dereference(r.Uri, look.(mapper.Store[URI, *Declaration])); resourceDeclaration != nil {
return resourceDeclaration.GetContentReadWriter()
}
case ReferenceTypeDocument:
if document := Dereference(r.Uri, look.(mapper.Store[URI, *Document])); document != nil {
return document.GetContentReadWriter()
}
}
return r.Uri
}
func (r *Ref) Dereference(look any) any {
switch r.RefType {
case ReferenceTypeResource:
if look == nil {
return Dereference(r.Uri, r.resourceMapper)
} else {
return Dereference(r.Uri, look.(mapper.Store[URI, *Declaration]))
}
case ReferenceTypeDocument:
if look == nil {
return Dereference(r.Uri, r.documentMapper)
} else {
return Dereference(r.Uri, look.(mapper.Store[URI, *Document]))
}
}
return nil
}
func (r *Ref) DereferenceDefault() any {
switch r.RefType {
case ReferenceTypeResource:
return Dereference(r.Uri, r.resourceMapper)
case ReferenceTypeDocument:
return Dereference(r.Uri, r.documentMapper)
}
return nil
}
func (r *Ref) Parse() data.URIParser {
return r.Uri.Parse()
}
func (r *Ref) Exists() bool {
return r.Uri.Exists()
}
func (r *Ref) ContentReaderStream() (*transport.Reader, error) {
return r.Uri.ContentReaderStream()
}
func (r *Ref) ContentWriterStream() (*transport.Writer, error) {
return r.Uri.ContentWriterStream()
}
func (r *Ref) GetContent(w io.Writer) (contentReader io.ReadCloser, err error) {
target := r.DereferenceDefault()
if targetContent, ok := target.(data.ContentGetSetter); ok {
return targetContent.GetContent(w)
}
return nil, fmt.Errorf("Ref target does not support ContentGetSetter: %s, %s, %#v", r.RefType, r.Uri, target)
}
func (r *Ref) SetContent(contentReader io.Reader) error {
target := r.DereferenceDefault()
if targetContent, ok := target.(data.ContentGetSetter); ok {
return targetContent.SetContent(contentReader)
}
return fmt.Errorf("Ref target does not support ContentGetSetter: %s, %s, %#v", r.RefType, r.Uri, target)
}
func (r *Ref) Reader() (reader io.ReadCloser, err error) {
switch r.RefType {
case ReferenceTypeResource:
return r.ContentReaderStream()
case ReferenceTypeDocument:
reader, err = r.GetContent(nil)
if reader == nil {
return r.ContentReaderStream()
}
return
}
return nil, ErrInvalidReferenceType
}
func (r *Ref) String() string {
return r.Uri.String()
}
func (r *Ref) SetURL(url *url.URL) {
r.Uri.SetURL(url)
}
func (r *Ref) Extension() (string, string) {
return r.Uri.Extension()
}
func (r *Ref) IsEmpty() bool {
return r.Uri.IsEmpty()
}
func (r *Ref) FindIn(s *SearchPath) {
r.Uri.FindIn(s)
}

View File

@ -1,67 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"github.com/stretchr/testify/assert"
"testing"
"fmt"
_ "decl/internal/data"
"decl/internal/mapper"
"decl/internal/codec"
"log/slog"
"strings"
"io"
)
func TestReference(t *testing.T) {
f := NewFooResource()
resourceMapper := mapper.New[URI, *Declaration]()
f.Name = string(TempDir)
f.Size = 10
f.MockResource.InjectURI = func() string { return fmt.Sprintf("%s://%s", "foo", f.Name) }
d := NewDeclaration()
d.Type = "foo"
d.Attributes = f
resourceMapper[URI(d.URI())] = d
slog.Info("TestReference", "declaration", d, "mapper", resourceMapper)
var fooRef *Ref = NewRef()
fooRef.RefType = ReferenceTypeResource
fooRef.Uri = URI(fmt.Sprintf("foo://%s", string(TempDir)))
u := fooRef.Uri.Parse().URL()
assert.Equal(t, "foo", u.Scheme)
assert.True(t, fooRef.Uri.Exists())
fromRef := fooRef.Lookup(resourceMapper)
assert.NotNil(t, fromRef)
}
func TestDocumentRef(t *testing.T) {
docUri := fmt.Sprintf("file://%s/doc.yaml", TempDir)
DocumentRegistry.ResourceTypes = TestResourceTypes
document := `
---
resources:
- type: foo
attributes:
name: "testfoo"
size: 10022
`
TempDir.CreateFile("doc.yaml", document)
d := DocumentRegistry.NewDocument(URI(docUri))
assert.NotNil(t, d)
docReader := io.NopCloser(strings.NewReader(document))
e := d.LoadReader(docReader, codec.FormatYaml)
assert.Nil(t, e)
var docRef *Ref = NewRef()
docRef.RefType = ReferenceTypeDocument
docRef.Uri = URI(docUri)
u := docRef.Uri.Parse().URL()
assert.Equal(t, "file", u.Scheme)
assert.Equal(t, d, docRef.Dereference(DocumentRegistry.UriMap).(*Document))
}

View File

@ -1,133 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"encoding/json"
"gopkg.in/yaml.v3"
"net/url"
"decl/internal/data"
"decl/internal/mapper"
"errors"
)
var (
// ErrInvalidURI error = errors.New("Invalid URI")
ErrInvalidReferenceType error = errors.New("invalid ReferenceType value")
)
type ReferenceTypes interface {
*Document | *Declaration
}
type RefMap[T ReferenceTypes] mapper.Store[string, T]
type ReferenceMapper[T ReferenceTypes] interface {
mapper.Map[URI, T]
}
type ResourceMapper ReferenceMapper[*Declaration]
type DocumentMapper ReferenceMapper[*Document]
type ReferenceType string
type RefType[T ReferenceTypes, TypedReferenceMapper ReferenceMapper[T]] ReferenceType
type ReferenceURI[T ReferenceTypes] URI
type DocumentURI ReferenceURI[*Document]
type ResourceURI ReferenceURI[*Declaration]
const (
ReferenceTypeResource ReferenceType = "resource"
RefTypeResource RefType[*Declaration, mapper.Store[URI, *Declaration]] = RefType[*Declaration, mapper.Store[URI, *Declaration]](ReferenceTypeResource)
ReferenceTypeDocument ReferenceType = "document"
RefTypeDocument RefType[*Document, mapper.Store[URI, *Document]] = RefType[*Document, mapper.Store[URI, *Document]](ReferenceTypeDocument)
)
type CommonReferrer interface {
ReferenceType() ReferenceType
Parse() *url.URL
Exists() bool
data.ContentReadWriter
IsEmpty() bool
}
// interface for a reference
type Referrer[T ReferenceTypes, TypedReferenceMapper ReferenceMapper[T]] interface {
CommonReferrer
Lookup(TypedReferenceMapper) ContentReadWriter
GetContentReadWriter(TypedReferenceMapper) ContentReadWriter
Dereference(TypedReferenceMapper) T
}
/*
func (r ReferenceType) URIType() any {
switch r {
case ReferenceTypeDocument:
return ReferenceURI[*Document]
case ReferenceTypeResource:
return ReferenceURI[*Declaration]
}
}
*/
func (r *RefType[T, TypedReferenceMapper]) UnmarshalValue(value string) error {
switch value {
case string(ReferenceTypeResource), string(ReferenceTypeDocument):
*r = RefType[T, TypedReferenceMapper](value)
return nil
default:
return ErrInvalidReferenceType
}
}
func (r *RefType[T, TypedReferenceMapper]) UnmarshalJSON(data []byte) error {
var s string
if unmarshalReferenceTypeErr := json.Unmarshal(data, &s); unmarshalReferenceTypeErr != nil {
return unmarshalReferenceTypeErr
}
return r.UnmarshalValue(s)
}
func (r *RefType[T, TypedReferenceMapper]) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
return r.UnmarshalValue(s)
}
func (r *RefType[T, TypedReferenceMapper]) Dereference(uri URI, look TypedReferenceMapper) (result T) {
result = nil
if uri != "" {
if v,ok := look.Get(uri); ok {
result = v
}
}
return
}
func RefURI[T ReferenceTypes, RT RefType[T, ReferenceMapper[T]], RU ReferenceURI[T]](uri URI, reftype RT) (refuri RU) {
refuri = RU(uri)
return
}
/*
func (r *ReferenceURI[T]) GetContentReadWriter(mapper TypedReferenceMapper) ContentReadWriter {
if URI(r).String() == "" {
return nil
}
if mapper != nil {
if v,ok := mapper.Get(string(r)); ok {
return v.(data.Originator).GetContentReadWriter()
}
}
return r
}
*/
type ResourceMapSetter interface {
SetResourceMapper(ResourceMapper)
}
func NewResourceMapper() ResourceMapper {
return mapper.New[URI, *Declaration]()
}

View File

@ -60,8 +60,7 @@ func (r *Registry) HasDocument(key URI) bool {
} }
func (r *Registry) GetDocument(key URI) (*Document, bool) { func (r *Registry) GetDocument(key URI) (*Document, bool) {
document, result := r.UriMap.Get(key) return r.UriMap.Get(key)
return document, result
} }
func (r *Registry) SetDocument(key URI, value *Document) { func (r *Registry) SetDocument(key URI, value *Document) {
@ -70,10 +69,10 @@ 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.Set(uri, doc) r.UriMap[uri] = doc
} }
return return
} }
@ -89,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
} }
@ -131,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)
@ -149,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

@ -5,7 +5,6 @@ package folio
import ( import (
"decl/internal/transport" "decl/internal/transport"
"decl/internal/data" "decl/internal/data"
"decl/internal/mapper"
"errors" "errors"
"log/slog" "log/slog"
"net/url" "net/url"
@ -32,33 +31,32 @@ type ContentReadWriter interface {
type ResourceReference URI type ResourceReference URI
// Return a Content ReadWriter for the resource referred to. // Return a Content ReadWriter for the resource referred to.
func (r ResourceReference) Lookup(look mapper.Map[URI, *Declaration]) ContentReadWriter { func (r ResourceReference) Lookup(look data.ResourceMapper) ContentReadWriter {
if string(r) == "" { if string(r) == "" {
return nil return nil
} }
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(URI(r)); ok { if v,ok := look.Get(string(r)); ok {
slog.Info("ResourceReference.Lookup()", "resourcereference", r, "result", v.Resource()) return v.(ContentReadWriter)
return v.Resource().(ContentReadWriter)
} }
} }
return r return r
} }
func (r ResourceReference) Dereference(look mapper.Map[URI, *Declaration]) data.Resource { func (r ResourceReference) Dereference(look data.ResourceMapper) data.Resource {
slog.Info("ResourceReference.Dereference()", "resourcereference", r, "resourcemapper", look) slog.Info("ResourceReference.Dereference()", "resourcereference", r, "resourcemapper", look)
if look != nil { if look != nil {
if v,ok := look.Get(URI(r)); ok { if v,ok := look.Get(string(r)); ok {
slog.Info("ResourceReference.Dereference()", "resourcereference", r, "result", v) slog.Info("ResourceReference.Dereference()", "resourcereference", r, "result", v)
return v.Attributes return v.(*Declaration).Attributes
} }
} }
return nil return nil
} }
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 {
@ -72,11 +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()
}
func (r *ResourceReference) FindIn(s *SearchPath) {
(*URI)(r).FindIn(s)
}

View File

@ -6,18 +6,19 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"testing" "testing"
"fmt" "fmt"
"decl/internal/data"
"decl/internal/mapper" "decl/internal/mapper"
) )
func TestResourceReference(t *testing.T) { func TestResourceReference(t *testing.T) {
f := NewFooResource() f := NewFooResource()
resourceMapper := mapper.New[URI, *Declaration]() resourceMapper := mapper.New[string, data.Declaration]()
f.Name = string(TempDir) f.Name = string(TempDir)
f.Size = 10 f.Size = 10
d := NewDeclaration() d := NewDeclaration()
d.Type = "foo" d.Type = "foo"
d.Attributes = f d.Attributes = f
resourceMapper[URI(d.URI())] = d resourceMapper[d.URI()] = d
var foo ResourceReference = ResourceReference(fmt.Sprintf("foo://%s", string(TempDir))) var foo ResourceReference = ResourceReference(fmt.Sprintf("foo://%s", string(TempDir)))
u := foo.Parse() u := foo.Parse()

View File

@ -7,8 +7,8 @@
"properties": { "properties": {
"name": { "name": {
"type": "string", "type": "string",
"description": "foo name", "description": "foo name",
"minLength": 1 "minLength": 1
}, },
"size": { "size": {
"type": "integer", "type": "integer",

View File

@ -1,17 +0,0 @@
{
"$id": "ref.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ref",
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Type of object referred to",
"enum": [ "resource", "document" ]
},
"uri": {
"type": "string",
"description": "URI of the object referred to"
}
}
}

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,35 +35,18 @@ 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
} }
func (u *URI) FindIn(s *SearchPath) {
if s == nil {
s = NewSearchPath(ConfigKey("system.importpath").GetStringSlice())
}
*u = s.FindURI(*u)
}
func (u URI) Exists() bool { func (u URI) Exists() bool {
return transport.ExistsURI(string(u)) return transport.ExistsURI(string(u))
} }

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)
@ -37,13 +37,3 @@ func TestURINewResource(t *testing.T) {
} }
func TestURIFindIn(t *testing.T) {
searchPath := NewSearchPath(ConfigKey("system.importpath").GetStringSlice())
assert.Nil(t, searchPath.AddPath(string(TempDir)))
assert.Nil(t, TempDir.CreateFile("bar", "testdata"))
var file URI = "file://bar"
file.FindIn(searchPath)
assert.True(t, file.Exists())
assert.Equal(t, string(file), fmt.Sprintf("file://%s/bar", TempDir))
}

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

@ -9,22 +9,18 @@ import (
"path/filepath" "path/filepath"
"decl/internal/data" "decl/internal/data"
"decl/internal/folio" "decl/internal/folio"
_ "decl/internal/mapper"
"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,14 +28,12 @@ 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 folio.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
} }
@ -54,19 +48,15 @@ func (c *Common) ContentType() string {
return c.exttype return c.exttype
} }
func (c *Common) SetResourceMapper(resources folio.ResourceMapper) { func (c *Common) SetResourceMapper(resources data.ResourceMapper) {
c.Resources = resources c.Resources = resources
} }
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,
@ -84,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
} }
@ -126,41 +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 {
slog.Info("Common.AddError()", "errors", c.Errors, "err", err)
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,87 +28,74 @@ _ "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"
"time"
"errors"
) )
const ( const (
ContainerTypeName TypeName = "container" ContainerTypeName TypeName = "container"
) )
var (
ErrContainerWaitTimeOut = errors.New("Container wait timed out waiting for state")
)
type ContainerClient interface { type ContainerClient interface {
ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error)
ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error
ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error
ContainerList(context.Context, container.ListOptions) ([]types.Container, error) ContainerList(context.Context, container.ListOptions) ([]types.Container, error)
ContainerInspect(context.Context, string) (types.ContainerJSON, error) ContainerInspect(context.Context, string) (types.ContainerJSON, error)
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:"-"`
} }
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
}) })
} }
@ -127,27 +114,15 @@ func NewContainer(containerClientApi ContainerClient) *Container {
} }
} }
func (c *Container) Init(u data.URIParser) error { func (c *Container) SetResourceMapper(resources data.ResourceMapper) {
if u == nil { c.Resources = resources
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) Clone() data.Resource { 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,
@ -171,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,
} }
} }
@ -182,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)
} }
@ -191,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
} }
} }
@ -246,26 +221,6 @@ func (c *Container) Notify(m *machine.EventMessage) {
c.Common.State = "present" c.Common.State = "present"
panic(deleteErr) panic(deleteErr)
} }
case "restarting":
if restartErr := c.Restart(ctx); restartErr == nil {
if triggerErr := c.StateMachine().Trigger("restarted"); triggerErr == nil {
return
} else {
_ = c.AddError(triggerErr)
}
} else {
_ = c.AddError(restartErr)
if c.IsResourceInconsistent() {
if triggerErr := c.StateMachine().Trigger("restart-failed"); triggerErr == nil {
panic(restartErr)
} else {
_ = c.AddError(triggerErr)
panic(fmt.Errorf("%w - %w", restartErr, triggerErr))
}
}
_ = c.StateMachine().Trigger("notexists")
panic(restartErr)
}
case "present", "created", "read": case "present", "created", "read":
c.Common.State = "present" c.Common.State = "present"
case "running": case "running":
@ -278,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
} }
@ -329,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)
@ -397,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)
@ -431,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
} }
@ -496,10 +363,10 @@ func (c *Container) Read(ctx context.Context) ([]byte, error) {
func (c *Container) Inspect(ctx context.Context, containerID string) error { func (c *Container) Inspect(ctx context.Context, containerID string) error {
containerJSON, err := c.apiClient.ContainerInspect(ctx, containerID) containerJSON, err := c.apiClient.ContainerInspect(ctx, containerID)
if client.IsErrNotFound(err) || containerJSON.State == nil { if client.IsErrNotFound(err) {
c.Common.State = "absent" c.Common.State = "absent"
} else { } else {
c.Common.State = containerJSON.State.Status c.Common.State = "present"
c.Id = containerJSON.ID c.Id = containerJSON.ID
if c.Name == "" { if c.Name == "" {
if containerJSON.Name[0] == '/' { if containerJSON.Name[0] == '/' {
@ -510,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
@ -517,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
@ -566,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))
} }
@ -604,64 +467,3 @@ func (c *Container) ResolveId(ctx context.Context) string {
} }
return "" return ""
} }
type WaitCondition func(state *types.ContainerState) bool
func (c *Container) wait(ctx context.Context, untilstate WaitCondition) error {
statusCh := make(chan bool)
timeoutCtx, cancel := context.WithTimeout(ctx, 60 * time.Second)
defer cancel()
go func() {
for {
if containerJSON, err := c.apiClient.ContainerInspect(ctx, c.Id); err == nil {
statusCh <- untilstate(containerJSON.State)
return
}
time.Sleep(2 * time.Second)
}
}()
select {
case <-timeoutCtx.Done():
return ErrContainerWaitTimeOut
case <-statusCh:
}
return nil
}
func (c *Container) Restart(ctx context.Context) (err error) {
if err = c.apiClient.ContainerRestart(ctx, c.Id, container.StopOptions{}); err != nil {
return err
}
if err = c.wait(ctx, func(state *types.ContainerState) bool {
return state.Running
}); err != nil {
return err
}
/*
waitTimeout := 60 * time.Second
interval := 2 * time.Second
deadline := time.Now().Add(waitTimeout)
for {
if time.Now().After(deadline) {
panic("")
}
if state.Running {
if state.Health != nil {
fmt.Println("Health status:", state.Health.Status)
if state.Health.Status == "healthy" {
fmt.Println("Container is healthy and running.")
break
}
} else {
fmt.Println("Container is running (no health check defined).")
break
}
}
time.Sleep(interval)
}
*/
return nil
}

View File

@ -93,6 +93,7 @@ type ContainerImage struct {
outputWriter strings.Builder `json:"-" yaml:"-"` outputWriter strings.Builder `json:"-" yaml:"-"`
apiClient ContainerImageClient apiClient ContainerImageClient
Resources data.ResourceMapper `json:"-" yaml:"-"`
contextDocument data.Document `json:"-" yaml:"-"` contextDocument data.Document `json:"-" yaml:"-"`
ConverterTypes data.TypesRegistry[data.Converter] `json:"-" yaml:"-"` ConverterTypes data.TypesRegistry[data.Converter] `json:"-" yaml:"-"`
@ -100,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
}) })
} }
@ -120,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) {
@ -188,8 +175,8 @@ func (c *ContainerImage) RegistryAuth() (string, error) {
} }
} }
func (c *ContainerImage) NormalizePath() error { func (c *ContainerImage) SetResourceMapper(resources data.ResourceMapper) {
return nil c.Resources = resources
} }
func (c *ContainerImage) Clone() data.Resource { func (c *ContainerImage) Clone() data.Resource {
@ -262,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)
} }
@ -784,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" }
@ -810,3 +821,5 @@ func (c *ContainerImage) ResolveId(ctx context.Context) string {
} }
return c.Id return c.Id
} }

View File

@ -214,8 +214,8 @@ attributes:
_, readErr := contextFile.Resource().Read(context.Background()) _, readErr := contextFile.Resource().Read(context.Background())
assert.Nil(t, readErr) assert.Nil(t, readErr)
c.Resources = folio.NewResourceMapper() c.Resources = data.NewResourceMapper()
c.Resources.Set(folio.URI(contextDirUri), contextFile) c.Resources.Set(contextDirUri, contextFile)
d, contextErr := c.ContextDocument() d, contextErr := c.ContextDocument()
assert.Nil(t, contextErr) assert.Nil(t, contextErr)

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,32 +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:"-"`
} }
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
@ -72,30 +71,22 @@ 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 { func (n *ContainerNetwork) SetResourceMapper(resources data.ResourceMapper) {
if u == nil { n.Resources = resources
u = folio.URI(n.URI()).Parse()
}
return n.SetParsedURI(u)
}
func (n *ContainerNetwork) NormalizePath() error {
return nil
} }
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,
} }
} }
@ -157,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,23 +5,20 @@ package resource
import ( import (
"context" "context"
"decl/tests/mocks" "decl/tests/mocks"
"decl/internal/codec" _ "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"
"time"
) )
func TestNewContainerResource(t *testing.T) { func TestNewContainerResource(t *testing.T) {
@ -49,9 +46,6 @@ func TestReadContainer(t *testing.T) {
ID: "123456789abc", ID: "123456789abc",
Name: "test", Name: "test",
Image: "alpine", Image: "alpine",
State: &types.ContainerState{
Status: "running",
},
}}, nil }}, nil
}, },
InjectContainerWait: func(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) { InjectContainerWait: func(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
@ -61,25 +55,21 @@ 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)
assert.NotNil(t, c) assert.NotEqual(t, nil, c)
e := c.LoadDecl(decl) e := c.LoadDecl(decl)
assert.Nil(t, e) assert.Equal(t, nil, e)
assert.Equal(t, "testcontainer", c.Name) assert.Equal(t, "testcontainer", c.Name)
resourceYaml, readContainerErr := c.Read(ctx) resourceYaml, readContainerErr := c.Read(ctx)
assert.Nil(t, readContainerErr) assert.Equal(t, nil, readContainerErr)
assert.Greater(t, len(resourceYaml), 0) assert.Greater(t, len(resourceYaml), 0)
assert.Equal(t, "running", c.State)
} }
func TestCreateDeleteContainer(t *testing.T) { func TestCreateContainer(t *testing.T) {
m := &mocks.MockContainerClient{ 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) { 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 return container.CreateResponse{ID: "abcdef012", Warnings: []string{}}, nil
@ -97,9 +87,6 @@ func TestCreateDeleteContainer(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 := `
@ -120,153 +107,3 @@ func TestCreateDeleteContainer(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)
}
func TestWaitContainer(t *testing.T) {
mockState := &types.ContainerState{
Status: "",
}
m := &mocks.MockContainerClient{
InjectContainerInspect: func(ctx context.Context, containerID string) (types.ContainerJSON, error) {
return types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
ID: "abcdef012",
Name: "testcontainer",
Image: "alpine",
State: mockState,
}}, nil
},
InjectContainerCreate: func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) {
mockState.Status = "created"
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(strings.NewReader("done.")), nil
},
}
c := NewContainer(m)
assert.Nil(t, c.LoadString(`
name: "testcontainer"
image: "alpine"
state: present
`, codec.FormatYaml))
assert.Nil(t, c.Apply())
assert.Equal(t, "testcontainer", c.Name)
assert.Nil(t, c.wait(context.Background(), func(state *types.ContainerState) bool {
return state.Status == "running"
}))
}
func TestRestartContainer(t *testing.T) {
mockState := &types.ContainerState{
Status: "",
}
m := &mocks.MockContainerClient{
InjectContainerInspect: func(ctx context.Context, containerID string) (types.ContainerJSON, error) {
return types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
ID: "abcdef012",
Name: "testcontainer",
Image: "alpine",
State: mockState,
}}, nil
},
InjectContainerCreate: func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) {
mockState.Status = "created"
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
},
InjectContainerRestart: func(ctx context.Context, containerID string, options container.StopOptions) error {
go func() {
time.Sleep(100 * time.Millisecond)
mockState.Status = "running"
mockState.Running = true
}()
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(strings.NewReader("done.")), nil
},
}
c := NewContainer(m)
assert.Nil(t, c.LoadString(`
name: "testcontainer"
image: "alpine"
state: present
`, codec.FormatYaml))
assert.Equal(t, "testcontainer", c.Name)
assert.Nil(t, c.Apply())
c.State = "present" // overwrite the state
c.StateMachine().Trigger("restart")
assert.Equal(t, "running", c.State)
assert.Nil(t, c.Apply())
}

View File

@ -1,290 +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
}
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) 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

@ -34,25 +34,14 @@ type Exec struct {
ReadTemplate *command.Command `yaml:"read,omitempty" json:"read,omitempty"` ReadTemplate *command.Command `yaml:"read,omitempty" json:"read,omitempty"`
UpdateTemplate *command.Command `yaml:"update,omitempty" json:"update,omitempty"` UpdateTemplate *command.Command `yaml:"update,omitempty" json:"update,omitempty"`
DeleteTemplate *command.Command `yaml:"delete,omitempty" json:"delete,omitempty"` DeleteTemplate *command.Command `yaml:"delete,omitempty" json:"delete,omitempty"`
Resources data.ResourceMapper `yaml:"-" json:"-"`
} }
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
}) })
} }
@ -61,9 +50,13 @@ func NewExec() *Exec {
return e return e
} }
func (x *Exec) SetResourceMapper(resources data.ResourceMapper) {
x.Resources = resources
}
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,
@ -83,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) {
@ -35,16 +34,7 @@ func TestExecApplyResourceTransformation(t *testing.T) {
} }
func TestReadExec(t *testing.T) { func TestReadExec(t *testing.T) {
x := NewExec()
decl := `
read:
path: ls
args:
- -al
`
assert.Nil(t, x.LoadDecl(decl))
assert.Equal(t, "ls", x.ReadTemplate.Path)
assert.Equal(t, command.CommandArg("-al"), x.ReadTemplate.Args[0])
} }
func TestReadExecError(t *testing.T) { func TestReadExecError(t *testing.T) {
@ -65,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"
@ -27,7 +28,6 @@ import (
"strings" "strings"
"embed" "embed"
"compress/gzip" "compress/gzip"
"path/filepath"
) )
const ( const (
@ -56,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
}) })
} }
@ -78,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"`
@ -98,8 +109,11 @@ 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:"-"`
} }
type ResourceFileInfo struct { type ResourceFileInfo struct {
@ -109,49 +123,37 @@ 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) 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) func (f *File) SetResourceMapper(resources data.ResourceMapper) {
ext.WriteRune('.') f.Resources = resources
ext.WriteString(f.fileext)
return ext.String()
} }
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,
@ -163,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,
} }
} }
@ -179,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)
}
} }
} }
@ -278,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) {
@ -287,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)
} }
@ -319,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":
@ -361,19 +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() error {
if f.config != nil {
if prefixPath, configErr := f.config.GetValue("prefix"); configErr == nil {
f.Common.Path = filepath.Join(prefixPath.(string), f.Common.Path)
}
}
return f.Common.NormalizePath()
}
/*
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 {
@ -385,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)
@ -398,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)
} }
@ -458,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:
@ -491,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
} }
@ -506,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
} }
@ -518,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
} }
@ -540,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() {
@ -596,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("File.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("File.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)
@ -614,9 +603,9 @@ func (f *File) ReadStat() (err error) {
} }
slog.Info("File.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
} }
@ -624,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 {
@ -666,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,25 +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:"-"`
groupStatus *user.Group `json:"-" yaml:"-"` State string `json:"state,omitempty" yaml:"state,omitempty"`
config data.ConfigurationValueGetter
Resources data.ResourceMapper `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
}) })
} }
@ -80,32 +82,15 @@ func FindSystemGroupType() GroupType {
return GroupTypeAddGroup return GroupTypeAddGroup
} }
func (g *Group) Init(u data.URIParser) error { func (g *Group) SetResourceMapper(resources data.ResourceMapper) {
if u == nil { g.Resources = resources
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) 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()
@ -124,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)
} }
@ -190,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 {
@ -235,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
@ -263,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) {
@ -399,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 != "" {
@ -414,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
@ -423,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 {
@ -480,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
} }
@ -91,39 +96,25 @@ type HTTP struct {
LastModified time.Time `json:"lastmodified,omitempty" yaml:"lastmodified,omitempty"` LastModified time.Time `json:"lastmodified,omitempty" yaml:"lastmodified,omitempty"`
Size int64 `yaml:"size,omitempty" json:"size,omitempty"` Size int64 `yaml:"size,omitempty" json:"size,omitempty"`
SignatureValue string `yaml:"signature,omitempty" json:"signature,omitempty"` SignatureValue string `yaml:"signature,omitempty" json:"signature,omitempty"`
Resources data.ResourceMapper `yaml:"-" json:"-"`
reader *transport.Reader `yaml:"-" json:"-"` reader *transport.Reader `yaml:"-" json:"-"`
writer *transport.ReadWriter `yaml:"-" json:"-"` writer *transport.ReadWriter `yaml:"-" json:"-"`
} }
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 { func (h *HTTP) SetResourceMapper(resources data.ResourceMapper) {
switch scheme { h.Resources = resources
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) Open() (err error) { func (h *HTTP) Open() (err error) {
@ -228,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
} }
@ -268,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()
@ -336,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
@ -351,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
@ -362,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()
} }

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