add encoder/decoder support for json and yaml
Some checks failed
Lint / golangci-lint (push) Failing after 10m8s
Declarative Tests / test (push) Successful in 54s

add support for jsonschema verification
This commit is contained in:
Matthew Rich 2024-04-03 09:54:50 -07:00
parent 7181075568
commit 5a49359722
23 changed files with 830 additions and 110 deletions

5
go.mod
View File

@ -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
View File

@ -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=

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View 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
}

View 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)
}

View File

@ -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)
}

View File

@ -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))
}

View 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
}

View 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)
}

View File

@ -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
}
}

View File

@ -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
View 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
}

View 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)
}

View File

@ -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"

View 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
}

View 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))
}

View 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"
}
}
}

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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 {

View File

@ -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
}