add encoder/decoder support for json and yaml
add support for jsonschema verification
This commit is contained in:
parent
7181075568
commit
5a49359722
5
go.mod
5
go.mod
@ -8,7 +8,7 @@ require (
|
|||||||
github.com/Microsoft/go-winio v0.4.14 // indirect
|
github.com/Microsoft/go-winio v0.4.14 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/distribution/reference v0.5.0 // indirect
|
github.com/distribution/reference v0.5.0 // indirect
|
||||||
github.com/docker/docker v25.0.3+incompatible // indirect
|
github.com/docker/docker v25.0.5+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.5.0 // indirect
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
@ -19,6 +19,9 @@ require (
|
|||||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||||
|
11
go.sum
11
go.sum
@ -1,11 +1,14 @@
|
|||||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
||||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
||||||
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/docker v25.0.3+incompatible h1:D5fy/lYmY7bvZa0XTZ5/UJPljor41F+vdyJG5luQLfQ=
|
github.com/docker/docker v25.0.3+incompatible h1:D5fy/lYmY7bvZa0XTZ5/UJPljor41F+vdyJG5luQLfQ=
|
||||||
github.com/docker/docker v25.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v25.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE=
|
||||||
|
github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
@ -33,10 +36,18 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||||
|
@ -35,31 +35,31 @@ type ContainerClient interface {
|
|||||||
|
|
||||||
type Container struct {
|
type Container struct {
|
||||||
loader YamlLoader
|
loader YamlLoader
|
||||||
Id string `yaml:"ID",omitempty`
|
Id string `json:"ID,omitempty" yaml:"ID,omitempty"`
|
||||||
Name string `yaml:"name"`
|
Name string `json:"name" yaml:"name"`
|
||||||
Path string `yaml:"path"`
|
Path string `json:"path" yaml:"path"`
|
||||||
Cmd []string `yaml:"cmd",omitempty`
|
Cmd []string `json:"cmd,omitempty" yaml:"cmd,omitempty"`
|
||||||
Entrypoint strslice.StrSlice `yaml:"entrypoint",omitempty`
|
Entrypoint strslice.StrSlice `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty"`
|
||||||
Args []string `yaml:"args",omitempty`
|
Args []string `json:"args,omitempty" yaml:"args,omitempty"`
|
||||||
Environment map[string]string `yaml:"environment"`
|
Environment map[string]string `json:"environment" yaml:"environment"`
|
||||||
Image string `yaml:"image"`
|
Image string `json:"image" yaml:"image"`
|
||||||
ResolvConfPath string `yaml:"resolvconfpath"`
|
ResolvConfPath string `json:"resolvconfpath" yaml:"resolvconfpath"`
|
||||||
HostnamePath string `yaml:"hostnamepath"`
|
HostnamePath string `json:"hostnamepath" yaml:"hostnamepath"`
|
||||||
HostsPath string `yaml:"hostspath"`
|
HostsPath string `json:"hostpath" yaml:"hostspath"`
|
||||||
LogPath string `yaml:"logpath"`
|
LogPath string `json:"logpath" yaml:"logpath"`
|
||||||
Created string `yaml:"created"`
|
Created string `json:"created" yaml:"created"`
|
||||||
ContainerState types.ContainerState `yaml:"containerstate"`
|
ContainerState types.ContainerState `json:"containerstate" yaml:"containerstate"`
|
||||||
RestartCount int `yaml:"restartcount"`
|
RestartCount int `json:"restartcount" yaml:"restartcount"`
|
||||||
Driver string `yaml:"driver"`
|
Driver string `json:"driver" yaml:"driver"`
|
||||||
Platform string `yaml:"platform"`
|
Platform string `json:"platform" yaml:"platform"`
|
||||||
MountLabel string `yaml:"mountlabel"`
|
MountLabel string `json:"mountlabel" yaml:"mountlabel"`
|
||||||
ProcessLabel string `yaml:"processlabel"`
|
ProcessLabel string `json:"processlabel" yaml:"processlabel"`
|
||||||
AppArmorProfile string `yaml:"apparmorprofile"`
|
AppArmorProfile string `json:"apparmorprofile" yaml:"apparmorprofile"`
|
||||||
ExecIDs []string `yaml:"execids"`
|
ExecIDs []string `json:"execids" yaml:"execids"`
|
||||||
HostConfig container.HostConfig `yaml:"hostconfig"`
|
HostConfig container.HostConfig `json:"hostconfig" yaml:"hostconfig"`
|
||||||
GraphDriver types.GraphDriverData `yaml:"graphdriver"`
|
GraphDriver types.GraphDriverData `json:"graphdriver" yaml:"graphdriver"`
|
||||||
SizeRw *int64 `json:",omitempty"`
|
SizeRw *int64 `json:",omitempty" yaml:",omitempty"`
|
||||||
SizeRootFs *int64 `json:",omitempty"`
|
SizeRootFs *int64 `json:",omitempty" yaml:",omitempty"`
|
||||||
/*
|
/*
|
||||||
Mounts []MountPoint
|
Mounts []MountPoint
|
||||||
Config *container.Config
|
Config *container.Config
|
||||||
|
@ -4,15 +4,19 @@ package resource
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type DeclarationType struct {
|
||||||
|
Type TypeName `json:"type" yaml:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
type Declaration struct {
|
type Declaration struct {
|
||||||
Type string `yaml:"type"`
|
Type TypeName `json:"type" yaml:"type"`
|
||||||
Attributes yaml.Node `yaml:"attributes"`
|
Attributes Resource `json:"attributes" yaml:"attributes"`
|
||||||
Implementation Resource `-`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourceLoader interface {
|
type ResourceLoader interface {
|
||||||
@ -43,41 +47,83 @@ func (d *Declaration) LoadDecl(yamlResourceDeclaration string) error {
|
|||||||
func (d *Declaration) NewResource() error {
|
func (d *Declaration) NewResource() error {
|
||||||
uri := fmt.Sprintf("%s://", d.Type)
|
uri := fmt.Sprintf("%s://", d.Type)
|
||||||
newResource, err := ResourceTypes.New(uri)
|
newResource, err := ResourceTypes.New(uri)
|
||||||
d.Implementation = newResource
|
d.Attributes = newResource
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Declaration) LoadResourceFromYaml() (Resource, error) {
|
|
||||||
var errResource error
|
|
||||||
if d.Implementation == nil {
|
|
||||||
errResource = d.NewResource()
|
|
||||||
if errResource != nil {
|
|
||||||
return nil, errResource
|
|
||||||
}
|
|
||||||
}
|
|
||||||
d.Attributes.Decode(d.Implementation)
|
|
||||||
d.Implementation.ResolveId(context.Background())
|
|
||||||
return d.Implementation, errResource
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Declaration) UpdateYamlFromResource() error {
|
|
||||||
if d.Implementation != nil {
|
|
||||||
return d.Attributes.Encode(d.Implementation)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Declaration) Resource() Resource {
|
func (d *Declaration) Resource() Resource {
|
||||||
return d.Implementation
|
return d.Attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Declaration) SetURI(uri string) error {
|
func (d *Declaration) SetURI(uri string) error {
|
||||||
slog.Info("SetURI()", "uri", uri)
|
slog.Info("SetURI()", "uri", uri)
|
||||||
d.Implementation = NewResource(uri)
|
d.Attributes = NewResource(uri)
|
||||||
if d.Implementation == nil {
|
if d.Attributes == nil {
|
||||||
panic("unknown resource")
|
panic("unknown resource")
|
||||||
}
|
}
|
||||||
d.Type = d.Implementation.Type()
|
d.Type = TypeName(d.Attributes.Type())
|
||||||
d.Implementation.Read(context.Background()) // fix context
|
d.Attributes.Read(context.Background()) // fix context
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Declaration) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
t := &DeclarationType{}
|
||||||
|
if unmarshalResourceTypeErr := value.Decode(t); unmarshalResourceTypeErr != nil {
|
||||||
|
return unmarshalResourceTypeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Type = t.Type
|
||||||
|
newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", d.Type))
|
||||||
|
if resourceErr != nil {
|
||||||
|
return resourceErr
|
||||||
|
}
|
||||||
|
d.Attributes = newResource
|
||||||
|
resourceAttrs := struct {
|
||||||
|
Attributes yaml.Node `json:"attributes"`
|
||||||
|
}{}
|
||||||
|
if unmarshalAttributesErr := value.Decode(&resourceAttrs); unmarshalAttributesErr != nil {
|
||||||
|
return unmarshalAttributesErr
|
||||||
|
}
|
||||||
|
if unmarshalResourceErr := resourceAttrs.Attributes.Decode(d.Attributes); unmarshalResourceErr != nil {
|
||||||
|
return unmarshalResourceErr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Declaration) UnmarshalJSON(data []byte) error {
|
||||||
|
t := &DeclarationType{}
|
||||||
|
if unmarshalResourceTypeErr := json.Unmarshal(data, t); unmarshalResourceTypeErr != nil {
|
||||||
|
return unmarshalResourceTypeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Type = t.Type
|
||||||
|
|
||||||
|
newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", d.Type))
|
||||||
|
if resourceErr != nil {
|
||||||
|
return resourceErr
|
||||||
|
}
|
||||||
|
d.Attributes = newResource
|
||||||
|
|
||||||
|
resourceAttrs := struct {
|
||||||
|
Attributes Resource `json:"attributes"`
|
||||||
|
}{Attributes: newResource}
|
||||||
|
if unmarshalAttributesErr := json.Unmarshal(data, &resourceAttrs); unmarshalAttributesErr != nil {
|
||||||
|
return unmarshalAttributesErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func (d *Declaration) MarshalJSON() ([]byte, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.WriteByte('"')
|
||||||
|
buf.WriteString("value"))
|
||||||
|
buf.WriteByte('"')
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (d *Declaration) MarshalYAML() (any, error) {
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
package resource
|
package resource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
_ "log"
|
_ "log"
|
||||||
@ -55,7 +56,7 @@ func TestNewResourceDeclarationType(t *testing.T) {
|
|||||||
assert.NotEqual(t, nil, resourceDeclaration)
|
assert.NotEqual(t, nil, resourceDeclaration)
|
||||||
|
|
||||||
resourceDeclaration.LoadDecl(decl)
|
resourceDeclaration.LoadDecl(decl)
|
||||||
assert.Equal(t, "file", resourceDeclaration.Type)
|
assert.Equal(t, TypeName("file"), resourceDeclaration.Type)
|
||||||
assert.NotEqual(t, nil, resourceDeclaration.Attributes)
|
assert.NotEqual(t, nil, resourceDeclaration.Attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +71,38 @@ func TestDeclarationNewResource(t *testing.T) {
|
|||||||
errNewFileResource := resourceDeclaration.NewResource()
|
errNewFileResource := resourceDeclaration.NewResource()
|
||||||
assert.Nil(t, errNewFileResource)
|
assert.Nil(t, errNewFileResource)
|
||||||
|
|
||||||
//assert.NotNil(t, resourceDeclaration.Implementation)
|
|
||||||
assert.NotNil(t, resourceDeclaration.Attributes)
|
assert.NotNil(t, resourceDeclaration.Attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeclarationJson(t *testing.T) {
|
||||||
|
fileDeclJson := `
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"attributes": {
|
||||||
|
"path": "foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
resourceDeclaration := NewDeclaration()
|
||||||
|
e := json.Unmarshal([]byte(fileDeclJson), resourceDeclaration)
|
||||||
|
assert.Nil(t, e)
|
||||||
|
assert.Equal(t, TypeName("file"), resourceDeclaration.Type)
|
||||||
|
assert.Equal(t, "foo", resourceDeclaration.Attributes.(*File).Path)
|
||||||
|
|
||||||
|
userDeclJson := `
|
||||||
|
{
|
||||||
|
"type": "user",
|
||||||
|
"attributes": {
|
||||||
|
"name": "testuser",
|
||||||
|
"uid": 10012
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
userResourceDeclaration := NewDeclaration()
|
||||||
|
ue := json.Unmarshal([]byte(userDeclJson), userResourceDeclaration)
|
||||||
|
assert.Nil(t, ue)
|
||||||
|
assert.Equal(t, TypeName("user"), userResourceDeclaration.Type)
|
||||||
|
assert.Equal(t, "testuser", userResourceDeclaration.Attributes.(*User).Name)
|
||||||
|
assert.Equal(t, 10012, userResourceDeclaration.Attributes.(*User).UID)
|
||||||
|
|
||||||
|
}
|
||||||
|
34
internal/resource/decoder.go
Normal file
34
internal/resource/decoder.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
_ "fmt"
|
||||||
|
_ "github.com/xeipuuv/gojsonschema"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"io"
|
||||||
|
_ "log"
|
||||||
|
)
|
||||||
|
|
||||||
|
//type JSONDecoder json.Decoder
|
||||||
|
|
||||||
|
type Decoder interface {
|
||||||
|
Decode(v any) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDecoder() *Decoder {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJSONDecoder(r io.Reader) Decoder {
|
||||||
|
return json.NewDecoder(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYAMLDecoder(r io.Reader) Decoder {
|
||||||
|
return yaml.NewDecoder(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProtoBufDecoder(r io.Reader) Decoder {
|
||||||
|
return nil
|
||||||
|
}
|
39
internal/resource/decoder_test.go
Normal file
39
internal/resource/decoder_test.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
_ "log"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewYAMLDecoder(t *testing.T) {
|
||||||
|
e := NewYAMLDecoder(strings.NewReader(""))
|
||||||
|
assert.NotNil(t, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDecoderDecodeJSON(t *testing.T) {
|
||||||
|
decl := `{
|
||||||
|
"name": "testuser",
|
||||||
|
"uid": 12001,
|
||||||
|
"group": "12001",
|
||||||
|
"home": "/home/testuser",
|
||||||
|
"state": "present"
|
||||||
|
}`
|
||||||
|
|
||||||
|
jsonReader := strings.NewReader(decl)
|
||||||
|
user := NewUser()
|
||||||
|
|
||||||
|
e := NewJSONDecoder(jsonReader)
|
||||||
|
assert.NotNil(t, e)
|
||||||
|
docErr := e.Decode(user)
|
||||||
|
assert.Nil(t, docErr)
|
||||||
|
|
||||||
|
s := NewSchema(user.Type())
|
||||||
|
|
||||||
|
validateErr := s.Validate(decl)
|
||||||
|
assert.Nil(t, validateErr)
|
||||||
|
}
|
@ -3,15 +3,19 @@
|
|||||||
package resource
|
package resource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
_ "fmt"
|
_ "fmt"
|
||||||
|
"github.com/xeipuuv/gojsonschema"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"io"
|
"io"
|
||||||
_ "log"
|
_ "log"
|
||||||
_ "net/url"
|
_ "net/url"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Document struct {
|
type Document struct {
|
||||||
ResourceDecls []Declaration `yaml:"resources"`
|
ResourceDecls []Declaration `json:"resources" yaml:"resources"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDocument() *Document {
|
func NewDocument() *Document {
|
||||||
@ -20,10 +24,28 @@ func NewDocument() *Document {
|
|||||||
|
|
||||||
func (d *Document) Load(r io.Reader) error {
|
func (d *Document) Load(r io.Reader) error {
|
||||||
yamlDecoder := yaml.NewDecoder(r)
|
yamlDecoder := yaml.NewDecoder(r)
|
||||||
yamlDecoder.Decode(d)
|
if e := yamlDecoder.Decode(d); e != nil {
|
||||||
for i := range d.ResourceDecls {
|
return e
|
||||||
if _, e := d.ResourceDecls[i].LoadResourceFromYaml(); e != nil {
|
}
|
||||||
return e
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) Validate() error {
|
||||||
|
jsonDocument, jsonErr := d.JSON()
|
||||||
|
if jsonErr == nil {
|
||||||
|
schemaLoader := gojsonschema.NewReferenceLoader("file://schemas/document.jsonschema")
|
||||||
|
documentLoader := gojsonschema.NewBytesLoader(jsonDocument)
|
||||||
|
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.Valid() {
|
||||||
|
schemaErrors := strings.Builder{}
|
||||||
|
for _, err := range result.Errors() {
|
||||||
|
schemaErrors.WriteString(err.String() + "\n")
|
||||||
|
}
|
||||||
|
return errors.New(schemaErrors.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -43,16 +65,15 @@ func (d *Document) Apply() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Document) Generate(w io.Writer) error {
|
func (d *Document) Generate(w io.Writer) error {
|
||||||
yamlEncoder := yaml.NewEncoder(w)
|
e := NewYAMLEncoder(w)
|
||||||
yamlEncoder.Encode(d)
|
e.Encode(d)
|
||||||
return yamlEncoder.Close()
|
return e.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration Resource) {
|
func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration Resource) {
|
||||||
decl := NewDeclaration()
|
decl := NewDeclaration()
|
||||||
decl.Type = resourceType
|
decl.Type = TypeName(resourceType)
|
||||||
decl.Implementation = resourceDeclaration
|
decl.Attributes = resourceDeclaration
|
||||||
decl.UpdateYamlFromResource()
|
|
||||||
d.ResourceDecls = append(d.ResourceDecls, *decl)
|
d.ResourceDecls = append(d.ResourceDecls, *decl)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,8 +82,15 @@ func (d *Document) AddResource(uri string) error {
|
|||||||
//if e == nil {
|
//if e == nil {
|
||||||
decl := NewDeclaration()
|
decl := NewDeclaration()
|
||||||
decl.SetURI(uri)
|
decl.SetURI(uri)
|
||||||
decl.UpdateYamlFromResource()
|
|
||||||
d.ResourceDecls = append(d.ResourceDecls, *decl)
|
d.ResourceDecls = append(d.ResourceDecls, *decl)
|
||||||
//}
|
//}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Document) JSON() ([]byte, error) {
|
||||||
|
return json.Marshal(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Document) YAML() ([]byte, error) {
|
||||||
|
return yaml.Marshal(d)
|
||||||
|
}
|
||||||
|
@ -44,19 +44,18 @@ resources:
|
|||||||
- type: user
|
- type: user
|
||||||
attributes:
|
attributes:
|
||||||
name: "testuser"
|
name: "testuser"
|
||||||
uid: "10022"
|
uid: 10022
|
||||||
gid: "10022"
|
group: "10022"
|
||||||
home: "/home/testuser"
|
home: "/home/testuser"
|
||||||
state: present
|
state: present
|
||||||
`, file)
|
`, file)
|
||||||
|
|
||||||
d := NewDocument()
|
d := NewDocument()
|
||||||
assert.NotEqual(t, nil, d)
|
assert.NotEqual(t, nil, d)
|
||||||
|
|
||||||
docReader := strings.NewReader(document)
|
docReader := strings.NewReader(document)
|
||||||
|
|
||||||
e := d.Load(docReader)
|
e := d.Load(docReader)
|
||||||
assert.Equal(t, nil, e)
|
assert.Nil(t, e)
|
||||||
|
|
||||||
resources := d.Resources()
|
resources := d.Resources()
|
||||||
assert.Equal(t, 2, len(resources))
|
assert.Equal(t, 2, len(resources))
|
||||||
@ -110,7 +109,6 @@ resources:
|
|||||||
f.(*File).Path = filepath.Join(TempDir, "foo.txt")
|
f.(*File).Path = filepath.Join(TempDir, "foo.txt")
|
||||||
f.(*File).Read(ctx)
|
f.(*File).Read(ctx)
|
||||||
d.AddResourceDeclaration("file", f)
|
d.AddResourceDeclaration("file", f)
|
||||||
|
|
||||||
ey := d.Generate(&documentYaml)
|
ey := d.Generate(&documentYaml)
|
||||||
assert.Equal(t, nil, ey)
|
assert.Equal(t, nil, ey)
|
||||||
|
|
||||||
@ -127,3 +125,59 @@ func TestDocumentAddResource(t *testing.T) {
|
|||||||
assert.NotNil(t, d)
|
assert.NotNil(t, d)
|
||||||
d.AddResource(fmt.Sprintf("file://%s", file))
|
d.AddResource(fmt.Sprintf("file://%s", file))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDocumentJSON(t *testing.T) {
|
||||||
|
document := fmt.Sprintf(`
|
||||||
|
---
|
||||||
|
resources:
|
||||||
|
- type: user
|
||||||
|
attributes:
|
||||||
|
name: "testuser"
|
||||||
|
uid: 10022
|
||||||
|
group: "10022"
|
||||||
|
home: "/home/testuser"
|
||||||
|
state: present
|
||||||
|
`)
|
||||||
|
d := NewDocument()
|
||||||
|
assert.NotNil(t, d)
|
||||||
|
docReader := strings.NewReader(document)
|
||||||
|
|
||||||
|
e := d.Load(docReader)
|
||||||
|
assert.Nil(t, e)
|
||||||
|
|
||||||
|
marshalledJSON, jsonErr := d.JSON()
|
||||||
|
assert.Nil(t, jsonErr)
|
||||||
|
assert.Greater(t, len(marshalledJSON), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocumentJSONSchema(t *testing.T) {
|
||||||
|
document := NewDocument()
|
||||||
|
document.ResourceDecls = []Declaration{}
|
||||||
|
e := document.Validate()
|
||||||
|
assert.Nil(t, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocumentYAML(t *testing.T) {
|
||||||
|
document := fmt.Sprintf(`
|
||||||
|
---
|
||||||
|
resources:
|
||||||
|
- type: user
|
||||||
|
attributes:
|
||||||
|
name: "testuser"
|
||||||
|
uid: 10022
|
||||||
|
group: "10022"
|
||||||
|
home: "/home/testuser"
|
||||||
|
state: present
|
||||||
|
`)
|
||||||
|
d := NewDocument()
|
||||||
|
assert.NotNil(t, d)
|
||||||
|
docReader := strings.NewReader(document)
|
||||||
|
|
||||||
|
e := d.Load(docReader)
|
||||||
|
assert.Nil(t, e)
|
||||||
|
|
||||||
|
marshalledYAML, yamlErr := d.YAML()
|
||||||
|
assert.Nil(t, yamlErr)
|
||||||
|
assert.YAMLEq(t, string(document), string(marshalledYAML))
|
||||||
|
|
||||||
|
}
|
||||||
|
42
internal/resource/encoder.go
Normal file
42
internal/resource/encoder.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
_ "fmt"
|
||||||
|
_ "github.com/xeipuuv/gojsonschema"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"io"
|
||||||
|
_ "log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JSONEncoder json.Encoder
|
||||||
|
|
||||||
|
type Encoder interface {
|
||||||
|
Encode(v any) error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEncoder() *Encoder {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJSONEncoder(w io.Writer) Encoder {
|
||||||
|
return (*JSONEncoder)(json.NewEncoder(w))
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYAMLEncoder(w io.Writer) Encoder {
|
||||||
|
return yaml.NewEncoder(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProtoBufEncoder(w io.Writer) Encoder {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JSONEncoder) Encode(v any) error {
|
||||||
|
return (*json.Encoder)(j).Encode(v)
|
||||||
|
}
|
||||||
|
func (j *JSONEncoder) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
33
internal/resource/encoder_test.go
Normal file
33
internal/resource/encoder_test.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
_ "log"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewYAMLEncoder(t *testing.T) {
|
||||||
|
var yamlDoc strings.Builder
|
||||||
|
e := NewYAMLEncoder(&yamlDoc)
|
||||||
|
assert.NotNil(t, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewEncoderEncodeJSON(t *testing.T) {
|
||||||
|
var jsonDoc strings.Builder
|
||||||
|
file := NewFile()
|
||||||
|
file.Path = "foo"
|
||||||
|
|
||||||
|
e := NewJSONEncoder(&jsonDoc)
|
||||||
|
assert.NotNil(t, e)
|
||||||
|
docErr := e.Encode(file)
|
||||||
|
assert.Nil(t, docErr)
|
||||||
|
|
||||||
|
s := NewSchema(file.Type())
|
||||||
|
|
||||||
|
validateErr := s.Validate(jsonDoc.String())
|
||||||
|
assert.Nil(t, validateErr)
|
||||||
|
}
|
@ -42,22 +42,25 @@ func init() {
|
|||||||
// Manage the state of file system objects
|
// Manage the state of file system objects
|
||||||
type File struct {
|
type File struct {
|
||||||
loader YamlLoader
|
loader YamlLoader
|
||||||
Path string `yaml:"path"`
|
Path string `json:"path" yaml:"path"`
|
||||||
Owner string `yaml:"owner"`
|
Owner string `json:"owner" yaml:"owner"`
|
||||||
Group string `yaml:"group"`
|
Group string `json:"group" yaml:"group"`
|
||||||
Mode string `yaml:"mode"`
|
Mode string `json:"mode" yaml:"mode"`
|
||||||
|
|
||||||
Atime time.Time `yaml:"atime",omitempty`
|
Atime time.Time `json:"atime,omitempty" yaml:"atime,omitempty"`
|
||||||
Ctime time.Time `yaml:"ctime",omitempty`
|
Ctime time.Time `json:"ctime,omitempty" yaml:"ctime,omitempty"`
|
||||||
Mtime time.Time `yaml:"mtime",omitempty`
|
Mtime time.Time `json:"mtime,omitempty" yaml:"mtime,omitempty"`
|
||||||
|
|
||||||
Content string `yaml:"content",omitempty`
|
Content string `json:"content,omitempty" yaml:"content,omitempty"`
|
||||||
FileType FileType `yaml:"filetype"`
|
Target string `json:"target,omitempty" yaml:"target,omitempty"`
|
||||||
State string `yaml:"state"`
|
FileType FileType `json:"filetype" yaml:"filetype"`
|
||||||
|
State string `json:"state" yaml:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFile() *File {
|
func NewFile() *File {
|
||||||
return &File{loader: YamlLoadDecl, FileType: RegularFile}
|
currentUser, _ := user.Current()
|
||||||
|
group, _ := user.LookupGroupId(currentUser.Gid)
|
||||||
|
return &File{loader: YamlLoadDecl, Owner: currentUser.Username, Group: group.Name, Mode: "0666", FileType: RegularFile}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *File) URI() string {
|
func (f *File) URI() string {
|
||||||
@ -75,7 +78,6 @@ func (f *File) SetURI(uri string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *File) Apply() error {
|
func (f *File) Apply() error {
|
||||||
|
|
||||||
switch f.State {
|
switch f.State {
|
||||||
case "absent":
|
case "absent":
|
||||||
removeErr := os.Remove(f.Path)
|
removeErr := os.Remove(f.Path)
|
||||||
@ -102,6 +104,11 @@ func (f *File) Apply() error {
|
|||||||
//e := os.Stat(f.path)
|
//e := os.Stat(f.path)
|
||||||
//if os.IsNotExist(e) {
|
//if os.IsNotExist(e) {
|
||||||
switch f.FileType {
|
switch f.FileType {
|
||||||
|
case SymbolicLinkFile:
|
||||||
|
linkErr := os.Symlink(f.Target, f.Path)
|
||||||
|
if linkErr != nil {
|
||||||
|
return linkErr
|
||||||
|
}
|
||||||
case DirectoryFile:
|
case DirectoryFile:
|
||||||
os.MkdirAll(f.Path, os.FileMode(mode))
|
os.MkdirAll(f.Path, os.FileMode(mode))
|
||||||
default:
|
default:
|
||||||
@ -150,18 +157,19 @@ func (f *File) ResolveId(ctx context.Context) string {
|
|||||||
return filePath
|
return filePath
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *File) Read(ctx context.Context) ([]byte, error) {
|
func (f *File) NormalizePath() error {
|
||||||
filePath, fileAbsErr := filepath.Abs(f.Path)
|
filePath, fileAbsErr := filepath.Abs(f.Path)
|
||||||
if fileAbsErr != nil {
|
if fileAbsErr == nil {
|
||||||
panic(fileAbsErr)
|
f.Path = filePath
|
||||||
}
|
}
|
||||||
f.Path = filePath
|
return fileAbsErr
|
||||||
|
}
|
||||||
info, e := os.Stat(f.Path)
|
|
||||||
|
|
||||||
|
func (f *File) ReadStat() error {
|
||||||
|
info, e := os.Lstat(f.Path)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
f.State = "absent"
|
f.State = "absent"
|
||||||
return nil, e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Mtime = info.ModTime()
|
f.Mtime = info.ModTime()
|
||||||
@ -187,19 +195,38 @@ func (f *File) Read(ctx context.Context) ([]byte, error) {
|
|||||||
f.Group = fileGroup.Name
|
f.Group = fileGroup.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Mode = fmt.Sprintf("%04o", info.Mode().Perm())
|
f.Mode = fmt.Sprintf("%04o", info.Mode().Perm())
|
||||||
|
f.FileType.SetMode(info.Mode())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
file, fileErr := os.Open(f.Path)
|
func (f *File) Read(ctx context.Context) ([]byte, error) {
|
||||||
if fileErr != nil {
|
f.NormalizePath()
|
||||||
panic(fileErr)
|
|
||||||
|
statErr := f.ReadStat()
|
||||||
|
if statErr != nil {
|
||||||
|
return nil, statErr
|
||||||
}
|
}
|
||||||
|
|
||||||
fileContent, ioErr := io.ReadAll(file)
|
switch f.FileType {
|
||||||
if ioErr != nil {
|
case RegularFile:
|
||||||
panic(ioErr)
|
file, fileErr := os.Open(f.Path)
|
||||||
|
if fileErr != nil {
|
||||||
|
panic(fileErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileContent, ioErr := io.ReadAll(file)
|
||||||
|
if ioErr != nil {
|
||||||
|
panic(ioErr)
|
||||||
|
}
|
||||||
|
f.Content = string(fileContent)
|
||||||
|
case SymbolicLinkFile:
|
||||||
|
linkTarget, pathErr := os.Readlink(f.Path)
|
||||||
|
if pathErr != nil {
|
||||||
|
return nil, pathErr
|
||||||
|
}
|
||||||
|
f.Target = linkTarget
|
||||||
}
|
}
|
||||||
f.Content = string(fileContent)
|
|
||||||
f.State = "present"
|
f.State = "present"
|
||||||
return yaml.Marshal(f)
|
return yaml.Marshal(f)
|
||||||
}
|
}
|
||||||
@ -220,3 +247,22 @@ func (f *FileType) UnmarshalYAML(value *yaml.Node) error {
|
|||||||
return errors.New("invalid FileType value")
|
return errors.New("invalid FileType value")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FileType) SetMode(mode os.FileMode) {
|
||||||
|
switch true {
|
||||||
|
case mode.IsRegular():
|
||||||
|
*f = RegularFile
|
||||||
|
case mode.IsDir():
|
||||||
|
*f = DirectoryFile
|
||||||
|
case mode&os.ModeSymlink != 0:
|
||||||
|
*f = SymbolicLinkFile
|
||||||
|
case mode&os.ModeNamedPipe != 0:
|
||||||
|
*f = NamedPipeFile
|
||||||
|
case mode&os.ModeSocket != 0:
|
||||||
|
*f = SocketFile
|
||||||
|
case mode&os.ModeCharDevice != 0:
|
||||||
|
*f = CharacterDeviceFile
|
||||||
|
case mode&os.ModeDevice != 0:
|
||||||
|
*f = BlockDeviceFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -188,3 +188,58 @@ func TestFileSetURI(t *testing.T) {
|
|||||||
assert.Equal(t, "file", f.Type())
|
assert.Equal(t, "file", f.Type())
|
||||||
assert.Equal(t, file, f.Path)
|
assert.Equal(t, file, f.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFileNormalizePath(t *testing.T) {
|
||||||
|
absFile, absFilePathErr := filepath.Abs(filepath.Join(TempDir, "./testuri.txt"))
|
||||||
|
assert.Nil(t, absFilePathErr)
|
||||||
|
|
||||||
|
file := filepath.Join(TempDir, "./testuri.txt")
|
||||||
|
|
||||||
|
f := NewFile()
|
||||||
|
assert.NotNil(t, f)
|
||||||
|
|
||||||
|
f.Path = file
|
||||||
|
e := f.NormalizePath()
|
||||||
|
assert.Nil(t, e)
|
||||||
|
|
||||||
|
assert.Equal(t, absFile, f.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileReadStat(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
link := filepath.Join(TempDir, "link.txt")
|
||||||
|
linkTargetFile := filepath.Join(TempDir, "testuri.txt")
|
||||||
|
|
||||||
|
f := NewFile()
|
||||||
|
assert.NotNil(t, f)
|
||||||
|
|
||||||
|
f.Path = linkTargetFile
|
||||||
|
e := f.NormalizePath()
|
||||||
|
assert.Nil(t, e)
|
||||||
|
|
||||||
|
statErr := f.ReadStat()
|
||||||
|
assert.Error(t, statErr)
|
||||||
|
|
||||||
|
f.Owner = "nobody"
|
||||||
|
f.Group = "nobody"
|
||||||
|
f.State = "present"
|
||||||
|
assert.Nil(t, f.Apply())
|
||||||
|
|
||||||
|
assert.Nil(t, f.ReadStat())
|
||||||
|
|
||||||
|
l := NewFile()
|
||||||
|
assert.NotNil(t, l)
|
||||||
|
|
||||||
|
l.FileType = SymbolicLinkFile
|
||||||
|
l.Path = link
|
||||||
|
l.Target = linkTargetFile
|
||||||
|
l.State = "present"
|
||||||
|
|
||||||
|
l.Apply()
|
||||||
|
l.ReadStat()
|
||||||
|
|
||||||
|
testRead := NewFile()
|
||||||
|
testRead.Path = link
|
||||||
|
testRead.Read(ctx)
|
||||||
|
assert.Equal(t, linkTargetFile, testRead.Target)
|
||||||
|
}
|
||||||
|
75
internal/resource/http.go
Normal file
75
internal/resource/http.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
_ "errors"
|
||||||
|
"fmt"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
_ "io"
|
||||||
|
"net/url"
|
||||||
|
_ "os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ResourceTypes.Register("http", HTTPFactory)
|
||||||
|
ResourceTypes.Register("https", HTTPFactory)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HTTPFactory(u *url.URL) Resource {
|
||||||
|
h := NewHTTP()
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage the state of an HTTP endpoint
|
||||||
|
type HTTP struct {
|
||||||
|
loader YamlLoader
|
||||||
|
Endpoint string `yaml:"endpoint"`
|
||||||
|
|
||||||
|
Body string `yaml:"body,omitempty"`
|
||||||
|
State string `yaml:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTP() *HTTP {
|
||||||
|
return &HTTP{loader: YamlLoadDecl}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) URI() string {
|
||||||
|
return h.Endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) SetURI(uri string) error {
|
||||||
|
if _, e := url.Parse(uri); e != nil {
|
||||||
|
return fmt.Errorf("%w: %s is not a file", ErrInvalidResourceURI, uri)
|
||||||
|
}
|
||||||
|
h.Endpoint = uri
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) Apply() error {
|
||||||
|
|
||||||
|
switch h.State {
|
||||||
|
case "absent":
|
||||||
|
case "present":
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) LoadDecl(yamlFileResourceDeclaration string) error {
|
||||||
|
return h.loader(yamlFileResourceDeclaration, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) ResolveId(ctx context.Context) string {
|
||||||
|
return h.Endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) Read(ctx context.Context) ([]byte, error) {
|
||||||
|
return yaml.Marshal(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) Type() string {
|
||||||
|
u, _ := url.Parse(h.Endpoint)
|
||||||
|
return u.Scheme
|
||||||
|
}
|
25
internal/resource/http_test.go
Normal file
25
internal/resource/http_test.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "context"
|
||||||
|
_ "encoding/json"
|
||||||
|
_ "fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
_ "gopkg.in/yaml.v3"
|
||||||
|
_ "io"
|
||||||
|
_ "log"
|
||||||
|
_ "net/http"
|
||||||
|
_ "net/http/httptest"
|
||||||
|
_ "net/url"
|
||||||
|
_ "os"
|
||||||
|
_ "path/filepath"
|
||||||
|
_ "strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewHTTPResource(t *testing.T) {
|
||||||
|
f := NewHTTP()
|
||||||
|
assert.NotEqual(t, nil, f)
|
||||||
|
}
|
@ -5,6 +5,7 @@ package resource
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
_ "encoding/json"
|
||||||
_ "fmt"
|
_ "fmt"
|
||||||
_ "gopkg.in/yaml.v3"
|
_ "gopkg.in/yaml.v3"
|
||||||
_ "net/url"
|
_ "net/url"
|
||||||
|
41
internal/resource/schema.go
Normal file
41
internal/resource/schema.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/xeipuuv/gojsonschema"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Schema struct {
|
||||||
|
schema gojsonschema.JSONLoader
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSchema(name string) *Schema {
|
||||||
|
path := fmt.Sprintf("file://schemas/%s.jsonschema", name)
|
||||||
|
|
||||||
|
return &Schema{schema: gojsonschema.NewReferenceLoader(path)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schema) Validate(source string) error {
|
||||||
|
// loader := gojsonschema.NewGoLoader(source)
|
||||||
|
loader := gojsonschema.NewStringLoader(source)
|
||||||
|
result, err := gojsonschema.Validate(s.schema, loader)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("%#v %#v %#v %#v\n", source, loader, result, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.Valid() {
|
||||||
|
schemaErrors := strings.Builder{}
|
||||||
|
for _, err := range result.Errors() {
|
||||||
|
schemaErrors.WriteString(err.String() + "\n")
|
||||||
|
}
|
||||||
|
return errors.New(schemaErrors.String())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
80
internal/resource/schema_test.go
Normal file
80
internal/resource/schema_test.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
_ "gopkg.in/yaml.v3"
|
||||||
|
_ "io"
|
||||||
|
_ "log"
|
||||||
|
_ "net/http"
|
||||||
|
_ "net/http/httptest"
|
||||||
|
_ "net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
_ "strings"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewSchema(t *testing.T) {
|
||||||
|
s := NewSchema("document")
|
||||||
|
assert.NotEqual(t, nil, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSchemaValidate(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
s := NewSchema("file")
|
||||||
|
assert.NotEqual(t, nil, s)
|
||||||
|
|
||||||
|
file, _ := filepath.Abs(filepath.Join(TempDir, "fooread.txt"))
|
||||||
|
|
||||||
|
declarationAttributes := `
|
||||||
|
path: "%s"
|
||||||
|
owner: "nobody"
|
||||||
|
group: "nobody"
|
||||||
|
mode: "0600"
|
||||||
|
atime: 2001-12-15T01:01:01.000000001Z
|
||||||
|
ctime: %s
|
||||||
|
mtime: 2001-12-15T01:01:01.000000001Z
|
||||||
|
content: |-
|
||||||
|
test line 1
|
||||||
|
test line 2
|
||||||
|
filetype: "regular"
|
||||||
|
state: present
|
||||||
|
`
|
||||||
|
|
||||||
|
decl := fmt.Sprintf(declarationAttributes, file, "2001-12-15T01:01:01.000000001Z")
|
||||||
|
|
||||||
|
testFile := NewFile()
|
||||||
|
e := testFile.LoadDecl(decl)
|
||||||
|
assert.Equal(t, nil, e)
|
||||||
|
testFile.Apply()
|
||||||
|
|
||||||
|
jsonDoc, jsonErr := json.Marshal(testFile)
|
||||||
|
assert.Nil(t, jsonErr)
|
||||||
|
|
||||||
|
schemaErr := s.Validate(string(jsonDoc))
|
||||||
|
assert.Nil(t, schemaErr)
|
||||||
|
|
||||||
|
f := NewFile()
|
||||||
|
assert.NotEqual(t, nil, f)
|
||||||
|
|
||||||
|
f.Path = file
|
||||||
|
r, e := f.Read(ctx)
|
||||||
|
assert.Equal(t, nil, e)
|
||||||
|
assert.Equal(t, "nobody", f.Owner)
|
||||||
|
|
||||||
|
info, statErr := os.Stat(file)
|
||||||
|
assert.Nil(t, statErr)
|
||||||
|
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||||
|
assert.True(t, ok)
|
||||||
|
cTime := time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))
|
||||||
|
|
||||||
|
expected := fmt.Sprintf(declarationAttributes, file, cTime.Format(time.RFC3339Nano))
|
||||||
|
assert.YAMLEq(t, expected, string(r))
|
||||||
|
}
|
39
internal/resource/schemas/user.jsonschema
Normal file
39
internal/resource/schemas/user.jsonschema
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "user",
|
||||||
|
"description": "A user account",
|
||||||
|
"type": "object",
|
||||||
|
"required": [ "name" ],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"uid": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 65535
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"groups": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"gecos": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "User description"
|
||||||
|
},
|
||||||
|
"createhome": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "create user home directory"
|
||||||
|
},
|
||||||
|
"shell": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "login shell"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -13,6 +14,8 @@ var (
|
|||||||
ResourceTypes *Types = NewTypes()
|
ResourceTypes *Types = NewTypes()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type TypeName string //`json:"type"`
|
||||||
|
|
||||||
type TypeFactory func(*url.URL) Resource
|
type TypeFactory func(*url.URL) Resource
|
||||||
|
|
||||||
type Types struct {
|
type Types struct {
|
||||||
@ -45,3 +48,12 @@ func (t *Types) Has(typename string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *TypeName) UnmarshalJSON(b []byte) error {
|
||||||
|
ResourceTypeName := strings.Trim(string(b), "\"")
|
||||||
|
if ResourceTypes.Has(ResourceTypeName) {
|
||||||
|
*n = TypeName(ResourceTypeName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%w: %s", ErrUnknownResourceType, ResourceTypeName)
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@ package resource
|
|||||||
import (
|
import (
|
||||||
_ "context"
|
_ "context"
|
||||||
"decl/tests/mocks"
|
"decl/tests/mocks"
|
||||||
|
"encoding/json"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
@ -51,3 +52,14 @@ func TestResourceTypesHasType(t *testing.T) {
|
|||||||
|
|
||||||
assert.True(t, resourceTypes.Has("foo"))
|
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))
|
||||||
|
}
|
||||||
|
@ -17,16 +17,16 @@ import (
|
|||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
loader YamlLoader
|
loader YamlLoader
|
||||||
Name string `yaml:"name"`
|
Name string `json:"name" yaml:"name"`
|
||||||
UID int `yaml:"uid"`
|
UID int `json:"uid,omitempty" yaml:"uid,omitempty"`
|
||||||
Group string `yaml:"group"`
|
Group string `json:"group,omitempty" yaml:"group,omitempty"`
|
||||||
Groups []string `yaml:"groups",omitempty`
|
Groups []string `json:"groups,omitempty" yaml:"groups,omitempty"`
|
||||||
Gecos string `yaml:"gecos"`
|
Gecos string `json:"gecos,omitempty" yaml:"gecos,omitempty"`
|
||||||
Home string `yaml:"home"`
|
Home string `json:"home" yaml:"home"`
|
||||||
CreateHome bool `yaml:"createhome"omitempty`
|
CreateHome bool `json:"createhome,omitempty" yaml:"createhome,omitempty"`
|
||||||
Shell string `yaml:"shell"`
|
Shell string `json:"shell,omitempty" yaml:"shell,omitempty"`
|
||||||
|
|
||||||
State string `yaml:"state"`
|
State string `json:"state" yaml:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUser() *User {
|
func NewUser() *User {
|
||||||
|
@ -4,6 +4,8 @@ package mocks
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
_ "gopkg.in/yaml.v3"
|
_ "gopkg.in/yaml.v3"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MockResource struct {
|
type MockResource struct {
|
||||||
@ -38,3 +40,12 @@ func (m *MockResource) Read(ctx context.Context) ([]byte, error) {
|
|||||||
func (m *MockResource) Type() string {
|
func (m *MockResource) Type() string {
|
||||||
return m.InjectType()
|
return m.InjectType()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockResource) UnmarshalJSON(data []byte) error {
|
||||||
|
fmt.Printf("UnmarshalJSON %#v\n", string(data))
|
||||||
|
panic(data)
|
||||||
|
if err := json.Unmarshal(data, m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user