add support for configuration documents
This commit is contained in:
parent
52c083a3d9
commit
1460d2285b
2
Makefile
2
Makefile
@ -30,3 +30,5 @@ run:
|
||||
clean:
|
||||
go clean -modcache
|
||||
rm jx
|
||||
lint:
|
||||
golangci-lint run --verbose ./...
|
||||
|
50
cli_test.go
50
cli_test.go
@ -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)
|
||||
}
|
||||
|
104
cmd/cli/main.go
104
cmd/cli/main.go
@ -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()
|
||||
|
||||
@ -61,6 +65,10 @@ var jxSubCommands = []SubCommand {
|
||||
Name: "import",
|
||||
Run: ImportSubCommand,
|
||||
},
|
||||
{
|
||||
Name: "config",
|
||||
Run: ConfigSubCommand,
|
||||
},
|
||||
}
|
||||
|
||||
func VersionUsage() {
|
||||
@ -81,12 +89,29 @@ func LoggerConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -97,6 +122,31 @@ func LoadSourceURI(uri string) []*resource.Document {
|
||||
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) {
|
||||
ImportResource = cmd.String("resource", "", "(uri) Add a resource to the document.")
|
||||
ImportMerge = cmd.Bool("merge", false, "Merge resources into a single document.")
|
||||
@ -105,6 +155,12 @@ 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() {
|
||||
@ -182,6 +238,13 @@ 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() {
|
||||
@ -191,8 +254,11 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("main.Apply()", "documents", 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 {
|
||||
@ -292,8 +358,21 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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)
|
||||
|
12
examples/golangci-lint.jx.yaml
Normal file
12
examples/golangci-lint.jx.yaml
Normal 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
2
go.mod
@ -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
134
internal/config/block.go
Normal 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
|
||||
}
|
49
internal/config/block_test.go
Normal file
49
internal/config/block_test.go
Normal 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)
|
||||
}
|
9
internal/config/configs/facter.yaml
Normal file
9
internal/config/configs/facter.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
configurations:
|
||||
- name: facts
|
||||
type: exec
|
||||
values:
|
||||
path: /usr/bin/facter
|
||||
args:
|
||||
- "-j"
|
||||
format: "json"
|
||||
|
45
internal/config/configsource.go
Normal file
45
internal/config/configsource.go
Normal 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)
|
||||
}
|
30
internal/config/configtarget.go
Normal file
30
internal/config/configtarget.go
Normal 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
|
||||
}
|
48
internal/config/configuration.go
Normal file
48
internal/config/configuration.go
Normal 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)
|
||||
}
|
22
internal/config/configuration_test.go
Normal file
22
internal/config/configuration_test.go
Normal 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
202
internal/config/document.go
Normal 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
|
||||
}
|
53
internal/config/document_test.go
Normal file
53
internal/config/document_test.go
Normal 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
121
internal/config/exec.go
Normal 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
137
internal/config/file.go
Normal 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
109
internal/config/fs.go
Normal 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
|
||||
}
|
||||
|
48
internal/config/generic.go
Normal file
48
internal/config/generic.go
Normal 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
|
||||
}
|
13
internal/config/generic_test.go
Normal file
13
internal/config/generic_test.go
Normal 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
54
internal/config/schema.go
Normal 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
|
||||
}
|
49
internal/config/schema_test.go
Normal file
49
internal/config/schema_test.go
Normal 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())
|
||||
}
|
||||
*/
|
22
internal/config/schemas/block.schema.json
Normal file
22
internal/config/schemas/block.schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
19
internal/config/schemas/document.schema.json
Normal file
19
internal/config/schemas/document.schema.json
Normal 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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
11
internal/resource/config.go
Normal file
11
internal/resource/config.go
Normal 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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -1,4 +1,5 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
package resource
|
||||
|
||||
import (
|
||||
|
@ -10,6 +10,10 @@
|
||||
"description": "Resource type name.",
|
||||
"enum": [ "file" ]
|
||||
},
|
||||
"config": {
|
||||
"type": "string",
|
||||
"description": "Config name"
|
||||
},
|
||||
"attributes": {
|
||||
"$ref": "file.jsonschema"
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -10,6 +10,10 @@
|
||||
"description": "Resource type name.",
|
||||
"enum": [ "http" ]
|
||||
},
|
||||
"config": {
|
||||
"type": "string",
|
||||
"description": "Config name."
|
||||
},
|
||||
"attributes": {
|
||||
"$ref": "http.jsonschema"
|
||||
}
|
||||
|
@ -10,6 +10,10 @@
|
||||
"description": "Resource type name.",
|
||||
"enum": [ "iptable" ]
|
||||
},
|
||||
"config": {
|
||||
"type": "string",
|
||||
"description": "Config name"
|
||||
},
|
||||
"attributes": {
|
||||
"$ref": "iptable.jsonschema"
|
||||
}
|
||||
|
252
internal/resource/service.go
Normal file
252
internal/resource/service.go
Normal 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
|
||||
}
|
36
internal/resource/service_test.go
Normal file
36
internal/resource/service_test.go
Normal 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))
|
||||
}
|
@ -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) {
|
||||
|
@ -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))
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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() })
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user