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:
go clean -modcache
rm jx
lint:
golangci-lint run --verbose ./...

View File

@ -14,12 +14,29 @@ import (
"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) {
if _, e := os.Stat("./jx"); errors.Is(e, os.ErrNotExist) {
t.Skip("cli not built")
}
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.NotEqual(t, "", string(yaml))
assert.Greater(t, len(yaml), 0)
@ -47,7 +64,38 @@ resources:
defer ts.Close()
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.NotEqual(t, "", string(yaml))
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 (
"context"
"io"
"os"
"flag"
"log/slog"
_ "errors"
"fmt"
_ "gopkg.in/yaml.v3"
"decl/internal/codec"
"decl/internal/config"
"decl/internal/resource"
"decl/internal/source"
"decl/internal/target"
"decl/internal/codec"
_ "errors"
"flag"
"fmt"
_ "gopkg.in/yaml.v3"
"io"
"log/slog"
"net/url"
"os"
)
const (
@ -38,6 +39,9 @@ var ImportResource *string
var ApplyDelete *bool
var ConfigPath string
var ConfigDoc *config.Document = config.NewDocument()
var ctx context.Context = context.Background()
@ -48,7 +52,7 @@ type SubCommand struct {
Run RunCommand
}
var jxSubCommands = []SubCommand {
var jxSubCommands = []SubCommand{
{
Name: "diff",
Run: DiffSubCommand,
@ -61,6 +65,10 @@ var jxSubCommands = []SubCommand {
Name: "import",
Run: ImportSubCommand,
},
{
Name: "config",
Run: ConfigSubCommand,
},
}
func VersionUsage() {
@ -74,19 +82,36 @@ func LoggerConfig() {
var programLevel = new(slog.LevelVar)
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel}))
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)
} else {
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 {
slog.Info("loading ", "uri", uri)
if uri != "" {
ds, err := source.SourceTypes.New(uri)
if err != nil {
slog.Error("Failed loading document from source", "error", err)
return nil
}
extractDocuments, extractErr := ds.ExtractResources(nil)
if extractErr != nil {
@ -94,7 +119,32 @@ func LoadSourceURI(uri string) []*resource.Document {
}
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) {
@ -105,23 +155,29 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
return e
}
if ConfigPath != "" {
for _, argConfigDoc := range LoadConfigURI(ConfigPath) {
ConfigDoc.Append(argConfigDoc)
}
}
merged := resource.NewDocument()
documents := make([]*resource.Document, 0, 100)
for _,source := range cmd.Args() {
for _, source := range cmd.Args() {
loaded := LoadSourceURI(source)
if loaded != nil {
documents = append(documents, loaded...)
}
}
/*
/*
switch *GlobalOformat {
case FormatYaml:
encoder = resource.NewYAMLEncoder(output)
case FormatJson:
encoder = resource.NewJSONEncoder(output)
}
*/
*/
slog.Info("main.ImportResource", "args", os.Args, "output", 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())
}
for _,d := range documents {
for _, d := range documents {
if d != nil {
if *ImportResource != "" {
@ -153,7 +209,7 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
if *GlobalQuiet {
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
}
}
@ -182,17 +238,27 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
if e := cmd.Parse(os.Args[2:]); e != nil {
return e
}
if ConfigPath != "" {
for _, argConfigDoc := range LoadConfigURI(ConfigPath) {
ConfigDoc.Append(argConfigDoc)
}
}
var encoder codec.Encoder
documents := make([]*resource.Document, 0, 100)
for _,source := range cmd.Args() {
for _, source := range cmd.Args() {
loaded := LoadSourceURI(source)
if loaded != nil {
documents = append(documents, loaded...)
}
}
slog.Info("main.Apply()", "documents", documents)
for _,d := range documents {
slog.Info("main.Apply()", "documents", documents, "configdoc", ConfigDoc)
for _, d := range documents {
d.SetConfig(ConfigDoc)
slog.Info("main.Apply()", "doc", d)
var overrideState string = ""
if *ApplyDelete {
@ -212,7 +278,7 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
}
if *GlobalQuiet {
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
}
}
@ -242,7 +308,7 @@ func DiffSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
for i, doc := range rightDocuments {
if doc != nil {
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 {
slog.Info("jx diff ", "err", e)
//return e
@ -262,20 +328,20 @@ func DiffSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
break
}
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
}
index++
continue
}
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
}
index++
continue
}
if _,e := leftDocuments[index].Diff(rightDocuments[index], output); e != nil {
if _, e := leftDocuments[index].Diff(rightDocuments[index], output); e != nil {
return e
}
index++
@ -292,8 +358,21 @@ func main() {
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.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")
cmdFlagSet.StringVar(&GlobalOutput, "output", "-", "Output target (default stdout)")
cmdFlagSet.StringVar(&GlobalOutput, "o", "-", "Output target (default stdout)")
@ -318,7 +397,14 @@ func main() {
cmdFlagSet.PrintDefaults()
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 e := subCmd.Run(cmdFlagSet, os.Stdout); e != nil {
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
go 1.22.1
go 1.22.3
require (
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"`
config ConfigurationValueGetter
apiClient ContainerClient
}
func init() {
ResourceTypes.Register("container", func(u *url.URL) Resource {
ResourceTypes.Register([]string{"container"}, func(u *url.URL) Resource {
c := NewContainer(nil)
c.Name = filepath.Join(u.Hostname(), u.Path)
return c
@ -158,6 +159,10 @@ func (c *Container) SetURI(uri string) error {
return e
}
func (c *Container) UseConfig(config ConfigurationValueGetter) {
c.config = config
}
func (c *Container) JSON() ([]byte, error) {
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)
if client.IsErrNotFound(err) {
c.State = "absent"
@ -328,8 +341,12 @@ func (c *Container) Read(ctx context.Context) ([]byte, error) {
c.State = "present"
c.Id = containerJSON.ID
if c.Name == "" {
if containerJSON.Name[0] == '/' {
c.Name = containerJSON.Name[1:]
} else {
c.Name = containerJSON.Name
}
}
c.Path = containerJSON.Path
c.Image = containerJSON.Image
if containerJSON.State != nil {
@ -343,8 +360,7 @@ func (c *Container) Read(ctx context.Context) ([]byte, error) {
c.RestartCount = containerJSON.RestartCount
c.Driver = containerJSON.Driver
}
slog.Info("Read() ", "type", c.Type(), "name", c.Name, "Id", c.Id)
return yaml.Marshal(c)
return nil
}
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"`
State string `yaml:"state,omitempty" json:"state,omitempty"`
config ConfigurationValueGetter
apiClient ContainerImageClient
}
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.Name = strings.Join([]string{u.Hostname(), u.Path}, ":")
return c
@ -122,6 +123,10 @@ func (c *ContainerImage) SetURI(uri string) error {
return e
}
func (c *ContainerImage) UseConfig(config ConfigurationValueGetter) {
c.config = config
}
func (c *ContainerImage) JSON() ([]byte, error) {
return json.Marshal(c)
}

View File

@ -38,11 +38,12 @@ type ContainerNetwork struct {
State string `yaml:"state"`
config ConfigurationValueGetter
apiClient ContainerNetworkClient
}
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.Name = filepath.Join(u.Hostname(), u.Path)
return n
@ -115,6 +116,10 @@ func (n *ContainerNetwork) SetURI(uri string) error {
return e
}
func (n *ContainerNetwork) UseConfig(config ConfigurationValueGetter) {
n.config = config
}
func (n *ContainerNetwork) JSON() ([]byte, error) {
return json.Marshal(n)
}

View File

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

View File

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

View File

@ -12,15 +12,25 @@ _ "net/url"
"github.com/sters/yaml-diff/yamldiff"
"strings"
"decl/internal/codec"
"decl/internal/types"
"decl/internal/config"
"context"
)
type ResourceMap[Value any] map[string]Value
type Document struct {
uris ResourceMap[*Declaration]
ResourceDecls []Declaration `json:"resources" yaml:"resources"`
config *config.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 {
@ -34,18 +44,35 @@ func (d *Document) Filter(filter ResourceSelector) []*Declaration {
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 {
clone := NewDocument()
clone.config = d.config
clone.ResourceDecls = make([]Declaration, len(d.ResourceDecls))
for i, res := range d.ResourceDecls {
clone.ResourceDecls[i] = *res.Clone()
clone.ResourceDecls[i].SetDocument(clone)
clone.ResourceDecls[i].SetConfig(d.config)
}
return clone
}
func (d *Document) Load(r io.Reader) error {
func (d *Document) Load(r io.Reader) (err error) {
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 {
@ -68,6 +95,14 @@ func (d *Document) Validate() error {
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 {
return d.ResourceDecls
}
@ -95,6 +130,7 @@ func (d *Document) Apply(state string) error {
if state != "" {
d.ResourceDecls[idx].Transition = state
}
d.ResourceDecls[idx].SetConfig(d.config)
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)
return e
@ -117,11 +153,17 @@ func (d *Document) Generate(w io.Writer) error {
return err
}
func (d *Document) MapResourceURI(uri string, declaration *Declaration) {
d.uris[uri] = declaration
}
func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration Resource) {
decl := NewDeclaration()
decl.Type = TypeName(resourceType)
decl.Attributes = resourceDeclaration
decl.SetDocument(d)
d.ResourceDecls = append(d.ResourceDecls, *decl)
d.MapResourceURI(decl.Attributes.URI(), decl)
}
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 {
return e
}
decl.SetDocument(d)
d.ResourceDecls = append(d.ResourceDecls, *decl)
d.MapResourceURI(decl.Attributes.URI(), decl)
return nil
}

View File

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

View File

@ -20,6 +20,8 @@ import (
"crypto/sha256"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
"decl/internal/iofilter"
"strings"
)
type FileType string
@ -41,7 +43,7 @@ var ErrInvalidFileOwner error = errors.New("Unknown User")
var ErrInvalidFileGroup error = errors.New("Unknown Group")
func init() {
ResourceTypes.Register("file", func(u *url.URL) Resource {
ResourceTypes.Register([]string{"file"}, func(u *url.URL) Resource {
f := NewFile()
f.Path = filepath.Join(u.Hostname(), u.Path)
return f
@ -62,11 +64,14 @@ type File struct {
Mtime time.Time `json:"mtime,omitempty" yaml:"mtime,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"`
Size int64 `json:"size,omitempty" yaml:"size,omitempty"`
Target string `json:"target,omitempty" yaml:"target,omitempty"`
FileType FileType `json:"filetype" yaml:"filetype"`
State string `json:"state,omitempty" yaml:"state,omitempty"`
SerializeContent bool `json:"serializecontent,omitempty" yaml:"serializecontent,omitempty"`
config ConfigurationValueGetter
}
type ResourceFileInfo struct {
@ -76,7 +81,7 @@ type ResourceFileInfo struct {
func NewFile() *File {
currentUser, _ := user.Current()
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)
return f
}
@ -178,6 +183,10 @@ func (f *File) SetURI(uri string) error {
return e
}
func (f *File) UseConfig(config ConfigurationValueGetter) {
f.config = config
}
func (f *File) Validate() error {
return fmt.Errorf("failed")
}
@ -211,6 +220,11 @@ func (f *File) ResolveId(ctx context.Context) string {
}
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 {
filePath, fileAbsErr := filepath.Abs(f.Path)
if fileAbsErr == nil {
@ -257,6 +271,7 @@ func (f *ResourceFileInfo) Sys() any {
}
func (f *File) Create(ctx context.Context) error {
slog.Info("File.Create()", "file", f)
uid, uidErr := LookupUID(f.Owner)
if uidErr != nil {
return fmt.Errorf("%w: unkwnon user %d", ErrInvalidFileOwner, uid)
@ -284,6 +299,27 @@ func (f *File) Create(ctx context.Context) error {
default:
fallthrough
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)
if e != nil {
return e
@ -292,16 +328,25 @@ func (f *File) Create(ctx context.Context) error {
if chmodErr := createdFile.Chmod(os.FileMode(mode)); chmodErr != nil {
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))
if writeErr != nil {
return writeErr
}
*/
f.Sha256 = fmt.Sprintf("%x", hash.Sum(nil))
if !f.Mtime.IsZero() && !f.Atime.IsZero() {
if chtimesErr := os.Chtimes(f.Path, f.Atime, f.Mtime); chtimesErr != nil {
return chtimesErr
}
}
}
if chownErr := os.Chown(f.Path, uid, gid); chownErr != nil {
return chownErr
}
@ -372,6 +417,7 @@ func (f *File) Read(ctx context.Context) ([]byte, error) {
switch f.FileType {
case RegularFile:
if len(f.ContentSourceRef) == 0 || f.SerializeContent {
file, fileErr := os.Open(f.Path)
if fileErr != nil {
panic(fileErr)
@ -383,6 +429,7 @@ func (f *File) Read(ctx context.Context) ([]byte, error) {
}
f.Content = string(fileContent)
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fileContent))
}
case SymbolicLinkFile:
linkTarget, pathErr := os.Readlink(f.Path)
if pathErr != nil {

View File

@ -397,3 +397,54 @@ func TestFileDelete(t *testing.T) {
assert.Nil(t, stater.Trigger("delete"))
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() {
ResourceTypes.Register("http", HTTPFactory)
ResourceTypes.Register("https", HTTPFactory)
ResourceTypes.Register([]string{"http", "https"}, HTTPFactory)
}
func HTTPFactory(u *url.URL) Resource {
@ -41,7 +40,10 @@ type HTTP struct {
Endpoint string `yaml:"endpoint" json:"endpoint"`
Headers []HTTPHeader `yaml:"headers,omitempty" json:"headers,omitempty"`
Body string `yaml:"body,omitempty" json:"body,omitempty"`
Status string `yaml:"status,omitempty" json:"status,omitempty"`
StatusCode int `yaml:"statuscode,omitempty" json:"statuscode,omitempty"`
State string `yaml:"state,omitempty" json:"state,omitempty"`
config ConfigurationValueGetter
}
func NewHTTP() *HTTP {
@ -67,9 +69,22 @@ func (h *HTTP) StateMachine() machine.Stater {
func (h *HTTP) Notify(m *machine.EventMessage) {
ctx := context.Background()
slog.Info("Notify()", "http", h, "m", m)
switch m.On {
case machine.ENTERSTATEEVENT:
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":
if e := h.Create(ctx); e == nil {
if triggerErr := h.stater.Trigger("created"); triggerErr == nil {
@ -77,7 +92,21 @@ func (h *HTTP) Notify(m *machine.EventMessage) {
}
}
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"
}
case machine.EXITSTATEEVENT:
@ -96,6 +125,10 @@ func (h *HTTP) SetURI(uri string) error {
return nil
}
func (h *HTTP) UseConfig(config ConfigurationValueGetter) {
h.config = config
}
func (h *HTTP) JSON() ([]byte, error) {
return json.Marshal(h)
}
@ -140,10 +173,18 @@ func (h *HTTP) Create(ctx context.Context) error {
if reqErr != nil {
return reqErr
}
if tokenErr := h.ReadAuthorizationTokenFromConfig(req); tokenErr != nil {
slog.Error("ReadAuthorizationTokenFromConfig()", "error", tokenErr)
}
for _,header := range h.Headers {
req.Header.Add(header.Name, header.Value)
}
resp, err := h.client.Do(req)
h.Status = resp.Status
h.StatusCode = resp.StatusCode
if err != nil {
return err
}
@ -151,6 +192,18 @@ func (h *HTTP) Create(ctx context.Context) error {
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) {
req, reqErr := http.NewRequestWithContext(ctx, "GET", h.Endpoint, 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)
tokenErr := h.ReadAuthorizationTokenFromConfig(req)
if tokenErr != nil {
slog.Error("ReadAuthorizationTokenFromConfig()", "error", tokenErr)
}
if len(h.Headers) > 0 {
for _,header := range h.Headers {
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)
slog.Info("Http.Read()", "response", resp, "error", err)
h.Status = resp.Status
h.StatusCode = resp.StatusCode
if err != nil {
return nil, err
}
@ -177,6 +238,10 @@ func (h *HTTP) Read(ctx context.Context) ([]byte, error) {
return yaml.Marshal(h)
}
func (h *HTTP) Delete(ctx context.Context) error {
return nil
}
func (h *HTTP) Type() string {
return "http"
}

View File

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

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ import (
"strings"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
"decl/internal/command"
)
type PackageType string
@ -31,58 +32,44 @@ const (
PackageTypeYum PackageType = "yum"
)
var SystemPackageType PackageType = FindSystemPackageType()
type Package struct {
stater machine.Stater `yaml:"-" json:"-"`
Source string `yaml:"source,omitempty" json:"source,omitempty"`
Name string `yaml:"name" json:"name"`
Required string `json:"required,omitempty" yaml:"required,omitempty"`
Version string `yaml:"version,omitempty" json:"version,omitempty"`
PackageType PackageType `yaml:"type" json:"type"`
CreateCommand *Command `yaml:"-" json:"-"`
ReadCommand *Command `yaml:"-" json:"-"`
UpdateCommand *Command `yaml:"-" json:"-"`
DeleteCommand *Command `yaml:"-" json:"-"`
CreateCommand *command.Command `yaml:"-" json:"-"`
ReadCommand *command.Command `yaml:"-" json:"-"`
UpdateCommand *command.Command `yaml:"-" json:"-"`
DeleteCommand *command.Command `yaml:"-" json:"-"`
// state attributes
State string `yaml:"state,omitempty" json:"state,omitempty"`
config ConfigurationValueGetter
}
func init() {
ResourceTypes.Register("package", 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 {
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
})
}
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 {
return &Package{ PackageType: PackageTypeApk }
return &Package{ PackageType: SystemPackageType }
}
func (p *Package) Clone() Resource {
@ -106,17 +93,44 @@ func (p *Package) StateMachine() machine.Stater {
func (p *Package) Notify(m *machine.EventMessage) {
ctx := context.Background()
slog.Info("Notify()", "package", p, "m", m)
switch m.On {
case machine.ENTERSTATEEVENT:
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":
if e := p.Create(ctx); e == nil {
if triggerErr := p.stater.Trigger("created"); triggerErr == nil {
if triggerErr := p.StateMachine().Trigger("created"); triggerErr == nil {
return
}
}
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"
}
case machine.EXITSTATEEVENT:
@ -147,6 +161,10 @@ func (p *Package) SetURI(uri string) error {
return e
}
func (p *Package) UseConfig(config ConfigurationValueGetter) {
p.config = config
}
func (p *Package) JSON() ([]byte, error) {
return json.Marshal(p)
}
@ -176,6 +194,16 @@ func (p *Package) Create(ctx context.Context) error {
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 {
if p.Version == "latest" {
p.Version = ""
@ -228,22 +256,69 @@ func (p *Package) UnmarshalYAML(value *yaml.Node) error {
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 {
case PackageTypeApk:
return NewApkCreateCommand(), NewApkReadCommand(), NewApkUpdateCommand(), NewApkDeleteCommand()
case PackageTypeApt:
return NewAptCreateCommand(), NewAptReadCommand(), NewAptUpdateCommand(), NewAptDeleteCommand()
case PackageTypeDeb:
return NewDebCreateCommand(), NewDebReadCommand(), NewDebUpdateCommand(), NewDebDeleteCommand()
case PackageTypeDnf:
return NewDnfCreateCommand(), NewDnfReadCommand(), NewDnfUpdateCommand(), NewDnfDeleteCommand()
case PackageTypeRpm:
return NewRpmCreateCommand(), NewRpmReadCommand(), NewRpmUpdateCommand(), NewRpmDeleteCommand()
case PackageTypePip:
return NewPipCreateCommand(), NewPipReadCommand(), NewPipUpdateCommand(), NewPipDeleteCommand()
case PackageTypeYum:
return NewYumCreateCommand(), NewYumReadCommand(), NewYumUpdateCommand(), NewYumDeleteCommand()
default:
}
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 {
switch value {
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)
}
func NewApkCreateCommand() *Command {
c := NewCommand()
func NewApkCreateCommand() *command.Command {
c := command.NewCommand()
c.Path = "apk"
c.Args = []CommandArg{
CommandArg("add"),
CommandArg("{{ .Name }}{{ .Required }}"),
c.Args = []command.CommandArg{
command.CommandArg("add"),
command.CommandArg("{{ .Name }}{{ .Required }}"),
}
return c
}
func NewApkReadCommand() *Command {
c := NewCommand()
func NewApkReadCommand() *command.Command {
c := command.NewCommand()
c.Path = "apk"
c.Args = []CommandArg{
CommandArg("info"),
CommandArg("-ev"),
CommandArg("{{ .Name }}"),
c.Args = []command.CommandArg{
command.CommandArg("info"),
command.CommandArg("-ev"),
command.CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
p := target.(*Package)
@ -303,44 +378,82 @@ func NewApkReadCommand() *Command {
return c
}
func NewApkUpdateCommand() *Command {
c := NewCommand()
func NewApkUpdateCommand() *command.Command {
c := command.NewCommand()
c.Path = "apk"
c.Args = []CommandArg{
CommandArg("del"),
CommandArg("{{ .Name }}"),
c.Args = []command.CommandArg{
command.CommandArg("add"),
command.CommandArg("{{ .Name }}{{ .Required }}"),
}
return c
}
func NewApkDeleteCommand() *Command {
c := NewCommand()
func NewApkDeleteCommand() *command.Command {
c := command.NewCommand()
c.Path = "apk"
c.Args = []CommandArg{
CommandArg("del"),
CommandArg("{{ .Name }}"),
c.Args = []command.CommandArg{
command.CommandArg("del"),
command.CommandArg("{{ .Name }}"),
}
return c
}
func NewAptCreateCommand() *Command {
c := NewCommand()
func NewApkReadPackagesCommand() *command.Command {
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.Split = false
c.Args = []CommandArg{
CommandArg("satisfy"),
CommandArg("-y"),
CommandArg("{{ .Name }} ({{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }})"),
c.Args = []command.CommandArg{
command.CommandArg("satisfy"),
command.CommandArg("-y"),
command.CommandArg("{{ .Name }} ({{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }})"),
}
return c
}
func NewAptReadCommand() *Command {
c := NewCommand()
func NewAptReadCommand() *command.Command {
c := command.NewCommand()
c.Path = "dpkg"
c.Args = []CommandArg{
CommandArg("-s"),
CommandArg("{{ .Name }}"),
c.Args = []command.CommandArg{
command.CommandArg("-s"),
command.CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
p := target.(*Package)
@ -379,22 +492,404 @@ func NewAptReadCommand() *Command {
return c
}
func NewAptUpdateCommand() *Command {
c := NewCommand()
c.Path = "apt"
c.Args = []CommandArg{
CommandArg("install"),
CommandArg("{{ .Name }}"),
func NewAptUpdateCommand() *command.Command {
c := command.NewCommand()
c.Path = "apt-get"
c.Split = false
c.Args = []command.CommandArg{
command.CommandArg("satisfy"),
command.CommandArg("-y"),
command.CommandArg("{{ .Name }} ({{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }})"),
}
return c
}
func NewAptDeleteCommand() *Command {
c := NewCommand()
func NewAptDeleteCommand() *command.Command {
c := command.NewCommand()
c.Path = "apt"
c.Args = []CommandArg{
CommandArg("remove"),
CommandArg("{{ .Name }}"),
c.FailOnError = false
c.Args = []command.CommandArg{
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
}

View File

@ -16,6 +16,7 @@ import (
_ "os"
_ "strings"
"testing"
"decl/internal/command"
)
func TestNewPackageResource(t *testing.T) {
@ -56,7 +57,7 @@ type: apk
assert.Nil(t, loadErr)
assert.Equal(t, "latest", p.Version)
p.ReadCommand = (*Command)(m)
p.ReadCommand = (*command.Command)(m)
yaml, readErr := p.Read(context.Background())
assert.Nil(t, readErr)
assert.Greater(t, len(yaml), 0)
@ -106,3 +107,30 @@ func TestPackageSetURI(t *testing.T) {
assert.Equal(t, "package", p.Type())
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"
_ "net/url"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/transport"
)
type ResourceReference string
type ResourceSelector func(r *Declaration) bool
type Resource interface {
@ -19,6 +22,7 @@ type Resource interface {
StateMachine() machine.Stater
URI() string
SetURI(string) error
UseConfig(config ConfigurationValueGetter)
ResolveId(context.Context) string
ResourceLoader
StateTransformer
@ -27,6 +31,14 @@ type Resource interface {
Clone() Resource
}
type ContentReader interface {
ContentReaderStream() (*transport.Reader, error)
}
type ContentWriter interface {
ContentWriterStream() (*transport.Writer, error)
}
type ResourceValidator interface {
Validate() error
}
@ -65,6 +77,14 @@ func NewResource(uri string) Resource {
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 {
// start_destroy -> absent -> start_create -> present -> start_destroy
stater := machine.New("unknown")

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,10 @@
"description": "Resource type name.",
"enum": [ "iptable" ]
},
"config": {
"type": "string",
"description": "Config name"
},
"attributes": {
"$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 (
"errors"
"fmt"
"net/url"
_ "net/url"
"strings"
"decl/internal/types"
)
var (
ErrUnknownResourceType = errors.New("Unknown resource type")
ResourceTypes *Types = NewTypes()
ResourceTypes *types.Types[Resource] = types.New[Resource]()
)
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 {
ResourceTypeName := strings.Trim(string(b), "\"")
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:"-"`
DeleteCommand *Command `json:"-" yaml:"-"`
State string `json:"state,omitempty" yaml:"state,omitempty"`
config ConfigurationValueGetter
}
func NewUser() *User {
@ -52,7 +53,7 @@ func NewUser() *User {
}
func init() {
ResourceTypes.Register("user", func(u *url.URL) Resource {
ResourceTypes.Register([]string{"user"}, func(u *url.URL) Resource {
user := NewUser()
user.Name = u.Hostname()
user.UID = LookupUIDString(u.Hostname())
@ -126,6 +127,10 @@ func (u *User) URI() string {
return fmt.Sprintf("user://%s", u.Name)
}
func (u *User) UseConfig(config ConfigurationValueGetter) {
u.config = config
}
func (u *User) ResolveId(ctx context.Context) string {
return LookupUIDString(u.Name)
}

View File

@ -5,14 +5,14 @@ package source
import (
"context"
_ "encoding/json"
_ "fmt"
"fmt"
_ "gopkg.in/yaml.v3"
"net/url"
_ "path/filepath"
"decl/internal/resource"
_ "os"
_ "io"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"log/slog"
)
@ -46,9 +46,10 @@ func init() {
func (c *Container) Type() string { return "container" }
func (c *Container) ExtractResources(filter ResourceSelector) ([]*resource.Document, error) {
var extractErr error
ctx := context.Background()
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 {
return nil, err
}
@ -56,9 +57,11 @@ func (c *Container) ExtractResources(filter ResourceSelector) ([]*resource.Docum
document := resource.NewDocument()
for _, container := range containers {
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)
}
return []*resource.Document{document}, nil
return []*resource.Document{document}, extractErr
}

View File

@ -5,83 +5,19 @@ package source
import (
"errors"
"fmt"
"net/url"
_ "net/url"
"strings"
"path/filepath"
_ "path/filepath"
"decl/internal/types"
)
var (
ErrUnknownSourceType = errors.New("Unknown source type")
SourceTypes *Types = NewTypes()
SourceTypes *types.Types[DocSource] = types.New[DocSource]()
)
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 {
SourceTypeName := strings.Trim(string(b), "\"")
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) {
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.writeHandle = os.Stdout
} 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
}
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.postRequest, err = http.NewRequestWithContext(ctx, "POST", u.String(), h.buffer)
if err != nil {
return
}
h.getRequest, err = http.NewRequestWithContext(ctx, "GET", u.String(), nil)
return
}
@ -73,7 +76,7 @@ func (h *HTTP) Path() string {
func (h *HTTP) Signature() (documentSignature string) {
if h.getResponse != nil {
documentSignature := h.getResponse.Header.Get("Signature")
documentSignature = h.getResponse.Header.Get("Signature")
if documentSignature == "" {
signatureResp, signatureErr := h.Client.Get(fmt.Sprintf("%s.sig", h.uri.String()))
if signatureErr == nil {
@ -113,7 +116,7 @@ func (h *HTTP) Reader() io.ReadCloser {
func (h *HTTP) Writer() io.WriteCloser {
var err error
if h.postResponse, err = h.Client.Do(h.postRequest); err != nil {
h.postResponse, err = h.Client.Do(h.postRequest)
panic(err)
}
return h.buffer
}

View File

@ -44,6 +44,14 @@ func NewReader(u *url.URL) (reader *Reader, e error) {
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 {
uri *url.URL
handle Handler
@ -65,6 +73,14 @@ func NewWriter(u *url.URL) (writer *Writer, e error) {
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) {
return r.stream.Read(b)
}