add support for RSA keys/certs
Some checks failed
Lint / golangci-lint (push) Successful in 9m53s
Declarative Tests / test (push) Failing after 5s
Declarative Tests / build-fedora (push) Successful in 2m12s
Declarative Tests / build-ubuntu-focal (push) Successful in 1m21s

This commit is contained in:
Matthew Rich 2024-07-17 01:34:57 -07:00
parent 2c9e178554
commit a6426da6e1
79 changed files with 2645 additions and 254 deletions

View File

@ -32,3 +32,6 @@ clean:
rm jx
lint:
golangci-lint run --verbose ./...
vulncheck:
govulncheck ./...
go vet ./...

View File

@ -74,13 +74,14 @@ Read a resource document from an http endpoint.
Resources:
* [container](examples/container.yaml) [schema](internal/resource/schemas/container.schema.json)
* [container-image](examples/container-image.yaml) [schema](internal/resource/schemas/container-image.schema.json)
* [container-network](examples/container-network.yaml) [schema](internal/resource/schemas/container-network.schema.json)
* [exec](examples/exec.yaml) [schema](internal/resource/schemas/exec.schema.json)
* [file](examples/file.yaml) [schema](internal/resource/schemas/file.schema.json)
* [http](examples/http.yaml) [schema](internal/resource/schemas/http.schema.json)
* [iptable](examples/iptable.yaml) [schema](internal/resource/schemas/iptable.schema.json)
* [network_route](examples/network_route.yaml) [schema](internal/resource/schemas/network_route.schema.json)
* [package](examples/package.yaml) [schema](internal/resource/schemas/package.schema.json)
* [user](examples/user.yaml) [schema](internal/resource/schemas/user.schema.json)
* [container](examples/container.jx.yaml) [schema](internal/resource/schemas/container.schema.json)
* [container-image](examples/container-image.jx.yaml) [schema](internal/resource/schemas/container-image.schema.json)
* [container-network](examples/container-network.jx.yaml) [schema](internal/resource/schemas/container-network.schema.json)
* [exec](examples/exec.jx.yaml) [schema](internal/resource/schemas/exec.schema.json)
* [file](examples/file.jx.yaml) [schema](internal/resource/schemas/file.schema.json)
* [group](examples/group.jx.yaml) [schema](internal/resource/schemas/group.schema.json)
* [http](examples/http.jx.yaml) [schema](internal/resource/schemas/http.schema.json)
* [iptable](examples/iptable.jx.yaml) [schema](internal/resource/schemas/iptable.schema.json)
* [network_route](examples/network_route.jx.yaml) [schema](internal/resource/schemas/network_route.schema.json)
* [package](examples/package.jx.yaml) [schema](internal/resource/schemas/package.schema.json)
* [user](examples/user.jx.yaml) [schema](internal/resource/schemas/user.schema.json)

View File

