From 52c083a3d97611ed0b1571749e75c36f7fb561d6 Mon Sep 17 00:00:00 2001 From: Matthew Rich Date: Mon, 1 Jul 2024 00:16:55 -0700 Subject: [PATCH] add source for containers, packages --- internal/codec/decoder.go | 10 +- internal/codec/encoder.go | 10 +- internal/codec/types.go | 61 ++++++++++++ internal/codec/types_test.go | 41 ++++++++ internal/command/command.go | 143 +++++++++++++++++++++++++++ internal/command/command_test.go | 60 +++++++++++ internal/resource/exec.go | 7 +- internal/source/container.go | 64 ++++++++++++ internal/source/package.go | 62 ++++++++++++ internal/source/package_test.go | 23 +++++ internal/transport/file.go | 116 ++++++++++++++++++++++ internal/transport/file_test.go | 39 ++++++++ internal/transport/http.go | 119 ++++++++++++++++++++++ internal/transport/http_test.go | 21 ++++ internal/transport/transport.go | 104 ++++++++++++------- internal/transport/transport_test.go | 24 +++++ internal/types/types.go | 18 ++-- 17 files changed, 879 insertions(+), 43 deletions(-) create mode 100644 internal/codec/types.go create mode 100644 internal/codec/types_test.go create mode 100644 internal/command/command.go create mode 100644 internal/command/command_test.go create mode 100644 internal/source/container.go create mode 100644 internal/source/package.go create mode 100644 internal/source/package_test.go create mode 100644 internal/transport/file.go create mode 100644 internal/transport/file_test.go create mode 100644 internal/transport/http.go create mode 100644 internal/transport/http_test.go diff --git a/internal/codec/decoder.go b/internal/codec/decoder.go index 3b375e4..9f89d54 100644 --- a/internal/codec/decoder.go +++ b/internal/codec/decoder.go @@ -18,7 +18,15 @@ type Decoder interface { 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 } diff --git a/internal/codec/encoder.go b/internal/codec/encoder.go index da2fbc6..80c09bd 100644 --- a/internal/codec/encoder.go +++ b/internal/codec/encoder.go @@ -18,7 +18,15 @@ type Encoder interface { 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 } diff --git a/internal/codec/types.go b/internal/codec/types.go new file mode 100644 index 0000000..6c6b26b --- /dev/null +++ b/internal/codec/types.go @@ -0,0 +1,61 @@ +// Copyright 2024 Matthew Rich . 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) +} diff --git a/internal/codec/types_test.go b/internal/codec/types_test.go new file mode 100644 index 0000000..7d15461 --- /dev/null +++ b/internal/codec/types_test.go @@ -0,0 +1,41 @@ +// Copyright 2024 Matthew Rich . 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) +} diff --git a/internal/command/command.go b/internal/command/command.go new file mode 100644 index 0000000..45a8882 --- /dev/null +++ b/internal/command/command.go @@ -0,0 +1,143 @@ +// Copyright 2024 Matthew Rich . 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) +} diff --git a/internal/command/command_test.go b/internal/command/command_test.go new file mode 100644 index 0000000..6fb43c9 --- /dev/null +++ b/internal/command/command_test.go @@ -0,0 +1,60 @@ +// Copyright 2024 Matthew Rich . 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) +} diff --git a/internal/resource/exec.go b/internal/resource/exec.go index ff20292..305d0ef 100644 --- a/internal/resource/exec.go +++ b/internal/resource/exec.go @@ -25,12 +25,13 @@ type Exec struct { UpdateTemplate Command `yaml:"update" json:"update"` DeleteTemplate Command `yaml:"delete" json:"delete"` + config ConfigurationValueGetter // state attributes State string `yaml:"state"` } func init() { - ResourceTypes.Register("exec", func(u *url.URL) Resource { + ResourceTypes.Register([]string{"exec"}, func(u *url.URL) Resource { x := NewExec() return x }) @@ -75,6 +76,10 @@ func (x *Exec) SetURI(uri string) error { return e } +func (x *Exec) UseConfig(config ConfigurationValueGetter) { + x.config = config +} + func (x *Exec) ResolveId(ctx context.Context) string { return "" } diff --git a/internal/source/container.go b/internal/source/container.go new file mode 100644 index 0000000..eda47be --- /dev/null +++ b/internal/source/container.go @@ -0,0 +1,64 @@ +// Copyright 2024 Matthew Rich . 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 +} diff --git a/internal/source/package.go b/internal/source/package.go new file mode 100644 index 0000000..39c61e9 --- /dev/null +++ b/internal/source/package.go @@ -0,0 +1,62 @@ +// Copyright 2024 Matthew Rich . 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 +} diff --git a/internal/source/package_test.go b/internal/source/package_test.go new file mode 100644 index 0000000..334468a --- /dev/null +++ b/internal/source/package_test.go @@ -0,0 +1,23 @@ +// Copyright 2024 Matthew Rich . 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) +} diff --git a/internal/transport/file.go b/internal/transport/file.go new file mode 100644 index 0000000..99dfec6 --- /dev/null +++ b/internal/transport/file.go @@ -0,0 +1,116 @@ +// Copyright 2024 Matthew Rich . 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 +} diff --git a/internal/transport/file_test.go b/internal/transport/file_test.go new file mode 100644 index 0000000..4129b36 --- /dev/null +++ b/internal/transport/file_test.go @@ -0,0 +1,39 @@ +// Copyright 2024 Matthew Rich . 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") +} diff --git a/internal/transport/http.go b/internal/transport/http.go new file mode 100644 index 0000000..ec78be5 --- /dev/null +++ b/internal/transport/http.go @@ -0,0 +1,119 @@ +// Copyright 2024 Matthew Rich . 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 +} diff --git a/internal/transport/http_test.go b/internal/transport/http_test.go new file mode 100644 index 0000000..99298a3 --- /dev/null +++ b/internal/transport/http_test.go @@ -0,0 +1,21 @@ +// Copyright 2024 Matthew Rich . 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) +} diff --git a/internal/transport/transport.go b/internal/transport/transport.go index f8c29e5..9a2b533 100644 --- a/internal/transport/transport.go +++ b/internal/transport/transport.go @@ -4,48 +4,65 @@ package transport import ( _ "errors" - "fmt" +_ "fmt" "net/url" - "net/http" +_ "net/http" _ "strings" - "path/filepath" +_ "path/filepath" "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 { uri *url.URL - handle any + handle Handler stream io.ReadCloser } -func NewReader(u *url.URL) (*Reader, error) { - var e error - r := &Reader{ uri: u } +func NewReader(u *url.URL) (reader *Reader, e error) { + ctx := context.Background() + reader = &Reader{ uri: u } switch u.Scheme { case "http", "https": - resp, err := http.Get(u.String()) - r.handle = resp - r.stream = resp.Body - e = err + reader.handle, e = NewHTTP(u, ctx) case "file": fallthrough default: - path := filepath.Join(u.Hostname(), u.RequestURI()) - file, err := os.Open(path) - r.handle = file - r.stream = file - e = err + reader.handle, e = NewFile(u) } - return r, e + reader.stream = reader.handle.Reader() + return } type Writer struct { - + uri *url.URL + handle Handler + stream io.WriteCloser } -func NewWriter(url *url.URL) (*Writer, error) { - return nil, nil +func NewWriter(u *url.URL) (writer *Writer, e error) { + 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) { @@ -56,17 +73,36 @@ func (r *Reader) Close() error { return r.stream.Close() } -func (r *Reader) Signature() string { - documentSignature := r.handle.(*http.Response).Header.Get("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) - } - } - } - return documentSignature +func (r *Reader) ContentType() string { + return r.handle.ContentType() +} + +func (r *Reader) Gzip() bool { + return r.handle.Gzip() +} + +func (r *Reader) Signature() string { + return r.handle.Signature() +} + + + +func (w *Writer) Write(b []byte) (int, error) { + return w.stream.Write(b) +} + +func (w *Writer) Close() error { + return w.stream.Close() +} + +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() } diff --git a/internal/transport/transport_test.go b/internal/transport/transport_test.go index b20c4dc..7bbb3a8 100644 --- a/internal/transport/transport_test.go +++ b/internal/transport/transport_test.go @@ -13,6 +13,14 @@ import ( var TempDir string +var testFileResourceDoc string = ` +resources: +- type: file + transition: read + attributes: + path: /tmp/foobar +` + func TestMain(m *testing.M) { var err error TempDir, err = os.MkdirTemp("", "testtransportfile") @@ -38,3 +46,19 @@ func TestNewTransportReader(t *testing.T) { assert.Nil(t, err) 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") +} diff --git a/internal/types/types.go b/internal/types/types.go index e27dbbc..cbca7f1 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -10,11 +10,17 @@ import ( "path/filepath" ) +/* + +The `types` package provides a generic method of registering a type factory. + +*/ + var ( 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 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 { - TypeName := strings.Trim(string(b), "\"") - if SourceTypes.Has(TypeName) { - *n = TypeName(TypeName) +func (n *Name[Registry]) UnmarshalJSON(b []byte) error { + ProductTypeName := strings.Trim(string(b), "\"") + if Registry.Has(ProductTypeName) { + *n = Name[Registry](ProductTypeName) return nil } - return fmt.Errorf("%w: %s", ErrUnknownType, TypeName) + return fmt.Errorf("%w: %s", ErrUnknownType, ProductTypeName) } */