add support for configuration documents
Some checks failed
Declarative Tests / build-ubuntu-focal (push) Waiting to run
Lint / golangci-lint (push) Failing after 15s
Declarative Tests / test (push) Failing after 5s
Declarative Tests / build-fedora (push) Has been cancelled

This commit is contained in:
Matthew Rich 2024-07-01 14:54:18 -07:00
parent 52c083a3d9
commit 1460d2285b
57 changed files with 2698 additions and 432 deletions

View File

@ -30,3 +30,5 @@ run:
clean: clean:
go clean -modcache go clean -modcache
rm jx rm jx
lint:
golangci-lint run --verbose ./...

View File

@ -14,12 +14,29 @@ import (
"fmt" "fmt"
) )
var TempDir string
func TestMain(m *testing.M) {
var err error
TempDir, err = os.MkdirTemp("", "testcli")
if err != nil || TempDir == "" {
slog.Error("TestMain()", "error", err)
}
rc := m.Run()
os.RemoveAll(TempDir)
os.Exit(rc)
}
func TestCli(t *testing.T) { func TestCli(t *testing.T) {
if _, e := os.Stat("./jx"); errors.Is(e, os.ErrNotExist) { if _, e := os.Stat("./jx"); errors.Is(e, os.ErrNotExist) {
t.Skip("cli not built") t.Skip("cli not built")
} }
yaml, cliErr := exec.Command("./jx", "import", "--resource", "file://COPYRIGHT").Output() yaml, cliErr := exec.Command("./jx", "import", "--resource", "file://COPYRIGHT").Output()
slog.Info("TestCli", "err", cliErr) if cliErr != nil {
slog.Info("Debug CLI error", "error", cliErr, "stderr", cliErr.(*exec.ExitError).Stderr)
}
assert.Nil(t, cliErr) assert.Nil(t, cliErr)
assert.NotEqual(t, "", string(yaml)) assert.NotEqual(t, "", string(yaml))
assert.Greater(t, len(yaml), 0) assert.Greater(t, len(yaml), 0)
@ -47,7 +64,38 @@ resources:
defer ts.Close() defer ts.Close()
yaml, cliErr := exec.Command("./jx", "import", "--resource", ts.URL).Output() yaml, cliErr := exec.Command("./jx", "import", "--resource", ts.URL).Output()
if cliErr != nil {
slog.Info("Debug CLI error", "error", cliErr, "stderr", cliErr.(*exec.ExitError).Stderr)
}
assert.Nil(t, cliErr) assert.Nil(t, cliErr)
assert.NotEqual(t, "", string(yaml)) assert.NotEqual(t, "", string(yaml))
assert.Greater(t, len(yaml), 0) assert.Greater(t, len(yaml), 0)
} }
func TestCliConfigSource(t *testing.T) {
if _, e := os.Stat("./jx"); errors.Is(e, os.ErrNotExist) {
t.Skip("cli not built")
}
configYaml := `
configurations:
- name: myhttpconnection
values:
http_user: foo
http_pass: bar
`
configPath := fmt.Sprintf("%s/testconfig.yaml", TempDir)
f, err := os.Create(configPath)
assert.Nil(t, err)
defer f.Close()
_, writeErr := f.Write([]byte(configYaml))
assert.Nil(t, writeErr)
yaml, cliErr := exec.Command("./jx", "import", "--config", configPath, "--resource", "file://COPYRIGHT").Output()
if cliErr != nil {
slog.Info("Debug CLI error", "error", cliErr, "stderr", cliErr.(*exec.ExitError).Stderr)
}
assert.Nil(t, cliErr)
slog.Info("TestConfigSource", "yaml", yaml)
}

View File

