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 rm jx
lint: lint:
golangci-lint run --verbose ./... golangci-lint run --verbose ./...
vulncheck:
govulncheck ./...
go vet ./...

View File

@ -74,13 +74,14 @@ Read a resource document from an http endpoint.
Resources: Resources:
* [container](examples/container.yaml) [schema](internal/resource/schemas/container.schema.json) * [container](examples/container.jx.yaml) [schema](internal/resource/schemas/container.schema.json)
* [container-image](examples/container-image.yaml) [schema](internal/resource/schemas/container-image.schema.json) * [container-image](examples/container-image.jx.yaml) [schema](internal/resource/schemas/container-image.schema.json)
* [container-network](examples/container-network.yaml) [schema](internal/resource/schemas/container-network.schema.json) * [container-network](examples/container-network.jx.yaml) [schema](internal/resource/schemas/container-network.schema.json)
* [exec](examples/exec.yaml) [schema](internal/resource/schemas/exec.schema.json) * [exec](examples/exec.jx.yaml) [schema](internal/resource/schemas/exec.schema.json)
* [file](examples/file.yaml) [schema](internal/resource/schemas/file.schema.json) * [file](examples/file.jx.yaml) [schema](internal/resource/schemas/file.schema.json)
* [http](examples/http.yaml) [schema](internal/resource/schemas/http.schema.json) * [group](examples/group.jx.yaml) [schema](internal/resource/schemas/group.schema.json)
* [iptable](examples/iptable.yaml) [schema](internal/resource/schemas/iptable.schema.json) * [http](examples/http.jx.yaml) [schema](internal/resource/schemas/http.schema.json)
* [network_route](examples/network_route.yaml) [schema](internal/resource/schemas/network_route.schema.json) * [iptable](examples/iptable.jx.yaml) [schema](internal/resource/schemas/iptable.schema.json)
* [package](examples/package.yaml) [schema](internal/resource/schemas/package.schema.json) * [network_route](examples/network_route.jx.yaml) [schema](internal/resource/schemas/network_route.schema.json)
* [user](examples/user.yaml) [schema](internal/resource/schemas/user.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 ( import (
"context" "context"
"decl/internal/codec"
"decl/internal/config" "decl/internal/config"
"decl/internal/resource" "decl/internal/resource"
"decl/internal/source" "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) slog.Info("main.ImportResource", "args", os.Args, "output", GlobalOutput)
outputTarget, err := target.TargetTypes.New(GlobalOutput) outputTarget, err := target.TargetTypes.New(GlobalOutput)
if err != nil { if err != nil {
@ -209,7 +199,7 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
if *GlobalQuiet { if *GlobalQuiet {
for _, dr := range d.Resources() { 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 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) documents := make([]*resource.Document, 0, 100)
for _, source := range cmd.Args() { for _, source := range cmd.Args() {
loaded := LoadSourceURI(source) loaded := LoadSourceURI(source)
@ -270,12 +259,12 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
return e return e
} }
switch *GlobalOformat { outputTarget, err := target.TargetTypes.New(GlobalOutput)
case FormatYaml: if err != nil {
encoder = codec.NewYAMLEncoder(output) slog.Error("Failed opening target", "error", err)
case FormatJson:
encoder = codec.NewJSONEncoder(output)
} }
defer outputTarget.Close()
if *GlobalQuiet { if *GlobalQuiet {
for _, dr := range d.Resources() { for _, dr := range d.Resources() {
if _, e := output.Write([]byte(dr.Resource().URI())); e != nil { 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 { } else {
if documentGenerateErr := encoder.Encode(d); documentGenerateErr != nil { slog.Info("main.Apply", "outputTarget", outputTarget, "type", outputTarget.Type())
return documentGenerateErr 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 module decl
go 1.22.1 go 1.22.5
require ( require (
gitea.rosskeen.house/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3 gitea.rosskeen.house/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520193117-1835255b6d02 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/docker/go-connections v0.5.0
github.com/opencontainers/image-spec v1.1.0 github.com/opencontainers/image-spec v1.1.0
github.com/sters/yaml-diff v1.3.2 github.com/sters/yaml-diff v1.3.2
@ -28,6 +28,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // 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/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest 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/metric v1.25.0 // indirect
go.opentelemetry.io/otel/sdk v1.25.0 // indirect go.opentelemetry.io/otel/sdk v1.25.0 // indirect
go.opentelemetry.io/otel/trace 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/time v0.5.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
gotest.tools/v3 v3.5.1 // 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE= github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE=
github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 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 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@ -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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 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-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.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-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-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-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.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 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-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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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-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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@ -3,6 +3,8 @@
package codec package codec
import ( import (
"io"
"fmt"
"errors" "errors"
"encoding/json" "encoding/json"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -23,7 +25,7 @@ func (f *Format) Validate() error {
case FormatYaml, FormatJson, FormatProtoBuf: case FormatYaml, FormatJson, FormatProtoBuf:
return nil return nil
default: 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) 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" _ "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"errors"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"io" "io"
"log/slog" "log/slog"
@ -17,8 +18,11 @@ import (
"decl/internal/codec" "decl/internal/codec"
) )
var ErrUnknownCommand error = errors.New("Unable to find command in path")
type CommandExecutor func(value any) ([]byte, error) type CommandExecutor func(value any) ([]byte, error)
type CommandExtractAttributes func(output []byte, target any) error type CommandExtractAttributes func(output []byte, target any) error
type CommandExists func() error
type CommandArg string type CommandArg string
@ -30,10 +34,17 @@ type Command struct {
FailOnError bool `json:"failonerror" yaml:"failonerror"` FailOnError bool `json:"failonerror" yaml:"failonerror"`
Executor CommandExecutor `json:"-" yaml:"-"` Executor CommandExecutor `json:"-" yaml:"-"`
Extractor CommandExtractAttributes `json:"-" yaml:"-"` Extractor CommandExtractAttributes `json:"-" yaml:"-"`
CommandExists CommandExists `json:"-" yaml:"-"`
} }
func NewCommand() *Command { func NewCommand() *Command {
c := &Command{ Split: true, FailOnError: true } 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) { c.Executor = func(value any) ([]byte, error) {
args, err := c.Template(value) args, err := c.Template(value)
if err != nil { if err != nil {
@ -87,10 +98,7 @@ func (c *Command) SetCmdEnv(cmd *exec.Cmd) {
} }
func (c *Command) Exists() bool { func (c *Command) Exists() bool {
if _, err := exec.LookPath(c.Path); err != nil { return c.CommandExists() == nil
return false
}
return true
} }
func (c *Command) Template(value any) ([]string, error) { 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 { func NewConfigFileSource(u *url.URL) *ConfigFile {
t := NewConfigFileFromURI(u) t := NewConfigFileFromURI(u)
t.reader,_ = transport.NewReader(u) t.reader,_ = transport.NewReader(u)
if formatErr := t.Format.Set(t.reader.ContentType()); formatErr != nil { contentType := codec.Format(t.reader.ContentType())
panic(formatErr) if contentType.Validate() == nil {
if formatErr := t.Format.Set(t.reader.ContentType()); formatErr != nil {
panic(formatErr)
}
} }
t.decoder = codec.NewDecoder(t.reader, t.Format) t.decoder = codec.NewDecoder(t.reader, t.Format)
return t return t
@ -53,8 +56,11 @@ func NewConfigFileSource(u *url.URL) *ConfigFile {
func NewConfigFileTarget(u *url.URL) *ConfigFile { func NewConfigFileTarget(u *url.URL) *ConfigFile {
t := NewConfigFileFromURI(u) t := NewConfigFileFromURI(u)
t.writer,_ = transport.NewWriter(u) t.writer,_ = transport.NewWriter(u)
if formatErr := t.Format.Set(t.writer.ContentType()); formatErr != nil { contentType := codec.Format(t.writer.ContentType())
panic(formatErr) if contentType.Validate() == nil {
if formatErr := t.Format.Set(t.writer.ContentType()); formatErr != nil {
panic(formatErr)
}
} }
t.encoder = codec.NewEncoder(t.writer, t.Format) t.encoder = codec.NewEncoder(t.writer, t.Format)
return t return t

View File

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

View File

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

View File

@ -13,10 +13,13 @@
"type": { "type": {
"type": "string", "type": "string",
"description": "Config type name.", "description": "Config type name.",
"enum": [ "generic", "exec" ] "enum": [ "generic", "exec", "certificate" ]
}, },
"values": { "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

@ -79,6 +79,7 @@ type Container struct {
config ConfigurationValueGetter config ConfigurationValueGetter
apiClient ContainerClient apiClient ContainerClient
Resources ResourceMapper `json:"-" yaml:"-"`
} }
func init() { 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 { func (c *Container) Clone() Resource {
return &Container { return &Container {
Id: c.Id, Id: c.Id,

View File

@ -23,9 +23,9 @@ _ "os/exec"
) )
type ContainerImageClient interface { 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) 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 Close() error
} }
@ -44,12 +44,14 @@ type ContainerImage struct {
config ConfigurationValueGetter config ConfigurationValueGetter
apiClient ContainerImageClient apiClient ContainerImageClient
Resources ResourceMapper `json:"-" yaml:"-"`
} }
func init() { func init() {
ResourceTypes.Register([]string{"container-image"}, func(u *url.URL) Resource { ResourceTypes.Register([]string{"container-image"}, func(u *url.URL) Resource {
c := NewContainerImage(nil) c := NewContainerImage(nil)
c.Name = strings.Join([]string{u.Hostname(), u.Path}, ":") c.Name = ContainerImageNameFromURI(u)
slog.Info("NewContainerImage", "container", c)
return 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 { func (c *ContainerImage) Clone() Resource {
return &ContainerImage { return &ContainerImage {
Id: c.Id, Id: c.Id,
@ -91,9 +97,9 @@ func (c *ContainerImage) StateMachine() machine.Stater {
return c.stater return c.stater
} }
func (c *ContainerImage) URI() string { func URIFromContainerImageName(imageName string) string {
var host, namespace, repo string var host, namespace, repo string
elements := strings.Split(c.Name, "/") elements := strings.Split(imageName, "/")
switch len(elements) { switch len(elements) {
case 1: case 1:
repo = elements[0] 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}, "/")) 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 { func (c *ContainerImage) SetURI(uri string) error {
resourceUri, e := url.Parse(uri) resourceUri, e := url.Parse(uri)
if e == nil { if e == nil {
@ -209,30 +240,49 @@ func (c *ContainerImage) Create(ctx context.Context) error {
return nil return nil
} }
func (c *ContainerImage) Read(ctx context.Context) ([]byte, error) { func (c *ContainerImage) Pull(ctx context.Context) error {
out, err := c.apiClient.ImagePull(ctx, c.Name, types.ImagePullOptions{}) out, err := c.apiClient.ImagePull(ctx, c.Name, image.PullOptions{})
slog.Info("Read()", "name", c.Name, "error", err) slog.Info("ContainerImage.Pull()", "name", c.Name, "error", err)
if err == nil {
_, outputErr := io.ReadAll(out) 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 { if err != nil {
return nil, fmt.Errorf("%w: %s %s", err, c.Type(), c.Name) panic(err)
} }
return
}
if outputErr != nil { func (c *ContainerImage) Read(ctx context.Context) (resourceYaml []byte, err error) {
return nil, fmt.Errorf("%w: %s %s", outputErr, c.Type(), c.Name) 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 err != nil {
if client.IsErrNotFound(err) { if client.IsErrNotFound(err) {
slog.Info("ContainerImage.Read()", "oldstate", c.State, "newstate", "absent", "error", err) if pullErr := c.Pull(ctx); pullErr != nil {
c.State = "absent" panic(pullErr)
}
imageInspect = c.Inspect(ctx)
} else { } else {
panic(err) panic(err)
} }
return nil, err
} }
c.State = "present" c.State = "present"
@ -249,13 +299,13 @@ func (c *ContainerImage) Read(ctx context.Context) ([]byte, error) {
c.OS = imageInspect.Os c.OS = imageInspect.Os
c.Size = imageInspect.Size c.Size = imageInspect.Size
c.Comment = imageInspect.Comment 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) return yaml.Marshal(c)
} }
func (c *ContainerImage) Delete(ctx context.Context) error { func (c *ContainerImage) Delete(ctx context.Context) error {
slog.Info("ContainerImage.Delete()", "image", c) slog.Info("ContainerImage.Delete()", "image", c)
options := types.ImageRemoveOptions{ options := image.RemoveOptions{
Force: false, Force: false,
PruneChildren: false, PruneChildren: false,
} }

View File

@ -13,7 +13,7 @@ _ "fmt"
"io" "io"
_ "net/http" _ "net/http"
_ "net/http/httptest" _ "net/http/httptest"
_ "net/url" "net/url"
_ "os" _ "os"
"strings" "strings"
"testing" "testing"
@ -24,6 +24,45 @@ func TestNewContainerImageResource(t *testing.T) {
assert.NotNil(t, c) 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) { func TestReadContainerImage(t *testing.T) {
output := io.NopCloser(strings.NewReader("testdata")) output := io.NopCloser(strings.NewReader("testdata"))
ctx := context.Background() ctx := context.Background()
@ -32,10 +71,10 @@ func TestReadContainerImage(t *testing.T) {
state: present state: present
` `
m := &mocks.MockContainerClient{ 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 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 return nil, nil
}, },
InjectImageInspectWithRaw: func(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) { 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. // Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
// Container resource
package resource package resource
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "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/api/types/strslice"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -24,22 +21,31 @@ _ "strings"
"io" "io"
"gitea.rosskeen.house/rosskeen.house/machine" "gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec" "decl/internal/codec"
"log/slog"
"time"
) )
type ContainerNetworkClient interface { type ContainerNetworkClient interface {
ContainerClient 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 { type ContainerNetwork struct {
stater machine.Stater `json:"-" yaml:"-"` stater machine.Stater `json:"-" yaml:"-"`
Id string `json:"ID,omitempty" yaml:"ID,omitempty"` Id string `json:"ID,omitempty" yaml:"ID,omitempty"`
Name string `json:"name" yaml:"name"` 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"` State string `yaml:"state"`
config ConfigurationValueGetter config ConfigurationValueGetter
apiClient ContainerNetworkClient apiClient ContainerNetworkClient
Resources ResourceMapper `json:"-" yaml:"-"`
} }
func init() { 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 { func (n *ContainerNetwork) Clone() Resource {
return &ContainerNetwork { return &ContainerNetwork {
Id: n.Id, Id: n.Id,
@ -82,22 +92,48 @@ func (n *ContainerNetwork) StateMachine() machine.Stater {
func (n *ContainerNetwork) Notify(m *machine.EventMessage) { func (n *ContainerNetwork) Notify(m *machine.EventMessage) {
ctx := context.Background() ctx := context.Background()
slog.Info("Notify()", "ContainerNetwork", n, "m", m)
switch m.On { switch m.On {
case machine.ENTERSTATEEVENT: case machine.ENTERSTATEEVENT:
switch m.Dest { 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": case "start_create":
if e := n.Create(ctx); e == nil { if e := n.Create(ctx); e == nil {
if triggerErr := n.stater.Trigger("created"); triggerErr == nil { if triggerErr := n.StateMachine().Trigger("created"); triggerErr == nil {
return return
} }
} }
n.State = "absent" n.State = "absent"
case "present": case "absent":
n.State = "absent"
case "present", "created", "read":
n.State = "present" n.State = "present"
} }
case machine.EXITSTATEEVENT: case machine.EXITSTATEEVENT:
} }
} }
func (n *ContainerNetwork) URI() string { func (n *ContainerNetwork) URI() string {
@ -148,7 +184,7 @@ func (n *ContainerNetwork) LoadDecl(yamlResourceDeclaration string) error {
} }
func (n *ContainerNetwork) Create(ctx context.Context) 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", Driver: "bridge",
}) })
if err != nil { if err != nil {
@ -159,9 +195,52 @@ func (n *ContainerNetwork) Create(ctx context.Context) error {
return nil 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) { 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) return yaml.Marshal(n)
} }

View File

@ -9,7 +9,7 @@ _ "encoding/json"
_ "fmt" _ "fmt"
_ "github.com/docker/docker/api/types" _ "github.com/docker/docker/api/types"
_ "github.com/docker/docker/api/types/container" _ "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" "github.com/stretchr/testify/assert"
_ "io" _ "io"
_ "net/http" _ "net/http"
@ -32,6 +32,19 @@ func TestReadContainerNetwork(t *testing.T) {
state: present state: present
` `
m := &mocks.MockContainerClient{ 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) n := NewContainerNetwork(m)

View File

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

View File

@ -19,6 +19,15 @@ _ "net/url"
type ResourceMap[Value any] map[string]Value 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 { type Document struct {
uris ResourceMap[*Declaration] uris ResourceMap[*Declaration]
ResourceDecls []Declaration `json:"resources" yaml:"resources"` ResourceDecls []Declaration `json:"resources" yaml:"resources"`
@ -66,10 +75,10 @@ func (d *Document) Clone() *Document {
func (d *Document) Load(r io.Reader) (err error) { func (d *Document) Load(r io.Reader) (err error) {
c := codec.NewYAMLDecoder(r) c := codec.NewYAMLDecoder(r)
err = c.Decode(d) err = c.Decode(d)
slog.Info("Document.Load()", "error", err)
if err == nil { if err == nil {
for i := range d.ResourceDecls { for i := range d.ResourceDecls {
d.ResourceDecls[i].SetDocument(d) d.ResourceDecls[i].SetDocument(d)
d.ResourceDecls[i].SetConfig(d.config)
} }
} }
return return
@ -158,22 +167,23 @@ func (d *Document) MapResourceURI(uri string, declaration *Declaration) {
} }
func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration Resource) { 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.Type = TypeName(resourceType)
decl.Attributes = resourceDeclaration decl.Attributes = resourceDeclaration
decl.SetDocument(d)
d.ResourceDecls = append(d.ResourceDecls, *decl) d.ResourceDecls = append(d.ResourceDecls, *decl)
d.MapResourceURI(decl.Attributes.URI(), decl) d.MapResourceURI(decl.Attributes.URI(), decl)
decl.SetDocument(d)
} }
func (d *Document) AddResource(uri string) error { func (d *Document) AddResource(uri string) error {
decl := NewDeclaration() decl := NewDeclarationFromDocument(d)
if e := decl.SetURI(uri); e != nil { if e := decl.SetURI(uri); e != nil {
return e return e
} }
decl.SetDocument(d)
d.ResourceDecls = append(d.ResourceDecls, *decl) d.ResourceDecls = append(d.ResourceDecls, *decl)
d.MapResourceURI(decl.Attributes.URI(), decl) d.MapResourceURI(decl.Attributes.URI(), decl)
decl.SetDocument(d)
return nil return nil
} }
@ -228,3 +238,31 @@ func (d *Document) Diff(with *Document, output io.Writer) (returnOutput string,
} }
return "", nil 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 config ConfigurationValueGetter
// state attributes // state attributes
State string `yaml:"state"` State string `yaml:"state"`
Resources ResourceMapper `yaml:"-" json:"-"`
} }
func init() { func init() {
@ -41,6 +42,10 @@ func NewExec() *Exec {
return &Exec{} return &Exec{}
} }
func (x *Exec) SetResourceMapper(resources ResourceMapper) {
x.Resources = resources
}
func (x *Exec) Clone() Resource { func (x *Exec) Clone() Resource {
return &Exec { return &Exec {
Id: x.Id, Id: x.Id,

View File

@ -24,8 +24,10 @@ import (
"strings" "strings"
) )
// Describes the type of file the resource represents
type FileType string type FileType string
// Supported file types
const ( const (
RegularFile FileType = "regular" RegularFile FileType = "regular"
DirectoryFile FileType = "directory" 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 { type File struct {
stater machine.Stater `json:"-" yaml:"-"` stater machine.Stater `json:"-" yaml:"-"`
normalizePath bool `json:"-" yaml:"-"` normalizePath bool `json:"-" yaml:"-"`
@ -72,6 +82,7 @@ type File struct {
State string `json:"state,omitempty" yaml:"state,omitempty"` State string `json:"state,omitempty" yaml:"state,omitempty"`
SerializeContent bool `json:"serializecontent,omitempty" yaml:"serializecontent,omitempty"` SerializeContent bool `json:"serializecontent,omitempty" yaml:"serializecontent,omitempty"`
config ConfigurationValueGetter config ConfigurationValueGetter
Resources ResourceMapper `json:"-" yaml:"-"`
} }
type ResourceFileInfo struct { type ResourceFileInfo struct {
@ -92,6 +103,10 @@ func NewNormalizedFile() *File {
return f return f
} }
func (f *File) SetResourceMapper(resources ResourceMapper) {
f.Resources = resources
}
func (f *File) Clone() Resource { func (f *File) Clone() Resource {
return &File { return &File {
normalizePath: f.normalizePath, normalizePath: f.normalizePath,
@ -305,7 +320,7 @@ func (f *File) Create(ctx context.Context) error {
f.Size = 0 f.Size = 0
var contentReader io.ReadCloser var contentReader io.ReadCloser
if len(f.Content) == 0 && len(f.ContentSourceRef) != 0 { 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 contentReader = refReader
} else { } else {
return err 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"` StatusCode int `yaml:"statuscode,omitempty" json:"statuscode,omitempty"`
State string `yaml:"state,omitempty" json:"state,omitempty"` State string `yaml:"state,omitempty" json:"state,omitempty"`
config ConfigurationValueGetter config ConfigurationValueGetter
Resources ResourceMapper `yaml:"-" json:"-"`
} }
func NewHTTP() *HTTP { func NewHTTP() *HTTP {
return &HTTP{ client: &http.Client{} } return &HTTP{ client: &http.Client{} }
} }
func (h *HTTP) SetResourceMapper(resources ResourceMapper) {
h.Resources = resources
}
func (h *HTTP) Clone() Resource { func (h *HTTP) Clone() Resource {
return &HTTP { return &HTTP {
client: h.client, client: h.client,

View File

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

View File

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

View File

@ -77,3 +77,12 @@ func LookupGID(groupName string) (int, error) {
return gid, nil 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" 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() var SystemPackageType PackageType = FindSystemPackageType()
type Package struct { type Package struct {
@ -49,6 +52,7 @@ type Package struct {
// state attributes // state attributes
State string `yaml:"state,omitempty" json:"state,omitempty"` State string `yaml:"state,omitempty" json:"state,omitempty"`
config ConfigurationValueGetter config ConfigurationValueGetter
Resources ResourceMapper `yaml:"-" json:"-"`
} }
func init() { func init() {
@ -72,6 +76,10 @@ func NewPackage() *Package {
return &Package{ PackageType: SystemPackageType } return &Package{ PackageType: SystemPackageType }
} }
func (p *Package) SetResourceMapper(resources ResourceMapper) {
p.Resources = resources
}
func (p *Package) Clone() Resource { func (p *Package) Clone() Resource {
newp := &Package { newp := &Package {
Name: p.Name, Name: p.Name,
@ -228,15 +236,19 @@ func (p *Package) LoadDecl(yamlResourceDeclaration string) error {
func (p *Package) Type() string { return "package" } func (p *Package) Type() string { return "package" }
func (p *Package) Read(ctx context.Context) ([]byte, error) { func (p *Package) Read(ctx context.Context) ([]byte, error) {
out, err := p.ReadCommand.Execute(p) if p.ReadCommand.Exists() {
if err != nil { out, err := p.ReadCommand.Execute(p)
return nil, err 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 { func (p *Package) UnmarshalJSON(data []byte) error {
@ -325,7 +337,7 @@ func (p *PackageType) UnmarshalValue(value string) error {
*p = PackageType(value) *p = PackageType(value)
return nil return nil
default: default:
return errors.New("invalid PackageType value") return ErrInvalidPackageType
} }
} }

View File

@ -42,6 +42,7 @@ type: apk
p := NewPackage() p := NewPackage()
assert.NotNil(t, p) assert.NotNil(t, p)
m := &MockCommand{ m := &MockCommand{
CommandExists: func() error { return nil },
Executor: func(value any) ([]byte, error) { Executor: func(value any) ([]byte, error) {
return nil, nil return nil, nil
}, },
@ -134,3 +135,27 @@ Version: 1.2.2
assert.Equal(t, "1.2.2", p.Version) assert.Equal(t, "1.2.2", p.Version)
assert.Nil(t, p.Validate()) 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" _ "encoding/json"
_ "fmt" _ "fmt"
_ "gopkg.in/yaml.v3" _ "gopkg.in/yaml.v3"
_ "net/url" "net/url"
"gitea.rosskeen.house/rosskeen.house/machine" "gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/transport" "decl/internal/transport"
"log/slog"
) )
type ResourceReference string type ResourceReference string
@ -29,6 +30,7 @@ type Resource interface {
ResourceReader ResourceReader
ResourceValidator ResourceValidator
Clone() Resource Clone() Resource
SetResourceMapper(resources ResourceMapper)
} }
type ContentReader interface { type ContentReader interface {
@ -39,6 +41,11 @@ type ContentWriter interface {
ContentWriterStream() (*transport.Writer, error) ContentWriterStream() (*transport.Writer, error)
} }
type ContentReadWriter interface {
ContentReader
ContentWriter
}
type ResourceValidator interface { type ResourceValidator interface {
Validate() error Validate() error
} }
@ -77,6 +84,39 @@ func NewResource(uri string) Resource {
return nil 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) { func (r ResourceReference) ContentReaderStream() (*transport.Reader, error) {
return transport.NewReaderURI(string(r)) return transport.NewReaderURI(string(r))
} }

View File

@ -13,7 +13,6 @@ import (
"log/slog" "log/slog"
) )
//go:embed schemas/*.jsonschema
//go:embed schemas/*.schema.json //go:embed schemas/*.schema.json
var schemaFiles embed.FS var schemaFiles embed.FS
@ -22,7 +21,7 @@ type Schema struct {
} }
func NewSchema(name string) *Schema { func NewSchema(name string) *Schema {
path := fmt.Sprintf("file://schemas/%s.jsonschema", name) path := fmt.Sprintf("file://schemas/%s.schema.json", name)
return &Schema{schema: gojsonschema.NewReferenceLoaderFileSystem(path, http.FS(schemaFiles))} return &Schema{schema: gojsonschema.NewReferenceLoaderFileSystem(path, http.FS(schemaFiles))}
//return &Schema{schema: gojsonschema.NewReferenceLoader(path)} //return &Schema{schema: gojsonschema.NewReferenceLoader(path)}
@ -44,6 +43,7 @@ func (s *Schema) Validate(source string) error {
for _, err := range result.Errors() { for _, err := range result.Errors() {
schemaErrors.WriteString(err.String() + "\n") schemaErrors.WriteString(err.String() + "\n")
} }
schemaErrors.WriteString(source)
return errors.New(schemaErrors.String()) return errors.New(schemaErrors.String())
} }
return nil 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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "declaration", "title": "declaration",
"type": "object", "type": "object",
@ -11,7 +11,7 @@
"enum": [ "container" ] "enum": [ "container" ]
}, },
"attributes": { "attributes": {
"$ref": "container.jsonschema" "$ref": "container.schema.json"
} }
} }
} }

View File

@ -8,7 +8,7 @@
"properties": { "properties": {
"name": { "name": {
"type": "string", "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": { "properties": {
"name": { "name": {
"type": "string", "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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "container", "title": "container",
"description": "A docker 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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "exec-declaration", "title": "exec-declaration",
"type": "object", "type": "object",
@ -11,7 +11,7 @@
"enum": [ "exec" ] "enum": [ "exec" ]
}, },
"attributes": { "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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "exec", "title": "exec",
"type": "object", "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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "file-declaration", "title": "file-declaration",
"type": "object", "type": "object",
@ -15,7 +15,7 @@
"description": "Config name" "description": "Config name"
}, },
"attributes": { "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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "file", "title": "file",
"type": "object", "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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "http-declaration", "title": "http-declaration",
"type": "object", "type": "object",
@ -15,7 +15,7 @@
"description": "Config name." "description": "Config name."
}, },
"attributes": { "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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "http", "title": "http",
"type": "object", "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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "iptable-declaration", "title": "iptable-declaration",
"type": "object", "type": "object",
@ -15,7 +15,7 @@
"description": "Config name" "description": "Config name"
}, },
"attributes": { "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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "iptable", "title": "iptable",
"type": "object", "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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "network-route-declaration", "title": "network-route-declaration",
"type": "object", "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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "package-declaration", "title": "package-declaration",
"type": "object", "type": "object",
@ -11,7 +11,7 @@
"enum": [ "package" ] "enum": [ "package" ]
}, },
"attributes": { "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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "package", "title": "package",
"type": "object", "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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "declaration", "title": "declaration",
"type": "object", "type": "object",
@ -14,7 +14,7 @@
"$ref": "storagetransition.schema.json" "$ref": "storagetransition.schema.json"
}, },
"attributes": { "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#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "user", "title": "user",
"description": "A user account", "description": "A user account",
@ -8,7 +8,7 @@
"properties": { "properties": {
"name": { "name": {
"type": "string", "type": "string",
"pattern": "^[a-z]([-_a-z0-9]{0,31})$" "pattern": "^[_a-z]([-_a-z0-9]{0,31})$"
}, },
"uid": { "uid": {
"type": "string", "type": "string",

View File

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

View File

@ -12,11 +12,12 @@ _ "os"
"os/exec" "os/exec"
"os/user" "os/user"
"io" "io"
"strings"
"encoding/json" "encoding/json"
"errors" "errors"
"gitea.rosskeen.house/rosskeen.house/machine" "gitea.rosskeen.house/rosskeen.house/machine"
"strings"
"decl/internal/codec" "decl/internal/codec"
"decl/internal/command"
) )
type decodeUser User type decodeUser User
@ -28,6 +29,11 @@ const (
UserTypeUserAdd = "useradd" 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 { type User struct {
stater machine.Stater `json:"-" yaml:"-"` stater machine.Stater `json:"-" yaml:"-"`
Name string `json:"name" yaml:"name"` Name string `json:"name" yaml:"name"`
@ -40,12 +46,13 @@ type User struct {
Shell string `json:"shell,omitempty" yaml:"shell,omitempty"` Shell string `json:"shell,omitempty" yaml:"shell,omitempty"`
UserType UserType `json:"-" yaml:"-"` UserType UserType `json:"-" yaml:"-"`
CreateCommand *Command `json:"-" yaml:"-"` CreateCommand *command.Command `json:"-" yaml:"-"`
ReadCommand *Command `json:"-" yaml:"-"` ReadCommand *command.Command `json:"-" yaml:"-"`
UpdateCommand *Command `json:"-" yaml:"-"` UpdateCommand *command.Command `json:"-" yaml:"-"`
DeleteCommand *Command `json:"-" yaml:"-"` DeleteCommand *command.Command `json:"-" yaml:"-"`
State string `json:"state,omitempty" yaml:"state,omitempty"` State string `json:"state,omitempty" yaml:"state,omitempty"`
config ConfigurationValueGetter config ConfigurationValueGetter
Resources ResourceMapper `json:"-" yaml:"-"`
} }
func NewUser() *User { 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 { func (u *User) Clone() Resource {
newu := &User { newu := &User {
Name: u.Name, Name: u.Name,
@ -97,6 +118,30 @@ func (u *User) Notify(m *machine.EventMessage) {
switch m.On { switch m.On {
case machine.ENTERSTATEEVENT: case machine.ENTERSTATEEVENT:
switch m.Dest { 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": case "start_create":
if e := u.Create(ctx); e == nil { if e := u.Create(ctx); e == nil {
if triggerErr := u.stater.Trigger("created"); triggerErr == nil { if triggerErr := u.stater.Trigger("created"); triggerErr == nil {
@ -104,7 +149,9 @@ func (u *User) Notify(m *machine.EventMessage) {
} }
} }
u.State = "absent" u.State = "absent"
case "present": case "absent":
u.State = "absent"
case "present", "created", "read":
u.State = "present" u.State = "present"
} }
case machine.EXITSTATEEVENT: case machine.EXITSTATEEVENT:
@ -140,6 +187,7 @@ func (u *User) Validate() error {
} }
func (u *User) Apply() error { func (u *User) Apply() error {
ctx := context.Background()
switch u.State { switch u.State {
case "present": case "present":
_, NoUserExists := LookupUID(u.Name) _, NoUserExists := LookupUID(u.Name)
@ -148,7 +196,7 @@ func (u *User) Apply() error {
return cmdErr return cmdErr
} }
case "absent": case "absent":
cmdErr := u.Delete() cmdErr := u.Delete(ctx)
return cmdErr return cmdErr
} }
return nil return nil
@ -162,33 +210,6 @@ func (u *User) LoadDecl(yamlResourceDeclaration string) error {
return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(u) 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) Type() string { return "user" }
func (u *User) Create(ctx context.Context) (error) { 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) _, err := u.DeleteCommand.Execute(u)
if err != nil { if err != nil {
return err return err
@ -238,7 +259,7 @@ func (u *User) UnmarshalYAML(value *yaml.Node) error {
return nil 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 { switch *u {
case UserTypeUserAdd: case UserTypeUserAdd:
return NewUserAddCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewUserDelDeleteCommand() 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 { func (u *UserType) UnmarshalValue(value string) error {
switch value { switch value {
case string(UserTypeUserAdd), string(UserTypeAddUser): case string(UserTypeUserAdd), string(UserTypeAddUser):
*u = UserType(value) *u = UserType(value)
return nil return nil
default: 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) return u.UnmarshalValue(s)
} }
func NewUserAddCreateCommand() *Command { func NewReadUsersCommand() *command.Command {
c := NewCommand() 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.Path = "useradd"
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"), command.CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
CommandArg("{{ if .Gecos }}-c {{ .Gecos }}{{ end }}"), command.CommandArg("{{ if .Gecos }}-c {{ .Gecos }}{{ end }}"),
CommandArg("{{ if .Group }}-g {{ .Group }}{{ end }}"), command.CommandArg("{{ if .Group }}-g {{ .Group }}{{ end }}"),
CommandArg("{{ if .Groups }}-G {{ range .Groups }}{{ . }},{{- end }}{{ end }}"), command.CommandArg("{{ if .Groups }}-G {{ range .Groups }}{{ . }},{{- end }}{{ end }}"),
CommandArg("{{ if .Home }}-d {{ .Home }}{{ end }}"), command.CommandArg("{{ if .Home }}-d {{ .Home }}{{ end }}"),
CommandArg("{{ if .CreateHome }}-m{{ else }}-M{{ end }}"), command.CommandArg("{{ if .CreateHome }}-m{{ else }}-M{{ end }}"),
CommandArg("{{ .Name }}"), command.CommandArg("{{ .Name }}"),
} }
c.Extractor = func(out []byte, target any) error { c.Extractor = func(out []byte, target any) error {
return nil return nil
@ -309,17 +393,17 @@ func NewUserAddCreateCommand() *Command {
return c return c
} }
func NewAddUserCreateCommand() *Command { func NewAddUserCreateCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "adduser" c.Path = "adduser"
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"), command.CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
CommandArg("{{ if .Gecos }}-g {{ .Gecos }}{{ end }}"), command.CommandArg("{{ if .Gecos }}-g {{ .Gecos }}{{ end }}"),
CommandArg("{{ if .Group }}-G {{ .Group }}{{ end }}"), command.CommandArg("{{ if .Group }}-G {{ .Group }}{{ end }}"),
CommandArg("{{ if .Home }}-h {{ .Home }}{{ end }}"), command.CommandArg("{{ if .Home }}-h {{ .Home }}{{ end }}"),
CommandArg("{{ if not .CreateHome }}-H{{ end }}"), command.CommandArg("{{ if not .CreateHome }}-H{{ end }}"),
CommandArg("-D"), command.CommandArg("-D"),
CommandArg("{{ .Name }}"), command.CommandArg("{{ .Name }}"),
} }
c.Extractor = func(out []byte, target any) error { c.Extractor = func(out []byte, target any) error {
return nil return nil
@ -335,8 +419,8 @@ func NewAddUserCreateCommand() *Command {
return c return c
} }
func NewUserReadCommand() *Command { func NewUserReadCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Extractor = func(out []byte, target any) error { c.Extractor = func(out []byte, target any) error {
u := target.(*User) u := target.(*User)
u.State = "absent" u.State = "absent"
@ -369,15 +453,15 @@ func NewUserReadCommand() *Command {
return c return c
} }
func NewUserUpdateCommand() *Command { func NewUserUpdateCommand() *command.Command {
return nil return nil
} }
func NewUserDelDeleteCommand() *Command { func NewUserDelDeleteCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "userdel" c.Path = "userdel"
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("{{ .Name }}"), command.CommandArg("{{ .Name }}"),
} }
c.Extractor = func(out []byte, target any) error { c.Extractor = func(out []byte, target any) error {
return nil return nil
@ -385,11 +469,11 @@ func NewUserDelDeleteCommand() *Command {
return c return c
} }
func NewDelUserDeleteCommand() *Command { func NewDelUserDeleteCommand() *command.Command {
c := NewCommand() c := command.NewCommand()
c.Path = "deluser" c.Path = "deluser"
c.Args = []CommandArg{ c.Args = []command.CommandArg{
CommandArg("{{ .Name }}"), command.CommandArg("{{ .Name }}"),
} }
c.Extractor = func(out []byte, target any) error { c.Extractor = func(out []byte, target any) error {
return nil return nil

View File

@ -80,6 +80,7 @@ func (d *DeclFile) ExtractResources(filter ResourceSelector) ([]*resource.Docume
for { for {
doc := resource.NewDocument() doc := resource.NewDocument()
e := decoder.Decode(doc) e := decoder.Decode(doc)
slog.Info("ExtractResources().Decode()", "document", doc, "error", e)
if errors.Is(e, io.EOF) { if errors.Is(e, io.EOF) {
break 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() { func init() {
SourceTypes.Register([]string{"package"}, func(u *url.URL) DocSource { SourceTypes.Register([]string{"package"}, func(u *url.URL) DocSource {
p := NewPackage() 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 return p
}) })
@ -40,6 +43,9 @@ func (p *Package) ExtractResources(filter ResourceSelector) ([]*resource.Documen
slog.Info("package source ExtractResources()", "package", p) slog.Info("package source ExtractResources()", "package", p)
installedPackages := make([]*resource.Package, 0, 100) installedPackages := make([]*resource.Package, 0, 100)
cmd := p.PackageType.NewReadPackagesCommand() cmd := p.PackageType.NewReadPackagesCommand()
if cmd == nil {
return documents, resource.ErrUnsupportedPackageType
}
if out, err := cmd.Execute(p); err == nil { if out, err := cmd.Execute(p); err == nil {
slog.Info("package source ExtractResources()", "output", out) slog.Info("package source ExtractResources()", "output", out)
if exErr := cmd.Extractor(out, &installedPackages); exErr != nil { if exErr := cmd.Extractor(out, &installedPackages); exErr != nil {
@ -51,7 +57,6 @@ func (p *Package) ExtractResources(filter ResourceSelector) ([]*resource.Documen
pkg = resource.NewPackage() pkg = resource.NewPackage()
} }
pkg.PackageType = p.PackageType pkg.PackageType = p.PackageType
document.AddResourceDeclaration("package", pkg) document.AddResourceDeclaration("package", pkg)
} }
documents = append(documents, document) 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 { type MockContainerClient struct {
InjectContainerStart func(ctx context.Context, containerID string, options container.StartOptions) error 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) 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) InjectContainerList func(context.Context, container.ListOptions) ([]types.Container, error)
InjectContainerInspect func(context.Context, string) (types.ContainerJSON, error) InjectContainerInspect func(context.Context, string) (types.ContainerJSON, error)
InjectContainerRemove func(context.Context, string, container.RemoveOptions) error InjectContainerRemove func(context.Context, string, container.RemoveOptions) error
InjectContainerStop func(context.Context, string, container.StopOptions) error InjectContainerStop func(context.Context, string, container.StopOptions) error
InjectContainerWait func(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan 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) 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 InjectClose func() error
} }
@ -31,11 +33,11 @@ func (m *MockContainerClient) ContainerWait(ctx context.Context, containerID str
return m.InjectContainerWait(ctx, containerID, condition) 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) 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) return m.InjectImagePull(ctx, refStr, options)
} }
@ -77,6 +79,14 @@ func (m *MockContainerClient) Close() error {
return m.InjectClose() 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) 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)
}