diff --git a/Makefile b/Makefile index ef4b538..51b8277 100644 --- a/Makefile +++ b/Makefile @@ -30,3 +30,5 @@ run: clean: go clean -modcache rm jx +lint: + golangci-lint run --verbose ./... diff --git a/cli_test.go b/cli_test.go index 1f360cf..2dea655 100644 --- a/cli_test.go +++ b/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) +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 3f403d3..c3a1c83 100644 --- a/cmd/cli/main.go +++ b/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 ( @@ -25,8 +26,8 @@ const ( var ( version string - commit string - date string + commit string + date string ) var GlobalOformat *string @@ -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() @@ -45,21 +49,25 @@ type RunCommand func(cmd *flag.FlagSet, output io.Writer) error type SubCommand struct { Name string - Run RunCommand + Run RunCommand } -var jxSubCommands = []SubCommand { +var jxSubCommands = []SubCommand{ { Name: "diff", - Run: DiffSubCommand, + Run: DiffSubCommand, }, { Name: "apply", - Run: ApplySubCommand, + Run: ApplySubCommand, }, { Name: "import", - Run: ImportSubCommand, + Run: ImportSubCommand, + }, + { + Name: "config", + Run: ConfigSubCommand, }, } @@ -74,19 +82,36 @@ func LoggerConfig() { var programLevel = new(slog.LevelVar) logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel})) slog.SetDefault(logger) - if debugLogging,ok := os.LookupEnv("JX_DEBUG"); ok && debugLogging != "" { + if debugLogging, ok := os.LookupEnv("JX_DEBUG"); ok && debugLogging != "" { programLevel.Set(slog.LevelDebug) } else { programLevel.Set(slog.LevelError) } } +func LoadConfigURI(uri string) []*config.Document { + slog.Info("LoadConfigURI()", "uri", uri) + if uri != "" { + cs, err := config.ConfigSourceTypes.New(uri) + if err != nil { + slog.Error("Failed loading config document from source", "error", err) + } + extractConfigs, extractErr := cs.Extract(nil) + if extractErr != nil { + slog.Error("Failed loading configs from source", "error", extractErr) + } + return extractConfigs + } + return []*config.Document{config.NewDocument()} +} + func LoadSourceURI(uri string) []*resource.Document { slog.Info("loading ", "uri", uri) if uri != "" { ds, err := source.SourceTypes.New(uri) if err != nil { slog.Error("Failed loading document from source", "error", err) + return nil } extractDocuments, extractErr := ds.ExtractResources(nil) if extractErr != nil { @@ -94,7 +119,32 @@ func LoadSourceURI(uri string) []*resource.Document { } return extractDocuments } - return []*resource.Document{ resource.NewDocument() } + return []*resource.Document{resource.NewDocument()} +} + +func ConfigSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) { + e := cmd.Parse(os.Args[2:]) + if e != nil { // returns ErrHelp + return e + } + + slog.Info("ConfigSubCommand", "configdoc", ConfigDoc) + for _, configSource := range cmd.Args() { + for _, argConfigDoc := range LoadConfigURI(configSource) { + ConfigDoc.Append(argConfigDoc) + } + } + + outputTarget, err := config.ConfigTargetTypes.New(GlobalOutput) + if err != nil { + slog.Error("Failed opening target", "error", err) + } + defer outputTarget.Close() + + if outputErr := outputTarget.EmitResources([]*config.Document{ConfigDoc}, nil); outputErr != nil { + return outputErr + } + return nil } func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) { @@ -105,23 +155,29 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) { return e } + if ConfigPath != "" { + for _, argConfigDoc := range LoadConfigURI(ConfigPath) { + ConfigDoc.Append(argConfigDoc) + } + } + merged := resource.NewDocument() documents := make([]*resource.Document, 0, 100) - for _,source := range cmd.Args() { - loaded := LoadSourceURI(source) + for _, source := range cmd.Args() { + loaded := LoadSourceURI(source) if loaded != nil { documents = append(documents, loaded...) } } -/* - switch *GlobalOformat { - case FormatYaml: - encoder = resource.NewYAMLEncoder(output) - case FormatJson: - encoder = resource.NewJSONEncoder(output) - } -*/ + /* + switch *GlobalOformat { + case FormatYaml: + encoder = resource.NewYAMLEncoder(output) + case FormatJson: + encoder = resource.NewJSONEncoder(output) + } + */ slog.Info("main.ImportResource", "args", os.Args, "output", GlobalOutput) outputTarget, err := target.TargetTypes.New(GlobalOutput) @@ -134,7 +190,7 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) { documents = append(documents, resource.NewDocument()) } - for _,d := range documents { + for _, d := range documents { if d != nil { if *ImportResource != "" { @@ -153,7 +209,7 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) { if *GlobalQuiet { for _, dr := range d.Resources() { - if _,e := output.Write([]byte(dr.Resource().URI())); e != nil { + if _, e := output.Write([]byte(dr.Resource().URI())); e != nil { return e } } @@ -182,17 +238,27 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) { if e := cmd.Parse(os.Args[2:]); e != nil { return e } + + if ConfigPath != "" { + for _, argConfigDoc := range LoadConfigURI(ConfigPath) { + ConfigDoc.Append(argConfigDoc) + } + } + var encoder codec.Encoder documents := make([]*resource.Document, 0, 100) - for _,source := range cmd.Args() { - loaded := LoadSourceURI(source) + for _, source := range cmd.Args() { + loaded := LoadSourceURI(source) if loaded != nil { documents = append(documents, loaded...) } } - slog.Info("main.Apply()", "documents", documents) - for _,d := range documents { + slog.Info("main.Apply()", "documents", documents, "configdoc", ConfigDoc) + for _, d := range documents { + + d.SetConfig(ConfigDoc) + slog.Info("main.Apply()", "doc", d) var overrideState string = "" if *ApplyDelete { @@ -212,7 +278,7 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) { } if *GlobalQuiet { for _, dr := range d.Resources() { - if _,e := output.Write([]byte(dr.Resource().URI())); e != nil { + if _, e := output.Write([]byte(dr.Resource().URI())); e != nil { return e } } @@ -242,7 +308,7 @@ func DiffSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) { for i, doc := range rightDocuments { if doc != nil { leftDocuments = append(leftDocuments, doc.Clone()) - for _,resourceDeclaration := range leftDocuments[i].Resources() { + for _, resourceDeclaration := range leftDocuments[i].Resources() { if _, e := resourceDeclaration.Resource().Read(ctx); e != nil { slog.Info("jx diff ", "err", e) //return e @@ -262,20 +328,20 @@ func DiffSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) { break } if index >= len(rightDocuments) { - if _,e := leftDocuments[index].Diff(resource.NewDocument(), output); e != nil { + if _, e := leftDocuments[index].Diff(resource.NewDocument(), output); e != nil { return e } index++ continue } if index >= len(leftDocuments) { - if _,e := resource.NewDocument().Diff(rightDocuments[index], output); e != nil { + if _, e := resource.NewDocument().Diff(rightDocuments[index], output); e != nil { return e } index++ continue } - if _,e := leftDocuments[index].Diff(rightDocuments[index], output); e != nil { + if _, e := leftDocuments[index].Diff(rightDocuments[index], output); e != nil { return e } index++ @@ -292,8 +358,21 @@ func main() { os.Exit(1) } - for _,subCmd := range jxSubCommands { + DefaultConfigurations, configErr := config.Configurations() + if configErr != nil { + slog.Error("Failed loading default configuration", "error", configErr) + } + + for _, argConfigDoc := range DefaultConfigurations { + ConfigDoc.Append(argConfigDoc) + } + + for _, subCmd := range jxSubCommands { cmdFlagSet := flag.NewFlagSet(subCmd.Name, flag.ExitOnError) + + cmdFlagSet.StringVar(&ConfigPath, "config", "/etc/jx/config.yaml", "Config file path") + cmdFlagSet.StringVar(&ConfigPath, "c", "/etc/jx/config.yaml", "Config file path") + GlobalOformat = cmdFlagSet.String("oformat", "yaml", "Output serialization format") cmdFlagSet.StringVar(&GlobalOutput, "output", "-", "Output target (default stdout)") cmdFlagSet.StringVar(&GlobalOutput, "o", "-", "Output target (default stdout)") @@ -318,7 +397,14 @@ func main() { cmdFlagSet.PrintDefaults() VersionUsage() } + case "config": + cmdFlagSet.Usage = func() { + fmt.Println("jx config source...") + cmdFlagSet.PrintDefaults() + VersionUsage() + } } + slog.Info("CLI", "cmd", subCmd.Name) if os.Args[1] == subCmd.Name { if e := subCmd.Run(cmdFlagSet, os.Stdout); e != nil { slog.Error("Failed running command", "command", os.Args[1], "error", e) diff --git a/examples/golangci-lint.jx.yaml b/examples/golangci-lint.jx.yaml new file mode 100644 index 0000000..af75812 --- /dev/null +++ b/examples/golangci-lint.jx.yaml @@ -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 diff --git a/go.mod b/go.mod index 266ef8b..e983ee2 100644 --- a/go.mod +++ b/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 diff --git a/internal/config/block.go b/internal/config/block.go new file mode 100644 index 0000000..cf1af1b --- /dev/null +++ b/internal/config/block.go @@ -0,0 +1,134 @@ +// Copyright 2024 Matthew Rich . 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 +} diff --git a/internal/config/block_test.go b/internal/config/block_test.go new file mode 100644 index 0000000..37efe27 --- /dev/null +++ b/internal/config/block_test.go @@ -0,0 +1,49 @@ +// Copyright 2024 Matthew Rich . 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) +} diff --git a/internal/config/configs/facter.yaml b/internal/config/configs/facter.yaml new file mode 100644 index 0000000..688d095 --- /dev/null +++ b/internal/config/configs/facter.yaml @@ -0,0 +1,9 @@ +configurations: +- name: facts + type: exec + values: + path: /usr/bin/facter + args: + - "-j" + format: "json" + diff --git a/internal/config/configsource.go b/internal/config/configsource.go new file mode 100644 index 0000000..cd94730 --- /dev/null +++ b/internal/config/configsource.go @@ -0,0 +1,45 @@ +// Copyright 2024 Matthew Rich . 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) +} diff --git a/internal/config/configtarget.go b/internal/config/configtarget.go new file mode 100644 index 0000000..4af4a51 --- /dev/null +++ b/internal/config/configtarget.go @@ -0,0 +1,30 @@ +// Copyright 2024 Matthew Rich . 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 +} diff --git a/internal/config/configuration.go b/internal/config/configuration.go new file mode 100644 index 0000000..48f44ac --- /dev/null +++ b/internal/config/configuration.go @@ -0,0 +1,48 @@ +// Copyright 2024 Matthew Rich . 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) +} diff --git a/internal/config/configuration_test.go b/internal/config/configuration_test.go new file mode 100644 index 0000000..73c7259 --- /dev/null +++ b/internal/config/configuration_test.go @@ -0,0 +1,22 @@ +// Copyright 2024 Matthew Rich . 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) +} diff --git a/internal/config/document.go b/internal/config/document.go new file mode 100644 index 0000000..3612598 --- /dev/null +++ b/internal/config/document.go @@ -0,0 +1,202 @@ +// Copyright 2024 Matthew Rich . 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 +} diff --git a/internal/config/document_test.go b/internal/config/document_test.go new file mode 100644 index 0000000..7d12d8c --- /dev/null +++ b/internal/config/document_test.go @@ -0,0 +1,53 @@ +// Copyright 2024 Matthew Rich . 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) +} diff --git a/internal/config/exec.go b/internal/config/exec.go new file mode 100644 index 0000000..8f89923 --- /dev/null +++ b/internal/config/exec.go @@ -0,0 +1,121 @@ +// Copyright 2024 Matthew Rich . 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 + } +} + diff --git a/internal/config/file.go b/internal/config/file.go new file mode 100644 index 0000000..67599db --- /dev/null +++ b/internal/config/file.go @@ -0,0 +1,137 @@ +// Copyright 2024 Matthew Rich . 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 +} diff --git a/internal/config/fs.go b/internal/config/fs.go new file mode 100644 index 0000000..d1c25c7 --- /dev/null +++ b/internal/config/fs.go @@ -0,0 +1,109 @@ +// Copyright 2024 Matthew Rich . 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 +} + diff --git a/internal/config/generic.go b/internal/config/generic.go new file mode 100644 index 0000000..8ea7194 --- /dev/null +++ b/internal/config/generic.go @@ -0,0 +1,48 @@ +// Copyright 2024 Matthew Rich . 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 +} diff --git a/internal/config/generic_test.go b/internal/config/generic_test.go new file mode 100644 index 0000000..ac25699 --- /dev/null +++ b/internal/config/generic_test.go @@ -0,0 +1,13 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package config + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewGenericConfig(t *testing.T) { + g := NewGeneric() + assert.NotNil(t, g) +} diff --git a/internal/config/schema.go b/internal/config/schema.go new file mode 100644 index 0000000..a41df08 --- /dev/null +++ b/internal/config/schema.go @@ -0,0 +1,54 @@ +// Copyright 2024 Matthew Rich . 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 +} diff --git a/internal/config/schema_test.go b/internal/config/schema_test.go new file mode 100644 index 0000000..44c18c4 --- /dev/null +++ b/internal/config/schema_test.go @@ -0,0 +1,49 @@ +// Copyright 2024 Matthew Rich . 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()) +} +*/ diff --git a/internal/config/schemas/block.schema.json b/internal/config/schemas/block.schema.json new file mode 100644 index 0000000..cdd2d23 --- /dev/null +++ b/internal/config/schemas/block.schema.json @@ -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" + } + } +} diff --git a/internal/config/schemas/document.schema.json b/internal/config/schemas/document.schema.json new file mode 100644 index 0000000..6b59cdd --- /dev/null +++ b/internal/config/schemas/document.schema.json @@ -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" } + ] + } + } + } +} + diff --git a/internal/resource/config.go b/internal/resource/config.go new file mode 100644 index 0000000..cfb7813 --- /dev/null +++ b/internal/resource/config.go @@ -0,0 +1,11 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package resource + +import ( +) + +type ConfigurationValueGetter interface { + GetValue(key string) (any, error) +} + diff --git a/internal/resource/container.go b/internal/resource/container.go index bb797cd..c4248d2 100644 --- a/internal/resource/container.go +++ b/internal/resource/container.go @@ -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,7 +341,11 @@ func (c *Container) Read(ctx context.Context) ([]byte, error) { c.State = "present" c.Id = containerJSON.ID if c.Name == "" { - c.Name = containerJSON.Name + if containerJSON.Name[0] == '/' { + c.Name = containerJSON.Name[1:] + } else { + c.Name = containerJSON.Name + } } c.Path = containerJSON.Path c.Image = containerJSON.Image @@ -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 { diff --git a/internal/resource/container_image.go b/internal/resource/container_image.go index 77596de..d769129 100644 --- a/internal/resource/container_image.go +++ b/internal/resource/container_image.go @@ -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) } diff --git a/internal/resource/container_network.go b/internal/resource/container_network.go index 8f867d2..890e4fc 100644 --- a/internal/resource/container_network.go +++ b/internal/resource/container_network.go @@ -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) } diff --git a/internal/resource/declaration.go b/internal/resource/declaration.go index a69e164..7bbafe9 100644 --- a/internal/resource/declaration.go +++ b/internal/resource/declaration.go @@ -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 diff --git a/internal/resource/declaration_test.go b/internal/resource/declaration_test.go index 6d28169..09e2d9f 100644 --- a/internal/resource/declaration_test.go +++ b/internal/resource/declaration_test.go @@ -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() diff --git a/internal/resource/document.go b/internal/resource/document.go index 9deeaad..fd2bf7a 100644 --- a/internal/resource/document.go +++ b/internal/resource/document.go @@ -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 } diff --git a/internal/resource/document_test.go b/internal/resource/document_test.go index 85555ba..cb5838e 100644 --- a/internal/resource/document_test.go +++ b/internal/resource/document_test.go @@ -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()) diff --git a/internal/resource/file.go b/internal/resource/file.go index 09a69f5..947b31d 100644 --- a/internal/resource/file.go +++ b/internal/resource/file.go @@ -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,17 +417,19 @@ func (f *File) Read(ctx context.Context) ([]byte, error) { switch f.FileType { case RegularFile: - file, fileErr := os.Open(f.Path) - if fileErr != nil { - panic(fileErr) - } + if len(f.ContentSourceRef) == 0 || f.SerializeContent { + file, fileErr := os.Open(f.Path) + if fileErr != nil { + panic(fileErr) + } - fileContent, ioErr := io.ReadAll(file) - if ioErr != nil { - panic(ioErr) + fileContent, ioErr := io.ReadAll(file) + if ioErr != nil { + panic(ioErr) + } + f.Content = string(fileContent) + f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fileContent)) } - f.Content = string(fileContent) - f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fileContent)) case SymbolicLinkFile: linkTarget, pathErr := os.Readlink(f.Path) if pathErr != nil { diff --git a/internal/resource/file_test.go b/internal/resource/file_test.go index 172a7be..1ec7290 100644 --- a/internal/resource/file_test.go +++ b/internal/resource/file_test.go @@ -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) +} diff --git a/internal/resource/http.go b/internal/resource/http.go index 4fb51fe..52c7ea9 100644 --- a/internal/resource/http.go +++ b/internal/resource/http.go @@ -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" } diff --git a/internal/resource/iptables.go b/internal/resource/iptables.go index 199d015..9bc97e3 100644 --- a/internal/resource/iptables.go +++ b/internal/resource/iptables.go @@ -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 } diff --git a/internal/resource/iptables_test.go b/internal/resource/iptables_test.go index 7a285c9..05b8ca8 100644 --- a/internal/resource/iptables_test.go +++ b/internal/resource/iptables_test.go @@ -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) diff --git a/internal/resource/mock_command_test.go b/internal/resource/mock_command_test.go index 73fa351..fef650d 100644 --- a/internal/resource/mock_command_test.go +++ b/internal/resource/mock_command_test.go @@ -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 diff --git a/internal/resource/network_route.go b/internal/resource/network_route.go index 4c1e6b8..ce96058 100644 --- a/internal/resource/network_route.go +++ b/internal/resource/network_route.go @@ -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") } diff --git a/internal/resource/package.go b/internal/resource/package.go index c86ac75..cfa2cd6 100644 --- a/internal/resource/package.go +++ b/internal/resource/package.go @@ -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 } diff --git a/internal/resource/package_test.go b/internal/resource/package_test.go index 1c32798..823582e 100644 --- a/internal/resource/package_test.go +++ b/internal/resource/package_test.go @@ -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()) +} diff --git a/internal/resource/resource.go b/internal/resource/resource.go index bf5b7b5..935ae41 100644 --- a/internal/resource/resource.go +++ b/internal/resource/resource.go @@ -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") diff --git a/internal/resource/resource_test.go b/internal/resource/resource_test.go index af00ac9..4b5a3bd 100644 --- a/internal/resource/resource_test.go +++ b/internal/resource/resource_test.go @@ -1,4 +1,5 @@ // Copyright 2024 Matthew Rich . All rights reserved. + package resource import ( diff --git a/internal/resource/schemas/file-declaration.jsonschema b/internal/resource/schemas/file-declaration.jsonschema index 70023da..8c16fe5 100644 --- a/internal/resource/schemas/file-declaration.jsonschema +++ b/internal/resource/schemas/file-declaration.jsonschema @@ -10,6 +10,10 @@ "description": "Resource type name.", "enum": [ "file" ] }, + "config": { + "type": "string", + "description": "Config name" + }, "attributes": { "$ref": "file.jsonschema" } diff --git a/internal/resource/schemas/file.jsonschema b/internal/resource/schemas/file.jsonschema index 47118ac..1dea4ad 100644 --- a/internal/resource/schemas/file.jsonschema +++ b/internal/resource/schemas/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" diff --git a/internal/resource/schemas/http-declaration.jsonschema b/internal/resource/schemas/http-declaration.jsonschema index 6888785..d344d20 100644 --- a/internal/resource/schemas/http-declaration.jsonschema +++ b/internal/resource/schemas/http-declaration.jsonschema @@ -10,6 +10,10 @@ "description": "Resource type name.", "enum": [ "http" ] }, + "config": { + "type": "string", + "description": "Config name." + }, "attributes": { "$ref": "http.jsonschema" } diff --git a/internal/resource/schemas/iptable-declaration.jsonschema b/internal/resource/schemas/iptable-declaration.jsonschema index 65dd25b..f6c0cc5 100644 --- a/internal/resource/schemas/iptable-declaration.jsonschema +++ b/internal/resource/schemas/iptable-declaration.jsonschema @@ -10,6 +10,10 @@ "description": "Resource type name.", "enum": [ "iptable" ] }, + "config": { + "type": "string", + "description": "Config name" + }, "attributes": { "$ref": "iptable.jsonschema" } diff --git a/internal/resource/service.go b/internal/resource/service.go new file mode 100644 index 0000000..a1f9656 --- /dev/null +++ b/internal/resource/service.go @@ -0,0 +1,252 @@ +// Copyright 2024 Matthew Rich . 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 +} diff --git a/internal/resource/service_test.go b/internal/resource/service_test.go new file mode 100644 index 0000000..a854f12 --- /dev/null +++ b/internal/resource/service_test.go @@ -0,0 +1,36 @@ +// Copyright 2024 Matthew Rich . 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)) +} diff --git a/internal/resource/types.go b/internal/resource/types.go index e22c3d0..ee67b15 100644 --- a/internal/resource/types.go +++ b/internal/resource/types.go @@ -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) { diff --git a/internal/resource/types_test.go b/internal/resource/types_test.go deleted file mode 100644 index eae257d..0000000 --- a/internal/resource/types_test.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2024 Matthew Rich . 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)) -} diff --git a/internal/resource/user.go b/internal/resource/user.go index ee1dc7d..0e55803 100644 --- a/internal/resource/user.go +++ b/internal/resource/user.go @@ -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) } diff --git a/internal/source/container.go b/internal/source/container.go index eda47be..1a695ec 100644 --- a/internal/source/container.go +++ b/internal/source/container.go @@ -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 } diff --git a/internal/source/types.go b/internal/source/types.go index e206934..caf7830 100644 --- a/internal/source/types.go +++ b/internal/source/types.go @@ -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) { diff --git a/internal/source/types_test.go b/internal/source/types_test.go index f735bae..353a99f 100644 --- a/internal/source/types_test.go +++ b/internal/source/types_test.go @@ -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() }) diff --git a/internal/transport/file.go b/internal/transport/file.go index 99dfec6..1a9c8b4 100644 --- a/internal/transport/file.go +++ b/internal/transport/file.go @@ -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 diff --git a/internal/transport/http.go b/internal/transport/http.go index ec78be5..ac502e1 100644 --- a/internal/transport/http.go +++ b/internal/transport/http.go @@ -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 } diff --git a/internal/transport/transport.go b/internal/transport/transport.go index 9a2b533..57284f8 100644 --- a/internal/transport/transport.go +++ b/internal/transport/transport.go @@ -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) }