update resource schemas
This commit is contained in:
parent
dc843d98a7
commit
33dd463180
@ -55,6 +55,9 @@ func main() {
|
|||||||
if e := d.Load(resourceFile); e != nil {
|
if e := d.Load(resourceFile); e != nil {
|
||||||
log.Fatal(e)
|
log.Fatal(e)
|
||||||
}
|
}
|
||||||
|
if validationErr := d.Validate(); validationErr != nil {
|
||||||
|
log.Fatal(validationErr)
|
||||||
|
}
|
||||||
if applyErr := d.Apply(); applyErr != nil {
|
if applyErr := d.Apply(); applyErr != nil {
|
||||||
log.Fatal(applyErr)
|
log.Fatal(applyErr)
|
||||||
}
|
}
|
||||||
|
@ -3,28 +3,50 @@
|
|||||||
package resource
|
package resource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "context"
|
_ "context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
_ "log"
|
"io"
|
||||||
_ "net/url"
|
"log/slog"
|
||||||
_ "os"
|
_ "net/url"
|
||||||
|
_ "os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
"io"
|
|
||||||
"encoding/json"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type CommandExecutor func(value any) ([]byte, error)
|
||||||
|
type CommandExtractAttributes func(output []byte, target any) error
|
||||||
|
|
||||||
type CommandArg string
|
type CommandArg string
|
||||||
|
|
||||||
type Command struct {
|
type Command struct {
|
||||||
Path string `json:"path" yaml:"path"`
|
Path string `json:"path" yaml:"path"`
|
||||||
Args []CommandArg `json:"args" yaml:"args"`
|
Args []CommandArg `json:"args" yaml:"args"`
|
||||||
|
Executor CommandExecutor `json:"-" yaml:"-"`
|
||||||
|
Extractor CommandExtractAttributes `json:"-" yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommand() *Command {
|
func NewCommand() *Command {
|
||||||
return &Command{}
|
c := &Command{}
|
||||||
|
c.Executor = func(value any) ([]byte, error) {
|
||||||
|
args, err := c.Template(value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cmd := exec.Command(c.Path, args...)
|
||||||
|
stderr, pipeErr := cmd.StderrPipe()
|
||||||
|
if pipeErr != nil {
|
||||||
|
return nil, pipeErr
|
||||||
|
}
|
||||||
|
output, err := cmd.Output()
|
||||||
|
|
||||||
|
stdErrOutput, _ := io.ReadAll(stderr)
|
||||||
|
slog.Info("execute()", "path", c.Path, "args", args, "output", output, "error", stdErrOutput)
|
||||||
|
return output, err
|
||||||
|
}
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Command) Load(r io.Reader) error {
|
func (c *Command) Load(r io.Reader) error {
|
||||||
@ -51,11 +73,7 @@ func (c *Command) Template(value any) ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Command) Execute(value any) ([]byte, error) {
|
func (c *Command) Execute(value any) ([]byte, error) {
|
||||||
args, err := c.Template(value)
|
return c.Executor(value)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return exec.Command(c.Path, args...).Output()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandArg) UnmarshalValue(value string) error {
|
func (c *CommandArg) UnmarshalValue(value string) error {
|
||||||
|
@ -21,7 +21,8 @@ import (
|
|||||||
_ "os"
|
_ "os"
|
||||||
_ "os/exec"
|
_ "os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
_ "strings"
|
_ "strings"
|
||||||
|
"encoding/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContainerClient interface {
|
type ContainerClient interface {
|
||||||
@ -110,6 +111,14 @@ func (c *Container) SetURI(uri string) error {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Container) JSON() ([]byte, error) {
|
||||||
|
return json.Marshal(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) Validate() error {
|
||||||
|
return fmt.Errorf("failed")
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Container) Apply() error {
|
func (c *Container) Apply() error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
switch c.State {
|
switch c.State {
|
||||||
|
@ -4,14 +4,11 @@ package resource
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
_ "fmt"
|
||||||
_ "fmt"
|
|
||||||
"github.com/xeipuuv/gojsonschema"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"io"
|
"io"
|
||||||
_ "log"
|
"log/slog"
|
||||||
_ "net/url"
|
_ "net/url"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Document struct {
|
type Document struct {
|
||||||
@ -29,21 +26,20 @@ func (d *Document) Load(r io.Reader) error {
|
|||||||
|
|
||||||
func (d *Document) Validate() error {
|
func (d *Document) Validate() error {
|
||||||
jsonDocument, jsonErr := d.JSON()
|
jsonDocument, jsonErr := d.JSON()
|
||||||
|
slog.Info("document.Validate() json", "json", jsonDocument, "err", jsonErr)
|
||||||
if jsonErr == nil {
|
if jsonErr == nil {
|
||||||
schemaLoader := gojsonschema.NewReferenceLoader("file://schemas/document.jsonschema")
|
s := NewSchema("document")
|
||||||
documentLoader := gojsonschema.NewBytesLoader(jsonDocument)
|
err := s.Validate(string(jsonDocument))
|
||||||
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
if !result.Valid() {
|
for i := range d.ResourceDecls {
|
||||||
schemaErrors := strings.Builder{}
|
if e := d.ResourceDecls[i].Resource().Validate(); e != nil {
|
||||||
for _, err := range result.Errors() {
|
return fmt.Errorf("failed to validate resource %s; %w", d.ResourceDecls[i].Resource().URI(), e)
|
||||||
schemaErrors.WriteString(err.String() + "\n")
|
|
||||||
}
|
}
|
||||||
return errors.New(schemaErrors.String())
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,10 @@ func (x *Exec) ResolveId(ctx context.Context) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *Exec) Validate() error {
|
||||||
|
return fmt.Errorf("failed")
|
||||||
|
}
|
||||||
|
|
||||||
func (x *Exec) Apply() error {
|
func (x *Exec) Apply() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -79,6 +79,10 @@ func (f *File) SetURI(uri string) error {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *File) Validate() error {
|
||||||
|
return fmt.Errorf("failed")
|
||||||
|
}
|
||||||
|
|
||||||
func (f *File) Apply() error {
|
func (f *File) Apply() error {
|
||||||
switch f.State {
|
switch f.State {
|
||||||
case "absent":
|
case "absent":
|
||||||
|
@ -25,10 +25,10 @@ func HTTPFactory(u *url.URL) Resource {
|
|||||||
|
|
||||||
// Manage the state of an HTTP endpoint
|
// Manage the state of an HTTP endpoint
|
||||||
type HTTP struct {
|
type HTTP struct {
|
||||||
Endpoint string `yaml:"endpoint"`
|
Endpoint string `yaml:"endpoint" json:"endpoint"`
|
||||||
|
|
||||||
Body string `yaml:"body,omitempty"`
|
Body string `yaml:"body,omitempty" json:"body,omitempty"`
|
||||||
State string `yaml:"state"`
|
State string `yaml:"state" json:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTP() *HTTP {
|
func NewHTTP() *HTTP {
|
||||||
@ -47,6 +47,10 @@ func (h *HTTP) SetURI(uri string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HTTP) Validate() error {
|
||||||
|
return fmt.Errorf("failed")
|
||||||
|
}
|
||||||
|
|
||||||
func (h *HTTP) Apply() error {
|
func (h *HTTP) Apply() error {
|
||||||
|
|
||||||
switch h.State {
|
switch h.State {
|
||||||
|
@ -131,6 +131,10 @@ func (n *NetworkRoute) SetURI(uri string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NetworkRoute) Validate() error {
|
||||||
|
return fmt.Errorf("failed")
|
||||||
|
}
|
||||||
|
|
||||||
func (n *NetworkRoute) Apply() error {
|
func (n *NetworkRoute) Apply() error {
|
||||||
|
|
||||||
switch n.State {
|
switch n.State {
|
||||||
|
@ -4,23 +4,43 @@ package resource
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
_ "log"
|
|
||||||
"net/url"
|
|
||||||
_ "os"
|
|
||||||
_ "os/exec"
|
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/url"
|
||||||
|
_ "os"
|
||||||
|
_ "os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
_ "strings"
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PackageType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PackageTypeApk PackageType = "apk"
|
||||||
|
PackageTypeApt PackageType = "apt"
|
||||||
|
PackageTypeDeb PackageType = "deb"
|
||||||
|
PackageTypeDnf PackageType = "dnf"
|
||||||
|
PackageTypeRpm PackageType = "rpm"
|
||||||
|
PackageTypePip PackageType = "pip"
|
||||||
|
PackageTypeYum PackageType = "yum"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Package struct {
|
type Package struct {
|
||||||
Name string `yaml:"name" json:"name"`
|
Name string `yaml:"name" json:"name"`
|
||||||
Version string `yaml:"version" json:"version"`
|
Required string `json:"required" yaml:"required"`
|
||||||
|
Version string `yaml:"version" json:"version"`
|
||||||
|
PackageType PackageType `yaml:"type" json:"type"`
|
||||||
|
|
||||||
|
CreateCommand *Command `yaml:"-" json:"-"`
|
||||||
|
ReadCommand *Command `yaml:"-" json:"-"`
|
||||||
|
UpdateCommand *Command `yaml:"-" json:"-"`
|
||||||
|
DeleteCommand *Command `yaml:"-" json:"-"`
|
||||||
// state attributes
|
// state attributes
|
||||||
State string `yaml:"state"`
|
State string `yaml:"state" json:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -28,6 +48,34 @@ func init() {
|
|||||||
p := NewPackage()
|
p := NewPackage()
|
||||||
return p
|
return p
|
||||||
})
|
})
|
||||||
|
ResourceTypes.Register(string(PackageTypeApk), func(u *url.URL) Resource {
|
||||||
|
p := NewPackage()
|
||||||
|
return p
|
||||||
|
})
|
||||||
|
ResourceTypes.Register(string(PackageTypeApt), func(u *url.URL) Resource {
|
||||||
|
p := NewPackage()
|
||||||
|
return p
|
||||||
|
})
|
||||||
|
ResourceTypes.Register(string(PackageTypeDeb), func(u *url.URL) Resource {
|
||||||
|
p := NewPackage()
|
||||||
|
return p
|
||||||
|
})
|
||||||
|
ResourceTypes.Register(string(PackageTypeDnf), func(u *url.URL) Resource {
|
||||||
|
p := NewPackage()
|
||||||
|
return p
|
||||||
|
})
|
||||||
|
ResourceTypes.Register(string(PackageTypeRpm), func(u *url.URL) Resource {
|
||||||
|
p := NewPackage()
|
||||||
|
return p
|
||||||
|
})
|
||||||
|
ResourceTypes.Register(string(PackageTypePip), func(u *url.URL) Resource {
|
||||||
|
p := NewPackage()
|
||||||
|
return p
|
||||||
|
})
|
||||||
|
ResourceTypes.Register(string(PackageTypeYum), func(u *url.URL) Resource {
|
||||||
|
p := NewPackage()
|
||||||
|
return p
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPackage() *Package {
|
func NewPackage() *Package {
|
||||||
@ -35,18 +83,22 @@ func NewPackage() *Package {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Package) URI() string {
|
func (p *Package) URI() string {
|
||||||
return fmt.Sprintf("package://%s?version=%s", p.Name, p.Version)
|
return fmt.Sprintf("package://%s?version=%s&type=%s", p.Name, p.Version, p.PackageType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Package) SetURI(uri string) error {
|
func (p *Package) SetURI(uri string) error {
|
||||||
resourceUri, e := url.Parse(uri)
|
resourceUri, e := url.Parse(uri)
|
||||||
if e == nil {
|
if e == nil {
|
||||||
if resourceUri.Scheme == "package" {
|
if resourceUri.Scheme == "package" {
|
||||||
p.Name = filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI())
|
p.Name = filepath.Join(resourceUri.Hostname(), resourceUri.Path)
|
||||||
p.Version = resourceUri.Query().Get("version")
|
p.Version = resourceUri.Query().Get("version")
|
||||||
if p.Version == "" {
|
if p.Version == "" {
|
||||||
p.Version = "latest"
|
p.Version = "latest"
|
||||||
}
|
}
|
||||||
|
p.PackageType = PackageType(resourceUri.Query().Get("type"))
|
||||||
|
if p.PackageType == "" {
|
||||||
|
e = fmt.Errorf("%w: %s is not a package known resource ", ErrInvalidResourceURI, uri)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
e = fmt.Errorf("%w: %s is not a package resource ", ErrInvalidResourceURI, uri)
|
e = fmt.Errorf("%w: %s is not a package resource ", ErrInvalidResourceURI, uri)
|
||||||
}
|
}
|
||||||
@ -54,15 +106,36 @@ func (p *Package) SetURI(uri string) error {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Package) JSON() ([]byte, error) {
|
||||||
|
return json.Marshal(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Package) Validate() error {
|
||||||
|
s := NewSchema(p.Type())
|
||||||
|
jsonDoc, jsonErr := p.JSON()
|
||||||
|
if jsonErr == nil {
|
||||||
|
return s.Validate(string(jsonDoc))
|
||||||
|
}
|
||||||
|
return jsonErr
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Package) ResolveId(ctx context.Context) string {
|
func (p *Package) ResolveId(ctx context.Context) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Package) Apply() error {
|
func (p *Package) Apply() error {
|
||||||
|
if p.Version == "latest" {
|
||||||
|
p.Version = ""
|
||||||
|
}
|
||||||
|
_, err := p.CreateCommand.Execute(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Read(context.Background())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (p *Package) Load(r io.Reader) error {
|
func (p *Package) Load(r io.Reader) error {
|
||||||
c := NewYAMLDecoder(r)
|
c := NewYAMLDecoder(r)
|
||||||
return c.Decode(p)
|
return c.Decode(p)
|
||||||
@ -76,16 +149,81 @@ func (p *Package) LoadDecl(yamlResourceDeclaration string) error {
|
|||||||
func (p *Package) Type() string { return "package" }
|
func (p *Package) Type() string { return "package" }
|
||||||
|
|
||||||
func (p *Package) Read(ctx context.Context) ([]byte, error) {
|
func (p *Package) Read(ctx context.Context) ([]byte, error) {
|
||||||
|
out, err := p.ReadCommand.Execute(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
exErr := p.ReadCommand.Extractor(out, p)
|
||||||
|
if exErr != nil {
|
||||||
|
return nil, exErr
|
||||||
|
}
|
||||||
return yaml.Marshal(p)
|
return yaml.Marshal(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Package) UnmarshalJSON(data []byte) error {
|
||||||
|
if unmarshalErr := json.Unmarshal(data, *p); unmarshalErr != nil {
|
||||||
|
return unmarshalErr
|
||||||
|
}
|
||||||
|
p.CreateCommand, p.ReadCommand, p.UpdateCommand, p.DeleteCommand = p.PackageType.NewCRUD()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Package) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
type decodePackage Package
|
||||||
|
if unmarshalErr := value.Decode((*decodePackage)(p)); unmarshalErr != nil {
|
||||||
|
return unmarshalErr
|
||||||
|
}
|
||||||
|
p.CreateCommand, p.ReadCommand, p.UpdateCommand, p.DeleteCommand = p.PackageType.NewCRUD()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PackageType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) {
|
||||||
|
switch *p {
|
||||||
|
case PackageTypeApk:
|
||||||
|
return NewApkCreateCommand(), NewApkReadCommand(), NewApkUpdateCommand(), NewApkDeleteCommand()
|
||||||
|
case PackageTypeApt:
|
||||||
|
return NewAptCreateCommand(), NewAptReadCommand(), NewAptUpdateCommand(), NewAptDeleteCommand()
|
||||||
|
case PackageTypeDeb:
|
||||||
|
case PackageTypeDnf:
|
||||||
|
case PackageTypeRpm:
|
||||||
|
case PackageTypePip:
|
||||||
|
case PackageTypeYum:
|
||||||
|
}
|
||||||
|
return nil, nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PackageType) UnmarshalValue(value string) error {
|
||||||
|
switch value {
|
||||||
|
case string(PackageTypeApk), string(PackageTypeApt), string(PackageTypeDeb), string(PackageTypeDnf), string(PackageTypeRpm), string(PackageTypePip), string(PackageTypeYum):
|
||||||
|
*p = PackageType(value)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errors.New("invalid PackageType value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PackageType) UnmarshalJSON(data []byte) error {
|
||||||
|
var s string
|
||||||
|
if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
|
||||||
|
return unmarshalRouteTypeErr
|
||||||
|
}
|
||||||
|
return p.UnmarshalValue(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PackageType) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
var s string
|
||||||
|
if err := value.Decode(&s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return p.UnmarshalValue(s)
|
||||||
|
}
|
||||||
|
|
||||||
func NewApkCreateCommand() *Command {
|
func NewApkCreateCommand() *Command {
|
||||||
c := NewCommand()
|
c := NewCommand()
|
||||||
c.Path = "apk"
|
c.Path = "apk"
|
||||||
c.Args = []CommandArg{
|
c.Args = []CommandArg{
|
||||||
CommandArg("add"),
|
CommandArg("add"),
|
||||||
CommandArg("{{ .Name }}={{ .Version }}"),
|
CommandArg("{{ .Name }}{{ .Required }}"),
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@ -98,6 +236,28 @@ func NewApkReadCommand() *Command {
|
|||||||
CommandArg("-ev"),
|
CommandArg("-ev"),
|
||||||
CommandArg("{{ .Name }}"),
|
CommandArg("{{ .Name }}"),
|
||||||
}
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
p := target.(*Package)
|
||||||
|
pkg := strings.Split(string(out), "-")
|
||||||
|
if pkg[0] == p.Name {
|
||||||
|
p.Name = pkg[0]
|
||||||
|
p.Version = pkg[1]
|
||||||
|
p.State = "present"
|
||||||
|
} else {
|
||||||
|
p.State = "absent"
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApkUpdateCommand() *Command {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Path = "apk"
|
||||||
|
c.Args = []CommandArg{
|
||||||
|
CommandArg("del"),
|
||||||
|
CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,3 +270,70 @@ func NewApkDeleteCommand() *Command {
|
|||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewAptCreateCommand() *Command {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Path = "apt-get"
|
||||||
|
c.Args = []CommandArg{
|
||||||
|
CommandArg("satisfy"),
|
||||||
|
CommandArg("-y"),
|
||||||
|
CommandArg("{{ .Name }} ({{ .Required }})"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAptReadCommand() *Command {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Path = "dpkg"
|
||||||
|
c.Args = []CommandArg{
|
||||||
|
CommandArg("-s"),
|
||||||
|
CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
c.Extractor = func(out []byte, target any) error {
|
||||||
|
p := target.(*Package)
|
||||||
|
slog.Info("Extract()", "out", out)
|
||||||
|
pkginfo := strings.Split(string(out), "\n")
|
||||||
|
for _, infofield := range pkginfo {
|
||||||
|
if len(infofield) > 0 && infofield[0] != ' ' {
|
||||||
|
fieldKeyValue := strings.SplitN(infofield, ":", 2)
|
||||||
|
if len(fieldKeyValue) > 1 {
|
||||||
|
key := strings.TrimSpace(fieldKeyValue[0])
|
||||||
|
value := strings.TrimSpace(fieldKeyValue[1])
|
||||||
|
switch key {
|
||||||
|
case "Package":
|
||||||
|
if value == p.Name {
|
||||||
|
p.State = "present"
|
||||||
|
} else {
|
||||||
|
p.State = "absent"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case "Version":
|
||||||
|
p.Version = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAptUpdateCommand() *Command {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Path = "apt"
|
||||||
|
c.Args = []CommandArg{
|
||||||
|
CommandArg("install"),
|
||||||
|
CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAptDeleteCommand() *Command {
|
||||||
|
c := NewCommand()
|
||||||
|
c.Path = "apt"
|
||||||
|
c.Args = []CommandArg{
|
||||||
|
CommandArg("remove"),
|
||||||
|
CommandArg("{{ .Name }}"),
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
@ -4,17 +4,17 @@ package resource
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
_ "encoding/json"
|
_ "encoding/json"
|
||||||
_ "fmt"
|
_ "fmt"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
_ "gopkg.in/yaml.v3"
|
_ "gopkg.in/yaml.v3"
|
||||||
_ "io"
|
_ "io"
|
||||||
_ "log"
|
"log/slog"
|
||||||
_ "net/http"
|
_ "net/http"
|
||||||
_ "net/http/httptest"
|
_ "net/http/httptest"
|
||||||
_ "net/url"
|
_ "net/url"
|
||||||
_ "os"
|
_ "os"
|
||||||
_ "strings"
|
_ "strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ func TestPackageApplyResourceTransformation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReadPackage(t *testing.T) {
|
func TestReadPackage(t *testing.T) {
|
||||||
decl:=`
|
decl := `
|
||||||
name: vim
|
name: vim
|
||||||
version: latest
|
version: latest
|
||||||
type: apk
|
type: apk
|
||||||
@ -40,13 +40,56 @@ type: apk
|
|||||||
|
|
||||||
p := NewPackage()
|
p := NewPackage()
|
||||||
assert.NotNil(t, p)
|
assert.NotNil(t, p)
|
||||||
|
m := &MockCommand{
|
||||||
|
Executor: func(value any) ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
Extractor: func(output []byte, target any) error {
|
||||||
|
p.Name = "vim"
|
||||||
|
p.Version = "1.1.1"
|
||||||
|
p.State = "present"
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
loadErr := p.LoadDecl(decl)
|
loadErr := p.LoadDecl(decl)
|
||||||
assert.Nil(t, loadErr)
|
assert.Nil(t, loadErr)
|
||||||
|
assert.Equal(t, "latest", p.Version)
|
||||||
|
|
||||||
|
p.ReadCommand = (*Command)(m)
|
||||||
yaml, readErr := p.Read(context.Background())
|
yaml, readErr := p.Read(context.Background())
|
||||||
assert.Nil(t, readErr)
|
assert.Nil(t, readErr)
|
||||||
assert.Greater(t, len(yaml), 0)
|
assert.Greater(t, len(yaml), 0)
|
||||||
|
slog.Info("read()", "yaml", yaml)
|
||||||
|
assert.Equal(t, "1.1.1", p.Version)
|
||||||
|
slog.Info("resource: ", "package", p)
|
||||||
|
assert.Nil(t, p.Validate())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadAptPackage(t *testing.T) {
|
||||||
|
decl := `
|
||||||
|
name: vim
|
||||||
|
required: ">1.1.1"
|
||||||
|
type: apt
|
||||||
|
`
|
||||||
|
p := NewPackage()
|
||||||
|
assert.NotNil(t, p)
|
||||||
|
loadErr := p.LoadDecl(decl)
|
||||||
|
assert.Nil(t, loadErr)
|
||||||
|
assert.Equal(t, ">1.1.1", p.Required)
|
||||||
|
p.ReadCommand = NewAptReadCommand()
|
||||||
|
p.ReadCommand.Executor = func(value any) ([]byte, error) {
|
||||||
|
return []byte(`
|
||||||
|
Package: vim
|
||||||
|
Version: 1.2.2
|
||||||
|
`), nil
|
||||||
|
}
|
||||||
|
yaml, readErr := p.Read(context.Background())
|
||||||
|
assert.Nil(t, readErr)
|
||||||
|
assert.Greater(t, len(yaml), 0)
|
||||||
|
slog.Info("read()", "yaml", yaml)
|
||||||
|
assert.Equal(t, "1.2.2", p.Version)
|
||||||
|
assert.Nil(t, p.Validate())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadPackageError(t *testing.T) {
|
func TestReadPackageError(t *testing.T) {
|
||||||
@ -58,7 +101,7 @@ func TestCreatePackage(t *testing.T) {
|
|||||||
func TestPackageSetURI(t *testing.T) {
|
func TestPackageSetURI(t *testing.T) {
|
||||||
p := NewPackage()
|
p := NewPackage()
|
||||||
assert.NotNil(t, p)
|
assert.NotNil(t, p)
|
||||||
e := p.SetURI("package://" + "12345_key")
|
e := p.SetURI("package://" + "12345_key?type=apk")
|
||||||
assert.Nil(t, e)
|
assert.Nil(t, e)
|
||||||
assert.Equal(t, "package", p.Type())
|
assert.Equal(t, "package", p.Type())
|
||||||
assert.Equal(t, "12345_key", p.Name)
|
assert.Equal(t, "12345_key", p.Name)
|
||||||
|
@ -19,9 +19,9 @@ type Resource interface {
|
|||||||
ResourceLoader
|
ResourceLoader
|
||||||
StateTransformer
|
StateTransformer
|
||||||
ResourceReader
|
ResourceReader
|
||||||
|
ResourceValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate the type/uri
|
|
||||||
type ResourceValidator interface {
|
type ResourceValidator interface {
|
||||||
Validate() error
|
Validate() error
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/xeipuuv/gojsonschema"
|
"github.com/xeipuuv/gojsonschema"
|
||||||
"strings"
|
"strings"
|
||||||
|
"embed"
|
||||||
|
"net/http"
|
||||||
|
"log/slog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed schemas/*.jsonschema
|
||||||
|
var schemaFiles embed.FS
|
||||||
|
|
||||||
type Schema struct {
|
type Schema struct {
|
||||||
schema gojsonschema.JSONLoader
|
schema gojsonschema.JSONLoader
|
||||||
}
|
}
|
||||||
@ -17,7 +23,8 @@ type Schema struct {
|
|||||||
func NewSchema(name string) *Schema {
|
func NewSchema(name string) *Schema {
|
||||||
path := fmt.Sprintf("file://schemas/%s.jsonschema", name)
|
path := fmt.Sprintf("file://schemas/%s.jsonschema", name)
|
||||||
|
|
||||||
return &Schema{schema: gojsonschema.NewReferenceLoader(path)}
|
return &Schema{schema: gojsonschema.NewReferenceLoaderFileSystem(path, http.FS(schemaFiles))}
|
||||||
|
//return &Schema{schema: gojsonschema.NewReferenceLoader(path)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Schema) Validate(source string) error {
|
func (s *Schema) Validate(source string) error {
|
||||||
@ -26,9 +33,10 @@ func (s *Schema) Validate(source string) error {
|
|||||||
result, err := gojsonschema.Validate(s.schema, loader)
|
result, err := gojsonschema.Validate(s.schema, loader)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("%#v %#v %#v %#v\n", source, loader, result, err)
|
slog.Info("schema error", "source", source, "schema", s.schema, "result", result, "err", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
slog.Info("schema", "source", source, "schema", s.schema, "result", result, "err", err)
|
||||||
|
|
||||||
if !result.Valid() {
|
if !result.Valid() {
|
||||||
schemaErrors := strings.Builder{}
|
schemaErrors := strings.Builder{}
|
||||||
@ -39,3 +47,11 @@ func (s *Schema) Validate(source string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Schema) ValidateSchema() error {
|
||||||
|
sl := gojsonschema.NewSchemaLoader()
|
||||||
|
sl.Validate = true
|
||||||
|
schemaErr := sl.AddSchemas(s.schema)
|
||||||
|
slog.Info("validate schema definition", "schemaloader", sl, "err", schemaErr)
|
||||||
|
return schemaErr
|
||||||
|
}
|
||||||
|
@ -26,7 +26,7 @@ func TestNewSchema(t *testing.T) {
|
|||||||
assert.NotEqual(t, nil, s)
|
assert.NotEqual(t, nil, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSchemaValidate(t *testing.T) {
|
func TestSchemaValidateJSON(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
s := NewSchema("file")
|
s := NewSchema("file")
|
||||||
assert.NotEqual(t, nil, s)
|
assert.NotEqual(t, nil, s)
|
||||||
@ -79,3 +79,10 @@ func TestSchemaValidate(t *testing.T) {
|
|||||||
expected := fmt.Sprintf(declarationAttributes, file, cTime.Format(time.RFC3339Nano))
|
expected := fmt.Sprintf(declarationAttributes, file, cTime.Format(time.RFC3339Nano))
|
||||||
assert.YAMLEq(t, expected, string(r))
|
assert.YAMLEq(t, expected, string(r))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSchemaValidateSchema(t *testing.T) {
|
||||||
|
s := NewSchema("document")
|
||||||
|
assert.NotNil(t, s)
|
||||||
|
|
||||||
|
assert.Nil(t, s.ValidateSchema())
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"$id": "document.jsonschema",
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"title": "document",
|
"title": "document",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -8,7 +9,12 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "Resources list",
|
"description": "Resources list",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object"
|
"oneOf": [
|
||||||
|
{ "$ref": "package-declaration.jsonschema" },
|
||||||
|
{ "$ref": "file-declaration.jsonschema" },
|
||||||
|
{ "$ref": "user-declaration.jsonschema" },
|
||||||
|
{ "$ref": "exec-declaration.jsonschema" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
17
internal/resource/schemas/exec-declaration.jsonschema
Normal file
17
internal/resource/schemas/exec-declaration.jsonschema
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$id": "exec-declaration.jsonschema",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "exec-declaration",
|
||||||
|
"type": "object",
|
||||||
|
"required": [ "type", "attributes" ],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Resource type name.",
|
||||||
|
"enum": [ "exec" ]
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"$ref": "exec.jsonschema"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"$id": "exec.jsonschema",
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"title": "exec",
|
"title": "exec",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
17
internal/resource/schemas/file-declaration.jsonschema
Normal file
17
internal/resource/schemas/file-declaration.jsonschema
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$id": "file-declaration.jsonschema",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "file-declaration",
|
||||||
|
"type": "object",
|
||||||
|
"required": [ "type", "attributes" ],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Resource type name.",
|
||||||
|
"enum": [ "file" ]
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"$ref": "file.jsonschema"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"$id": "file.jsonschema",
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"title": "file",
|
"title": "file",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
17
internal/resource/schemas/package-declaration.jsonschema
Normal file
17
internal/resource/schemas/package-declaration.jsonschema
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$id": "package-declaration.jsonschema",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "package-declaration",
|
||||||
|
"type": "object",
|
||||||
|
"required": [ "type", "attributes" ],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Resource type name.",
|
||||||
|
"enum": [ "package" ]
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"$ref": "package.jsonschema"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,18 @@
|
|||||||
{
|
{
|
||||||
|
"$id": "package.jsonschema",
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"title": "package",
|
"title": "package",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [ "name", "version", "type" ],
|
"required": [ "name", "type" ],
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9+.-_]+$"
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"description": "version requirement",
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^([><~=][=]?[a-zA-Z0-9+.-_]+|)$"
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
17
internal/resource/schemas/user-declaration.jsonschema
Normal file
17
internal/resource/schemas/user-declaration.jsonschema
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$id": "user-declaration.jsonschema",
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "declaration",
|
||||||
|
"type": "object",
|
||||||
|
"required": [ "type", "attributes" ],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Resource type name.",
|
||||||
|
"enum": [ "user" ]
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"$ref": "user.jsonschema"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"$id": "user.jsonschema",
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"title": "user",
|
"title": "user",
|
||||||
"description": "A user account",
|
"description": "A user account",
|
||||||
@ -6,7 +7,8 @@
|
|||||||
"required": [ "name" ],
|
"required": [ "name" ],
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"pattern": "^[a-z]([-_a-z0-9]{0,31})$"
|
||||||
},
|
},
|
||||||
"uid": {
|
"uid": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
@ -50,6 +50,10 @@ func (u *User) ResolveId(ctx context.Context) string {
|
|||||||
return LookupUIDString(u.Name)
|
return LookupUIDString(u.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) Validate() error {
|
||||||
|
return fmt.Errorf("failed")
|
||||||
|
}
|
||||||
|
|
||||||
func (u *User) Apply() error {
|
func (u *User) Apply() error {
|
||||||
switch u.State {
|
switch u.State {
|
||||||
case "present":
|
case "present":
|
||||||
|
@ -13,6 +13,7 @@ type MockResource struct {
|
|||||||
InjectType func() string
|
InjectType func() string
|
||||||
InjectResolveId func(ctx context.Context) string
|
InjectResolveId func(ctx context.Context) string
|
||||||
InjectLoadDecl func(string) error
|
InjectLoadDecl func(string) error
|
||||||
|
InjectValidate func() error
|
||||||
InjectApply func() error
|
InjectApply func() error
|
||||||
InjectRead func(context.Context) ([]byte, error)
|
InjectRead func(context.Context) ([]byte, error)
|
||||||
}
|
}
|
||||||
@ -29,6 +30,10 @@ func (m *MockResource) LoadDecl(yamlResourceDeclaration string) error {
|
|||||||
return m.InjectLoadDecl(yamlResourceDeclaration)
|
return m.InjectLoadDecl(yamlResourceDeclaration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockResource) Validate() error {
|
||||||
|
return m.InjectValidate()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MockResource) Apply() error {
|
func (m *MockResource) Apply() error {
|
||||||
return m.InjectApply()
|
return m.InjectApply()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user