add support for RSA keys/certs
This commit is contained in:
parent
2c9e178554
commit
a6426da6e1
3
Makefile
3
Makefile
@ -32,3 +32,6 @@ clean:
|
|||||||
rm jx
|
rm jx
|
||||||
lint:
|
lint:
|
||||||
golangci-lint run --verbose ./...
|
golangci-lint run --verbose ./...
|
||||||
|
vulncheck:
|
||||||
|
govulncheck ./...
|
||||||
|
go vet ./...
|
||||||
|
21
README.md
21
README.md
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
examples/certificate.jx.yaml
Normal file
15
examples/certificate.jx.yaml
Normal 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
|
43
examples/config/cert.cfg.jx.yaml
Normal file
43
examples/config/cert.cfg.jx.yaml
Normal 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
|
5
examples/container-image.jx.yaml
Normal file
5
examples/container-image.jx.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
resources:
|
||||||
|
- type: container-image
|
||||||
|
transition: read
|
||||||
|
attributes:
|
||||||
|
name: nginx:latest
|
6
examples/group.jx.yaml
Normal file
6
examples/group.jx.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
resources:
|
||||||
|
- type: group
|
||||||
|
transition: create
|
||||||
|
attributes:
|
||||||
|
name: "testgroup"
|
||||||
|
gid: "12001"
|
11
examples/iptable.jx.yaml
Normal file
11
examples/iptable.jx.yaml
Normal 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
9
examples/package.jx.yaml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
resources:
|
||||||
|
- type: package
|
||||||
|
transition: create
|
||||||
|
attributes:
|
||||||
|
name: zip
|
||||||
|
version: 3.0-12build2
|
||||||
|
type: apt
|
||||||
|
state: present
|
||||||
|
|
9
go.mod
9
go.mod
@ -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
22
go.sum
@ -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=
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
83
internal/config/certificate.go
Normal file
83
internal/config/certificate.go
Normal 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
|
||||||
|
}
|
33
internal/config/certificate_test.go
Normal file
33
internal/config/certificate_test.go
Normal 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)
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
62
internal/config/schemas/certificate.schema.json
Normal file
62
internal/config/schemas/certificate.schema.json
Normal 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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
internal/config/schemas/config.schema.json
Normal file
18
internal/config/schemas/config.schema.json
Normal 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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
65
internal/config/schemas/pkixname.schema.json
Normal file
65
internal/config/schemas/pkixname.schema.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -36,7 +36,7 @@ type ContainerClient interface {
|
|||||||
ContainerInspect(context.Context, string) (types.ContainerJSON, error)
|
ContainerInspect(context.Context, string) (types.ContainerJSON, error)
|
||||||
ContainerRemove(context.Context, string, container.RemoveOptions) error
|
ContainerRemove(context.Context, string, container.RemoveOptions) error
|
||||||
ContainerStop(context.Context, string, container.StopOptions) error
|
ContainerStop(context.Context, string, container.StopOptions) error
|
||||||
ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error)
|
ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error)
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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,
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
18
internal/resource/filter.go
Normal file
18
internal/resource/filter.go
Normal 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
407
internal/resource/group.go
Normal 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
|
||||||
|
}
|
75
internal/resource/group_test.go
Normal file
75
internal/resource/group_test.go
Normal 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)
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
522
internal/resource/pki.go
Normal 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)
|
||||||
|
}
|
164
internal/resource/pki_test.go
Normal file
164
internal/resource/pki_test.go
Normal 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())
|
||||||
|
|
||||||
|
}
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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})|)$"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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]+)$"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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",
|
@ -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" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
31
internal/resource/schemas/document.schema.json
Normal file
31
internal/resource/schemas/document.schema.json
Normal 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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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",
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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",
|
20
internal/resource/schemas/group-declaration.schema.json
Normal file
20
internal/resource/schemas/group-declaration.schema.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
internal/resource/schemas/group.schema.json
Normal file
18
internal/resource/schemas/group.schema.json
Normal 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]*$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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",
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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",
|
@ -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",
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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",
|
17
internal/resource/schemas/pki-declaration.schema.json
Normal file
17
internal/resource/schemas/pki-declaration.schema.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
76
internal/resource/schemas/pki.schema.json
Normal file
76
internal/resource/schemas/pki.schema.json
Normal 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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
65
internal/resource/schemas/pkixname.schema.json
Normal file
65
internal/resource/schemas/pkixname.schema.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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",
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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
68
internal/source/group.go
Normal 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
|
||||||
|
}
|
@ -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
68
internal/source/user.go
Normal 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
|
||||||
|
}
|
23
internal/source/user_test.go
Normal file
23
internal/source/user_test.go
Normal 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)
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user