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
|
||||
lint:
|
||||
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:
|
||||
|
||||
* [container](examples/container.yaml) [schema](internal/resource/schemas/container.schema.json)
|
||||
* [container-image](examples/container-image.yaml) [schema](internal/resource/schemas/container-image.schema.json)
|
||||
* [container-network](examples/container-network.yaml) [schema](internal/resource/schemas/container-network.schema.json)
|
||||
* [exec](examples/exec.yaml) [schema](internal/resource/schemas/exec.schema.json)
|
||||
* [file](examples/file.yaml) [schema](internal/resource/schemas/file.schema.json)
|
||||
* [http](examples/http.yaml) [schema](internal/resource/schemas/http.schema.json)
|
||||
* [iptable](examples/iptable.yaml) [schema](internal/resource/schemas/iptable.schema.json)
|
||||
* [network_route](examples/network_route.yaml) [schema](internal/resource/schemas/network_route.schema.json)
|
||||
* [package](examples/package.yaml) [schema](internal/resource/schemas/package.schema.json)
|
||||
* [user](examples/user.yaml) [schema](internal/resource/schemas/user.schema.json)
|
||||
* [container](examples/container.jx.yaml) [schema](internal/resource/schemas/container.schema.json)
|
||||
* [container-image](examples/container-image.jx.yaml) [schema](internal/resource/schemas/container-image.schema.json)
|
||||
* [container-network](examples/container-network.jx.yaml) [schema](internal/resource/schemas/container-network.schema.json)
|
||||
* [exec](examples/exec.jx.yaml) [schema](internal/resource/schemas/exec.schema.json)
|
||||
* [file](examples/file.jx.yaml) [schema](internal/resource/schemas/file.schema.json)
|
||||
* [group](examples/group.jx.yaml) [schema](internal/resource/schemas/group.schema.json)
|
||||
* [http](examples/http.jx.yaml) [schema](internal/resource/schemas/http.schema.json)
|
||||
* [iptable](examples/iptable.jx.yaml) [schema](internal/resource/schemas/iptable.schema.json)
|
||||
* [network_route](examples/network_route.jx.yaml) [schema](internal/resource/schemas/network_route.schema.json)
|
||||
* [package](examples/package.jx.yaml) [schema](internal/resource/schemas/package.schema.json)
|
||||
* [user](examples/user.jx.yaml) [schema](internal/resource/schemas/user.schema.json)
|
||||
|
@ -4,7 +4,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"decl/internal/codec"
|
||||
"decl/internal/config"
|
||||
"decl/internal/resource"
|
||||
"decl/internal/source"
|
||||
@ -170,15 +169,6 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
switch *GlobalOformat {
|
||||
case FormatYaml:
|
||||
encoder = resource.NewYAMLEncoder(output)
|
||||
case FormatJson:
|
||||
encoder = resource.NewJSONEncoder(output)
|
||||
}
|
||||
*/
|
||||
|
||||
slog.Info("main.ImportResource", "args", os.Args, "output", GlobalOutput)
|
||||
outputTarget, err := target.TargetTypes.New(GlobalOutput)
|
||||
if err != nil {
|
||||
@ -209,7 +199,7 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
||||
|
||||
if *GlobalQuiet {
|
||||
for _, dr := range d.Resources() {
|
||||
if _, e := output.Write([]byte(dr.Resource().URI())); e != nil {
|
||||
if _, e := output.Write([]byte(fmt.Sprintf("%s\n", dr.Resource().URI()))); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
@ -245,7 +235,6 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
var encoder codec.Encoder
|
||||
documents := make([]*resource.Document, 0, 100)
|
||||
for _, source := range cmd.Args() {
|
||||
loaded := LoadSourceURI(source)
|
||||
@ -270,12 +259,12 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
||||
return e
|
||||
}
|
||||
|
||||
switch *GlobalOformat {
|
||||
case FormatYaml:
|
||||
encoder = codec.NewYAMLEncoder(output)
|
||||
case FormatJson:
|
||||
encoder = codec.NewJSONEncoder(output)
|
||||
outputTarget, err := target.TargetTypes.New(GlobalOutput)
|
||||
if err != nil {
|
||||
slog.Error("Failed opening target", "error", err)
|
||||
}
|
||||
defer outputTarget.Close()
|
||||
|
||||
if *GlobalQuiet {
|
||||
for _, dr := range d.Resources() {
|
||||
if _, e := output.Write([]byte(dr.Resource().URI())); e != nil {
|
||||
@ -283,8 +272,9 @@ func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if documentGenerateErr := encoder.Encode(d); documentGenerateErr != nil {
|
||||
return documentGenerateErr
|
||||
slog.Info("main.Apply", "outputTarget", outputTarget, "type", outputTarget.Type())
|
||||
if outputErr := outputTarget.EmitResources([]*resource.Document{d}, nil); outputErr != nil {
|
||||
return outputErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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
|
||||
|
||||
go 1.22.1
|
||||
go 1.22.5
|
||||
|
||||
require (
|
||||
gitea.rosskeen.house/pylon/luaruntime v0.0.0-20240513200425-f413d8adf7b3
|
||||
gitea.rosskeen.house/rosskeen.house/machine v0.0.0-20240520193117-1835255b6d02
|
||||
github.com/docker/docker v25.0.5+incompatible
|
||||
github.com/docker/docker v27.0.3+incompatible
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/sters/yaml-diff v1.3.2
|
||||
@ -28,6 +28,7 @@ require (
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
@ -41,7 +42,9 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.25.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.25.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.25.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/crypto v0.24.0 // indirect
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
|
22
go.sum
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/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
||||
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE=
|
||||
github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE=
|
||||
github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@ -58,6 +58,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
@ -111,16 +113,16 @@ go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7e
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -131,12 +133,12 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
@ -3,6 +3,8 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"io"
|
||||
"fmt"
|
||||
"errors"
|
||||
"encoding/json"
|
||||
"gopkg.in/yaml.v3"
|
||||
@ -23,7 +25,7 @@ func (f *Format) Validate() error {
|
||||
case FormatYaml, FormatJson, FormatProtoBuf:
|
||||
return nil
|
||||
default:
|
||||
return ErrInvalidFormat
|
||||
return fmt.Errorf("%w: %s", ErrInvalidFormat, *f)
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,3 +61,20 @@ func (f *Format) UnmarshalYAML(value *yaml.Node) error {
|
||||
}
|
||||
return f.UnmarshalValue(s)
|
||||
}
|
||||
|
||||
func (f Format) Encoder(w io.Writer) Encoder {
|
||||
return NewEncoder(w, f)
|
||||
}
|
||||
|
||||
func (f Format) Decoder(r io.Reader) Decoder {
|
||||
return NewDecoder(r, f)
|
||||
}
|
||||
|
||||
func (f Format) Serialize(object any, w io.Writer) error {
|
||||
return f.Encoder(w).Encode(object)
|
||||
}
|
||||
|
||||
func (f Format) Deserialize(r io.Reader, object any) error {
|
||||
return f.Decoder(r).Decode(object)
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
_ "context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io"
|
||||
"log/slog"
|
||||
@ -17,8 +18,11 @@ import (
|
||||
"decl/internal/codec"
|
||||
)
|
||||
|
||||
var ErrUnknownCommand error = errors.New("Unable to find command in path")
|
||||
|
||||
type CommandExecutor func(value any) ([]byte, error)
|
||||
type CommandExtractAttributes func(output []byte, target any) error
|
||||
type CommandExists func() error
|
||||
|
||||
type CommandArg string
|
||||
|
||||
@ -30,10 +34,17 @@ type Command struct {
|
||||
FailOnError bool `json:"failonerror" yaml:"failonerror"`
|
||||
Executor CommandExecutor `json:"-" yaml:"-"`
|
||||
Extractor CommandExtractAttributes `json:"-" yaml:"-"`
|
||||
CommandExists CommandExists `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
func NewCommand() *Command {
|
||||
c := &Command{ Split: true, FailOnError: true }
|
||||
c.CommandExists = func() error {
|
||||
if _, err := exec.LookPath(c.Path); err != nil {
|
||||
return fmt.Errorf("%w - %w", ErrUnknownCommand, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
c.Executor = func(value any) ([]byte, error) {
|
||||
args, err := c.Template(value)
|
||||
if err != nil {
|
||||
@ -87,10 +98,7 @@ func (c *Command) SetCmdEnv(cmd *exec.Cmd) {
|
||||
}
|
||||
|
||||
func (c *Command) Exists() bool {
|
||||
if _, err := exec.LookPath(c.Path); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return c.CommandExists() == nil
|
||||
}
|
||||
|
||||
func (c *Command) Template(value any) ([]string, error) {
|
||||
|
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 {
|
||||
t := NewConfigFileFromURI(u)
|
||||
t.reader,_ = transport.NewReader(u)
|
||||
if formatErr := t.Format.Set(t.reader.ContentType()); formatErr != nil {
|
||||
panic(formatErr)
|
||||
contentType := codec.Format(t.reader.ContentType())
|
||||
if contentType.Validate() == nil {
|
||||
if formatErr := t.Format.Set(t.reader.ContentType()); formatErr != nil {
|
||||
panic(formatErr)
|
||||
}
|
||||
}
|
||||
t.decoder = codec.NewDecoder(t.reader, t.Format)
|
||||
return t
|
||||
@ -53,8 +56,11 @@ func NewConfigFileSource(u *url.URL) *ConfigFile {
|
||||
func NewConfigFileTarget(u *url.URL) *ConfigFile {
|
||||
t := NewConfigFileFromURI(u)
|
||||
t.writer,_ = transport.NewWriter(u)
|
||||
if formatErr := t.Format.Set(t.writer.ContentType()); formatErr != nil {
|
||||
panic(formatErr)
|
||||
contentType := codec.Format(t.writer.ContentType())
|
||||
if contentType.Validate() == nil {
|
||||
if formatErr := t.Format.Set(t.writer.ContentType()); formatErr != nil {
|
||||
panic(formatErr)
|
||||
}
|
||||
}
|
||||
t.encoder = codec.NewEncoder(t.writer, t.Format)
|
||||
return t
|
||||
|
@ -10,36 +10,36 @@ import (
|
||||
|
||||
func init() {
|
||||
ConfigTypes.Register([]string{"generic"}, func(u *url.URL) Configuration {
|
||||
g := NewGeneric()
|
||||
g := NewGeneric[any]()
|
||||
return g
|
||||
})
|
||||
}
|
||||
|
||||
type Generic map[string]any
|
||||
type Generic[Value any] map[string]Value
|
||||
|
||||
func NewGeneric() *Generic {
|
||||
g := make(Generic)
|
||||
func NewGeneric[Value any]() *Generic[Value] {
|
||||
g := make(Generic[Value])
|
||||
return &g
|
||||
}
|
||||
|
||||
func (g *Generic) Clone() Configuration {
|
||||
func (g *Generic[Value]) Clone() Configuration {
|
||||
jsonGeneric, _ := json.Marshal(g)
|
||||
clone := make(Generic)
|
||||
if unmarshalErr := json.Unmarshal(jsonGeneric, &clone); unmarshalErr != nil {
|
||||
clone := NewGeneric[Value]()
|
||||
if unmarshalErr := json.Unmarshal(jsonGeneric, clone); unmarshalErr != nil {
|
||||
panic(unmarshalErr)
|
||||
}
|
||||
return &clone
|
||||
return clone
|
||||
}
|
||||
|
||||
func (g *Generic) Type() string {
|
||||
func (g *Generic[Value]) Type() string {
|
||||
return "generic"
|
||||
}
|
||||
|
||||
func (g *Generic) Read(context.Context) ([]byte, error) {
|
||||
func (g *Generic[Value]) Read(context.Context) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (g *Generic) GetValue(name string) (result any, err error) {
|
||||
func (g *Generic[Value]) GetValue(name string) (result any, err error) {
|
||||
var ok bool
|
||||
if result, ok = (*g)[name]; !ok {
|
||||
err = ErrUnknownConfigurationKey
|
||||
|
@ -8,6 +8,6 @@ import (
|
||||
)
|
||||
|
||||
func TestNewGenericConfig(t *testing.T) {
|
||||
g := NewGeneric()
|
||||
g := NewGeneric[any]()
|
||||
assert.NotNil(t, g)
|
||||
}
|
||||
|
@ -13,10 +13,13 @@
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Config type name.",
|
||||
"enum": [ "generic", "exec" ]
|
||||
"enum": [ "generic", "exec", "certificate" ]
|
||||
},
|
||||
"values": {
|
||||
"type": "object"
|
||||
"oneOf": [
|
||||
{ "type": "object" },
|
||||
{ "$ref": "certificate.schema.json" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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)
|
||||
ContainerRemove(context.Context, string, container.RemoveOptions) error
|
||||
ContainerStop(context.Context, string, container.StopOptions) error
|
||||
ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error)
|
||||
ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
@ -79,6 +79,7 @@ type Container struct {
|
||||
|
||||
config ConfigurationValueGetter
|
||||
apiClient ContainerClient
|
||||
Resources ResourceMapper `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
@ -103,6 +104,10 @@ func NewContainer(containerClientApi ContainerClient) *Container {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) SetResourceMapper(resources ResourceMapper) {
|
||||
c.Resources = resources
|
||||
}
|
||||
|
||||
func (c *Container) Clone() Resource {
|
||||
return &Container {
|
||||
Id: c.Id,
|
||||
|
@ -23,9 +23,9 @@ _ "os/exec"
|
||||
)
|
||||
|
||||
type ContainerImageClient interface {
|
||||
ImagePull(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error)
|
||||
ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error)
|
||||
ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error)
|
||||
ImageRemove(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error)
|
||||
ImageRemove(ctx context.Context, imageID string, options image.RemoveOptions) ([]image.DeleteResponse, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
@ -44,12 +44,14 @@ type ContainerImage struct {
|
||||
|
||||
config ConfigurationValueGetter
|
||||
apiClient ContainerImageClient
|
||||
Resources ResourceMapper `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
ResourceTypes.Register([]string{"container-image"}, func(u *url.URL) Resource {
|
||||
c := NewContainerImage(nil)
|
||||
c.Name = strings.Join([]string{u.Hostname(), u.Path}, ":")
|
||||
c.Name = ContainerImageNameFromURI(u)
|
||||
slog.Info("NewContainerImage", "container", c)
|
||||
return c
|
||||
})
|
||||
}
|
||||
@ -68,6 +70,10 @@ func NewContainerImage(containerClientApi ContainerImageClient) *ContainerImage
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ContainerImage) SetResourceMapper(resources ResourceMapper) {
|
||||
c.Resources = resources
|
||||
}
|
||||
|
||||
func (c *ContainerImage) Clone() Resource {
|
||||
return &ContainerImage {
|
||||
Id: c.Id,
|
||||
@ -91,9 +97,9 @@ func (c *ContainerImage) StateMachine() machine.Stater {
|
||||
return c.stater
|
||||
}
|
||||
|
||||
func (c *ContainerImage) URI() string {
|
||||
func URIFromContainerImageName(imageName string) string {
|
||||
var host, namespace, repo string
|
||||
elements := strings.Split(c.Name, "/")
|
||||
elements := strings.Split(imageName, "/")
|
||||
switch len(elements) {
|
||||
case 1:
|
||||
repo = elements[0]
|
||||
@ -111,6 +117,31 @@ func (c *ContainerImage) URI() string {
|
||||
return fmt.Sprintf("container-image://%s/%s", host, strings.Join([]string{namespace, repo}, "/"))
|
||||
}
|
||||
|
||||
// Reconstruct the image name from a given parsed URL
|
||||
func ContainerImageNameFromURI(u *url.URL) string {
|
||||
var host string = u.Hostname()
|
||||
// var host, namespace, repo string
|
||||
elements := strings.FieldsFunc(u.RequestURI(), func(c rune) bool { return c == '/' })
|
||||
slog.Info("ContainerImageNameFromURI", "url", u, "elements", elements)
|
||||
/*
|
||||
switch len(elements) {
|
||||
case 1:
|
||||
repo = elements[0]
|
||||
case 2:
|
||||
namespace = elements[0]
|
||||
repo = elements[1]
|
||||
}
|
||||
*/
|
||||
if host == "" {
|
||||
return strings.Join(elements, "/")
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", host, strings.Join(elements, "/"))
|
||||
}
|
||||
|
||||
func (c *ContainerImage) URI() string {
|
||||
return URIFromContainerImageName(c.Name)
|
||||
}
|
||||
|
||||
func (c *ContainerImage) SetURI(uri string) error {
|
||||
resourceUri, e := url.Parse(uri)
|
||||
if e == nil {
|
||||
@ -209,30 +240,49 @@ func (c *ContainerImage) Create(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ContainerImage) Read(ctx context.Context) ([]byte, error) {
|
||||
out, err := c.apiClient.ImagePull(ctx, c.Name, types.ImagePullOptions{})
|
||||
slog.Info("Read()", "name", c.Name, "error", err)
|
||||
|
||||
_, outputErr := io.ReadAll(out)
|
||||
|
||||
func (c *ContainerImage) Pull(ctx context.Context) error {
|
||||
out, err := c.apiClient.ImagePull(ctx, c.Name, image.PullOptions{})
|
||||
slog.Info("ContainerImage.Pull()", "name", c.Name, "error", err)
|
||||
if err == nil {
|
||||
if _, outputErr := io.ReadAll(out); outputErr != nil {
|
||||
return fmt.Errorf("%w: %s %s", outputErr, c.Type(), c.Name)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("%w: %s %s", err, c.Type(), c.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ContainerImage) Inspect(ctx context.Context) (imageInspect types.ImageInspect) {
|
||||
var err error
|
||||
imageInspect, _, err = c.apiClient.ImageInspectWithRaw(ctx, c.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s %s", err, c.Type(), c.Name)
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if outputErr != nil {
|
||||
return nil, fmt.Errorf("%w: %s %s", outputErr, c.Type(), c.Name)
|
||||
}
|
||||
func (c *ContainerImage) Read(ctx context.Context) (resourceYaml []byte, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
c.State = "absent"
|
||||
resourceYaml = nil
|
||||
err = fmt.Errorf("%w", r.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
imageInspect, _, err := c.apiClient.ImageInspectWithRaw(ctx, c.Name)
|
||||
var imageInspect types.ImageInspect
|
||||
imageInspect, _, err = c.apiClient.ImageInspectWithRaw(ctx, c.Name)
|
||||
slog.Info("ContainerImage.Read()", "name", c.Name, "error", err)
|
||||
if err != nil {
|
||||
if client.IsErrNotFound(err) {
|
||||
slog.Info("ContainerImage.Read()", "oldstate", c.State, "newstate", "absent", "error", err)
|
||||
c.State = "absent"
|
||||
if pullErr := c.Pull(ctx); pullErr != nil {
|
||||
panic(pullErr)
|
||||
}
|
||||
imageInspect = c.Inspect(ctx)
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.State = "present"
|
||||
@ -249,13 +299,13 @@ func (c *ContainerImage) Read(ctx context.Context) ([]byte, error) {
|
||||
c.OS = imageInspect.Os
|
||||
c.Size = imageInspect.Size
|
||||
c.Comment = imageInspect.Comment
|
||||
slog.Info("Read() ", "type", c.Type(), "name", c.Name, "Id", c.Id)
|
||||
slog.Info("ContainerImage.Read()", "type", c.Type(), "name", c.Name, "Id", c.Id, "state", c.State, "error", err)
|
||||
return yaml.Marshal(c)
|
||||
}
|
||||
|
||||
func (c *ContainerImage) Delete(ctx context.Context) error {
|
||||
slog.Info("ContainerImage.Delete()", "image", c)
|
||||
options := types.ImageRemoveOptions{
|
||||
options := image.RemoveOptions{
|
||||
Force: false,
|
||||
PruneChildren: false,
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ _ "fmt"
|
||||
"io"
|
||||
_ "net/http"
|
||||
_ "net/http/httptest"
|
||||
_ "net/url"
|
||||
"net/url"
|
||||
_ "os"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -24,6 +24,45 @@ func TestNewContainerImageResource(t *testing.T) {
|
||||
assert.NotNil(t, c)
|
||||
}
|
||||
|
||||
func TestContainerImageURI(t *testing.T) {
|
||||
case0URI := URIFromContainerImageName("foo")
|
||||
assert.Equal(t, "container-image:///foo", case0URI)
|
||||
case1URI := URIFromContainerImageName("foo:bar")
|
||||
assert.Equal(t, "container-image:///foo:bar", case1URI)
|
||||
case2URI := URIFromContainerImageName("quuz/foo:bar")
|
||||
assert.Equal(t, "container-image:///quuz/foo:bar", case2URI)
|
||||
case3URI := URIFromContainerImageName("myhost/quuz/foo:bar")
|
||||
assert.Equal(t, "container-image://myhost/quuz/foo:bar", case3URI)
|
||||
}
|
||||
|
||||
func TestLoadFromContainerImageURI(t *testing.T) {
|
||||
testURI := URIFromContainerImageName("myhost/quuz/foo:bar")
|
||||
newResource, resourceErr := ResourceTypes.New(testURI)
|
||||
assert.Nil(t, resourceErr)
|
||||
assert.NotNil(t, newResource)
|
||||
assert.IsType(t, &ContainerImage{}, newResource)
|
||||
assert.Equal(t, "myhost/quuz/foo:bar", newResource.(*ContainerImage).Name)
|
||||
assert.Equal(t, testURI, newResource.URI())
|
||||
}
|
||||
|
||||
func TestContainerImageNameFromURI(t *testing.T) {
|
||||
case0u,_ := url.Parse("container-image:///foo")
|
||||
case0Image := ContainerImageNameFromURI(case0u)
|
||||
assert.Equal(t, "foo", case0Image)
|
||||
|
||||
case1u,_ := url.Parse("container-image:///foo:bar")
|
||||
case1Image := ContainerImageNameFromURI(case1u)
|
||||
assert.Equal(t, "foo:bar", case1Image)
|
||||
|
||||
case2u,_ := url.Parse("container-image:///quuz/foo:bar")
|
||||
case2Image := ContainerImageNameFromURI(case2u)
|
||||
assert.Equal(t, "quuz/foo:bar", case2Image)
|
||||
|
||||
case3u,_ := url.Parse("container-image://myhost/quuz/foo:bar")
|
||||
case3Image := ContainerImageNameFromURI(case3u)
|
||||
assert.Equal(t, "myhost/quuz/foo:bar", case3Image)
|
||||
}
|
||||
|
||||
func TestReadContainerImage(t *testing.T) {
|
||||
output := io.NopCloser(strings.NewReader("testdata"))
|
||||
ctx := context.Background()
|
||||
@ -32,10 +71,10 @@ func TestReadContainerImage(t *testing.T) {
|
||||
state: present
|
||||
`
|
||||
m := &mocks.MockContainerClient{
|
||||
InjectImagePull: func(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error) {
|
||||
InjectImagePull: func(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error) {
|
||||
return output, nil
|
||||
},
|
||||
InjectImageRemove: func(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
|
||||
InjectImageRemove: func(ctx context.Context, imageID string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
|
||||
return nil, nil
|
||||
},
|
||||
InjectImageInspectWithRaw: func(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) {
|
||||
|
@ -1,16 +1,13 @@
|
||||
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||||
|
||||
// Container resource
|
||||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
_ "github.com/docker/docker/api/types/mount"
|
||||
_ "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
_ "github.com/docker/docker/api/types/strslice"
|
||||
"github.com/docker/docker/client"
|
||||
"gopkg.in/yaml.v3"
|
||||
@ -24,22 +21,31 @@ _ "strings"
|
||||
"io"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
"decl/internal/codec"
|
||||
"log/slog"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ContainerNetworkClient interface {
|
||||
ContainerClient
|
||||
NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
|
||||
NetworkCreate(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error)
|
||||
NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error)
|
||||
NetworkInspect(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error)
|
||||
}
|
||||
|
||||
type ContainerNetwork struct {
|
||||
stater machine.Stater `json:"-" yaml:"-"`
|
||||
Id string `json:"ID,omitempty" yaml:"ID,omitempty"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
|
||||
Id string `json:"ID,omitempty" yaml:"ID,omitempty"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Driver string `json:"driver,omitempty" yaml:"driver,omitempty"`
|
||||
EnableIPv6 bool `json:"enableipv6,omitempty" yaml:"enableipv6,omitempty"`
|
||||
Internal bool `json:"internal,omitempty" yaml:"internal,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
|
||||
Created time.Time `json:"created" yaml:"created"`
|
||||
State string `yaml:"state"`
|
||||
|
||||
config ConfigurationValueGetter
|
||||
apiClient ContainerNetworkClient
|
||||
Resources ResourceMapper `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
@ -64,6 +70,10 @@ func NewContainerNetwork(containerClientApi ContainerNetworkClient) *ContainerNe
|
||||
}
|
||||
}
|
||||
|
||||
func (n *ContainerNetwork) SetResourceMapper(resources ResourceMapper) {
|
||||
n.Resources = resources
|
||||
}
|
||||
|
||||
func (n *ContainerNetwork) Clone() Resource {
|
||||
return &ContainerNetwork {
|
||||
Id: n.Id,
|
||||
@ -82,22 +92,48 @@ func (n *ContainerNetwork) StateMachine() machine.Stater {
|
||||
|
||||
func (n *ContainerNetwork) Notify(m *machine.EventMessage) {
|
||||
ctx := context.Background()
|
||||
slog.Info("Notify()", "ContainerNetwork", n, "m", m)
|
||||
switch m.On {
|
||||
case machine.ENTERSTATEEVENT:
|
||||
switch m.Dest {
|
||||
case "start_read":
|
||||
if _,readErr := n.Read(ctx); readErr == nil {
|
||||
if triggerErr := n.StateMachine().Trigger("state_read"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
n.State = "absent"
|
||||
panic(triggerErr)
|
||||
}
|
||||
} else {
|
||||
n.State = "absent"
|
||||
panic(readErr)
|
||||
}
|
||||
case "start_delete":
|
||||
if deleteErr := n.Delete(ctx); deleteErr == nil {
|
||||
if triggerErr := n.StateMachine().Trigger("deleted"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
n.State = "present"
|
||||
panic(triggerErr)
|
||||
}
|
||||
} else {
|
||||
n.State = "present"
|
||||
panic(deleteErr)
|
||||
}
|
||||
case "start_create":
|
||||
if e := n.Create(ctx); e == nil {
|
||||
if triggerErr := n.stater.Trigger("created"); triggerErr == nil {
|
||||
if triggerErr := n.StateMachine().Trigger("created"); triggerErr == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
n.State = "absent"
|
||||
case "present":
|
||||
case "absent":
|
||||
n.State = "absent"
|
||||
case "present", "created", "read":
|
||||
n.State = "present"
|
||||
}
|
||||
case machine.EXITSTATEEVENT:
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (n *ContainerNetwork) URI() string {
|
||||
@ -148,7 +184,7 @@ func (n *ContainerNetwork) LoadDecl(yamlResourceDeclaration string) error {
|
||||
}
|
||||
|
||||
func (n *ContainerNetwork) Create(ctx context.Context) error {
|
||||
networkResp, err := n.apiClient.NetworkCreate(ctx, n.Name, types.NetworkCreate{
|
||||
networkResp, err := n.apiClient.NetworkCreate(ctx, n.Name, network.CreateOptions{
|
||||
Driver: "bridge",
|
||||
})
|
||||
if err != nil {
|
||||
@ -159,9 +195,52 @@ func (n *ContainerNetwork) Create(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// produce yaml representation of any resource
|
||||
func (n *ContainerNetwork) Inspect(ctx context.Context, networkID string) error {
|
||||
networkInspect, err := n.apiClient.NetworkInspect(ctx, networkID, network.InspectOptions{})
|
||||
if client.IsErrNotFound(err) {
|
||||
n.State = "absent"
|
||||
} else {
|
||||
n.State = "present"
|
||||
n.Id = networkInspect.ID
|
||||
if n.Name == "" {
|
||||
if networkInspect.Name[0] == '/' {
|
||||
n.Name = networkInspect.Name[1:]
|
||||
} else {
|
||||
n.Name = networkInspect.Name
|
||||
}
|
||||
}
|
||||
n.Created = networkInspect.Created
|
||||
n.Internal = networkInspect.Internal
|
||||
n.Driver = networkInspect.Driver
|
||||
n.Labels = networkInspect.Labels
|
||||
n.EnableIPv6 = networkInspect.EnableIPv6
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *ContainerNetwork) Read(ctx context.Context) ([]byte, error) {
|
||||
var networkID string
|
||||
filterArgs := filters.NewArgs()
|
||||
filterArgs.Add("name", n.Name)
|
||||
networks, err := n.apiClient.NetworkList(ctx, network.ListOptions{
|
||||
Filters: filterArgs,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s %s", err, n.Type(), n.Name)
|
||||
}
|
||||
|
||||
for _, net := range networks {
|
||||
if net.Name == n.Name {
|
||||
networkID = net.ID
|
||||
}
|
||||
}
|
||||
|
||||
if inspectErr := n.Inspect(ctx, networkID); inspectErr != nil {
|
||||
return nil, fmt.Errorf("%w: network %s", inspectErr, networkID)
|
||||
}
|
||||
slog.Info("Read() ", "type", n.Type(), "name", n.Name, "Id", n.Id)
|
||||
|
||||
return yaml.Marshal(n)
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ _ "encoding/json"
|
||||
_ "fmt"
|
||||
_ "github.com/docker/docker/api/types"
|
||||
_ "github.com/docker/docker/api/types/container"
|
||||
_ "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/stretchr/testify/assert"
|
||||
_ "io"
|
||||
_ "net/http"
|
||||
@ -32,6 +32,19 @@ func TestReadContainerNetwork(t *testing.T) {
|
||||
state: present
|
||||
`
|
||||
m := &mocks.MockContainerClient{
|
||||
InjectNetworkList: func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
|
||||
return []network.Summary{
|
||||
{ID: "123456789abc"},
|
||||
{ID: "123456789def"},
|
||||
}, nil
|
||||
},
|
||||
InjectNetworkInspect: func(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error) {
|
||||
return network.Inspect{
|
||||
ID: "123456789abc",
|
||||
Name: "test",
|
||||
Driver: "bridge",
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
n := NewContainerNetwork(m)
|
||||
|
@ -46,8 +46,15 @@ func NewDeclaration() *Declaration {
|
||||
return &Declaration{}
|
||||
}
|
||||
|
||||
func NewDeclarationFromDocument(document *Document) *Declaration {
|
||||
return &Declaration{ document: document }
|
||||
}
|
||||
|
||||
func (d *Declaration) SetDocument(newDocument *Document) {
|
||||
slog.Info("Declaration.SetDocument()")
|
||||
d.document = newDocument
|
||||
d.SetConfig(d.document.config)
|
||||
d.Attributes.SetResourceMapper(d.document.uris)
|
||||
}
|
||||
|
||||
func (d *Declaration) ResolveId(ctx context.Context) string {
|
||||
@ -101,18 +108,22 @@ func (d *Declaration) Apply() (result error) {
|
||||
}()
|
||||
|
||||
stater := d.Attributes.StateMachine()
|
||||
slog.Info("Declaration.Apply()", "machine", stater, "machine.state", stater.CurrentState(), "uri", d.Attributes.URI())
|
||||
switch d.Transition {
|
||||
case "read":
|
||||
result = stater.Trigger("read")
|
||||
case "delete", "absent":
|
||||
slog.Info("Declaration.Apply()", "machine", stater, "machine.state", stater.CurrentState(), "uri", d.Attributes.URI())
|
||||
if stater.CurrentState() == "present" {
|
||||
result = stater.Trigger("delete")
|
||||
}
|
||||
case "update":
|
||||
if result = stater.Trigger("update"); result != nil {
|
||||
return result
|
||||
}
|
||||
result = stater.Trigger("read")
|
||||
default:
|
||||
fallthrough
|
||||
case "create", "present":
|
||||
slog.Info("Declaration.Apply()", "machine", stater, "machine.state", stater.CurrentState(), "uri", d.Attributes.URI())
|
||||
if stater.CurrentState() == "absent" || stater.CurrentState() == "unknown" {
|
||||
if result = stater.Trigger("create"); result != nil {
|
||||
return result
|
||||
@ -150,6 +161,7 @@ func (d *Declaration) UnmarshalValue(value *DeclarationType) error {
|
||||
d.Config = value.Config
|
||||
newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", value.Type))
|
||||
if resourceErr != nil {
|
||||
slog.Info("Declaration.UnmarshalValue", "value", value, "error", resourceErr)
|
||||
return resourceErr
|
||||
}
|
||||
d.Attributes = newResource
|
||||
|
@ -19,6 +19,15 @@ _ "net/url"
|
||||
|
||||
type ResourceMap[Value any] map[string]Value
|
||||
|
||||
func (rm ResourceMap[Value]) Get(key string) (any, bool) {
|
||||
v, ok := rm[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
type ResourceMapper interface {
|
||||
Get(key string) (any, bool)
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
uris ResourceMap[*Declaration]
|
||||
ResourceDecls []Declaration `json:"resources" yaml:"resources"`
|
||||
@ -66,10 +75,10 @@ func (d *Document) Clone() *Document {
|
||||
func (d *Document) Load(r io.Reader) (err error) {
|
||||
c := codec.NewYAMLDecoder(r)
|
||||
err = c.Decode(d)
|
||||
slog.Info("Document.Load()", "error", err)
|
||||
if err == nil {
|
||||
for i := range d.ResourceDecls {
|
||||
d.ResourceDecls[i].SetDocument(d)
|
||||
d.ResourceDecls[i].SetConfig(d.config)
|
||||
}
|
||||
}
|
||||
return
|
||||
@ -158,22 +167,23 @@ func (d *Document) MapResourceURI(uri string, declaration *Declaration) {
|
||||
}
|
||||
|
||||
func (d *Document) AddResourceDeclaration(resourceType string, resourceDeclaration Resource) {
|
||||
decl := NewDeclaration()
|
||||
slog.Info("Document.AddResourceDeclaration()", "type", resourceType, "resource", resourceDeclaration)
|
||||
decl := NewDeclarationFromDocument(d)
|
||||
decl.Type = TypeName(resourceType)
|
||||
decl.Attributes = resourceDeclaration
|
||||
decl.SetDocument(d)
|
||||
d.ResourceDecls = append(d.ResourceDecls, *decl)
|
||||
d.MapResourceURI(decl.Attributes.URI(), decl)
|
||||
decl.SetDocument(d)
|
||||
}
|
||||
|
||||
func (d *Document) AddResource(uri string) error {
|
||||
decl := NewDeclaration()
|
||||
decl := NewDeclarationFromDocument(d)
|
||||
if e := decl.SetURI(uri); e != nil {
|
||||
return e
|
||||
}
|
||||
decl.SetDocument(d)
|
||||
d.ResourceDecls = append(d.ResourceDecls, *decl)
|
||||
d.MapResourceURI(decl.Attributes.URI(), decl)
|
||||
decl.SetDocument(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -228,3 +238,31 @@ func (d *Document) Diff(with *Document, output io.Writer) (returnOutput string,
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
|
||||
func (d *Document) UnmarshalYAML(value *yaml.Node) error {
|
||||
type decodeDocument Document
|
||||
t := (*decodeDocument)(d)
|
||||
if unmarshalDocumentErr := value.Decode(t); unmarshalDocumentErr != nil {
|
||||
return unmarshalDocumentErr
|
||||
}
|
||||
for i := range d.ResourceDecls {
|
||||
d.ResourceDecls[i].SetDocument(d)
|
||||
d.MapResourceURI(d.ResourceDecls[i].Attributes.URI(), &d.ResourceDecls[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Document) UnmarshalJSON(data []byte) error {
|
||||
type decodeDocument Document
|
||||
t := (*decodeDocument)(d)
|
||||
if unmarshalDocumentErr := json.Unmarshal(data, t); unmarshalDocumentErr != nil {
|
||||
return unmarshalDocumentErr
|
||||
}
|
||||
for i := range d.ResourceDecls {
|
||||
d.ResourceDecls[i].SetDocument(d)
|
||||
d.MapResourceURI(d.ResourceDecls[i].Attributes.URI(), &d.ResourceDecls[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ type Exec struct {
|
||||
config ConfigurationValueGetter
|
||||
// state attributes
|
||||
State string `yaml:"state"`
|
||||
Resources ResourceMapper `yaml:"-" json:"-"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
@ -41,6 +42,10 @@ func NewExec() *Exec {
|
||||
return &Exec{}
|
||||
}
|
||||
|
||||
func (x *Exec) SetResourceMapper(resources ResourceMapper) {
|
||||
x.Resources = resources
|
||||
}
|
||||
|
||||
func (x *Exec) Clone() Resource {
|
||||
return &Exec {
|
||||
Id: x.Id,
|
||||
|
@ -24,8 +24,10 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Describes the type of file the resource represents
|
||||
type FileType string
|
||||
|
||||
// Supported file types
|
||||
const (
|
||||
RegularFile FileType = "regular"
|
||||
DirectoryFile FileType = "directory"
|
||||
@ -50,7 +52,15 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
// Manage the state of file system objects
|
||||
/*
|
||||
|
||||
Manage the state of file system objects
|
||||
The file content may be serialized directly in the `Content` field
|
||||
or the `ContentSourceRef/sourceref` may be used to refer to the source
|
||||
of the content from which to stream the content.
|
||||
The `SerializeContent` the flag allows forcing the content to be serialized in the output.
|
||||
|
||||
*/
|
||||
type File struct {
|
||||
stater machine.Stater `json:"-" yaml:"-"`
|
||||
normalizePath bool `json:"-" yaml:"-"`
|
||||
@ -72,6 +82,7 @@ type File struct {
|
||||
State string `json:"state,omitempty" yaml:"state,omitempty"`
|
||||
SerializeContent bool `json:"serializecontent,omitempty" yaml:"serializecontent,omitempty"`
|
||||
config ConfigurationValueGetter
|
||||
Resources ResourceMapper `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
type ResourceFileInfo struct {
|
||||
@ -92,6 +103,10 @@ func NewNormalizedFile() *File {
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *File) SetResourceMapper(resources ResourceMapper) {
|
||||
f.Resources = resources
|
||||
}
|
||||
|
||||
func (f *File) Clone() Resource {
|
||||
return &File {
|
||||
normalizePath: f.normalizePath,
|
||||
@ -305,7 +320,7 @@ func (f *File) Create(ctx context.Context) error {
|
||||
f.Size = 0
|
||||
var contentReader io.ReadCloser
|
||||
if len(f.Content) == 0 && len(f.ContentSourceRef) != 0 {
|
||||
if refReader, err := f.ContentSourceRef.ContentReaderStream(); err == nil {
|
||||
if refReader, err := f.ContentSourceRef.Lookup(nil).ContentReaderStream(); err == nil {
|
||||
contentReader = refReader
|
||||
} else {
|
||||
return err
|
||||
|
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"`
|
||||
State string `yaml:"state,omitempty" json:"state,omitempty"`
|
||||
config ConfigurationValueGetter
|
||||
Resources ResourceMapper `yaml:"-" json:"-"`
|
||||
}
|
||||
|
||||
func NewHTTP() *HTTP {
|
||||
return &HTTP{ client: &http.Client{} }
|
||||
}
|
||||
|
||||
func (h *HTTP) SetResourceMapper(resources ResourceMapper) {
|
||||
h.Resources = resources
|
||||
}
|
||||
|
||||
func (h *HTTP) Clone() Resource {
|
||||
return &HTTP {
|
||||
client: h.client,
|
||||
|
@ -129,6 +129,7 @@ type Iptable struct {
|
||||
DeleteCommand *command.Command `yaml:"-" json:"-"`
|
||||
|
||||
config ConfigurationValueGetter
|
||||
Resources ResourceMapper `yaml:"-" json:"-"`
|
||||
}
|
||||
|
||||
func NewIptable() *Iptable {
|
||||
@ -137,6 +138,10 @@ func NewIptable() *Iptable {
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *Iptable) SetResourceMapper(resources ResourceMapper) {
|
||||
i.Resources = resources
|
||||
}
|
||||
|
||||
func (i *Iptable) Clone() Resource {
|
||||
newIpt := &Iptable {
|
||||
Id: i.Id,
|
||||
|
@ -126,6 +126,7 @@ type NetworkRoute struct {
|
||||
|
||||
State string `json:"state" yaml:"state"`
|
||||
config ConfigurationValueGetter
|
||||
Resources ResourceMapper `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
func NewNetworkRoute() *NetworkRoute {
|
||||
@ -134,6 +135,10 @@ func NewNetworkRoute() *NetworkRoute {
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *NetworkRoute) SetResourceMapper(resources ResourceMapper) {
|
||||
n.Resources = resources
|
||||
}
|
||||
|
||||
func (n *NetworkRoute) Clone() Resource {
|
||||
newn := &NetworkRoute {
|
||||
Id: n.Id,
|
||||
|
@ -77,3 +77,12 @@ func LookupGID(groupName string) (int, error) {
|
||||
|
||||
return gid, nil
|
||||
}
|
||||
|
||||
func LookupGIDString(groupName string) string {
|
||||
group, groupLookupErr := user.LookupGroup(groupName)
|
||||
if groupLookupErr != nil {
|
||||
return ""
|
||||
}
|
||||
return group.Gid
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,9 @@ const (
|
||||
PackageTypeYum PackageType = "yum"
|
||||
)
|
||||
|
||||
var ErrUnsupportedPackageType error = errors.New("The PackageType is not supported on this system")
|
||||
var ErrInvalidPackageType error = errors.New("invalid PackageType value")
|
||||
|
||||
var SystemPackageType PackageType = FindSystemPackageType()
|
||||
|
||||
type Package struct {
|
||||
@ -49,6 +52,7 @@ type Package struct {
|
||||
// state attributes
|
||||
State string `yaml:"state,omitempty" json:"state,omitempty"`
|
||||
config ConfigurationValueGetter
|
||||
Resources ResourceMapper `yaml:"-" json:"-"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
@ -72,6 +76,10 @@ func NewPackage() *Package {
|
||||
return &Package{ PackageType: SystemPackageType }
|
||||
}
|
||||
|
||||
func (p *Package) SetResourceMapper(resources ResourceMapper) {
|
||||
p.Resources = resources
|
||||
}
|
||||
|
||||
func (p *Package) Clone() Resource {
|
||||
newp := &Package {
|
||||
Name: p.Name,
|
||||
@ -228,15 +236,19 @@ func (p *Package) LoadDecl(yamlResourceDeclaration string) error {
|
||||
func (p *Package) Type() string { return "package" }
|
||||
|
||||
func (p *Package) Read(ctx context.Context) ([]byte, error) {
|
||||
out, err := p.ReadCommand.Execute(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if p.ReadCommand.Exists() {
|
||||
out, err := p.ReadCommand.Execute(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exErr := p.ReadCommand.Extractor(out, p)
|
||||
if exErr != nil {
|
||||
return nil, exErr
|
||||
}
|
||||
return yaml.Marshal(p)
|
||||
} else {
|
||||
return nil, ErrUnsupportedPackageType
|
||||
}
|
||||
exErr := p.ReadCommand.Extractor(out, p)
|
||||
if exErr != nil {
|
||||
return nil, exErr
|
||||
}
|
||||
return yaml.Marshal(p)
|
||||
}
|
||||
|
||||
func (p *Package) UnmarshalJSON(data []byte) error {
|
||||
@ -325,7 +337,7 @@ func (p *PackageType) UnmarshalValue(value string) error {
|
||||
*p = PackageType(value)
|
||||
return nil
|
||||
default:
|
||||
return errors.New("invalid PackageType value")
|
||||
return ErrInvalidPackageType
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,7 @@ type: apk
|
||||
p := NewPackage()
|
||||
assert.NotNil(t, p)
|
||||
m := &MockCommand{
|
||||
CommandExists: func() error { return nil },
|
||||
Executor: func(value any) ([]byte, error) {
|
||||
return nil, nil
|
||||
},
|
||||
@ -134,3 +135,27 @@ Version: 1.2.2
|
||||
assert.Equal(t, "1.2.2", p.Version)
|
||||
assert.Nil(t, p.Validate())
|
||||
}
|
||||
|
||||
func TestPackageTypeErr(t *testing.T) {
|
||||
|
||||
decl := `
|
||||
name: vim
|
||||
source: vim-8.2.3995-1ubuntu2.17.deb
|
||||
type: deb
|
||||
`
|
||||
p := NewPackage()
|
||||
assert.NotNil(t, p)
|
||||
loadErr := p.LoadDecl(decl)
|
||||
assert.Nil(t, loadErr)
|
||||
p.ReadCommand = NewDebReadCommand()
|
||||
p.ReadCommand.CommandExists = func() error { return command.ErrUnknownCommand }
|
||||
p.ReadCommand.Executor = func(value any) ([]byte, error) {
|
||||
return []byte(`
|
||||
Package: vim
|
||||
Version: 1.2.2
|
||||
`), nil
|
||||
}
|
||||
_, readErr := p.Read(context.Background())
|
||||
assert.ErrorIs(t, readErr, ErrUnsupportedPackageType)
|
||||
|
||||
}
|
||||
|
522
internal/resource/pki.go
Normal file
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"
|
||||
_ "fmt"
|
||||
_ "gopkg.in/yaml.v3"
|
||||
_ "net/url"
|
||||
"net/url"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
"decl/internal/transport"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type ResourceReference string
|
||||
@ -29,6 +30,7 @@ type Resource interface {
|
||||
ResourceReader
|
||||
ResourceValidator
|
||||
Clone() Resource
|
||||
SetResourceMapper(resources ResourceMapper)
|
||||
}
|
||||
|
||||
type ContentReader interface {
|
||||
@ -39,6 +41,11 @@ type ContentWriter interface {
|
||||
ContentWriterStream() (*transport.Writer, error)
|
||||
}
|
||||
|
||||
type ContentReadWriter interface {
|
||||
ContentReader
|
||||
ContentWriter
|
||||
}
|
||||
|
||||
type ResourceValidator interface {
|
||||
Validate() error
|
||||
}
|
||||
@ -77,6 +84,39 @@ func NewResource(uri string) Resource {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return a Content ReadWriter for the resource referred to.
|
||||
func (r ResourceReference) Lookup(look ResourceMapper) ContentReadWriter {
|
||||
slog.Info("ResourceReference.Lookup()", "resourcereference", r, "resourcemapper", look)
|
||||
if look != nil {
|
||||
if v,ok := look.Get(string(r)); ok {
|
||||
return v.(ContentReadWriter)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ResourceReference) Dereference(look ResourceMapper) Resource {
|
||||
slog.Info("ResourceReference.Dereference()", "resourcereference", r, "resourcemapper", look)
|
||||
if look != nil {
|
||||
if v,ok := look.Get(string(r)); ok {
|
||||
return v.(*Declaration).Attributes
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r ResourceReference) Parse() *url.URL {
|
||||
u, e := url.Parse(string(r))
|
||||
if e == nil {
|
||||
return u
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r ResourceReference) Exists() bool {
|
||||
return transport.ExistsURI(string(r))
|
||||
}
|
||||
|
||||
func (r ResourceReference) ContentReaderStream() (*transport.Reader, error) {
|
||||
return transport.NewReaderURI(string(r))
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import (
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
//go:embed schemas/*.jsonschema
|
||||
//go:embed schemas/*.schema.json
|
||||
var schemaFiles embed.FS
|
||||
|
||||
@ -22,7 +21,7 @@ type Schema struct {
|
||||
}
|
||||
|
||||
func NewSchema(name string) *Schema {
|
||||
path := fmt.Sprintf("file://schemas/%s.jsonschema", name)
|
||||
path := fmt.Sprintf("file://schemas/%s.schema.json", name)
|
||||
|
||||
return &Schema{schema: gojsonschema.NewReferenceLoaderFileSystem(path, http.FS(schemaFiles))}
|
||||
//return &Schema{schema: gojsonschema.NewReferenceLoader(path)}
|
||||
@ -44,6 +43,7 @@ func (s *Schema) Validate(source string) error {
|
||||
for _, err := range result.Errors() {
|
||||
schemaErrors.WriteString(err.String() + "\n")
|
||||
}
|
||||
schemaErrors.WriteString(source)
|
||||
return errors.New(schemaErrors.String())
|
||||
}
|
||||
return nil
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$id": "container-declaration.jsonschema",
|
||||
"$id": "container-declaration.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "declaration",
|
||||
"type": "object",
|
||||
@ -11,7 +11,7 @@
|
||||
"enum": [ "container" ]
|
||||
},
|
||||
"attributes": {
|
||||
"$ref": "container.jsonschema"
|
||||
"$ref": "container.schema.json"
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z]([-_a-z0-9:]{0,31})$"
|
||||
"pattern": "^(?:[-0-9A-Za-z_.]+((?::[0-9]+|)(?:/[-a-z0-9._]+/[-a-z0-9._]+))|)(?:/|)(?:[-a-z0-9._]+(?:/[-a-z0-9._]+|))(:(?:[-0-9A-Za-z_.]{1,127})|)$"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
"properties": {
|
||||
"name": {
|
||||
"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#",
|
||||
"title": "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#",
|
||||
"title": "exec-declaration",
|
||||
"type": "object",
|
||||
@ -11,7 +11,7 @@
|
||||
"enum": [ "exec" ]
|
||||
},
|
||||
"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#",
|
||||
"title": "exec",
|
||||
"type": "object",
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$id": "file-declaration.jsonschema",
|
||||
"$id": "file-declaration.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "file-declaration",
|
||||
"type": "object",
|
||||
@ -15,7 +15,7 @@
|
||||
"description": "Config name"
|
||||
},
|
||||
"attributes": {
|
||||
"$ref": "file.jsonschema"
|
||||
"$ref": "file.schema.json"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$id": "file.jsonschema",
|
||||
"$id": "file.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "file",
|
||||
"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#",
|
||||
"title": "http-declaration",
|
||||
"type": "object",
|
||||
@ -15,7 +15,7 @@
|
||||
"description": "Config name."
|
||||
},
|
||||
"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#",
|
||||
"title": "http",
|
||||
"type": "object",
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$id": "iptable-declaration.jsonschema",
|
||||
"$id": "iptable-declaration.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "iptable-declaration",
|
||||
"type": "object",
|
||||
@ -15,7 +15,7 @@
|
||||
"description": "Config name"
|
||||
},
|
||||
"attributes": {
|
||||
"$ref": "iptable.jsonschema"
|
||||
"$ref": "iptable.schema.json"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$id": "iptable.jsonschema",
|
||||
"$id": "iptable.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "iptable",
|
||||
"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#",
|
||||
"title": "network-route-declaration",
|
||||
"type": "object",
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$id": "package-declaration.jsonschema",
|
||||
"$id": "package-declaration.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "package-declaration",
|
||||
"type": "object",
|
||||
@ -11,7 +11,7 @@
|
||||
"enum": [ "package" ]
|
||||
},
|
||||
"attributes": {
|
||||
"$ref": "package.jsonschema"
|
||||
"$ref": "package.schema.json"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$id": "package.jsonschema",
|
||||
"$id": "package.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "package",
|
||||
"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#",
|
||||
"title": "declaration",
|
||||
"type": "object",
|
||||
@ -14,7 +14,7 @@
|
||||
"$ref": "storagetransition.schema.json"
|
||||
},
|
||||
"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#",
|
||||
"title": "user",
|
||||
"description": "A user account",
|
||||
@ -8,7 +8,7 @@
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z]([-_a-z0-9]{0,31})$"
|
||||
"pattern": "^[_a-z]([-_a-z0-9]{0,31})$"
|
||||
},
|
||||
"uid": {
|
||||
"type": "string",
|
@ -36,6 +36,7 @@ type Service struct {
|
||||
|
||||
State string `yaml:"state,omitempty" json:"state,omitempty"`
|
||||
config ConfigurationValueGetter
|
||||
Resources ResourceMapper `yaml:"-" json:"-"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
@ -107,6 +108,10 @@ func (s *Service) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) SetResourceMapper(resources ResourceMapper) {
|
||||
s.Resources = resources
|
||||
}
|
||||
|
||||
func (s *Service) Clone() Resource {
|
||||
news := &Service{
|
||||
Name: s.Name,
|
||||
|
@ -12,11 +12,12 @@ _ "os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"io"
|
||||
"strings"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"gitea.rosskeen.house/rosskeen.house/machine"
|
||||
"strings"
|
||||
"decl/internal/codec"
|
||||
"decl/internal/command"
|
||||
)
|
||||
|
||||
type decodeUser User
|
||||
@ -28,6 +29,11 @@ const (
|
||||
UserTypeUserAdd = "useradd"
|
||||
)
|
||||
|
||||
var ErrUnsupportedUserType error = errors.New("The UserType is not supported on this system")
|
||||
var ErrInvalidUserType error = errors.New("invalid UserType value")
|
||||
|
||||
var SystemUserType UserType = FindSystemUserType()
|
||||
|
||||
type User struct {
|
||||
stater machine.Stater `json:"-" yaml:"-"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
@ -40,12 +46,13 @@ type User struct {
|
||||
Shell string `json:"shell,omitempty" yaml:"shell,omitempty"`
|
||||
UserType UserType `json:"-" yaml:"-"`
|
||||
|
||||
CreateCommand *Command `json:"-" yaml:"-"`
|
||||
ReadCommand *Command `json:"-" yaml:"-"`
|
||||
UpdateCommand *Command `json:"-" yaml:"-"`
|
||||
DeleteCommand *Command `json:"-" yaml:"-"`
|
||||
CreateCommand *command.Command `json:"-" yaml:"-"`
|
||||
ReadCommand *command.Command `json:"-" yaml:"-"`
|
||||
UpdateCommand *command.Command `json:"-" yaml:"-"`
|
||||
DeleteCommand *command.Command `json:"-" yaml:"-"`
|
||||
State string `json:"state,omitempty" yaml:"state,omitempty"`
|
||||
config ConfigurationValueGetter
|
||||
Resources ResourceMapper `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
func NewUser() *User {
|
||||
@ -68,6 +75,20 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
func FindSystemUserType() UserType {
|
||||
for _, userType := range []UserType{UserTypeAddUser, UserTypeUserAdd} {
|
||||
c := userType.NewCreateCommand()
|
||||
if c.Exists() {
|
||||
return userType
|
||||
}
|
||||
}
|
||||
return UserTypeAddUser
|
||||
}
|
||||
|
||||
func (u *User) SetResourceMapper(resources ResourceMapper) {
|
||||
u.Resources = resources
|
||||
}
|
||||
|
||||
func (u *User) Clone() Resource {
|
||||
newu := &User {
|
||||
Name: u.Name,
|
||||
@ -97,6 +118,30 @@ func (u *User) Notify(m *machine.EventMessage) {
|
||||
switch m.On {
|
||||
case machine.ENTERSTATEEVENT:
|
||||
switch m.Dest {
|
||||
case "start_read":
|
||||
if _,readErr := u.Read(ctx); readErr == nil {
|
||||
if triggerErr := u.StateMachine().Trigger("state_read"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
u.State = "absent"
|
||||
panic(triggerErr)
|
||||
}
|
||||
} else {
|
||||
u.State = "absent"
|
||||
panic(readErr)
|
||||
}
|
||||
case "start_delete":
|
||||
if deleteErr := u.Delete(ctx); deleteErr == nil {
|
||||
if triggerErr := u.StateMachine().Trigger("deleted"); triggerErr == nil {
|
||||
return
|
||||
} else {
|
||||
u.State = "present"
|
||||
panic(triggerErr)
|
||||
}
|
||||
} else {
|
||||
u.State = "present"
|
||||
panic(deleteErr)
|
||||
}
|
||||
case "start_create":
|
||||
if e := u.Create(ctx); e == nil {
|
||||
if triggerErr := u.stater.Trigger("created"); triggerErr == nil {
|
||||
@ -104,7 +149,9 @@ func (u *User) Notify(m *machine.EventMessage) {
|
||||
}
|
||||
}
|
||||
u.State = "absent"
|
||||
case "present":
|
||||
case "absent":
|
||||
u.State = "absent"
|
||||
case "present", "created", "read":
|
||||
u.State = "present"
|
||||
}
|
||||
case machine.EXITSTATEEVENT:
|
||||
@ -140,6 +187,7 @@ func (u *User) Validate() error {
|
||||
}
|
||||
|
||||
func (u *User) Apply() error {
|
||||
ctx := context.Background()
|
||||
switch u.State {
|
||||
case "present":
|
||||
_, NoUserExists := LookupUID(u.Name)
|
||||
@ -148,7 +196,7 @@ func (u *User) Apply() error {
|
||||
return cmdErr
|
||||
}
|
||||
case "absent":
|
||||
cmdErr := u.Delete()
|
||||
cmdErr := u.Delete(ctx)
|
||||
return cmdErr
|
||||
}
|
||||
return nil
|
||||
@ -162,33 +210,6 @@ func (u *User) LoadDecl(yamlResourceDeclaration string) error {
|
||||
return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(u)
|
||||
}
|
||||
|
||||
func (u *User) AddUserCommand(args *[]string) error {
|
||||
*args = append(*args, "-D")
|
||||
if u.Group != "" {
|
||||
*args = append(*args, "-G", u.Group)
|
||||
}
|
||||
if u.Home != "" {
|
||||
*args = append(*args, "-h", u.Home)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) UserAddCommand(args *[]string) error {
|
||||
if u.Group != "" {
|
||||
*args = append(*args, "-g", u.Group)
|
||||
}
|
||||
if len(u.Groups) > 0 {
|
||||
*args = append(*args, "-G", strings.Join(u.Groups, ","))
|
||||
}
|
||||
if u.Home != "" {
|
||||
*args = append(*args, "-d", u.Home)
|
||||
}
|
||||
if u.CreateHome {
|
||||
*args = append(*args, "-m")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Type() string { return "user" }
|
||||
|
||||
func (u *User) Create(ctx context.Context) (error) {
|
||||
@ -213,7 +234,7 @@ func (u *User) Read(ctx context.Context) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) Delete() (error) {
|
||||
func (u *User) Delete(ctx context.Context) (error) {
|
||||
_, err := u.DeleteCommand.Execute(u)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -238,7 +259,7 @@ func (u *User) UnmarshalYAML(value *yaml.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserType) NewCRUD() (create *Command, read *Command, update *Command, del *Command) {
|
||||
func (u *UserType) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) {
|
||||
switch *u {
|
||||
case UserTypeUserAdd:
|
||||
return NewUserAddCreateCommand(), NewUserReadCommand(), NewUserUpdateCommand(), NewUserDelDeleteCommand()
|
||||
@ -257,13 +278,33 @@ func (u *UserType) NewCRUD() (create *Command, read *Command, update *Command, d
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UserType) NewCreateCommand() (create *command.Command) {
|
||||
switch *u {
|
||||
case UserTypeUserAdd:
|
||||
return NewUserAddCreateCommand()
|
||||
case UserTypeAddUser:
|
||||
return NewAddUserCreateCommand()
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserType) NewReadCommand() (*command.Command) {
|
||||
return NewUserReadCommand()
|
||||
}
|
||||
|
||||
func (p *UserType) NewReadUsersCommand() (*command.Command) {
|
||||
return NewReadUsersCommand()
|
||||
}
|
||||
|
||||
|
||||
func (u *UserType) UnmarshalValue(value string) error {
|
||||
switch value {
|
||||
case string(UserTypeUserAdd), string(UserTypeAddUser):
|
||||
*u = UserType(value)
|
||||
return nil
|
||||
default:
|
||||
return errors.New("invalid UserType value")
|
||||
return ErrInvalidUserType
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,17 +324,60 @@ func (u *UserType) UnmarshalYAML(value *yaml.Node) error {
|
||||
return u.UnmarshalValue(s)
|
||||
}
|
||||
|
||||
func NewUserAddCreateCommand() *Command {
|
||||
c := NewCommand()
|
||||
func NewReadUsersCommand() *command.Command {
|
||||
c := command.NewCommand()
|
||||
c.Path = "getent"
|
||||
c.Args = []command.CommandArg{
|
||||
command.CommandArg("passwd"),
|
||||
}
|
||||
|
||||
c.Extractor = func(out []byte, target any) error {
|
||||
Users := target.(*[]*User)
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
lineIndex := 0
|
||||
for _, line := range lines {
|
||||
userRecord := strings.Split(strings.TrimSpace(line), ":")
|
||||
if len(*Users) <= lineIndex + 1 {
|
||||
*Users = append(*Users, NewUser())
|
||||
}
|
||||
u := (*Users)[lineIndex]
|
||||
u.Name = userRecord[0]
|
||||
u.UID = userRecord[2]
|
||||
u.Gecos = userRecord[4]
|
||||
u.Home = userRecord[5]
|
||||
u.Shell = userRecord[6]
|
||||
if readUser, userErr := user.Lookup(u.Name); userErr == nil {
|
||||
if groups, groupsErr := readUser.GroupIds(); groupsErr == nil {
|
||||
for _, secondaryGroup := range groups {
|
||||
if readGroup, groupErr := user.LookupGroupId(secondaryGroup); groupErr == nil {
|
||||
u.Groups = append(u.Groups, readGroup.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if readGroup, groupErr := user.LookupGroupId(userRecord[3]); groupErr == nil {
|
||||
u.Group = readGroup.Name
|
||||
}
|
||||
u.State = "present"
|
||||
u.UserType = SystemUserType
|
||||
lineIndex++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func NewUserAddCreateCommand() *command.Command {
|
||||
c := command.NewCommand()
|
||||
c.Path = "useradd"
|
||||
c.Args = []CommandArg{
|
||||
CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
|
||||
CommandArg("{{ if .Gecos }}-c {{ .Gecos }}{{ end }}"),
|
||||
CommandArg("{{ if .Group }}-g {{ .Group }}{{ end }}"),
|
||||
CommandArg("{{ if .Groups }}-G {{ range .Groups }}{{ . }},{{- end }}{{ end }}"),
|
||||
CommandArg("{{ if .Home }}-d {{ .Home }}{{ end }}"),
|
||||
CommandArg("{{ if .CreateHome }}-m{{ else }}-M{{ end }}"),
|
||||
CommandArg("{{ .Name }}"),
|
||||
c.Args = []command.CommandArg{
|
||||
command.CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
|
||||
command.CommandArg("{{ if .Gecos }}-c {{ .Gecos }}{{ end }}"),
|
||||
command.CommandArg("{{ if .Group }}-g {{ .Group }}{{ end }}"),
|
||||
command.CommandArg("{{ if .Groups }}-G {{ range .Groups }}{{ . }},{{- end }}{{ end }}"),
|
||||
command.CommandArg("{{ if .Home }}-d {{ .Home }}{{ end }}"),
|
||||
command.CommandArg("{{ if .CreateHome }}-m{{ else }}-M{{ end }}"),
|
||||
command.CommandArg("{{ .Name }}"),
|
||||
}
|
||||
c.Extractor = func(out []byte, target any) error {
|
||||
return nil
|
||||
@ -309,17 +393,17 @@ func NewUserAddCreateCommand() *Command {
|
||||
return c
|
||||
}
|
||||
|
||||
func NewAddUserCreateCommand() *Command {
|
||||
c := NewCommand()
|
||||
func NewAddUserCreateCommand() *command.Command {
|
||||
c := command.NewCommand()
|
||||
c.Path = "adduser"
|
||||
c.Args = []CommandArg{
|
||||
CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
|
||||
CommandArg("{{ if .Gecos }}-g {{ .Gecos }}{{ end }}"),
|
||||
CommandArg("{{ if .Group }}-G {{ .Group }}{{ end }}"),
|
||||
CommandArg("{{ if .Home }}-h {{ .Home }}{{ end }}"),
|
||||
CommandArg("{{ if not .CreateHome }}-H{{ end }}"),
|
||||
CommandArg("-D"),
|
||||
CommandArg("{{ .Name }}"),
|
||||
c.Args = []command.CommandArg{
|
||||
command.CommandArg("{{ if .UID }}-u {{ .UID }}{{ end }}"),
|
||||
command.CommandArg("{{ if .Gecos }}-g {{ .Gecos }}{{ end }}"),
|
||||
command.CommandArg("{{ if .Group }}-G {{ .Group }}{{ end }}"),
|
||||
command.CommandArg("{{ if .Home }}-h {{ .Home }}{{ end }}"),
|
||||
command.CommandArg("{{ if not .CreateHome }}-H{{ end }}"),
|
||||
command.CommandArg("-D"),
|
||||
command.CommandArg("{{ .Name }}"),
|
||||
}
|
||||
c.Extractor = func(out []byte, target any) error {
|
||||
return nil
|
||||
@ -335,8 +419,8 @@ func NewAddUserCreateCommand() *Command {
|
||||
return c
|
||||
}
|
||||
|
||||
func NewUserReadCommand() *Command {
|
||||
c := NewCommand()
|
||||
func NewUserReadCommand() *command.Command {
|
||||
c := command.NewCommand()
|
||||
c.Extractor = func(out []byte, target any) error {
|
||||
u := target.(*User)
|
||||
u.State = "absent"
|
||||
@ -369,15 +453,15 @@ func NewUserReadCommand() *Command {
|
||||
return c
|
||||
}
|
||||
|
||||
func NewUserUpdateCommand() *Command {
|
||||
func NewUserUpdateCommand() *command.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewUserDelDeleteCommand() *Command {
|
||||
c := NewCommand()
|
||||
func NewUserDelDeleteCommand() *command.Command {
|
||||
c := command.NewCommand()
|
||||
c.Path = "userdel"
|
||||
c.Args = []CommandArg{
|
||||
CommandArg("{{ .Name }}"),
|
||||
c.Args = []command.CommandArg{
|
||||
command.CommandArg("{{ .Name }}"),
|
||||
}
|
||||
c.Extractor = func(out []byte, target any) error {
|
||||
return nil
|
||||
@ -385,11 +469,11 @@ func NewUserDelDeleteCommand() *Command {
|
||||
return c
|
||||
}
|
||||
|
||||
func NewDelUserDeleteCommand() *Command {
|
||||
c := NewCommand()
|
||||
func NewDelUserDeleteCommand() *command.Command {
|
||||
c := command.NewCommand()
|
||||
c.Path = "deluser"
|
||||
c.Args = []CommandArg{
|
||||
CommandArg("{{ .Name }}"),
|
||||
c.Args = []command.CommandArg{
|
||||
command.CommandArg("{{ .Name }}"),
|
||||
}
|
||||
c.Extractor = func(out []byte, target any) error {
|
||||
return nil
|
||||
|
@ -80,6 +80,7 @@ func (d *DeclFile) ExtractResources(filter ResourceSelector) ([]*resource.Docume
|
||||
for {
|
||||
doc := resource.NewDocument()
|
||||
e := decoder.Decode(doc)
|
||||
slog.Info("ExtractResources().Decode()", "document", doc, "error", e)
|
||||
if errors.Is(e, io.EOF) {
|
||||
break
|
||||
}
|
||||
|
68
internal/source/group.go
Normal file
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() {
|
||||
SourceTypes.Register([]string{"package"}, func(u *url.URL) DocSource {
|
||||
p := NewPackage()
|
||||
p.PackageType = resource.PackageType(u.Query().Get("type"))
|
||||
packageType := u.Query().Get("type")
|
||||
if len(packageType) > 0 {
|
||||
p.PackageType = resource.PackageType(packageType)
|
||||
}
|
||||
return p
|
||||
})
|
||||
|
||||
@ -40,6 +43,9 @@ func (p *Package) ExtractResources(filter ResourceSelector) ([]*resource.Documen
|
||||
slog.Info("package source ExtractResources()", "package", p)
|
||||
installedPackages := make([]*resource.Package, 0, 100)
|
||||
cmd := p.PackageType.NewReadPackagesCommand()
|
||||
if cmd == nil {
|
||||
return documents, resource.ErrUnsupportedPackageType
|
||||
}
|
||||
if out, err := cmd.Execute(p); err == nil {
|
||||
slog.Info("package source ExtractResources()", "output", out)
|
||||
if exErr := cmd.Extractor(out, &installedPackages); exErr != nil {
|
||||
@ -51,7 +57,6 @@ func (p *Package) ExtractResources(filter ResourceSelector) ([]*resource.Documen
|
||||
pkg = resource.NewPackage()
|
||||
}
|
||||
pkg.PackageType = p.PackageType
|
||||
|
||||
document.AddResourceDeclaration("package", pkg)
|
||||
}
|
||||
documents = append(documents, document)
|
||||
|
68
internal/source/user.go
Normal file
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 {
|
||||
InjectContainerStart func(ctx context.Context, containerID string, options container.StartOptions) error
|
||||
InjectContainerCreate func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error)
|
||||
InjectNetworkCreate func(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
|
||||
InjectNetworkCreate func(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error)
|
||||
InjectNetworkList func(ctx context.Context, options network.ListOptions) ([]network.Summary, error)
|
||||
InjectNetworkInspect func(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error)
|
||||
InjectContainerList func(context.Context, container.ListOptions) ([]types.Container, error)
|
||||
InjectContainerInspect func(context.Context, string) (types.ContainerJSON, error)
|
||||
InjectContainerRemove func(context.Context, string, container.RemoveOptions) error
|
||||
InjectContainerStop func(context.Context, string, container.StopOptions) error
|
||||
InjectContainerWait func(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error)
|
||||
InjectImagePull func(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error)
|
||||
InjectImagePull func(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error)
|
||||
InjectImageInspectWithRaw func(ctx context.Context, imageID string) (types.ImageInspect, []byte, error)
|
||||
InjectImageRemove func(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error)
|
||||
InjectImageRemove func(ctx context.Context, imageID string, options image.RemoveOptions) ([]image.DeleteResponse, error)
|
||||
InjectClose func() error
|
||||
}
|
||||
|
||||
@ -31,11 +33,11 @@ func (m *MockContainerClient) ContainerWait(ctx context.Context, containerID str
|
||||
return m.InjectContainerWait(ctx, containerID, condition)
|
||||
}
|
||||
|
||||
func (m *MockContainerClient) ImageRemove(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
|
||||
func (m *MockContainerClient) ImageRemove(ctx context.Context, imageID string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
|
||||
return m.InjectImageRemove(ctx, imageID, options)
|
||||
}
|
||||
|
||||
func (m *MockContainerClient) ImagePull(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error) {
|
||||
func (m *MockContainerClient) ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error) {
|
||||
return m.InjectImagePull(ctx, refStr, options)
|
||||
}
|
||||
|
||||
@ -77,6 +79,14 @@ func (m *MockContainerClient) Close() error {
|
||||
return m.InjectClose()
|
||||
}
|
||||
|
||||
func (m *MockContainerClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
|
||||
func (m *MockContainerClient) NetworkCreate(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error) {
|
||||
return m.InjectNetworkCreate(ctx, name, options)
|
||||
}
|
||||
|
||||
func (m *MockContainerClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
|
||||
return m.InjectNetworkList(ctx, options)
|
||||
}
|
||||
|
||||
func (m *MockContainerClient) NetworkInspect(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error) {
|
||||
return m.InjectNetworkInspect(ctx, networkID, options)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user