add source for containers, packages
This commit is contained in:
parent
e604806f81
commit
52c083a3d9
@ -18,7 +18,15 @@ type Decoder interface {
|
|||||||
Decode(v any) error
|
Decode(v any) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDecoder() *Decoder {
|
func NewDecoder(r io.Reader, format Format) Decoder {
|
||||||
|
switch format {
|
||||||
|
case FormatYaml:
|
||||||
|
return NewYAMLDecoder(r)
|
||||||
|
case FormatJson:
|
||||||
|
return NewJSONDecoder(r)
|
||||||
|
case FormatProtoBuf:
|
||||||
|
return NewProtoBufDecoder(r)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,15 @@ type Encoder interface {
|
|||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEncoder() *Encoder {
|
func NewEncoder(w io.Writer, format Format) Encoder {
|
||||||
|
switch format {
|
||||||
|
case FormatYaml:
|
||||||
|
return NewYAMLEncoder(w)
|
||||||
|
case FormatJson:
|
||||||
|
return NewJSONEncoder(w)
|
||||||
|
case FormatProtoBuf:
|
||||||
|
return NewProtoBufEncoder(w)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
61
internal/codec/types.go
Normal file
61
internal/codec/types.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package codec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"encoding/json"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FormatYaml Format = "yaml"
|
||||||
|
FormatJson Format = "json"
|
||||||
|
FormatProtoBuf Format = "protobuf"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrInvalidFormat error = errors.New("invalid Format value")
|
||||||
|
|
||||||
|
type Format string
|
||||||
|
|
||||||
|
func (f *Format) Validate() error {
|
||||||
|
switch *f {
|
||||||
|
case FormatYaml, FormatJson, FormatProtoBuf:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return ErrInvalidFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Format) Set(value string) (err error) {
|
||||||
|
if err = (*Format)(&value).Validate(); err == nil {
|
||||||
|
*f = Format(value)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Format) UnmarshalValue(value string) error {
|
||||||
|
switch value {
|
||||||
|
case string(FormatYaml), string(FormatJson), string(FormatProtoBuf):
|
||||||
|
*f = Format(value)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return ErrInvalidFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Format) UnmarshalJSON(data []byte) error {
|
||||||
|
var s string
|
||||||
|
if unmarshalFormatTypeErr := json.Unmarshal(data, &s); unmarshalFormatTypeErr != nil {
|
||||||
|
return unmarshalFormatTypeErr
|
||||||
|
}
|
||||||
|
return f.UnmarshalValue(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Format) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
var s string
|
||||||
|
if err := value.Decode(&s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.UnmarshalValue(s)
|
||||||
|
}
|
41
internal/codec/types_test.go
Normal file
41
internal/codec/types_test.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package codec
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
_ "log"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestDec struct {
|
||||||
|
FormatType Format `yaml:"formattype" json:"formattype"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatType(t *testing.T) {
|
||||||
|
yamlData := `
|
||||||
|
formattype: json
|
||||||
|
`
|
||||||
|
v := &TestDec{}
|
||||||
|
|
||||||
|
dec := NewYAMLStringDecoder(yamlData)
|
||||||
|
e := dec.Decode(v)
|
||||||
|
|
||||||
|
assert.Nil(t, e)
|
||||||
|
|
||||||
|
assert.Equal(t, FormatJson, v.FormatType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatTypeErr(t *testing.T) {
|
||||||
|
yamlData := `
|
||||||
|
formattype: foo
|
||||||
|
`
|
||||||
|
|
||||||
|
v := &TestDec{}
|
||||||
|
|
||||||
|
dec := NewYAMLStringDecoder(yamlData)
|
||||||
|
e := dec.Decode(v)
|
||||||
|
|
||||||
|
assert.ErrorIs(t, ErrInvalidFormat, e)
|
||||||
|
}
|
143
internal/command/command.go
Normal file
143
internal/command/command.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
_ "net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"decl/internal/codec"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CommandExecutor func(value any) ([]byte, error)
|
||||||
|
type CommandExtractAttributes func(output []byte, target any) error
|
||||||
|
|
||||||
|
type CommandArg string
|
||||||
|
|
||||||
|
type Command struct {
|
||||||
|
Path string `json:"path" yaml:"path"`
|
||||||
|
Args []CommandArg `json:"args" yaml:"args"`
|
||||||
|
Env []string `json:"env" yaml:"env"`
|
||||||
|
Split bool `json:"split" yaml:"split"`
|
||||||
|
FailOnError bool `json:"failonerror" yaml:"failonerror"`
|
||||||
|
Executor CommandExecutor `json:"-" yaml:"-"`
|
||||||
|
Extractor CommandExtractAttributes `json:"-" yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommand() *Command {
|
||||||
|
c := &Command{ Split: true, FailOnError: true }
|
||||||
|
c.Executor = func(value any) ([]byte, error) {
|
||||||
|
args, err := c.Template(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cmd := exec.Command(c.Path, args...)
|
||||||
|
c.SetCmdEnv(cmd)
|
||||||
|
|
||||||
|
slog.Info("execute() - cmd", "path", c.Path, "args", args)
|
||||||
|
output, stdoutPipeErr := cmd.StdoutPipe()
|
||||||
|
if stdoutPipeErr != nil {
|
||||||
|
return nil, stdoutPipeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr, pipeErr := cmd.StderrPipe()
|
||||||
|
if pipeErr != nil {
|
||||||
|
return nil, pipeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if startErr := cmd.Start(); startErr != nil {
|
||||||
|
return nil, startErr
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("execute() - start", "cmd", cmd)
|
||||||
|
stdOutOutput, _ := io.ReadAll(output)
|
||||||
|
stdErrOutput, _ := io.ReadAll(stderr)
|
||||||
|
|
||||||
|
slog.Info("execute() - io", "stdout", string(stdOutOutput), "stderr", string(stdErrOutput))
|
||||||
|
waitErr := cmd.Wait()
|
||||||
|
|
||||||
|
slog.Info("execute()", "path", c.Path, "args", args, "output", string(stdOutOutput), "error", string(stdErrOutput))
|
||||||
|
|
||||||
|
if len(stdErrOutput) > 0 && c.FailOnError {
|
||||||
|
return stdOutOutput, fmt.Errorf("%w %s", waitErr, string(stdErrOutput))
|
||||||
|
}
|
||||||
|
return stdOutOutput, waitErr
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) Load(r io.Reader) error {
|
||||||
|
return codec.NewYAMLDecoder(r).Decode(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) LoadDecl(yamlResourceDeclaration string) error {
|
||||||
|
return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) SetCmdEnv(cmd *exec.Cmd) {
|
||||||
|
cmd.Env = append(os.Environ(), c.Env...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) Exists() bool {
|
||||||
|
if _, err := exec.LookPath(c.Path); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) Template(value any) ([]string, error) {
|
||||||
|
var args []string = make([]string, 0, len(c.Args) * 2)
|
||||||
|
for i, arg := range c.Args {
|
||||||
|
var commandLineArg strings.Builder
|
||||||
|
err := template.Must(template.New(fmt.Sprintf("arg%d", i)).Parse(string(arg))).Execute(&commandLineArg, value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if commandLineArg.Len() > 0 {
|
||||||
|
var splitArg []string
|
||||||
|
if c.Split {
|
||||||
|
splitArg = strings.Split(commandLineArg.String(), " ")
|
||||||
|
} else {
|
||||||
|
splitArg = []string{commandLineArg.String()}
|
||||||
|
}
|
||||||
|
slog.Info("Template()", "split", splitArg, "len", len(splitArg))
|
||||||
|
args = append(args, splitArg...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Template()", "Args", c.Args, "lencargs", len(c.Args), "args", args, "lenargs", len(args), "value", value)
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) Execute(value any) ([]byte, error) {
|
||||||
|
return c.Executor(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandArg) UnmarshalValue(value string) error {
|
||||||
|
*c = CommandArg(value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandArg) UnmarshalJSON(data []byte) error {
|
||||||
|
var s string
|
||||||
|
if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
|
||||||
|
return unmarshalRouteTypeErr
|
||||||
|
}
|
||||||
|
return c.UnmarshalValue(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandArg) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
var s string
|
||||||
|
if err := value.Decode(&s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.UnmarshalValue(s)
|
||||||
|
}
|
60
internal/command/command_test.go
Normal file
60
internal/command/command_test.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
_ "os"
|
||||||
|
_ "strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewCommand(t *testing.T) {
|
||||||
|
c := NewCommand()
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandLoad(t *testing.T) {
|
||||||
|
c := NewCommand()
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
|
||||||
|
decl := `
|
||||||
|
path: find
|
||||||
|
args:
|
||||||
|
- "{{ .Path }}"
|
||||||
|
`
|
||||||
|
|
||||||
|
assert.Nil(t, c.LoadDecl(decl))
|
||||||
|
assert.Equal(t, "find", c.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandTemplate(t *testing.T) {
|
||||||
|
c := NewCommand()
|
||||||
|
assert.NotNil(t, c)
|
||||||
|
|
||||||
|
decl := `
|
||||||
|
path: find
|
||||||
|
args:
|
||||||
|
- "{{ .Path }}"
|
||||||
|
`
|
||||||
|
|
||||||
|
assert.Nil(t, c.LoadDecl(decl))
|
||||||
|
assert.Equal(t, "find", c.Path)
|
||||||
|
assert.Equal(t, 1, len(c.Args))
|
||||||
|
|
||||||
|
f := struct { Path string } {
|
||||||
|
Path: "./",
|
||||||
|
}
|
||||||
|
|
||||||
|
args, templateErr := c.Template(f)
|
||||||
|
assert.Nil(t, templateErr)
|
||||||
|
assert.Equal(t, 1, len(args))
|
||||||
|
|
||||||
|
assert.Equal(t, "./", string(args[0]))
|
||||||
|
|
||||||
|
out, err := c.Execute(f)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Greater(t, len(out), 0)
|
||||||
|
}
|
@ -25,12 +25,13 @@ type Exec struct {
|
|||||||
UpdateTemplate Command `yaml:"update" json:"update"`
|
UpdateTemplate Command `yaml:"update" json:"update"`
|
||||||
DeleteTemplate Command `yaml:"delete" json:"delete"`
|
DeleteTemplate Command `yaml:"delete" json:"delete"`
|
||||||
|
|
||||||
|
config ConfigurationValueGetter
|
||||||
// state attributes
|
// state attributes
|
||||||
State string `yaml:"state"`
|
State string `yaml:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ResourceTypes.Register("exec", func(u *url.URL) Resource {
|
ResourceTypes.Register([]string{"exec"}, func(u *url.URL) Resource {
|
||||||
x := NewExec()
|
x := NewExec()
|
||||||
return x
|
return x
|
||||||
})
|
})
|
||||||
@ -75,6 +76,10 @@ func (x *Exec) SetURI(uri string) error {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *Exec) UseConfig(config ConfigurationValueGetter) {
|
||||||
|
x.config = config
|
||||||
|
}
|
||||||
|
|
||||||
func (x *Exec) ResolveId(ctx context.Context) string {
|
func (x *Exec) ResolveId(ctx context.Context) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
64
internal/source/container.go
Normal file
64
internal/source/container.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
_ "encoding/json"
|
||||||
|
_ "fmt"
|
||||||
|
_ "gopkg.in/yaml.v3"
|
||||||
|
"net/url"
|
||||||
|
_ "path/filepath"
|
||||||
|
"decl/internal/resource"
|
||||||
|
_ "os"
|
||||||
|
_ "io"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Container struct {
|
||||||
|
apiClient resource.ContainerClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewContainer(containerClientApi resource.ContainerClient) *Container {
|
||||||
|
var apiClient resource.ContainerClient = containerClientApi
|
||||||
|
if apiClient == nil {
|
||||||
|
var err error
|
||||||
|
apiClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Container{
|
||||||
|
apiClient: apiClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SourceTypes.Register([]string{"container"}, func(u *url.URL) DocSource {
|
||||||
|
c := NewContainer(nil)
|
||||||
|
return c
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) Type() string { return "container" }
|
||||||
|
|
||||||
|
func (c *Container) ExtractResources(filter ResourceSelector) ([]*resource.Document, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
slog.Info("container source ExtractResources()", "container", c)
|
||||||
|
containers, err := c.apiClient.ContainerList(ctx, types.ContainerListOptions{All: true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
document := resource.NewDocument()
|
||||||
|
for _, container := range containers {
|
||||||
|
runningContainer := resource.NewContainer(nil)
|
||||||
|
runningContainer.Inspect(ctx, container.ID)
|
||||||
|
document.AddResourceDeclaration("container", runningContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*resource.Document{document}, nil
|
||||||
|
}
|
62
internal/source/package.go
Normal file
62
internal/source/package.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "context"
|
||||||
|
_ "encoding/json"
|
||||||
|
_ "fmt"
|
||||||
|
_ "gopkg.in/yaml.v3"
|
||||||
|
"net/url"
|
||||||
|
_ "path/filepath"
|
||||||
|
"decl/internal/resource"
|
||||||
|
_ "os"
|
||||||
|
_ "io"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Package struct {
|
||||||
|
PackageType resource.PackageType `yaml:"type" json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPackage() *Package {
|
||||||
|
return &Package{ PackageType: resource.SystemPackageType }
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SourceTypes.Register([]string{"package"}, func(u *url.URL) DocSource {
|
||||||
|
p := NewPackage()
|
||||||
|
return p
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Package) Type() string { return "package" }
|
||||||
|
|
||||||
|
func (p *Package) ExtractResources(filter ResourceSelector) ([]*resource.Document, error) {
|
||||||
|
documents := make([]*resource.Document, 0, 100)
|
||||||
|
|
||||||
|
slog.Info("package source ExtractResources()", "package", p)
|
||||||
|
installedPackages := make([]*resource.Package, 0, 100)
|
||||||
|
cmd := p.PackageType.NewReadPackagesCommand()
|
||||||
|
if out, err := cmd.Execute(p); err == nil {
|
||||||
|
slog.Info("package source ExtractResources()", "output", out)
|
||||||
|
if exErr := cmd.Extractor(out, &installedPackages); exErr != nil {
|
||||||
|
return documents, exErr
|
||||||
|
}
|
||||||
|
document := resource.NewDocument()
|
||||||
|
for _, pkg := range installedPackages {
|
||||||
|
if pkg == nil {
|
||||||
|
pkg = resource.NewPackage()
|
||||||
|
}
|
||||||
|
pkg.PackageType = p.PackageType
|
||||||
|
|
||||||
|
document.AddResourceDeclaration("package", pkg)
|
||||||
|
}
|
||||||
|
documents = append(documents, document)
|
||||||
|
} else {
|
||||||
|
slog.Info("package source ExtractResources()", "output", out, "error", err)
|
||||||
|
return documents, err
|
||||||
|
}
|
||||||
|
return documents, nil
|
||||||
|
}
|
23
internal/source/package_test.go
Normal file
23
internal/source/package_test.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewPackageSource(t *testing.T) {
|
||||||
|
s := NewPackage()
|
||||||
|
assert.NotNil(t, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractPackages(t *testing.T) {
|
||||||
|
p := NewPackage()
|
||||||
|
assert.NotNil(t, p)
|
||||||
|
|
||||||
|
document, err := p.ExtractResources(nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, document)
|
||||||
|
assert.Greater(t, len(document), 0)
|
||||||
|
}
|
116
internal/transport/file.go
Normal file
116
internal/transport/file.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package transport
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "errors"
|
||||||
|
"path/filepath"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"fmt"
|
||||||
|
"compress/gzip"
|
||||||
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
uri *url.URL
|
||||||
|
path string
|
||||||
|
exttype string
|
||||||
|
fileext string
|
||||||
|
readHandle *os.File
|
||||||
|
writeHandle *os.File
|
||||||
|
gzip bool
|
||||||
|
gzipWriter io.WriteCloser
|
||||||
|
gzipReader io.ReadCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFile(u *url.URL) (f *File, err error) {
|
||||||
|
f = &File {
|
||||||
|
uri: u,
|
||||||
|
path: filepath.Join(u.Hostname(), u.RequestURI()),
|
||||||
|
}
|
||||||
|
f.extension()
|
||||||
|
f.DetectGzip()
|
||||||
|
|
||||||
|
if f.path == "" || f.path == "-" {
|
||||||
|
f.readHandle = os.Stdin
|
||||||
|
f.writeHandle = os.Stdout
|
||||||
|
} else {
|
||||||
|
if f.readHandle, err = os.Open(f.Path()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.writeHandle = f.readHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.Gzip() {
|
||||||
|
f.gzipWriter = gzip.NewWriter(f.writeHandle)
|
||||||
|
if f.gzipReader, err = gzip.NewReader(f.readHandle); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) extension() {
|
||||||
|
elements := strings.Split(f.path, ".")
|
||||||
|
numberOfElements := len(elements)
|
||||||
|
if numberOfElements > 2 {
|
||||||
|
f.exttype = elements[numberOfElements - 2]
|
||||||
|
f.fileext = elements[numberOfElements - 1]
|
||||||
|
}
|
||||||
|
f.exttype = elements[numberOfElements - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) DetectGzip() {
|
||||||
|
f.gzip = (f.uri.Query().Get("gzip") == "true" || f.fileext == "gz")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) URI() *url.URL {
|
||||||
|
return f.uri
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Path() string {
|
||||||
|
return f.path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Signature() (documentSignature string) {
|
||||||
|
if signatureResp, signatureErr := os.Open(fmt.Sprintf("%s.sig", f.uri.String())); signatureErr == nil {
|
||||||
|
defer signatureResp.Close()
|
||||||
|
readSignatureBody, readSignatureErr := io.ReadAll(signatureResp)
|
||||||
|
if readSignatureErr == nil {
|
||||||
|
documentSignature = string(readSignatureBody)
|
||||||
|
} else {
|
||||||
|
panic(readSignatureErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic(signatureErr)
|
||||||
|
}
|
||||||
|
return documentSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) ContentType() string {
|
||||||
|
return f.exttype
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) SetGzip(gzip bool) {
|
||||||
|
f.gzip = gzip
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Gzip() bool {
|
||||||
|
return f.gzip
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Reader() io.ReadCloser {
|
||||||
|
if f.Gzip() {
|
||||||
|
return f.gzipReader
|
||||||
|
}
|
||||||
|
return f.readHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Writer() io.WriteCloser {
|
||||||
|
if f.Gzip() {
|
||||||
|
return f.gzipWriter
|
||||||
|
}
|
||||||
|
return f.writeHandle
|
||||||
|
}
|
39
internal/transport/file_test.go
Normal file
39
internal/transport/file_test.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package transport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
var TransportFileTestFile = fmt.Sprintf("%s/foo", TempDir)
|
||||||
|
|
||||||
|
func TestNewTransportFileReader(t *testing.T) {
|
||||||
|
path := fmt.Sprintf("%s/foo", TempDir)
|
||||||
|
u, e := url.Parse(fmt.Sprintf("file://%s", path))
|
||||||
|
assert.Nil(t, e)
|
||||||
|
|
||||||
|
writeErr := os.WriteFile(path, []byte("test"), 0644)
|
||||||
|
assert.Nil(t, writeErr)
|
||||||
|
|
||||||
|
file, err := NewFile(u)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, file.Path(), path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewTransportFileReaderExtension(t *testing.T) {
|
||||||
|
u, e := url.Parse(fmt.Sprintf("file://%s.yaml", TransportFileTestFile))
|
||||||
|
assert.Nil(t, e)
|
||||||
|
|
||||||
|
f := &File{
|
||||||
|
uri: u,
|
||||||
|
path: filepath.Join(u.Hostname(), u.RequestURI()),
|
||||||
|
}
|
||||||
|
f.extension()
|
||||||
|
assert.Equal(t, f.exttype, "yaml")
|
||||||
|
}
|
119
internal/transport/http.go
Normal file
119
internal/transport/http.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package transport
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "errors"
|
||||||
|
"io"
|
||||||
|
_ "os"
|
||||||
|
"net/url"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"fmt"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BufferCloser struct {
|
||||||
|
stream io.Closer
|
||||||
|
*bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTP struct {
|
||||||
|
uri *url.URL
|
||||||
|
path string
|
||||||
|
exttype string
|
||||||
|
fileext string
|
||||||
|
buffer BufferCloser
|
||||||
|
getRequest *http.Request
|
||||||
|
getResponse *http.Response
|
||||||
|
postRequest *http.Request
|
||||||
|
postResponse *http.Response
|
||||||
|
Client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b BufferCloser) Close() error {
|
||||||
|
if b.stream != nil {
|
||||||
|
return b.stream.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTP(u *url.URL, ctx context.Context) (h *HTTP, err error) {
|
||||||
|
h = &HTTP {
|
||||||
|
uri: u,
|
||||||
|
path: filepath.Join(u.Hostname(), u.RequestURI()),
|
||||||
|
Client: http.DefaultClient,
|
||||||
|
}
|
||||||
|
h.extension()
|
||||||
|
|
||||||
|
h.postRequest, err = http.NewRequestWithContext(ctx, "POST", u.String(), h.buffer)
|
||||||
|
h.getRequest, err = http.NewRequestWithContext(ctx, "GET", u.String(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) extension() {
|
||||||
|
elements := strings.Split(h.path, ".")
|
||||||
|
numberOfElements := len(elements)
|
||||||
|
if numberOfElements > 2 {
|
||||||
|
h.exttype = elements[numberOfElements - 2]
|
||||||
|
h.fileext = elements[numberOfElements - 1]
|
||||||
|
}
|
||||||
|
h.exttype = elements[numberOfElements - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) URI() *url.URL {
|
||||||
|
return h.uri
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) Path() string {
|
||||||
|
return h.path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) Signature() (documentSignature string) {
|
||||||
|
if h.getResponse != nil {
|
||||||
|
documentSignature := h.getResponse.Header.Get("Signature")
|
||||||
|
if documentSignature == "" {
|
||||||
|
signatureResp, signatureErr := h.Client.Get(fmt.Sprintf("%s.sig", h.uri.String()))
|
||||||
|
if signatureErr == nil {
|
||||||
|
defer signatureResp.Body.Close()
|
||||||
|
readSignatureBody, readSignatureErr := io.ReadAll(signatureResp.Body)
|
||||||
|
if readSignatureErr == nil {
|
||||||
|
documentSignature = string(readSignatureBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return documentSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) ContentType() (contenttype string) {
|
||||||
|
contenttype = h.getResponse.Header.Get("Content-Type")
|
||||||
|
switch contenttype {
|
||||||
|
case "application/octet-stream":
|
||||||
|
return h.exttype
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) Gzip() bool {
|
||||||
|
return h.fileext == "gz"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) Reader() io.ReadCloser {
|
||||||
|
var err error
|
||||||
|
if h.getResponse, err = h.Client.Do(h.getRequest); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return h.getResponse.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) Writer() io.WriteCloser {
|
||||||
|
var err error
|
||||||
|
if h.postResponse, err = h.Client.Do(h.postRequest); err != nil {
|
||||||
|
h.postResponse, err = h.Client.Do(h.postRequest)
|
||||||
|
}
|
||||||
|
return h.buffer
|
||||||
|
}
|
21
internal/transport/http_test.go
Normal file
21
internal/transport/http_test.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package transport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
_ "fmt"
|
||||||
|
_ "os"
|
||||||
|
"net/url"
|
||||||
|
_ "path/filepath"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewTransportHTTPReader(t *testing.T) {
|
||||||
|
u, urlErr := url.Parse("https://localhost/resource")
|
||||||
|
assert.Nil(t, urlErr)
|
||||||
|
h, err := NewHTTP(u, context.Background())
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, h)
|
||||||
|
}
|
@ -4,48 +4,65 @@ package transport
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "errors"
|
_ "errors"
|
||||||
"fmt"
|
_ "fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"net/http"
|
_ "net/http"
|
||||||
_ "strings"
|
_ "strings"
|
||||||
"path/filepath"
|
_ "path/filepath"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
_ "os"
|
||||||
|
"context"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
URI() *url.URL
|
||||||
|
ContentType() string
|
||||||
|
Gzip() bool
|
||||||
|
Signature() string
|
||||||
|
Reader() io.ReadCloser
|
||||||
|
Writer() io.WriteCloser
|
||||||
|
}
|
||||||
|
|
||||||
type Reader struct {
|
type Reader struct {
|
||||||
uri *url.URL
|
uri *url.URL
|
||||||
handle any
|
handle Handler
|
||||||
stream io.ReadCloser
|
stream io.ReadCloser
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReader(u *url.URL) (*Reader, error) {
|
func NewReader(u *url.URL) (reader *Reader, e error) {
|
||||||
var e error
|
ctx := context.Background()
|
||||||
r := &Reader{ uri: u }
|
reader = &Reader{ uri: u }
|
||||||
switch u.Scheme {
|
switch u.Scheme {
|
||||||
case "http", "https":
|
case "http", "https":
|
||||||
resp, err := http.Get(u.String())
|
reader.handle, e = NewHTTP(u, ctx)
|
||||||
r.handle = resp
|
|
||||||
r.stream = resp.Body
|
|
||||||
e = err
|
|
||||||
case "file":
|
case "file":
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
path := filepath.Join(u.Hostname(), u.RequestURI())
|
reader.handle, e = NewFile(u)
|
||||||
file, err := os.Open(path)
|
|
||||||
r.handle = file
|
|
||||||
r.stream = file
|
|
||||||
e = err
|
|
||||||
}
|
}
|
||||||
return r, e
|
reader.stream = reader.handle.Reader()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type Writer struct {
|
type Writer struct {
|
||||||
|
uri *url.URL
|
||||||
|
handle Handler
|
||||||
|
stream io.WriteCloser
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWriter(url *url.URL) (*Writer, error) {
|
func NewWriter(u *url.URL) (writer *Writer, e error) {
|
||||||
return nil, nil
|
ctx := context.Background()
|
||||||
|
writer = &Writer{ uri: u }
|
||||||
|
switch u.Scheme {
|
||||||
|
case "http", "https":
|
||||||
|
writer.handle, e = NewHTTP(u, ctx)
|
||||||
|
case "file":
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
writer.handle, e = NewFile(u)
|
||||||
|
}
|
||||||
|
writer.stream = writer.handle.Writer()
|
||||||
|
return writer, e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) Read(b []byte) (int, error) {
|
func (r *Reader) Read(b []byte) (int, error) {
|
||||||
@ -56,17 +73,36 @@ func (r *Reader) Close() error {
|
|||||||
return r.stream.Close()
|
return r.stream.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Reader) ContentType() string {
|
||||||
|
return r.handle.ContentType()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) Gzip() bool {
|
||||||
|
return r.handle.Gzip()
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Reader) Signature() string {
|
func (r *Reader) Signature() string {
|
||||||
documentSignature := r.handle.(*http.Response).Header.Get("Signature")
|
return r.handle.Signature()
|
||||||
if documentSignature == "" {
|
|
||||||
signatureResp, signatureErr := http.Get(fmt.Sprintf("%s.sig", r.uri.String()))
|
|
||||||
if signatureErr == nil {
|
|
||||||
defer signatureResp.Body.Close()
|
|
||||||
readSignatureBody, readSignatureErr := io.ReadAll(signatureResp.Body)
|
|
||||||
if readSignatureErr == nil {
|
|
||||||
documentSignature = string(readSignatureBody)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func (w *Writer) Write(b []byte) (int, error) {
|
||||||
|
return w.stream.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Writer) Close() error {
|
||||||
|
return w.stream.Close()
|
||||||
}
|
}
|
||||||
return documentSignature
|
|
||||||
|
func (w *Writer) ContentType() string {
|
||||||
|
return w.handle.ContentType()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) Gzip() bool {
|
||||||
|
return w.handle.Gzip()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) Signature() string {
|
||||||
|
return w.handle.Signature()
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,14 @@ import (
|
|||||||
|
|
||||||
var TempDir string
|
var TempDir string
|
||||||
|
|
||||||
|
var testFileResourceDoc string = `
|
||||||
|
resources:
|
||||||
|
- type: file
|
||||||
|
transition: read
|
||||||
|
attributes:
|
||||||
|
path: /tmp/foobar
|
||||||
|
`
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
var err error
|
var err error
|
||||||
TempDir, err = os.MkdirTemp("", "testtransportfile")
|
TempDir, err = os.MkdirTemp("", "testtransportfile")
|
||||||
@ -38,3 +46,19 @@ func TestNewTransportReader(t *testing.T) {
|
|||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.NotNil(t, reader)
|
assert.NotNil(t, reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTransportReaderContentType(t *testing.T) {
|
||||||
|
path := fmt.Sprintf("%s/foo.jx.yaml", TempDir)
|
||||||
|
u, e := url.Parse(fmt.Sprintf("file://%s", path))
|
||||||
|
assert.Nil(t, e)
|
||||||
|
|
||||||
|
writeErr := os.WriteFile(path, []byte(testFileResourceDoc), 0644)
|
||||||
|
|
||||||
|
assert.Nil(t, writeErr)
|
||||||
|
|
||||||
|
reader, err := NewReader(u)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, reader)
|
||||||
|
|
||||||
|
assert.Equal(t, reader.ContentType(), "yaml")
|
||||||
|
}
|
||||||
|
@ -10,11 +10,17 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
The `types` package provides a generic method of registering a type factory.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrUnknownType = errors.New("Unknown type")
|
ErrUnknownType = errors.New("Unknown type")
|
||||||
)
|
)
|
||||||
|
|
||||||
//type Name string //`json:"type"`
|
//type Name[Registry any] string //`json:"type"`
|
||||||
|
|
||||||
type Factory[Product any] func(*url.URL) Product
|
type Factory[Product any] func(*url.URL) Product
|
||||||
type RegistryTypeMap[Product any] map[string]Factory[Product]
|
type RegistryTypeMap[Product any] map[string]Factory[Product]
|
||||||
@ -85,12 +91,12 @@ func (t *Types[Product]) Get(typename string) Factory[Product] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
func (n *Name) UnmarshalJSON(b []byte) error {
|
func (n *Name[Registry]) UnmarshalJSON(b []byte) error {
|
||||||
TypeName := strings.Trim(string(b), "\"")
|
ProductTypeName := strings.Trim(string(b), "\"")
|
||||||
if SourceTypes.Has(TypeName) {
|
if Registry.Has(ProductTypeName) {
|
||||||
*n = TypeName(TypeName)
|
*n = Name[Registry](ProductTypeName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%w: %s", ErrUnknownType, TypeName)
|
return fmt.Errorf("%w: %s", ErrUnknownType, ProductTypeName)
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user