@ -4,7 +4,6 @@ package main
import (
"context"
"decl/internal/codec"
"decl/internal/config"
"decl/internal/resource"
"decl/internal/source"
@ -170,15 +169,6 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
}
}
/*
switch *GlobalOformat {
case FormatYaml:
encoder = resource.NewYAMLEncoder(output)
case FormatJson:
encoder = resource.NewJSONEncoder(output)
}
*/
slog.Info("main.ImportResource", "args", os.Args, "output", GlobalOutput)
outputTarget, err := target.TargetTypes.New(GlobalOutput)
if err != nil {
@ -209,7 +199,7 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
if *GlobalQuiet {
for _, dr := range d.Resources() {
if _, e := output.Write([]byte(dr.Resource().URI())); e != nil {
if _, e := output.Write([]byte(fmt.Sprintf("%s\n", dr.Resource().URI()))); e != nil {
return e
}
}
@ -245,7 +235,6 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
}
}
var encoder codec.Encoder
documents := make([]*resource.Document, 0, 100)
for _, source := range cmd.Args() {
loaded := LoadSourceURI(source)
@ -270,12 +259,12 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
return e
}
switch *GlobalOformat {
case FormatYaml:
encoder = codec.NewYAMLEncoder(output)
case FormatJson:
encoder = codec.NewJSONEncoder(output)
outputTarget, err := target.TargetTypes.New(GlobalOutput)
if err != nil {
slog.Error("Failed opening target", "error", err)
}
defer outputTarget.Close()
if *GlobalQuiet {
for _, dr := range d.Resources() {
if _, e := output.Write([]byte(dr.Resource().URI())); e != nil {
@ -283,8 +272,9 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
}
}
} else {
if documentGenerateErr := encoder.Encode(d); documentGenerateErr != nil {
return documentGenerateErr
slog.Info("main.Apply", "outputTarget", outputTarget, "type", outputTarget.Type())
if outputErr := outputTarget.EmitResources([]*resource.Document{d}, nil); outputErr != nil {
return outputErr
}
}
}

View File

@ -0,0 +1,15 @@
resources:
- type: pki
transition: create
config: myca
attributes:
privatekeyref: file://myca_privkey.pem
publickeyref: file://myca_pubkey.pem
certificateref: file://myca_cert.pem
- type: pki
transition: update
attributes:
signedbyref: pki://myca_privkey.pem
privatekeyref: file://mycert_key.pem
publickeyref: file://mycert_pubkey.pem
certificateref: file://mycert.pem

View File

@ -0,0 +1,43 @@
configurations:
- name: myca
type: certificate
values:
certtemplate:
serialnumber: 2024
subject:
organization:
- RKH
country:
- US
province:
- CA
locality:
- San Francisco
streetaddress:
- 0 cert st
postalcode:
- 94101
notbefore: 2024-07-10
notafter: 2025-07-10
basicconstraintsvalid: true
isca: true
- name: mycert
type: certificate
values:
certtemplate:
serialnumber: 2025
subject:
organization:
- RKH
country:
- US
province:
- CA
locality:
- San Francisco
streetaddress:
- 0 cert st
postalcode:
- 94101
notbefore: 2024-07-10
notafter: 2025-07-10

View File

@ -0,0 +1,5 @@
resources:
- type: container-image
transition: read
attributes:
name: nginx:latest

6
examples/group.jx.yaml Normal file
View File

@ -0,0 +1,6 @@
resources:
- type: group
transition: create
attributes:
name: "testgroup"
gid: "12001"

11
examples/iptable.jx.yaml Normal file
View File

@ -0,0 +1,11 @@
resources:
- type: iptable
transition: create
attributes:
id: 1
table: filter
chain: INPUT
jump: LIBVIRT_INP
state: present
resourcetype: rule

9
examples/package.jx.yaml Normal file
View File

@ -0,0 +1,9 @@
resources:
- type: package
transition: create
attributes:
name: zip
version: 3.0-12build2
type: apt
state: present

9
go.mod
View File

@ -1,11 +1,11 @@
module decl
go 1.22.1
go 1.22.5
require (
gitea.rosskeen.house/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520193117-1835255b6d02
github.com/docker/docker v25.0.5+incompatible
github.com/docker/docker v27.0.3+incompatible
github.com/docker/go-connections v0.5.0
github.com/opencontainers/image-spec v1.1.0
github.com/sters/yaml-diff v1.3.2
@ -28,6 +28,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
@ -41,7 +42,9 @@ require (
go.opentelemetry.io/otel/metric v1.25.0 // indirect
go.opentelemetry.io/otel/sdk v1.25.0 // indirect
go.opentelemetry.io/otel/trace v1.25.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
gotest.tools/v3 v3.5.1 // indirect

22
go.sum
View File

@ -17,8 +17,8 @@ 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.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE=
github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE=
github.com/docker/docker v27.0.3+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=
@ -58,6 +58,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
@ -111,16 +113,16 @@ go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7e
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -131,12 +133,12 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@ -3,6 +3,8 @@
package codec
import (
"io"
"fmt"
"errors"
"encoding/json"
"gopkg.in/yaml.v3"
@ -23,7 +25,7 @@ func (f *Format) Validate() error {
case FormatYaml, FormatJson, FormatProtoBuf:
return nil
default:
return ErrInvalidFormat
return fmt.Errorf("%w: %s", ErrInvalidFormat, *f)
}
}
@ -59,3 +61,20 @@ func (f *Format) UnmarshalYAML(value *yaml.Node) error {
}
return f.UnmarshalValue(s)
}
func (f Format) Encoder(w io.Writer) Encoder {
return NewEncoder(w, f)
}
func (f Format) Decoder(r io.Reader) Decoder {
return NewDecoder(r, f)
}
func (f Format) Serialize(object any, w io.Writer) error {
return f.Encoder(w).Encode(object)
}
func (f Format) Deserialize(r io.Reader, object any) error {
return f.Decoder(r).Decode(object)
}

View File

@ -6,6 +6,7 @@ import (
_ "context"
"encoding/json"
"fmt"
"errors"
"gopkg.in/yaml.v3"
"io"
"log/slog"
@ -17,8 +18,11 @@ import (
"decl/internal/codec"
)
var ErrUnknownCommand error = errors.New("Unable to find command in path")
type CommandExecutor func(value any) ([]byte, error)
type CommandExtractAttributes func(output []byte, target any) error
type CommandExists func() error
type CommandArg string
@ -30,10 +34,17 @@ type Command struct {
FailOnError bool `json:"failonerror" yaml:"failonerror"`
Executor CommandExecutor `json:"-" yaml:"-"`
Extractor CommandExtractAttributes `json:"-" yaml:"-"`
CommandExists CommandExists `json:"-" yaml:"-"`
}
func NewCommand() *Command {
c := &Command{ Split: true, FailOnError: true }
c.CommandExists = func() error {
if _, err := exec.LookPath(c.Path); err != nil {
return fmt.Errorf("%w - %w", ErrUnknownCommand, err)
}
return nil
}
c.Executor = func(value any) ([]byte, error) {
args, err := c.Template(value)
if err != nil {
@ -87,10 +98,7 @@ func (c *Command) SetCmdEnv(cmd *exec.Cmd) {
}
func (c *Command) Exists() bool {
if _, err := exec.LookPath(c.Path); err != nil {
return false
}
return true
return c.CommandExists() == nil
}
func (c *Command) Template(value any) ([]string, error) {

View File

@ -0,0 +1,83 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"context"
"io"
"net/url"
"decl/internal/codec"
"encoding/json"
"gopkg.in/yaml.v3"
"crypto/x509"
)
func init() {
ConfigTypes.Register([]string{"certificate"}, func(u *url.URL) Configuration {
c := NewCertificate()
return c
})
}
type Certificate map[string]*x509.Certificate
func NewCertificate() *Certificate {
c := make(Certificate)
return &c
}
func (c *Certificate) Read(ctx context.Context) ([]byte, error) {
return nil, nil
}
func (c *Certificate) Load(r io.Reader) (err error) {
err = codec.NewYAMLDecoder(r).Decode(c)
if err == nil {
_, err = c.Read(context.Background())
}
return err
}
func (c *Certificate) LoadYAML(yamlData string) (err error) {
err = codec.NewYAMLStringDecoder(yamlData).Decode(c)
if err == nil {
_, err = c.Read(context.Background())
}
return err
}
func (c *Certificate) UnmarshalJSON(data []byte) error {
if unmarshalErr := json.Unmarshal(data, c); unmarshalErr != nil {
return unmarshalErr
}
return nil
}
func (c *Certificate) UnmarshalYAML(value *yaml.Node) error {
type decodeCertificate Certificate
if unmarshalErr := value.Decode((*decodeCertificate)(c)); unmarshalErr != nil {
return unmarshalErr
}
return nil
}
func (c *Certificate) Clone() Configuration {
jsonGeneric, _ := json.Marshal(c)
clone := NewCertificate()
if unmarshalErr := json.Unmarshal(jsonGeneric, &clone); unmarshalErr != nil {
panic(unmarshalErr)
}
return clone
}
func (c *Certificate) Type() string {
return "certificate"
}
func (c *Certificate) GetValue(name string) (result any, err error) {
var ok bool
if result, ok = (*c)[name]; !ok {
err = ErrUnknownConfigurationKey
}
return
}

View File

@ -0,0 +1,33 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"github.com/stretchr/testify/assert"
"testing"
"crypto/x509"
)
func TestNewCertificateConfig(t *testing.T) {
c := NewCertificate()
assert.NotNil(t, c)
}
func TestNewCertificateConfigYAML(t *testing.T) {
c := NewCertificate()
assert.NotNil(t, c)
config := `
catemplate:
subject:
organization:
- RKH
notbefore: 2024-07-10
`
yamlErr := c.LoadYAML(config)
assert.Nil(t, yamlErr)
crt, err := c.GetValue("catemplate")
assert.Nil(t, err)
assert.Equal(t, []string{"RKH"}, crt.(*x509.Certificate).Subject.Organization)
}

View File

@ -43,8 +43,11 @@ func NewConfigFileFromURI(u *url.URL) *ConfigFile {
func NewConfigFileSource(u *url.URL) *ConfigFile {
t := NewConfigFileFromURI(u)
t.reader,_ = transport.NewReader(u)
if formatErr := t.Format.Set(t.reader.ContentType()); formatErr != nil {
panic(formatErr)
contentType := codec.Format(t.reader.ContentType())
if contentType.Validate() == nil {
if formatErr := t.Format.Set(t.reader.ContentType()); formatErr != nil {
panic(formatErr)
}
}
t.decoder = codec.NewDecoder(t.reader, t.Format)
return t
@ -53,8 +56,11 @@ func NewConfigFileSource(u *url.URL) *ConfigFile {
func NewConfigFileTarget(u *url.URL) *ConfigFile {
t := NewConfigFileFromURI(u)
t.writer,_ = transport.NewWriter(u)
if formatErr := t.Format.Set(t.writer.ContentType()); formatErr != nil {
panic(formatErr)
contentType := codec.Format(t.writer.ContentType())
if contentType.Validate() == nil {
if formatErr := t.Format.Set(t.writer.ContentType()); formatErr != nil {
panic(formatErr)
}
}
t.encoder = codec.NewEncoder(t.writer, t.Format)
return t

View File

@ -10,36 +10,36 @@ import (
func init() {
ConfigTypes.Register([]string{"generic"}, func(u *url.URL) Configuration {
g := NewGeneric()
g := NewGeneric[any]()
return g
})
}
type Generic map[string]any
type Generic[Value any] map[string]Value
func NewGeneric() *Generic {
g := make(Generic)
func NewGeneric[Value any]() *Generic[Value] {
g := make(Generic[Value])
return &g
}
func (g *Generic) Clone() Configuration {
func (g *Generic[Value]) Clone() Configuration {
jsonGeneric, _ := json.Marshal(g)
clone := make(Generic)
if unmarshalErr := json.Unmarshal(jsonGeneric, &clone); unmarshalErr != nil {
clone := NewGeneric[Value]()
if unmarshalErr := json.Unmarshal(jsonGeneric, clone); unmarshalErr != nil {
panic(unmarshalErr)
}
return &clone
return clone
}
func (g *Generic) Type() string {
func (g *Generic[Value]) Type() string {
return "generic"
}
func (g *Generic) Read(context.Context) ([]byte, error) {
func (g *Generic[Value]) Read(context.Context) ([]byte, error) {
return nil, nil
}
func (g *Generic) GetValue(name string) (result any, err error) {
func (g *Generic[Value]) GetValue(name string) (result any, err error) {
var ok bool
if result, ok = (*g)[name]; !ok {
err = ErrUnknownConfigurationKey

View File

@ -8,6 +8,6 @@ import (
)
func TestNewGenericConfig(t *testing.T) {
g := NewGeneric()
g := NewGeneric[any]()
assert.NotNil(t, g)
}

View File

@ -13,10 +13,13 @@
"type": {
"type": "string",
"description": "Config type name.",
"enum": [ "generic", "exec" ]
"enum": [ "generic", "exec", "certificate" ]
},
"values": {
"type": "object"
"oneOf": [
{ "type": "object" },
{ "$ref": "certificate.schema.json" }
]
}
}
}

View File

@ -0,0 +1,62 @@
{
"$id": "certificate.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "certificate",
"type": "object",
"required": [ "path", "filetype" ],
"properties": {
"SerialNumber": {
"type": "integer",
"description": "Serial number",
"minLength": 1
},
"Issuer": {
"$ref": "pkixname.schema.json"
},
"Subject": {
"$ref": "pkixname.schema.json"
},
"NotBefore": {
"type": "string",
"format": "date-time",
"description": "Cert is not valid before time in YYYY-MM-DDTHH:MM:SS.sssssssssZ format."
},
"NotAfter": {
"type": "string",
"format": "date-time",
"description": "Cert is not valid after time in YYYY-MM-DDTHH:MM:SS.sssssssssZ format."
},
"KeyUsage": {
"type": "integer",
"enum": [
1,
2,
3,
4,
5,
6,
7,
8,
9
],
"description": "Actions valid for a key. E.g. 1 = KeyUsageDigitalSignature"
},
"ExtKeyUsage": {
"type": "array",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 13
},
"description": "Extended set of actions valid for a key"
},
"BasicConstraintsValid": {
"type": "boolean",
"description": "BasicConstraintsValid indicates whether IsCA, MaxPathLen, and MaxPathLenZero are valid"
},
"IsCA": {
"type": "boolean",
"description": ""
}
}
}

View File

@ -0,0 +1,18 @@
{
"$id": "config.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "config",
"type": "object",
"required": [ "configurations" ],
"properties": {
"configurations": {
"type": "array",
"description": "Configurations list",
"items": {
"oneOf": [
{ "$ref": "block.schema.json" }
]
}
}
}
}

View File

@ -0,0 +1,65 @@
{
"$id": "pkixname.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "pkixname",
"type": "object",
"properties": {
"Country": {
"type": "array",
"description": "Country name",
"items": {
"type": "string"
}
},
"Organization": {
"type": "array",
"description": "Organization name",
"items": {
"type": "string"
}
},
"OrganizationalUnit": {
"type": "array",
"description": "Organizational Unit name",
"items": {
"type": "string"
}
},
"Locality": {
"type": "array",
"description": "Locality name",
"items": {
"type": "string"
}
},
"Province": {
"type": "array",
"description": "Province name",
"items": {
"type": "string"
}
},
"StreetAddress": {
"type": "array",
"description": "Street address",
"items": {
"type": "string"
}
},
"PostalCode": {
"type": "array",
"description": "Postal Code",
"items": {
"type": "string"
}
},
"SerialNumber": {
"type": "string",
"description": ""
},
"CommonName": {
"type": "string",
"description": "Name"
}
}
}

View File

@ -36,7 +36,7 @@ type ContainerClient interface {
ContainerInspect(context.Context, string) (types.ContainerJSON, error)
ContainerRemove(context.Context, string, container.RemoveOptions) error
ContainerStop(context.Context, string, container.StopOptions) error
ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error)
ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error)
Close() error
}
@ -79,6 +79,7 @@ type Container struct {
config ConfigurationValueGetter
apiClient ContainerClient
Resources ResourceMapper `json:"-" yaml:"-"`
}
func init() {
@ -103,6 +104,10 @@ func NewContainer(containerClientApi ContainerClient) *Container {
}
}
func (c *Container) SetResourceMapper(resources ResourceMapper) {
c.Resources = resources
}
func (c *Container) Clone() Resource {
return &Container {
Id: c.Id,

View File

@ -23,9 +23,9 @@ _ "os/exec"
)
type ContainerImageClient interface {
ImagePull(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error)
ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error)
ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error)
ImageRemove(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error)
ImageRemove(ctx context.Context, imageID string, options image.RemoveOptions) ([]image.DeleteResponse, error)
Close() error
}
@ -44,12 +44,14 @@ type ContainerImage struct {
config ConfigurationValueGetter
apiClient ContainerImageClient
Resources ResourceMapper `json:"-" yaml:"-"`
}
func init() {
ResourceTypes.Register([]string{"container-image"}, func(u *url.URL) Resource {
c := NewContainerImage(nil)
c.Name = strings.Join([]string{u.Hostname(), u.Path}, ":")
c.Name = ContainerImageNameFromURI(u)
slog.Info("NewContainerImage", "container", c)
return c
})
}
@ -68,6 +70,10 @@ func NewContainerImage(containerClientApi ContainerImageClient) *ContainerImage
}
}
func (c *ContainerImage) SetResourceMapper(resources ResourceMapper) {
c.Resources = resources
}
func (c *ContainerImage) Clone() Resource {
return &ContainerImage {
Id: c.Id,
@ -91,9 +97,9 @@ func (c *ContainerImage) StateMachine() machine.Stater {
return c.stater
}
func (c *ContainerImage) URI() string {
func URIFromContainerImageName(imageName string) string {
var host, namespace, repo string
elements := strings.Split(c.Name, "/")
elements := strings.Split(imageName, "/")
switch len(elements) {
case 1:
repo = elements[0]
@ -111,6 +117,31 @@ func (c *ContainerImage) URI() string {
return fmt.Sprintf("container-image://%s/%s", host, strings.Join([]string{namespace, repo}, "/"))
}
// Reconstruct the image name from a given parsed URL
func ContainerImageNameFromURI(u *url.URL) string {
var host string = u.Hostname()
// var host, namespace, repo string
elements := strings.FieldsFunc(u.RequestURI(), func(c rune) bool { return c == '/' })
slog.Info("ContainerImageNameFromURI", "url", u, "elements", elements)
/*
switch len(elements) {
case 1:
repo = elements[0]
case 2:
namespace = elements[0]
repo = elements[1]
}
*/
if host == "" {
return strings.Join(elements, "/")
}
return fmt.Sprintf("%s/%s", host, strings.Join(elements, "/"))
}
func (c *ContainerImage) URI() string {
return URIFromContainerImageName(c.Name)
}
func (c *ContainerImage) SetURI(uri string) error {
resourceUri, e := url.Parse(uri)
if e == nil {
@ -209,30 +240,49 @@ func (c *ContainerImage) Create(ctx context.Context) error {
return nil
}
func (c *ContainerImage) Read(ctx context.Context) ([]byte, error) {
out, err := c.apiClient.ImagePull(ctx, c.Name, types.ImagePullOptions{})
slog.Info("Read()", "name", c.Name, "error", err)
_, outputErr := io.ReadAll(out)
func (c *ContainerImage) Pull(ctx context.Context) error {
out, err := c.apiClient.ImagePull(ctx, c.Name, image.PullOptions{})
slog.Info("ContainerImage.Pull()", "name", c.Name, "error", err)
if err == nil {
if _, outputErr := io.ReadAll(out); outputErr != nil {
return fmt.Errorf("%w: %s %s", outputErr, c.Type(), c.Name)
}
} else {
return fmt.Errorf("%w: %s %s", err, c.Type(), c.Name)
}
return nil
}
func (c *ContainerImage) Inspect(ctx context.Context) (imageInspect types.ImageInspect) {
var err error
imageInspect, _, err = c.apiClient.ImageInspectWithRaw(ctx, c.Name)
if err != nil {
return nil, fmt.Errorf("%w: %s %s", err, c.Type(), c.Name)
panic(err)
}
return
}
if outputErr != nil {
return nil, fmt.Errorf("%w: %s %s", outputErr, c.Type(), c.Name)
}
func (c *ContainerImage) Read(ctx context.Context) (resourceYaml []byte, err error) {
defer func() {
if r := recover(); r != nil {
c.State = "absent"
resourceYaml = nil
err = fmt.Errorf("%w", r.(error))
}
}()
imageInspect, _, err := c.apiClient.ImageInspectWithRaw(ctx, c.Name)
var imageInspect types.ImageInspect
imageInspect, _, err = c.apiClient.ImageInspectWithRaw(ctx, c.Name)
slog.Info("ContainerImage.Read()", "name", c.Name, "error", err)
if err != nil {
if client.IsErrNotFound(err) {
slog.Info("ContainerImage.Read()", "oldstate", c.State, "newstate", "absent", "error", err)
c.State = "absent"
if pullErr := c.Pull(ctx); pullErr != nil {
panic(pullErr)
}
imageInspect = c.Inspect(ctx)
} else {
panic(err)
}
return nil, err
}
c.State = "present"
@ -249,13 +299,13 @@ func (c *ContainerImage) Read(ctx context.Context) ([]byte, error) {
c.OS = imageInspect.Os
c.Size = imageInspect.Size
c.Comment = imageInspect.Comment
slog.Info("Read() ", "type", c.Type(), "name", c.Name, "Id", c.Id)
slog.Info("ContainerImage.Read()", "type", c.Type(), "name", c.Name, "Id", c.Id, "state", c.State, "error", err)
return yaml.Marshal(c)
}
func (c *ContainerImage) Delete(ctx context.Context) error {
slog.Info("ContainerImage.Delete()", "image", c)
options := types.ImageRemoveOptions{
options := image.RemoveOptions{
Force: false,
PruneChildren: false,
}

View File

@ -13,7 +13,7 @@ _ "fmt"
"io"
_ "net/http"
_ "net/http/httptest"
_ "net/url"
"net/url"
_ "os"
"strings"
"testing"
@ -24,6 +24,45 @@ func TestNewContainerImageResource(t *testing.T) {
assert.NotNil(t, c)
}
func TestContainerImageURI(t *testing.T) {
case0URI := URIFromContainerImageName("foo")
assert.Equal(t, "container-image:///foo", case0URI)
case1URI := URIFromContainerImageName("foo:bar")
assert.Equal(t, "container-image:///foo:bar", case1URI)
case2URI := URIFromContainerImageName("quuz/foo:bar")
assert.Equal(t, "container-image:///quuz/foo:bar", case2URI)
case3URI := URIFromContainerImageName("myhost/quuz/foo:bar")
assert.Equal(t, "container-image://myhost/quuz/foo:bar", case3URI)
}
func TestLoadFromContainerImageURI(t *testing.T) {
testURI := URIFromContainerImageName("myhost/quuz/foo:bar")
newResource, resourceErr := ResourceTypes.New(testURI)
assert.Nil(t, resourceErr)
assert.NotNil(t, newResource)
assert.IsType(t, &ContainerImage{}, newResource)
assert.Equal(t, "myhost/quuz/foo:bar", newResource.(*ContainerImage).Name)
assert.Equal(t, testURI, newResource.URI())
}
func TestContainerImageNameFromURI(t *testing.T) {
case0u,_ := url.Parse("container-image:///foo")
case0Image := ContainerImageNameFromURI(case0u)
assert.Equal(t, "foo", case0Image)
case1u,_ := url.Parse("container-image:///foo:bar")
case1Image := ContainerImageNameFromURI(case1u)
assert.Equal(t, "foo:bar", case1Image)
case2u,_ := url.Parse("container-image:///quuz/foo:bar")
case2Image := ContainerImageNameFromURI(case2u)
assert.Equal(t, "quuz/foo:bar", case2Image)
case3u,_ := url.Parse("container-image://myhost/quuz/foo:bar")
case3Image := ContainerImageNameFromURI(case3u)
assert.Equal(t, "myhost/quuz/foo:bar", case3Image)
}
func TestReadContainerImage(t *testing.T) {
output := io.NopCloser(strings.NewReader("testdata"))
ctx := context.Background()
@ -32,10 +71,10 @@ func TestReadContainerImage(t *testing.T) {
state: present
`
m := &mocks.MockContainerClient{
InjectImagePull: func(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error) {
InjectImagePull: func(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error) {
return output, nil
},
InjectImageRemove: func(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
InjectImageRemove: func(ctx context.Context, imageID string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
return nil, nil
},
InjectImageInspectWithRaw: func(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) {

View File

@ -1,16 +1,13 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
// Container resource
package resource
import (
"context"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
_ "github.com/docker/docker/api/types/mount"
_ "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/network"
_ "github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/client"
"gopkg.in/yaml.v3"
@ -24,22 +21,31 @@ _ "strings"
"io"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
"log/slog"
"time"
)
type ContainerNetworkClient interface {
ContainerClient
NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
NetworkCreate(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error)
NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error)
NetworkInspect(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error)
}
type ContainerNetwork struct {
stater machine.Stater `json:"-" yaml:"-"`
Id string `json:"ID,omitempty" yaml:"ID,omitempty"`
Name string `json:"name" yaml:"name"`
Id string `json:"ID,omitempty" yaml:"ID,omitempty"`
Name string `json:"name" yaml:"name"`
Driver string `json:"driver,omitempty" yaml:"driver,omitempty"`
EnableIPv6 bool `json:"enableipv6,omitempty" yaml:"enableipv6,omitempty"`
Internal bool `json:"internal,omitempty" yaml:"internal,omitempty"`
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
Created time.Time `json:"created" yaml:"created"`
State string `yaml:"state"`
config ConfigurationValueGetter
apiClient ContainerNetworkClient
Resources ResourceMapper `json:"-" yaml:"-"`
}
func init() {
@ -64,6 +70,10 @@ func NewContainerNetwork(containerClientApi ContainerNetworkClient) *ContainerNe
}
}
func (n *ContainerNetwork) SetResourceMapper(resources ResourceMapper) {
n.Resources = resources
}
func (n *ContainerNetwork) Clone() Resource {
return &ContainerNetwork {
Id: n.Id,
@ -82,22 +92,48 @@ func (n *ContainerNetwork) StateMachine() machine.Stater {
func (n *ContainerNetwork) Notify(m *machine.EventMessage) {
ctx := context.Background()
slog.Info("Notify()", "ContainerNetwork", n, "m", m)
switch m.On {
case machine.ENTERSTATEEVENT:
switch m.Dest {
case "start_read":
if _,readErr := n.Read(ctx); readErr == nil {
if triggerErr := n.StateMachine().Trigger("state_read"); triggerErr == nil {
return
} else {
n.State = "absent"
panic(triggerErr)
}
} else {
n.State = "absent"
panic(readErr)
}
case "start_delete":
if deleteErr := n.Delete(ctx); deleteErr == nil {
if triggerErr := n.StateMachine().Trigger("deleted"); triggerErr == nil {
return
} else {
n.State = "present"
panic(triggerErr)
}
} else {
n.State = "present"
panic(deleteErr)
}
case "start_create":
if e := n.Create(ctx); e == nil {
if triggerErr := n.stater.Trigger("created"); triggerErr == nil {
if triggerErr := n.StateMachine().Trigger("created"); triggerErr == nil {
return
}
}
n.State = "absent"
case "present":
case "absent":
n.State = "absent"
case "present", "created", "read":
n.State = "present"
}
case machine.EXITSTATEEVENT:
}
}
func (n *ContainerNetwork) URI() string {
@ -148,7 +184,7 @@ func (n *ContainerNetwork) LoadDecl(yamlResourceDeclaration string) error {
}
func (n *ContainerNetwork) Create(ctx context.Context) error {
networkResp, err := n.apiClient.NetworkCreate(ctx, n.Name, types.NetworkCreate{
networkResp, err := n.apiClient.NetworkCreate(ctx, n.Name, network.CreateOptions{
Driver: "bridge",
})
if err != nil {
@ -159,9 +195,52 @@ func (n *ContainerNetwork) Create(ctx context.Context) error {
return nil
}
// produce yaml representation of any resource
func (n *ContainerNetwork) Inspect(ctx context.Context, networkID string) error {
networkInspect, err := n.apiClient.NetworkInspect(ctx, networkID, network.InspectOptions{})
if client.IsErrNotFound(err) {
n.State = "absent"
} else {
n.State = "present"
n.Id = networkInspect.ID
if n.Name == "" {
if networkInspect.Name[0] == '/' {
n.Name = networkInspect.Name[1:]
} else {
n.Name = networkInspect.Name
}
}
n.Created = networkInspect.Created
n.Internal = networkInspect.Internal
n.Driver = networkInspect.Driver
n.Labels = networkInspect.Labels
n.EnableIPv6 = networkInspect.EnableIPv6
}
return nil
}
func (n *ContainerNetwork) Read(ctx context.Context) ([]byte, error) {
var networkID string
filterArgs := filters.NewArgs()
filterArgs.Add("name", n.Name)
networks, err := n.apiClient.NetworkList(ctx, network.ListOptions{
Filters: filterArgs,
})
if err != nil {
return nil, fmt.Errorf("%w: %s %s", err, n.Type(), n.Name)
}
for _, net := range networks {
if net.Name == n.Name {
networkID = net.ID
}
}
if inspectErr := n.Inspect(ctx, networkID); inspectErr != nil {
return nil, fmt.Errorf("%w: network %s", inspectErr, networkID)
}
slog.Info("Read() ", "type", n.Type(), "name", n.Name, "Id", n.Id)
return yaml.Marshal(n)
}

View File

@ -9,7 +9,7 @@ _ "encoding/json"
_ "fmt"
_ "github.com/docker/docker/api/types"
_ "github.com/docker/docker/api/types/container"
_ "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/network"
"github.com/stretchr/testify/assert"
_ "io"
_ "net/http"
@ -32,6 +32,19 @@ func TestReadContainerNetwork(t *testing.T) {
state: present
`
m := &mocks.MockContainerClient{
InjectNetworkList: func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
return []network.Summary{
{ID: "123456789abc"},
{ID: "123456789def"},
}, nil
},
InjectNetworkInspect: func(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error) {
return network.Inspect{
ID: "123456789abc",
Name: "test",
Driver: "bridge",
}, nil
},
}
n := NewContainerNetwork(m)

View File

@ -46,8 +46,15 @@ func NewDeclaration() *Declaration {
return &Declaration{}
}
func NewDeclarationFromDocument(document *Document) *Declaration {
return &Declaration{ document: document }
}
func (d *Declaration) SetDocument(newDocument *Document) {
slog.Info("Declaration.SetDocument()")
d.document = newDocument
d.SetConfig(d.document.config)
d.Attributes.SetResourceMapper(d.document.uris)
}
func (d *Declaration) ResolveId(ctx context.Context) string {
@ -101,18 +108,22 @@ func (d *Declaration) Apply() (result error) {
}()
stater := d.Attributes.StateMachine()
slog.Info("Declaration.Apply()", "machine", stater, "machine.state", stater.CurrentState(), "uri", d.Attributes.URI())
switch d.Transition {
case "read":
result = stater.Trigger("read")
case "delete", "absent":
slog.Info("Declaration.Apply()", "machine", stater, "machine.state", stater.CurrentState(), "uri", d.Attributes.URI())
if stater.CurrentState() == "present" {
result = stater.Trigger("delete")
}
case "update":
if result = stater.Trigger("update"); result != nil {
return result
}
result = stater.Trigger("read")
default:
fallthrough
case "create", "present":
slog.Info("Declaration.Apply()", "machine", stater, "machine.state", stater.CurrentState(), "uri", d.Attributes.URI())
if stater.CurrentState() == "absent" || stater.CurrentState() == "unknown" {
if result = stater.Trigger("create"); result != nil {
return result
@ -150,6 +161,7 @@ func (d *Declaration) UnmarshalValue(value *DeclarationType) error {
d.Config = value.Config
newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", value.Type))
if resourceErr != nil {
slog.Info("Declaration.UnmarshalValue", "value", value, "error", resourceErr)
return resourceErr
}
d.Attributes = newResource

View File

@ -19,6 +19,15 @@ _ "net/url"
type ResourceMap[Value any] map[string]Value
func (rm ResourceMap[Value]) Get(key string) (any, bool) {
v, ok := rm[key]
return v, ok
}
type ResourceMapper interface {
Get(key string) (any, bool)
}
type Document struct {
uris ResourceMap[*Declaration]
ResourceDecls []Declaration `json:"resources" yaml:"resources"`
@ -66,10 +75,10 @@ func (d *Document) Clone() *Document {
func (d *Document) Load(r io.Reader) (err error) {
c := codec.NewYAMLDecoder(r)
err = c.Decode(d)
slog.Info("Document.Load()", "error", err)
if err == nil {
for i := range d.ResourceDecls {
d.ResourceDecls[i].SetDocument(d)
d.ResourceDecls[i].SetConfig(d.config)
}
}
return
@ -158,22 +167,23 @@ func (d *Document) MapResourceURI(uri string, declaration *Declaration) {
}
func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration Resource) {
decl := NewDeclaration()
slog.Info("Document.AddResourceDeclaration()", "type", resourceType, "resource", resourceDeclaration)
decl := NewDeclarationFromDocument(d)
decl.Type = TypeName(resourceType)
decl.Attributes = resourceDeclaration
decl.SetDocument(d)
d.ResourceDecls = append(d.ResourceDecls, *decl)
d.MapResourceURI(decl.Attributes.URI(), decl)
decl.SetDocument(d)
}
func (d *Document) AddResource(uri string) error {
decl := NewDeclaration()
decl := NewDeclarationFromDocument(d)
if e := decl.SetURI(uri); e != nil {
return e
}
decl.SetDocument(d)
d.ResourceDecls = append(d.ResourceDecls, *decl)
d.MapResourceURI(decl.Attributes.URI(), decl)
decl.SetDocument(d)
return nil
}
@ -228,3 +238,31 @@ func (d *Document) Diff(with *Document, output io.Writer) (returnOutput string,
}
return "", nil
}
func (d *Document) UnmarshalYAML(value *yaml.Node) error {
type decodeDocument Document
t := (*decodeDocument)(d)
if unmarshalDocumentErr := value.Decode(t); unmarshalDocumentErr != nil {
return unmarshalDocumentErr
}
for i := range d.ResourceDecls {
d.ResourceDecls[i].SetDocument(d)
d.MapResourceURI(d.ResourceDecls[i].Attributes.URI(), &d.ResourceDecls[i])
}
return nil
}
func (d *Document) UnmarshalJSON(data []byte) error {
type decodeDocument Document
t := (*decodeDocument)(d)
if unmarshalDocumentErr := json.Unmarshal(data, t); unmarshalDocumentErr != nil {
return unmarshalDocumentErr
}
for i := range d.ResourceDecls {
d.ResourceDecls[i].SetDocument(d)
d.MapResourceURI(d.ResourceDecls[i].Attributes.URI(), &d.ResourceDecls[i])
}
return nil
}

View File

@ -28,6 +28,7 @@ type Exec struct {
config ConfigurationValueGetter
// state attributes
State string `yaml:"state"`
Resources ResourceMapper `yaml:"-" json:"-"`
}
func init() {
@ -41,6 +42,10 @@ func NewExec() *Exec {
return &Exec{}
}
func (x *Exec) SetResourceMapper(resources ResourceMapper) {
x.Resources = resources
}
func (x *Exec) Clone() Resource {
return &Exec {
Id: x.Id,

View File

@ -24,8 +24,10 @@ import (
"strings"
)
// Describes the type of file the resource represents
type FileType string
// Supported file types
const (
RegularFile FileType = "regular"
DirectoryFile FileType = "directory"
@ -50,7 +52,15 @@ func init() {
})
}
// Manage the state of file system objects
/*
Manage the state of file system objects
The file content may be serialized directly in the `Content` field
or the `ContentSourceRef/sourceref` may be used to refer to the source
of the content from which to stream the content.
The `SerializeContent` the flag allows forcing the content to be serialized in the output.
*/
type File struct {
stater machine.Stater `json:"-" yaml:"-"`
normalizePath bool `json:"-" yaml:"-"`
@ -72,6 +82,7 @@ type File struct {
State string `json:"state,omitempty" yaml:"state,omitempty"`
SerializeContent bool `json:"serializecontent,omitempty" yaml:"serializecontent,omitempty"`
config ConfigurationValueGetter
Resources ResourceMapper `json:"-" yaml:"-"`
}
type ResourceFileInfo struct {
@ -92,6 +103,10 @@ func NewNormalizedFile() *File {
return f
}
func (f *File) SetResourceMapper(resources ResourceMapper) {
f.Resources = resources
}
func (f *File) Clone() Resource {
return &File {
normalizePath: f.normalizePath,
@ -305,7 +320,7 @@ func (f *File) Create(ctx context.Context) error {
f.Size = 0
var contentReader io.ReadCloser
if len(f.Content) == 0 && len(f.ContentSourceRef) != 0 {
if refReader, err := f.ContentSourceRef.ContentReaderStream(); err == nil {
if refReader, err := f.ContentSourceRef.Lookup(nil).ContentReaderStream(); err == nil {
contentReader = refReader
} else {
return err

View File

@ -0,0 +1,18 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
)
type FilterTerm string
type FilterDefinition struct {
FilterTerms []FilterTerm `yaml:"term" json:"term"`
}
func NewFilterDefinition() *FilterDefinition {
return &FilterDefinition{}
}

407
internal/resource/group.go Normal file
View File

@ -0,0 +1,407 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
"context"
"fmt"
"gopkg.in/yaml.v3"
_ "log/slog"
"net/url"
_ "os"
"os/exec"
"os/user"
"io"
"encoding/json"
"errors"
"strings"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
"decl/internal/command"
)
type decodeGroup Group
type GroupType string
const (
GroupTypeAddGroup = "addgroup"
GroupTypeGroupAdd = "groupadd"
)
var ErrUnsupportedGroupType error = errors.New("The GroupType is not supported on this system")
var ErrInvalidGroupType error = errors.New("invalid GroupType value")
var SystemGroupType GroupType = FindSystemGroupType()
type Group struct {
stater machine.Stater `json:"-" yaml:"-"`
Name string `json:"name" yaml:"name"`
GID string `json:"gid,omitempty" yaml:"gid,omitempty"`
GroupType GroupType `json:"-" yaml:"-"`
CreateCommand *command.Command `json:"-" yaml:"-"`
ReadCommand *command.Command `json:"-" yaml:"-"`
UpdateCommand *command.Command `json:"-" yaml:"-"`
DeleteCommand *command.Command `json:"-" yaml:"-"`
State string `json:"state,omitempty" yaml:"state,omitempty"`
config ConfigurationValueGetter
Resources ResourceMapper `json:"-" yaml:"-"`
}
func NewGroup() *Group {
return &Group{}
}
func init() {
ResourceTypes.Register([]string{"group"}, func(u *url.URL) Resource {
group := NewGroup()
group.Name = u.Hostname()
group.GID = LookupGIDString(u.Hostname())
if _, addGroupPathErr := exec.LookPath("addgroup"); addGroupPathErr == nil {
group.GroupType = GroupTypeAddGroup
}
if _, pathErr := exec.LookPath("groupadd"); pathErr == nil {
group.GroupType = GroupTypeGroupAdd
}
group.CreateCommand, group.ReadCommand, group.UpdateCommand, group.DeleteCommand = group.GroupType.NewCRUD()
return group
})
}
func FindSystemGroupType() GroupType {
for _, groupType := range []GroupType{GroupTypeAddGroup, GroupTypeGroupAdd} {
c := groupType.NewCreateCommand()
if c.Exists() {
return groupType
}
}
return GroupTypeAddGroup
}
func (g *Group) SetResourceMapper(resources ResourceMapper) {
g.Resources = resources
}
func (g *Group) Clone() Resource {
newg := &Group {
Name: g.Name,
GID: g.GID,
State: g.State,
GroupType: g.GroupType,
}
newg.CreateCommand, newg.ReadCommand, newg.UpdateCommand, newg.DeleteCommand = g.GroupType.NewCRUD()
return newg
}
func (g *Group) StateMachine() machine.Stater {
if g.stater == nil {
g.stater = StorageMachine(g)
}
return g.stater
}
func (g *Group) Notify(m *machine.EventMessage) {
ctx := context.Background()
switch m.On {
case machine.ENTERSTATEEVENT:
switch m.Dest {
case "start_create":
if e := g.Create(ctx); e == nil {
if triggerErr := g.stater.Trigger("created"); triggerErr == nil {
return
}
}
g.State = "absent"
case "present":
g.State = "present"
}
case machine.EXITSTATEEVENT:
}
}
func (g *Group) SetURI(uri string) error {
resourceUri, e := url.Parse(uri)
if e == nil {
if resourceUri.Scheme == "group" {
g.Name = resourceUri.Hostname()
} else {
e = fmt.Errorf("%w: %s is not a group", ErrInvalidResourceURI, uri)
}
}
return e
}
func (g *Group) URI() string {
return fmt.Sprintf("group://%s", g.Name)
}
func (g *Group) UseConfig(config ConfigurationValueGetter) {
g.config = config
}
func (g *Group) ResolveId(ctx context.Context) string {
return LookupUIDString(g.Name)
}
func (g *Group) Validate() error {
return fmt.Errorf("failed")
}
func (g *Group) Apply() error {
switch g.State {
case "present":
_, NoGroupExists := LookupGID(g.Name)
if NoGroupExists != nil {
cmdErr := g.Create(context.Background())
return cmdErr
}
case "absent":
cmdErr := g.Delete()
return cmdErr
}
return nil
}
func (g *Group) Load(r io.Reader) error {
return codec.NewYAMLDecoder(r).Decode(g)
}
func (g *Group) LoadDecl(yamlResourceDeclaration string) error {
return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(g)
}
func (g *Group) Type() string { return "group" }
func (g *Group) Create(ctx context.Context) (error) {
_, err := g.CreateCommand.Execute(g)
if err != nil {
return err
}
_,e := g.Read(ctx)
return e
}
func (g *Group) Read(ctx context.Context) ([]byte, error) {
exErr := g.ReadCommand.Extractor(nil, g)
if exErr != nil {
g.State = "absent"
}
if yaml, yamlErr := yaml.Marshal(g); yamlErr != nil {
return yaml, yamlErr
} else {
return yaml, exErr
}
}
func (g *Group) Delete() (error) {
_, err := g.DeleteCommand.Execute(g)
if err != nil {
return err
}
return err
}
func (g *Group) UnmarshalJSON(data []byte) error {
if unmarshalErr := json.Unmarshal(data, (*decodeGroup)(g)); unmarshalErr != nil {
return unmarshalErr
}
g.CreateCommand, g.ReadCommand, g.UpdateCommand, g.DeleteCommand = g.GroupType.NewCRUD()
return nil
}
func (g *Group) UnmarshalYAML(value *yaml.Node) error {
if unmarshalErr := value.Decode((*decodeGroup)(g)); unmarshalErr != nil {
return unmarshalErr
}
g.CreateCommand, g.ReadCommand, g.UpdateCommand, g.DeleteCommand = g.GroupType.NewCRUD()
return nil
}
func (g *GroupType) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) {
switch *g {
case GroupTypeGroupAdd:
return NewGroupAddCreateCommand(), NewGroupReadCommand(), NewGroupUpdateCommand(), NewGroupDelDeleteCommand()
case GroupTypeAddGroup:
return NewAddGroupCreateCommand(), NewGroupReadCommand(), NewGroupUpdateCommand(), NewDelGroupDeleteCommand()
default:
if _, addGroupPathErr := exec.LookPath("addgroup"); addGroupPathErr == nil {
*g = GroupTypeAddGroup
return NewAddGroupCreateCommand(), NewGroupReadCommand(), NewGroupUpdateCommand(), NewDelGroupDeleteCommand()
}
if _, pathErr := exec.LookPath("groupadd"); pathErr == nil {
*g = GroupTypeGroupAdd
return NewGroupAddCreateCommand(), NewGroupReadCommand(), NewGroupUpdateCommand(), NewGroupDelDeleteCommand()
}
return NewGroupAddCreateCommand(), NewGroupReadCommand(), NewGroupUpdateCommand(), NewGroupDelDeleteCommand()
}
}
func (g *GroupType) NewCreateCommand() (create *command.Command) {
switch *g {
case GroupTypeGroupAdd:
return NewGroupAddCreateCommand()
case GroupTypeAddGroup:
return NewAddGroupCreateCommand()
default:
}
return nil
}
func (g *GroupType) NewReadGroupsCommand() *command.Command {
return NewReadGroupsCommand()
}
func (g *GroupType) UnmarshalValue(value string) error {
switch value {
case string(GroupTypeGroupAdd), string(GroupTypeAddGroup):
*g = GroupType(value)
return nil
default:
return errors.New("invalid GroupType value")
}
}
func (g *GroupType) UnmarshalJSON(data []byte) error {
var s string
if unmarshalGroupTypeErr := json.Unmarshal(data, &s); unmarshalGroupTypeErr != nil {
return unmarshalGroupTypeErr
}
return g.UnmarshalValue(s)
}
func (g *GroupType) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
return g.UnmarshalValue(s)
}
func NewGroupAddCreateCommand() *command.Command {
c := command.NewCommand()
c.Path = "groupadd"
c.Args = []command.CommandArg{
command.CommandArg("{{ if .GID }}-g {{ .GID }}{{ end }}"),
command.CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
return nil
/*
for _,line := range strings.Split(string(out), "\n") {
if line == "iptables: Chain already exists." {
return nil
}
}
return fmt.Errorf(string(out))
*/
}
return c
}
func NewAddGroupCreateCommand() *command.Command {
c := command.NewCommand()
c.Path = "addgroup"
c.Args = []command.CommandArg{
command.CommandArg("{{ if .GID }}-g {{ .GID }}{{ end }}"),
command.CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
return nil
/*
for _,line := range strings.Split(string(out), "\n") {
if line == "iptables: Chain already exists." {
return nil
}
}
return fmt.Errorf(string(out))
*/
}
return c
}
func NewGroupReadCommand() *command.Command {
c := command.NewCommand()
c.Extractor = func(out []byte, target any) error {
g := target.(*Group)
g.State = "absent"
var readGroup *user.Group
var e error
if g.Name != "" {
readGroup, e = user.LookupGroup(g.Name)
} else {
if g.GID != "" {
readGroup, e = user.LookupGroupId(g.GID)
}
}
if e == nil {
g.Name = readGroup.Name
g.GID = readGroup.Gid
if g.GID != "" {
g.State = "present"
}
}
return e
}
return c
}
func NewGroupUpdateCommand() *command.Command {
return nil
}
func NewGroupDelDeleteCommand() *command.Command {
c := command.NewCommand()
c.Path = "groupdel"
c.Args = []command.CommandArg{
command.CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
return nil
}
return c
}
func NewDelGroupDeleteCommand() *command.Command {
c := command.NewCommand()
c.Path = "delgroup"
c.Args = []command.CommandArg{
command.CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
return nil
}
return c
}
func NewReadGroupsCommand() *command.Command {
c := command.NewCommand()
c.Path = "getent"
c.Args = []command.CommandArg{
command.CommandArg("passwd"),
}
c.Extractor = func(out []byte, target any) error {
Groups := target.(*[]*Group)
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
lineIndex := 0
for _, line := range lines {
groupRecord := strings.Split(strings.TrimSpace(line), ":")
if len(*Groups) <= lineIndex + 1 {
*Groups = append(*Groups, NewGroup())
}
g := (*Groups)[lineIndex]
g.Name = groupRecord[0]
g.GID = groupRecord[2]
g.State = "present"
g.GroupType = SystemGroupType
lineIndex++
}
return nil
}
return c
}

View File

@ -0,0 +1,75 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
"context"
_ "encoding/json"
"github.com/stretchr/testify/assert"
_ "io"
_ "net/http"
_ "net/http/httptest"
_ "net/url"
_ "os"
_ "strings"
"testing"
)
func TestNewGroupResource(t *testing.T) {
g := NewGroup()
assert.NotNil(t, g)
}
func TestReadGroup(t *testing.T) {
ctx := context.Background()
decl := `
name: "syslog"
`
g := NewGroup()
e := g.LoadDecl(decl)
assert.Nil(t, e)
assert.Equal(t, "syslog", g.Name)
_, readErr := g.Read(ctx)
assert.Nil(t, readErr)
assert.Equal(t, "111", g.GID)
}
func TestCreateGroup(t *testing.T) {
decl := `
name: "testgroup"
gid: 12001
state: present
`
g := NewGroup()
e := g.LoadDecl(decl)
assert.Equal(t, nil, e)
assert.Equal(t, "testgroup", g.Name)
g.CreateCommand.Executor = func(value any) ([]byte, error) {
return []byte(``), nil
}
g.ReadCommand.Extractor = func(out []byte, target any) error {
return nil
}
g.DeleteCommand.Executor = func(value any) ([]byte, error) {
return nil, nil
}
applyErr := g.Apply()
assert.Nil(t, applyErr)
assert.Equal(t, "12001", g.GID)
g.State = "absent"
applyDeleteErr := g.Apply()
assert.Nil(t, applyDeleteErr)
}

View File

@ -44,12 +44,17 @@ type HTTP struct {
StatusCode int `yaml:"statuscode,omitempty" json:"statuscode,omitempty"`
State string `yaml:"state,omitempty" json:"state,omitempty"`
config ConfigurationValueGetter
Resources ResourceMapper `yaml:"-" json:"-"`
}
func NewHTTP() *HTTP {
return &HTTP{ client: &http.Client{} }
}
func (h *HTTP) SetResourceMapper(resources ResourceMapper) {
h.Resources = resources
}
func (h *HTTP) Clone() Resource {
return &HTTP {
client: h.client,

View File

@ -129,6 +129,7 @@ type Iptable struct {
DeleteCommand *command.Command `yaml:"-" json:"-"`
config ConfigurationValueGetter
Resources ResourceMapper `yaml:"-" json:"-"`
}
func NewIptable() *Iptable {
@ -137,6 +138,10 @@ func NewIptable() *Iptable {
return i
}
func (i *Iptable) SetResourceMapper(resources ResourceMapper) {
i.Resources = resources
}
func (i *Iptable) Clone() Resource {
newIpt := &Iptable {
Id: i.Id,

View File

@ -126,6 +126,7 @@ type NetworkRoute struct {
State string `json:"state" yaml:"state"`
config ConfigurationValueGetter
Resources ResourceMapper `json:"-" yaml:"-"`
}
func NewNetworkRoute() *NetworkRoute {
@ -134,6 +135,10 @@ func NewNetworkRoute() *NetworkRoute {
return n
}
func (n *NetworkRoute) SetResourceMapper(resources ResourceMapper) {
n.Resources = resources
}
func (n *NetworkRoute) Clone() Resource {
newn := &NetworkRoute {
Id: n.Id,

View File

@ -77,3 +77,12 @@ func LookupGID(groupName string) (int, error) {
return gid, nil
}
func LookupGIDString(groupName string) string {
group, groupLookupErr := user.LookupGroup(groupName)
if groupLookupErr != nil {
return ""
}
return group.Gid
}

View File

@ -32,6 +32,9 @@ const (
PackageTypeYum PackageType = "yum"
)
var ErrUnsupportedPackageType error = errors.New("The PackageType is not supported on this system")
var ErrInvalidPackageType error = errors.New("invalid PackageType value")
var SystemPackageType PackageType = FindSystemPackageType()
type Package struct {
@ -49,6 +52,7 @@ type Package struct {
// state attributes
State string `yaml:"state,omitempty" json:"state,omitempty"`
config ConfigurationValueGetter
Resources ResourceMapper `yaml:"-" json:"-"`
}
func init() {
@ -72,6 +76,10 @@ func NewPackage() *Package {
return &Package{ PackageType: SystemPackageType }
}
func (p *Package) SetResourceMapper(resources ResourceMapper) {
p.Resources = resources
}
func (p *Package) Clone() Resource {
newp := &Package {
Name: p.Name,
@ -228,15 +236,19 @@ func (p *Package) LoadDecl(yamlResourceDeclaration string) error {
func (p *Package) Type() string { return "package" }
func (p *Package) Read(ctx context.Context) ([]byte, error) {
out, err := p.ReadCommand.Execute(p)
if err != nil {
return nil, err
if p.ReadCommand.Exists() {
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)
} else {
return nil, ErrUnsupportedPackageType
}
exErr := p.ReadCommand.Extractor(out, p)
if exErr != nil {
return nil, exErr
}
return yaml.Marshal(p)
}
func (p *Package) UnmarshalJSON(data []byte) error {
@ -325,7 +337,7 @@ func (p *PackageType) UnmarshalValue(value string) error {
*p = PackageType(value)
return nil
default:
return errors.New("invalid PackageType value")
return ErrInvalidPackageType
}
}

View File

@ -42,6 +42,7 @@ type: apk
p := NewPackage()
assert.NotNil(t, p)
m := &MockCommand{
CommandExists: func() error { return nil },
Executor: func(value any) ([]byte, error) {
return nil, nil
},
@ -134,3 +135,27 @@ Version: 1.2.2
assert.Equal(t, "1.2.2", p.Version)
assert.Nil(t, p.Validate())
}
func TestPackageTypeErr(t *testing.T) {
decl := `
name: vim
source: vim-8.2.3995-1ubuntu2.17.deb
type: deb
`
p := NewPackage()
assert.NotNil(t, p)
loadErr := p.LoadDecl(decl)
assert.Nil(t, loadErr)
p.ReadCommand = NewDebReadCommand()
p.ReadCommand.CommandExists = func() error { return command.ErrUnknownCommand }
p.ReadCommand.Executor = func(value any) ([]byte, error) {
return []byte(`
Package: vim
Version: 1.2.2
`), nil
}
_, readErr := p.Read(context.Background())
assert.ErrorIs(t, readErr, ErrUnsupportedPackageType)
}

522
internal/resource/pki.go Normal file
View File

@ -0,0 +1,522 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
"context"
"errors"
"fmt"
"log/slog"
"gopkg.in/yaml.v3"
"net/url"
"path/filepath"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
"decl/internal/ext"
"decl/internal/transport"
"crypto/x509"
"crypto/x509/pkix"
"crypto/rsa"
"crypto/rand"
"encoding/pem"
"encoding/json"
"time"
"math/big"
"io"
"strings"
)
// Describes the type of certificate file the resource represents
type EncodingType string
// Supported file types
const (
EncodingTypePem EncodingType = "pem"
)
var ErrPKIInvalidEncodingType error = errors.New("Invalid EncodingType")
var ErrPKIFailedDecodingPemBlock error = errors.New("Failed decoding pem block")
func init() {
ResourceTypes.Register([]string{"pki"}, func(u *url.URL) Resource {
k := NewPKI()
ref := ResourceReference(filepath.Join(u.Hostname(), u.Path))
if len(ref) > 0 {
k.PrivateKeyRef = ref
}
return k
})
}
type Certificate struct {
*x509.Certificate `yaml:",inline" json:",inline"`
Raw []byte `yaml:"-" json:"-"`
RawTBSCertificate []byte `yaml:"-" json:"-"`
RawSubjectPublicKeyInfo []byte `yaml:"-" json:"-"`
RawSubject []byte `yaml:"-" json:"-"`
RawIssuer []byte `yaml:"-" json:"-"`
}
type PKI struct {
stater machine.Stater `json:"-" yaml:"-"`
PrivateKeyPem string `json:"privatekey,omitempty" yaml:"privatekey,omitempty"`
PublicKeyPem string `json:"publickey,omitempty" yaml:"publickey,omitempty"`
CertificatePem string `json:"certificate,omitempty" yaml:"certificate,omitempty"`
PrivateKeyRef ResourceReference `json:"privatekeyref,omitempty" yaml:"privatekeyref,omitempty"` // Describes a resource URI to read/write the private key content
PublicKeyRef ResourceReference `json:"publickeyref,omitempty" yaml:"publickeyref,omitempty"` // Describes a resource URI to read/write the public key content
CertificateRef ResourceReference `json:"certificateref,omitempty" yaml:"certificateref,omitempty"` // Describes a resource URI to read/write the certificate content
SignedByRef ResourceReference `json:"signedbyref,omitempty" yaml:"signedbyref,omitempty"` // Describes a resource URI for the signing cert
privateKey *rsa.PrivateKey `json:"-" yaml:"-"`
publicKey *rsa.PublicKey `json:"-" yaml:"-"`
Values *Certificate `json:"values,omitempty" yaml:"values,omitempty"`
Certificate *x509.Certificate `json:"-" yaml:"-"`
Bits int `json:"bits" yaml:"bits"`
EncodingType EncodingType `json:"type" yaml:"type"`
//State string `json:"state,omitempty" yaml:"state,omitempty"`
config ConfigurationValueGetter
Resources ResourceMapper `json:"-" yaml:"-"`
}
func NewPKI() *PKI {
p := &PKI{ EncodingType: EncodingTypePem, Bits: 2048 }
return p
}
func (k *PKI) SetResourceMapper(resources ResourceMapper) {
slog.Info("PKI.SetResourceMapper()", "resources", resources)
k.Resources = resources
}
func (k *PKI) Clone() Resource {
return &PKI {
EncodingType: k.EncodingType,
//State: k.State,
}
}
func (k *PKI) StateMachine() machine.Stater {
if k.stater == nil {
k.stater = StorageMachine(k)
}
return k.stater
}
func (k *PKI) Notify(m *machine.EventMessage) {
ctx := context.Background()
slog.Info("Notify()", k.Type(), k, "m", m)
switch m.On {
case machine.ENTERSTATEEVENT:
switch m.Dest {
case "start_read":
if _,readErr := k.Read(ctx); readErr == nil {
if triggerErr := k.StateMachine().Trigger("state_read"); triggerErr == nil {
return
} else {
//k.State = "absent"
panic(triggerErr)
}
} else {
//k.State = "absent"
panic(readErr)
}
case "start_create":
if ! (k.PrivateKeyRef.Exists() || k.PublicKeyRef.Exists() || k.CertificateRef.Exists()) {
if e := k.Create(ctx); e == nil {
if triggerErr := k.StateMachine().Trigger("created"); triggerErr == nil {
return
}
}
}
//k.State = "absent"
case "start_update":
if e := k.Update(ctx); e == nil {
if triggerErr := k.StateMachine().Trigger("updated"); triggerErr == nil {
return
}
}
case "start_delete":
if deleteErr := k.Delete(ctx); deleteErr == nil {
if triggerErr := k.StateMachine().Trigger("deleted"); triggerErr == nil {
return
} else {
//k.State = "present"
panic(triggerErr)
}
} else {
//k.State = "present"
panic(deleteErr)
}
case "absent":
//k.State = "absent"
case "present", "updated", "created", "read":
//k.State = "present"
}
case machine.EXITSTATEEVENT:
}
}
func (k *PKI) URI() string {
u := k.PrivateKeyRef.Parse()
return fmt.Sprintf("pki://%s", filepath.Join(u.Hostname(), u.RequestURI()))
}
func (k *PKI) SetURI(uri string) error {
resourceUri, e := url.Parse(uri)
if e == nil {
if resourceUri.Scheme == "pki" {
k.PrivateKeyRef = ResourceReference(fmt.Sprintf("pki://%s", filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI())))
} else {
e = fmt.Errorf("%w: %s is not a cert", ErrInvalidResourceURI, uri)
}
}
return e
}
func (k *PKI) UseConfig(config ConfigurationValueGetter) {
k.config = config
}
func (k *PKI) Validate() error {
return fmt.Errorf("failed")
}
func (k *PKI) Apply() error {
/*
ctx := context.Background()
switch k.State {
case "absent":
return k.Delete(ctx)
case "present":
return k.Create(ctx)
}
*/
return nil
}
func (k *PKI) LoadDecl(yamlResourceDeclaration string) (err error) {
d := codec.NewYAMLStringDecoder(yamlResourceDeclaration)
err = d.Decode(k)
return
}
func (k *PKI) ResolveId(ctx context.Context) string {
return string(k.PrivateKeyRef)
}
func (k *PKI) GenerateKey() (err error) {
k.privateKey, err = rsa.GenerateKey(rand.Reader, k.Bits)
return
}
func (k *PKI) PublicKey() {
if k.privateKey != nil {
k.publicKey = k.privateKey.Public().(*rsa.PublicKey)
}
}
func (k *PKI) Encode() {
var privFileStream, pubFileStream io.WriteCloser
switch k.EncodingType {
case EncodingTypePem:
if len(k.PrivateKeyRef) > 0 {
privFileStream, _ = k.PrivateKeyRef.Lookup(k.Resources).ContentWriterStream()
} else {
var privateKeyContent strings.Builder
privFileStream = ext.WriteNopCloser(&privateKeyContent)
defer func() { k.PrivateKeyPem = privateKeyContent.String() }()
}
defer privFileStream.Close()
privEncodeErr := pem.Encode(privFileStream, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k.privateKey)})
if privEncodeErr != nil {
panic(privEncodeErr)
}
if len(k.PublicKeyRef) > 0 {
pubFileStream, _ = k.PublicKeyRef.Lookup(k.Resources).ContentWriterStream()
} else {
var publicKeyContent strings.Builder
pubFileStream = ext.WriteNopCloser(&publicKeyContent)
defer func() { k.PublicKeyPem = publicKeyContent.String() }()
}
defer pubFileStream.Close()
pubEncodeErr := pem.Encode(pubFileStream, &pem.Block{Type: "RSA PUBLIC KEY", Bytes: x509.MarshalPKCS1PublicKey(k.publicKey)})
if pubEncodeErr != nil {
panic(pubEncodeErr)
}
}
}
func (k *PKI) Decode() {
slog.Info("PKI.Decode()", "privatekey", k.PrivateKeyRef, "publickey", k.PublicKeyRef, "certificate", k.CertificateRef)
var err error
switch k.EncodingType {
case EncodingTypePem:
if len(k.PrivateKeyRef) > 0 && k.PrivateKeyRef.Exists() {
privReader := k.PrivateKeyRef.Lookup(k.Resources)
privFileStream, _ := privReader.ContentReaderStream()
defer privFileStream.Close()
PrivateKeyPemData, readErr := io.ReadAll(privFileStream)
if readErr != nil {
panic(readErr)
}
k.PrivateKeyPem = string(PrivateKeyPemData)
block, _ := pem.Decode(PrivateKeyPemData)
if block != nil {
k.privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
panic(err)
}
} else {
panic(ErrPKIFailedDecodingPemBlock)
}
}
slog.Info("PKI.Decode() decoded private key", "error", err)
if len(k.PublicKeyRef) > 0 && k.PublicKeyRef.Exists() {
pubReader := k.PublicKeyRef.Lookup(k.Resources)
pubFileStream, _ := pubReader.ContentReaderStream()
defer pubFileStream.Close()
PublicKeyPemData, readErr := io.ReadAll(pubFileStream)
if readErr != nil {
panic(err)
}
k.PublicKeyPem = string(PublicKeyPemData)
block, _ := pem.Decode(PublicKeyPemData)
if block != nil {
k.publicKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
if err != nil {
panic(err)
}
} else {
panic(ErrPKIFailedDecodingPemBlock)
}
}
slog.Info("PKI.Decode() decoded public key", "publickey", k.PublicKeyPem, "error", err)
if len(k.CertificateRef) > 0 && k.CertificateRef.Exists() {
certReader := k.CertificateRef.Lookup(k.Resources)
certFileStream, _ := certReader.ContentReaderStream()
if certFileStream != nil {
defer certFileStream.Close()
CertificatePemData, readErr := io.ReadAll(certFileStream)
if readErr != nil {
panic(readErr)
}
slog.Info("PKI.Decode() certificate", "pem", CertificatePemData, "error", err)
k.CertificatePem = string(CertificatePemData)
block, _ := pem.Decode(CertificatePemData)
if block != nil {
k.Certificate, err = x509.ParseCertificate(block.Bytes)
if err != nil {
panic(err)
}
} else {
panic(ErrPKIFailedDecodingPemBlock)
}
}
}
}
slog.Info("PKI.Decode()", "error", err)
}
func (k *PKI) ContentReaderStream() (*transport.Reader, error) {
return nil, nil
}
func (k *PKI) ContentWriterStream() (*transport.Writer, error) {
return nil, nil
}
func (k *PKI) SignedBy() (cert *x509.Certificate, pub *rsa.PublicKey, priv *rsa.PrivateKey) {
if len(k.SignedByRef) > 0 {
r := k.SignedByRef.Dereference(k.Resources)
if r != nil {
pkiResource := r.(*PKI)
return pkiResource.Certificate, pkiResource.publicKey, pkiResource.privateKey
}
}
return nil, nil, nil
}
func (k *PKI) CertConfig() (certTemplate *x509.Certificate) {
if k.Values != nil {
certTemplate = k.Values.Certificate
} else {
var caEpoch int64 = 1721005200000
certTemplate = &x509.Certificate {
SerialNumber: big.NewInt(time.Now().UnixMilli() - caEpoch),
Subject: pkix.Name {
Organization: []string{""},
Country: []string{""},
Province: []string{""},
Locality: []string{""},
StreetAddress: []string{""},
PostalCode: []string{""},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
}
if k.config != nil {
if value, configErr := k.config.GetValue("certtemplate"); configErr == nil {
certTemplate = value.(*x509.Certificate)
}
}
slog.Info("PKI.CertConfig()", "template", certTemplate)
return
}
func (k *PKI) GenerateCertificate() (error) {
var signingCert *x509.Certificate
var signingPubKey *rsa.PublicKey
var signingPrivKey *rsa.PrivateKey
var certFileStream io.WriteCloser
certTemplate := k.CertConfig()
signingCert, signingPubKey, signingPrivKey = k.SignedBy()
if signingCert != nil && signingPubKey != nil && signingPrivKey != nil {
slog.Info("PKI.Certificate()", "signedby", k.SignedByRef)
} else {
signingCert = certTemplate
signingPubKey = k.publicKey
signingPrivKey = k.privateKey
}
if k.Certificate != nil {
certTemplate = k.Certificate
}
cert, err := x509.CreateCertificate(rand.Reader, certTemplate, signingCert, signingPubKey, signingPrivKey)
if err != nil {
slog.Error("PKI.Certificate() - x509.CreateCertificate", "cert", cert, "error", err)
return err
}
if len(k.CertificateRef) > 0 {
certFileStream, _ = k.CertificateRef.Lookup(k.Resources).ContentWriterStream()
} else {
var certContent strings.Builder
certFileStream = ext.WriteNopCloser(&certContent)
defer func() { k.CertificatePem = certContent.String() }()
}
defer certFileStream.Close()
certEncodeErr := pem.Encode(certFileStream, &pem.Block{Type: "CERTIFICATE", Bytes: cert})
return certEncodeErr
}
func (k *PKI) Create(ctx context.Context) (err error) {
if err = k.GenerateKey(); err == nil {
k.PublicKey()
k.Encode()
if err = k.GenerateCertificate(); err == nil {
return
}
}
return
}
func (f *PKI) Delete(ctx context.Context) error {
return nil
}
func (k *PKI) Read(ctx context.Context) ([]byte, error) {
k.Decode()
return yaml.Marshal(k)
}
func (k *PKI) Update(ctx context.Context) (err error) {
if err = k.GenerateKey(); err == nil {
k.PublicKey()
k.Encode()
if err = k.GenerateCertificate(); err == nil {
return
}
}
return
}
func (k *PKI) Type() string { return "pki" }
/*
func (k *PKI) UnmarshalValue(value *PKIContent) error {
d.Type = value.Type
d.Transition = value.Transition
d.Config = value.Config
newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", value.Type))
if resourceErr != nil {
slog.Info("Declaration.UnmarshalValue", "value", value, "error", resourceErr)
return resourceErr
}
d.Attributes = newResource
return nil
}
func (k *PKI) UnmarshalYAML(value *yaml.Node) error {
t := &DeclarationType{}
if unmarshalResourceTypeErr := value.Decode(t); unmarshalResourceTypeErr != nil {
return unmarshalResourceTypeErr
}
if err := d.UnmarshalValue(t); err != nil {
return err
}
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
}
if err := d.UnmarshalValue(t); err != nil {
return err
}
resourceAttrs := struct {
Attributes Resource `json:"attributes"`
}{Attributes: d.Attributes}
if unmarshalAttributesErr := json.Unmarshal(data, &resourceAttrs); unmarshalAttributesErr != nil {
return unmarshalAttributesErr
}
return nil
}
*/
func (t *EncodingType) UnmarshalValue(value string) error {
switch value {
case string(EncodingTypePem):
*t = EncodingType(value)
return nil
default:
return ErrPKIInvalidEncodingType
}
}
func (t *EncodingType) UnmarshalJSON(data []byte) error {
var s string
if unmarshalEncodingTypeErr := json.Unmarshal(data, &s); unmarshalEncodingTypeErr != nil {
return unmarshalEncodingTypeErr
}
return t.UnmarshalValue(s)
}
func (t *EncodingType) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
return t.UnmarshalValue(s)
}

View File

@ -0,0 +1,164 @@
// 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"
_ "os"
"decl/internal/transport"
"decl/internal/ext"
"decl/internal/codec"
"strings"
"testing"
"path/filepath"
)
type TestResourceMapper func(key string) (any, bool)
func (rm TestResourceMapper) Get(key string) (any, bool) {
return rm(key)
}
type StringContentReadWriter func() (any, error)
func (s StringContentReadWriter) ContentWriterStream() (*transport.Writer, error) {
w, e := s()
return w.(*transport.Writer), e
}
func (s StringContentReadWriter) ContentReaderStream() (*transport.Reader, error) {
r, e := s()
return r.(*transport.Reader), e
}
func TestNewPKIKeysResource(t *testing.T) {
r := NewPKI()
assert.NotNil(t, r)
}
func TestPKIPrivateKey(t *testing.T) {
r := NewPKI()
assert.NotNil(t, r)
assert.Equal(t, 2048, r.Bits)
assert.Nil(t, r.GenerateKey())
assert.NotNil(t, r.privateKey)
r.PublicKey()
assert.NotNil(t, r.publicKey)
}
func TestPKIEncodeKeys(t *testing.T) {
var privateTarget, publicTarget, certTarget strings.Builder
r := NewPKI()
assert.NotNil(t, r)
assert.Equal(t, 2048, r.Bits)
assert.Nil(t, r.GenerateKey())
assert.NotNil(t, r.privateKey)
r.PublicKey()
assert.NotNil(t, r.publicKey)
r.Resources = TestResourceMapper(func(key string) (any, bool) {
switch key {
case "buffer://privatekey":
return StringContentReadWriter(func() (any, error) {
w := &transport.Writer{}
w.SetStream(ext.WriteNopCloser(&privateTarget))
return w, nil
}), true
case "buffer://publickey":
return StringContentReadWriter(func() (any, error) {
w := &transport.Writer{}
w.SetStream(ext.WriteNopCloser(&publicTarget))
return w, nil
}), true
case "buffer://certificate":
return StringContentReadWriter(func() (any, error) {
w := &transport.Writer{}
w.SetStream(ext.WriteNopCloser(&certTarget))
return w, nil
}), true
}
return nil, false
})
r.PrivateKeyRef = ResourceReference("buffer://privatekey")
r.PublicKeyRef = ResourceReference("buffer://publickey")
r.Encode()
assert.Equal(t, "-----BEGIN RSA PRIVATE KEY-----", strings.Split(privateTarget.String(), "\n")[0])
assert.Equal(t, "-----BEGIN RSA PUBLIC KEY-----", strings.Split(publicTarget.String(), "\n")[0])
r.CertificateRef = ResourceReference("buffer://certificate")
e := r.GenerateCertificate()
assert.Nil(t, e)
assert.Equal(t, "-----BEGIN CERTIFICATE-----", strings.Split(certTarget.String(), "\n")[0])
}
func TestPKIResource(t *testing.T) {
privateKeyFile, _ := filepath.Abs(filepath.Join(TempDir, "fooprivatekey.pem"))
publicKeyFile, _ := filepath.Abs(filepath.Join(TempDir, "foopublickey.pem"))
certFile, _ := filepath.Abs(filepath.Join(TempDir, "foocert.pem"))
var resourceYaml, readResourceYaml strings.Builder
expected := fmt.Sprintf(`
privatekeyref: "file://%s"
publickeyref: "file://%s"
certificateref: "file://%s"
bits: 2048
type: pem
`, privateKeyFile, publicKeyFile, certFile)
r := NewPKI()
assert.NotNil(t, r)
assert.Equal(t, 2048, r.Bits)
assert.Nil(t, r.GenerateKey())
assert.NotNil(t, r.privateKey)
r.PublicKey()
assert.NotNil(t, r.publicKey)
r.PrivateKeyRef = ResourceReference(fmt.Sprintf("file://%s", privateKeyFile))
r.PublicKeyRef = ResourceReference(fmt.Sprintf("file://%s", publicKeyFile))
r.CertificateRef = ResourceReference(fmt.Sprintf("file://%s", certFile))
createErr := r.Create(context.Background())
assert.Nil(t, createErr)
assert.FileExists(t, privateKeyFile)
assert.FileExists(t, publicKeyFile)
assert.FileExists(t, certFile)
serializeErr := codec.FormatYaml.Serialize(r, &resourceYaml)
assert.Nil(t, serializeErr)
assert.YAMLEq(t, expected, resourceYaml.String())
read := NewPKI()
assert.NotNil(t, read)
read.PrivateKeyRef = ResourceReference(fmt.Sprintf("file://%s", privateKeyFile))
read.PublicKeyRef = ResourceReference(fmt.Sprintf("file://%s", publicKeyFile))
read.CertificateRef = ResourceReference(fmt.Sprintf("file://%s", certFile))
_, readErr := read.Read(context.Background())
assert.Nil(t, readErr)
expectedContent := fmt.Sprintf(`
privatekey: |
%s
publickey: |
%s
certificate: |
%s
privatekeyref: "file://%s"
publickeyref: "file://%s"
certificateref: "file://%s"
bits: 2048
type: pem
`, strings.Replace(read.PrivateKeyPem, "\n", "\n ", -1), strings.Replace(read.PublicKeyPem, "\n", "\n ", -1), strings.Replace(read.CertificatePem, "\n", "\n ", -1), privateKeyFile, publicKeyFile, certFile)
readSerializeErr := codec.FormatYaml.Serialize(read, &readResourceYaml)
assert.Nil(t, readSerializeErr)
assert.YAMLEq(t, expectedContent, readResourceYaml.String())
}

View File

@ -8,9 +8,10 @@ import (
_ "encoding/json"
_ "fmt"
_ "gopkg.in/yaml.v3"
_ "net/url"
"net/url"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/transport"
"log/slog"
)
type ResourceReference string
@ -29,6 +30,7 @@ type Resource interface {
ResourceReader
ResourceValidator
Clone() Resource
SetResourceMapper(resources ResourceMapper)
}
type ContentReader interface {
@ -39,6 +41,11 @@ type ContentWriter interface {
ContentWriterStream() (*transport.Writer, error)
}
type ContentReadWriter interface {
ContentReader
ContentWriter
}
type ResourceValidator interface {
Validate() error
}
@ -77,6 +84,39 @@ func NewResource(uri string) Resource {
return nil
}
// Return a Content ReadWriter for the resource referred to.
func (r ResourceReference) Lookup(look ResourceMapper) ContentReadWriter {
slog.Info("ResourceReference.Lookup()", "resourcereference", r, "resourcemapper", look)
if look != nil {
if v,ok := look.Get(string(r)); ok {
return v.(ContentReadWriter)
}
}
return r
}
func (r ResourceReference) Dereference(look ResourceMapper) Resource {
slog.Info("ResourceReference.Dereference()", "resourcereference", r, "resourcemapper", look)
if look != nil {
if v,ok := look.Get(string(r)); ok {
return v.(*Declaration).Attributes
}
}
return nil
}
func (r ResourceReference) Parse() *url.URL {
u, e := url.Parse(string(r))
if e == nil {
return u
}
return nil
}
func (r ResourceReference) Exists() bool {
return transport.ExistsURI(string(r))
}
func (r ResourceReference) ContentReaderStream() (*transport.Reader, error) {
return transport.NewReaderURI(string(r))
}

View File

@ -13,7 +13,6 @@ import (
"log/slog"
)
//go:embed schemas/*.jsonschema
//go:embed schemas/*.schema.json
var schemaFiles embed.FS
@ -22,7 +21,7 @@ type Schema struct {
}
func NewSchema(name string) *Schema {
path := fmt.Sprintf("file://schemas/%s.jsonschema", name)
path := fmt.Sprintf("file://schemas/%s.schema.json", name)
return &Schema{schema: gojsonschema.NewReferenceLoaderFileSystem(path, http.FS(schemaFiles))}
//return &Schema{schema: gojsonschema.NewReferenceLoader(path)}
@ -44,6 +43,7 @@ func (s *Schema) Validate(source string) error {
for _, err := range result.Errors() {
schemaErrors.WriteString(err.String() + "\n")
}
schemaErrors.WriteString(source)
return errors.New(schemaErrors.String())
}
return nil

View File

@ -1,5 +1,5 @@
{
"$id": "container-declaration.jsonschema",
"$id": "container-declaration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "declaration",
"type": "object",
@ -11,7 +11,7 @@
"enum": [ "container" ]
},
"attributes": {
"$ref": "container.jsonschema"
"$ref": "container.schema.json"
}
}
}

View File

@ -8,7 +8,7 @@
"properties": {
"name": {
"type": "string",
"pattern": "^[a-z]([-_a-z0-9:]{0,31})$"
"pattern": "^(?:[-0-9A-Za-z_.]+((?::[0-9]+|)(?:/[-a-z0-9._]+/[-a-z0-9._]+))|)(?:/|)(?:[-a-z0-9._]+(?:/[-a-z0-9._]+|))(:(?:[-0-9A-Za-z_.]{1,127})|)$"
}
}
}

View File

@ -8,7 +8,7 @@
"properties": {
"name": {
"type": "string",
"pattern": "^[a-z]([-_a-z0-9]{0,31})$"
"pattern": "^[a-zA-Z]([-_a-zA-Z0-9]+)$"
}
}
}

View File

@ -1,5 +1,5 @@
{
"$id": "container.jsonschema",
"$id": "container.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "container",
"description": "A docker container",

View File

@ -1,28 +0,0 @@
{
"$id": "document.jsonschema",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "document",
"type": "object",
"required": [ "resources" ],
"properties": {
"resources": {
"type": "array",
"description": "Resources list",
"items": {
"oneOf": [
{ "$ref": "package-declaration.jsonschema" },
{ "$ref": "file-declaration.jsonschema" },
{ "$ref": "http-declaration.jsonschema" },
{ "$ref": "user-declaration.jsonschema" },
{ "$ref": "exec-declaration.jsonschema" },
{ "$ref": "network-route-declaration.schema.json" },
{ "$ref": "iptable-declaration.jsonschema" },
{ "$ref": "container-declaration.jsonschema" },
{ "$ref": "container-network-declaration.schema.json" },
{ "$ref": "container-image-declaration.schema.json" }
]
}
}
}
}

View File

@ -0,0 +1,31 @@
{
"$id": "document.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "document",
"type": "object",
"required": [ "resources" ],
"properties": {
"resources": {
"type": "array",
"description": "Resources list",
"items": {
"oneOf": [
{ "$ref": "certificate-declaration.schema.json" },
{ "$ref": "container-declaration.schema.json" },
{ "$ref": "container-network-declaration.schema.json" },
{ "$ref": "container-image-declaration.schema.json" },
{ "$ref": "exec-declaration.schema.json" },
{ "$ref": "file-declaration.schema.json" },
{ "$ref": "group-declaration.schema.json" },
{ "$ref": "http-declaration.schema.json" },
{ "$ref": "iptable-declaration.schema.json" },
{ "$ref": "network-route-declaration.schema.json" },
{ "$ref": "package-declaration.schema.json" },
{ "$ref": "pki-declaration.schema.json" },
{ "$ref": "user-declaration.schema.json" }
]
}
}
}
}

View File

@ -1,5 +1,5 @@
{
"$id": "exec-declaration.jsonschema",
"$id": "exec-declaration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "exec-declaration",
"type": "object",
@ -11,7 +11,7 @@
"enum": [ "exec" ]
},
"attributes": {
"$ref": "exec.jsonschema"
"$ref": "exec.schema.json"
}
}
}

View File

@ -1,5 +1,5 @@
{
"$id": "exec.jsonschema",
"$id": "exec.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "exec",
"type": "object",

View File

@ -1,5 +1,5 @@
{
"$id": "file-declaration.jsonschema",
"$id": "file-declaration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "file-declaration",
"type": "object",
@ -15,7 +15,7 @@
"description": "Config name"
},
"attributes": {
"$ref": "file.jsonschema"
"$ref": "file.schema.json"
}
}
}

View File

@ -1,5 +1,5 @@
{
"$id": "file.jsonschema",
"$id": "file.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "file",
"type": "object",

View File

@ -0,0 +1,20 @@
{
"$id": "group-declaration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "declaration",
"type": "object",
"required": [ "type", "attributes" ],
"properties": {
"type": {
"type": "string",
"description": "Resource type name.",
"enum": [ "group" ]
},
"transition": {
"$ref": "storagetransition.schema.json"
},
"attributes": {
"$ref": "group.schema.json"
}
}
}

View File

@ -0,0 +1,18 @@
{
"$id": "group.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "group",
"description": "A group account",
"type": "object",
"required": [ "name" ],
"properties": {
"name": {
"type": "string",
"pattern": "^[_a-z]([-_a-z0-9]{0,31})$"
},
"gid": {
"type": "string",
"pattern": "^[0-9]*$"
}
}
}

View File

@ -1,5 +1,5 @@
{
"$id": "http-declaration.jsonschema",
"$id": "http-declaration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "http-declaration",
"type": "object",
@ -15,7 +15,7 @@
"description": "Config name."
},
"attributes": {
"$ref": "http.jsonschema"
"$ref": "http.schema.json"
}
}
}

View File

@ -1,5 +1,5 @@
{
"$id": "http.jsonschema",
"$id": "http.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "http",
"type": "object",

View File

@ -1,5 +1,5 @@
{
"$id": "iptable-declaration.jsonschema",
"$id": "iptable-declaration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "iptable-declaration",
"type": "object",
@ -15,7 +15,7 @@
"description": "Config name"
},
"attributes": {
"$ref": "iptable.jsonschema"
"$ref": "iptable.schema.json"
}
}
}

View File

@ -1,5 +1,5 @@
{
"$id": "iptable.jsonschema",
"$id": "iptable.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "iptable",
"type": "object",

View File

@ -1,5 +1,5 @@
{
"$id": "network-route-declaration.jsonschema",
"$id": "network-route-declaration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "network-route-declaration",
"type": "object",

View File

@ -1,5 +1,5 @@
{
"$id": "package-declaration.jsonschema",
"$id": "package-declaration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "package-declaration",
"type": "object",
@ -11,7 +11,7 @@
"enum": [ "package" ]
},
"attributes": {
"$ref": "package.jsonschema"
"$ref": "package.schema.json"
}
}
}

View File

@ -1,5 +1,5 @@
{
"$id": "package.jsonschema",
"$id": "package.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "package",
"type": "object",

View File

@ -0,0 +1,17 @@
{
"$id": "pki-declaration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "declaration",
"type": "object",
"required": [ "type", "attributes" ],
"properties": {
"type": {
"type": "string",
"description": "Resource type name.",
"enum": [ "pki" ]
},
"attributes": {
"$ref": "pki.schema.json"
}
}
}

View File

@ -0,0 +1,76 @@
{
"$id": "pki.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "pki",
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"pem"
]
},
"publickey": {
"type": "string"
},
"privatekey": {
"type": "string"
},
"certificate": {
"type": "string"
},
"SerialNumber": {
"type": "integer",
"description": "Serial number",
"minLength": 1
},
"Issuer": {
"$ref": "pkixname.schema.json"
},
"Subject": {
"$ref": "pkixname.schema.json"
},
"NotBefore": {
"type": "string",
"format": "date-time",
"description": "Cert is not valid before time in YYYY-MM-DDTHH:MM:SS.sssssssssZ format."
},
"NotAfter": {
"type": "string",
"format": "date-time",
"description": "Cert is not valid after time in YYYY-MM-DDTHH:MM:SS.sssssssssZ format."
},
"KeyUsage": {
"type": "integer",
"enum": [
1,
2,
3,
4,
5,
6,
7,
8,
9
],
"description": "Actions valid for a key. E.g. 1 = KeyUsageDigitalSignature"
},
"ExtKeyUsage": {
"type": "array",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 13
},
"description": "Extended set of actions valid for a key"
},
"BasicConstraintsValid": {
"type": "boolean",
"description": "BasicConstraintsValid indicates whether IsCA, MaxPathLen, and MaxPathLenZero are valid"
},
"IsCA": {
"type": "boolean",
"description": ""
}
}
}

View File

@ -0,0 +1,65 @@
{
"$id": "pkixname.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "pkixname",
"type": "object",
"properties": {
"Country": {
"type": "array",
"description": "Country name",
"items": {
"type": "string"
}
},
"Organization": {
"type": "array",
"description": "Organization name",
"items": {
"type": "string"
}
},
"OrganizationalUnit": {
"type": "array",
"description": "Organizational Unit name",
"items": {
"type": "string"
}
},
"Locality": {
"type": "array",
"description": "Locality name",
"items": {
"type": "string"
}
},
"Province": {
"type": "array",
"description": "Province name",
"items": {
"type": "string"
}
},
"StreetAddress": {
"type": "array",
"description": "Street address",
"items": {
"type": "string"
}
},
"PostalCode": {
"type": "array",
"description": "Postal Code",
"items": {
"type": "string"
}
},
"SerialNumber": {
"type": "string",
"description": ""
},
"CommonName": {
"type": "string",
"description": "Name"
}
}
}

View File

@ -1,5 +1,5 @@
{
"$id": "user-declaration.jsonschema",
"$id": "user-declaration.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "declaration",
"type": "object",
@ -14,7 +14,7 @@
"$ref": "storagetransition.schema.json"
},
"attributes": {
"$ref": "user.jsonschema"
"$ref": "user.schema.json"
}
}
}

View File

@ -1,5 +1,5 @@
{
"$id": "user.jsonschema",
"$id": "user.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "user",
"description": "A user account",
@ -8,7 +8,7 @@
"properties": {
"name": {
"type": "string",
"pattern": "^[a-z]([-_a-z0-9]{0,31})$"
"pattern": "^[_a-z]([-_a-z0-9]{0,31})$"
},
"uid": {
"type": "string",

View File

@ -36,6 +36,7 @@ type Service struct {
State string `yaml:"state,omitempty" json:"state,omitempty"`
config ConfigurationValueGetter
Resources ResourceMapper `yaml:"-" json:"-"`
}
func init() {
@ -107,6 +108,10 @@ func (s *Service) Validate() error {
return nil
}
func (s *Service) SetResourceMapper(resources ResourceMapper) {
s.Resources = resources
}
func (s *Service) Clone() Resource {
news := &Service{
Name: s.Name,

View File

@ -12,11 +12,12 @@ _ "os"
"os/exec"
"os/user"
"io"
"strings"
"encoding/json"
"errors"
"gitea.rosskeen.house/rosskeen.house/machine"
"strings"
"decl/internal/codec"
"decl/internal/command"
)
type decodeUser User
@ -28,6 +29,11 @@ const (
UserTypeUserAdd = "useradd"
)
var ErrUnsupportedUserType error = errors.New("The UserType is not supported on this system")
var ErrInvalidUserType error = errors.New("invalid UserType value")
var SystemUserType UserType = FindSystemUserType()
type User struct {
stater machine.Stater `json:"-" yaml:"-"`
Name string `json:"name" yaml:"name"`
@ -40,12 +46,13 @@ type User struct {
Shell string `json:"shell,omitempty" yaml:"shell,omitempty"`
UserType UserType `json:"-" yaml:"-"`
CreateCommand *Command `json:"-" yaml:"-"`
ReadCommand *Command `json:"-" yaml:"-"`
UpdateCommand *Command `json:"-" yaml:"-"`
DeleteCommand *Command `json:"-" yaml:"-"`
CreateCommand *command.Command `json:"-" yaml:"-"`
ReadCommand *command.Command `json:"-" yaml:"-"`
UpdateCommand *command.Command `json:"-" yaml:"-"`
DeleteCommand *command.Command `json:"-" yaml:"-"`
State string `json:"state,omitempty" yaml:"state,omitempty"`
config ConfigurationValueGetter
Resources ResourceMapper `json:"-" yaml:"-"`
}
func NewUser() *User {
@ -68,6 +75,20 @@ func init() {
})
}
func FindSystemUserType() UserType {
for _, userType := range []UserType{UserTypeAddUser, UserTypeUserAdd} {
c := userType.NewCreateCommand()
if c.Exists() {
return userType
}
}
return UserTypeAddUser
}
func (u *User) SetResourceMapper(resources ResourceMapper) {
u.Resources = resources
}
func (u *User) Clone() Resource {
newu := &User {
Name: u.Name,
@ -97,6 +118,30 @@ func (u *User) Notify(m *machine.EventMessage) {
switch m.On {
case machine.ENTERSTATEEVENT:
switch m.Dest {
case "start_read":
if _,readErr := u.Read(ctx); readErr == nil {
if triggerErr := u.StateMachine().Trigger("state_read"); triggerErr == nil {
return
} else {
u.State = "absent"
panic(triggerErr)
}
} else {
u.State = "absent"
panic(readErr)
}
case "start_delete":
if deleteErr := u.Delete(ctx); deleteErr == nil {
if triggerErr := u.StateMachine().Trigger("deleted"); triggerErr == nil {
return
} else {
u.State = "present"
panic(triggerErr)
}
} else {
u.State = "present"
panic(deleteErr)
}
case "start_create":
if e := u.Create(ctx); e == nil {
if triggerErr := u.stater.Trigger("created"); triggerErr == nil {
@ -104,7 +149,9 @@ func (u *User) Notify(m *machine.EventMessage) {
}
}
u.State = "absent"
case "present":
case "absent":
u.State = "absent"
case "present", "created", "read":
u.State = "present"
}
case machine.EXITSTATEEVENT:
@ -140,6 +187,7 @@ func (u *User) Validate() error {
}
func (u *User) Apply() error {
ctx := context.Background()
switch u.State {
case "present":
_, NoUserExists := LookupUID(u.Name)
@ -148,7 +196,7 @@ func (u *User) Apply() error {
return cmdErr
}
case "absent":
cmdErr := u.Delete()
cmdErr := u.Delete(ctx)
return cmdErr
}
return nil
@ -162,33 +210,6 @@ func (u *User) LoadDecl(yamlResourceDeclaration string) error {
return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(u)
}
func (u *User) AddUserCommand(args *[]string) error {
*args = append(*args, "-D")
if u.Group != "" {
*args = append(*args, "-G", u.Group)
}
if u.Home != "" {
*args = append(*args, "-h", u.Home)
}
return nil
}
func (u *User) UserAddCommand(args *[]string) error {
if u.Group != "" {
*args = append(*args, "-g", u.Group)
}
if len(u.Groups) > 0 {
*args = append(*args, "-G", strings.Join(u.Groups, ","))
}
if u.Home != "" {
*args = append(*args, "-d", u.Home)
}
if u.CreateHome {
*args = append(*args, "-m")
}
return nil
}
func (u *User) Type() string { return "user" }
func (u *User) Create(ctx context.Context) (error) {
@ -213,7 +234,7 @@ func (u *User) Read(ctx context.Context) ([]byte, error) {
}
}
func (u *User) Delete() (error) {
func (u *User) Delete(ctx context.Context) (error) {
_, err := u.DeleteCommand.Execute(u)
if err != nil {
return err
@ -238,7 +259,7 @@ func (u *User) UnmarshalYAML(value *yaml.Node) error {
return nil
}
func (u *UserType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) {
func (u *UserType) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) {
switch *u {
case UserTypeUserAdd:
return NewUserAddCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewUserDelDeleteCommand()
@ -257,13 +278,33 @@ func (u *UserType) NewCRUD() (create *Command, read *Command, update *Command, d
}
}
func (u *UserType) NewCreateCommand() (create *command.Command) {
switch *u {
case UserTypeUserAdd:
return NewUserAddCreateCommand()
case UserTypeAddUser:
return NewAddUserCreateCommand()
default:
}
return nil
}
func (u *UserType) NewReadCommand() (*command.Command) {
return NewUserReadCommand()
}
func (p *UserType) NewReadUsersCommand() (*command.Command) {
return NewReadUsersCommand()
}
func (u *UserType) UnmarshalValue(value string) error {
switch value {
case string(UserTypeUserAdd), string(UserTypeAddUser):
*u = UserType(value)
return nil
default:
return errors.New("invalid UserType value")
return ErrInvalidUserType
}
}
@ -283,17 +324,60 @@ func (u *UserType) UnmarshalYAML(value *yaml.Node) error {
return u.UnmarshalValue(s)
}
func NewUserAddCreateCommand() *Command {
c := NewCommand()
func NewReadUsersCommand() *command.Command {
c := command.NewCommand()
c.Path = "getent"
c.Args = []command.CommandArg{
command.CommandArg("passwd"),
}
c.Extractor = func(out []byte, target any) error {
Users := target.(*[]*User)
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
lineIndex := 0
for _, line := range lines {
userRecord := strings.Split(strings.TrimSpace(line), ":")
if len(*Users) <= lineIndex + 1 {
*Users = append(*Users, NewUser())
}
u := (*Users)[lineIndex]
u.Name = userRecord[0]
u.UID = userRecord[2]
u.Gecos = userRecord[4]
u.Home = userRecord[5]
u.Shell = userRecord[6]
if readUser, userErr := user.Lookup(u.Name); userErr == nil {
if groups, groupsErr := readUser.GroupIds(); groupsErr == nil {
for _, secondaryGroup := range groups {
if readGroup, groupErr := user.LookupGroupId(secondaryGroup); groupErr == nil {
u.Groups = append(u.Groups, readGroup.Name)
}
}
}
}
if readGroup, groupErr := user.LookupGroupId(userRecord[3]); groupErr == nil {
u.Group = readGroup.Name
}
u.State = "present"
u.UserType = SystemUserType
lineIndex++
}
return nil
}
return c
}
func NewUserAddCreateCommand() *command.Command {
c := command.NewCommand()
c.Path = "useradd"
c.Args = []CommandArg{
CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
CommandArg("{{ if .Gecos }}-c {{ .Gecos }}{{ end }}"),
CommandArg("{{ if .Group }}-g {{ .Group }}{{ end }}"),
CommandArg("{{ if .Groups }}-G {{ range .Groups }}{{ . }},{{- end }}{{ end }}"),
CommandArg("{{ if .Home }}-d {{ .Home }}{{ end }}"),
CommandArg("{{ if .CreateHome }}-m{{ else }}-M{{ end }}"),
CommandArg("{{ .Name }}"),
c.Args = []command.CommandArg{
command.CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
command.CommandArg("{{ if .Gecos }}-c {{ .Gecos }}{{ end }}"),
command.CommandArg("{{ if .Group }}-g {{ .Group }}{{ end }}"),
command.CommandArg("{{ if .Groups }}-G {{ range .Groups }}{{ . }},{{- end }}{{ end }}"),
command.CommandArg("{{ if .Home }}-d {{ .Home }}{{ end }}"),
command.CommandArg("{{ if .CreateHome }}-m{{ else }}-M{{ end }}"),
command.CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
return nil
@ -309,17 +393,17 @@ func NewUserAddCreateCommand() *Command {
return c
}
func NewAddUserCreateCommand() *Command {
c := NewCommand()
func NewAddUserCreateCommand() *command.Command {
c := command.NewCommand()
c.Path = "adduser"
c.Args = []CommandArg{
CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
CommandArg("{{ if .Gecos }}-g {{ .Gecos }}{{ end }}"),
CommandArg("{{ if .Group }}-G {{ .Group }}{{ end }}"),
CommandArg("{{ if .Home }}-h {{ .Home }}{{ end }}"),
CommandArg("{{ if not .CreateHome }}-H{{ end }}"),
CommandArg("-D"),
CommandArg("{{ .Name }}"),
c.Args = []command.CommandArg{
command.CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
command.CommandArg("{{ if .Gecos }}-g {{ .Gecos }}{{ end }}"),
command.CommandArg("{{ if .Group }}-G {{ .Group }}{{ end }}"),
command.CommandArg("{{ if .Home }}-h {{ .Home }}{{ end }}"),
command.CommandArg("{{ if not .CreateHome }}-H{{ end }}"),
command.CommandArg("-D"),
command.CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
return nil
@ -335,8 +419,8 @@ func NewAddUserCreateCommand() *Command {
return c
}
func NewUserReadCommand() *Command {
c := NewCommand()
func NewUserReadCommand() *command.Command {
c := command.NewCommand()
c.Extractor = func(out []byte, target any) error {
u := target.(*User)
u.State = "absent"
@ -369,15 +453,15 @@ func NewUserReadCommand() *Command {
return c
}
func NewUserUpdateCommand() *Command {
func NewUserUpdateCommand() *command.Command {
return nil
}
func NewUserDelDeleteCommand() *Command {
c := NewCommand()
func NewUserDelDeleteCommand() *command.Command {
c := command.NewCommand()
c.Path = "userdel"
c.Args = []CommandArg{
CommandArg("{{ .Name }}"),
c.Args = []command.CommandArg{
command.CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
return nil
@ -385,11 +469,11 @@ func NewUserDelDeleteCommand() *Command {
return c
}
func NewDelUserDeleteCommand() *Command {
c := NewCommand()
func NewDelUserDeleteCommand() *command.Command {
c := command.NewCommand()
c.Path = "deluser"
c.Args = []CommandArg{
CommandArg("{{ .Name }}"),
c.Args = []command.CommandArg{
command.CommandArg("{{ .Name }}"),
}
c.Extractor = func(out []byte, target any) error {
return nil

View File

@ -80,6 +80,7 @@ func (d *DeclFile) ExtractResources(filter ResourceSelector) ([]*resource.Docume
for {
doc := resource.NewDocument()
e := decoder.Decode(doc)
slog.Info("ExtractResources().Decode()", "document", doc, "error", e)
if errors.Is(e, io.EOF) {
break
}

68
internal/source/group.go Normal file
View File

@ -0,0 +1,68 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package source
import (
_ "context"
_ "encoding/json"
_ "fmt"
_ "gopkg.in/yaml.v3"
"net/url"
_ "path/filepath"
"decl/internal/resource"
_ "os"
_ "io"
"log/slog"
)
type Group struct {
GroupType resource.GroupType `yaml:"type" json:"type"`
}
func NewGroup() *Group {
return &Group{ GroupType: resource.SystemGroupType }
}
func init() {
SourceTypes.Register([]string{"group"}, func(u *url.URL) DocSource {
groupSource := NewGroup()
groupType := u.Query().Get("type")
if len(groupType) > 0 {
groupSource.GroupType = resource.GroupType(groupType)
}
return groupSource
})
}
func (g *Group) Type() string { return "group" }
func (g *Group) ExtractResources(filter ResourceSelector) ([]*resource.Document, error) {
documents := make([]*resource.Document, 0, 100)
slog.Info("group source ExtractResources()", "group", g)
Groups := make([]*resource.Group, 0, 100)
cmd := g.GroupType.NewReadGroupsCommand()
if cmd == nil {
return documents, resource.ErrUnsupportedGroupType
}
if out, err := cmd.Execute(g); err == nil {
slog.Info("group source ExtractResources()", "output", out)
if exErr := cmd.Extractor(out, &Groups); exErr != nil {
return documents, exErr
}
document := resource.NewDocument()
for _, grp := range Groups {
if grp == nil {
grp = resource.NewGroup()
}
grp.GroupType = g.GroupType
document.AddResourceDeclaration("group", grp)
}
documents = append(documents, document)
} else {
slog.Info("group source ExtractResources()", "output", out, "error", err)
return documents, err
}
return documents, nil
}

View File

@ -26,7 +26,10 @@ func NewPackage() *Package {
func init() {
SourceTypes.Register([]string{"package"}, func(u *url.URL) DocSource {
p := NewPackage()
p.PackageType = resource.PackageType(u.Query().Get("type"))
packageType := u.Query().Get("type")
if len(packageType) > 0 {
p.PackageType = resource.PackageType(packageType)
}
return p
})
@ -40,6 +43,9 @@ func (p *Package) ExtractResources(filter ResourceSelector) ([]*resource.Documen
slog.Info("package source ExtractResources()", "package", p)
installedPackages := make([]*resource.Package, 0, 100)
cmd := p.PackageType.NewReadPackagesCommand()
if cmd == nil {
return documents, resource.ErrUnsupportedPackageType
}
if out, err := cmd.Execute(p); err == nil {
slog.Info("package source ExtractResources()", "output", out)
if exErr := cmd.Extractor(out, &installedPackages); exErr != nil {
@ -51,7 +57,6 @@ func (p *Package) ExtractResources(filter ResourceSelector) ([]*resource.Documen
pkg = resource.NewPackage()
}
pkg.PackageType = p.PackageType
document.AddResourceDeclaration("package", pkg)
}
documents = append(documents, document)

68
internal/source/user.go Normal file
View File

@ -0,0 +1,68 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package source
import (
_ "context"
_ "encoding/json"
_ "fmt"
_ "gopkg.in/yaml.v3"
"net/url"
_ "path/filepath"
"decl/internal/resource"
_ "os"
_ "io"
"log/slog"
)
type User struct {
UserType resource.UserType `yaml:"type" json:"type"`
}
func NewUser() *User {
return &User{ UserType: resource.SystemUserType }
}
func init() {
SourceTypes.Register([]string{"user"}, func(u *url.URL) DocSource {
userSource := NewUser()
userType := u.Query().Get("type")
if len(userType) > 0 {
userSource.UserType = resource.UserType(userType)
}
return userSource
})
}
func (u *User) Type() string { return "user" }
func (u *User) ExtractResources(filter ResourceSelector) ([]*resource.Document, error) {
documents := make([]*resource.Document, 0, 100)
slog.Info("user source ExtractResources()", "user", u)
Users := make([]*resource.User, 0, 100)
cmd := u.UserType.NewReadUsersCommand()
if cmd == nil {
return documents, resource.ErrUnsupportedUserType
}
if out, err := cmd.Execute(u); err == nil {
slog.Info("user source ExtractResources()", "output", out)
if exErr := cmd.Extractor(out, &Users); exErr != nil {
return documents, exErr
}
document := resource.NewDocument()
for _, usr := range Users {
if usr == nil {
usr = resource.NewUser()
}
usr.UserType = u.UserType
document.AddResourceDeclaration("user", usr)
}
documents = append(documents, document)
} else {
slog.Info("user source ExtractResources()", "output", out, "error", err)
return documents, err
}
return documents, nil
}

View File

@ -0,0 +1,23 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package source
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestNewUserSource(t *testing.T) {
s := NewUser()
assert.NotNil(t, s)
}
func TestExtractUsers(t *testing.T) {
u := NewUser()
assert.NotNil(t, u)
document, err := u.ExtractResources(nil)
assert.Nil(t, err)
assert.NotNil(t, document)
assert.Greater(t, len(document), 0)
}

View File

@ -15,15 +15,17 @@ import (
type MockContainerClient struct {
InjectContainerStart func(ctx context.Context, containerID string, options container.StartOptions) error
InjectContainerCreate func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error)
InjectNetworkCreate func(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
InjectNetworkCreate func(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error)
InjectNetworkList func(ctx context.Context, options network.ListOptions) ([]network.Summary, error)
InjectNetworkInspect func(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error)
InjectContainerList func(context.Context, container.ListOptions) ([]types.Container, error)
InjectContainerInspect func(context.Context, string) (types.ContainerJSON, error)
InjectContainerRemove func(context.Context, string, container.RemoveOptions) error
InjectContainerStop func(context.Context, string, container.StopOptions) error
InjectContainerWait func(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error)
InjectImagePull func(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error)
InjectImagePull func(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error)
InjectImageInspectWithRaw func(ctx context.Context, imageID string) (types.ImageInspect, []byte, error)
InjectImageRemove func(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error)
InjectImageRemove func(ctx context.Context, imageID string, options image.RemoveOptions) ([]image.DeleteResponse, error)
InjectClose func() error
}
@ -31,11 +33,11 @@ func (m *MockContainerClient) ContainerWait(ctx context.Context, containerID str
return m.InjectContainerWait(ctx, containerID, condition)
}
func (m *MockContainerClient) ImageRemove(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
func (m *MockContainerClient) ImageRemove(ctx context.Context, imageID string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
return m.InjectImageRemove(ctx, imageID, options)
}
func (m *MockContainerClient) ImagePull(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error) {
func (m *MockContainerClient) ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error) {
return m.InjectImagePull(ctx, refStr, options)
}
@ -77,6 +79,14 @@ func (m *MockContainerClient) Close() error {
return m.InjectClose()
}
func (m *MockContainerClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
func (m *MockContainerClient) NetworkCreate(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error) {
return m.InjectNetworkCreate(ctx, name, options)
}
func (m *MockContainerClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
return m.InjectNetworkList(ctx, options)
}
func (m *MockContainerClient) NetworkInspect(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error) {
return m.InjectNetworkInspect(ctx, networkID, options)
}