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/davecgh/go-spew v1.1.1 // 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-units v0.5.0 // 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/pkg/errors v0.9.1 // 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/otel 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/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/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/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/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/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/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/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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
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 {
|
||||
loader YamlLoader
|
||||
Id string `yaml:"ID",omitempty`
|
||||
Name string `yaml:"name"`
|
||||
Path string `yaml:"path"`
|
||||
Cmd []string `yaml:"cmd",omitempty`
|
||||
Entrypoint strslice.StrSlice `yaml:"entrypoint",omitempty`
|
||||
Args []string `yaml:"args",omitempty`
|
||||
Environment map[string]string `yaml:"environment"`
|
||||
Image string `yaml:"image"`
|
||||
ResolvConfPath string `yaml:"resolvconfpath"`
|
||||
HostnamePath string `yaml:"hostnamepath"`
|
||||
HostsPath string `yaml:"hostspath"`
|
||||
LogPath string `yaml:"logpath"`
|
||||
Created string `yaml:"created"`
|
||||
ContainerState types.ContainerState `yaml:"containerstate"`
|
||||
RestartCount int `yaml:"restartcount"`
|
||||
Driver string `yaml:"driver"`
|
||||
Platform string `yaml:"platform"`
|
||||
MountLabel string `yaml:"mountlabel"`
|
||||
ProcessLabel string `yaml:"processlabel"`
|
||||
AppArmorProfile string `yaml:"apparmorprofile"`
|
||||
ExecIDs []string `yaml:"execids"`
|
||||
HostConfig container.HostConfig `yaml:"hostconfig"`
|
||||
GraphDriver types.GraphDriverData `yaml:"graphdriver"`
|
||||
SizeRw *int64 `json:",omitempty"`
|
||||
SizeRootFs *int64 `json:",omitempty"`
|
||||
Id string `json:"ID,omitempty" yaml:"ID,omitempty"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Path string `json:"path" yaml:"path"`
|
||||
Cmd []string `json:"cmd,omitempty" yaml:"cmd,omitempty"`
|
||||
Entrypoint strslice.StrSlice `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty"`
|
||||
Args []string `json:"args,omitempty" yaml:"args,omitempty"`
|
||||
Environment map[string]string `json:"environment" yaml:"environment"`
|
||||
Image string `json:"image" yaml:"image"`
|
||||
ResolvConfPath string `json:"resolvconfpath" yaml:"resolvconfpath"`
|
||||
HostnamePath string `json:"hostnamepath" yaml:"hostnamepath"`
|
||||
HostsPath string `json:"hostpath" yaml:"hostspath"`
|
||||
LogPath string `json:"logpath" yaml:"logpath"`
|
||||
Created string `json:"created" yaml:"created"`
|
||||
ContainerState types.ContainerState `json:"containerstate" yaml:"containerstate"`
|
||||
RestartCount int `json:"restartcount" yaml:"restartcount"`
|
||||
Driver string `json:"driver" yaml:"driver"`
|
||||
Platform string `json:"platform" yaml:"platform"`
|
||||
MountLabel string `json:"mountlabel" yaml:"mountlabel"`
|
||||
ProcessLabel string `json:"processlabel" yaml:"processlabel"`
|
||||
AppArmorProfile string `json:"apparmorprofile" yaml:"apparmorprofile"`
|
||||
ExecIDs []string `json:"execids" yaml:"execids"`
|
||||
HostConfig container.HostConfig `json:"hostconfig" yaml:"hostconfig"`
|
||||
GraphDriver types.GraphDriverData `json:"graphdriver" yaml:"graphdriver"`
|
||||
SizeRw *int64 `json:",omitempty" yaml:",omitempty"`
|
||||
SizeRootFs *int64 `json:",omitempty" yaml:",omitempty"`
|
||||
/*
|
||||
Mounts []MountPoint
|
||||
Config *container.Config
|
||||
|
@ -4,15 +4,19 @@ package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type DeclarationType struct {
|
||||
Type TypeName `json:"type" yaml:"type"`
|
||||
}
|
||||
|
||||
type Declaration struct {
|
||||
Type string `yaml:"type"`
|
||||
Attributes yaml.Node `yaml:"attributes"`
|
||||
Implementation Resource `-`
|
||||
Type TypeName `json:"type" yaml:"type"`
|
||||
Attributes Resource `json:"attributes" yaml:"attributes"`
|
||||
}
|
||||
|
||||
type ResourceLoader interface {
|
||||
@ -43,41 +47,83 @@ func (d *Declaration) LoadDecl(yamlResourceDeclaration string) error {
|
||||
func (d *Declaration) NewResource() error {
|
||||
uri := fmt.Sprintf("%s://", d.Type)
|
||||
newResource, err := ResourceTypes.New(uri)
|
||||
d.Implementation = newResource
|
||||
d.Attributes = newResource
|
||||
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 {
|
||||
return d.Implementation
|
||||
return d.Attributes
|
||||
}
|
||||
|
||||
func (d *Declaration) SetURI(uri string) error {
|
||||
slog.Info("SetURI()", "uri", uri)
|
||||
d.Implementation = NewResource(uri)
|
||||
if d.Implementation == nil {
|
||||
d.Attributes = NewResource(uri)
|
||||
if d.Attributes == nil {
|
||||
panic("unknown resource")
|
||||
}
|
||||
d.Type = d.Implementation.Type()
|
||||
d.Implementation.Read(context.Background()) // fix context
|
||||
d.Type = TypeName(d.Attributes.Type())
|
||||
d.Attributes.Read(context.Background()) // fix context
|
||||
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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
_ "log"
|
||||
@ -55,7 +56,7 @@ func TestNewResourceDeclarationType(t *testing.T) {
|
||||
assert.NotEqual(t, nil, resourceDeclaration)
|
||||
|
||||
resourceDeclaration.LoadDecl(decl)
|
||||
assert.Equal(t, "file", resourceDeclaration.Type)
|
||||
assert.Equal(t, TypeName("file"), resourceDeclaration.Type)
|
||||
assert.NotEqual(t, nil, resourceDeclaration.Attributes)
|
||||
}
|
||||
|
||||
@ -70,6 +71,38 @@ func TestDeclarationNewResource(t *testing.T) {
|
||||
errNewFileResource := resourceDeclaration.NewResource()
|
||||
assert.Nil(t, errNewFileResource)
|
||||
|
||||
//assert.NotNil(t, resourceDeclaration.Implementation)
|
||||
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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
_ "fmt"
|
||||
"github.com/xeipuuv/gojsonschema"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io"
|
||||
_ "log"
|
||||
_ "net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Document struct {
|
||||
ResourceDecls []Declaration `yaml:"resources"`
|
||||
ResourceDecls []Declaration `json:"resources" yaml:"resources"`
|
||||
}
|
||||
|
||||
func NewDocument() *Document {
|
||||
@ -20,10 +24,28 @@ func NewDocument() *Document {
|
||||
|
||||
func (d *Document) Load(r io.Reader) error {
|
||||
yamlDecoder := yaml.NewDecoder(r)
|
||||
yamlDecoder.Decode(d)
|
||||
for i := range d.ResourceDecls {
|
||||
if _, e := d.ResourceDecls[i].LoadResourceFromYaml(); e != nil {
|
||||
return e
|
||||
if e := yamlDecoder.Decode(d); 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
|
||||
@ -43,16 +65,15 @@ func (d *Document) Apply() error {
|
||||
}
|
||||
|
||||
func (d *Document) Generate(w io.Writer) error {
|
||||
yamlEncoder := yaml.NewEncoder(w)
|
||||
yamlEncoder.Encode(d)
|
||||
return yamlEncoder.Close()
|
||||
e := NewYAMLEncoder(w)
|
||||
e.Encode(d)
|
||||
return e.Close()
|
||||
}
|
||||
|
||||
func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration Resource) {
|
||||
decl := NewDeclaration()
|
||||
decl.Type = resourceType
|
||||
decl.Implementation = resourceDeclaration
|
||||
decl.UpdateYamlFromResource()
|
||||
decl.Type = TypeName(resourceType)
|
||||
decl.Attributes = resourceDeclaration
|
||||
d.ResourceDecls = append(d.ResourceDecls, *decl)
|
||||
}
|
||||
|
||||
@ -61,8 +82,15 @@ func (d *Document) AddResource(uri string) error {
|
||||
//if e == nil {
|
||||
decl := NewDeclaration()
|
||||
decl.SetURI(uri)
|
||||
decl.UpdateYamlFromResource()
|
||||
d.ResourceDecls = append(d.ResourceDecls, *decl)
|
||||
//}
|
||||
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
|
||||
attributes:
|
||||
name: "testuser"
|
||||
uid: "10022"
|
||||
gid: "10022"
|
||||
uid: 10022
|
||||
group: "10022"
|
||||
home: "/home/testuser"
|
||||
state: present
|
||||
`, file)
|
||||
|
||||
d := NewDocument()
|
||||
assert.NotEqual(t, nil, d)
|
||||
|
||||
docReader := strings.NewReader(document)
|
||||
|
||||
e := d.Load(docReader)
|
||||
assert.Equal(t, nil, e)
|
||||
assert.Nil(t, e)
|
||||
|
||||
resources := d.Resources()
|
||||
assert.Equal(t, 2, len(resources))
|
||||
@ -110,7 +109,6 @@ resources:
|
||||
f.(*File).Path = filepath.Join(TempDir, "foo.txt")
|
||||
f.(*File).Read(ctx)
|
||||
d.AddResourceDeclaration("file", f)
|
||||
|
||||
ey := d.Generate(&documentYaml)
|
||||
assert.Equal(t, nil, ey)
|
||||
|
||||
@ -127,3 +125,59 @@ func TestDocumentAddResource(t *testing.T) {
|
||||
assert.NotNil(t, d)
|
||||
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
|
||||
type File struct {
|
||||
loader YamlLoader
|
||||
Path string `yaml:"path"`
|
||||
Owner string `yaml:"owner"`
|
||||
Group string `yaml:"group"`
|
||||
Mode string `yaml:"mode"`
|
||||
Path string `json:"path" yaml:"path"`
|
||||
Owner string `json:"owner" yaml:"owner"`
|
||||
Group string `json:"group" yaml:"group"`
|
||||
Mode string `json:"mode" yaml:"mode"`
|
||||
|
||||
Atime time.Time `yaml:"atime",omitempty`
|
||||
Ctime time.Time `yaml:"ctime",omitempty`
|
||||
Mtime time.Time `yaml:"mtime",omitempty`
|
||||
Atime time.Time `json:"atime,omitempty" yaml:"atime,omitempty"`
|
||||
Ctime time.Time `json:"ctime,omitempty" yaml:"ctime,omitempty"`
|
||||
Mtime time.Time `json:"mtime,omitempty" yaml:"mtime,omitempty"`
|
||||
|
||||
Content string `yaml:"content",omitempty`
|
||||
FileType FileType `yaml:"filetype"`
|
||||
State string `yaml:"state"`
|
||||
Content string `json:"content,omitempty" yaml:"content,omitempty"`
|
||||
Target string `json:"target,omitempty" yaml:"target,omitempty"`
|
||||
FileType FileType `json:"filetype" yaml:"filetype"`
|
||||
State string `json:"state" yaml:"state"`
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -75,7 +78,6 @@ func (f *File) SetURI(uri string) error {
|
||||
}
|
||||
|
||||
func (f *File) Apply() error {
|
||||
|
||||
switch f.State {
|
||||
case "absent":
|
||||
removeErr := os.Remove(f.Path)
|
||||
@ -102,6 +104,11 @@ func (f *File) Apply() error {
|
||||
//e := os.Stat(f.path)
|
||||
//if os.IsNotExist(e) {
|
||||
switch f.FileType {
|
||||
case SymbolicLinkFile:
|
||||
linkErr := os.Symlink(f.Target, f.Path)
|
||||
if linkErr != nil {
|
||||
return linkErr
|
||||
}
|
||||
case DirectoryFile:
|
||||
os.MkdirAll(f.Path, os.FileMode(mode))
|
||||
default:
|
||||
@ -150,18 +157,19 @@ func (f *File) ResolveId(ctx context.Context) string {
|
||||
return filePath
|
||||
}
|
||||
|
||||
func (f *File) Read(ctx context.Context) ([]byte, error) {
|
||||
func (f *File) NormalizePath() error {
|
||||
filePath, fileAbsErr := filepath.Abs(f.Path)
|
||||
if fileAbsErr != nil {
|
||||
panic(fileAbsErr)
|
||||
if fileAbsErr == nil {
|
||||
f.Path = filePath
|
||||
}
|
||||
f.Path = filePath
|
||||
|
||||
info, e := os.Stat(f.Path)
|
||||
return fileAbsErr
|
||||
}
|
||||
|
||||
func (f *File) ReadStat() error {
|
||||
info, e := os.Lstat(f.Path)
|
||||
if e != nil {
|
||||
f.State = "absent"
|
||||
return nil, e
|
||||
return e
|
||||
}
|
||||
|
||||
f.Mtime = info.ModTime()
|
||||
@ -187,19 +195,38 @@ func (f *File) Read(ctx context.Context) ([]byte, error) {
|
||||
f.Group = fileGroup.Name
|
||||
}
|
||||
}
|
||||
|
||||
f.Mode = fmt.Sprintf("%04o", info.Mode().Perm())
|
||||
f.FileType.SetMode(info.Mode())
|
||||
return nil
|
||||
}
|
||||
|
||||
file, fileErr := os.Open(f.Path)
|
||||
if fileErr != nil {
|
||||
panic(fileErr)
|
||||
func (f *File) Read(ctx context.Context) ([]byte, error) {
|
||||
f.NormalizePath()
|
||||
|
||||
statErr := f.ReadStat()
|
||||
if statErr != nil {
|
||||
return nil, statErr
|
||||
}
|
||||
|
||||
fileContent, ioErr := io.ReadAll(file)
|
||||
if ioErr != nil {
|
||||
panic(ioErr)
|
||||
switch f.FileType {
|
||||
case RegularFile:
|
||||
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"
|
||||
return yaml.Marshal(f)
|
||||
}
|
||||
@ -220,3 +247,22 @@ func (f *FileType) UnmarshalYAML(value *yaml.Node) error {
|
||||
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.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 (
|
||||
"context"
|
||||
_ "encoding/json"
|
||||
_ "fmt"
|
||||
_ "gopkg.in/yaml.v3"
|
||||
_ "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"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -13,6 +14,8 @@ var (
|
||||
ResourceTypes *Types = NewTypes()
|
||||
)
|
||||
|
||||
type TypeName string //`json:"type"`
|
||||
|
||||
type TypeFactory func(*url.URL) Resource
|
||||
|
||||
type Types struct {
|
||||
@ -45,3 +48,12 @@ func (t *Types) Has(typename string) bool {
|
||||
}
|
||||
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 (
|
||||
_ "context"
|
||||
"decl/tests/mocks"
|
||||
"encoding/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/url"
|
||||
"testing"
|
||||
@ -51,3 +52,14 @@ func TestResourceTypesHasType(t *testing.T) {
|
||||
|
||||
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 {
|
||||
loader YamlLoader
|
||||
Name string `yaml:"name"`
|
||||
UID int `yaml:"uid"`
|
||||
Group string `yaml:"group"`
|
||||
Groups []string `yaml:"groups",omitempty`
|
||||
Gecos string `yaml:"gecos"`
|
||||
Home string `yaml:"home"`
|
||||
CreateHome bool `yaml:"createhome"omitempty`
|
||||
Shell string `yaml:"shell"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
UID int `json:"uid,omitempty" yaml:"uid,omitempty"`
|
||||
Group string `json:"group,omitempty" yaml:"group,omitempty"`
|
||||
Groups []string `json:"groups,omitempty" yaml:"groups,omitempty"`
|
||||
Gecos string `json:"gecos,omitempty" yaml:"gecos,omitempty"`
|
||||
Home string `json:"home" yaml:"home"`
|
||||
CreateHome bool `json:"createhome,omitempty" yaml:"createhome,omitempty"`
|
||||
Shell string `json:"shell,omitempty" yaml:"shell,omitempty"`
|
||||
|
||||
State string `yaml:"state"`
|
||||
State string `json:"state" yaml:"state"`
|
||||
}
|
||||
|
||||
func NewUser() *User {
|
||||
|
@ -4,6 +4,8 @@ package mocks
|
||||
import (
|
||||
"context"
|
||||
_ "gopkg.in/yaml.v3"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type MockResource struct {
|
||||
@ -38,3 +40,12 @@ func (m *MockResource) Read(ctx context.Context) ([]byte, error) {
|
||||
func (m *MockResource) Type() string {
|
||||
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