@ -4,18 +4,19 @@ package main
import ( import (
"context" "context"
"io" "decl/internal/codec"
"os" "decl/internal/config"
"flag"
"log/slog"
_ "errors"
"fmt"
_ "gopkg.in/yaml.v3"
"decl/internal/resource" "decl/internal/resource"
"decl/internal/source" "decl/internal/source"
"decl/internal/target" "decl/internal/target"
"decl/internal/codec" _ "errors"
"flag"
"fmt"
_ "gopkg.in/yaml.v3"
"io"
"log/slog"
"net/url" "net/url"
"os"
) )
const ( const (
@ -25,8 +26,8 @@ const (
var ( var (
version string version string
commit string commit string
date string date string
) )
var GlobalOformat *string var GlobalOformat *string
@ -38,6 +39,9 @@ var ImportResource *string
var ApplyDelete *bool var ApplyDelete *bool
var ConfigPath string
var ConfigDoc *config.Document = config.NewDocument()
var ctx context.Context = context.Background() var ctx context.Context = context.Background()
@ -45,21 +49,25 @@ type RunCommand func(cmd *flag.FlagSet, output io.Writer) error
type SubCommand struct { type SubCommand struct {
Name string Name string
Run RunCommand Run RunCommand
} }
var jxSubCommands = []SubCommand { var jxSubCommands = []SubCommand{
{ {
Name: "diff", Name: "diff",
Run: DiffSubCommand, Run: DiffSubCommand,
}, },
{ {
Name: "apply", Name: "apply",
Run: ApplySubCommand, Run: ApplySubCommand,
}, },
{ {
Name: "import", Name: "import",
Run: ImportSubCommand, Run: ImportSubCommand,
},
{
Name: "config",
Run: ConfigSubCommand,
}, },
} }
@ -74,19 +82,36 @@ func LoggerConfig() {
var programLevel = new(slog.LevelVar) var programLevel = new(slog.LevelVar)
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel})) logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel}))
slog.SetDefault(logger) slog.SetDefault(logger)
if debugLogging,ok := os.LookupEnv("JX_DEBUG"); ok && debugLogging != "" { if debugLogging, ok := os.LookupEnv("JX_DEBUG"); ok && debugLogging != "" {
programLevel.Set(slog.LevelDebug) programLevel.Set(slog.LevelDebug)
} else { } else {
programLevel.Set(slog.LevelError) programLevel.Set(slog.LevelError)
} }
} }
func LoadConfigURI(uri string) []*config.Document {
slog.Info("LoadConfigURI()", "uri", uri)
if uri != "" {
cs, err := config.ConfigSourceTypes.New(uri)
if err != nil {
slog.Error("Failed loading config document from source", "error", err)
}
extractConfigs, extractErr := cs.Extract(nil)
if extractErr != nil {
slog.Error("Failed loading configs from source", "error", extractErr)
}
return extractConfigs
}
return []*config.Document{config.NewDocument()}
}
func LoadSourceURI(uri string) []*resource.Document { func LoadSourceURI(uri string) []*resource.Document {
slog.Info("loading ", "uri", uri) slog.Info("loading ", "uri", uri)
if uri != "" { if uri != "" {
ds, err := source.SourceTypes.New(uri) ds, err := source.SourceTypes.New(uri)
if err != nil { if err != nil {
slog.Error("Failed loading document from source", "error", err) slog.Error("Failed loading document from source", "error", err)
return nil
} }
extractDocuments, extractErr := ds.ExtractResources(nil) extractDocuments, extractErr := ds.ExtractResources(nil)
if extractErr != nil { if extractErr != nil {
@ -94,7 +119,32 @@ func LoadSourceURI(uri string) []*resource.Document {
} }
return extractDocuments return extractDocuments
} }
return []*resource.Document{ resource.NewDocument() } return []*resource.Document{resource.NewDocument()}
}
func ConfigSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
e := cmd.Parse(os.Args[2:])
if e != nil { // returns ErrHelp
return e
}
slog.Info("ConfigSubCommand", "configdoc", ConfigDoc)
for _, configSource := range cmd.Args() {
for _, argConfigDoc := range LoadConfigURI(configSource) {
ConfigDoc.Append(argConfigDoc)
}
}
outputTarget, err := config.ConfigTargetTypes.New(GlobalOutput)
if err != nil {
slog.Error("Failed opening target", "error", err)
}
defer outputTarget.Close()
if outputErr := outputTarget.EmitResources([]*config.Document{ConfigDoc}, nil); outputErr != nil {
return outputErr
}
return nil
} }
func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) { func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
@ -105,23 +155,29 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
return e return e
} }
if ConfigPath != "" {
for _, argConfigDoc := range LoadConfigURI(ConfigPath) {
ConfigDoc.Append(argConfigDoc)
}
}
merged := resource.NewDocument() merged := resource.NewDocument()
documents := make([]*resource.Document, 0, 100) documents := make([]*resource.Document, 0, 100)
for _,source := range cmd.Args() { for _, source := range cmd.Args() {
loaded := LoadSourceURI(source) loaded := LoadSourceURI(source)
if loaded != nil { if loaded != nil {
documents = append(documents, loaded...) documents = append(documents, loaded...)
} }
} }
/* /*
switch *GlobalOformat { switch *GlobalOformat {
case FormatYaml: case FormatYaml:
encoder = resource.NewYAMLEncoder(output) encoder = resource.NewYAMLEncoder(output)
case FormatJson: case FormatJson:
encoder = resource.NewJSONEncoder(output) encoder = resource.NewJSONEncoder(output)
} }
*/ */
slog.Info("main.ImportResource", "args", os.Args, "output", GlobalOutput) slog.Info("main.ImportResource", "args", os.Args, "output", GlobalOutput)
outputTarget, err := target.TargetTypes.New(GlobalOutput) outputTarget, err := target.TargetTypes.New(GlobalOutput)
@ -134,7 +190,7 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
documents = append(documents, resource.NewDocument()) documents = append(documents, resource.NewDocument())
} }
for _,d := range documents { for _, d := range documents {
if d != nil { if d != nil {
if *ImportResource != "" { if *ImportResource != "" {
@ -153,7 +209,7 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
if *GlobalQuiet { if *GlobalQuiet {
for _, dr := range d.Resources() { for _, dr := range d.Resources() {
if _,e := output.Write([]byte(dr.Resource().URI())); e != nil { if _, e := output.Write([]byte(dr.Resource().URI())); e != nil {
return e return e
} }
} }
@ -182,17 +238,27 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
if e := cmd.Parse(os.Args[2:]); e != nil { if e := cmd.Parse(os.Args[2:]); e != nil {
return e return e
} }
if ConfigPath != "" {
for _, argConfigDoc := range LoadConfigURI(ConfigPath) {
ConfigDoc.Append(argConfigDoc)
}
}
var encoder codec.Encoder var encoder codec.Encoder
documents := make([]*resource.Document, 0, 100) documents := make([]*resource.Document, 0, 100)
for _,source := range cmd.Args() { for _, source := range cmd.Args() {
loaded := LoadSourceURI(source) loaded := LoadSourceURI(source)
if loaded != nil { if loaded != nil {
documents = append(documents, loaded...) documents = append(documents, loaded...)
} }
} }
slog.Info("main.Apply()", "documents", documents) slog.Info("main.Apply()", "documents", documents, "configdoc", ConfigDoc)
for _,d := range documents { for _, d := range documents {
d.SetConfig(ConfigDoc)
slog.Info("main.Apply()", "doc", d) slog.Info("main.Apply()", "doc", d)
var overrideState string = "" var overrideState string = ""
if *ApplyDelete { if *ApplyDelete {
@ -212,7 +278,7 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
} }
if *GlobalQuiet { if *GlobalQuiet {
for _, dr := range d.Resources() { for _, dr := range d.Resources() {
if _,e := output.Write([]byte(dr.Resource().URI())); e != nil { if _, e := output.Write([]byte(dr.Resource().URI())); e != nil {
return e return e
} }
} }
@ -242,7 +308,7 @@ func DiffSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
for i, doc := range rightDocuments { for i, doc := range rightDocuments {
if doc != nil { if doc != nil {
leftDocuments = append(leftDocuments, doc.Clone()) leftDocuments = append(leftDocuments, doc.Clone())
for _,resourceDeclaration := range leftDocuments[i].Resources() { for _, resourceDeclaration := range leftDocuments[i].Resources() {
if _, e := resourceDeclaration.Resource().Read(ctx); e != nil { if _, e := resourceDeclaration.Resource().Read(ctx); e != nil {
slog.Info("jx diff ", "err", e) slog.Info("jx diff ", "err", e)
//return e //return e
@ -262,20 +328,20 @@ func DiffSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
break break
} }
if index >= len(rightDocuments) { if index >= len(rightDocuments) {
if _,e := leftDocuments[index].Diff(resource.NewDocument(), output); e != nil { if _, e := leftDocuments[index].Diff(resource.NewDocument(), output); e != nil {
return e return e
} }
index++ index++
continue continue
} }
if index >= len(leftDocuments) { if index >= len(leftDocuments) {
if _,e := resource.NewDocument().Diff(rightDocuments[index], output); e != nil { if _, e := resource.NewDocument().Diff(rightDocuments[index], output); e != nil {
return e return e
} }
index++ index++
continue continue
} }
if _,e := leftDocuments[index].Diff(rightDocuments[index], output); e != nil { if _, e := leftDocuments[index].Diff(rightDocuments[index], output); e != nil {
return e return e
} }
index++ index++
@ -292,8 +358,21 @@ func main() {
os.Exit(1) os.Exit(1)
} }
for _,subCmd := range jxSubCommands { DefaultConfigurations, configErr := config.Configurations()
if configErr != nil {
slog.Error("Failed loading default configuration", "error", configErr)
}
for _, argConfigDoc := range DefaultConfigurations {
ConfigDoc.Append(argConfigDoc)
}
for _, subCmd := range jxSubCommands {
cmdFlagSet := flag.NewFlagSet(subCmd.Name, flag.ExitOnError) cmdFlagSet := flag.NewFlagSet(subCmd.Name, flag.ExitOnError)
cmdFlagSet.StringVar(&ConfigPath, "config", "/etc/jx/config.yaml", "Config file path")
cmdFlagSet.StringVar(&ConfigPath, "c", "/etc/jx/config.yaml", "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)")
cmdFlagSet.StringVar(&GlobalOutput, "o", "-", "Output target (default stdout)") cmdFlagSet.StringVar(&GlobalOutput, "o", "-", "Output target (default stdout)")
@ -318,7 +397,14 @@ func main() {
cmdFlagSet.PrintDefaults() cmdFlagSet.PrintDefaults()
VersionUsage() VersionUsage()
} }
case "config":
cmdFlagSet.Usage = func() {
fmt.Println("jx config source...")
cmdFlagSet.PrintDefaults()
VersionUsage()
}
} }
slog.Info("CLI", "cmd", subCmd.Name)
if os.Args[1] == subCmd.Name { if os.Args[1] == subCmd.Name {
if e := subCmd.Run(cmdFlagSet, os.Stdout); e != nil { if e := subCmd.Run(cmdFlagSet, os.Stdout); e != nil {
slog.Error("Failed running command", "command", os.Args[1], "error", e) slog.Error("Failed running command", "command", os.Args[1], "error", e)

View File

@ -0,0 +1,12 @@
resources:
- type: file
transition: create
attributes:
path: golangci-lint-1.55.2-linux-amd64.deb
sourceref: https://github.com/golangci/golangci-lint/releases/download/v1.55.2/golangci-lint-1.55.2-linux-amd64.deb
- type: package
transition: create
attributes:
name: golangci-lint
source: golangci-lint-1.55.2-linux-amd64.deb
type: deb

2
go.mod
View File

@ -1,6 +1,6 @@
module decl module decl
go 1.22.1 go 1.22.3
require ( require (
gitea.rosskeen.house/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3 gitea.rosskeen.house/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3

134
internal/config/block.go Normal file
View File

@ -0,0 +1,134 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"context"
"encoding/json"
"fmt"
"io"
"gopkg.in/yaml.v3"
"log/slog"
"decl/internal/codec"
)
type BlockType struct {
Name string `json:"name" yaml:"name"`
Type TypeName `json:"type" yaml:"type"`
}
type Block struct {
Name string `json:"name" yaml:"name"`
Type TypeName `json:"type" yaml:"type"`
Values Configuration `json:"values" yaml:"values"`
}
func NewBlock() *Block {
return &Block{ Type: "generic" }
}
func (b *Block) Clone() *Block {
return &Block {
Type: b.Type,
Values: b.Values.Clone(),
}
}
func (b *Block) Load(r io.Reader) error {
return codec.NewYAMLDecoder(r).Decode(b)
}
func (b *Block) LoadBlock(yamlBlock string) (err error) {
err = codec.NewYAMLStringDecoder(yamlBlock).Decode(b)
slog.Info("LoadBlock()", "yaml", yamlBlock, "object", b, "err", err)
return
}
func (b *Block) NewConfiguration() error {
uri := fmt.Sprintf("%s://", b.Type)
newConfig, err := ConfigTypes.New(uri)
b.Values = newConfig
return err
}
func (b *Block) GetValue(key string) (any, error) {
return b.Values.GetValue(key)
}
func (b *Block) Configuration() Configuration {
return b.Values
}
func (b *Block) SetURI(uri string) (e error) {
b.Values = NewConfiguration(uri)
if b.Values == nil {
return ErrUnknownConfigurationType
}
b.Type = TypeName(b.Values.Type())
_,e = b.Values.Read(context.Background())
return e
}
func (b *Block) UnmarshalValue(value *BlockType) error {
b.Name = value.Name
if value.Type == "" {
b.Type = "generic"
} else {
b.Type = value.Type
}
newConfig, configErr := ConfigTypes.New(fmt.Sprintf("%s://", b.Type))
if configErr != nil {
return configErr
}
b.Values = newConfig
return nil
}
func (b *Block) UnmarshalYAML(value *yaml.Node) error {
t := &BlockType{}
if unmarshalConfigurationTypeErr := value.Decode(t); unmarshalConfigurationTypeErr != nil {
return unmarshalConfigurationTypeErr
}
if err := b.UnmarshalValue(t); err != nil {
return err
}
configurationVals := struct {
Values yaml.Node `json:"values"`
}{}
if unmarshalValuesErr := value.Decode(&configurationVals); unmarshalValuesErr != nil {
return unmarshalValuesErr
}
if unmarshalConfigurationErr := configurationVals.Values.Decode(b.Values); unmarshalConfigurationErr != nil {
return unmarshalConfigurationErr
}
_, readErr := b.Values.Read(context.Background())
return readErr
}
func (b *Block) UnmarshalJSON(data []byte) error {
t := &BlockType{}
if unmarshalConfigurationTypeErr := json.Unmarshal(data, t); unmarshalConfigurationTypeErr != nil {
return unmarshalConfigurationTypeErr
}
if err := b.UnmarshalValue(t); err != nil {
return err
}
configurationVals := struct {
Values Configuration `json:"values"`
}{Values: b.Values}
if unmarshalValuesErr := json.Unmarshal(data, &configurationVals); unmarshalValuesErr != nil {
return unmarshalValuesErr
}
_, readErr := b.Values.Read(context.Background())
return readErr
}
func (b *Block) MarshalYAML() (any, error) {
return b, nil
}

View File

@ -0,0 +1,49 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
_ "fmt"
"github.com/stretchr/testify/assert"
"log"
"os"
"testing"
"strings"
)
var TempDir string
func TestMain(m *testing.M) {
var err error
TempDir, err = os.MkdirTemp("", "testconfig")
if err != nil || TempDir == "" {
log.Fatal(err)
}
rc := m.Run()
os.RemoveAll(TempDir)
os.Exit(rc)
}
func TestNewBlock(t *testing.T) {
configYaml := `
name: "foo"
values:
http_user: "test"
http_pass: "password"
`
docReader := strings.NewReader(configYaml)
block := NewBlock()
assert.NotNil(t, block)
assert.Nil(t, block.Load(docReader))
assert.Equal(t, "foo", block.Name)
val, err := block.GetValue("http_user")
assert.Nil(t, err)
assert.Equal(t, "test", val)
missingVal, missingErr := block.GetValue("content")
assert.ErrorIs(t, missingErr, ErrUnknownConfigurationKey)
assert.Nil(t, missingVal)
}

View File

@ -0,0 +1,9 @@
configurations:
- name: facts
type: exec
values:
path: /usr/bin/facter
args:
- "-j"
format: "json"

View File

@ -0,0 +1,45 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
_ "context"
_ "encoding/json"
_ "fmt"
_ "gopkg.in/yaml.v3"
_ "net/url"
_ "regexp"
_ "strings"
_ "os"
_ "io"
_ "compress/gzip"
_ "archive/tar"
_ "errors"
_ "path/filepath"
_ "decl/internal/codec"
"embed"
)
type ConfigurationSelector func(b *Block) bool
type ConfigSource interface {
Type() string
Extract(filter ConfigurationSelector) ([]*Document, error)
}
func NewConfigSource(uri string) ConfigSource {
s, e := ConfigSourceTypes.New(uri)
if e == nil {
return s
}
return nil
}
//go:embed configs/*.yaml
var configFiles embed.FS
func Configurations() ([]*Document, error) {
fs := NewConfigFS(configFiles)
return fs.Extract(nil)
}

View File

@ -0,0 +1,30 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
_ "context"
_ "encoding/json"
_ "fmt"
_ "gopkg.in/yaml.v3"
_ "net/url"
_ "regexp"
_ "strings"
_ "os"
_ "io"
)
type ConfigTarget interface {
Type() string
EmitResources(documents []*Document, filter ConfigurationSelector) error
Close() error
}
func NewConfigTarget(uri string) ConfigTarget {
s, e := ConfigTargetTypes.New(uri)
if e == nil {
return s
}
return nil
}

View File

@ -0,0 +1,48 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"errors"
"fmt"
_ "net/url"
_ "decl/internal/codec"
_ "io"
"decl/internal/types"
"decl/internal/data"
"strings"
)
var (
ErrUnknownConfigurationType = errors.New("Unknown configuration type")
ErrUnknownConfigurationKey = errors.New("Unknown configuration key")
ConfigTypes *types.Types[Configuration] = types.New[Configuration]()
ConfigSourceTypes *types.Types[ConfigSource] = types.New[ConfigSource]()
ConfigTargetTypes *types.Types[ConfigTarget] = types.New[ConfigTarget]()
)
type TypeName string //`json:"type"`
type Configuration interface {
Type() string
data.Reader
GetValue(name string) (any, error)
Clone() Configuration
}
func NewConfiguration(uri string) Configuration {
c, e := ConfigTypes.New(uri)
if e == nil {
return c
}
return nil
}
func (n *TypeName) UnmarshalJSON(b []byte) error {
ConfigTypeName := strings.Trim(string(b), "\"")
if ConfigTypes.Has(ConfigTypeName) {
*n = TypeName(ConfigTypeName)
return nil
}
return fmt.Errorf("%w: %s", ErrUnknownConfigurationType, ConfigTypeName)
}

View File

@ -0,0 +1,22 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
_ "context"
_ "fmt"
"github.com/stretchr/testify/assert"
_ "log"
_ "os"
_ "path/filepath"
_ "strings"
"testing"
)
func TestNewConfiguration(t *testing.T) {
configurationUri := "generic://"
testConfig := NewConfiguration(configurationUri)
assert.NotNil(t, testConfig)
v, _ := testConfig.GetValue("foo")
assert.Nil(t, v)
}

202
internal/config/document.go Normal file
View File

@ -0,0 +1,202 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"errors"
"encoding/json"
"fmt"
"gopkg.in/yaml.v3"
"io"
"log/slog"
_ "net/url"
"github.com/sters/yaml-diff/yamldiff"
"strings"
"decl/internal/codec"
_ "context"
)
var (
ErrConfigUndefinedName = errors.New("Config block is missing a defined name")
)
type ConfigNamesMap[Value any] map[string]Value
type Document struct {
names ConfigNamesMap[*Block]
ConfigBlocks []Block `json:"configurations" yaml:"configurations"`
}
func NewDocument() *Document {
return &Document{ names: make(ConfigNamesMap[*Block]) }
}
func (d *Document) Filter(filter ConfigurationSelector) []*Block {
configurations := make([]*Block, 0, len(d.ConfigBlocks))
for i := range d.ConfigBlocks {
filterConfig := &d.ConfigBlocks[i]
if filter == nil || filter(filterConfig) {
configurations = append(configurations, &d.ConfigBlocks[i])
}
}
return configurations
}
func (d *Document) Clone() *Document {
clone := NewDocument()
clone.ConfigBlocks = make([]Block, len(d.ConfigBlocks))
for i, res := range d.ConfigBlocks {
clone.ConfigBlocks[i] = *res.Clone()
}
return clone
}
func (d *Document) Load(r io.Reader) error {
c := codec.NewYAMLDecoder(r)
return c.Decode(d);
}
func (d *Document) Validate() error {
jsonDocument, jsonErr := d.JSON()
if jsonErr == nil {
s := NewSchema("document")
err := s.Validate(string(jsonDocument))
if err != nil {
return err
}
}
return nil
}
func (d *Document) Configurations() []Block {
return d.ConfigBlocks
}
func (d *Document) Generate(w io.Writer) error {
e := codec.NewYAMLEncoder(w)
err := e.Encode(d);
if err == nil {
return e.Close()
}
e.Close()
return err
}
func (d *Document) Append(doc *Document) {
if doc != nil {
for i := range doc.ConfigBlocks {
slog.Info("Document.Append()", "doc", doc, "block", doc.ConfigBlocks[i], "targetdoc", d)
d.AddConfigurationBlock(doc.ConfigBlocks[i].Name, doc.ConfigBlocks[i].Type, doc.ConfigBlocks[i].Values)
}
}
}
func (d *Document) AddConfigurationBlock(configurationName string, configurationType TypeName, configuration Configuration) {
cfg := NewBlock()
cfg.Name = configurationName
cfg.Type = configurationType
cfg.Values = configuration
d.names[cfg.Name] = cfg
d.ConfigBlocks = append(d.ConfigBlocks, *cfg)
}
func (d *Document) AddConfiguration(uri string) error {
cfg := NewBlock()
if e := cfg.SetURI(uri); e != nil {
return e
}
if cfg.Name == "" {
return ErrConfigUndefinedName
}
d.names[cfg.Name] = cfg
d.ConfigBlocks = append(d.ConfigBlocks, *cfg)
return nil
}
func (d *Document) Has(name string) bool {
_, ok := d.names[name]
return ok
}
func (d *Document) Get(name string) *Block {
return d.names[name]
}
func (d *Document) JSON() ([]byte, error) {
return json.Marshal(d)
}
func (d *Document) YAML() ([]byte, error) {
return yaml.Marshal(d)
}
func (d *Document) IndexName() error {
for _, b := range d.ConfigBlocks {
d.names[b.Name] = &b
}
return nil
}
func (d *Document) UnmarshalYAML(value *yaml.Node) error {
documentBlocks := struct {
ConfigBlocks *[]Block `json:"configurations" yaml:"configurations"`
}{ ConfigBlocks: &d.ConfigBlocks }
if unmarshalDocumentErr := value.Decode(documentBlocks); unmarshalDocumentErr != nil {
return unmarshalDocumentErr
}
return d.IndexName()
}
func (d *Document) UnmarshalJSON(data []byte) error {
documentBlocks := struct {
ConfigBlocks *[]Block `json:"configurations" yaml:"configurations"`
}{ ConfigBlocks: &d.ConfigBlocks }
if unmarshalDocumentErr := json.Unmarshal(data, &documentBlocks); unmarshalDocumentErr != nil {
return unmarshalDocumentErr
}
return d.IndexName()
}
func (d *Document) Diff(with *Document, output io.Writer) (returnOutput string, diffErr error) {
defer func() {
if r := recover(); r != nil {
returnOutput = ""
diffErr = fmt.Errorf("%s", r)
}
}()
slog.Info("Document.Diff()")
opts := []yamldiff.DoOptionFunc{}
if output == nil {
output = &strings.Builder{}
}
ydata, yerr := d.YAML()
if yerr != nil {
return "", yerr
}
yamlDiff,yamlDiffErr := yamldiff.Load(string(ydata))
if yamlDiffErr != nil {
return "", yamlDiffErr
}
wdata,werr := with.YAML()
if werr != nil {
return "", werr
}
withDiff,withDiffErr := yamldiff.Load(string(wdata))
if withDiffErr != nil {
return "", withDiffErr
}
for _,docDiffResults := range yamldiff.Do(yamlDiff, withDiff, opts...) {
slog.Info("Diff()", "diff", docDiffResults, "dump", docDiffResults.Dump())
_,e := output.Write([]byte(docDiffResults.Dump()))
if e != nil {
return "", e
}
}
slog.Info("Document.Diff() ", "document.yaml", ydata, "with.yaml", wdata)
if stringOutput, ok := output.(*strings.Builder); ok {
return stringOutput.String(), nil
}
return "", nil
}

View File

@ -0,0 +1,53 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"github.com/stretchr/testify/assert"
"strings"
"testing"
)
func TestNewDocument(t *testing.T) {
d := NewDocument()
assert.NotNil(t, d)
}
func TestDocumentLoader(t *testing.T) {
document := `
---
configurations:
- type: generic
name: global
values:
install_dir: /opt/jx
- name: system
values:
dist: ubuntu
release: focal
`
d := NewDocument()
assert.NotNil(t, d)
docReader := strings.NewReader(document)
e := d.Load(docReader)
assert.Nil(t, e)
configurations := d.Configurations()
assert.Equal(t, 2, len(configurations))
b := d.Get("system")
assert.NotNil(t, b)
cfg := b.Configuration()
value, valueErr := cfg.GetValue("dist")
assert.Nil(t, valueErr)
assert.Equal(t, "ubuntu", value)
}
func TestDocumentJSONSchema(t *testing.T) {
document := NewDocument()
document.ConfigBlocks = []Block{}
e := document.Validate()
assert.Nil(t, e)
}

121
internal/config/exec.go Normal file
View File

@ -0,0 +1,121 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"context"
"io"
"net/url"
"decl/internal/codec"
"decl/internal/command"
"encoding/json"
"gopkg.in/yaml.v3"
)
func init() {
ConfigTypes.Register([]string{"exec"}, func(u *url.URL) Configuration {
x := NewExec()
return x
})
}
type Exec struct {
Path string `yaml:"path" json:"path"`
Args []command.CommandArg `yaml:"args" json:"args"`
ValuesFormat codec.Format `yaml:"format" json:"format"`
Values map[string]any `yaml:"values" json:"values"`
ReadCommand *command.Command `yaml:"-" json:"-"`
}
func NewExec() *Exec {
x := &Exec{}
return x
}
func (x *Exec) Read(ctx context.Context) ([]byte, error) {
out, err := x.ReadCommand.Execute(x)
if err != nil {
return nil, err
}
exErr := x.ReadCommand.Extractor(out, x)
if exErr != nil {
return nil, exErr
}
return nil, exErr
}
func (x *Exec) Load(r io.Reader) (err error) {
err = codec.NewYAMLDecoder(r).Decode(x)
if err == nil {
_, err = x.Read(context.Background())
}
return err
}
func (x *Exec) LoadYAML(yamlData string) (err error) {
err = codec.NewYAMLStringDecoder(yamlData).Decode(x)
if err == nil {
_, err = x.Read(context.Background())
}
return err
}
func (x *Exec) UnmarshalJSON(data []byte) error {
if unmarshalErr := json.Unmarshal(data, x); unmarshalErr != nil {
return unmarshalErr
}
x.NewReadConfigCommand()
return nil
}
func (x *Exec) UnmarshalYAML(value *yaml.Node) error {
type decodeExec Exec
if unmarshalErr := value.Decode((*decodeExec)(x)); unmarshalErr != nil {
return unmarshalErr
}
x.NewReadConfigCommand()
return nil
}
func (x *Exec) Clone() Configuration {
clone := NewExec()
clone.Path = x.Path
clone.Args = x.Args
clone.ValuesFormat = x.ValuesFormat
clone.Values = x.Values
clone.ReadCommand = x.ReadCommand
return clone
}
func (x *Exec) Type() string {
return "exec"
}
func (x *Exec) GetValue(name string) (result any, err error) {
var ok bool
if result, ok = x.Values[name]; !ok {
err = ErrUnknownConfigurationKey
}
return
}
func (ex *Exec) NewReadConfigCommand() {
ex.ReadCommand = command.NewCommand()
ex.ReadCommand.Path = ex.Path
ex.ReadCommand.Args = ex.Args
ex.ReadCommand.Extractor = func(out []byte, target any) error {
x := target.(*Exec)
switch x.ValuesFormat {
case codec.FormatYaml:
return codec.NewYAMLStringDecoder(string(out)).Decode(&x.Values)
case codec.FormatJson:
return codec.NewJSONStringDecoder(string(out)).Decode(&x.Values)
case codec.FormatProtoBuf:
}
return nil
}
}

137
internal/config/file.go Normal file
View File

@ -0,0 +1,137 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
_ "context"
_ "encoding/json"
_ "fmt"
_ "gopkg.in/yaml.v3"
"net/url"
"path/filepath"
"decl/internal/transport"
"decl/internal/codec"
_ "os"
"io"
"errors"
"log/slog"
)
type ConfigFile struct {
Path string `yaml:"path" json:"path"`
Format codec.Format `yaml:"format" json:"format"`
reader *transport.Reader `yaml:"-" json:"-"`
writer *transport.Writer `yaml:"-" json:"-"`
encoder codec.Encoder `yaml:"-" json:"-"`
decoder codec.Decoder `yaml:"-" json:"-"`
}
func NewConfigFile() *ConfigFile {
return &ConfigFile{ Format: codec.FormatYaml }
}
func NewConfigFileFromURI(u *url.URL) *ConfigFile {
t := NewConfigFile()
if u.Scheme == "file" {
t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI()))
} else {
t.Path = filepath.Join(u.Hostname(), u.Path)
}
return t
}
func NewConfigFileSource(u *url.URL) *ConfigFile {
t := NewConfigFileFromURI(u)
t.reader,_ = transport.NewReader(u)
if formatErr := t.Format.Set(t.reader.ContentType()); formatErr != nil {
panic(formatErr)
}
t.decoder = codec.NewDecoder(t.reader, t.Format)
return t
}
func NewConfigFileTarget(u *url.URL) *ConfigFile {
t := NewConfigFileFromURI(u)
t.writer,_ = transport.NewWriter(u)
if formatErr := t.Format.Set(t.writer.ContentType()); formatErr != nil {
panic(formatErr)
}
t.encoder = codec.NewEncoder(t.writer, t.Format)
return t
}
func init() {
ConfigSourceTypes.Register([]string{"file"}, func(u *url.URL) ConfigSource {
return NewConfigFileSource(u)
})
ConfigSourceTypes.Register([]string{"pb","pb.gz","json","json.gz","yaml","yml","yaml.gz","yml.gz"}, func(u *url.URL) ConfigSource {
return NewConfigFileSource(u)
})
ConfigTargetTypes.Register([]string{"file"}, func(u *url.URL) ConfigTarget {
return NewConfigFileTarget(u)
})
ConfigTargetTypes.Register([]string{"pb","pb.gz","json","json.gz","yaml","yml","yaml.gz","yml.gz"}, func(u *url.URL) ConfigTarget {
return NewConfigFileTarget(u)
})
}
func (c *ConfigFile) Type() string { return "file" }
func (c *ConfigFile) Extract(filter ConfigurationSelector) ([]*Document, error) {
documents := make([]*Document, 0, 100)
defer func() {
c.reader.Close()
}()
slog.Info("Extract()", "documents", documents)
index := 0
for {
doc := NewDocument()
e := c.decoder.Decode(doc)
if errors.Is(e, io.EOF) {
break
}
if e != nil {
return documents, e
}
slog.Info("Extract()", "res", doc.ConfigBlocks[0].Values)
if validationErr := doc.Validate(); validationErr != nil {
return documents, validationErr
}
documents = append(documents, doc)
index++
}
return documents, nil
}
func (c *ConfigFile) EmitResources(documents []*Document, filter ConfigurationSelector) (error) {
defer func() {
c.encoder.Close()
c.writer.Close()
}()
for _, doc := range documents {
emitDoc := NewDocument()
if validationErr := doc.Validate(); validationErr != nil {
return validationErr
}
for _, block := range doc.Filter(filter) {
emitDoc.ConfigBlocks = append(emitDoc.ConfigBlocks, *block)
}
slog.Info("EmitResources", "doctarget", c, "encoder", c.encoder, "emit", emitDoc)
if documentErr := c.encoder.Encode(emitDoc); documentErr != nil {
slog.Info("EmitResources", "err", documentErr)
return documentErr
}
}
return nil
}
func (c *ConfigFile) Close() (error) {
return nil
}

109
internal/config/fs.go Normal file
View File

@ -0,0 +1,109 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
_ "context"
_ "encoding/json"
_ "fmt"
_ "gopkg.in/yaml.v3"
"net/url"
"path/filepath"
"decl/internal/codec"
"os"
"io"
"errors"
"io/fs"
"log/slog"
)
type ConfigFS struct {
Path string `yaml:"path" json:"path"`
subDirsStack []fs.FS `yaml:"-" json:"-"`
fsys fs.FS `yaml:"-" json:"-"`
}
func NewConfigFS(fsys fs.FS) *ConfigFS {
return &ConfigFS{
subDirsStack: make([]fs.FS, 0, 100),
fsys: fsys,
}
}
func init() {
ConfigSourceTypes.Register([]string{"fs"}, func(u *url.URL) ConfigSource {
t := NewConfigFS(nil)
t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI()))
t.fsys = os.DirFS(t.Path)
return t
})
}
func (c *ConfigFS) Type() string { return "fs" }
func (c *ConfigFS) ExtractDirectory(fsys fs.FS) ([]*Document, error) {
documents := make([]*Document, 0, 100)
files, err := fs.ReadDir(fsys, ".")
if err != nil {
return nil, err
}
for _,file := range files {
slog.Info("ConfigFS.ExtractDirectory", "file", file)
if file.IsDir() {
dir, subErr := fs.Sub(fsys, file.Name())
if subErr != nil {
return nil, subErr
}
c.subDirsStack = append(c.subDirsStack, dir)
} else {
fileHandle, fileErr := fsys.Open(file.Name())
if fileErr != nil {
return nil, fileErr
}
decoder := codec.NewYAMLDecoder(fileHandle)
doc := NewDocument()
e := decoder.Decode(doc)
if errors.Is(e, io.EOF) {
break
}
if e != nil {
return documents, e
}
slog.Info("ConfigFS.ExtractDirectory", "doc", doc)
if validationErr := doc.Validate(); validationErr != nil {
return documents, validationErr
}
documents = append(documents, doc)
}
}
return documents, nil
}
func (c *ConfigFS) Extract(filter ConfigurationSelector) ([]*Document, error) {
documents := make([]*Document, 0, 100)
path := c.fsys
c.subDirsStack = append(c.subDirsStack, path)
for {
if len(c.subDirsStack) == 0 {
break
}
var dirPath fs.FS
dirPath, c.subDirsStack = c.subDirsStack[len(c.subDirsStack) - 1], c.subDirsStack[:len(c.subDirsStack) - 1]
docs, dirErr := c.ExtractDirectory(dirPath)
if dirErr != nil {
return documents, dirErr
}
documents = append(documents, docs...)
}
return documents, nil
}

View File

@ -0,0 +1,48 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"context"
"encoding/json"
"net/url"
)
func init() {
ConfigTypes.Register([]string{"generic"}, func(u *url.URL) Configuration {
g := NewGeneric()
return g
})
}
type Generic map[string]any
func NewGeneric() *Generic {
g := make(Generic)
return &g
}
func (g *Generic) Clone() Configuration {
jsonGeneric, _ := json.Marshal(g)
clone := make(Generic)
if unmarshalErr := json.Unmarshal(jsonGeneric, &clone); unmarshalErr != nil {
panic(unmarshalErr)
}
return &clone
}
func (g *Generic) Type() string {
return "generic"
}
func (g *Generic) Read(context.Context) ([]byte, error) {
return nil, nil
}
func (g *Generic) GetValue(name string) (result any, err error) {
var ok bool
if result, ok = (*g)[name]; !ok {
err = ErrUnknownConfigurationKey
}
return
}

View File

@ -0,0 +1,13 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestNewGenericConfig(t *testing.T) {
g := NewGeneric()
assert.NotNil(t, g)
}

54
internal/config/schema.go Normal file
View File

@ -0,0 +1,54 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"errors"
"fmt"
"github.com/xeipuuv/gojsonschema"
"strings"
"embed"
"net/http"
"log/slog"
)
//go:embed schemas/*.schema.json
var schemaFiles embed.FS
type Schema struct {
schema gojsonschema.JSONLoader
}
func NewSchema(name string) *Schema {
path := fmt.Sprintf("file://schemas/%s.schema.json", name)
return &Schema{schema: gojsonschema.NewReferenceLoaderFileSystem(path, http.FS(schemaFiles))}
}
func (s *Schema) Validate(source string) error {
loader := gojsonschema.NewStringLoader(source)
result, err := gojsonschema.Validate(s.schema, loader)
if err != nil {
slog.Info("schema error", "source", source, "schema", s.schema, "result", result, "err", err)
return err
}
slog.Info("schema", "source", source, "schema", s.schema, "result", result, "err", err)
if !result.Valid() {
schemaErrors := strings.Builder{}
for _, err := range result.Errors() {
schemaErrors.WriteString(err.String() + "\n")
}
return errors.New(schemaErrors.String())
}
return nil
}
func (s *Schema) ValidateSchema() error {
sl := gojsonschema.NewSchemaLoader()
sl.Validate = true
schemaErr := sl.AddSchemas(s.schema)
slog.Info("validate schema definition", "schemaloader", sl, "err", schemaErr)
return schemaErr
}

View File

@ -0,0 +1,49 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"encoding/json"
"github.com/stretchr/testify/assert"
"testing"
)
func TestNewSchema(t *testing.T) {
s := NewSchema("document")
assert.NotEqual(t, nil, s)
}
func TestSchemaValidateJSON(t *testing.T) {
// ctx := context.Background()
s := NewSchema("block")
assert.NotNil(t, s)
assert.Nil(t, s.ValidateSchema())
configBlockYaml := `
type: "generic"
name: "foo"
values:
bar: quuz
`
testConfig := NewBlock()
e := testConfig.LoadBlock(configBlockYaml)
assert.Nil(t, e)
assert.Equal(t, "foo", testConfig.Name)
jsonDoc, jsonErr := json.Marshal(testConfig)
assert.Nil(t, jsonErr)
schemaErr := s.Validate(string(jsonDoc))
assert.Nil(t, schemaErr)
}
/*
func TestSchemaValidateSchema(t *testing.T) {
s := NewSchema("document")
assert.NotNil(t, s)
assert.Nil(t, s.ValidateSchema())
}
*/

View File

@ -0,0 +1,22 @@
{
"$id": "block.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "block",
"type": "object",
"required": [ "name", "values" ],
"properties": {
"name": {
"type": "string",
"description": "Config block name",
"minLength": 2
},
"type": {
"type": "string",
"description": "Config type name.",
"enum": [ "generic", "exec" ]
},
"values": {
"type": "object"
}
}
}

View File

@ -0,0 +1,19 @@
{
"$id": "document.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "document",
"type": "object",
"required": [ "configurations" ],
"properties": {
"configurations": {
"type": "array",
"description": "Configurations list",
"items": {
"oneOf": [
{ "$ref": "block.schema.json" }
]
}
}
}
}

View File

@ -0,0 +1,11 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
)
type ConfigurationValueGetter interface {
GetValue(key string) (any, error)
}

View File

@ -77,11 +77,12 @@ type Container struct {
State string `yaml:"state,omitempty" json:"state,omitempty"` State string `yaml:"state,omitempty" json:"state,omitempty"`
config ConfigurationValueGetter
apiClient ContainerClient apiClient ContainerClient
} }
func init() { func init() {
ResourceTypes.Register("container", func(u *url.URL) Resource { ResourceTypes.Register([]string{"container"}, func(u *url.URL) Resource {
c := NewContainer(nil) c := NewContainer(nil)
c.Name = filepath.Join(u.Hostname(), u.Path) c.Name = filepath.Join(u.Hostname(), u.Path)
return c return c
@ -158,6 +159,10 @@ func (c *Container) SetURI(uri string) error {
return e return e
} }
func (c *Container) UseConfig(config ConfigurationValueGetter) {
c.config = config
}
func (c *Container) JSON() ([]byte, error) { func (c *Container) JSON() ([]byte, error) {
return json.Marshal(c) return json.Marshal(c)
} }
@ -321,6 +326,14 @@ func (c *Container) Read(ctx context.Context) ([]byte, error) {
} }
} }
if inspectErr := c.Inspect(ctx, containerID); inspectErr != nil {
return nil, fmt.Errorf("%w: container %s", inspectErr, containerID)
}
slog.Info("Read() ", "type", c.Type(), "name", c.Name, "Id", c.Id)
return yaml.Marshal(c)
}
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) { if client.IsErrNotFound(err) {
c.State = "absent" c.State = "absent"
@ -328,7 +341,11 @@ func (c *Container) Read(ctx context.Context) ([]byte, error) {
c.State = "present" c.State = "present"
c.Id = containerJSON.ID c.Id = containerJSON.ID
if c.Name == "" { if c.Name == "" {
c.Name = containerJSON.Name if containerJSON.Name[0] == '/' {
c.Name = containerJSON.Name[1:]
} else {
c.Name = containerJSON.Name
}
} }
c.Path = containerJSON.Path c.Path = containerJSON.Path
c.Image = containerJSON.Image c.Image = containerJSON.Image
@ -343,8 +360,7 @@ func (c *Container) Read(ctx context.Context) ([]byte, error) {
c.RestartCount = containerJSON.RestartCount c.RestartCount = containerJSON.RestartCount
c.Driver = containerJSON.Driver c.Driver = containerJSON.Driver
} }
slog.Info("Read() ", "type", c.Type(), "name", c.Name, "Id", c.Id) return nil
return yaml.Marshal(c)
} }
func (c *Container) Delete(ctx context.Context) error { func (c *Container) Delete(ctx context.Context) error {

View File

@ -42,11 +42,12 @@ type ContainerImage struct {
Comment string `json:"comment,omitempty" yaml:"comment,omitempty"` Comment string `json:"comment,omitempty" yaml:"comment,omitempty"`
State string `yaml:"state,omitempty" json:"state,omitempty"` State string `yaml:"state,omitempty" json:"state,omitempty"`
config ConfigurationValueGetter
apiClient ContainerImageClient apiClient ContainerImageClient
} }
func init() { func init() {
ResourceTypes.Register("container-image", func(u *url.URL) Resource { ResourceTypes.Register([]string{"container-image"}, func(u *url.URL) Resource {
c := NewContainerImage(nil) c := NewContainerImage(nil)
c.Name = strings.Join([]string{u.Hostname(), u.Path}, ":") c.Name = strings.Join([]string{u.Hostname(), u.Path}, ":")
return c return c
@ -122,6 +123,10 @@ func (c *ContainerImage) SetURI(uri string) error {
return e return e
} }
func (c *ContainerImage) UseConfig(config ConfigurationValueGetter) {
c.config = config
}
func (c *ContainerImage) JSON() ([]byte, error) { func (c *ContainerImage) JSON() ([]byte, error) {
return json.Marshal(c) return json.Marshal(c)
} }

View File

@ -38,11 +38,12 @@ type ContainerNetwork struct {
State string `yaml:"state"` State string `yaml:"state"`
config ConfigurationValueGetter
apiClient ContainerNetworkClient apiClient ContainerNetworkClient
} }
func init() { func init() {
ResourceTypes.Register("container-network", func(u *url.URL) Resource { ResourceTypes.Register([]string{"container-network"}, func(u *url.URL) Resource {
n := NewContainerNetwork(nil) n := NewContainerNetwork(nil)
n.Name = filepath.Join(u.Hostname(), u.Path) n.Name = filepath.Join(u.Hostname(), u.Path)
return n return n
@ -115,6 +116,10 @@ func (n *ContainerNetwork) SetURI(uri string) error {
return e return e
} }
func (n *ContainerNetwork) UseConfig(config 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

@ -3,6 +3,7 @@
package resource package resource
import ( import (
_ "errors"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -12,18 +13,25 @@ import (
_ "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/config"
) )
type ConfigName string
type DeclarationType struct { type DeclarationType struct {
Type TypeName `json:"type" yaml:"type"` Type TypeName `json:"type" yaml:"type"`
Transition string `json:"transition,omitempty" yaml:"transition,omitempty"` Transition string `json:"transition,omitempty" yaml:"transition,omitempty"`
Config ConfigName `json:"config,omitempty" yaml:"config,omitempty"`
} }
type Declaration struct { type Declaration struct {
Type TypeName `json:"type" yaml:"type"` Type TypeName `json:"type" yaml:"type"`
Transition string `json:"transition,omitempty" yaml:"transition,omitempty"` Transition string `json:"transition,omitempty" yaml:"transition,omitempty"`
Attributes Resource `json:"attributes" yaml:"attributes"` Attributes Resource `json:"attributes" yaml:"attributes"`
Config ConfigName `json:"config,omitempty" yaml:"config,omitempty"`
runtime luaruntime.LuaRunner runtime luaruntime.LuaRunner
document *Document
configBlock *config.Block
} }
type ResourceLoader interface { type ResourceLoader interface {
@ -38,6 +46,10 @@ func NewDeclaration() *Declaration {
return &Declaration{} return &Declaration{}
} }
func (d *Declaration) SetDocument(newDocument *Document) {
d.document = newDocument
}
func (d *Declaration) ResolveId(ctx context.Context) string { func (d *Declaration) ResolveId(ctx context.Context) string {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -58,6 +70,7 @@ func (d *Declaration) Clone() *Declaration {
Transition: d.Transition, Transition: d.Transition,
Attributes: d.Attributes.Clone(), Attributes: d.Attributes.Clone(),
runtime: luaruntime.New(), runtime: luaruntime.New(),
Config: d.Config,
} }
} }
@ -110,6 +123,15 @@ func (d *Declaration) Apply() (result error) {
return result return result
} }
func (d *Declaration) SetConfig(configDoc *config.Document) {
if configDoc != nil {
if configDoc.Has(string(d.Config)) {
d.configBlock = configDoc.Get(string(d.Config))
d.Attributes.UseConfig(d.configBlock)
}
}
}
func (d *Declaration) SetURI(uri string) error { func (d *Declaration) SetURI(uri string) error {
slog.Info("Declaration.SetURI()", "uri", uri, "declaration", d) slog.Info("Declaration.SetURI()", "uri", uri, "declaration", d)
d.Attributes = NewResource(uri) d.Attributes = NewResource(uri)
@ -125,6 +147,7 @@ func (d *Declaration) SetURI(uri string) error {
func (d *Declaration) UnmarshalValue(value *DeclarationType) error { func (d *Declaration) UnmarshalValue(value *DeclarationType) error {
d.Type = value.Type d.Type = value.Type
d.Transition = value.Transition d.Transition = value.Transition
d.Config = value.Config
newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", value.Type)) newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", value.Type))
if resourceErr != nil { if resourceErr != nil {
return resourceErr return resourceErr

View File

@ -10,6 +10,7 @@ import (
_ "log" _ "log"
_ "os" _ "os"
"path/filepath" "path/filepath"
"decl/internal/types"
"testing" "testing"
) )
@ -70,7 +71,7 @@ func TestDeclarationNewResource(t *testing.T) {
assert.NotNil(t, resourceDeclaration) assert.NotNil(t, resourceDeclaration)
errNewUnknownResource := resourceDeclaration.NewResource() errNewUnknownResource := resourceDeclaration.NewResource()
assert.ErrorIs(t, errNewUnknownResource, ErrUnknownResourceType) assert.ErrorIs(t, errNewUnknownResource, types.ErrUnknownType)
resourceDeclaration.Type = "file" resourceDeclaration.Type = "file"
errNewFileResource := resourceDeclaration.NewResource() errNewFileResource := resourceDeclaration.NewResource()

View File

@ -12,15 +12,25 @@ _ "net/url"
"github.com/sters/yaml-diff/yamldiff" "github.com/sters/yaml-diff/yamldiff"
"strings" "strings"
"decl/internal/codec" "decl/internal/codec"
"decl/internal/types"
"decl/internal/config"
"context" "context"
) )
type ResourceMap[Value any] map[string]Value
type Document struct { type Document struct {
uris ResourceMap[*Declaration]
ResourceDecls []Declaration `json:"resources" yaml:"resources"` ResourceDecls []Declaration `json:"resources" yaml:"resources"`
config *config.Document
} }
func NewDocument() *Document { func NewDocument() *Document {
return &Document{} return &Document{ uris: make(ResourceMap[*Declaration]) }
}
func (d *Document) Types() *types.Types[Resource] {
return ResourceTypes
} }
func (d *Document) Filter(filter ResourceSelector) []*Declaration { func (d *Document) Filter(filter ResourceSelector) []*Declaration {
@ -34,18 +44,35 @@ func (d *Document) Filter(filter ResourceSelector) []*Declaration {
return resources return resources
} }
func (d *Document) GetResource(uri string) *Declaration {
if decl, ok := d.uris[uri]; ok {
return decl
}
return nil
}
func (d *Document) Clone() *Document { func (d *Document) Clone() *Document {
clone := NewDocument() clone := NewDocument()
clone.config = d.config
clone.ResourceDecls = make([]Declaration, len(d.ResourceDecls)) clone.ResourceDecls = make([]Declaration, len(d.ResourceDecls))
for i, res := range d.ResourceDecls { for i, res := range d.ResourceDecls {
clone.ResourceDecls[i] = *res.Clone() clone.ResourceDecls[i] = *res.Clone()
clone.ResourceDecls[i].SetDocument(clone)
clone.ResourceDecls[i].SetConfig(d.config)
} }
return clone return clone
} }
func (d *Document) Load(r io.Reader) error { func (d *Document) Load(r io.Reader) (err error) {
c := codec.NewYAMLDecoder(r) c := codec.NewYAMLDecoder(r)
return c.Decode(d); err = c.Decode(d)
if err == nil {
for i := range d.ResourceDecls {
d.ResourceDecls[i].SetDocument(d)
d.ResourceDecls[i].SetConfig(d.config)
}
}
return
} }
func (d *Document) Validate() error { func (d *Document) Validate() error {
@ -68,6 +95,14 @@ func (d *Document) Validate() error {
return nil return nil
} }
func (d *Document) SetConfig(config *config.Document) {
d.config = config
}
func (d *Document) ConfigDoc() *config.Document {
return d.config
}
func (d *Document) Resources() []Declaration { func (d *Document) Resources() []Declaration {
return d.ResourceDecls return d.ResourceDecls
} }
@ -95,6 +130,7 @@ func (d *Document) Apply(state string) error {
if state != "" { if state != "" {
d.ResourceDecls[idx].Transition = state d.ResourceDecls[idx].Transition = state
} }
d.ResourceDecls[idx].SetConfig(d.config)
if e := d.ResourceDecls[idx].Apply(); e != nil { if e := d.ResourceDecls[idx].Apply(); e != nil {
slog.Error("Document.Apply() error applying resource", "index", idx, "uri", d.ResourceDecls[idx].Resource().URI(), "resource", d.ResourceDecls[idx].Resource(), "error", e) slog.Error("Document.Apply() error applying resource", "index", idx, "uri", d.ResourceDecls[idx].Resource().URI(), "resource", d.ResourceDecls[idx].Resource(), "error", e)
return e return e
@ -117,11 +153,17 @@ func (d *Document) Generate(w io.Writer) error {
return err return err
} }
func (d *Document) MapResourceURI(uri string, declaration *Declaration) {
d.uris[uri] = declaration
}
func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration Resource) { func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration Resource) {
decl := NewDeclaration() decl := NewDeclaration()
decl.Type = TypeName(resourceType) decl.Type = TypeName(resourceType)
decl.Attributes = resourceDeclaration decl.Attributes = resourceDeclaration
decl.SetDocument(d)
d.ResourceDecls = append(d.ResourceDecls, *decl) d.ResourceDecls = append(d.ResourceDecls, *decl)
d.MapResourceURI(decl.Attributes.URI(), decl)
} }
func (d *Document) AddResource(uri string) error { func (d *Document) AddResource(uri string) error {
@ -129,8 +171,9 @@ func (d *Document) AddResource(uri string) error {
if e := decl.SetURI(uri); e != nil { if e := decl.SetURI(uri); e != nil {
return e return e
} }
decl.SetDocument(d)
d.ResourceDecls = append(d.ResourceDecls, *decl) d.ResourceDecls = append(d.ResourceDecls, *decl)
d.MapResourceURI(decl.Attributes.URI(), decl)
return nil return nil
} }

View File

@ -18,7 +18,7 @@ import (
func TestNewDocumentLoader(t *testing.T) { func TestNewDocumentLoader(t *testing.T) {
d := NewDocument() d := NewDocument()
assert.NotEqual(t, nil, d) assert.NotNil(t, d)
} }
func TestDocumentLoader(t *testing.T) { func TestDocumentLoader(t *testing.T) {
@ -109,7 +109,7 @@ resources:
var documentYaml strings.Builder var documentYaml strings.Builder
d := NewDocument() d := NewDocument()
assert.NotEqual(t, nil, d) assert.NotNil(t, d)
f, e := ResourceTypes.New("file://") f, e := ResourceTypes.New("file://")
assert.Nil(t, e) assert.Nil(t, e)
@ -120,7 +120,7 @@ resources:
assert.Nil(t, readErr) assert.Nil(t, readErr)
d.AddResourceDeclaration("file", f) d.AddResourceDeclaration("file", f)
ey := d.Generate(&documentYaml) ey := d.Generate(&documentYaml)
assert.Equal(t, nil, ey) assert.Nil(t, ey)
assert.Greater(t, documentYaml.Len(), 0) assert.Greater(t, documentYaml.Len(), 0)
assert.YAMLEq(t, expected, documentYaml.String()) assert.YAMLEq(t, expected, documentYaml.String())

View File

@ -20,6 +20,8 @@ import (
"crypto/sha256" "crypto/sha256"
"gitea.rosskeen.house/rosskeen.house/machine" "gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec" "decl/internal/codec"
"decl/internal/iofilter"
"strings"
) )
type FileType string type FileType string
@ -41,7 +43,7 @@ var ErrInvalidFileOwner error = errors.New("Unknown User")
var ErrInvalidFileGroup error = errors.New("Unknown Group") var ErrInvalidFileGroup error = errors.New("Unknown Group")
func init() { func init() {
ResourceTypes.Register("file", func(u *url.URL) Resource { ResourceTypes.Register([]string{"file"}, func(u *url.URL) Resource {
f := NewFile() f := NewFile()
f.Path = filepath.Join(u.Hostname(), u.Path) f.Path = filepath.Join(u.Hostname(), u.Path)
return f return f
@ -62,11 +64,14 @@ type File struct {
Mtime time.Time `json:"mtime,omitempty" yaml:"mtime,omitempty"` Mtime time.Time `json:"mtime,omitempty" yaml:"mtime,omitempty"`
Content string `json:"content,omitempty" yaml:"content,omitempty"` Content string `json:"content,omitempty" yaml:"content,omitempty"`
ContentSourceRef ResourceReference `json:"sourceref,omitempty" yaml:"sourceref,omitempty"`
Sha256 string `json:"sha256,omitempty" yaml:"sha256,omitempty"` Sha256 string `json:"sha256,omitempty" yaml:"sha256,omitempty"`
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"` State string `json:"state,omitempty" yaml:"state,omitempty"`
SerializeContent bool `json:"serializecontent,omitempty" yaml:"serializecontent,omitempty"`
config ConfigurationValueGetter
} }
type ResourceFileInfo struct { type ResourceFileInfo struct {
@ -76,7 +81,7 @@ 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{ normalizePath: false, Owner: currentUser.Username, Group: group.Name, Mode: "0644", FileType: RegularFile} f := &File{ normalizePath: false, Owner: currentUser.Username, Group: group.Name, Mode: "0644", FileType: RegularFile, SerializeContent: false }
slog.Info("NewFile()", "file", f) slog.Info("NewFile()", "file", f)
return f return f
} }
@ -178,6 +183,10 @@ func (f *File) SetURI(uri string) error {
return e return e
} }
func (f *File) UseConfig(config ConfigurationValueGetter) {
f.config = config
}
func (f *File) Validate() error { func (f *File) Validate() error {
return fmt.Errorf("failed") return fmt.Errorf("failed")
} }
@ -211,6 +220,11 @@ func (f *File) ResolveId(ctx context.Context) string {
} }
func (f *File) NormalizePath() error { func (f *File) NormalizePath() error {
if f.config != nil {
if prefixPath, configErr := f.config.GetValue("prefix"); configErr == nil {
f.Path = filepath.Join(prefixPath.(string), f.Path)
}
}
if f.normalizePath { if f.normalizePath {
filePath, fileAbsErr := filepath.Abs(f.Path) filePath, fileAbsErr := filepath.Abs(f.Path)
if fileAbsErr == nil { if fileAbsErr == nil {
@ -257,6 +271,7 @@ func (f *ResourceFileInfo) Sys() any {
} }
func (f *File) Create(ctx context.Context) error { func (f *File) Create(ctx context.Context) error {
slog.Info("File.Create()", "file", f)
uid, uidErr := LookupUID(f.Owner) uid, uidErr := LookupUID(f.Owner)
if uidErr != nil { if uidErr != nil {
return fmt.Errorf("%w: unkwnon user %d", ErrInvalidFileOwner, uid) return fmt.Errorf("%w: unkwnon user %d", ErrInvalidFileOwner, uid)
@ -284,6 +299,27 @@ func (f *File) Create(ctx context.Context) error {
default: default:
fallthrough fallthrough
case RegularFile: case RegularFile:
copyBuffer := make([]byte, 32 * 1024)
hash := sha256.New()
f.Size = 0
var contentReader io.ReadCloser
if len(f.Content) == 0 && len(f.ContentSourceRef) != 0 {
if refReader, err := f.ContentSourceRef.ContentReaderStream(); err == nil {
contentReader = refReader
} else {
return err
}
} else {
contentReader = io.NopCloser(strings.NewReader(f.Content))
}
sumReadData := iofilter.NewReader(contentReader, func(p []byte, readn int, readerr error) (n int, err error) {
hash.Write(p[:readn])
f.Size += int64(readn)
return readn, readerr
})
createdFile, e := os.Create(f.Path) createdFile, e := os.Create(f.Path)
if e != nil { if e != nil {
return e return e
@ -292,16 +328,25 @@ func (f *File) Create(ctx context.Context) error {
if chmodErr := createdFile.Chmod(os.FileMode(mode)); chmodErr != nil { if chmodErr := createdFile.Chmod(os.FileMode(mode)); chmodErr != nil {
return chmodErr return chmodErr
} }
_, writeErr := io.CopyBuffer(createdFile, sumReadData, copyBuffer)
if writeErr != nil {
return fmt.Errorf("File.Create(): CopyBuffer failed %v %v: %w", createdFile, contentReader, writeErr)
}
/*
_, writeErr := createdFile.Write([]byte(f.Content)) _, writeErr := createdFile.Write([]byte(f.Content))
if writeErr != nil { if writeErr != nil {
return writeErr return writeErr
} }
*/
f.Sha256 = fmt.Sprintf("%x", hash.Sum(nil))
if !f.Mtime.IsZero() && !f.Atime.IsZero() { if !f.Mtime.IsZero() && !f.Atime.IsZero() {
if chtimesErr := os.Chtimes(f.Path, f.Atime, f.Mtime); chtimesErr != nil { if chtimesErr := os.Chtimes(f.Path, f.Atime, f.Mtime); chtimesErr != nil {
return chtimesErr return chtimesErr
} }
} }
} }
if chownErr := os.Chown(f.Path, uid, gid); chownErr != nil { if chownErr := os.Chown(f.Path, uid, gid); chownErr != nil {
return chownErr return chownErr
} }
@ -372,17 +417,19 @@ func (f *File) Read(ctx context.Context) ([]byte, error) {
switch f.FileType { switch f.FileType {
case RegularFile: case RegularFile:
file, fileErr := os.Open(f.Path) if len(f.ContentSourceRef) == 0 || f.SerializeContent {
if fileErr != nil { file, fileErr := os.Open(f.Path)
panic(fileErr) if fileErr != nil {
} panic(fileErr)
}
fileContent, ioErr := io.ReadAll(file) fileContent, ioErr := io.ReadAll(file)
if ioErr != nil { if ioErr != nil {
panic(ioErr) panic(ioErr)
}
f.Content = string(fileContent)
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fileContent))
} }
f.Content = string(fileContent)
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fileContent))
case SymbolicLinkFile: case SymbolicLinkFile:
linkTarget, pathErr := os.Readlink(f.Path) linkTarget, pathErr := os.Readlink(f.Path)
if pathErr != nil { if pathErr != nil {

View File

@ -397,3 +397,54 @@ func TestFileDelete(t *testing.T) {
assert.Nil(t, stater.Trigger("delete")) assert.Nil(t, stater.Trigger("delete"))
assert.NoFileExists(t, file, nil) assert.NoFileExists(t, file, nil)
} }
func TestFileContentRef(t *testing.T) {
file, _ := filepath.Abs(filepath.Join(TempDir, "src.txt"))
copyFile, _ := filepath.Abs(filepath.Join(TempDir, "copy.txt"))
decl := fmt.Sprintf(`
path: "%s"
owner: "%s"
group: "%s"
mode: "0600"
content: |-
test line 1
test line 2
state: present
`, file, ProcessTestUserName, ProcessTestGroupName)
contentRef := fmt.Sprintf(`
path: "%s"
owner: "%s"
group: "%s"
mode: "0600"
sourceref: "file://%s"
state: present
`, file, ProcessTestUserName, ProcessTestGroupName, copyFile)
f := NewFile()
stater := f.StateMachine()
e := f.LoadDecl(decl)
assert.Nil(t, e)
assert.Equal(t, ProcessTestUserName, f.Owner)
assert.Nil(t, stater.Trigger("create"))
assert.FileExists(t, file, nil)
s, e := os.Stat(file)
assert.Nil(t, e)
assert.Greater(t, s.Size(), int64(0))
fr := NewFile()
loadErr := fr.LoadDecl(contentRef)
assert.Nil(t, loadErr)
assert.Equal(t, ProcessTestUserName, fr.Owner)
assert.Nil(t, fr.StateMachine().Trigger("create"))
assert.FileExists(t, file, nil)
_, statErr := os.Stat(file)
assert.Nil(t, statErr)
assert.Nil(t, stater.Trigger("delete"))
assert.NoFileExists(t, file, nil)
}

View File

@ -19,8 +19,7 @@ _ "os"
) )
func init() { func init() {
ResourceTypes.Register("http", HTTPFactory) ResourceTypes.Register([]string{"http", "https"}, HTTPFactory)
ResourceTypes.Register("https", HTTPFactory)
} }
func HTTPFactory(u *url.URL) Resource { func HTTPFactory(u *url.URL) Resource {
@ -41,7 +40,10 @@ type HTTP struct {
Endpoint string `yaml:"endpoint" json:"endpoint"` Endpoint string `yaml:"endpoint" json:"endpoint"`
Headers []HTTPHeader `yaml:"headers,omitempty" json:"headers,omitempty"` Headers []HTTPHeader `yaml:"headers,omitempty" json:"headers,omitempty"`
Body string `yaml:"body,omitempty" json:"body,omitempty"` Body string `yaml:"body,omitempty" json:"body,omitempty"`
Status string `yaml:"status,omitempty" json:"status,omitempty"`
StatusCode int `yaml:"statuscode,omitempty" json:"statuscode,omitempty"`
State string `yaml:"state,omitempty" json:"state,omitempty"` State string `yaml:"state,omitempty" json:"state,omitempty"`
config ConfigurationValueGetter
} }
func NewHTTP() *HTTP { func NewHTTP() *HTTP {
@ -67,9 +69,22 @@ func (h *HTTP) StateMachine() machine.Stater {
func (h *HTTP) Notify(m *machine.EventMessage) { func (h *HTTP) Notify(m *machine.EventMessage) {
ctx := context.Background() ctx := context.Background()
slog.Info("Notify()", "http", h, "m", m)
switch m.On { switch m.On {
case machine.ENTERSTATEEVENT: case machine.ENTERSTATEEVENT:
switch m.Dest { switch m.Dest {
case "start_read":
if _,readErr := h.Read(ctx); readErr == nil {
if triggerErr := h.StateMachine().Trigger("state_read"); triggerErr == nil {
return
} else {
h.State = "absent"
panic(triggerErr)
}
} else {
h.State = "absent"
panic(readErr)
}
case "start_create": case "start_create":
if e := h.Create(ctx); e == nil { if e := h.Create(ctx); e == nil {
if triggerErr := h.stater.Trigger("created"); triggerErr == nil { if triggerErr := h.stater.Trigger("created"); triggerErr == nil {
@ -77,7 +92,21 @@ func (h *HTTP) Notify(m *machine.EventMessage) {
} }
} }
h.State = "absent" h.State = "absent"
case "present": case "start_delete":
if deleteErr := h.Delete(ctx); deleteErr == nil {
if triggerErr := h.StateMachine().Trigger("deleted"); triggerErr == nil {
return
} else {
h.State = "present"
panic(triggerErr)
}
} else {
h.State = "present"
panic(deleteErr)
}
case "absent":
h.State = "absent"
case "present", "created", "read":
h.State = "present" h.State = "present"
} }
case machine.EXITSTATEEVENT: case machine.EXITSTATEEVENT:
@ -96,6 +125,10 @@ func (h *HTTP) SetURI(uri string) error {
return nil return nil
} }
func (h *HTTP) UseConfig(config ConfigurationValueGetter) {
h.config = config
}
func (h *HTTP) JSON() ([]byte, error) { func (h *HTTP) JSON() ([]byte, error) {
return json.Marshal(h) return json.Marshal(h)
} }
@ -140,10 +173,18 @@ func (h *HTTP) Create(ctx context.Context) error {
if reqErr != nil { if reqErr != nil {
return reqErr return reqErr
} }
if tokenErr := h.ReadAuthorizationTokenFromConfig(req); tokenErr != nil {
slog.Error("ReadAuthorizationTokenFromConfig()", "error", tokenErr)
}
for _,header := range h.Headers { for _,header := range h.Headers {
req.Header.Add(header.Name, header.Value) req.Header.Add(header.Name, header.Value)
} }
resp, err := h.client.Do(req) resp, err := h.client.Do(req)
h.Status = resp.Status
h.StatusCode = resp.StatusCode
if err != nil { if err != nil {
return err return err
} }
@ -151,6 +192,18 @@ func (h *HTTP) Create(ctx context.Context) error {
return err return err
} }
func (h *HTTP) ReadAuthorizationTokenFromConfig(req *http.Request) error {
if h.config != nil {
token, tokenErr := h.config.GetValue("authorization_token")
if tokenErr == nil {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
}
slog.Info("ReadAuthorizationTokenFromConfig()", "error", tokenErr)
return tokenErr
}
return nil
}
func (h *HTTP) Read(ctx context.Context) ([]byte, error) { func (h *HTTP) Read(ctx context.Context) ([]byte, error) {
req, reqErr := http.NewRequestWithContext(ctx, "GET", h.Endpoint, nil) req, reqErr := http.NewRequestWithContext(ctx, "GET", h.Endpoint, nil)
if reqErr != nil { if reqErr != nil {
@ -158,6 +211,11 @@ func (h *HTTP) Read(ctx context.Context) ([]byte, error) {
} }
slog.Info("HTTP.Read() ", "request", req, "err", reqErr) slog.Info("HTTP.Read() ", "request", req, "err", reqErr)
tokenErr := h.ReadAuthorizationTokenFromConfig(req)
if tokenErr != nil {
slog.Error("ReadAuthorizationTokenFromConfig()", "error", tokenErr)
}
if len(h.Headers) > 0 { if len(h.Headers) > 0 {
for _,header := range h.Headers { for _,header := range h.Headers {
req.Header.Add(header.Name, header.Value) req.Header.Add(header.Name, header.Value)
@ -165,6 +223,9 @@ func (h *HTTP) Read(ctx context.Context) ([]byte, error) {
} }
resp, err := h.client.Do(req) resp, err := h.client.Do(req)
slog.Info("Http.Read()", "response", resp, "error", err)
h.Status = resp.Status
h.StatusCode = resp.StatusCode
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -177,6 +238,10 @@ func (h *HTTP) Read(ctx context.Context) ([]byte, error) {
return yaml.Marshal(h) return yaml.Marshal(h)
} }
func (h *HTTP) Delete(ctx context.Context) error {
return nil
}
func (h *HTTP) Type() string { func (h *HTTP) Type() string {
return "http" return "http"
} }

View File

@ -18,10 +18,11 @@ _ "os/exec"
"log/slog" "log/slog"
"gitea.rosskeen.house/rosskeen.house/machine" "gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec" "decl/internal/codec"
"decl/internal/command"
) )
func init() { func init() {
ResourceTypes.Register("iptable", func(u *url.URL) Resource { ResourceTypes.Register([]string{"iptable"}, func(u *url.URL) Resource {
i := NewIptable() i := NewIptable()
i.Table = IptableName(u.Hostname()) i.Table = IptableName(u.Hostname())
if len(u.Path) > 0 { if len(u.Path) > 0 {
@ -122,10 +123,12 @@ type Iptable struct {
ChainLength uint `json:"-" yaml:"-"` ChainLength uint `json:"-" yaml:"-"`
ResourceType IptableType `json:"resourcetype,omitempty" yaml:"resourcetype,omitempty"` ResourceType IptableType `json:"resourcetype,omitempty" yaml:"resourcetype,omitempty"`
CreateCommand *Command `yaml:"-" json:"-"` CreateCommand *command.Command `yaml:"-" json:"-"`
ReadCommand *Command `yaml:"-" json:"-"` ReadCommand *command.Command `yaml:"-" json:"-"`
UpdateCommand *Command `yaml:"-" json:"-"` UpdateCommand *command.Command `yaml:"-" json:"-"`
DeleteCommand *Command `yaml:"-" json:"-"` DeleteCommand *command.Command `yaml:"-" json:"-"`
config ConfigurationValueGetter
} }
func NewIptable() *Iptable { func NewIptable() *Iptable {
@ -203,6 +206,10 @@ func (i *Iptable) SetURI(uri string) error {
return e return e
} }
func (i *Iptable) UseConfig(config ConfigurationValueGetter) {
i.config = config
}
func (i *Iptable) Validate() error { func (i *Iptable) Validate() error {
s := NewSchema(i.Type()) s := NewSchema(i.Type())
jsonDoc, jsonErr := i.JSON() jsonDoc, jsonErr := i.JSON()
@ -233,7 +240,7 @@ func (i *Iptable) UnmarshalYAML(value *yaml.Node) error {
return nil return nil
} }
func (i *Iptable) NewCRUD() (create *Command, read *Command, update *Command, del *Command) { func (i *Iptable) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) {
return NewIptableCreateCommand(), NewIptableReadCommand(), NewIptableUpdateCommand(), NewIptableDeleteCommand() return NewIptableCreateCommand(), NewIptableReadCommand(), NewIptableUpdateCommand(), NewIptableDeleteCommand()
} }
@ -403,11 +410,11 @@ func (i *Iptable) MatchRule(flags []string) (match bool) {
} }
func (i *Iptable) ReadChainLength() error { func (i *Iptable) ReadChainLength() error {
c := NewCommand() c := command.NewCommand()
c.Path = "iptables" c.Path = "iptables"
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("-S"), command.CommandArg("-S"),
CommandArg("{{ .Chain }}"), command.CommandArg("{{ .Chain }}"),
} }
output,err := c.Execute(i) output,err := c.Execute(i)
if err == nil { if err == nil {
@ -452,7 +459,7 @@ func (i *Iptable) Read(ctx context.Context) ([]byte, error) {
func (i *Iptable) Type() string { return "iptable" } func (i *Iptable) Type() string { return "iptable" }
func (i *IptableType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) { func (i *IptableType) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) {
switch *i { switch *i {
case IptableTypeRule: case IptableTypeRule:
return NewIptableCreateCommand(), NewIptableReadCommand(), NewIptableUpdateCommand(), NewIptableDeleteCommand() return NewIptableCreateCommand(), NewIptableReadCommand(), NewIptableUpdateCommand(), NewIptableDeleteCommand()
@ -489,24 +496,24 @@ func (i *IptableType) UnmarshalYAML(value *yaml.Node) error {
return i.UnmarshalValue(s) return i.UnmarshalValue(s)
} }
func NewIptableCreateCommand() *Command { func NewIptableCreateCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "iptables" c.Path = "iptables"
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("-t"), command.CommandArg("-t"),
CommandArg("{{ .Table }}"), command.CommandArg("{{ .Table }}"),
CommandArg("{{ if le .Id .ChainLength }}-R{{ else }}-A{{ end }}"), command.CommandArg("{{ if le .Id .ChainLength }}-R{{ else }}-A{{ end }}"),
CommandArg("{{ .Chain }}"), command.CommandArg("{{ .Chain }}"),
CommandArg("{{ if le .Id .ChainLength }}{{ .Id }}{{ end }}"), command.CommandArg("{{ if le .Id .ChainLength }}{{ .Id }}{{ end }}"),
CommandArg("{{ if .In }}-i {{ .In }}{{ else if .Out }}-o {{ .Out }}{{ end }}"), command.CommandArg("{{ if .In }}-i {{ .In }}{{ else if .Out }}-o {{ .Out }}{{ end }}"),
CommandArg("{{ range .Match }}-m {{ . }} {{- end }}"), command.CommandArg("{{ range .Match }}-m {{ . }} {{- end }}"),
CommandArg("{{ if .Source }}-s {{ .Source }}{{ end }}"), command.CommandArg("{{ if .Source }}-s {{ .Source }}{{ end }}"),
CommandArg("{{ if .Sport }}--sport {{ .Sport }}{{ end }}"), command.CommandArg("{{ if .Sport }}--sport {{ .Sport }}{{ end }}"),
CommandArg("{{ if .Destination }}-d {{ .Destination }}{{ end }}"), command.CommandArg("{{ if .Destination }}-d {{ .Destination }}{{ end }}"),
CommandArg("{{ if .Dport }}--dport {{ .Dport }}{{ end }}"), command.CommandArg("{{ if .Dport }}--dport {{ .Dport }}{{ end }}"),
CommandArg("{{ if .Proto }}-p {{ .Proto }}{{ end }}"), command.CommandArg("{{ if .Proto }}-p {{ .Proto }}{{ end }}"),
CommandArg("{{ range .Flags }} --{{ .Name }} {{ .Value }} {{ end }}"), command.CommandArg("{{ range .Flags }} --{{ .Name }} {{ .Value }} {{ end }}"),
CommandArg("{{ if .Jump }}-j {{ .Jump }}{{ end }}"), command.CommandArg("{{ if .Jump }}-j {{ .Jump }}{{ end }}"),
} }
return c return c
} }
@ -539,15 +546,15 @@ func IptableExtractRule(lineNumber uint, ruleLine string, target *Iptable) (stat
} }
func NewIptableReadCommand() *Command { func NewIptableReadCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "iptables" c.Path = "iptables"
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("-t"), command.CommandArg("-t"),
CommandArg("{{ .Table }}"), command.CommandArg("{{ .Table }}"),
CommandArg("-S"), command.CommandArg("-S"),
CommandArg("{{ .Chain }}"), command.CommandArg("{{ .Chain }}"),
CommandArg("{{ if .Id }}{{ .Id }}{{ end }}"), command.CommandArg("{{ if .Id }}{{ .Id }}{{ end }}"),
} }
c.Extractor = func(out []byte, target any) error { c.Extractor = func(out []byte, target any) error {
i := target.(*Iptable) i := target.(*Iptable)
@ -581,14 +588,14 @@ func NewIptableReadCommand() *Command {
return c return c
} }
func NewIptableReadChainCommand() *Command { func NewIptableReadChainCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "iptables" c.Path = "iptables"
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("-t"), command.CommandArg("-t"),
CommandArg("{{ .Table }}"), command.CommandArg("{{ .Table }}"),
CommandArg("-S"), command.CommandArg("-S"),
CommandArg("{{ .Chain }}"), command.CommandArg("{{ .Chain }}"),
} }
c.Extractor = func(out []byte, target any) error { c.Extractor = func(out []byte, target any) error {
IptableChainRules := target.(*[]*Iptable) IptableChainRules := target.(*[]*Iptable)
@ -620,22 +627,22 @@ func NewIptableReadChainCommand() *Command {
return c return c
} }
func NewIptableUpdateCommand() *Command { func NewIptableUpdateCommand() *command.Command {
return NewIptableCreateCommand() return NewIptableCreateCommand()
} }
func NewIptableDeleteCommand() *Command { func NewIptableDeleteCommand() *command.Command {
return nil return nil
} }
func NewIptableChainCreateCommand() *Command { func NewIptableChainCreateCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "iptables" c.Path = "iptables"
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("-t"), command.CommandArg("-t"),
CommandArg("{{ .Table }}"), command.CommandArg("{{ .Table }}"),
CommandArg("-N"), command.CommandArg("-N"),
CommandArg("{{ .Chain }}"), command.CommandArg("{{ .Chain }}"),
} }
c.Extractor = func(out []byte, target any) error { c.Extractor = func(out []byte, target any) error {
slog.Info("IptableChain Extractor", "output", out, "command", c) slog.Info("IptableChain Extractor", "output", out, "command", c)
@ -743,14 +750,14 @@ func RuleExtractorById(out []byte, target any) (err error) {
return return
} }
func NewIptableChainReadCommand() *Command { func NewIptableChainReadCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "iptables" c.Path = "iptables"
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("-t"), command.CommandArg("-t"),
CommandArg("{{ .Table }}"), command.CommandArg("{{ .Table }}"),
CommandArg("-S"), command.CommandArg("-S"),
CommandArg("{{ .Chain }}"), command.CommandArg("{{ .Chain }}"),
} }
c.Extractor = func(out []byte, target any) error { c.Extractor = func(out []byte, target any) error {
i := target.(*Iptable) i := target.(*Iptable)
@ -776,11 +783,11 @@ func NewIptableChainReadCommand() *Command {
return c return c
} }
func NewIptableChainUpdateCommand() *Command { func NewIptableChainUpdateCommand() *command.Command {
return NewIptableChainCreateCommand() return NewIptableChainCreateCommand()
} }
func NewIptableChainDeleteCommand() *Command { func NewIptableChainDeleteCommand() *command.Command {
return nil return nil
} }

View File

@ -19,6 +19,7 @@ _ "strings"
_ "syscall" _ "syscall"
"testing" "testing"
_ "time" _ "time"
"decl/internal/command"
) )
func TestNewIptableResource(t *testing.T) { func TestNewIptableResource(t *testing.T) {
@ -65,7 +66,7 @@ func TestReadIptable(t *testing.T) {
e := testRule.LoadDecl(declarationAttributes) e := testRule.LoadDecl(declarationAttributes)
assert.Nil(t, e) assert.Nil(t, e)
testRule.ReadCommand = (*Command)(m) testRule.ReadCommand = (*command.Command)(m)
// testRuleErr := testRule.Apply() // testRuleErr := testRule.Apply()
// assert.Nil(t, testRuleErr) // assert.Nil(t, testRuleErr)
r, e := testRule.Read(ctx) r, e := testRule.Read(ctx)

View File

@ -8,9 +8,10 @@ _ "github.com/stretchr/testify/assert"
_ "os" _ "os"
_ "strings" _ "strings"
_ "testing" _ "testing"
"decl/internal/command"
) )
type MockCommand Command type MockCommand command.Command
func (m *MockCommand) Execute(value any) ([]byte, error) { func (m *MockCommand) Execute(value any) ([]byte, error) {
return nil, nil return nil, nil

View File

@ -19,7 +19,7 @@ _ "strconv"
) )
func init() { func init() {
ResourceTypes.Register("route", func(u *url.URL) Resource { ResourceTypes.Register([]string{"route"}, func(u *url.URL) Resource {
n := NewNetworkRoute() n := NewNetworkRoute()
return n return n
}) })
@ -125,6 +125,7 @@ type NetworkRoute struct {
DeleteCommand *Command `yaml:"-" json:"-"` DeleteCommand *Command `yaml:"-" json:"-"`
State string `json:"state" yaml:"state"` State string `json:"state" yaml:"state"`
config ConfigurationValueGetter
} }
func NewNetworkRoute() *NetworkRoute { func NewNetworkRoute() *NetworkRoute {
@ -194,6 +195,10 @@ func (n *NetworkRoute) SetURI(uri string) error {
return nil return nil
} }
func (n *NetworkRoute) UseConfig(config ConfigurationValueGetter) {
n.config = config
}
func (n *NetworkRoute) Validate() error { func (n *NetworkRoute) Validate() error {
return fmt.Errorf("failed") return fmt.Errorf("failed")
} }

View File

@ -17,6 +17,7 @@ import (
"strings" "strings"
"gitea.rosskeen.house/rosskeen.house/machine" "gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec" "decl/internal/codec"
"decl/internal/command"
) )
type PackageType string type PackageType string
@ -31,58 +32,44 @@ const (
PackageTypeYum PackageType = "yum" PackageTypeYum PackageType = "yum"
) )
var SystemPackageType PackageType = FindSystemPackageType()
type Package struct { type Package struct {
stater machine.Stater `yaml:"-" json:"-"` stater machine.Stater `yaml:"-" json:"-"`
Source string `yaml:"source,omitempty" json:"source,omitempty"`
Name string `yaml:"name" json:"name"` Name string `yaml:"name" json:"name"`
Required string `json:"required,omitempty" yaml:"required,omitempty"` Required string `json:"required,omitempty" yaml:"required,omitempty"`
Version string `yaml:"version,omitempty" json:"version,omitempty"` Version string `yaml:"version,omitempty" json:"version,omitempty"`
PackageType PackageType `yaml:"type" json:"type"` PackageType PackageType `yaml:"type" json:"type"`
CreateCommand *Command `yaml:"-" json:"-"` CreateCommand *command.Command `yaml:"-" json:"-"`
ReadCommand *Command `yaml:"-" json:"-"` ReadCommand *command.Command `yaml:"-" json:"-"`
UpdateCommand *Command `yaml:"-" json:"-"` UpdateCommand *command.Command `yaml:"-" json:"-"`
DeleteCommand *Command `yaml:"-" json:"-"` DeleteCommand *command.Command `yaml:"-" json:"-"`
// state attributes // state attributes
State string `yaml:"state,omitempty" json:"state,omitempty"` State string `yaml:"state,omitempty" json:"state,omitempty"`
config ConfigurationValueGetter
} }
func init() { func init() {
ResourceTypes.Register("package", func(u *url.URL) Resource { ResourceTypes.Register([]string{"package", string(PackageTypeApk), string(PackageTypeApt), string(PackageTypeDeb), string(PackageTypeDnf), string(PackageTypeRpm), string(PackageTypePip), string(PackageTypeYum)}, func(u *url.URL) Resource {
p := NewPackage()
return p
})
ResourceTypes.Register(string(PackageTypeApk), func(u *url.URL) Resource {
p := NewPackage()
return p
})
ResourceTypes.Register(string(PackageTypeApt), func(u *url.URL) Resource {
p := NewPackage()
return p
})
ResourceTypes.Register(string(PackageTypeDeb), func(u *url.URL) Resource {
p := NewPackage()
return p
})
ResourceTypes.Register(string(PackageTypeDnf), func(u *url.URL) Resource {
p := NewPackage()
return p
})
ResourceTypes.Register(string(PackageTypeRpm), func(u *url.URL) Resource {
p := NewPackage()
return p
})
ResourceTypes.Register(string(PackageTypePip), func(u *url.URL) Resource {
p := NewPackage()
return p
})
ResourceTypes.Register(string(PackageTypeYum), func(u *url.URL) Resource {
p := NewPackage() p := NewPackage()
return p return p
}) })
} }
func FindSystemPackageType() PackageType {
for _, packageType := range []PackageType{PackageTypeApk, PackageTypeApt, PackageTypeDeb, PackageTypeDnf, PackageTypeRpm, PackageTypePip, PackageTypeYum} {
c := packageType.NewReadCommand()
if c.Exists() {
return packageType
}
}
return PackageTypeApk
}
func NewPackage() *Package { func NewPackage() *Package {
return &Package{ PackageType: PackageTypeApk } return &Package{ PackageType: SystemPackageType }
} }
func (p *Package) Clone() Resource { func (p *Package) Clone() Resource {
@ -106,17 +93,44 @@ func (p *Package) StateMachine() machine.Stater {
func (p *Package) Notify(m *machine.EventMessage) { func (p *Package) Notify(m *machine.EventMessage) {
ctx := context.Background() ctx := context.Background()
slog.Info("Notify()", "package", p, "m", m)
switch m.On { switch m.On {
case machine.ENTERSTATEEVENT: case machine.ENTERSTATEEVENT:
switch m.Dest { switch m.Dest {
case "start_read":
if _,readErr := p.Read(ctx); readErr == nil {
if triggerErr := p.StateMachine().Trigger("state_read"); triggerErr == nil {
return
} else {
p.State = "absent"
panic(triggerErr)
}
} else {
p.State = "absent"
panic(readErr)
}
case "start_create": case "start_create":
if e := p.Create(ctx); e == nil { if e := p.Create(ctx); e == nil {
if triggerErr := p.stater.Trigger("created"); triggerErr == nil { if triggerErr := p.StateMachine().Trigger("created"); triggerErr == nil {
return return
} }
} }
p.State = "absent" p.State = "absent"
case "present": case "start_delete":
if deleteErr := p.Delete(ctx); deleteErr == nil {
if triggerErr := p.StateMachine().Trigger("deleted"); triggerErr == nil {
return
} else {
p.State = "present"
panic(triggerErr)
}
} else {
p.State = "present"
panic(deleteErr)
}
case "absent":
p.State = "absent"
case "present", "created", "read":
p.State = "present" p.State = "present"
} }
case machine.EXITSTATEEVENT: case machine.EXITSTATEEVENT:
@ -147,6 +161,10 @@ func (p *Package) SetURI(uri string) error {
return e return e
} }
func (p *Package) UseConfig(config ConfigurationValueGetter) {
p.config = config
}
func (p *Package) JSON() ([]byte, error) { func (p *Package) JSON() ([]byte, error) {
return json.Marshal(p) return json.Marshal(p)
} }
@ -176,6 +194,16 @@ func (p *Package) Create(ctx context.Context) error {
return e return e
} }
func (p *Package) Delete(ctx context.Context) error {
_, err := p.DeleteCommand.Execute(p)
if err != nil {
return err
}
_,e := p.Read(ctx)
return e
}
func (p *Package) Apply() error { func (p *Package) Apply() error {
if p.Version == "latest" { if p.Version == "latest" {
p.Version = "" p.Version = ""
@ -228,22 +256,69 @@ func (p *Package) UnmarshalYAML(value *yaml.Node) error {
return nil return nil
} }
func (p *PackageType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) { func (p *PackageType) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) {
switch *p { switch *p {
case PackageTypeApk: case PackageTypeApk:
return NewApkCreateCommand(), NewApkReadCommand(), NewApkUpdateCommand(), NewApkDeleteCommand() return NewApkCreateCommand(), NewApkReadCommand(), NewApkUpdateCommand(), NewApkDeleteCommand()
case PackageTypeApt: case PackageTypeApt:
return NewAptCreateCommand(), NewAptReadCommand(), NewAptUpdateCommand(), NewAptDeleteCommand() return NewAptCreateCommand(), NewAptReadCommand(), NewAptUpdateCommand(), NewAptDeleteCommand()
case PackageTypeDeb: case PackageTypeDeb:
return NewDebCreateCommand(), NewDebReadCommand(), NewDebUpdateCommand(), NewDebDeleteCommand()
case PackageTypeDnf: case PackageTypeDnf:
return NewDnfCreateCommand(), NewDnfReadCommand(), NewDnfUpdateCommand(), NewDnfDeleteCommand()
case PackageTypeRpm: case PackageTypeRpm:
return NewRpmCreateCommand(), NewRpmReadCommand(), NewRpmUpdateCommand(), NewRpmDeleteCommand()
case PackageTypePip: case PackageTypePip:
return NewPipCreateCommand(), NewPipReadCommand(), NewPipUpdateCommand(), NewPipDeleteCommand()
case PackageTypeYum: case PackageTypeYum:
return NewYumCreateCommand(), NewYumReadCommand(), NewYumUpdateCommand(), NewYumDeleteCommand()
default: default:
} }
return nil, nil, nil, nil return nil, nil, nil, nil
} }
func (p *PackageType) NewReadCommand() (read *command.Command) {
switch *p {
case PackageTypeApk:
return NewApkReadCommand()
case PackageTypeApt:
return NewAptReadCommand()
case PackageTypeDeb:
return NewDebReadCommand()
case PackageTypeDnf:
return NewDnfReadCommand()
case PackageTypeRpm:
return NewRpmReadCommand()
case PackageTypePip:
return NewPipReadCommand()
case PackageTypeYum:
return NewYumReadCommand()
default:
}
return nil
}
func (p *PackageType) NewReadPackagesCommand() (read *command.Command) {
switch *p {
case PackageTypeApk:
return NewApkReadPackagesCommand()
case PackageTypeApt:
return NewAptReadPackagesCommand()
case PackageTypeDeb:
// return NewDebReadPackagesCommand()
case PackageTypeDnf:
// return NewDnfReadPackagesCommand()
case PackageTypeRpm:
// return NewRpmReadPackagesCommand()
case PackageTypePip:
// return NewPipReadPackagesCommand()
case PackageTypeYum:
// return NewYumReadPackagesCommand()
default:
}
return nil
}
func (p *PackageType) UnmarshalValue(value string) error { func (p *PackageType) UnmarshalValue(value string) error {
switch value { switch value {
case string(PackageTypeApk), string(PackageTypeApt), string(PackageTypeDeb), string(PackageTypeDnf), string(PackageTypeRpm), string(PackageTypePip), string(PackageTypeYum): case string(PackageTypeApk), string(PackageTypeApt), string(PackageTypeDeb), string(PackageTypeDnf), string(PackageTypeRpm), string(PackageTypePip), string(PackageTypeYum):
@ -270,23 +345,23 @@ func (p *PackageType) UnmarshalYAML(value *yaml.Node) error {
return p.UnmarshalValue(s) return p.UnmarshalValue(s)
} }
func NewApkCreateCommand() *Command { func NewApkCreateCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "apk" c.Path = "apk"
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("add"), command.CommandArg("add"),
CommandArg("{{ .Name }}{{ .Required }}"), command.CommandArg("{{ .Name }}{{ .Required }}"),
} }
return c return c
} }
func NewApkReadCommand() *Command { func NewApkReadCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "apk" c.Path = "apk"
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("info"), command.CommandArg("info"),
CommandArg("-ev"), command.CommandArg("-ev"),
CommandArg("{{ .Name }}"), command.CommandArg("{{ .Name }}"),
} }
c.Extractor = func(out []byte, target any) error { c.Extractor = func(out []byte, target any) error {
p := target.(*Package) p := target.(*Package)
@ -303,44 +378,82 @@ func NewApkReadCommand() *Command {
return c return c
} }
func NewApkUpdateCommand() *Command { func NewApkUpdateCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "apk" c.Path = "apk"
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("del"), command.CommandArg("add"),
CommandArg("{{ .Name }}"), command.CommandArg("{{ .Name }}{{ .Required }}"),
} }
return c return c
} }
func NewApkDeleteCommand() *Command { func NewApkDeleteCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "apk" c.Path = "apk"
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("del"), command.CommandArg("del"),
CommandArg("{{ .Name }}"), command.CommandArg("{{ .Name }}"),
} }
return c return c
} }
func NewAptCreateCommand() *Command { func NewApkReadPackagesCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "apk"
c.Args = []command.CommandArg{
command.CommandArg("list"),
command.CommandArg("--installed"),
}
c.Extractor = func(out []byte, target any) error {
Packages := target.(*[]*Package)
numberOfPackages := len(*Packages)
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
numberOfLines := len(lines)
diff := (numberOfLines - 1) - numberOfPackages
if diff > 0 {
for i := 0; i < diff; i++ {
*Packages = append(*Packages, NewPackage())
}
}
for lineIndex, line := range lines {
p := (*Packages)[lineIndex]
installedPackage := strings.Fields(strings.TrimSpace(line))
packageFields := strings.Split(installedPackage[0], "-")
numberOfFields := len(packageFields)
if numberOfFields > 2 {
packageName := strings.Join(packageFields[:numberOfFields - 3], "-")
packageVersion := strings.Join(packageFields[numberOfFields - 2:numberOfFields - 1], "-")
p.Name = packageName
p.State = "present"
p.Version = packageVersion
}
}
return nil
}
return c
}
func NewAptCreateCommand() *command.Command {
c := command.NewCommand()
c.Path = "apt-get" c.Path = "apt-get"
c.Split = false c.Split = false
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("satisfy"), command.CommandArg("satisfy"),
CommandArg("-y"), command.CommandArg("-y"),
CommandArg("{{ .Name }} ({{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }})"), command.CommandArg("{{ .Name }} ({{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }})"),
} }
return c return c
} }
func NewAptReadCommand() *Command { func NewAptReadCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "dpkg" c.Path = "dpkg"
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("-s"), command.CommandArg("-s"),
CommandArg("{{ .Name }}"), command.CommandArg("{{ .Name }}"),
} }
c.Extractor = func(out []byte, target any) error { c.Extractor = func(out []byte, target any) error {
p := target.(*Package) p := target.(*Package)
@ -379,22 +492,404 @@ func NewAptReadCommand() *Command {
return c return c
} }
func NewAptUpdateCommand() *Command { func NewAptUpdateCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "apt" c.Path = "apt-get"
c.Args = []CommandArg{ c.Split = false
CommandArg("install"), c.Args = []command.CommandArg{
CommandArg("{{ .Name }}"), command.CommandArg("satisfy"),
command.CommandArg("-y"),
command.CommandArg("{{ .Name }} ({{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }})"),
} }
return c return c
} }
func NewAptDeleteCommand() *Command { func NewAptDeleteCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "apt" c.Path = "apt"
c.Args = []CommandArg{ c.FailOnError = false
CommandArg("remove"), c.Args = []command.CommandArg{
CommandArg("{{ .Name }}"), command.CommandArg("remove"),
command.CommandArg("{{ .Name }}"),
}
return c
}
func NewAptReadPackagesCommand() *command.Command {
c := command.NewCommand()
c.Path = "apt"
c.FailOnError = false
c.Args = []command.CommandArg{
command.CommandArg("list"),
command.CommandArg("--installed"),
}
c.Env = []string{ "DEBIAN_FRONTEND=noninteractive" }
c.Extractor = func(out []byte, target any) error {
Packages := target.(*[]*Package)
numberOfPackages := len(*Packages)
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
numberOfLines := len(lines)
diff := (numberOfLines - 1) - numberOfPackages
if diff > 0 {
for i := 0; i < diff; i++ {
*Packages = append(*Packages, NewPackage())
}
}
for lineIndex, line := range lines[1:] {
p := (*Packages)[lineIndex]
installedPackage := strings.Fields(strings.TrimSpace(line))
packageFields := strings.Split(installedPackage[0], "/")
packageName := packageFields[0]
packageVersion := installedPackage[1]
p.Name = packageName
p.State = "present"
p.Version = packageVersion
}
return nil
}
return c
}
func NewDebCreateCommand() *command.Command {
c := command.NewCommand()
c.Path = "dpkg"
c.Split = false
c.Args = []command.CommandArg{
command.CommandArg("-i"),
command.CommandArg("{{ .Source }}"),
}
return c
}
func NewDebReadCommand() *command.Command {
c := command.NewCommand()
c.Path = "dpkg"
c.Args = []command.CommandArg{
command.CommandArg("-s"),
command.CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
p := target.(*Package)
slog.Info("Extract()", "out", out)
pkginfo := strings.Split(string(out), "\n")
for _, infofield := range pkginfo {
if len(infofield) > 0 && infofield[0] != ' ' {
fieldKeyValue := strings.SplitN(infofield, ":", 2)
if len(fieldKeyValue) > 1 {
key := strings.TrimSpace(fieldKeyValue[0])
value := strings.TrimSpace(fieldKeyValue[1])
switch key {
case "Package":
if value != p.Name {
p.State = "absent"
return nil
}
case "Status":
statusFields := strings.SplitN(value, " ", 3)
if len(statusFields) > 1 {
if statusFields[2] == "installed" {
p.State = "present"
} else {
p.State = "absent"
}
}
case "Version":
p.Version = value
}
}
}
}
slog.Info("Extract()", "package", p)
return nil
}
return c
}
func NewDebUpdateCommand() *command.Command {
c := command.NewCommand()
c.Path = "dpkg"
c.Args = []command.CommandArg{
command.CommandArg("-i"),
command.CommandArg("{{ .Source }}"),
}
return c
}
func NewDebDeleteCommand() *command.Command {
c := command.NewCommand()
c.Path = "dpkg"
c.Args = []command.CommandArg{
command.CommandArg("-r"),
command.CommandArg("{{ .Name }}"),
}
return c
}
func NewDnfCreateCommand() *command.Command {
c := command.NewCommand()
c.Path = "dnf"
c.Split = false
c.Args = []command.CommandArg{
command.CommandArg("install"),
command.CommandArg("-q"),
command.CommandArg("-y"),
command.CommandArg("{{ .Name }}{{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }}"),
}
return c
}
func NewDnfReadCommand() *command.Command {
c := command.NewCommand()
c.Path = "dnf"
c.Args = []command.CommandArg{
command.CommandArg("-q"),
command.CommandArg("list"),
command.CommandArg("installed"),
command.CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
p := target.(*Package)
slog.Info("Extract()", "out", out)
pkginfo := strings.Split(string(out), "\n")
for _, packageLines := range pkginfo {
fields := strings.Fields(packageLines)
packageNameField := strings.Split(fields[0], ".")
packageName := strings.TrimSpace(packageNameField[0])
//packageArch := strings.TrimSpace(packageNameField[1])
if packageName == p.Name {
p.State = "present"
packageVersionField := strings.Split(fields[1], ":")
//packageEpoch := strings.TrimSpace(packageVersionField[0])
packageVersion := strings.TrimSpace(packageVersionField[1])
p.Version = packageVersion
return nil
}
}
p.State = "absent"
slog.Info("Extract()", "package", p)
return nil
}
return c
}
func NewDnfUpdateCommand() *command.Command {
c := command.NewCommand()
c.Path = "dnf"
c.Args = []command.CommandArg{
command.CommandArg("-q"),
command.CommandArg("-y"),
command.CommandArg("install"),
command.CommandArg("{{ .Name }}"),
}
return c
}
func NewDnfDeleteCommand() *command.Command {
c := command.NewCommand()
c.Path = "dnf"
c.Args = []command.CommandArg{
command.CommandArg("-q"),
command.CommandArg("-y"),
command.CommandArg("remove"),
command.CommandArg("{{ .Name }}"),
}
return c
}
func NewRpmCreateCommand() *command.Command {
c := command.NewCommand()
c.Path = "rpm"
c.Split = false
c.Args = []command.CommandArg{
command.CommandArg("-i"),
command.CommandArg("{{ .Name }}"),
}
return c
}
func NewRpmReadCommand() *command.Command {
c := command.NewCommand()
c.Path = "rpm"
c.Args = []command.CommandArg{
command.CommandArg("-q"),
command.CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
p := target.(*Package)
slog.Info("Extract()", "out", out)
pkginfo := strings.Split(string(out), "\n")
for _, packageLine := range pkginfo {
packageFields := strings.Split(packageLine, "-")
numberOfFields := len(packageFields)
if numberOfFields > 2 {
packageName := strings.Join(packageFields[:numberOfFields - 3], "-")
packageVersion := strings.Join(packageFields[numberOfFields - 2:numberOfFields - 1], "-")
if packageName == p.Name {
p.State = "present"
p.Version = packageVersion
return nil
}
}
}
p.State = "absent"
slog.Info("Extract()", "package", p)
return nil
}
return c
}
func NewRpmUpdateCommand() *command.Command {
c := command.NewCommand()
c.Path = "rpm"
c.Args = []command.CommandArg{
command.CommandArg("-i"),
command.CommandArg("{{ .Name }}"),
}
return c
}
func NewRpmDeleteCommand() *command.Command {
c := command.NewCommand()
c.Path = "rpm"
c.Args = []command.CommandArg{
command.CommandArg("-e"),
command.CommandArg("{{ .Name }}"),
}
return c
}
func NewPipCreateCommand() *command.Command {
c := command.NewCommand()
c.Path = "pip"
c.Split = false
c.Args = []command.CommandArg{
command.CommandArg("install"),
command.CommandArg("{{ .Name }}{{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }}"),
}
return c
}
func NewPipReadCommand() *command.Command {
c := command.NewCommand()
c.Path = "pip"
c.Args = []command.CommandArg{
command.CommandArg("list"),
}
c.Extractor = func(out []byte, target any) error {
p := target.(*Package)
pkginfo := strings.Split(string(out), "\n")
for _, packageLine := range pkginfo[2:] {
packageFields := strings.Fields(packageLine)
numberOfFields := len(packageFields)
if numberOfFields == 2 {
packageName := packageFields[0]
packageVersion := packageFields[1]
if packageName == p.Name {
p.State = "present"
p.Version = packageVersion
return nil
}
}
}
p.State = "absent"
slog.Info("Extract()", "package", p)
return nil
}
return c
}
func NewPipUpdateCommand() *command.Command {
c := command.NewCommand()
c.Path = "pip"
c.Args = []command.CommandArg{
command.CommandArg("install"),
command.CommandArg("{{ .Name }}{{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }}"),
}
return c
}
func NewPipDeleteCommand() *command.Command {
c := command.NewCommand()
c.Path = "pip"
c.Args = []command.CommandArg{
command.CommandArg("uninstall"),
command.CommandArg("{{ .Name }}"),
}
return c
}
func NewYumCreateCommand() *command.Command {
c := command.NewCommand()
c.Path = "yum"
c.Split = false
c.Args = []command.CommandArg{
command.CommandArg("install"),
command.CommandArg("-q"),
command.CommandArg("-y"),
command.CommandArg("{{ .Name }}{{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }}"),
}
return c
}
func NewYumReadCommand() *command.Command {
c := command.NewCommand()
c.Path = "yum"
c.Args = []command.CommandArg{
command.CommandArg("-q"),
command.CommandArg("list"),
command.CommandArg("installed"),
command.CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
p := target.(*Package)
slog.Info("Extract()", "out", out)
pkginfo := strings.Split(string(out), "\n")
for _, packageLines := range pkginfo {
fields := strings.Fields(packageLines)
packageNameField := strings.Split(fields[0], ".")
packageName := strings.TrimSpace(packageNameField[0])
//packageArch := strings.TrimSpace(packageNameField[1])
if packageName == p.Name {
p.State = "present"
packageVersionField := strings.Split(fields[1], ":")
//packageEpoch := strings.TrimSpace(packageVersionField[0])
packageVersion := strings.TrimSpace(packageVersionField[1])
p.Version = packageVersion
return nil
}
}
p.State = "absent"
slog.Info("Extract()", "package", p)
return nil
}
return c
}
func NewYumUpdateCommand() *command.Command {
c := command.NewCommand()
c.Path = "yum"
c.Args = []command.CommandArg{
command.CommandArg("-q"),
command.CommandArg("-y"),
command.CommandArg("install"),
command.CommandArg("{{ .Name }}"),
}
return c
}
func NewYumDeleteCommand() *command.Command {
c := command.NewCommand()
c.Path = "yum"
c.Args = []command.CommandArg{
command.CommandArg("-q"),
command.CommandArg("-y"),
command.CommandArg("remove"),
command.CommandArg("{{ .Name }}"),
} }
return c return c
} }

View File

@ -16,6 +16,7 @@ import (
_ "os" _ "os"
_ "strings" _ "strings"
"testing" "testing"
"decl/internal/command"
) )
func TestNewPackageResource(t *testing.T) { func TestNewPackageResource(t *testing.T) {
@ -56,7 +57,7 @@ type: apk
assert.Nil(t, loadErr) assert.Nil(t, loadErr)
assert.Equal(t, "latest", p.Version) assert.Equal(t, "latest", p.Version)
p.ReadCommand = (*Command)(m) p.ReadCommand = (*command.Command)(m)
yaml, readErr := p.Read(context.Background()) yaml, readErr := p.Read(context.Background())
assert.Nil(t, readErr) assert.Nil(t, readErr)
assert.Greater(t, len(yaml), 0) assert.Greater(t, len(yaml), 0)
@ -106,3 +107,30 @@ func TestPackageSetURI(t *testing.T) {
assert.Equal(t, "package", p.Type()) assert.Equal(t, "package", p.Type())
assert.Equal(t, "12345_key", p.Name) assert.Equal(t, "12345_key", p.Name)
} }
func TestReadDebPackage(t *testing.T) {
decl := `
name: vim
source: vim-8.2.3995-1ubuntu2.17.deb
type: deb
`
p := NewPackage()
assert.NotNil(t, p)
loadErr := p.LoadDecl(decl)
assert.Nil(t, loadErr)
p.ReadCommand = NewDebReadCommand()
p.ReadCommand.Executor = func(value any) ([]byte, error) {
return []byte(`
Package: vim
Version: 1.2.2
`), nil
}
yaml, readErr := p.Read(context.Background())
assert.Nil(t, readErr)
slog.Info("Package.Read()", "package", p)
assert.Greater(t, len(yaml), 0)
slog.Info("read()", "yaml", yaml)
assert.Equal(t, "1.2.2", p.Version)
assert.Nil(t, p.Validate())
}

View File

@ -10,8 +10,11 @@ import (
_ "gopkg.in/yaml.v3" _ "gopkg.in/yaml.v3"
_ "net/url" _ "net/url"
"gitea.rosskeen.house/rosskeen.house/machine" "gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/transport"
) )
type ResourceReference string
type ResourceSelector func(r *Declaration) bool type ResourceSelector func(r *Declaration) bool
type Resource interface { type Resource interface {
@ -19,6 +22,7 @@ type Resource interface {
StateMachine() machine.Stater StateMachine() machine.Stater
URI() string URI() string
SetURI(string) error SetURI(string) error
UseConfig(config ConfigurationValueGetter)
ResolveId(context.Context) string ResolveId(context.Context) string
ResourceLoader ResourceLoader
StateTransformer StateTransformer
@ -27,6 +31,14 @@ type Resource interface {
Clone() Resource Clone() Resource
} }
type ContentReader interface {
ContentReaderStream() (*transport.Reader, error)
}
type ContentWriter interface {
ContentWriterStream() (*transport.Writer, error)
}
type ResourceValidator interface { type ResourceValidator interface {
Validate() error Validate() error
} }
@ -65,6 +77,14 @@ func NewResource(uri string) Resource {
return nil return nil
} }
func (r ResourceReference) ContentReaderStream() (*transport.Reader, error) {
return transport.NewReaderURI(string(r))
}
func (r ResourceReference) ContentWriterStream() (*transport.Writer, error) {
return transport.NewWriterURI(string(r))
}
func StorageMachine(sub machine.Subscriber) machine.Stater { func StorageMachine(sub machine.Subscriber) machine.Stater {
// start_destroy -> absent -> start_create -> present -> start_destroy // start_destroy -> absent -> start_create -> present -> start_destroy
stater := machine.New("unknown") stater := machine.New("unknown")

View File

@ -1,4 +1,5 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved. // Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource package resource
import ( import (

View File

@ -10,6 +10,10 @@
"description": "Resource type name.", "description": "Resource type name.",
"enum": [ "file" ] "enum": [ "file" ]
}, },
"config": {
"type": "string",
"description": "Config name"
},
"attributes": { "attributes": {
"$ref": "file.jsonschema" "$ref": "file.jsonschema"
} }

View File

@ -38,6 +38,10 @@
"type": "string", "type": "string",
"description": "file content" "description": "file content"
}, },
"sourceref": {
"type": "string",
"description": "file content source uri"
},
"target": { "target": {
"type": "string", "type": "string",
"description": "Symbolic link target path" "description": "Symbolic link target path"

View File

@ -10,6 +10,10 @@
"description": "Resource type name.", "description": "Resource type name.",
"enum": [ "http" ] "enum": [ "http" ]
}, },
"config": {
"type": "string",
"description": "Config name."
},
"attributes": { "attributes": {
"$ref": "http.jsonschema" "$ref": "http.jsonschema"
} }

View File

@ -10,6 +10,10 @@
"description": "Resource type name.", "description": "Resource type name.",
"enum": [ "iptable" ] "enum": [ "iptable" ]
}, },
"config": {
"type": "string",
"description": "Config name"
},
"attributes": { "attributes": {
"$ref": "iptable.jsonschema" "$ref": "iptable.jsonschema"
} }

View File

@ -0,0 +1,252 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
// Service resource
package resource
import (
"context"
"fmt"
_ "log/slog"
"net/url"
"path/filepath"
"io"
"gopkg.in/yaml.v3"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
"encoding/json"
"strings"
)
type ServiceManagerType string
const (
ServiceManagerTypeSystemd ServiceManagerType = "systemd"
ServiceManagerTypeSysV ServiceManagerType = "sysv"
)
type Service struct {
stater machine.Stater `yaml:"-" json:"-"`
Name string `json:"name" yaml:"name"`
ServiceManagerType ServiceManagerType `json:"servicemanager,omitempty" yaml:"servicemanager,omitempty"`
CreateCommand *Command `yaml:"-" json:"-"`
ReadCommand *Command `yaml:"-" json:"-"`
UpdateCommand *Command `yaml:"-" json:"-"`
DeleteCommand *Command `yaml:"-" json:"-"`
State string `yaml:"state,omitempty" json:"state,omitempty"`
config ConfigurationValueGetter
}
func init() {
ResourceTypes.Register([]string{"service"}, func(u *url.URL) Resource {
s := NewService()
s.Name = filepath.Join(u.Hostname(), u.Path)
s.CreateCommand, s.ReadCommand, s.UpdateCommand, s.DeleteCommand = s.ServiceManagerType.NewCRUD()
return s
})
}
func NewService() *Service {
return &Service{ ServiceManagerType: ServiceManagerTypeSystemd }
}
func (s *Service) StateMachine() machine.Stater {
if s.stater == nil {
s.stater = ProcessMachine(s)
}
return s.stater
}
func (s *Service) Notify(m *machine.EventMessage) {
ctx := context.Background()
switch m.On {
case machine.ENTERSTATEEVENT:
switch m.Dest {
case "start_create":
if e := s.Create(ctx); e == nil {
if triggerErr := s.stater.Trigger("created"); triggerErr == nil {
return
}
}
s.State = "absent"
case "created":
s.State = "present"
case "running":
s.State = "running"
}
case machine.EXITSTATEEVENT:
}
}
func (s *Service) URI() string {
return fmt.Sprintf("service://%s", s.Name)
}
func (s *Service) SetURI(uri string) error {
resourceUri, e := url.Parse(uri)
if e == nil {
if resourceUri.Scheme == s.Type() {
s.Name = filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI())
} else {
e = fmt.Errorf("%w: %s is not a %s", ErrInvalidResourceURI, uri, s.Type())
}
}
return e
}
func (s *Service) UseConfig(config ConfigurationValueGetter) {
s.config = config
}
func (s *Service) JSON() ([]byte, error) {
return json.Marshal(s)
}
func (s *Service) Validate() error {
return nil
}
func (s *Service) Clone() Resource {
news := &Service{
Name: s.Name,
ServiceManagerType: s.ServiceManagerType,
}
news.CreateCommand, news.ReadCommand, news.UpdateCommand, news.DeleteCommand = s.ServiceManagerType.NewCRUD()
return news
}
func (s *Service) Apply() error {
return nil
}
func (s *Service) Load(r io.Reader) error {
return codec.NewYAMLDecoder(r).Decode(s)
}
func (s *Service) LoadDecl(yamlResourceDeclaration string) error {
return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(s)
}
func (s *Service) UnmarshalJSON(data []byte) error {
if unmarshalErr := json.Unmarshal(data, s); unmarshalErr != nil {
return unmarshalErr
}
s.CreateCommand, s.ReadCommand, s.UpdateCommand, s.DeleteCommand = s.ServiceManagerType.NewCRUD()
return nil
}
func (s *Service) UnmarshalYAML(value *yaml.Node) error {
type decodeService Service
if unmarshalErr := value.Decode((*decodeService)(s)); unmarshalErr != nil {
return unmarshalErr
}
s.CreateCommand, s.ReadCommand, s.UpdateCommand, s.DeleteCommand = s.ServiceManagerType.NewCRUD()
return nil
}
func (s *ServiceManagerType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) {
switch *s {
case ServiceManagerTypeSystemd:
return NewSystemdCreateCommand(), NewSystemdReadCommand(), NewSystemdUpdateCommand(), NewSystemdDeleteCommand()
case ServiceManagerTypeSysV:
return NewSysVCreateCommand(), NewSysVReadCommand(), NewSysVUpdateCommand(), NewSysVDeleteCommand()
default:
}
return nil, nil, nil, nil
}
func (s *Service) Create(ctx context.Context) error {
return nil
}
func (s *Service) Read(ctx context.Context) ([]byte, error) {
return yaml.Marshal(s)
}
func (s *Service) Delete(ctx context.Context) error {
return nil
}
func (s *Service) Type() string { return "service" }
func (s *Service) ResolveId(ctx context.Context) string {
return ""
}
func NewSystemdCreateCommand() *Command {
c := NewCommand()
c.Path = "systemctl"
c.Args = []CommandArg{
CommandArg("enable"),
CommandArg("{{ .Name }}"),
}
return c
}
func NewSystemdReadCommand() *Command {
c := NewCommand()
c.Path = "systemctl"
c.Args = []CommandArg{
CommandArg("show"),
CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
s := target.(*Service)
serviceStatus := strings.Split(string(out), "\n")
for _, statusLine := range(serviceStatus) {
if len(statusLine) > 1 {
statusKeyValue := strings.Split(statusLine, "=")
key := statusKeyValue[0]
value := strings.TrimSpace(strings.Join(statusKeyValue[1:], "="))
switch key {
case "Id":
case "ActiveState":
switch value {
case "active":
if stateCreatedErr := s.stater.Trigger("created"); stateCreatedErr != nil {
return stateCreatedErr
}
case "inactive":
}
case "SubState":
switch value {
case "running":
if stateRunningErr := s.stater.Trigger("running"); stateRunningErr != nil {
return stateRunningErr
}
case "dead":
}
}
}
}
return nil
}
return c
}
func NewSystemdUpdateCommand() *Command {
return nil
}
func NewSystemdDeleteCommand() *Command {
return nil
}
func NewSysVCreateCommand() *Command {
return nil
}
func NewSysVReadCommand() *Command {
return nil
}
func NewSysVUpdateCommand() *Command {
return nil
}
func NewSysVDeleteCommand() *Command {
return nil
}

View File

@ -0,0 +1,36 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
"context"
_ "decl/tests/mocks"
_ "fmt"
"github.com/stretchr/testify/assert"
"testing"
)
func TestNewServiceResource(t *testing.T) {
c := NewService()
assert.NotNil(t, c)
}
func TestUriServiceResource(t *testing.T) {
c := NewService()
assert.Nil(t, c.SetURI("service://ssh"))
assert.Equal(t, "ssh", c.Name)
}
func TestReadServiceResource(t *testing.T) {
yamlResult := `
name: "ssh"
servicemanager: "systemd"
state: "present"
`
c := NewService()
c.Name = "ssh"
c.State = "present"
yamlData, err := c.Read(context.Background())
assert.Nil(t, err)
assert.YAMLEq(t, yamlResult, string(yamlData))
}

View File

@ -5,50 +5,18 @@ package resource
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/url" _ "net/url"
"strings" "strings"
"decl/internal/types"
) )
var ( var (
ErrUnknownResourceType = errors.New("Unknown resource type") ErrUnknownResourceType = errors.New("Unknown resource type")
ResourceTypes *Types = NewTypes() ResourceTypes *types.Types[Resource] = types.New[Resource]()
) )
type TypeName string //`json:"type"` type TypeName string //`json:"type"`
type TypeFactory func(*url.URL) Resource
type Types struct {
registry map[string]TypeFactory
}
func NewTypes() *Types {
return &Types{registry: make(map[string]TypeFactory)}
}
func (t *Types) Register(name string, factory TypeFactory) {
t.registry[name] = factory
}
func (t *Types) New(uri string) (Resource, error) {
u, e := url.Parse(uri)
if u == nil || e != nil {
return nil, fmt.Errorf("%w: %s - uri %s", ErrUnknownResourceType, e, uri)
}
if r, ok := t.registry[u.Scheme]; ok {
return r(u), nil
}
return nil, fmt.Errorf("%w: %s", ErrUnknownResourceType, u.Scheme)
}
func (t *Types) Has(typename string) bool {
if _, ok := t.registry[typename]; ok {
return true
}
return false
}
func (n *TypeName) UnmarshalJSON(b []byte) error { func (n *TypeName) UnmarshalJSON(b []byte) error {
ResourceTypeName := strings.Trim(string(b), "\"") ResourceTypeName := strings.Trim(string(b), "\"")
if ResourceTypes.Has(ResourceTypeName) { if ResourceTypes.Has(ResourceTypeName) {

View File

@ -1,64 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
_ "context"
"encoding/json"
"github.com/stretchr/testify/assert"
"net/url"
"testing"
)
func TestNewResourceTypes(t *testing.T) {
resourceTypes := NewTypes()
assert.NotEqual(t, nil, resourceTypes)
}
func TestNewResourceTypesRegister(t *testing.T) {
m := NewFooResource()
resourceTypes := NewTypes()
assert.NotEqual(t, nil, resourceTypes)
resourceTypes.Register("foo", func(*url.URL) Resource { return m })
r, e := resourceTypes.New("foo://")
assert.Equal(t, nil, e)
assert.Equal(t, m, r)
}
func TestResourceTypesFromURI(t *testing.T) {
m := NewFooResource()
resourceTypes := NewTypes()
assert.NotEqual(t, nil, resourceTypes)
resourceTypes.Register("foo", func(*url.URL) Resource { return m })
r, e := resourceTypes.New("foo://bar")
assert.Equal(t, nil, e)
assert.Equal(t, m, r)
}
func TestResourceTypesHasType(t *testing.T) {
m := NewFooResource()
resourceTypes := NewTypes()
assert.NotNil(t, resourceTypes)
resourceTypes.Register("foo", func(*url.URL) Resource { return m })
assert.True(t, resourceTypes.Has("foo"))
}
func TestResourceTypeName(t *testing.T) {
type fooResourceName struct {
Name TypeName `json:"type"`
}
fooTypeName := &fooResourceName{}
jsonType := `{ "type": "file" }`
e := json.Unmarshal([]byte(jsonType), &fooTypeName)
assert.Nil(t, e)
assert.Equal(t, "file", string(fooTypeName.Name))
}

View File

@ -45,6 +45,7 @@ type User struct {
UpdateCommand *Command `json:"-" yaml:"-"` UpdateCommand *Command `json:"-" yaml:"-"`
DeleteCommand *Command `json:"-" yaml:"-"` DeleteCommand *Command `json:"-" yaml:"-"`
State string `json:"state,omitempty" yaml:"state,omitempty"` State string `json:"state,omitempty" yaml:"state,omitempty"`
config ConfigurationValueGetter
} }
func NewUser() *User { func NewUser() *User {
@ -52,7 +53,7 @@ func NewUser() *User {
} }
func init() { func init() {
ResourceTypes.Register("user", func(u *url.URL) Resource { ResourceTypes.Register([]string{"user"}, func(u *url.URL) Resource {
user := NewUser() user := NewUser()
user.Name = u.Hostname() user.Name = u.Hostname()
user.UID = LookupUIDString(u.Hostname()) user.UID = LookupUIDString(u.Hostname())
@ -126,6 +127,10 @@ func (u *User) URI() string {
return fmt.Sprintf("user://%s", u.Name) return fmt.Sprintf("user://%s", u.Name)
} }
func (u *User) UseConfig(config ConfigurationValueGetter) {
u.config = config
}
func (u *User) ResolveId(ctx context.Context) string { func (u *User) ResolveId(ctx context.Context) string {
return LookupUIDString(u.Name) return LookupUIDString(u.Name)
} }

View File

@ -5,14 +5,14 @@ package source
import ( import (
"context" "context"
_ "encoding/json" _ "encoding/json"
_ "fmt" "fmt"
_ "gopkg.in/yaml.v3" _ "gopkg.in/yaml.v3"
"net/url" "net/url"
_ "path/filepath" _ "path/filepath"
"decl/internal/resource" "decl/internal/resource"
_ "os" _ "os"
_ "io" _ "io"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"log/slog" "log/slog"
) )
@ -46,9 +46,10 @@ func init() {
func (c *Container) Type() string { return "container" } func (c *Container) Type() string { return "container" }
func (c *Container) ExtractResources(filter ResourceSelector) ([]*resource.Document, error) { func (c *Container) ExtractResources(filter ResourceSelector) ([]*resource.Document, error) {
var extractErr error
ctx := context.Background() ctx := context.Background()
slog.Info("container source ExtractResources()", "container", c) slog.Info("container source ExtractResources()", "container", c)
containers, err := c.apiClient.ContainerList(ctx, types.ContainerListOptions{All: true}) containers, err := c.apiClient.ContainerList(ctx, container.ListOptions{All: true})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -56,9 +57,11 @@ func (c *Container) ExtractResources(filter ResourceSelector) ([]*resource.Docum
document := resource.NewDocument() document := resource.NewDocument()
for _, container := range containers { for _, container := range containers {
runningContainer := resource.NewContainer(nil) runningContainer := resource.NewContainer(nil)
runningContainer.Inspect(ctx, container.ID) if inspectErr := runningContainer.Inspect(ctx, container.ID); inspectErr != nil {
extractErr = fmt.Errorf("%w: %w", extractErr, inspectErr)
}
document.AddResourceDeclaration("container", runningContainer) document.AddResourceDeclaration("container", runningContainer)
} }
return []*resource.Document{document}, nil return []*resource.Document{document}, extractErr
} }

View File

@ -5,83 +5,19 @@ package source
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/url" _ "net/url"
"strings" "strings"
"path/filepath" _ "path/filepath"
"decl/internal/types"
) )
var ( var (
ErrUnknownSourceType = errors.New("Unknown source type") ErrUnknownSourceType = errors.New("Unknown source type")
SourceTypes *Types = NewTypes() SourceTypes *types.Types[DocSource] = types.New[DocSource]()
) )
type TypeName string //`json:"type"` type TypeName string //`json:"type"`
type TypeFactory func(*url.URL) DocSource
type Types struct {
registry map[string]TypeFactory
}
func NewTypes() *Types {
return &Types{registry: make(map[string]TypeFactory)}
}
func (t *Types) Register(names []string, factory TypeFactory) {
for _,name := range names {
t.registry[name] = factory
}
}
func (t *Types) FromExtension(path string) (TypeFactory, error) {
elements := strings.Split(path, ".")
numberOfElements := len(elements)
if numberOfElements > 2 {
if src := t.Get(strings.Join(elements[numberOfElements - 2: numberOfElements - 1], ".")); src != nil {
return src, nil
}
}
if src := t.Get(elements[numberOfElements - 1]); src != nil {
return src, nil
}
return nil, fmt.Errorf("%w: %s", ErrUnknownSourceType, path)
}
func (t *Types) New(uri string) (DocSource, error) {
u, e := url.Parse(uri)
if u == nil || e != nil {
return nil, fmt.Errorf("%w: %s", ErrUnknownSourceType, e)
}
if u.Scheme == "" {
u.Scheme = "file"
}
path := filepath.Join(u.Hostname(), u.Path)
if d, lookupErr := t.FromExtension(path); d != nil {
return d(u), lookupErr
}
if r, ok := t.registry[u.Scheme]; ok {
return r(u), nil
}
return nil, fmt.Errorf("%w: %s", ErrUnknownSourceType, u.Scheme)
}
func (t *Types) Has(typename string) bool {
if _, ok := t.registry[typename]; ok {
return true
}
return false
}
func (t *Types) Get(typename string) TypeFactory {
if d, ok := t.registry[typename]; ok {
return d
}
return nil
}
func (n *TypeName) UnmarshalJSON(b []byte) error { func (n *TypeName) UnmarshalJSON(b []byte) error {
SourceTypeName := strings.Trim(string(b), "\"") SourceTypeName := strings.Trim(string(b), "\"")
if SourceTypes.Has(SourceTypeName) { if SourceTypes.Has(SourceTypeName) {

View File

@ -33,48 +33,6 @@ func NewFileDocSource() DocSource {
} }
} }
func TestNewSourceTypes(t *testing.T) {
sourceTypes := NewTypes()
assert.NotNil(t, sourceTypes)
}
func TestNewSourceTypesRegister(t *testing.T) {
m := NewFooDocSource()
sourceTypes := NewTypes()
assert.NotNil(t, sourceTypes)
sourceTypes.Register([]string{"foo"}, func(*url.URL) DocSource { return m })
r, e := sourceTypes.New("foo://")
assert.Nil(t, e)
assert.Equal(t, m, r)
}
func TestResourceTypesFromURI(t *testing.T) {
m := NewFooDocSource()
sourceTypes := NewTypes()
assert.NotNil(t, sourceTypes)
sourceTypes.Register([]string{"foo"}, func(*url.URL) DocSource { return m })
r, e := sourceTypes.New("foo://bar")
assert.Nil(t, e)
assert.Equal(t, m, r)
}
func TestResourceTypesHasType(t *testing.T) {
m := NewFooDocSource()
sourceTypes := NewTypes()
assert.NotNil(t, sourceTypes)
sourceTypes.Register([]string{"foo"}, func(*url.URL) DocSource { return m })
assert.True(t, sourceTypes.Has("foo"))
}
func TestDocSourceTypeName(t *testing.T) { func TestDocSourceTypeName(t *testing.T) {
SourceTypes.Register([]string{"file"}, func(*url.URL) DocSource { return NewFileDocSource() }) SourceTypes.Register([]string{"file"}, func(*url.URL) DocSource { return NewFileDocSource() })

View File

@ -37,7 +37,7 @@ func NewFile(u *url.URL) (f *File, err error) {
f.readHandle = os.Stdin f.readHandle = os.Stdin
f.writeHandle = os.Stdout f.writeHandle = os.Stdout
} else { } else {
if f.readHandle, err = os.Open(f.Path()); err != nil { if f.readHandle, err = os.OpenFile(f.Path(), os.O_RDWR|os.O_CREATE, 0644); err != nil {
return return
} }
f.writeHandle = f.readHandle f.writeHandle = f.readHandle

View File

@ -49,6 +49,9 @@ func NewHTTP(u *url.URL, ctx context.Context) (h *HTTP, err error) {
h.extension() h.extension()
h.postRequest, err = http.NewRequestWithContext(ctx, "POST", u.String(), h.buffer) h.postRequest, err = http.NewRequestWithContext(ctx, "POST", u.String(), h.buffer)
if err != nil {
return
}
h.getRequest, err = http.NewRequestWithContext(ctx, "GET", u.String(), nil) h.getRequest, err = http.NewRequestWithContext(ctx, "GET", u.String(), nil)
return return
} }
@ -73,7 +76,7 @@ func (h *HTTP) Path() string {
func (h *HTTP) Signature() (documentSignature string) { func (h *HTTP) Signature() (documentSignature string) {
if h.getResponse != nil { if h.getResponse != nil {
documentSignature := h.getResponse.Header.Get("Signature") documentSignature = h.getResponse.Header.Get("Signature")
if documentSignature == "" { if documentSignature == "" {
signatureResp, signatureErr := h.Client.Get(fmt.Sprintf("%s.sig", h.uri.String())) signatureResp, signatureErr := h.Client.Get(fmt.Sprintf("%s.sig", h.uri.String()))
if signatureErr == nil { if signatureErr == nil {
@ -113,7 +116,7 @@ func (h *HTTP) Reader() io.ReadCloser {
func (h *HTTP) Writer() io.WriteCloser { func (h *HTTP) Writer() io.WriteCloser {
var err error var err error
if h.postResponse, err = h.Client.Do(h.postRequest); err != nil { if h.postResponse, err = h.Client.Do(h.postRequest); err != nil {
h.postResponse, err = h.Client.Do(h.postRequest) panic(err)
} }
return h.buffer return h.buffer
} }

View File

@ -44,6 +44,14 @@ func NewReader(u *url.URL) (reader *Reader, e error) {
return return
} }
func NewReaderURI(uri string) (reader *Reader, e error) {
var u *url.URL
if u, e = url.Parse(uri); e == nil {
return NewReader(u)
}
return
}
type Writer struct { type Writer struct {
uri *url.URL uri *url.URL
handle Handler handle Handler
@ -65,6 +73,14 @@ func NewWriter(u *url.URL) (writer *Writer, e error) {
return writer, e return writer, e
} }
func NewWriterURI(uri string) (writer *Writer, e error) {
var u *url.URL
if u, e = url.Parse(uri); e == nil {
return NewWriter(u)
}
return
}
func (r *Reader) Read(b []byte) (int, error) { func (r *Reader) Read(b []byte) (int, error) {
return r.stream.Read(b) return r.stream.Read(b)
} }