add support for configuration documents
This commit is contained in:
parent
52c083a3d9
commit
1460d2285b
2
Makefile
2
Makefile
@ -30,3 +30,5 @@ run:
|
|||||||
clean:
|
clean:
|
||||||
go clean -modcache
|
go clean -modcache
|
||||||
rm jx
|
rm jx
|
||||||
|
lint:
|
||||||
|
golangci-lint run --verbose ./...
|
||||||
|
50
cli_test.go
50
cli_test.go
@ -14,12 +14,29 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var TempDir string
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
var err error
|
||||||
|
TempDir, err = os.MkdirTemp("", "testcli")
|
||||||
|
if err != nil || TempDir == "" {
|
||||||
|
slog.Error("TestMain()", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rc := m.Run()
|
||||||
|
|
||||||
|
os.RemoveAll(TempDir)
|
||||||
|
os.Exit(rc)
|
||||||
|
}
|
||||||
|
|
||||||
func TestCli(t *testing.T) {
|
func TestCli(t *testing.T) {
|
||||||
if _, e := os.Stat("./jx"); errors.Is(e, os.ErrNotExist) {
|
if _, e := os.Stat("./jx"); errors.Is(e, os.ErrNotExist) {
|
||||||
t.Skip("cli not built")
|
t.Skip("cli not built")
|
||||||
}
|
}
|
||||||
yaml, cliErr := exec.Command("./jx", "import", "--resource", "file://COPYRIGHT").Output()
|
yaml, cliErr := exec.Command("./jx", "import", "--resource", "file://COPYRIGHT").Output()
|
||||||
slog.Info("TestCli", "err", cliErr)
|
if cliErr != nil {
|
||||||
|
slog.Info("Debug CLI error", "error", cliErr, "stderr", cliErr.(*exec.ExitError).Stderr)
|
||||||
|
}
|
||||||
assert.Nil(t, cliErr)
|
assert.Nil(t, cliErr)
|
||||||
assert.NotEqual(t, "", string(yaml))
|
assert.NotEqual(t, "", string(yaml))
|
||||||
assert.Greater(t, len(yaml), 0)
|
assert.Greater(t, len(yaml), 0)
|
||||||
@ -47,7 +64,38 @@ resources:
|
|||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
yaml, cliErr := exec.Command("./jx", "import", "--resource", ts.URL).Output()
|
yaml, cliErr := exec.Command("./jx", "import", "--resource", ts.URL).Output()
|
||||||
|
if cliErr != nil {
|
||||||
|
slog.Info("Debug CLI error", "error", cliErr, "stderr", cliErr.(*exec.ExitError).Stderr)
|
||||||
|
}
|
||||||
assert.Nil(t, cliErr)
|
assert.Nil(t, cliErr)
|
||||||
assert.NotEqual(t, "", string(yaml))
|
assert.NotEqual(t, "", string(yaml))
|
||||||
assert.Greater(t, len(yaml), 0)
|
assert.Greater(t, len(yaml), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCliConfigSource(t *testing.T) {
|
||||||
|
if _, e := os.Stat("./jx"); errors.Is(e, os.ErrNotExist) {
|
||||||
|
t.Skip("cli not built")
|
||||||
|
}
|
||||||
|
|
||||||
|
configYaml := `
|
||||||
|
configurations:
|
||||||
|
- name: myhttpconnection
|
||||||
|
values:
|
||||||
|
http_user: foo
|
||||||
|
http_pass: bar
|
||||||
|
`
|
||||||
|
|
||||||
|
configPath := fmt.Sprintf("%s/testconfig.yaml", TempDir)
|
||||||
|
f, err := os.Create(configPath)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer f.Close()
|
||||||
|
_, writeErr := f.Write([]byte(configYaml))
|
||||||
|
assert.Nil(t, writeErr)
|
||||||
|
|
||||||
|
yaml, cliErr := exec.Command("./jx", "import", "--config", configPath, "--resource", "file://COPYRIGHT").Output()
|
||||||
|
if cliErr != nil {
|
||||||
|
slog.Info("Debug CLI error", "error", cliErr, "stderr", cliErr.(*exec.ExitError).Stderr)
|
||||||
|
}
|
||||||
|
assert.Nil(t, cliErr)
|
||||||
|
slog.Info("TestConfigSource", "yaml", yaml)
|
||||||
|
}
|
||||||
|
164
cmd/cli/main.go
164
cmd/cli/main.go
@ -4,18 +4,19 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"decl/internal/codec"
|
||||||
"os"
|
"decl/internal/config"
|
||||||
"flag"
|
|
||||||
"log/slog"
|
|
||||||
_ "errors"
|
|
||||||
"fmt"
|
|
||||||
_ "gopkg.in/yaml.v3"
|
|
||||||
"decl/internal/resource"
|
"decl/internal/resource"
|
||||||
"decl/internal/source"
|
"decl/internal/source"
|
||||||
"decl/internal/target"
|
"decl/internal/target"
|
||||||
"decl/internal/codec"
|
_ "errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
_ "gopkg.in/yaml.v3"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -25,8 +26,8 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
version string
|
version string
|
||||||
commit string
|
commit string
|
||||||
date string
|
date string
|
||||||
)
|
)
|
||||||
|
|
||||||
var GlobalOformat *string
|
var GlobalOformat *string
|
||||||
@ -38,6 +39,9 @@ var ImportResource *string
|
|||||||
|
|
||||||
var ApplyDelete *bool
|
var ApplyDelete *bool
|
||||||
|
|
||||||
|
var ConfigPath string
|
||||||
|
|
||||||
|
var ConfigDoc *config.Document = config.NewDocument()
|
||||||
|
|
||||||
var ctx context.Context = context.Background()
|
var ctx context.Context = context.Background()
|
||||||
|
|
||||||
@ -45,21 +49,25 @@ type RunCommand func(cmd *flag.FlagSet, output io.Writer) error
|
|||||||
|
|
||||||
type SubCommand struct {
|
type SubCommand struct {
|
||||||
Name string
|
Name string
|
||||||
Run RunCommand
|
Run RunCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
var jxSubCommands = []SubCommand {
|
var jxSubCommands = []SubCommand{
|
||||||
{
|
{
|
||||||
Name: "diff",
|
Name: "diff",
|
||||||
Run: DiffSubCommand,
|
Run: DiffSubCommand,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "apply",
|
Name: "apply",
|
||||||
Run: ApplySubCommand,
|
Run: ApplySubCommand,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "import",
|
Name: "import",
|
||||||
Run: ImportSubCommand,
|
Run: ImportSubCommand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "config",
|
||||||
|
Run: ConfigSubCommand,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,19 +82,36 @@ func LoggerConfig() {
|
|||||||
var programLevel = new(slog.LevelVar)
|
var programLevel = new(slog.LevelVar)
|
||||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel}))
|
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel}))
|
||||||
slog.SetDefault(logger)
|
slog.SetDefault(logger)
|
||||||
if debugLogging,ok := os.LookupEnv("JX_DEBUG"); ok && debugLogging != "" {
|
if debugLogging, ok := os.LookupEnv("JX_DEBUG"); ok && debugLogging != "" {
|
||||||
programLevel.Set(slog.LevelDebug)
|
programLevel.Set(slog.LevelDebug)
|
||||||
} else {
|
} else {
|
||||||
programLevel.Set(slog.LevelError)
|
programLevel.Set(slog.LevelError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LoadConfigURI(uri string) []*config.Document {
|
||||||
|
slog.Info("LoadConfigURI()", "uri", uri)
|
||||||
|
if uri != "" {
|
||||||
|
cs, err := config.ConfigSourceTypes.New(uri)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed loading config document from source", "error", err)
|
||||||
|
}
|
||||||
|
extractConfigs, extractErr := cs.Extract(nil)
|
||||||
|
if extractErr != nil {
|
||||||
|
slog.Error("Failed loading configs from source", "error", extractErr)
|
||||||
|
}
|
||||||
|
return extractConfigs
|
||||||
|
}
|
||||||
|
return []*config.Document{config.NewDocument()}
|
||||||
|
}
|
||||||
|
|
||||||
func LoadSourceURI(uri string) []*resource.Document {
|
func LoadSourceURI(uri string) []*resource.Document {
|
||||||
slog.Info("loading ", "uri", uri)
|
slog.Info("loading ", "uri", uri)
|
||||||
if uri != "" {
|
if uri != "" {
|
||||||
ds, err := source.SourceTypes.New(uri)
|
ds, err := source.SourceTypes.New(uri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed loading document from source", "error", err)
|
slog.Error("Failed loading document from source", "error", err)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
extractDocuments, extractErr := ds.ExtractResources(nil)
|
extractDocuments, extractErr := ds.ExtractResources(nil)
|
||||||
if extractErr != nil {
|
if extractErr != nil {
|
||||||
@ -94,7 +119,32 @@ func LoadSourceURI(uri string) []*resource.Document {
|
|||||||
}
|
}
|
||||||
return extractDocuments
|
return extractDocuments
|
||||||
}
|
}
|
||||||
return []*resource.Document{ resource.NewDocument() }
|
return []*resource.Document{resource.NewDocument()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
||||||
|
e := cmd.Parse(os.Args[2:])
|
||||||
|
if e != nil { // returns ErrHelp
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("ConfigSubCommand", "configdoc", ConfigDoc)
|
||||||
|
for _, configSource := range cmd.Args() {
|
||||||
|
for _, argConfigDoc := range LoadConfigURI(configSource) {
|
||||||
|
ConfigDoc.Append(argConfigDoc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputTarget, err := config.ConfigTargetTypes.New(GlobalOutput)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed opening target", "error", err)
|
||||||
|
}
|
||||||
|
defer outputTarget.Close()
|
||||||
|
|
||||||
|
if outputErr := outputTarget.EmitResources([]*config.Document{ConfigDoc}, nil); outputErr != nil {
|
||||||
|
return outputErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
||||||
@ -105,23 +155,29 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ConfigPath != "" {
|
||||||
|
for _, argConfigDoc := range LoadConfigURI(ConfigPath) {
|
||||||
|
ConfigDoc.Append(argConfigDoc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
merged := resource.NewDocument()
|
merged := resource.NewDocument()
|
||||||
documents := make([]*resource.Document, 0, 100)
|
documents := make([]*resource.Document, 0, 100)
|
||||||
for _,source := range cmd.Args() {
|
for _, source := range cmd.Args() {
|
||||||
loaded := LoadSourceURI(source)
|
loaded := LoadSourceURI(source)
|
||||||
if loaded != nil {
|
if loaded != nil {
|
||||||
documents = append(documents, loaded...)
|
documents = append(documents, loaded...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
switch *GlobalOformat {
|
switch *GlobalOformat {
|
||||||
case FormatYaml:
|
case FormatYaml:
|
||||||
encoder = resource.NewYAMLEncoder(output)
|
encoder = resource.NewYAMLEncoder(output)
|
||||||
case FormatJson:
|
case FormatJson:
|
||||||
encoder = resource.NewJSONEncoder(output)
|
encoder = resource.NewJSONEncoder(output)
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
slog.Info("main.ImportResource", "args", os.Args, "output", GlobalOutput)
|
slog.Info("main.ImportResource", "args", os.Args, "output", GlobalOutput)
|
||||||
outputTarget, err := target.TargetTypes.New(GlobalOutput)
|
outputTarget, err := target.TargetTypes.New(GlobalOutput)
|
||||||
@ -134,7 +190,7 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
|||||||
documents = append(documents, resource.NewDocument())
|
documents = append(documents, resource.NewDocument())
|
||||||
}
|
}
|
||||||
|
|
||||||
for _,d := range documents {
|
for _, d := range documents {
|
||||||
if d != nil {
|
if d != nil {
|
||||||
|
|
||||||
if *ImportResource != "" {
|
if *ImportResource != "" {
|
||||||
@ -153,7 +209,7 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
|||||||
|
|
||||||
if *GlobalQuiet {
|
if *GlobalQuiet {
|
||||||
for _, dr := range d.Resources() {
|
for _, dr := range d.Resources() {
|
||||||
if _,e := output.Write([]byte(dr.Resource().URI())); e != nil {
|
if _, e := output.Write([]byte(dr.Resource().URI())); e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -182,17 +238,27 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
|||||||
if e := cmd.Parse(os.Args[2:]); e != nil {
|
if e := cmd.Parse(os.Args[2:]); e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ConfigPath != "" {
|
||||||
|
for _, argConfigDoc := range LoadConfigURI(ConfigPath) {
|
||||||
|
ConfigDoc.Append(argConfigDoc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var encoder codec.Encoder
|
var encoder codec.Encoder
|
||||||
documents := make([]*resource.Document, 0, 100)
|
documents := make([]*resource.Document, 0, 100)
|
||||||
for _,source := range cmd.Args() {
|
for _, source := range cmd.Args() {
|
||||||
loaded := LoadSourceURI(source)
|
loaded := LoadSourceURI(source)
|
||||||
if loaded != nil {
|
if loaded != nil {
|
||||||
documents = append(documents, loaded...)
|
documents = append(documents, loaded...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("main.Apply()", "documents", documents)
|
slog.Info("main.Apply()", "documents", documents, "configdoc", ConfigDoc)
|
||||||
for _,d := range documents {
|
for _, d := range documents {
|
||||||
|
|
||||||
|
d.SetConfig(ConfigDoc)
|
||||||
|
|
||||||
slog.Info("main.Apply()", "doc", d)
|
slog.Info("main.Apply()", "doc", d)
|
||||||
var overrideState string = ""
|
var overrideState string = ""
|
||||||
if *ApplyDelete {
|
if *ApplyDelete {
|
||||||
@ -212,7 +278,7 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
|||||||
}
|
}
|
||||||
if *GlobalQuiet {
|
if *GlobalQuiet {
|
||||||
for _, dr := range d.Resources() {
|
for _, dr := range d.Resources() {
|
||||||
if _,e := output.Write([]byte(dr.Resource().URI())); e != nil {
|
if _, e := output.Write([]byte(dr.Resource().URI())); e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -242,7 +308,7 @@ func DiffSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
|||||||
for i, doc := range rightDocuments {
|
for i, doc := range rightDocuments {
|
||||||
if doc != nil {
|
if doc != nil {
|
||||||
leftDocuments = append(leftDocuments, doc.Clone())
|
leftDocuments = append(leftDocuments, doc.Clone())
|
||||||
for _,resourceDeclaration := range leftDocuments[i].Resources() {
|
for _, resourceDeclaration := range leftDocuments[i].Resources() {
|
||||||
if _, e := resourceDeclaration.Resource().Read(ctx); e != nil {
|
if _, e := resourceDeclaration.Resource().Read(ctx); e != nil {
|
||||||
slog.Info("jx diff ", "err", e)
|
slog.Info("jx diff ", "err", e)
|
||||||
//return e
|
//return e
|
||||||
@ -262,20 +328,20 @@ func DiffSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if index >= len(rightDocuments) {
|
if index >= len(rightDocuments) {
|
||||||
if _,e := leftDocuments[index].Diff(resource.NewDocument(), output); e != nil {
|
if _, e := leftDocuments[index].Diff(resource.NewDocument(), output); e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
index++
|
index++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if index >= len(leftDocuments) {
|
if index >= len(leftDocuments) {
|
||||||
if _,e := resource.NewDocument().Diff(rightDocuments[index], output); e != nil {
|
if _, e := resource.NewDocument().Diff(rightDocuments[index], output); e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
index++
|
index++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _,e := leftDocuments[index].Diff(rightDocuments[index], output); e != nil {
|
if _, e := leftDocuments[index].Diff(rightDocuments[index], output); e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
index++
|
index++
|
||||||
@ -292,8 +358,21 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _,subCmd := range jxSubCommands {
|
DefaultConfigurations, configErr := config.Configurations()
|
||||||
|
if configErr != nil {
|
||||||
|
slog.Error("Failed loading default configuration", "error", configErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, argConfigDoc := range DefaultConfigurations {
|
||||||
|
ConfigDoc.Append(argConfigDoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, subCmd := range jxSubCommands {
|
||||||
cmdFlagSet := flag.NewFlagSet(subCmd.Name, flag.ExitOnError)
|
cmdFlagSet := flag.NewFlagSet(subCmd.Name, flag.ExitOnError)
|
||||||
|
|
||||||
|
cmdFlagSet.StringVar(&ConfigPath, "config", "/etc/jx/config.yaml", "Config file path")
|
||||||
|
cmdFlagSet.StringVar(&ConfigPath, "c", "/etc/jx/config.yaml", "Config file path")
|
||||||
|
|
||||||
GlobalOformat = cmdFlagSet.String("oformat", "yaml", "Output serialization format")
|
GlobalOformat = cmdFlagSet.String("oformat", "yaml", "Output serialization format")
|
||||||
cmdFlagSet.StringVar(&GlobalOutput, "output", "-", "Output target (default stdout)")
|
cmdFlagSet.StringVar(&GlobalOutput, "output", "-", "Output target (default stdout)")
|
||||||
cmdFlagSet.StringVar(&GlobalOutput, "o", "-", "Output target (default stdout)")
|
cmdFlagSet.StringVar(&GlobalOutput, "o", "-", "Output target (default stdout)")
|
||||||
@ -318,7 +397,14 @@ func main() {
|
|||||||
cmdFlagSet.PrintDefaults()
|
cmdFlagSet.PrintDefaults()
|
||||||
VersionUsage()
|
VersionUsage()
|
||||||
}
|
}
|
||||||
|
case "config":
|
||||||
|
cmdFlagSet.Usage = func() {
|
||||||
|
fmt.Println("jx config source...")
|
||||||
|
cmdFlagSet.PrintDefaults()
|
||||||
|
VersionUsage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
slog.Info("CLI", "cmd", subCmd.Name)
|
||||||
if os.Args[1] == subCmd.Name {
|
if os.Args[1] == subCmd.Name {
|
||||||
if e := subCmd.Run(cmdFlagSet, os.Stdout); e != nil {
|
if e := subCmd.Run(cmdFlagSet, os.Stdout); e != nil {
|
||||||
slog.Error("Failed running command", "command", os.Args[1], "error", e)
|
slog.Error("Failed running command", "command", os.Args[1], "error", e)
|
||||||
|
12
examples/golangci-lint.jx.yaml
Normal file
12
examples/golangci-lint.jx.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
resources:
|
||||||
|
- type: file
|
||||||
|
transition: create
|
||||||
|
attributes:
|
||||||
|
path: golangci-lint-1.55.2-linux-amd64.deb
|
||||||
|
sourceref: https://github.com/golangci/golangci-lint/releases/download/v1.55.2/golangci-lint-1.55.2-linux-amd64.deb
|
||||||
|
- type: package
|
||||||
|
transition: create
|
||||||
|
attributes:
|
||||||
|
name: golangci-lint
|
||||||
|
source: golangci-lint-1.55.2-linux-amd64.deb
|
||||||
|
type: deb
|
2
go.mod
2
go.mod
@ -1,6 +1,6 @@
|
|||||||
module decl
|
module decl
|
||||||
|
|
||||||
go 1.22.1
|
go 1.22.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.rosskeen.house/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3
|
gitea.rosskeen.house/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3
|
||||||
|
134
internal/config/block.go
Normal file
134
internal/config/block.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"log/slog"
|
||||||
|
"decl/internal/codec"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BlockType struct {
|
||||||
|
Name string `json:"name" yaml:"name"`
|
||||||
|
Type TypeName `json:"type" yaml:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Block struct {
|
||||||
|
Name string `json:"name" yaml:"name"`
|
||||||
|
Type TypeName `json:"type" yaml:"type"`
|
||||||
|
Values Configuration `json:"values" yaml:"values"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBlock() *Block {
|
||||||
|
return &Block{ Type: "generic" }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) Clone() *Block {
|
||||||
|
return &Block {
|
||||||
|
Type: b.Type,
|
||||||
|
Values: b.Values.Clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) Load(r io.Reader) error {
|
||||||
|
return codec.NewYAMLDecoder(r).Decode(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) LoadBlock(yamlBlock string) (err error) {
|
||||||
|
err = codec.NewYAMLStringDecoder(yamlBlock).Decode(b)
|
||||||
|
slog.Info("LoadBlock()", "yaml", yamlBlock, "object", b, "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) NewConfiguration() error {
|
||||||
|
uri := fmt.Sprintf("%s://", b.Type)
|
||||||
|
newConfig, err := ConfigTypes.New(uri)
|
||||||
|
b.Values = newConfig
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) GetValue(key string) (any, error) {
|
||||||
|
return b.Values.GetValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) Configuration() Configuration {
|
||||||
|
return b.Values
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) SetURI(uri string) (e error) {
|
||||||
|
b.Values = NewConfiguration(uri)
|
||||||
|
if b.Values == nil {
|
||||||
|
return ErrUnknownConfigurationType
|
||||||
|
}
|
||||||
|
b.Type = TypeName(b.Values.Type())
|
||||||
|
_,e = b.Values.Read(context.Background())
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (b *Block) UnmarshalValue(value *BlockType) error {
|
||||||
|
b.Name = value.Name
|
||||||
|
if value.Type == "" {
|
||||||
|
b.Type = "generic"
|
||||||
|
} else {
|
||||||
|
b.Type = value.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig, configErr := ConfigTypes.New(fmt.Sprintf("%s://", b.Type))
|
||||||
|
if configErr != nil {
|
||||||
|
return configErr
|
||||||
|
}
|
||||||
|
b.Values = newConfig
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
t := &BlockType{}
|
||||||
|
if unmarshalConfigurationTypeErr := value.Decode(t); unmarshalConfigurationTypeErr != nil {
|
||||||
|
return unmarshalConfigurationTypeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.UnmarshalValue(t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configurationVals := struct {
|
||||||
|
Values yaml.Node `json:"values"`
|
||||||
|
}{}
|
||||||
|
if unmarshalValuesErr := value.Decode(&configurationVals); unmarshalValuesErr != nil {
|
||||||
|
return unmarshalValuesErr
|
||||||
|
}
|
||||||
|
if unmarshalConfigurationErr := configurationVals.Values.Decode(b.Values); unmarshalConfigurationErr != nil {
|
||||||
|
return unmarshalConfigurationErr
|
||||||
|
}
|
||||||
|
_, readErr := b.Values.Read(context.Background())
|
||||||
|
return readErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) UnmarshalJSON(data []byte) error {
|
||||||
|
t := &BlockType{}
|
||||||
|
if unmarshalConfigurationTypeErr := json.Unmarshal(data, t); unmarshalConfigurationTypeErr != nil {
|
||||||
|
return unmarshalConfigurationTypeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.UnmarshalValue(t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configurationVals := struct {
|
||||||
|
Values Configuration `json:"values"`
|
||||||
|
}{Values: b.Values}
|
||||||
|
if unmarshalValuesErr := json.Unmarshal(data, &configurationVals); unmarshalValuesErr != nil {
|
||||||
|
return unmarshalValuesErr
|
||||||
|
}
|
||||||
|
_, readErr := b.Values.Read(context.Background())
|
||||||
|
return readErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) MarshalYAML() (any, error) {
|
||||||
|
return b, nil
|
||||||
|
}
|
49
internal/config/block_test.go
Normal file
49
internal/config/block_test.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var TempDir string
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
var err error
|
||||||
|
TempDir, err = os.MkdirTemp("", "testconfig")
|
||||||
|
if err != nil || TempDir == "" {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rc := m.Run()
|
||||||
|
|
||||||
|
os.RemoveAll(TempDir)
|
||||||
|
os.Exit(rc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewBlock(t *testing.T) {
|
||||||
|
configYaml := `
|
||||||
|
name: "foo"
|
||||||
|
values:
|
||||||
|
http_user: "test"
|
||||||
|
http_pass: "password"
|
||||||
|
`
|
||||||
|
docReader := strings.NewReader(configYaml)
|
||||||
|
|
||||||
|
block := NewBlock()
|
||||||
|
assert.NotNil(t, block)
|
||||||
|
assert.Nil(t, block.Load(docReader))
|
||||||
|
assert.Equal(t, "foo", block.Name)
|
||||||
|
val, err := block.GetValue("http_user")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, "test", val)
|
||||||
|
|
||||||
|
missingVal, missingErr := block.GetValue("content")
|
||||||
|
assert.ErrorIs(t, missingErr, ErrUnknownConfigurationKey)
|
||||||
|
assert.Nil(t, missingVal)
|
||||||
|
}
|
9
internal/config/configs/facter.yaml
Normal file
9
internal/config/configs/facter.yaml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
configurations:
|
||||||
|
- name: facts
|
||||||
|
type: exec
|
||||||
|
values:
|
||||||
|
path: /usr/bin/facter
|
||||||
|
args:
|
||||||
|
- "-j"
|
||||||
|
format: "json"
|
||||||
|
|
45
internal/config/configsource.go
Normal file
45
internal/config/configsource.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "context"
|
||||||
|
_ "encoding/json"
|
||||||
|
_ "fmt"
|
||||||
|
_ "gopkg.in/yaml.v3"
|
||||||
|
_ "net/url"
|
||||||
|
_ "regexp"
|
||||||
|
_ "strings"
|
||||||
|
_ "os"
|
||||||
|
_ "io"
|
||||||
|
_ "compress/gzip"
|
||||||
|
_ "archive/tar"
|
||||||
|
_ "errors"
|
||||||
|
_ "path/filepath"
|
||||||
|
_ "decl/internal/codec"
|
||||||
|
"embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigurationSelector func(b *Block) bool
|
||||||
|
|
||||||
|
type ConfigSource interface {
|
||||||
|
Type() string
|
||||||
|
|
||||||
|
Extract(filter ConfigurationSelector) ([]*Document, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigSource(uri string) ConfigSource {
|
||||||
|
s, e := ConfigSourceTypes.New(uri)
|
||||||
|
if e == nil {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed configs/*.yaml
|
||||||
|
var configFiles embed.FS
|
||||||
|
|
||||||
|
func Configurations() ([]*Document, error) {
|
||||||
|
fs := NewConfigFS(configFiles)
|
||||||
|
return fs.Extract(nil)
|
||||||
|
}
|
30
internal/config/configtarget.go
Normal file
30
internal/config/configtarget.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "context"
|
||||||
|
_ "encoding/json"
|
||||||
|
_ "fmt"
|
||||||
|
_ "gopkg.in/yaml.v3"
|
||||||
|
_ "net/url"
|
||||||
|
_ "regexp"
|
||||||
|
_ "strings"
|
||||||
|
_ "os"
|
||||||
|
_ "io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigTarget interface {
|
||||||
|
Type() string
|
||||||
|
|
||||||
|
EmitResources(documents []*Document, filter ConfigurationSelector) error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigTarget(uri string) ConfigTarget {
|
||||||
|
s, e := ConfigTargetTypes.New(uri)
|
||||||
|
if e == nil {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
48
internal/config/configuration.go
Normal file
48
internal/config/configuration.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
_ "net/url"
|
||||||
|
_ "decl/internal/codec"
|
||||||
|
_ "io"
|
||||||
|
"decl/internal/types"
|
||||||
|
"decl/internal/data"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnknownConfigurationType = errors.New("Unknown configuration type")
|
||||||
|
ErrUnknownConfigurationKey = errors.New("Unknown configuration key")
|
||||||
|
ConfigTypes *types.Types[Configuration] = types.New[Configuration]()
|
||||||
|
ConfigSourceTypes *types.Types[ConfigSource] = types.New[ConfigSource]()
|
||||||
|
ConfigTargetTypes *types.Types[ConfigTarget] = types.New[ConfigTarget]()
|
||||||
|
)
|
||||||
|
|
||||||
|
type TypeName string //`json:"type"`
|
||||||
|
|
||||||
|
type Configuration interface {
|
||||||
|
Type() string
|
||||||
|
data.Reader
|
||||||
|
GetValue(name string) (any, error)
|
||||||
|
Clone() Configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfiguration(uri string) Configuration {
|
||||||
|
c, e := ConfigTypes.New(uri)
|
||||||
|
if e == nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *TypeName) UnmarshalJSON(b []byte) error {
|
||||||
|
ConfigTypeName := strings.Trim(string(b), "\"")
|
||||||
|
if ConfigTypes.Has(ConfigTypeName) {
|
||||||
|
*n = TypeName(ConfigTypeName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%w: %s", ErrUnknownConfigurationType, ConfigTypeName)
|
||||||
|
}
|
22
internal/config/configuration_test.go
Normal file
22
internal/config/configuration_test.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "context"
|
||||||
|
_ "fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
_ "log"
|
||||||
|
_ "os"
|
||||||
|
_ "path/filepath"
|
||||||
|
_ "strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewConfiguration(t *testing.T) {
|
||||||
|
configurationUri := "generic://"
|
||||||
|
testConfig := NewConfiguration(configurationUri)
|
||||||
|
assert.NotNil(t, testConfig)
|
||||||
|
v, _ := testConfig.GetValue("foo")
|
||||||
|
assert.Nil(t, v)
|
||||||
|
}
|
202
internal/config/document.go
Normal file
202
internal/config/document.go
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
_ "net/url"
|
||||||
|
"github.com/sters/yaml-diff/yamldiff"
|
||||||
|
"strings"
|
||||||
|
"decl/internal/codec"
|
||||||
|
_ "context"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrConfigUndefinedName = errors.New("Config block is missing a defined name")
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigNamesMap[Value any] map[string]Value
|
||||||
|
|
||||||
|
type Document struct {
|
||||||
|
names ConfigNamesMap[*Block]
|
||||||
|
ConfigBlocks []Block `json:"configurations" yaml:"configurations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocument() *Document {
|
||||||
|
return &Document{ names: make(ConfigNamesMap[*Block]) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) Filter(filter ConfigurationSelector) []*Block {
|
||||||
|
configurations := make([]*Block, 0, len(d.ConfigBlocks))
|
||||||
|
for i := range d.ConfigBlocks {
|
||||||
|
filterConfig := &d.ConfigBlocks[i]
|
||||||
|
if filter == nil || filter(filterConfig) {
|
||||||
|
configurations = append(configurations, &d.ConfigBlocks[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return configurations
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) Clone() *Document {
|
||||||
|
clone := NewDocument()
|
||||||
|
clone.ConfigBlocks = make([]Block, len(d.ConfigBlocks))
|
||||||
|
for i, res := range d.ConfigBlocks {
|
||||||
|
clone.ConfigBlocks[i] = *res.Clone()
|
||||||
|
}
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) Load(r io.Reader) error {
|
||||||
|
c := codec.NewYAMLDecoder(r)
|
||||||
|
return c.Decode(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) Validate() error {
|
||||||
|
jsonDocument, jsonErr := d.JSON()
|
||||||
|
if jsonErr == nil {
|
||||||
|
s := NewSchema("document")
|
||||||
|
err := s.Validate(string(jsonDocument))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) Configurations() []Block {
|
||||||
|
return d.ConfigBlocks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) Generate(w io.Writer) error {
|
||||||
|
e := codec.NewYAMLEncoder(w)
|
||||||
|
err := e.Encode(d);
|
||||||
|
if err == nil {
|
||||||
|
return e.Close()
|
||||||
|
}
|
||||||
|
e.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) Append(doc *Document) {
|
||||||
|
if doc != nil {
|
||||||
|
for i := range doc.ConfigBlocks {
|
||||||
|
slog.Info("Document.Append()", "doc", doc, "block", doc.ConfigBlocks[i], "targetdoc", d)
|
||||||
|
d.AddConfigurationBlock(doc.ConfigBlocks[i].Name, doc.ConfigBlocks[i].Type, doc.ConfigBlocks[i].Values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) AddConfigurationBlock(configurationName string, configurationType TypeName, configuration Configuration) {
|
||||||
|
cfg := NewBlock()
|
||||||
|
cfg.Name = configurationName
|
||||||
|
cfg.Type = configurationType
|
||||||
|
cfg.Values = configuration
|
||||||
|
d.names[cfg.Name] = cfg
|
||||||
|
d.ConfigBlocks = append(d.ConfigBlocks, *cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) AddConfiguration(uri string) error {
|
||||||
|
cfg := NewBlock()
|
||||||
|
if e := cfg.SetURI(uri); e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if cfg.Name == "" {
|
||||||
|
return ErrConfigUndefinedName
|
||||||
|
}
|
||||||
|
d.names[cfg.Name] = cfg
|
||||||
|
d.ConfigBlocks = append(d.ConfigBlocks, *cfg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) Has(name string) bool {
|
||||||
|
_, ok := d.names[name]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) Get(name string) *Block {
|
||||||
|
return d.names[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) JSON() ([]byte, error) {
|
||||||
|
return json.Marshal(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) YAML() ([]byte, error) {
|
||||||
|
return yaml.Marshal(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) IndexName() error {
|
||||||
|
for _, b := range d.ConfigBlocks {
|
||||||
|
d.names[b.Name] = &b
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
documentBlocks := struct {
|
||||||
|
ConfigBlocks *[]Block `json:"configurations" yaml:"configurations"`
|
||||||
|
}{ ConfigBlocks: &d.ConfigBlocks }
|
||||||
|
if unmarshalDocumentErr := value.Decode(documentBlocks); unmarshalDocumentErr != nil {
|
||||||
|
return unmarshalDocumentErr
|
||||||
|
}
|
||||||
|
return d.IndexName()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) UnmarshalJSON(data []byte) error {
|
||||||
|
documentBlocks := struct {
|
||||||
|
ConfigBlocks *[]Block `json:"configurations" yaml:"configurations"`
|
||||||
|
}{ ConfigBlocks: &d.ConfigBlocks }
|
||||||
|
if unmarshalDocumentErr := json.Unmarshal(data, &documentBlocks); unmarshalDocumentErr != nil {
|
||||||
|
return unmarshalDocumentErr
|
||||||
|
}
|
||||||
|
return d.IndexName()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) Diff(with *Document, output io.Writer) (returnOutput string, diffErr error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
returnOutput = ""
|
||||||
|
diffErr = fmt.Errorf("%s", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
slog.Info("Document.Diff()")
|
||||||
|
opts := []yamldiff.DoOptionFunc{}
|
||||||
|
if output == nil {
|
||||||
|
output = &strings.Builder{}
|
||||||
|
}
|
||||||
|
ydata, yerr := d.YAML()
|
||||||
|
if yerr != nil {
|
||||||
|
return "", yerr
|
||||||
|
}
|
||||||
|
yamlDiff,yamlDiffErr := yamldiff.Load(string(ydata))
|
||||||
|
if yamlDiffErr != nil {
|
||||||
|
return "", yamlDiffErr
|
||||||
|
}
|
||||||
|
|
||||||
|
wdata,werr := with.YAML()
|
||||||
|
if werr != nil {
|
||||||
|
return "", werr
|
||||||
|
}
|
||||||
|
withDiff,withDiffErr := yamldiff.Load(string(wdata))
|
||||||
|
if withDiffErr != nil {
|
||||||
|
return "", withDiffErr
|
||||||
|
}
|
||||||
|
|
||||||
|
for _,docDiffResults := range yamldiff.Do(yamlDiff, withDiff, opts...) {
|
||||||
|
slog.Info("Diff()", "diff", docDiffResults, "dump", docDiffResults.Dump())
|
||||||
|
_,e := output.Write([]byte(docDiffResults.Dump()))
|
||||||
|
if e != nil {
|
||||||
|
return "", e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slog.Info("Document.Diff() ", "document.yaml", ydata, "with.yaml", wdata)
|
||||||
|
if stringOutput, ok := output.(*strings.Builder); ok {
|
||||||
|
return stringOutput.String(), nil
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
53
internal/config/document_test.go
Normal file
53
internal/config/document_test.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewDocument(t *testing.T) {
|
||||||
|
d := NewDocument()
|
||||||
|
assert.NotNil(t, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocumentLoader(t *testing.T) {
|
||||||
|
document := `
|
||||||
|
---
|
||||||
|
configurations:
|
||||||
|
- type: generic
|
||||||
|
name: global
|
||||||
|
values:
|
||||||
|
install_dir: /opt/jx
|
||||||
|
- name: system
|
||||||
|
values:
|
||||||
|
dist: ubuntu
|
||||||
|
release: focal
|
||||||
|
`
|
||||||
|
d := NewDocument()
|
||||||
|
assert.NotNil(t, d)
|
||||||
|
|
||||||
|
docReader := strings.NewReader(document)
|
||||||
|
|
||||||
|
e := d.Load(docReader)
|
||||||
|
assert.Nil(t, e)
|
||||||
|
|
||||||
|
configurations := d.Configurations()
|
||||||
|
assert.Equal(t, 2, len(configurations))
|
||||||
|
|
||||||
|
b := d.Get("system")
|
||||||
|
assert.NotNil(t, b)
|
||||||
|
cfg := b.Configuration()
|
||||||
|
value, valueErr := cfg.GetValue("dist")
|
||||||
|
assert.Nil(t, valueErr)
|
||||||
|
assert.Equal(t, "ubuntu", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocumentJSONSchema(t *testing.T) {
|
||||||
|
document := NewDocument()
|
||||||
|
document.ConfigBlocks = []Block{}
|
||||||
|
e := document.Validate()
|
||||||
|
assert.Nil(t, e)
|
||||||
|
}
|
121
internal/config/exec.go
Normal file
121
internal/config/exec.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"decl/internal/codec"
|
||||||
|
"decl/internal/command"
|
||||||
|
"encoding/json"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ConfigTypes.Register([]string{"exec"}, func(u *url.URL) Configuration {
|
||||||
|
x := NewExec()
|
||||||
|
return x
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Exec struct {
|
||||||
|
Path string `yaml:"path" json:"path"`
|
||||||
|
Args []command.CommandArg `yaml:"args" json:"args"`
|
||||||
|
ValuesFormat codec.Format `yaml:"format" json:"format"`
|
||||||
|
Values map[string]any `yaml:"values" json:"values"`
|
||||||
|
ReadCommand *command.Command `yaml:"-" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExec() *Exec {
|
||||||
|
x := &Exec{}
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Exec) Read(ctx context.Context) ([]byte, error) {
|
||||||
|
out, err := x.ReadCommand.Execute(x)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
exErr := x.ReadCommand.Extractor(out, x)
|
||||||
|
if exErr != nil {
|
||||||
|
return nil, exErr
|
||||||
|
}
|
||||||
|
return nil, exErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Exec) Load(r io.Reader) (err error) {
|
||||||
|
err = codec.NewYAMLDecoder(r).Decode(x)
|
||||||
|
if err == nil {
|
||||||
|
_, err = x.Read(context.Background())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Exec) LoadYAML(yamlData string) (err error) {
|
||||||
|
err = codec.NewYAMLStringDecoder(yamlData).Decode(x)
|
||||||
|
if err == nil {
|
||||||
|
_, err = x.Read(context.Background())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Exec) UnmarshalJSON(data []byte) error {
|
||||||
|
if unmarshalErr := json.Unmarshal(data, x); unmarshalErr != nil {
|
||||||
|
return unmarshalErr
|
||||||
|
}
|
||||||
|
x.NewReadConfigCommand()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Exec) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
type decodeExec Exec
|
||||||
|
if unmarshalErr := value.Decode((*decodeExec)(x)); unmarshalErr != nil {
|
||||||
|
return unmarshalErr
|
||||||
|
}
|
||||||
|
x.NewReadConfigCommand()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (x *Exec) Clone() Configuration {
|
||||||
|
clone := NewExec()
|
||||||
|
clone.Path = x.Path
|
||||||
|
clone.Args = x.Args
|
||||||
|
clone.ValuesFormat = x.ValuesFormat
|
||||||
|
clone.Values = x.Values
|
||||||
|
clone.ReadCommand = x.ReadCommand
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Exec) Type() string {
|
||||||
|
return "exec"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Exec) GetValue(name string) (result any, err error) {
|
||||||
|
var ok bool
|
||||||
|
if result, ok = x.Values[name]; !ok {
|
||||||
|
err = ErrUnknownConfigurationKey
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (ex *Exec) NewReadConfigCommand() {
|
||||||
|
ex.ReadCommand = command.NewCommand()
|
||||||
|
ex.ReadCommand.Path = ex.Path
|
||||||
|
ex.ReadCommand.Args = ex.Args
|
||||||
|
|
||||||
|
ex.ReadCommand.Extractor = func(out []byte, target any) error {
|
||||||
|
x := target.(*Exec)
|
||||||
|
switch x.ValuesFormat {
|
||||||
|
case codec.FormatYaml:
|
||||||
|
return codec.NewYAMLStringDecoder(string(out)).Decode(&x.Values)
|
||||||
|
case codec.FormatJson:
|
||||||
|
return codec.NewJSONStringDecoder(string(out)).Decode(&x.Values)
|
||||||
|
case codec.FormatProtoBuf:
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
137
internal/config/file.go
Normal file
137
internal/config/file.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "context"
|
||||||
|
_ "encoding/json"
|
||||||
|
_ "fmt"
|
||||||
|
_ "gopkg.in/yaml.v3"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"decl/internal/transport"
|
||||||
|
"decl/internal/codec"
|
||||||
|
_ "os"
|
||||||
|
"io"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigFile struct {
|
||||||
|
Path string `yaml:"path" json:"path"`
|
||||||
|
Format codec.Format `yaml:"format" json:"format"`
|
||||||
|
reader *transport.Reader `yaml:"-" json:"-"`
|
||||||
|
writer *transport.Writer `yaml:"-" json:"-"`
|
||||||
|
encoder codec.Encoder `yaml:"-" json:"-"`
|
||||||
|
decoder codec.Decoder `yaml:"-" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigFile() *ConfigFile {
|
||||||
|
return &ConfigFile{ Format: codec.FormatYaml }
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigFileFromURI(u *url.URL) *ConfigFile {
|
||||||
|
t := NewConfigFile()
|
||||||
|
if u.Scheme == "file" {
|
||||||
|
t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI()))
|
||||||
|
} else {
|
||||||
|
t.Path = filepath.Join(u.Hostname(), u.Path)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigFileSource(u *url.URL) *ConfigFile {
|
||||||
|
t := NewConfigFileFromURI(u)
|
||||||
|
t.reader,_ = transport.NewReader(u)
|
||||||
|
if formatErr := t.Format.Set(t.reader.ContentType()); formatErr != nil {
|
||||||
|
panic(formatErr)
|
||||||
|
}
|
||||||
|
t.decoder = codec.NewDecoder(t.reader, t.Format)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigFileTarget(u *url.URL) *ConfigFile {
|
||||||
|
t := NewConfigFileFromURI(u)
|
||||||
|
t.writer,_ = transport.NewWriter(u)
|
||||||
|
if formatErr := t.Format.Set(t.writer.ContentType()); formatErr != nil {
|
||||||
|
panic(formatErr)
|
||||||
|
}
|
||||||
|
t.encoder = codec.NewEncoder(t.writer, t.Format)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ConfigSourceTypes.Register([]string{"file"}, func(u *url.URL) ConfigSource {
|
||||||
|
return NewConfigFileSource(u)
|
||||||
|
})
|
||||||
|
|
||||||
|
ConfigSourceTypes.Register([]string{"pb","pb.gz","json","json.gz","yaml","yml","yaml.gz","yml.gz"}, func(u *url.URL) ConfigSource {
|
||||||
|
return NewConfigFileSource(u)
|
||||||
|
})
|
||||||
|
|
||||||
|
ConfigTargetTypes.Register([]string{"file"}, func(u *url.URL) ConfigTarget {
|
||||||
|
return NewConfigFileTarget(u)
|
||||||
|
})
|
||||||
|
|
||||||
|
ConfigTargetTypes.Register([]string{"pb","pb.gz","json","json.gz","yaml","yml","yaml.gz","yml.gz"}, func(u *url.URL) ConfigTarget {
|
||||||
|
return NewConfigFileTarget(u)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (c *ConfigFile) Type() string { return "file" }
|
||||||
|
|
||||||
|
func (c *ConfigFile) Extract(filter ConfigurationSelector) ([]*Document, error) {
|
||||||
|
documents := make([]*Document, 0, 100)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
c.reader.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
slog.Info("Extract()", "documents", documents)
|
||||||
|
index := 0
|
||||||
|
for {
|
||||||
|
doc := NewDocument()
|
||||||
|
e := c.decoder.Decode(doc)
|
||||||
|
if errors.Is(e, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if e != nil {
|
||||||
|
return documents, e
|
||||||
|
}
|
||||||
|
slog.Info("Extract()", "res", doc.ConfigBlocks[0].Values)
|
||||||
|
if validationErr := doc.Validate(); validationErr != nil {
|
||||||
|
return documents, validationErr
|
||||||
|
}
|
||||||
|
documents = append(documents, doc)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
return documents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigFile) EmitResources(documents []*Document, filter ConfigurationSelector) (error) {
|
||||||
|
defer func() {
|
||||||
|
c.encoder.Close()
|
||||||
|
c.writer.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, doc := range documents {
|
||||||
|
emitDoc := NewDocument()
|
||||||
|
if validationErr := doc.Validate(); validationErr != nil {
|
||||||
|
return validationErr
|
||||||
|
}
|
||||||
|
for _, block := range doc.Filter(filter) {
|
||||||
|
emitDoc.ConfigBlocks = append(emitDoc.ConfigBlocks, *block)
|
||||||
|
}
|
||||||
|
slog.Info("EmitResources", "doctarget", c, "encoder", c.encoder, "emit", emitDoc)
|
||||||
|
if documentErr := c.encoder.Encode(emitDoc); documentErr != nil {
|
||||||
|
slog.Info("EmitResources", "err", documentErr)
|
||||||
|
return documentErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigFile) Close() (error) {
|
||||||
|
return nil
|
||||||
|
}
|
109
internal/config/fs.go
Normal file
109
internal/config/fs.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "context"
|
||||||
|
_ "encoding/json"
|
||||||
|
_ "fmt"
|
||||||
|
_ "gopkg.in/yaml.v3"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"decl/internal/codec"
|
||||||
|
"os"
|
||||||
|
"io"
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
type ConfigFS struct {
|
||||||
|
Path string `yaml:"path" json:"path"`
|
||||||
|
subDirsStack []fs.FS `yaml:"-" json:"-"`
|
||||||
|
fsys fs.FS `yaml:"-" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigFS(fsys fs.FS) *ConfigFS {
|
||||||
|
return &ConfigFS{
|
||||||
|
subDirsStack: make([]fs.FS, 0, 100),
|
||||||
|
fsys: fsys,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ConfigSourceTypes.Register([]string{"fs"}, func(u *url.URL) ConfigSource {
|
||||||
|
|
||||||
|
t := NewConfigFS(nil)
|
||||||
|
t.Path,_ = filepath.Abs(filepath.Join(u.Hostname(), u.RequestURI()))
|
||||||
|
t.fsys = os.DirFS(t.Path)
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigFS) Type() string { return "fs" }
|
||||||
|
|
||||||
|
func (c *ConfigFS) ExtractDirectory(fsys fs.FS) ([]*Document, error) {
|
||||||
|
documents := make([]*Document, 0, 100)
|
||||||
|
|
||||||
|
files, err := fs.ReadDir(fsys, ".")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _,file := range files {
|
||||||
|
|
||||||
|
slog.Info("ConfigFS.ExtractDirectory", "file", file)
|
||||||
|
if file.IsDir() {
|
||||||
|
dir, subErr := fs.Sub(fsys, file.Name())
|
||||||
|
if subErr != nil {
|
||||||
|
return nil, subErr
|
||||||
|
}
|
||||||
|
c.subDirsStack = append(c.subDirsStack, dir)
|
||||||
|
} else {
|
||||||
|
fileHandle, fileErr := fsys.Open(file.Name())
|
||||||
|
if fileErr != nil {
|
||||||
|
return nil, fileErr
|
||||||
|
}
|
||||||
|
decoder := codec.NewYAMLDecoder(fileHandle)
|
||||||
|
doc := NewDocument()
|
||||||
|
e := decoder.Decode(doc)
|
||||||
|
if errors.Is(e, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if e != nil {
|
||||||
|
return documents, e
|
||||||
|
}
|
||||||
|
slog.Info("ConfigFS.ExtractDirectory", "doc", doc)
|
||||||
|
if validationErr := doc.Validate(); validationErr != nil {
|
||||||
|
return documents, validationErr
|
||||||
|
}
|
||||||
|
documents = append(documents, doc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return documents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigFS) Extract(filter ConfigurationSelector) ([]*Document, error) {
|
||||||
|
documents := make([]*Document, 0, 100)
|
||||||
|
|
||||||
|
path := c.fsys
|
||||||
|
c.subDirsStack = append(c.subDirsStack, path)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if len(c.subDirsStack) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var dirPath fs.FS
|
||||||
|
dirPath, c.subDirsStack = c.subDirsStack[len(c.subDirsStack) - 1], c.subDirsStack[:len(c.subDirsStack) - 1]
|
||||||
|
docs, dirErr := c.ExtractDirectory(dirPath)
|
||||||
|
if dirErr != nil {
|
||||||
|
return documents, dirErr
|
||||||
|
}
|
||||||
|
|
||||||
|
documents = append(documents, docs...)
|
||||||
|
}
|
||||||
|
return documents, nil
|
||||||
|
}
|
||||||
|
|
48
internal/config/generic.go
Normal file
48
internal/config/generic.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ConfigTypes.Register([]string{"generic"}, func(u *url.URL) Configuration {
|
||||||
|
g := NewGeneric()
|
||||||
|
return g
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Generic map[string]any
|
||||||
|
|
||||||
|
func NewGeneric() *Generic {
|
||||||
|
g := make(Generic)
|
||||||
|
return &g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generic) Clone() Configuration {
|
||||||
|
jsonGeneric, _ := json.Marshal(g)
|
||||||
|
clone := make(Generic)
|
||||||
|
if unmarshalErr := json.Unmarshal(jsonGeneric, &clone); unmarshalErr != nil {
|
||||||
|
panic(unmarshalErr)
|
||||||
|
}
|
||||||
|
return &clone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generic) Type() string {
|
||||||
|
return "generic"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generic) Read(context.Context) ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generic) GetValue(name string) (result any, err error) {
|
||||||
|
var ok bool
|
||||||
|
if result, ok = (*g)[name]; !ok {
|
||||||
|
err = ErrUnknownConfigurationKey
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
13
internal/config/generic_test.go
Normal file
13
internal/config/generic_test.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewGenericConfig(t *testing.T) {
|
||||||
|
g := NewGeneric()
|
||||||
|
assert.NotNil(t, g)
|
||||||
|
}
|
54
internal/config/schema.go
Normal file
54
internal/config/schema.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/xeipuuv/gojsonschema"
|
||||||
|
"strings"
|
||||||
|
"embed"
|
||||||
|
"net/http"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed schemas/*.schema.json
|
||||||
|
var schemaFiles embed.FS
|
||||||
|
|
||||||
|
type Schema struct {
|
||||||
|
schema gojsonschema.JSONLoader
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSchema(name string) *Schema {
|
||||||
|
path := fmt.Sprintf("file://schemas/%s.schema.json", name)
|
||||||
|
|
||||||
|
return &Schema{schema: gojsonschema.NewReferenceLoaderFileSystem(path, http.FS(schemaFiles))}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schema) Validate(source string) error {
|
||||||
|
loader := gojsonschema.NewStringLoader(source)
|
||||||
|
result, err := gojsonschema.Validate(s.schema, loader)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
slog.Info("schema error", "source", source, "schema", s.schema, "result", result, "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slog.Info("schema", "source", source, "schema", s.schema, "result", result, "err", err)
|
||||||
|
|
||||||
|
if !result.Valid() {
|
||||||
|
schemaErrors := strings.Builder{}
|
||||||
|
for _, err := range result.Errors() {
|
||||||
|
schemaErrors.WriteString(err.String() + "\n")
|
||||||
|
}
|
||||||
|
return errors.New(schemaErrors.String())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schema) ValidateSchema() error {
|
||||||
|
sl := gojsonschema.NewSchemaLoader()
|
||||||
|
sl.Validate = true
|
||||||
|
schemaErr := sl.AddSchemas(s.schema)
|
||||||
|
slog.Info("validate schema definition", "schemaloader", sl, "err", schemaErr)
|
||||||
|
return schemaErr
|
||||||
|
}
|
49
internal/config/schema_test.go
Normal file
49
internal/config/schema_test.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewSchema(t *testing.T) {
|
||||||
|
s := NewSchema("document")
|
||||||
|
assert.NotEqual(t, nil, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSchemaValidateJSON(t *testing.T) {
|
||||||
|
// ctx := context.Background()
|
||||||
|
s := NewSchema("block")
|
||||||
|
assert.NotNil(t, s)
|
||||||
|
|
||||||
|
assert.Nil(t, s.ValidateSchema())
|
||||||
|
|
||||||
|
configBlockYaml := `
|
||||||
|
type: "generic"
|
||||||
|
name: "foo"
|
||||||
|
values:
|
||||||
|
bar: quuz
|
||||||
|
`
|
||||||
|
|
||||||
|
testConfig := NewBlock()
|
||||||
|
e := testConfig.LoadBlock(configBlockYaml)
|
||||||
|
assert.Nil(t, e)
|
||||||
|
assert.Equal(t, "foo", testConfig.Name)
|
||||||
|
|
||||||
|
jsonDoc, jsonErr := json.Marshal(testConfig)
|
||||||
|
assert.Nil(t, jsonErr)
|
||||||
|
|
||||||
|
schemaErr := s.Validate(string(jsonDoc))
|
||||||
|
assert.Nil(t, schemaErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func TestSchemaValidateSchema(t *testing.T) {
|
||||||
|
s := NewSchema("document")
|
||||||
|
assert.NotNil(t, s)
|
||||||
|
|
||||||
|
assert.Nil(t, s.ValidateSchema())
|
||||||
|
}
|
||||||
|
*/
|
22
internal/config/schemas/block.schema.json
Normal file
22
internal/config/schemas/block.schema.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$id": "block.schema.json",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "block",
|
||||||
|
"type": "object",
|
||||||
|
"required": [ "name", "values" ],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Config block name",
|
||||||
|
"minLength": 2
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Config type name.",
|
||||||
|
"enum": [ "generic", "exec" ]
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
internal/config/schemas/document.schema.json
Normal file
19
internal/config/schemas/document.schema.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"$id": "document.schema.json",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "document",
|
||||||
|
"type": "object",
|
||||||
|
"required": [ "configurations" ],
|
||||||
|
"properties": {
|
||||||
|
"configurations": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Configurations list",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "$ref": "block.schema.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
11
internal/resource/config.go
Normal file
11
internal/resource/config.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigurationValueGetter interface {
|
||||||
|
GetValue(key string) (any, error)
|
||||||
|
}
|
||||||
|
|
@ -77,11 +77,12 @@ type Container struct {
|
|||||||
|
|
||||||
State string `yaml:"state,omitempty" json:"state,omitempty"`
|
State string `yaml:"state,omitempty" json:"state,omitempty"`
|
||||||
|
|
||||||
|
config ConfigurationValueGetter
|
||||||
apiClient ContainerClient
|
apiClient ContainerClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ResourceTypes.Register("container", func(u *url.URL) Resource {
|
ResourceTypes.Register([]string{"container"}, func(u *url.URL) Resource {
|
||||||
c := NewContainer(nil)
|
c := NewContainer(nil)
|
||||||
c.Name = filepath.Join(u.Hostname(), u.Path)
|
c.Name = filepath.Join(u.Hostname(), u.Path)
|
||||||
return c
|
return c
|
||||||
@ -158,6 +159,10 @@ func (c *Container) SetURI(uri string) error {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Container) UseConfig(config ConfigurationValueGetter) {
|
||||||
|
c.config = config
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Container) JSON() ([]byte, error) {
|
func (c *Container) JSON() ([]byte, error) {
|
||||||
return json.Marshal(c)
|
return json.Marshal(c)
|
||||||
}
|
}
|
||||||
@ -321,6 +326,14 @@ func (c *Container) Read(ctx context.Context) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if inspectErr := c.Inspect(ctx, containerID); inspectErr != nil {
|
||||||
|
return nil, fmt.Errorf("%w: container %s", inspectErr, containerID)
|
||||||
|
}
|
||||||
|
slog.Info("Read() ", "type", c.Type(), "name", c.Name, "Id", c.Id)
|
||||||
|
return yaml.Marshal(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) Inspect(ctx context.Context, containerID string) error {
|
||||||
containerJSON, err := c.apiClient.ContainerInspect(ctx, containerID)
|
containerJSON, err := c.apiClient.ContainerInspect(ctx, containerID)
|
||||||
if client.IsErrNotFound(err) {
|
if client.IsErrNotFound(err) {
|
||||||
c.State = "absent"
|
c.State = "absent"
|
||||||
@ -328,7 +341,11 @@ func (c *Container) Read(ctx context.Context) ([]byte, error) {
|
|||||||
c.State = "present"
|
c.State = "present"
|
||||||
c.Id = containerJSON.ID
|
c.Id = containerJSON.ID
|
||||||
if c.Name == "" {
|
if c.Name == "" {
|
||||||
c.Name = containerJSON.Name
|
if containerJSON.Name[0] == '/' {
|
||||||
|
c.Name = containerJSON.Name[1:]
|
||||||
|
} else {
|
||||||
|
c.Name = containerJSON.Name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
c.Path = containerJSON.Path
|
c.Path = containerJSON.Path
|
||||||
c.Image = containerJSON.Image
|
c.Image = containerJSON.Image
|
||||||
@ -343,8 +360,7 @@ func (c *Container) Read(ctx context.Context) ([]byte, error) {
|
|||||||
c.RestartCount = containerJSON.RestartCount
|
c.RestartCount = containerJSON.RestartCount
|
||||||
c.Driver = containerJSON.Driver
|
c.Driver = containerJSON.Driver
|
||||||
}
|
}
|
||||||
slog.Info("Read() ", "type", c.Type(), "name", c.Name, "Id", c.Id)
|
return nil
|
||||||
return yaml.Marshal(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) Delete(ctx context.Context) error {
|
func (c *Container) Delete(ctx context.Context) error {
|
||||||
|
@ -42,11 +42,12 @@ type ContainerImage struct {
|
|||||||
Comment string `json:"comment,omitempty" yaml:"comment,omitempty"`
|
Comment string `json:"comment,omitempty" yaml:"comment,omitempty"`
|
||||||
State string `yaml:"state,omitempty" json:"state,omitempty"`
|
State string `yaml:"state,omitempty" json:"state,omitempty"`
|
||||||
|
|
||||||
|
config ConfigurationValueGetter
|
||||||
apiClient ContainerImageClient
|
apiClient ContainerImageClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ResourceTypes.Register("container-image", func(u *url.URL) Resource {
|
ResourceTypes.Register([]string{"container-image"}, func(u *url.URL) Resource {
|
||||||
c := NewContainerImage(nil)
|
c := NewContainerImage(nil)
|
||||||
c.Name = strings.Join([]string{u.Hostname(), u.Path}, ":")
|
c.Name = strings.Join([]string{u.Hostname(), u.Path}, ":")
|
||||||
return c
|
return c
|
||||||
@ -122,6 +123,10 @@ func (c *ContainerImage) SetURI(uri string) error {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ContainerImage) UseConfig(config ConfigurationValueGetter) {
|
||||||
|
c.config = config
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ContainerImage) JSON() ([]byte, error) {
|
func (c *ContainerImage) JSON() ([]byte, error) {
|
||||||
return json.Marshal(c)
|
return json.Marshal(c)
|
||||||
}
|
}
|
||||||
|
@ -38,11 +38,12 @@ type ContainerNetwork struct {
|
|||||||
|
|
||||||
State string `yaml:"state"`
|
State string `yaml:"state"`
|
||||||
|
|
||||||
|
config ConfigurationValueGetter
|
||||||
apiClient ContainerNetworkClient
|
apiClient ContainerNetworkClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ResourceTypes.Register("container-network", func(u *url.URL) Resource {
|
ResourceTypes.Register([]string{"container-network"}, func(u *url.URL) Resource {
|
||||||
n := NewContainerNetwork(nil)
|
n := NewContainerNetwork(nil)
|
||||||
n.Name = filepath.Join(u.Hostname(), u.Path)
|
n.Name = filepath.Join(u.Hostname(), u.Path)
|
||||||
return n
|
return n
|
||||||
@ -115,6 +116,10 @@ func (n *ContainerNetwork) SetURI(uri string) error {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *ContainerNetwork) UseConfig(config ConfigurationValueGetter) {
|
||||||
|
n.config = config
|
||||||
|
}
|
||||||
|
|
||||||
func (n *ContainerNetwork) JSON() ([]byte, error) {
|
func (n *ContainerNetwork) JSON() ([]byte, error) {
|
||||||
return json.Marshal(n)
|
return json.Marshal(n)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
package resource
|
package resource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
_ "errors"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -12,18 +13,25 @@ import (
|
|||||||
_ "gitea.rosskeen.house/rosskeen.house/machine"
|
_ "gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
"gitea.rosskeen.house/pylon/luaruntime"
|
"gitea.rosskeen.house/pylon/luaruntime"
|
||||||
"decl/internal/codec"
|
"decl/internal/codec"
|
||||||
|
"decl/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ConfigName string
|
||||||
|
|
||||||
type DeclarationType struct {
|
type DeclarationType struct {
|
||||||
Type TypeName `json:"type" yaml:"type"`
|
Type TypeName `json:"type" yaml:"type"`
|
||||||
Transition string `json:"transition,omitempty" yaml:"transition,omitempty"`
|
Transition string `json:"transition,omitempty" yaml:"transition,omitempty"`
|
||||||
|
Config ConfigName `json:"config,omitempty" yaml:"config,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Declaration struct {
|
type Declaration struct {
|
||||||
Type TypeName `json:"type" yaml:"type"`
|
Type TypeName `json:"type" yaml:"type"`
|
||||||
Transition string `json:"transition,omitempty" yaml:"transition,omitempty"`
|
Transition string `json:"transition,omitempty" yaml:"transition,omitempty"`
|
||||||
Attributes Resource `json:"attributes" yaml:"attributes"`
|
Attributes Resource `json:"attributes" yaml:"attributes"`
|
||||||
|
Config ConfigName `json:"config,omitempty" yaml:"config,omitempty"`
|
||||||
runtime luaruntime.LuaRunner
|
runtime luaruntime.LuaRunner
|
||||||
|
document *Document
|
||||||
|
configBlock *config.Block
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourceLoader interface {
|
type ResourceLoader interface {
|
||||||
@ -38,6 +46,10 @@ func NewDeclaration() *Declaration {
|
|||||||
return &Declaration{}
|
return &Declaration{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Declaration) SetDocument(newDocument *Document) {
|
||||||
|
d.document = newDocument
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Declaration) ResolveId(ctx context.Context) string {
|
func (d *Declaration) ResolveId(ctx context.Context) string {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@ -58,6 +70,7 @@ func (d *Declaration) Clone() *Declaration {
|
|||||||
Transition: d.Transition,
|
Transition: d.Transition,
|
||||||
Attributes: d.Attributes.Clone(),
|
Attributes: d.Attributes.Clone(),
|
||||||
runtime: luaruntime.New(),
|
runtime: luaruntime.New(),
|
||||||
|
Config: d.Config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +123,15 @@ func (d *Declaration) Apply() (result error) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Declaration) SetConfig(configDoc *config.Document) {
|
||||||
|
if configDoc != nil {
|
||||||
|
if configDoc.Has(string(d.Config)) {
|
||||||
|
d.configBlock = configDoc.Get(string(d.Config))
|
||||||
|
d.Attributes.UseConfig(d.configBlock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Declaration) SetURI(uri string) error {
|
func (d *Declaration) SetURI(uri string) error {
|
||||||
slog.Info("Declaration.SetURI()", "uri", uri, "declaration", d)
|
slog.Info("Declaration.SetURI()", "uri", uri, "declaration", d)
|
||||||
d.Attributes = NewResource(uri)
|
d.Attributes = NewResource(uri)
|
||||||
@ -125,6 +147,7 @@ func (d *Declaration) SetURI(uri string) error {
|
|||||||
func (d *Declaration) UnmarshalValue(value *DeclarationType) error {
|
func (d *Declaration) UnmarshalValue(value *DeclarationType) error {
|
||||||
d.Type = value.Type
|
d.Type = value.Type
|
||||||
d.Transition = value.Transition
|
d.Transition = value.Transition
|
||||||
|
d.Config = value.Config
|
||||||
newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", value.Type))
|
newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", value.Type))
|
||||||
if resourceErr != nil {
|
if resourceErr != nil {
|
||||||
return resourceErr
|
return resourceErr
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
_ "log"
|
_ "log"
|
||||||
_ "os"
|
_ "os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"decl/internal/types"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -70,7 +71,7 @@ func TestDeclarationNewResource(t *testing.T) {
|
|||||||
assert.NotNil(t, resourceDeclaration)
|
assert.NotNil(t, resourceDeclaration)
|
||||||
|
|
||||||
errNewUnknownResource := resourceDeclaration.NewResource()
|
errNewUnknownResource := resourceDeclaration.NewResource()
|
||||||
assert.ErrorIs(t, errNewUnknownResource, ErrUnknownResourceType)
|
assert.ErrorIs(t, errNewUnknownResource, types.ErrUnknownType)
|
||||||
|
|
||||||
resourceDeclaration.Type = "file"
|
resourceDeclaration.Type = "file"
|
||||||
errNewFileResource := resourceDeclaration.NewResource()
|
errNewFileResource := resourceDeclaration.NewResource()
|
||||||
|
@ -12,15 +12,25 @@ _ "net/url"
|
|||||||
"github.com/sters/yaml-diff/yamldiff"
|
"github.com/sters/yaml-diff/yamldiff"
|
||||||
"strings"
|
"strings"
|
||||||
"decl/internal/codec"
|
"decl/internal/codec"
|
||||||
|
"decl/internal/types"
|
||||||
|
"decl/internal/config"
|
||||||
"context"
|
"context"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ResourceMap[Value any] map[string]Value
|
||||||
|
|
||||||
type Document struct {
|
type Document struct {
|
||||||
|
uris ResourceMap[*Declaration]
|
||||||
ResourceDecls []Declaration `json:"resources" yaml:"resources"`
|
ResourceDecls []Declaration `json:"resources" yaml:"resources"`
|
||||||
|
config *config.Document
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDocument() *Document {
|
func NewDocument() *Document {
|
||||||
return &Document{}
|
return &Document{ uris: make(ResourceMap[*Declaration]) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) Types() *types.Types[Resource] {
|
||||||
|
return ResourceTypes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Document) Filter(filter ResourceSelector) []*Declaration {
|
func (d *Document) Filter(filter ResourceSelector) []*Declaration {
|
||||||
@ -34,18 +44,35 @@ func (d *Document) Filter(filter ResourceSelector) []*Declaration {
|
|||||||
return resources
|
return resources
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Document) GetResource(uri string) *Declaration {
|
||||||
|
if decl, ok := d.uris[uri]; ok {
|
||||||
|
return decl
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Document) Clone() *Document {
|
func (d *Document) Clone() *Document {
|
||||||
clone := NewDocument()
|
clone := NewDocument()
|
||||||
|
clone.config = d.config
|
||||||
clone.ResourceDecls = make([]Declaration, len(d.ResourceDecls))
|
clone.ResourceDecls = make([]Declaration, len(d.ResourceDecls))
|
||||||
for i, res := range d.ResourceDecls {
|
for i, res := range d.ResourceDecls {
|
||||||
clone.ResourceDecls[i] = *res.Clone()
|
clone.ResourceDecls[i] = *res.Clone()
|
||||||
|
clone.ResourceDecls[i].SetDocument(clone)
|
||||||
|
clone.ResourceDecls[i].SetConfig(d.config)
|
||||||
}
|
}
|
||||||
return clone
|
return clone
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Document) Load(r io.Reader) error {
|
func (d *Document) Load(r io.Reader) (err error) {
|
||||||
c := codec.NewYAMLDecoder(r)
|
c := codec.NewYAMLDecoder(r)
|
||||||
return c.Decode(d);
|
err = c.Decode(d)
|
||||||
|
if err == nil {
|
||||||
|
for i := range d.ResourceDecls {
|
||||||
|
d.ResourceDecls[i].SetDocument(d)
|
||||||
|
d.ResourceDecls[i].SetConfig(d.config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Document) Validate() error {
|
func (d *Document) Validate() error {
|
||||||
@ -68,6 +95,14 @@ func (d *Document) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Document) SetConfig(config *config.Document) {
|
||||||
|
d.config = config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) ConfigDoc() *config.Document {
|
||||||
|
return d.config
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Document) Resources() []Declaration {
|
func (d *Document) Resources() []Declaration {
|
||||||
return d.ResourceDecls
|
return d.ResourceDecls
|
||||||
}
|
}
|
||||||
@ -95,6 +130,7 @@ func (d *Document) Apply(state string) error {
|
|||||||
if state != "" {
|
if state != "" {
|
||||||
d.ResourceDecls[idx].Transition = state
|
d.ResourceDecls[idx].Transition = state
|
||||||
}
|
}
|
||||||
|
d.ResourceDecls[idx].SetConfig(d.config)
|
||||||
if e := d.ResourceDecls[idx].Apply(); e != nil {
|
if e := d.ResourceDecls[idx].Apply(); e != nil {
|
||||||
slog.Error("Document.Apply() error applying resource", "index", idx, "uri", d.ResourceDecls[idx].Resource().URI(), "resource", d.ResourceDecls[idx].Resource(), "error", e)
|
slog.Error("Document.Apply() error applying resource", "index", idx, "uri", d.ResourceDecls[idx].Resource().URI(), "resource", d.ResourceDecls[idx].Resource(), "error", e)
|
||||||
return e
|
return e
|
||||||
@ -117,11 +153,17 @@ func (d *Document) Generate(w io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Document) MapResourceURI(uri string, declaration *Declaration) {
|
||||||
|
d.uris[uri] = declaration
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration Resource) {
|
func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration Resource) {
|
||||||
decl := NewDeclaration()
|
decl := NewDeclaration()
|
||||||
decl.Type = TypeName(resourceType)
|
decl.Type = TypeName(resourceType)
|
||||||
decl.Attributes = resourceDeclaration
|
decl.Attributes = resourceDeclaration
|
||||||
|
decl.SetDocument(d)
|
||||||
d.ResourceDecls = append(d.ResourceDecls, *decl)
|
d.ResourceDecls = append(d.ResourceDecls, *decl)
|
||||||
|
d.MapResourceURI(decl.Attributes.URI(), decl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Document) AddResource(uri string) error {
|
func (d *Document) AddResource(uri string) error {
|
||||||
@ -129,8 +171,9 @@ func (d *Document) AddResource(uri string) error {
|
|||||||
if e := decl.SetURI(uri); e != nil {
|
if e := decl.SetURI(uri); e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
decl.SetDocument(d)
|
||||||
d.ResourceDecls = append(d.ResourceDecls, *decl)
|
d.ResourceDecls = append(d.ResourceDecls, *decl)
|
||||||
|
d.MapResourceURI(decl.Attributes.URI(), decl)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import (
|
|||||||
|
|
||||||
func TestNewDocumentLoader(t *testing.T) {
|
func TestNewDocumentLoader(t *testing.T) {
|
||||||
d := NewDocument()
|
d := NewDocument()
|
||||||
assert.NotEqual(t, nil, d)
|
assert.NotNil(t, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDocumentLoader(t *testing.T) {
|
func TestDocumentLoader(t *testing.T) {
|
||||||
@ -109,7 +109,7 @@ resources:
|
|||||||
|
|
||||||
var documentYaml strings.Builder
|
var documentYaml strings.Builder
|
||||||
d := NewDocument()
|
d := NewDocument()
|
||||||
assert.NotEqual(t, nil, d)
|
assert.NotNil(t, d)
|
||||||
|
|
||||||
f, e := ResourceTypes.New("file://")
|
f, e := ResourceTypes.New("file://")
|
||||||
assert.Nil(t, e)
|
assert.Nil(t, e)
|
||||||
@ -120,7 +120,7 @@ resources:
|
|||||||
assert.Nil(t, readErr)
|
assert.Nil(t, readErr)
|
||||||
d.AddResourceDeclaration("file", f)
|
d.AddResourceDeclaration("file", f)
|
||||||
ey := d.Generate(&documentYaml)
|
ey := d.Generate(&documentYaml)
|
||||||
assert.Equal(t, nil, ey)
|
assert.Nil(t, ey)
|
||||||
|
|
||||||
assert.Greater(t, documentYaml.Len(), 0)
|
assert.Greater(t, documentYaml.Len(), 0)
|
||||||
assert.YAMLEq(t, expected, documentYaml.String())
|
assert.YAMLEq(t, expected, documentYaml.String())
|
||||||
|
@ -20,6 +20,8 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
"decl/internal/codec"
|
"decl/internal/codec"
|
||||||
|
"decl/internal/iofilter"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileType string
|
type FileType string
|
||||||
@ -41,7 +43,7 @@ var ErrInvalidFileOwner error = errors.New("Unknown User")
|
|||||||
var ErrInvalidFileGroup error = errors.New("Unknown Group")
|
var ErrInvalidFileGroup error = errors.New("Unknown Group")
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ResourceTypes.Register("file", func(u *url.URL) Resource {
|
ResourceTypes.Register([]string{"file"}, func(u *url.URL) Resource {
|
||||||
f := NewFile()
|
f := NewFile()
|
||||||
f.Path = filepath.Join(u.Hostname(), u.Path)
|
f.Path = filepath.Join(u.Hostname(), u.Path)
|
||||||
return f
|
return f
|
||||||
@ -62,11 +64,14 @@ type File struct {
|
|||||||
Mtime time.Time `json:"mtime,omitempty" yaml:"mtime,omitempty"`
|
Mtime time.Time `json:"mtime,omitempty" yaml:"mtime,omitempty"`
|
||||||
|
|
||||||
Content string `json:"content,omitempty" yaml:"content,omitempty"`
|
Content string `json:"content,omitempty" yaml:"content,omitempty"`
|
||||||
|
ContentSourceRef ResourceReference `json:"sourceref,omitempty" yaml:"sourceref,omitempty"`
|
||||||
Sha256 string `json:"sha256,omitempty" yaml:"sha256,omitempty"`
|
Sha256 string `json:"sha256,omitempty" yaml:"sha256,omitempty"`
|
||||||
Size int64 `json:"size,omitempty" yaml:"size,omitempty"`
|
Size int64 `json:"size,omitempty" yaml:"size,omitempty"`
|
||||||
Target string `json:"target,omitempty" yaml:"target,omitempty"`
|
Target string `json:"target,omitempty" yaml:"target,omitempty"`
|
||||||
FileType FileType `json:"filetype" yaml:"filetype"`
|
FileType FileType `json:"filetype" yaml:"filetype"`
|
||||||
State string `json:"state,omitempty" yaml:"state,omitempty"`
|
State string `json:"state,omitempty" yaml:"state,omitempty"`
|
||||||
|
SerializeContent bool `json:"serializecontent,omitempty" yaml:"serializecontent,omitempty"`
|
||||||
|
config ConfigurationValueGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourceFileInfo struct {
|
type ResourceFileInfo struct {
|
||||||
@ -76,7 +81,7 @@ type ResourceFileInfo struct {
|
|||||||
func NewFile() *File {
|
func NewFile() *File {
|
||||||
currentUser, _ := user.Current()
|
currentUser, _ := user.Current()
|
||||||
group, _ := user.LookupGroupId(currentUser.Gid)
|
group, _ := user.LookupGroupId(currentUser.Gid)
|
||||||
f := &File{ normalizePath: false, Owner: currentUser.Username, Group: group.Name, Mode: "0644", FileType: RegularFile}
|
f := &File{ normalizePath: false, Owner: currentUser.Username, Group: group.Name, Mode: "0644", FileType: RegularFile, SerializeContent: false }
|
||||||
slog.Info("NewFile()", "file", f)
|
slog.Info("NewFile()", "file", f)
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
@ -178,6 +183,10 @@ func (f *File) SetURI(uri string) error {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *File) UseConfig(config ConfigurationValueGetter) {
|
||||||
|
f.config = config
|
||||||
|
}
|
||||||
|
|
||||||
func (f *File) Validate() error {
|
func (f *File) Validate() error {
|
||||||
return fmt.Errorf("failed")
|
return fmt.Errorf("failed")
|
||||||
}
|
}
|
||||||
@ -211,6 +220,11 @@ func (f *File) ResolveId(ctx context.Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *File) NormalizePath() error {
|
func (f *File) NormalizePath() error {
|
||||||
|
if f.config != nil {
|
||||||
|
if prefixPath, configErr := f.config.GetValue("prefix"); configErr == nil {
|
||||||
|
f.Path = filepath.Join(prefixPath.(string), f.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
if f.normalizePath {
|
if f.normalizePath {
|
||||||
filePath, fileAbsErr := filepath.Abs(f.Path)
|
filePath, fileAbsErr := filepath.Abs(f.Path)
|
||||||
if fileAbsErr == nil {
|
if fileAbsErr == nil {
|
||||||
@ -257,6 +271,7 @@ func (f *ResourceFileInfo) Sys() any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *File) Create(ctx context.Context) error {
|
func (f *File) Create(ctx context.Context) error {
|
||||||
|
slog.Info("File.Create()", "file", f)
|
||||||
uid, uidErr := LookupUID(f.Owner)
|
uid, uidErr := LookupUID(f.Owner)
|
||||||
if uidErr != nil {
|
if uidErr != nil {
|
||||||
return fmt.Errorf("%w: unkwnon user %d", ErrInvalidFileOwner, uid)
|
return fmt.Errorf("%w: unkwnon user %d", ErrInvalidFileOwner, uid)
|
||||||
@ -284,6 +299,27 @@ func (f *File) Create(ctx context.Context) error {
|
|||||||
default:
|
default:
|
||||||
fallthrough
|
fallthrough
|
||||||
case RegularFile:
|
case RegularFile:
|
||||||
|
copyBuffer := make([]byte, 32 * 1024)
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
f.Size = 0
|
||||||
|
var contentReader io.ReadCloser
|
||||||
|
if len(f.Content) == 0 && len(f.ContentSourceRef) != 0 {
|
||||||
|
if refReader, err := f.ContentSourceRef.ContentReaderStream(); err == nil {
|
||||||
|
contentReader = refReader
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contentReader = io.NopCloser(strings.NewReader(f.Content))
|
||||||
|
}
|
||||||
|
|
||||||
|
sumReadData := iofilter.NewReader(contentReader, func(p []byte, readn int, readerr error) (n int, err error) {
|
||||||
|
hash.Write(p[:readn])
|
||||||
|
f.Size += int64(readn)
|
||||||
|
return readn, readerr
|
||||||
|
})
|
||||||
|
|
||||||
createdFile, e := os.Create(f.Path)
|
createdFile, e := os.Create(f.Path)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return e
|
return e
|
||||||
@ -292,16 +328,25 @@ func (f *File) Create(ctx context.Context) error {
|
|||||||
if chmodErr := createdFile.Chmod(os.FileMode(mode)); chmodErr != nil {
|
if chmodErr := createdFile.Chmod(os.FileMode(mode)); chmodErr != nil {
|
||||||
return chmodErr
|
return chmodErr
|
||||||
}
|
}
|
||||||
|
_, writeErr := io.CopyBuffer(createdFile, sumReadData, copyBuffer)
|
||||||
|
if writeErr != nil {
|
||||||
|
return fmt.Errorf("File.Create(): CopyBuffer failed %v %v: %w", createdFile, contentReader, writeErr)
|
||||||
|
}
|
||||||
|
/*
|
||||||
_, writeErr := createdFile.Write([]byte(f.Content))
|
_, writeErr := createdFile.Write([]byte(f.Content))
|
||||||
if writeErr != nil {
|
if writeErr != nil {
|
||||||
return writeErr
|
return writeErr
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
f.Sha256 = fmt.Sprintf("%x", hash.Sum(nil))
|
||||||
if !f.Mtime.IsZero() && !f.Atime.IsZero() {
|
if !f.Mtime.IsZero() && !f.Atime.IsZero() {
|
||||||
if chtimesErr := os.Chtimes(f.Path, f.Atime, f.Mtime); chtimesErr != nil {
|
if chtimesErr := os.Chtimes(f.Path, f.Atime, f.Mtime); chtimesErr != nil {
|
||||||
return chtimesErr
|
return chtimesErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if chownErr := os.Chown(f.Path, uid, gid); chownErr != nil {
|
if chownErr := os.Chown(f.Path, uid, gid); chownErr != nil {
|
||||||
return chownErr
|
return chownErr
|
||||||
}
|
}
|
||||||
@ -372,17 +417,19 @@ func (f *File) Read(ctx context.Context) ([]byte, error) {
|
|||||||
|
|
||||||
switch f.FileType {
|
switch f.FileType {
|
||||||
case RegularFile:
|
case RegularFile:
|
||||||
file, fileErr := os.Open(f.Path)
|
if len(f.ContentSourceRef) == 0 || f.SerializeContent {
|
||||||
if fileErr != nil {
|
file, fileErr := os.Open(f.Path)
|
||||||
panic(fileErr)
|
if fileErr != nil {
|
||||||
}
|
panic(fileErr)
|
||||||
|
}
|
||||||
|
|
||||||
fileContent, ioErr := io.ReadAll(file)
|
fileContent, ioErr := io.ReadAll(file)
|
||||||
if ioErr != nil {
|
if ioErr != nil {
|
||||||
panic(ioErr)
|
panic(ioErr)
|
||||||
|
}
|
||||||
|
f.Content = string(fileContent)
|
||||||
|
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fileContent))
|
||||||
}
|
}
|
||||||
f.Content = string(fileContent)
|
|
||||||
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fileContent))
|
|
||||||
case SymbolicLinkFile:
|
case SymbolicLinkFile:
|
||||||
linkTarget, pathErr := os.Readlink(f.Path)
|
linkTarget, pathErr := os.Readlink(f.Path)
|
||||||
if pathErr != nil {
|
if pathErr != nil {
|
||||||
|
@ -397,3 +397,54 @@ func TestFileDelete(t *testing.T) {
|
|||||||
assert.Nil(t, stater.Trigger("delete"))
|
assert.Nil(t, stater.Trigger("delete"))
|
||||||
assert.NoFileExists(t, file, nil)
|
assert.NoFileExists(t, file, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFileContentRef(t *testing.T) {
|
||||||
|
file, _ := filepath.Abs(filepath.Join(TempDir, "src.txt"))
|
||||||
|
copyFile, _ := filepath.Abs(filepath.Join(TempDir, "copy.txt"))
|
||||||
|
|
||||||
|
decl := fmt.Sprintf(`
|
||||||
|
path: "%s"
|
||||||
|
owner: "%s"
|
||||||
|
group: "%s"
|
||||||
|
mode: "0600"
|
||||||
|
content: |-
|
||||||
|
test line 1
|
||||||
|
test line 2
|
||||||
|
state: present
|
||||||
|
`, file, ProcessTestUserName, ProcessTestGroupName)
|
||||||
|
|
||||||
|
contentRef := fmt.Sprintf(`
|
||||||
|
path: "%s"
|
||||||
|
owner: "%s"
|
||||||
|
group: "%s"
|
||||||
|
mode: "0600"
|
||||||
|
sourceref: "file://%s"
|
||||||
|
state: present
|
||||||
|
`, file, ProcessTestUserName, ProcessTestGroupName, copyFile)
|
||||||
|
|
||||||
|
f := NewFile()
|
||||||
|
stater := f.StateMachine()
|
||||||
|
e := f.LoadDecl(decl)
|
||||||
|
assert.Nil(t, e)
|
||||||
|
assert.Equal(t, ProcessTestUserName, f.Owner)
|
||||||
|
|
||||||
|
assert.Nil(t, stater.Trigger("create"))
|
||||||
|
assert.FileExists(t, file, nil)
|
||||||
|
s, e := os.Stat(file)
|
||||||
|
assert.Nil(t, e)
|
||||||
|
|
||||||
|
assert.Greater(t, s.Size(), int64(0))
|
||||||
|
|
||||||
|
fr := NewFile()
|
||||||
|
loadErr := fr.LoadDecl(contentRef)
|
||||||
|
assert.Nil(t, loadErr)
|
||||||
|
assert.Equal(t, ProcessTestUserName, fr.Owner)
|
||||||
|
|
||||||
|
assert.Nil(t, fr.StateMachine().Trigger("create"))
|
||||||
|
assert.FileExists(t, file, nil)
|
||||||
|
_, statErr := os.Stat(file)
|
||||||
|
assert.Nil(t, statErr)
|
||||||
|
|
||||||
|
assert.Nil(t, stater.Trigger("delete"))
|
||||||
|
assert.NoFileExists(t, file, nil)
|
||||||
|
}
|
||||||
|
@ -19,8 +19,7 @@ _ "os"
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ResourceTypes.Register("http", HTTPFactory)
|
ResourceTypes.Register([]string{"http", "https"}, HTTPFactory)
|
||||||
ResourceTypes.Register("https", HTTPFactory)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func HTTPFactory(u *url.URL) Resource {
|
func HTTPFactory(u *url.URL) Resource {
|
||||||
@ -41,7 +40,10 @@ type HTTP struct {
|
|||||||
Endpoint string `yaml:"endpoint" json:"endpoint"`
|
Endpoint string `yaml:"endpoint" json:"endpoint"`
|
||||||
Headers []HTTPHeader `yaml:"headers,omitempty" json:"headers,omitempty"`
|
Headers []HTTPHeader `yaml:"headers,omitempty" json:"headers,omitempty"`
|
||||||
Body string `yaml:"body,omitempty" json:"body,omitempty"`
|
Body string `yaml:"body,omitempty" json:"body,omitempty"`
|
||||||
|
Status string `yaml:"status,omitempty" json:"status,omitempty"`
|
||||||
|
StatusCode int `yaml:"statuscode,omitempty" json:"statuscode,omitempty"`
|
||||||
State string `yaml:"state,omitempty" json:"state,omitempty"`
|
State string `yaml:"state,omitempty" json:"state,omitempty"`
|
||||||
|
config ConfigurationValueGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTP() *HTTP {
|
func NewHTTP() *HTTP {
|
||||||
@ -67,9 +69,22 @@ func (h *HTTP) StateMachine() machine.Stater {
|
|||||||
|
|
||||||
func (h *HTTP) Notify(m *machine.EventMessage) {
|
func (h *HTTP) Notify(m *machine.EventMessage) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
slog.Info("Notify()", "http", h, "m", m)
|
||||||
switch m.On {
|
switch m.On {
|
||||||
case machine.ENTERSTATEEVENT:
|
case machine.ENTERSTATEEVENT:
|
||||||
switch m.Dest {
|
switch m.Dest {
|
||||||
|
case "start_read":
|
||||||
|
if _,readErr := h.Read(ctx); readErr == nil {
|
||||||
|
if triggerErr := h.StateMachine().Trigger("state_read"); triggerErr == nil {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
h.State = "absent"
|
||||||
|
panic(triggerErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
h.State = "absent"
|
||||||
|
panic(readErr)
|
||||||
|
}
|
||||||
case "start_create":
|
case "start_create":
|
||||||
if e := h.Create(ctx); e == nil {
|
if e := h.Create(ctx); e == nil {
|
||||||
if triggerErr := h.stater.Trigger("created"); triggerErr == nil {
|
if triggerErr := h.stater.Trigger("created"); triggerErr == nil {
|
||||||
@ -77,7 +92,21 @@ func (h *HTTP) Notify(m *machine.EventMessage) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
h.State = "absent"
|
h.State = "absent"
|
||||||
case "present":
|
case "start_delete":
|
||||||
|
if deleteErr := h.Delete(ctx); deleteErr == nil {
|
||||||
|
if triggerErr := h.StateMachine().Trigger("deleted"); triggerErr == nil {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
h.State = "present"
|
||||||
|
panic(triggerErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
h.State = "present"
|
||||||
|
panic(deleteErr)
|
||||||
|
}
|
||||||
|
case "absent":
|
||||||
|
h.State = "absent"
|
||||||
|
case "present", "created", "read":
|
||||||
h.State = "present"
|
h.State = "present"
|
||||||
}
|
}
|
||||||
case machine.EXITSTATEEVENT:
|
case machine.EXITSTATEEVENT:
|
||||||
@ -96,6 +125,10 @@ func (h *HTTP) SetURI(uri string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) UseConfig(config ConfigurationValueGetter) {
|
||||||
|
h.config = config
|
||||||
|
}
|
||||||
|
|
||||||
func (h *HTTP) JSON() ([]byte, error) {
|
func (h *HTTP) JSON() ([]byte, error) {
|
||||||
return json.Marshal(h)
|
return json.Marshal(h)
|
||||||
}
|
}
|
||||||
@ -140,10 +173,18 @@ func (h *HTTP) Create(ctx context.Context) error {
|
|||||||
if reqErr != nil {
|
if reqErr != nil {
|
||||||
return reqErr
|
return reqErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tokenErr := h.ReadAuthorizationTokenFromConfig(req); tokenErr != nil {
|
||||||
|
slog.Error("ReadAuthorizationTokenFromConfig()", "error", tokenErr)
|
||||||
|
}
|
||||||
|
|
||||||
for _,header := range h.Headers {
|
for _,header := range h.Headers {
|
||||||
req.Header.Add(header.Name, header.Value)
|
req.Header.Add(header.Name, header.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.client.Do(req)
|
resp, err := h.client.Do(req)
|
||||||
|
h.Status = resp.Status
|
||||||
|
h.StatusCode = resp.StatusCode
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -151,6 +192,18 @@ func (h *HTTP) Create(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) ReadAuthorizationTokenFromConfig(req *http.Request) error {
|
||||||
|
if h.config != nil {
|
||||||
|
token, tokenErr := h.config.GetValue("authorization_token")
|
||||||
|
if tokenErr == nil {
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
}
|
||||||
|
slog.Info("ReadAuthorizationTokenFromConfig()", "error", tokenErr)
|
||||||
|
return tokenErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *HTTP) Read(ctx context.Context) ([]byte, error) {
|
func (h *HTTP) Read(ctx context.Context) ([]byte, error) {
|
||||||
req, reqErr := http.NewRequestWithContext(ctx, "GET", h.Endpoint, nil)
|
req, reqErr := http.NewRequestWithContext(ctx, "GET", h.Endpoint, nil)
|
||||||
if reqErr != nil {
|
if reqErr != nil {
|
||||||
@ -158,6 +211,11 @@ func (h *HTTP) Read(ctx context.Context) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
slog.Info("HTTP.Read() ", "request", req, "err", reqErr)
|
slog.Info("HTTP.Read() ", "request", req, "err", reqErr)
|
||||||
|
|
||||||
|
tokenErr := h.ReadAuthorizationTokenFromConfig(req)
|
||||||
|
if tokenErr != nil {
|
||||||
|
slog.Error("ReadAuthorizationTokenFromConfig()", "error", tokenErr)
|
||||||
|
}
|
||||||
|
|
||||||
if len(h.Headers) > 0 {
|
if len(h.Headers) > 0 {
|
||||||
for _,header := range h.Headers {
|
for _,header := range h.Headers {
|
||||||
req.Header.Add(header.Name, header.Value)
|
req.Header.Add(header.Name, header.Value)
|
||||||
@ -165,6 +223,9 @@ func (h *HTTP) Read(ctx context.Context) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.client.Do(req)
|
resp, err := h.client.Do(req)
|
||||||
|
slog.Info("Http.Read()", "response", resp, "error", err)
|
||||||
|
h.Status = resp.Status
|
||||||
|
h.StatusCode = resp.StatusCode
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -177,6 +238,10 @@ func (h *HTTP) Read(ctx context.Context) ([]byte, error) {
|
|||||||
return yaml.Marshal(h)
|
return yaml.Marshal(h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) Delete(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *HTTP) Type() string {
|
func (h *HTTP) Type() string {
|
||||||
return "http"
|
return "http"
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,11 @@ _ "os/exec"
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
"decl/internal/codec"
|
"decl/internal/codec"
|
||||||
|
"decl/internal/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ResourceTypes.Register("iptable", func(u *url.URL) Resource {
|
ResourceTypes.Register([]string{"iptable"}, func(u *url.URL) Resource {
|
||||||
i := NewIptable()
|
i := NewIptable()
|
||||||
i.Table = IptableName(u.Hostname())
|
i.Table = IptableName(u.Hostname())
|
||||||
if len(u.Path) > 0 {
|
if len(u.Path) > 0 {
|
||||||
@ -122,10 +123,12 @@ type Iptable struct {
|
|||||||
ChainLength uint `json:"-" yaml:"-"`
|
ChainLength uint `json:"-" yaml:"-"`
|
||||||
|
|
||||||
ResourceType IptableType `json:"resourcetype,omitempty" yaml:"resourcetype,omitempty"`
|
ResourceType IptableType `json:"resourcetype,omitempty" yaml:"resourcetype,omitempty"`
|
||||||
CreateCommand *Command `yaml:"-" json:"-"`
|
CreateCommand *command.Command `yaml:"-" json:"-"`
|
||||||
ReadCommand *Command `yaml:"-" json:"-"`
|
ReadCommand *command.Command `yaml:"-" json:"-"`
|
||||||
UpdateCommand *Command `yaml:"-" json:"-"`
|
UpdateCommand *command.Command `yaml:"-" json:"-"`
|
||||||
DeleteCommand *Command `yaml:"-" json:"-"`
|
DeleteCommand *command.Command `yaml:"-" json:"-"`
|
||||||
|
|
||||||
|
config ConfigurationValueGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIptable() *Iptable {
|
func NewIptable() *Iptable {
|
||||||
@ -203,6 +206,10 @@ func (i *Iptable) SetURI(uri string) error {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Iptable) UseConfig(config ConfigurationValueGetter) {
|
||||||
|
i.config = config
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Iptable) Validate() error {
|
func (i *Iptable) Validate() error {
|
||||||
s := NewSchema(i.Type())
|
s := NewSchema(i.Type())
|
||||||
jsonDoc, jsonErr := i.JSON()
|
jsonDoc, jsonErr := i.JSON()
|
||||||
@ -233,7 +240,7 @@ func (i *Iptable) UnmarshalYAML(value *yaml.Node) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Iptable) NewCRUD() (create *Command, read *Command, update *Command, del *Command) {
|
func (i *Iptable) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) {
|
||||||
return NewIptableCreateCommand(), NewIptableReadCommand(), NewIptableUpdateCommand(), NewIptableDeleteCommand()
|
return NewIptableCreateCommand(), NewIptableReadCommand(), NewIptableUpdateCommand(), NewIptableDeleteCommand()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,11 +410,11 @@ func (i *Iptable) MatchRule(flags []string) (match bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *Iptable) ReadChainLength() error {
|
func (i *Iptable) ReadChainLength() error {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "iptables"
|
c.Path = "iptables"
|
||||||
c.Args = []CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("-S"),
|
command.CommandArg("-S"),
|
||||||
CommandArg("{{ .Chain }}"),
|
command.CommandArg("{{ .Chain }}"),
|
||||||
}
|
}
|
||||||
output,err := c.Execute(i)
|
output,err := c.Execute(i)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -452,7 +459,7 @@ func (i *Iptable) Read(ctx context.Context) ([]byte, error) {
|
|||||||
|
|
||||||
func (i *Iptable) Type() string { return "iptable" }
|
func (i *Iptable) Type() string { return "iptable" }
|
||||||
|
|
||||||
func (i *IptableType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) {
|
func (i *IptableType) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) {
|
||||||
switch *i {
|
switch *i {
|
||||||
case IptableTypeRule:
|
case IptableTypeRule:
|
||||||
return NewIptableCreateCommand(), NewIptableReadCommand(), NewIptableUpdateCommand(), NewIptableDeleteCommand()
|
return NewIptableCreateCommand(), NewIptableReadCommand(), NewIptableUpdateCommand(), NewIptableDeleteCommand()
|
||||||
@ -489,24 +496,24 @@ func (i *IptableType) UnmarshalYAML(value *yaml.Node) error {
|
|||||||
return i.UnmarshalValue(s)
|
return i.UnmarshalValue(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIptableCreateCommand() *Command {
|
func NewIptableCreateCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "iptables"
|
c.Path = "iptables"
|
||||||
c.Args = []CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("-t"),
|
command.CommandArg("-t"),
|
||||||
CommandArg("{{ .Table }}"),
|
command.CommandArg("{{ .Table }}"),
|
||||||
CommandArg("{{ if le .Id .ChainLength }}-R{{ else }}-A{{ end }}"),
|
command.CommandArg("{{ if le .Id .ChainLength }}-R{{ else }}-A{{ end }}"),
|
||||||
CommandArg("{{ .Chain }}"),
|
command.CommandArg("{{ .Chain }}"),
|
||||||
CommandArg("{{ if le .Id .ChainLength }}{{ .Id }}{{ end }}"),
|
command.CommandArg("{{ if le .Id .ChainLength }}{{ .Id }}{{ end }}"),
|
||||||
CommandArg("{{ if .In }}-i {{ .In }}{{ else if .Out }}-o {{ .Out }}{{ end }}"),
|
command.CommandArg("{{ if .In }}-i {{ .In }}{{ else if .Out }}-o {{ .Out }}{{ end }}"),
|
||||||
CommandArg("{{ range .Match }}-m {{ . }} {{- end }}"),
|
command.CommandArg("{{ range .Match }}-m {{ . }} {{- end }}"),
|
||||||
CommandArg("{{ if .Source }}-s {{ .Source }}{{ end }}"),
|
command.CommandArg("{{ if .Source }}-s {{ .Source }}{{ end }}"),
|
||||||
CommandArg("{{ if .Sport }}--sport {{ .Sport }}{{ end }}"),
|
command.CommandArg("{{ if .Sport }}--sport {{ .Sport }}{{ end }}"),
|
||||||
CommandArg("{{ if .Destination }}-d {{ .Destination }}{{ end }}"),
|
command.CommandArg("{{ if .Destination }}-d {{ .Destination }}{{ end }}"),
|
||||||
CommandArg("{{ if .Dport }}--dport {{ .Dport }}{{ end }}"),
|
command.CommandArg("{{ if .Dport }}--dport {{ .Dport }}{{ end }}"),
|
||||||
CommandArg("{{ if .Proto }}-p {{ .Proto }}{{ end }}"),
|
command.CommandArg("{{ if .Proto }}-p {{ .Proto }}{{ end }}"),
|
||||||
CommandArg("{{ range .Flags }} --{{ .Name }} {{ .Value }} {{ end }}"),
|
command.CommandArg("{{ range .Flags }} --{{ .Name }} {{ .Value }} {{ end }}"),
|
||||||
CommandArg("{{ if .Jump }}-j {{ .Jump }}{{ end }}"),
|
command.CommandArg("{{ if .Jump }}-j {{ .Jump }}{{ end }}"),
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@ -539,15 +546,15 @@ func IptableExtractRule(lineNumber uint, ruleLine string, target *Iptable) (stat
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func NewIptableReadCommand() *Command {
|
func NewIptableReadCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "iptables"
|
c.Path = "iptables"
|
||||||
c.Args = []CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("-t"),
|
command.CommandArg("-t"),
|
||||||
CommandArg("{{ .Table }}"),
|
command.CommandArg("{{ .Table }}"),
|
||||||
CommandArg("-S"),
|
command.CommandArg("-S"),
|
||||||
CommandArg("{{ .Chain }}"),
|
command.CommandArg("{{ .Chain }}"),
|
||||||
CommandArg("{{ if .Id }}{{ .Id }}{{ end }}"),
|
command.CommandArg("{{ if .Id }}{{ .Id }}{{ end }}"),
|
||||||
}
|
}
|
||||||
c.Extractor = func(out []byte, target any) error {
|
c.Extractor = func(out []byte, target any) error {
|
||||||
i := target.(*Iptable)
|
i := target.(*Iptable)
|
||||||
@ -581,14 +588,14 @@ func NewIptableReadCommand() *Command {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIptableReadChainCommand() *Command {
|
func NewIptableReadChainCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "iptables"
|
c.Path = "iptables"
|
||||||
c.Args = []CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("-t"),
|
command.CommandArg("-t"),
|
||||||
CommandArg("{{ .Table }}"),
|
command.CommandArg("{{ .Table }}"),
|
||||||
CommandArg("-S"),
|
command.CommandArg("-S"),
|
||||||
CommandArg("{{ .Chain }}"),
|
command.CommandArg("{{ .Chain }}"),
|
||||||
}
|
}
|
||||||
c.Extractor = func(out []byte, target any) error {
|
c.Extractor = func(out []byte, target any) error {
|
||||||
IptableChainRules := target.(*[]*Iptable)
|
IptableChainRules := target.(*[]*Iptable)
|
||||||
@ -620,22 +627,22 @@ func NewIptableReadChainCommand() *Command {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIptableUpdateCommand() *Command {
|
func NewIptableUpdateCommand() *command.Command {
|
||||||
return NewIptableCreateCommand()
|
return NewIptableCreateCommand()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIptableDeleteCommand() *Command {
|
func NewIptableDeleteCommand() *command.Command {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIptableChainCreateCommand() *Command {
|
func NewIptableChainCreateCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "iptables"
|
c.Path = "iptables"
|
||||||
c.Args = []CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("-t"),
|
command.CommandArg("-t"),
|
||||||
CommandArg("{{ .Table }}"),
|
command.CommandArg("{{ .Table }}"),
|
||||||
CommandArg("-N"),
|
command.CommandArg("-N"),
|
||||||
CommandArg("{{ .Chain }}"),
|
command.CommandArg("{{ .Chain }}"),
|
||||||
}
|
}
|
||||||
c.Extractor = func(out []byte, target any) error {
|
c.Extractor = func(out []byte, target any) error {
|
||||||
slog.Info("IptableChain Extractor", "output", out, "command", c)
|
slog.Info("IptableChain Extractor", "output", out, "command", c)
|
||||||
@ -743,14 +750,14 @@ func RuleExtractorById(out []byte, target any) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIptableChainReadCommand() *Command {
|
func NewIptableChainReadCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "iptables"
|
c.Path = "iptables"
|
||||||
c.Args = []CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("-t"),
|
command.CommandArg("-t"),
|
||||||
CommandArg("{{ .Table }}"),
|
command.CommandArg("{{ .Table }}"),
|
||||||
CommandArg("-S"),
|
command.CommandArg("-S"),
|
||||||
CommandArg("{{ .Chain }}"),
|
command.CommandArg("{{ .Chain }}"),
|
||||||
}
|
}
|
||||||
c.Extractor = func(out []byte, target any) error {
|
c.Extractor = func(out []byte, target any) error {
|
||||||
i := target.(*Iptable)
|
i := target.(*Iptable)
|
||||||
@ -776,11 +783,11 @@ func NewIptableChainReadCommand() *Command {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIptableChainUpdateCommand() *Command {
|
func NewIptableChainUpdateCommand() *command.Command {
|
||||||
return NewIptableChainCreateCommand()
|
return NewIptableChainCreateCommand()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIptableChainDeleteCommand() *Command {
|
func NewIptableChainDeleteCommand() *command.Command {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ _ "strings"
|
|||||||
_ "syscall"
|
_ "syscall"
|
||||||
"testing"
|
"testing"
|
||||||
_ "time"
|
_ "time"
|
||||||
|
"decl/internal/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewIptableResource(t *testing.T) {
|
func TestNewIptableResource(t *testing.T) {
|
||||||
@ -65,7 +66,7 @@ func TestReadIptable(t *testing.T) {
|
|||||||
|
|
||||||
e := testRule.LoadDecl(declarationAttributes)
|
e := testRule.LoadDecl(declarationAttributes)
|
||||||
assert.Nil(t, e)
|
assert.Nil(t, e)
|
||||||
testRule.ReadCommand = (*Command)(m)
|
testRule.ReadCommand = (*command.Command)(m)
|
||||||
// testRuleErr := testRule.Apply()
|
// testRuleErr := testRule.Apply()
|
||||||
// assert.Nil(t, testRuleErr)
|
// assert.Nil(t, testRuleErr)
|
||||||
r, e := testRule.Read(ctx)
|
r, e := testRule.Read(ctx)
|
||||||
|
@ -8,9 +8,10 @@ _ "github.com/stretchr/testify/assert"
|
|||||||
_ "os"
|
_ "os"
|
||||||
_ "strings"
|
_ "strings"
|
||||||
_ "testing"
|
_ "testing"
|
||||||
|
"decl/internal/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MockCommand Command
|
type MockCommand command.Command
|
||||||
|
|
||||||
func (m *MockCommand) Execute(value any) ([]byte, error) {
|
func (m *MockCommand) Execute(value any) ([]byte, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -19,7 +19,7 @@ _ "strconv"
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ResourceTypes.Register("route", func(u *url.URL) Resource {
|
ResourceTypes.Register([]string{"route"}, func(u *url.URL) Resource {
|
||||||
n := NewNetworkRoute()
|
n := NewNetworkRoute()
|
||||||
return n
|
return n
|
||||||
})
|
})
|
||||||
@ -125,6 +125,7 @@ type NetworkRoute struct {
|
|||||||
DeleteCommand *Command `yaml:"-" json:"-"`
|
DeleteCommand *Command `yaml:"-" json:"-"`
|
||||||
|
|
||||||
State string `json:"state" yaml:"state"`
|
State string `json:"state" yaml:"state"`
|
||||||
|
config ConfigurationValueGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNetworkRoute() *NetworkRoute {
|
func NewNetworkRoute() *NetworkRoute {
|
||||||
@ -194,6 +195,10 @@ func (n *NetworkRoute) SetURI(uri string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NetworkRoute) UseConfig(config ConfigurationValueGetter) {
|
||||||
|
n.config = config
|
||||||
|
}
|
||||||
|
|
||||||
func (n *NetworkRoute) Validate() error {
|
func (n *NetworkRoute) Validate() error {
|
||||||
return fmt.Errorf("failed")
|
return fmt.Errorf("failed")
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
"decl/internal/codec"
|
"decl/internal/codec"
|
||||||
|
"decl/internal/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PackageType string
|
type PackageType string
|
||||||
@ -31,58 +32,44 @@ const (
|
|||||||
PackageTypeYum PackageType = "yum"
|
PackageTypeYum PackageType = "yum"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var SystemPackageType PackageType = FindSystemPackageType()
|
||||||
|
|
||||||
type Package struct {
|
type Package struct {
|
||||||
stater machine.Stater `yaml:"-" json:"-"`
|
stater machine.Stater `yaml:"-" json:"-"`
|
||||||
|
Source string `yaml:"source,omitempty" json:"source,omitempty"`
|
||||||
Name string `yaml:"name" json:"name"`
|
Name string `yaml:"name" json:"name"`
|
||||||
Required string `json:"required,omitempty" yaml:"required,omitempty"`
|
Required string `json:"required,omitempty" yaml:"required,omitempty"`
|
||||||
Version string `yaml:"version,omitempty" json:"version,omitempty"`
|
Version string `yaml:"version,omitempty" json:"version,omitempty"`
|
||||||
PackageType PackageType `yaml:"type" json:"type"`
|
PackageType PackageType `yaml:"type" json:"type"`
|
||||||
|
|
||||||
CreateCommand *Command `yaml:"-" json:"-"`
|
CreateCommand *command.Command `yaml:"-" json:"-"`
|
||||||
ReadCommand *Command `yaml:"-" json:"-"`
|
ReadCommand *command.Command `yaml:"-" json:"-"`
|
||||||
UpdateCommand *Command `yaml:"-" json:"-"`
|
UpdateCommand *command.Command `yaml:"-" json:"-"`
|
||||||
DeleteCommand *Command `yaml:"-" json:"-"`
|
DeleteCommand *command.Command `yaml:"-" json:"-"`
|
||||||
// state attributes
|
// state attributes
|
||||||
State string `yaml:"state,omitempty" json:"state,omitempty"`
|
State string `yaml:"state,omitempty" json:"state,omitempty"`
|
||||||
|
config ConfigurationValueGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ResourceTypes.Register("package", func(u *url.URL) Resource {
|
ResourceTypes.Register([]string{"package", string(PackageTypeApk), string(PackageTypeApt), string(PackageTypeDeb), string(PackageTypeDnf), string(PackageTypeRpm), string(PackageTypePip), string(PackageTypeYum)}, func(u *url.URL) Resource {
|
||||||
p := NewPackage()
|
|
||||||
return p
|
|
||||||
})
|
|
||||||
ResourceTypes.Register(string(PackageTypeApk), func(u *url.URL) Resource {
|
|
||||||
p := NewPackage()
|
|
||||||
return p
|
|
||||||
})
|
|
||||||
ResourceTypes.Register(string(PackageTypeApt), func(u *url.URL) Resource {
|
|
||||||
p := NewPackage()
|
|
||||||
return p
|
|
||||||
})
|
|
||||||
ResourceTypes.Register(string(PackageTypeDeb), func(u *url.URL) Resource {
|
|
||||||
p := NewPackage()
|
|
||||||
return p
|
|
||||||
})
|
|
||||||
ResourceTypes.Register(string(PackageTypeDnf), func(u *url.URL) Resource {
|
|
||||||
p := NewPackage()
|
|
||||||
return p
|
|
||||||
})
|
|
||||||
ResourceTypes.Register(string(PackageTypeRpm), func(u *url.URL) Resource {
|
|
||||||
p := NewPackage()
|
|
||||||
return p
|
|
||||||
})
|
|
||||||
ResourceTypes.Register(string(PackageTypePip), func(u *url.URL) Resource {
|
|
||||||
p := NewPackage()
|
|
||||||
return p
|
|
||||||
})
|
|
||||||
ResourceTypes.Register(string(PackageTypeYum), func(u *url.URL) Resource {
|
|
||||||
p := NewPackage()
|
p := NewPackage()
|
||||||
return p
|
return p
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FindSystemPackageType() PackageType {
|
||||||
|
for _, packageType := range []PackageType{PackageTypeApk, PackageTypeApt, PackageTypeDeb, PackageTypeDnf, PackageTypeRpm, PackageTypePip, PackageTypeYum} {
|
||||||
|
c := packageType.NewReadCommand()
|
||||||
|
if c.Exists() {
|
||||||
|
return packageType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PackageTypeApk
|
||||||
|
}
|
||||||
|
|
||||||
func NewPackage() *Package {
|
func NewPackage() *Package {
|
||||||
return &Package{ PackageType: PackageTypeApk }
|
return &Package{ PackageType: SystemPackageType }
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Package) Clone() Resource {
|
func (p *Package) Clone() Resource {
|
||||||
@ -106,17 +93,44 @@ func (p *Package) StateMachine() machine.Stater {
|
|||||||
|
|
||||||
func (p *Package) Notify(m *machine.EventMessage) {
|
func (p *Package) Notify(m *machine.EventMessage) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
slog.Info("Notify()", "package", p, "m", m)
|
||||||
switch m.On {
|
switch m.On {
|
||||||
case machine.ENTERSTATEEVENT:
|
case machine.ENTERSTATEEVENT:
|
||||||
switch m.Dest {
|
switch m.Dest {
|
||||||
|
case "start_read":
|
||||||
|
if _,readErr := p.Read(ctx); readErr == nil {
|
||||||
|
if triggerErr := p.StateMachine().Trigger("state_read"); triggerErr == nil {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
p.State = "absent"
|
||||||
|
panic(triggerErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.State = "absent"
|
||||||
|
panic(readErr)
|
||||||
|
}
|
||||||
case "start_create":
|
case "start_create":
|
||||||
if e := p.Create(ctx); e == nil {
|
if e := p.Create(ctx); e == nil {
|
||||||
if triggerErr := p.stater.Trigger("created"); triggerErr == nil {
|
if triggerErr := p.StateMachine().Trigger("created"); triggerErr == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.State = "absent"
|
p.State = "absent"
|
||||||
case "present":
|
case "start_delete":
|
||||||
|
if deleteErr := p.Delete(ctx); deleteErr == nil {
|
||||||
|
if triggerErr := p.StateMachine().Trigger("deleted"); triggerErr == nil {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
p.State = "present"
|
||||||
|
panic(triggerErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.State = "present"
|
||||||
|
panic(deleteErr)
|
||||||
|
}
|
||||||
|
case "absent":
|
||||||
|
p.State = "absent"
|
||||||
|
case "present", "created", "read":
|
||||||
p.State = "present"
|
p.State = "present"
|
||||||
}
|
}
|
||||||
case machine.EXITSTATEEVENT:
|
case machine.EXITSTATEEVENT:
|
||||||
@ -147,6 +161,10 @@ func (p *Package) SetURI(uri string) error {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Package) UseConfig(config ConfigurationValueGetter) {
|
||||||
|
p.config = config
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Package) JSON() ([]byte, error) {
|
func (p *Package) JSON() ([]byte, error) {
|
||||||
return json.Marshal(p)
|
return json.Marshal(p)
|
||||||
}
|
}
|
||||||
@ -176,6 +194,16 @@ func (p *Package) Create(ctx context.Context) error {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Package) Delete(ctx context.Context) error {
|
||||||
|
_, err := p.DeleteCommand.Execute(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_,e := p.Read(ctx)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func (p *Package) Apply() error {
|
func (p *Package) Apply() error {
|
||||||
if p.Version == "latest" {
|
if p.Version == "latest" {
|
||||||
p.Version = ""
|
p.Version = ""
|
||||||
@ -228,22 +256,69 @@ func (p *Package) UnmarshalYAML(value *yaml.Node) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PackageType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) {
|
func (p *PackageType) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) {
|
||||||
switch *p {
|
switch *p {
|
||||||
case PackageTypeApk:
|
case PackageTypeApk:
|
||||||
return NewApkCreateCommand(), NewApkReadCommand(), NewApkUpdateCommand(), NewApkDeleteCommand()
|
return NewApkCreateCommand(), NewApkReadCommand(), NewApkUpdateCommand(), NewApkDeleteCommand()
|
||||||
case PackageTypeApt:
|
case PackageTypeApt:
|
||||||
return NewAptCreateCommand(), NewAptReadCommand(), NewAptUpdateCommand(), NewAptDeleteCommand()
|
return NewAptCreateCommand(), NewAptReadCommand(), NewAptUpdateCommand(), NewAptDeleteCommand()
|
||||||
case PackageTypeDeb:
|
case PackageTypeDeb:
|
||||||
|
return NewDebCreateCommand(), NewDebReadCommand(), NewDebUpdateCommand(), NewDebDeleteCommand()
|
||||||
case PackageTypeDnf:
|
case PackageTypeDnf:
|
||||||
|
return NewDnfCreateCommand(), NewDnfReadCommand(), NewDnfUpdateCommand(), NewDnfDeleteCommand()
|
||||||
case PackageTypeRpm:
|
case PackageTypeRpm:
|
||||||
|
return NewRpmCreateCommand(), NewRpmReadCommand(), NewRpmUpdateCommand(), NewRpmDeleteCommand()
|
||||||
case PackageTypePip:
|
case PackageTypePip:
|
||||||
|
return NewPipCreateCommand(), NewPipReadCommand(), NewPipUpdateCommand(), NewPipDeleteCommand()
|
||||||
case PackageTypeYum:
|
case PackageTypeYum:
|
||||||
|
return NewYumCreateCommand(), NewYumReadCommand(), NewYumUpdateCommand(), NewYumDeleteCommand()
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
return nil, nil, nil, nil
|
return nil, nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PackageType) NewReadCommand() (read *command.Command) {
|
||||||
|
switch *p {
|
||||||
|
case PackageTypeApk:
|
||||||
|
return NewApkReadCommand()
|
||||||
|
case PackageTypeApt:
|
||||||
|
return NewAptReadCommand()
|
||||||
|
case PackageTypeDeb:
|
||||||
|
return NewDebReadCommand()
|
||||||
|
case PackageTypeDnf:
|
||||||
|
return NewDnfReadCommand()
|
||||||
|
case PackageTypeRpm:
|
||||||
|
return NewRpmReadCommand()
|
||||||
|
case PackageTypePip:
|
||||||
|
return NewPipReadCommand()
|
||||||
|
case PackageTypeYum:
|
||||||
|
return NewYumReadCommand()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PackageType) NewReadPackagesCommand() (read *command.Command) {
|
||||||
|
switch *p {
|
||||||
|
case PackageTypeApk:
|
||||||
|
return NewApkReadPackagesCommand()
|
||||||
|
case PackageTypeApt:
|
||||||
|
return NewAptReadPackagesCommand()
|
||||||
|
case PackageTypeDeb:
|
||||||
|
// return NewDebReadPackagesCommand()
|
||||||
|
case PackageTypeDnf:
|
||||||
|
// return NewDnfReadPackagesCommand()
|
||||||
|
case PackageTypeRpm:
|
||||||
|
// return NewRpmReadPackagesCommand()
|
||||||
|
case PackageTypePip:
|
||||||
|
// return NewPipReadPackagesCommand()
|
||||||
|
case PackageTypeYum:
|
||||||
|
// return NewYumReadPackagesCommand()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *PackageType) UnmarshalValue(value string) error {
|
func (p *PackageType) UnmarshalValue(value string) error {
|
||||||
switch value {
|
switch value {
|
||||||
case string(PackageTypeApk), string(PackageTypeApt), string(PackageTypeDeb), string(PackageTypeDnf), string(PackageTypeRpm), string(PackageTypePip), string(PackageTypeYum):
|
case string(PackageTypeApk), string(PackageTypeApt), string(PackageTypeDeb), string(PackageTypeDnf), string(PackageTypeRpm), string(PackageTypePip), string(PackageTypeYum):
|
||||||
@ -270,23 +345,23 @@ func (p *PackageType) UnmarshalYAML(value *yaml.Node) error {
|
|||||||
return p.UnmarshalValue(s)
|
return p.UnmarshalValue(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApkCreateCommand() *Command {
|
func NewApkCreateCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "apk"
|
c.Path = "apk"
|
||||||
c.Args = []CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("add"),
|
command.CommandArg("add"),
|
||||||
CommandArg("{{ .Name }}{{ .Required }}"),
|
command.CommandArg("{{ .Name }}{{ .Required }}"),
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApkReadCommand() *Command {
|
func NewApkReadCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "apk"
|
c.Path = "apk"
|
||||||
c.Args = []CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("info"),
|
command.CommandArg("info"),
|
||||||
CommandArg("-ev"),
|
command.CommandArg("-ev"),
|
||||||
CommandArg("{{ .Name }}"),
|
command.CommandArg("{{ .Name }}"),
|
||||||
}
|
}
|
||||||
c.Extractor = func(out []byte, target any) error {
|
c.Extractor = func(out []byte, target any) error {
|
||||||
p := target.(*Package)
|
p := target.(*Package)
|
||||||
@ -303,44 +378,82 @@ func NewApkReadCommand() *Command {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApkUpdateCommand() *Command {
|
func NewApkUpdateCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "apk"
|
c.Path = "apk"
|
||||||
c.Args = []CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("del"),
|
command.CommandArg("add"),
|
||||||
CommandArg("{{ .Name }}"),
|
command.CommandArg("{{ .Name }}{{ .Required }}"),
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApkDeleteCommand() *Command {
|
func NewApkDeleteCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "apk"
|
c.Path = "apk"
|
||||||
c.Args = []CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("del"),
|
command.CommandArg("del"),
|
||||||
CommandArg("{{ .Name }}"),
|
command.CommandArg("{{ .Name }}"),
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAptCreateCommand() *Command {
|
func NewApkReadPackagesCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
|
c.Path = "apk"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("list"),
|
||||||
|
command.CommandArg("--installed"),
|
||||||
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
Packages := target.(*[]*Package)
|
||||||
|
numberOfPackages := len(*Packages)
|
||||||
|
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||||
|
numberOfLines := len(lines)
|
||||||
|
diff := (numberOfLines - 1) - numberOfPackages
|
||||||
|
if diff > 0 {
|
||||||
|
for i := 0; i < diff; i++ {
|
||||||
|
*Packages = append(*Packages, NewPackage())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for lineIndex, line := range lines {
|
||||||
|
p := (*Packages)[lineIndex]
|
||||||
|
installedPackage := strings.Fields(strings.TrimSpace(line))
|
||||||
|
|
||||||
|
packageFields := strings.Split(installedPackage[0], "-")
|
||||||
|
numberOfFields := len(packageFields)
|
||||||
|
if numberOfFields > 2 {
|
||||||
|
packageName := strings.Join(packageFields[:numberOfFields - 3], "-")
|
||||||
|
packageVersion := strings.Join(packageFields[numberOfFields - 2:numberOfFields - 1], "-")
|
||||||
|
p.Name = packageName
|
||||||
|
p.State = "present"
|
||||||
|
p.Version = packageVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAptCreateCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
c.Path = "apt-get"
|
c.Path = "apt-get"
|
||||||
c.Split = false
|
c.Split = false
|
||||||
c.Args = []CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("satisfy"),
|
command.CommandArg("satisfy"),
|
||||||
CommandArg("-y"),
|
command.CommandArg("-y"),
|
||||||
CommandArg("{{ .Name }} ({{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }})"),
|
command.CommandArg("{{ .Name }} ({{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }})"),
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAptReadCommand() *Command {
|
func NewAptReadCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "dpkg"
|
c.Path = "dpkg"
|
||||||
c.Args = []CommandArg{
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("-s"),
|
command.CommandArg("-s"),
|
||||||
CommandArg("{{ .Name }}"),
|
command.CommandArg("{{ .Name }}"),
|
||||||
}
|
}
|
||||||
c.Extractor = func(out []byte, target any) error {
|
c.Extractor = func(out []byte, target any) error {
|
||||||
p := target.(*Package)
|
p := target.(*Package)
|
||||||
@ -379,22 +492,404 @@ func NewAptReadCommand() *Command {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAptUpdateCommand() *Command {
|
func NewAptUpdateCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "apt"
|
c.Path = "apt-get"
|
||||||
c.Args = []CommandArg{
|
c.Split = false
|
||||||
CommandArg("install"),
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("{{ .Name }}"),
|
command.CommandArg("satisfy"),
|
||||||
|
command.CommandArg("-y"),
|
||||||
|
command.CommandArg("{{ .Name }} ({{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }})"),
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAptDeleteCommand() *Command {
|
func NewAptDeleteCommand() *command.Command {
|
||||||
c := NewCommand()
|
c := command.NewCommand()
|
||||||
c.Path = "apt"
|
c.Path = "apt"
|
||||||
c.Args = []CommandArg{
|
c.FailOnError = false
|
||||||
CommandArg("remove"),
|
c.Args = []command.CommandArg{
|
||||||
CommandArg("{{ .Name }}"),
|
command.CommandArg("remove"),
|
||||||
|
command.CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAptReadPackagesCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "apt"
|
||||||
|
c.FailOnError = false
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("list"),
|
||||||
|
command.CommandArg("--installed"),
|
||||||
|
}
|
||||||
|
c.Env = []string{ "DEBIAN_FRONTEND=noninteractive" }
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
Packages := target.(*[]*Package)
|
||||||
|
numberOfPackages := len(*Packages)
|
||||||
|
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||||
|
numberOfLines := len(lines)
|
||||||
|
diff := (numberOfLines - 1) - numberOfPackages
|
||||||
|
if diff > 0 {
|
||||||
|
for i := 0; i < diff; i++ {
|
||||||
|
*Packages = append(*Packages, NewPackage())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for lineIndex, line := range lines[1:] {
|
||||||
|
p := (*Packages)[lineIndex]
|
||||||
|
installedPackage := strings.Fields(strings.TrimSpace(line))
|
||||||
|
|
||||||
|
packageFields := strings.Split(installedPackage[0], "/")
|
||||||
|
packageName := packageFields[0]
|
||||||
|
packageVersion := installedPackage[1]
|
||||||
|
p.Name = packageName
|
||||||
|
p.State = "present"
|
||||||
|
p.Version = packageVersion
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDebCreateCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "dpkg"
|
||||||
|
c.Split = false
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("-i"),
|
||||||
|
command.CommandArg("{{ .Source }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDebReadCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "dpkg"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("-s"),
|
||||||
|
command.CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
p := target.(*Package)
|
||||||
|
slog.Info("Extract()", "out", out)
|
||||||
|
pkginfo := strings.Split(string(out), "\n")
|
||||||
|
for _, infofield := range pkginfo {
|
||||||
|
if len(infofield) > 0 && infofield[0] != ' ' {
|
||||||
|
fieldKeyValue := strings.SplitN(infofield, ":", 2)
|
||||||
|
if len(fieldKeyValue) > 1 {
|
||||||
|
key := strings.TrimSpace(fieldKeyValue[0])
|
||||||
|
value := strings.TrimSpace(fieldKeyValue[1])
|
||||||
|
switch key {
|
||||||
|
case "Package":
|
||||||
|
if value != p.Name {
|
||||||
|
p.State = "absent"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case "Status":
|
||||||
|
statusFields := strings.SplitN(value, " ", 3)
|
||||||
|
if len(statusFields) > 1 {
|
||||||
|
if statusFields[2] == "installed" {
|
||||||
|
p.State = "present"
|
||||||
|
} else {
|
||||||
|
p.State = "absent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "Version":
|
||||||
|
p.Version = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slog.Info("Extract()", "package", p)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDebUpdateCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "dpkg"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("-i"),
|
||||||
|
command.CommandArg("{{ .Source }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDebDeleteCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "dpkg"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("-r"),
|
||||||
|
command.CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDnfCreateCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "dnf"
|
||||||
|
c.Split = false
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("install"),
|
||||||
|
command.CommandArg("-q"),
|
||||||
|
command.CommandArg("-y"),
|
||||||
|
command.CommandArg("{{ .Name }}{{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDnfReadCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "dnf"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("-q"),
|
||||||
|
command.CommandArg("list"),
|
||||||
|
command.CommandArg("installed"),
|
||||||
|
command.CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
p := target.(*Package)
|
||||||
|
slog.Info("Extract()", "out", out)
|
||||||
|
pkginfo := strings.Split(string(out), "\n")
|
||||||
|
for _, packageLines := range pkginfo {
|
||||||
|
fields := strings.Fields(packageLines)
|
||||||
|
packageNameField := strings.Split(fields[0], ".")
|
||||||
|
packageName := strings.TrimSpace(packageNameField[0])
|
||||||
|
//packageArch := strings.TrimSpace(packageNameField[1])
|
||||||
|
|
||||||
|
if packageName == p.Name {
|
||||||
|
p.State = "present"
|
||||||
|
packageVersionField := strings.Split(fields[1], ":")
|
||||||
|
//packageEpoch := strings.TrimSpace(packageVersionField[0])
|
||||||
|
packageVersion := strings.TrimSpace(packageVersionField[1])
|
||||||
|
p.Version = packageVersion
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.State = "absent"
|
||||||
|
slog.Info("Extract()", "package", p)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDnfUpdateCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "dnf"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("-q"),
|
||||||
|
command.CommandArg("-y"),
|
||||||
|
command.CommandArg("install"),
|
||||||
|
command.CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDnfDeleteCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "dnf"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("-q"),
|
||||||
|
command.CommandArg("-y"),
|
||||||
|
command.CommandArg("remove"),
|
||||||
|
command.CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func NewRpmCreateCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "rpm"
|
||||||
|
c.Split = false
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("-i"),
|
||||||
|
command.CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRpmReadCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "rpm"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("-q"),
|
||||||
|
command.CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
p := target.(*Package)
|
||||||
|
slog.Info("Extract()", "out", out)
|
||||||
|
pkginfo := strings.Split(string(out), "\n")
|
||||||
|
for _, packageLine := range pkginfo {
|
||||||
|
packageFields := strings.Split(packageLine, "-")
|
||||||
|
numberOfFields := len(packageFields)
|
||||||
|
if numberOfFields > 2 {
|
||||||
|
packageName := strings.Join(packageFields[:numberOfFields - 3], "-")
|
||||||
|
packageVersion := strings.Join(packageFields[numberOfFields - 2:numberOfFields - 1], "-")
|
||||||
|
if packageName == p.Name {
|
||||||
|
p.State = "present"
|
||||||
|
p.Version = packageVersion
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.State = "absent"
|
||||||
|
slog.Info("Extract()", "package", p)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRpmUpdateCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "rpm"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("-i"),
|
||||||
|
command.CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRpmDeleteCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "rpm"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("-e"),
|
||||||
|
command.CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPipCreateCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "pip"
|
||||||
|
c.Split = false
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("install"),
|
||||||
|
command.CommandArg("{{ .Name }}{{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPipReadCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "pip"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("list"),
|
||||||
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
p := target.(*Package)
|
||||||
|
pkginfo := strings.Split(string(out), "\n")
|
||||||
|
for _, packageLine := range pkginfo[2:] {
|
||||||
|
packageFields := strings.Fields(packageLine)
|
||||||
|
numberOfFields := len(packageFields)
|
||||||
|
if numberOfFields == 2 {
|
||||||
|
packageName := packageFields[0]
|
||||||
|
packageVersion := packageFields[1]
|
||||||
|
if packageName == p.Name {
|
||||||
|
p.State = "present"
|
||||||
|
p.Version = packageVersion
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.State = "absent"
|
||||||
|
slog.Info("Extract()", "package", p)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPipUpdateCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "pip"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("install"),
|
||||||
|
command.CommandArg("{{ .Name }}{{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPipDeleteCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "pip"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("uninstall"),
|
||||||
|
command.CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYumCreateCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "yum"
|
||||||
|
c.Split = false
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("install"),
|
||||||
|
command.CommandArg("-q"),
|
||||||
|
command.CommandArg("-y"),
|
||||||
|
command.CommandArg("{{ .Name }}{{ if .Required }}{{ .Required }}{{ else }}>=0.0.0{{ end }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYumReadCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "yum"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("-q"),
|
||||||
|
command.CommandArg("list"),
|
||||||
|
command.CommandArg("installed"),
|
||||||
|
command.CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
p := target.(*Package)
|
||||||
|
slog.Info("Extract()", "out", out)
|
||||||
|
pkginfo := strings.Split(string(out), "\n")
|
||||||
|
for _, packageLines := range pkginfo {
|
||||||
|
fields := strings.Fields(packageLines)
|
||||||
|
packageNameField := strings.Split(fields[0], ".")
|
||||||
|
packageName := strings.TrimSpace(packageNameField[0])
|
||||||
|
//packageArch := strings.TrimSpace(packageNameField[1])
|
||||||
|
|
||||||
|
if packageName == p.Name {
|
||||||
|
p.State = "present"
|
||||||
|
packageVersionField := strings.Split(fields[1], ":")
|
||||||
|
//packageEpoch := strings.TrimSpace(packageVersionField[0])
|
||||||
|
packageVersion := strings.TrimSpace(packageVersionField[1])
|
||||||
|
p.Version = packageVersion
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.State = "absent"
|
||||||
|
slog.Info("Extract()", "package", p)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYumUpdateCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "yum"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("-q"),
|
||||||
|
command.CommandArg("-y"),
|
||||||
|
command.CommandArg("install"),
|
||||||
|
command.CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYumDeleteCommand() *command.Command {
|
||||||
|
c := command.NewCommand()
|
||||||
|
c.Path = "yum"
|
||||||
|
c.Args = []command.CommandArg{
|
||||||
|
command.CommandArg("-q"),
|
||||||
|
command.CommandArg("-y"),
|
||||||
|
command.CommandArg("remove"),
|
||||||
|
command.CommandArg("{{ .Name }}"),
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
_ "os"
|
_ "os"
|
||||||
_ "strings"
|
_ "strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"decl/internal/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewPackageResource(t *testing.T) {
|
func TestNewPackageResource(t *testing.T) {
|
||||||
@ -56,7 +57,7 @@ type: apk
|
|||||||
assert.Nil(t, loadErr)
|
assert.Nil(t, loadErr)
|
||||||
assert.Equal(t, "latest", p.Version)
|
assert.Equal(t, "latest", p.Version)
|
||||||
|
|
||||||
p.ReadCommand = (*Command)(m)
|
p.ReadCommand = (*command.Command)(m)
|
||||||
yaml, readErr := p.Read(context.Background())
|
yaml, readErr := p.Read(context.Background())
|
||||||
assert.Nil(t, readErr)
|
assert.Nil(t, readErr)
|
||||||
assert.Greater(t, len(yaml), 0)
|
assert.Greater(t, len(yaml), 0)
|
||||||
@ -106,3 +107,30 @@ func TestPackageSetURI(t *testing.T) {
|
|||||||
assert.Equal(t, "package", p.Type())
|
assert.Equal(t, "package", p.Type())
|
||||||
assert.Equal(t, "12345_key", p.Name)
|
assert.Equal(t, "12345_key", p.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReadDebPackage(t *testing.T) {
|
||||||
|
decl := `
|
||||||
|
name: vim
|
||||||
|
source: vim-8.2.3995-1ubuntu2.17.deb
|
||||||
|
type: deb
|
||||||
|
`
|
||||||
|
p := NewPackage()
|
||||||
|
assert.NotNil(t, p)
|
||||||
|
loadErr := p.LoadDecl(decl)
|
||||||
|
assert.Nil(t, loadErr)
|
||||||
|
p.ReadCommand = NewDebReadCommand()
|
||||||
|
p.ReadCommand.Executor = func(value any) ([]byte, error) {
|
||||||
|
return []byte(`
|
||||||
|
Package: vim
|
||||||
|
Version: 1.2.2
|
||||||
|
`), nil
|
||||||
|
}
|
||||||
|
yaml, readErr := p.Read(context.Background())
|
||||||
|
assert.Nil(t, readErr)
|
||||||
|
|
||||||
|
slog.Info("Package.Read()", "package", p)
|
||||||
|
assert.Greater(t, len(yaml), 0)
|
||||||
|
slog.Info("read()", "yaml", yaml)
|
||||||
|
assert.Equal(t, "1.2.2", p.Version)
|
||||||
|
assert.Nil(t, p.Validate())
|
||||||
|
}
|
||||||
|
@ -10,8 +10,11 @@ import (
|
|||||||
_ "gopkg.in/yaml.v3"
|
_ "gopkg.in/yaml.v3"
|
||||||
_ "net/url"
|
_ "net/url"
|
||||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
|
"decl/internal/transport"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ResourceReference string
|
||||||
|
|
||||||
type ResourceSelector func(r *Declaration) bool
|
type ResourceSelector func(r *Declaration) bool
|
||||||
|
|
||||||
type Resource interface {
|
type Resource interface {
|
||||||
@ -19,6 +22,7 @@ type Resource interface {
|
|||||||
StateMachine() machine.Stater
|
StateMachine() machine.Stater
|
||||||
URI() string
|
URI() string
|
||||||
SetURI(string) error
|
SetURI(string) error
|
||||||
|
UseConfig(config ConfigurationValueGetter)
|
||||||
ResolveId(context.Context) string
|
ResolveId(context.Context) string
|
||||||
ResourceLoader
|
ResourceLoader
|
||||||
StateTransformer
|
StateTransformer
|
||||||
@ -27,6 +31,14 @@ type Resource interface {
|
|||||||
Clone() Resource
|
Clone() Resource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ContentReader interface {
|
||||||
|
ContentReaderStream() (*transport.Reader, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContentWriter interface {
|
||||||
|
ContentWriterStream() (*transport.Writer, error)
|
||||||
|
}
|
||||||
|
|
||||||
type ResourceValidator interface {
|
type ResourceValidator interface {
|
||||||
Validate() error
|
Validate() error
|
||||||
}
|
}
|
||||||
@ -65,6 +77,14 @@ func NewResource(uri string) Resource {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r ResourceReference) ContentReaderStream() (*transport.Reader, error) {
|
||||||
|
return transport.NewReaderURI(string(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ResourceReference) ContentWriterStream() (*transport.Writer, error) {
|
||||||
|
return transport.NewWriterURI(string(r))
|
||||||
|
}
|
||||||
|
|
||||||
func StorageMachine(sub machine.Subscriber) machine.Stater {
|
func StorageMachine(sub machine.Subscriber) machine.Stater {
|
||||||
// start_destroy -> absent -> start_create -> present -> start_destroy
|
// start_destroy -> absent -> start_create -> present -> start_destroy
|
||||||
stater := machine.New("unknown")
|
stater := machine.New("unknown")
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
package resource
|
package resource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -10,6 +10,10 @@
|
|||||||
"description": "Resource type name.",
|
"description": "Resource type name.",
|
||||||
"enum": [ "file" ]
|
"enum": [ "file" ]
|
||||||
},
|
},
|
||||||
|
"config": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Config name"
|
||||||
|
},
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"$ref": "file.jsonschema"
|
"$ref": "file.jsonschema"
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "file content"
|
"description": "file content"
|
||||||
},
|
},
|
||||||
|
"sourceref": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "file content source uri"
|
||||||
|
},
|
||||||
"target": {
|
"target": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Symbolic link target path"
|
"description": "Symbolic link target path"
|
||||||
|
@ -10,6 +10,10 @@
|
|||||||
"description": "Resource type name.",
|
"description": "Resource type name.",
|
||||||
"enum": [ "http" ]
|
"enum": [ "http" ]
|
||||||
},
|
},
|
||||||
|
"config": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Config name."
|
||||||
|
},
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"$ref": "http.jsonschema"
|
"$ref": "http.jsonschema"
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,10 @@
|
|||||||
"description": "Resource type name.",
|
"description": "Resource type name.",
|
||||||
"enum": [ "iptable" ]
|
"enum": [ "iptable" ]
|
||||||
},
|
},
|
||||||
|
"config": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Config name"
|
||||||
|
},
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"$ref": "iptable.jsonschema"
|
"$ref": "iptable.jsonschema"
|
||||||
}
|
}
|
||||||
|
252
internal/resource/service.go
Normal file
252
internal/resource/service.go
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
// Service resource
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
_ "log/slog"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"io"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||||
|
"decl/internal/codec"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceManagerType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ServiceManagerTypeSystemd ServiceManagerType = "systemd"
|
||||||
|
ServiceManagerTypeSysV ServiceManagerType = "sysv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
stater machine.Stater `yaml:"-" json:"-"`
|
||||||
|
Name string `json:"name" yaml:"name"`
|
||||||
|
ServiceManagerType ServiceManagerType `json:"servicemanager,omitempty" yaml:"servicemanager,omitempty"`
|
||||||
|
|
||||||
|
CreateCommand *Command `yaml:"-" json:"-"`
|
||||||
|
ReadCommand *Command `yaml:"-" json:"-"`
|
||||||
|
UpdateCommand *Command `yaml:"-" json:"-"`
|
||||||
|
DeleteCommand *Command `yaml:"-" json:"-"`
|
||||||
|
|
||||||
|
State string `yaml:"state,omitempty" json:"state,omitempty"`
|
||||||
|
config ConfigurationValueGetter
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ResourceTypes.Register([]string{"service"}, func(u *url.URL) Resource {
|
||||||
|
s := NewService()
|
||||||
|
s.Name = filepath.Join(u.Hostname(), u.Path)
|
||||||
|
s.CreateCommand, s.ReadCommand, s.UpdateCommand, s.DeleteCommand = s.ServiceManagerType.NewCRUD()
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService() *Service {
|
||||||
|
return &Service{ ServiceManagerType: ServiceManagerTypeSystemd }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) StateMachine() machine.Stater {
|
||||||
|
if s.stater == nil {
|
||||||
|
s.stater = ProcessMachine(s)
|
||||||
|
}
|
||||||
|
return s.stater
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Notify(m *machine.EventMessage) {
|
||||||
|
ctx := context.Background()
|
||||||
|
switch m.On {
|
||||||
|
case machine.ENTERSTATEEVENT:
|
||||||
|
switch m.Dest {
|
||||||
|
case "start_create":
|
||||||
|
if e := s.Create(ctx); e == nil {
|
||||||
|
if triggerErr := s.stater.Trigger("created"); triggerErr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.State = "absent"
|
||||||
|
case "created":
|
||||||
|
s.State = "present"
|
||||||
|
case "running":
|
||||||
|
s.State = "running"
|
||||||
|
}
|
||||||
|
case machine.EXITSTATEEVENT:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) URI() string {
|
||||||
|
return fmt.Sprintf("service://%s", s.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SetURI(uri string) error {
|
||||||
|
resourceUri, e := url.Parse(uri)
|
||||||
|
if e == nil {
|
||||||
|
if resourceUri.Scheme == s.Type() {
|
||||||
|
s.Name = filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI())
|
||||||
|
} else {
|
||||||
|
e = fmt.Errorf("%w: %s is not a %s", ErrInvalidResourceURI, uri, s.Type())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UseConfig(config ConfigurationValueGetter) {
|
||||||
|
s.config = config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) JSON() ([]byte, error) {
|
||||||
|
return json.Marshal(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Clone() Resource {
|
||||||
|
news := &Service{
|
||||||
|
Name: s.Name,
|
||||||
|
ServiceManagerType: s.ServiceManagerType,
|
||||||
|
}
|
||||||
|
news.CreateCommand, news.ReadCommand, news.UpdateCommand, news.DeleteCommand = s.ServiceManagerType.NewCRUD()
|
||||||
|
return news
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Apply() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Load(r io.Reader) error {
|
||||||
|
return codec.NewYAMLDecoder(r).Decode(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) LoadDecl(yamlResourceDeclaration string) error {
|
||||||
|
return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UnmarshalJSON(data []byte) error {
|
||||||
|
if unmarshalErr := json.Unmarshal(data, s); unmarshalErr != nil {
|
||||||
|
return unmarshalErr
|
||||||
|
}
|
||||||
|
s.CreateCommand, s.ReadCommand, s.UpdateCommand, s.DeleteCommand = s.ServiceManagerType.NewCRUD()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
type decodeService Service
|
||||||
|
if unmarshalErr := value.Decode((*decodeService)(s)); unmarshalErr != nil {
|
||||||
|
return unmarshalErr
|
||||||
|
}
|
||||||
|
s.CreateCommand, s.ReadCommand, s.UpdateCommand, s.DeleteCommand = s.ServiceManagerType.NewCRUD()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceManagerType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) {
|
||||||
|
switch *s {
|
||||||
|
case ServiceManagerTypeSystemd:
|
||||||
|
return NewSystemdCreateCommand(), NewSystemdReadCommand(), NewSystemdUpdateCommand(), NewSystemdDeleteCommand()
|
||||||
|
case ServiceManagerTypeSysV:
|
||||||
|
return NewSysVCreateCommand(), NewSysVReadCommand(), NewSysVUpdateCommand(), NewSysVDeleteCommand()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return nil, nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (s *Service) Create(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Read(ctx context.Context) ([]byte, error) {
|
||||||
|
|
||||||
|
return yaml.Marshal(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Type() string { return "service" }
|
||||||
|
|
||||||
|
func (s *Service) ResolveId(ctx context.Context) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSystemdCreateCommand() *Command {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Path = "systemctl"
|
||||||
|
c.Args = []CommandArg{
|
||||||
|
CommandArg("enable"),
|
||||||
|
CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSystemdReadCommand() *Command {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Path = "systemctl"
|
||||||
|
c.Args = []CommandArg{
|
||||||
|
CommandArg("show"),
|
||||||
|
CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
s := target.(*Service)
|
||||||
|
serviceStatus := strings.Split(string(out), "\n")
|
||||||
|
for _, statusLine := range(serviceStatus) {
|
||||||
|
if len(statusLine) > 1 {
|
||||||
|
statusKeyValue := strings.Split(statusLine, "=")
|
||||||
|
key := statusKeyValue[0]
|
||||||
|
value := strings.TrimSpace(strings.Join(statusKeyValue[1:], "="))
|
||||||
|
switch key {
|
||||||
|
case "Id":
|
||||||
|
case "ActiveState":
|
||||||
|
switch value {
|
||||||
|
case "active":
|
||||||
|
if stateCreatedErr := s.stater.Trigger("created"); stateCreatedErr != nil {
|
||||||
|
return stateCreatedErr
|
||||||
|
}
|
||||||
|
case "inactive":
|
||||||
|
}
|
||||||
|
case "SubState":
|
||||||
|
switch value {
|
||||||
|
case "running":
|
||||||
|
if stateRunningErr := s.stater.Trigger("running"); stateRunningErr != nil {
|
||||||
|
return stateRunningErr
|
||||||
|
}
|
||||||
|
case "dead":
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSystemdUpdateCommand() *Command {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSystemdDeleteCommand() *Command {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSysVCreateCommand() *Command {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSysVReadCommand() *Command {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSysVUpdateCommand() *Command {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSysVDeleteCommand() *Command {
|
||||||
|
return nil
|
||||||
|
}
|
36
internal/resource/service_test.go
Normal file
36
internal/resource/service_test.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
_ "decl/tests/mocks"
|
||||||
|
_ "fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewServiceResource(t *testing.T) {
|
||||||
|
c := NewService()
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUriServiceResource(t *testing.T) {
|
||||||
|
c := NewService()
|
||||||
|
assert.Nil(t, c.SetURI("service://ssh"))
|
||||||
|
assert.Equal(t, "ssh", c.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadServiceResource(t *testing.T) {
|
||||||
|
yamlResult := `
|
||||||
|
name: "ssh"
|
||||||
|
servicemanager: "systemd"
|
||||||
|
state: "present"
|
||||||
|
`
|
||||||
|
c := NewService()
|
||||||
|
c.Name = "ssh"
|
||||||
|
c.State = "present"
|
||||||
|
yamlData, err := c.Read(context.Background())
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.YAMLEq(t, yamlResult, string(yamlData))
|
||||||
|
}
|
@ -5,50 +5,18 @@ package resource
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
_ "net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"decl/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrUnknownResourceType = errors.New("Unknown resource type")
|
ErrUnknownResourceType = errors.New("Unknown resource type")
|
||||||
ResourceTypes *Types = NewTypes()
|
ResourceTypes *types.Types[Resource] = types.New[Resource]()
|
||||||
)
|
)
|
||||||
|
|
||||||
type TypeName string //`json:"type"`
|
type TypeName string //`json:"type"`
|
||||||
|
|
||||||
type TypeFactory func(*url.URL) Resource
|
|
||||||
|
|
||||||
type Types struct {
|
|
||||||
registry map[string]TypeFactory
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTypes() *Types {
|
|
||||||
return &Types{registry: make(map[string]TypeFactory)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Types) Register(name string, factory TypeFactory) {
|
|
||||||
t.registry[name] = factory
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Types) New(uri string) (Resource, error) {
|
|
||||||
u, e := url.Parse(uri)
|
|
||||||
if u == nil || e != nil {
|
|
||||||
return nil, fmt.Errorf("%w: %s - uri %s", ErrUnknownResourceType, e, uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r, ok := t.registry[u.Scheme]; ok {
|
|
||||||
return r(u), nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("%w: %s", ErrUnknownResourceType, u.Scheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Types) Has(typename string) bool {
|
|
||||||
if _, ok := t.registry[typename]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *TypeName) UnmarshalJSON(b []byte) error {
|
func (n *TypeName) UnmarshalJSON(b []byte) error {
|
||||||
ResourceTypeName := strings.Trim(string(b), "\"")
|
ResourceTypeName := strings.Trim(string(b), "\"")
|
||||||
if ResourceTypes.Has(ResourceTypeName) {
|
if ResourceTypes.Has(ResourceTypeName) {
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
|
||||||
package resource
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "context"
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewResourceTypes(t *testing.T) {
|
|
||||||
resourceTypes := NewTypes()
|
|
||||||
assert.NotEqual(t, nil, resourceTypes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewResourceTypesRegister(t *testing.T) {
|
|
||||||
m := NewFooResource()
|
|
||||||
|
|
||||||
resourceTypes := NewTypes()
|
|
||||||
assert.NotEqual(t, nil, resourceTypes)
|
|
||||||
|
|
||||||
resourceTypes.Register("foo", func(*url.URL) Resource { return m })
|
|
||||||
|
|
||||||
r, e := resourceTypes.New("foo://")
|
|
||||||
assert.Equal(t, nil, e)
|
|
||||||
assert.Equal(t, m, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceTypesFromURI(t *testing.T) {
|
|
||||||
m := NewFooResource()
|
|
||||||
|
|
||||||
resourceTypes := NewTypes()
|
|
||||||
assert.NotEqual(t, nil, resourceTypes)
|
|
||||||
|
|
||||||
resourceTypes.Register("foo", func(*url.URL) Resource { return m })
|
|
||||||
|
|
||||||
r, e := resourceTypes.New("foo://bar")
|
|
||||||
assert.Equal(t, nil, e)
|
|
||||||
assert.Equal(t, m, r)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceTypesHasType(t *testing.T) {
|
|
||||||
m := NewFooResource()
|
|
||||||
|
|
||||||
resourceTypes := NewTypes()
|
|
||||||
assert.NotNil(t, resourceTypes)
|
|
||||||
|
|
||||||
resourceTypes.Register("foo", func(*url.URL) Resource { return m })
|
|
||||||
|
|
||||||
assert.True(t, resourceTypes.Has("foo"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceTypeName(t *testing.T) {
|
|
||||||
type fooResourceName struct {
|
|
||||||
Name TypeName `json:"type"`
|
|
||||||
}
|
|
||||||
fooTypeName := &fooResourceName{}
|
|
||||||
jsonType := `{ "type": "file" }`
|
|
||||||
e := json.Unmarshal([]byte(jsonType), &fooTypeName)
|
|
||||||
assert.Nil(t, e)
|
|
||||||
assert.Equal(t, "file", string(fooTypeName.Name))
|
|
||||||
}
|
|
@ -45,6 +45,7 @@ type User struct {
|
|||||||
UpdateCommand *Command `json:"-" yaml:"-"`
|
UpdateCommand *Command `json:"-" yaml:"-"`
|
||||||
DeleteCommand *Command `json:"-" yaml:"-"`
|
DeleteCommand *Command `json:"-" yaml:"-"`
|
||||||
State string `json:"state,omitempty" yaml:"state,omitempty"`
|
State string `json:"state,omitempty" yaml:"state,omitempty"`
|
||||||
|
config ConfigurationValueGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUser() *User {
|
func NewUser() *User {
|
||||||
@ -52,7 +53,7 @@ func NewUser() *User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ResourceTypes.Register("user", func(u *url.URL) Resource {
|
ResourceTypes.Register([]string{"user"}, func(u *url.URL) Resource {
|
||||||
user := NewUser()
|
user := NewUser()
|
||||||
user.Name = u.Hostname()
|
user.Name = u.Hostname()
|
||||||
user.UID = LookupUIDString(u.Hostname())
|
user.UID = LookupUIDString(u.Hostname())
|
||||||
@ -126,6 +127,10 @@ func (u *User) URI() string {
|
|||||||
return fmt.Sprintf("user://%s", u.Name)
|
return fmt.Sprintf("user://%s", u.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) UseConfig(config ConfigurationValueGetter) {
|
||||||
|
u.config = config
|
||||||
|
}
|
||||||
|
|
||||||
func (u *User) ResolveId(ctx context.Context) string {
|
func (u *User) ResolveId(ctx context.Context) string {
|
||||||
return LookupUIDString(u.Name)
|
return LookupUIDString(u.Name)
|
||||||
}
|
}
|
||||||
|
@ -5,14 +5,14 @@ package source
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
_ "encoding/json"
|
_ "encoding/json"
|
||||||
_ "fmt"
|
"fmt"
|
||||||
_ "gopkg.in/yaml.v3"
|
_ "gopkg.in/yaml.v3"
|
||||||
"net/url"
|
"net/url"
|
||||||
_ "path/filepath"
|
_ "path/filepath"
|
||||||
"decl/internal/resource"
|
"decl/internal/resource"
|
||||||
_ "os"
|
_ "os"
|
||||||
_ "io"
|
_ "io"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
)
|
)
|
||||||
@ -46,9 +46,10 @@ func init() {
|
|||||||
func (c *Container) Type() string { return "container" }
|
func (c *Container) Type() string { return "container" }
|
||||||
|
|
||||||
func (c *Container) ExtractResources(filter ResourceSelector) ([]*resource.Document, error) {
|
func (c *Container) ExtractResources(filter ResourceSelector) ([]*resource.Document, error) {
|
||||||
|
var extractErr error
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
slog.Info("container source ExtractResources()", "container", c)
|
slog.Info("container source ExtractResources()", "container", c)
|
||||||
containers, err := c.apiClient.ContainerList(ctx, types.ContainerListOptions{All: true})
|
containers, err := c.apiClient.ContainerList(ctx, container.ListOptions{All: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -56,9 +57,11 @@ func (c *Container) ExtractResources(filter ResourceSelector) ([]*resource.Docum
|
|||||||
document := resource.NewDocument()
|
document := resource.NewDocument()
|
||||||
for _, container := range containers {
|
for _, container := range containers {
|
||||||
runningContainer := resource.NewContainer(nil)
|
runningContainer := resource.NewContainer(nil)
|
||||||
runningContainer.Inspect(ctx, container.ID)
|
if inspectErr := runningContainer.Inspect(ctx, container.ID); inspectErr != nil {
|
||||||
|
extractErr = fmt.Errorf("%w: %w", extractErr, inspectErr)
|
||||||
|
}
|
||||||
document.AddResourceDeclaration("container", runningContainer)
|
document.AddResourceDeclaration("container", runningContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
return []*resource.Document{document}, nil
|
return []*resource.Document{document}, extractErr
|
||||||
}
|
}
|
||||||
|
@ -5,83 +5,19 @@ package source
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
_ "net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"path/filepath"
|
_ "path/filepath"
|
||||||
|
"decl/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrUnknownSourceType = errors.New("Unknown source type")
|
ErrUnknownSourceType = errors.New("Unknown source type")
|
||||||
SourceTypes *Types = NewTypes()
|
SourceTypes *types.Types[DocSource] = types.New[DocSource]()
|
||||||
)
|
)
|
||||||
|
|
||||||
type TypeName string //`json:"type"`
|
type TypeName string //`json:"type"`
|
||||||
|
|
||||||
type TypeFactory func(*url.URL) DocSource
|
|
||||||
|
|
||||||
type Types struct {
|
|
||||||
registry map[string]TypeFactory
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTypes() *Types {
|
|
||||||
return &Types{registry: make(map[string]TypeFactory)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Types) Register(names []string, factory TypeFactory) {
|
|
||||||
for _,name := range names {
|
|
||||||
t.registry[name] = factory
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Types) FromExtension(path string) (TypeFactory, error) {
|
|
||||||
elements := strings.Split(path, ".")
|
|
||||||
numberOfElements := len(elements)
|
|
||||||
if numberOfElements > 2 {
|
|
||||||
if src := t.Get(strings.Join(elements[numberOfElements - 2: numberOfElements - 1], ".")); src != nil {
|
|
||||||
return src, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if src := t.Get(elements[numberOfElements - 1]); src != nil {
|
|
||||||
return src, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("%w: %s", ErrUnknownSourceType, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Types) New(uri string) (DocSource, error) {
|
|
||||||
u, e := url.Parse(uri)
|
|
||||||
if u == nil || e != nil {
|
|
||||||
return nil, fmt.Errorf("%w: %s", ErrUnknownSourceType, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Scheme == "" {
|
|
||||||
u.Scheme = "file"
|
|
||||||
}
|
|
||||||
|
|
||||||
path := filepath.Join(u.Hostname(), u.Path)
|
|
||||||
if d, lookupErr := t.FromExtension(path); d != nil {
|
|
||||||
return d(u), lookupErr
|
|
||||||
}
|
|
||||||
|
|
||||||
if r, ok := t.registry[u.Scheme]; ok {
|
|
||||||
return r(u), nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("%w: %s", ErrUnknownSourceType, u.Scheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Types) Has(typename string) bool {
|
|
||||||
if _, ok := t.registry[typename]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Types) Get(typename string) TypeFactory {
|
|
||||||
if d, ok := t.registry[typename]; ok {
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *TypeName) UnmarshalJSON(b []byte) error {
|
func (n *TypeName) UnmarshalJSON(b []byte) error {
|
||||||
SourceTypeName := strings.Trim(string(b), "\"")
|
SourceTypeName := strings.Trim(string(b), "\"")
|
||||||
if SourceTypes.Has(SourceTypeName) {
|
if SourceTypes.Has(SourceTypeName) {
|
||||||
|
@ -33,48 +33,6 @@ func NewFileDocSource() DocSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewSourceTypes(t *testing.T) {
|
|
||||||
sourceTypes := NewTypes()
|
|
||||||
assert.NotNil(t, sourceTypes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewSourceTypesRegister(t *testing.T) {
|
|
||||||
m := NewFooDocSource()
|
|
||||||
|
|
||||||
sourceTypes := NewTypes()
|
|
||||||
assert.NotNil(t, sourceTypes)
|
|
||||||
|
|
||||||
sourceTypes.Register([]string{"foo"}, func(*url.URL) DocSource { return m })
|
|
||||||
|
|
||||||
r, e := sourceTypes.New("foo://")
|
|
||||||
assert.Nil(t, e)
|
|
||||||
assert.Equal(t, m, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceTypesFromURI(t *testing.T) {
|
|
||||||
m := NewFooDocSource()
|
|
||||||
|
|
||||||
sourceTypes := NewTypes()
|
|
||||||
assert.NotNil(t, sourceTypes)
|
|
||||||
|
|
||||||
sourceTypes.Register([]string{"foo"}, func(*url.URL) DocSource { return m })
|
|
||||||
|
|
||||||
r, e := sourceTypes.New("foo://bar")
|
|
||||||
assert.Nil(t, e)
|
|
||||||
assert.Equal(t, m, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceTypesHasType(t *testing.T) {
|
|
||||||
m := NewFooDocSource()
|
|
||||||
|
|
||||||
sourceTypes := NewTypes()
|
|
||||||
assert.NotNil(t, sourceTypes)
|
|
||||||
|
|
||||||
sourceTypes.Register([]string{"foo"}, func(*url.URL) DocSource { return m })
|
|
||||||
|
|
||||||
assert.True(t, sourceTypes.Has("foo"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDocSourceTypeName(t *testing.T) {
|
func TestDocSourceTypeName(t *testing.T) {
|
||||||
SourceTypes.Register([]string{"file"}, func(*url.URL) DocSource { return NewFileDocSource() })
|
SourceTypes.Register([]string{"file"}, func(*url.URL) DocSource { return NewFileDocSource() })
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ func NewFile(u *url.URL) (f *File, err error) {
|
|||||||
f.readHandle = os.Stdin
|
f.readHandle = os.Stdin
|
||||||
f.writeHandle = os.Stdout
|
f.writeHandle = os.Stdout
|
||||||
} else {
|
} else {
|
||||||
if f.readHandle, err = os.Open(f.Path()); err != nil {
|
if f.readHandle, err = os.OpenFile(f.Path(), os.O_RDWR|os.O_CREATE, 0644); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f.writeHandle = f.readHandle
|
f.writeHandle = f.readHandle
|
||||||
|
@ -49,6 +49,9 @@ func NewHTTP(u *url.URL, ctx context.Context) (h *HTTP, err error) {
|
|||||||
h.extension()
|
h.extension()
|
||||||
|
|
||||||
h.postRequest, err = http.NewRequestWithContext(ctx, "POST", u.String(), h.buffer)
|
h.postRequest, err = http.NewRequestWithContext(ctx, "POST", u.String(), h.buffer)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
h.getRequest, err = http.NewRequestWithContext(ctx, "GET", u.String(), nil)
|
h.getRequest, err = http.NewRequestWithContext(ctx, "GET", u.String(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -73,7 +76,7 @@ func (h *HTTP) Path() string {
|
|||||||
|
|
||||||
func (h *HTTP) Signature() (documentSignature string) {
|
func (h *HTTP) Signature() (documentSignature string) {
|
||||||
if h.getResponse != nil {
|
if h.getResponse != nil {
|
||||||
documentSignature := h.getResponse.Header.Get("Signature")
|
documentSignature = h.getResponse.Header.Get("Signature")
|
||||||
if documentSignature == "" {
|
if documentSignature == "" {
|
||||||
signatureResp, signatureErr := h.Client.Get(fmt.Sprintf("%s.sig", h.uri.String()))
|
signatureResp, signatureErr := h.Client.Get(fmt.Sprintf("%s.sig", h.uri.String()))
|
||||||
if signatureErr == nil {
|
if signatureErr == nil {
|
||||||
@ -113,7 +116,7 @@ func (h *HTTP) Reader() io.ReadCloser {
|
|||||||
func (h *HTTP) Writer() io.WriteCloser {
|
func (h *HTTP) Writer() io.WriteCloser {
|
||||||
var err error
|
var err error
|
||||||
if h.postResponse, err = h.Client.Do(h.postRequest); err != nil {
|
if h.postResponse, err = h.Client.Do(h.postRequest); err != nil {
|
||||||
h.postResponse, err = h.Client.Do(h.postRequest)
|
panic(err)
|
||||||
}
|
}
|
||||||
return h.buffer
|
return h.buffer
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,14 @@ func NewReader(u *url.URL) (reader *Reader, e error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewReaderURI(uri string) (reader *Reader, e error) {
|
||||||
|
var u *url.URL
|
||||||
|
if u, e = url.Parse(uri); e == nil {
|
||||||
|
return NewReader(u)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type Writer struct {
|
type Writer struct {
|
||||||
uri *url.URL
|
uri *url.URL
|
||||||
handle Handler
|
handle Handler
|
||||||
@ -65,6 +73,14 @@ func NewWriter(u *url.URL) (writer *Writer, e error) {
|
|||||||
return writer, e
|
return writer, e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewWriterURI(uri string) (writer *Writer, e error) {
|
||||||
|
var u *url.URL
|
||||||
|
if u, e = url.Parse(uri); e == nil {
|
||||||
|
return NewWriter(u)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Reader) Read(b []byte) (int, error) {
|
func (r *Reader) Read(b []byte) (int, error) {
|
||||||
return r.stream.Read(b)
|
return r.stream.Read(b)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user