Compare commits

...

8 Commits

Author SHA1 Message Date
8e85305595 add openpgp keyring config
Some checks failed
Lint / golangci-lint (push) Failing after 10m2s
Declarative Tests / test (push) Failing after 21s
2025-08-25 04:00:49 +00:00
d305305fab add ref interfaces 2025-08-25 03:56:56 +00:00
00d12be3f5 add more debug info to schema errors 2025-08-25 03:53:49 +00:00
2d4234c6a0 add ref attribute type 2025-08-25 03:51:34 +00:00
fb544a455c generate a unique uri for each document 2025-08-25 03:35:33 +00:00
1e32bc7129 update schema 2025-08-25 03:12:08 +00:00
59f66af792 move SetResourceMapper to common 2025-08-25 01:44:51 +00:00
24a18c3094 move SetResourceMapper to common 2025-08-25 01:44:21 +00:00
58 changed files with 2593 additions and 440 deletions

View File

@ -1,90 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"crypto/x509"
"crypto/x509/pkix"
"crypto/rsa"
"crypto/rand"
"encoding/pem"
"encoding/json"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/packet"
)
type OpenPGP struct {
Armored string
entities openpgp.EntityList
}
func (o *OpenPGP) Read() (yamlData []byte, err error) {
pemReader := io.NopCloser(strings.NewReader(o.Armored))
o.entities, err = openpgp.ReadArmoredKeyRing(pemReader)
return
}
func (o *OpenPGP) UnmarshalJSON(data []byte) error {
if unmarshalErr := json.Unmarshal(data, o); unmarshalErr != nil {
return unmarshalErr
}
return nil
}
func (o *OpenPGP) UnmarshalYAML(value *yaml.Node) error {
type decodeOpenPGP OpenPGP
if unmarshalErr := value.Decode((*decodeOpenPGP)(o)); unmarshalErr != nil {
return unmarshalErr
}
return nil
}
func (o *OpenPGP) Clone() data.Configuration {
jsonGeneric, _ := json.Marshal(c)
clone := NewOpenPGP()
if unmarshalErr := json.Unmarshal(jsonGeneric, &clone); unmarshalErr != nil {
panic(unmarshalErr)
}
return clone
}
func (o *OpenPGP) Type() string {
return "openpgp"
}
func (o *OpenPGP) GetEntityIndex(key string) (index int, field string, err error) {
values := strings.SplitN(key, ".", 2)
if len(values) == 2 {
if index, err = strconv.Atoi(values[0]); err == nil {
field = values[1]
}
} else {
err = data.ErrUnknownConfigurationKey
}
return
}
func (o *OpenPGP) GetValue(name string) (result any, err error) {
var ok bool
if result, ok = (*c)[name]; !ok {
err = data.ErrUnknownConfigurationKey
}
return
}
// Expected key: 0.PrivateKey
func (o *OpenPGP) Has(key string) (ok bool) {
index, field, err := o.GetEntityIndex(key)
if len(o.entities) > index && err == nil {
switch key {
case PublicKey:
ok = o.entities[index].PrimaryKey != nil
case PrivateKey:
ok = o.entities[index].PrimaryKey != nil
}
}
return
}

View File

@ -1,31 +0,0 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"github.com/stretchr/testify/assert"
"testing"
"crypto/x509"
)
func TestNewOpenPGPConfig(t *testing.T) {
p := NewOpenPGP()
assert.NotNil(t, p)
}
func TestNewOpenPGPConfigYAML(t *testing.T) {
p := NewOpenPGP()
assert.NotNil(t, p)
config := `
openpgp:
publickey:
`
yamlErr := c.LoadYAML(config)
assert.Nil(t, yamlErr)
crt, err := c.GetValue("catemplate")
assert.Nil(t, err)
assert.Equal(t, []string{"RKH"}, crt.(*x509.Certificate).Subject.Organization)
}

View File

@ -0,0 +1,134 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"context"
"encoding/json"
"github.com/ProtonMail/go-crypto/openpgp"
"io"
"strings"
"strconv"
"gopkg.in/yaml.v3"
"decl/internal/data"
"decl/internal/codec"
"fmt"
"log/slog"
)
type OpenPGPKeyRing struct {
Keyring string `json:"keyring,omitempty" yaml:"keyring,omitempty"`
entities openpgp.EntityList
}
func NewOpenPGPKeyRing() *OpenPGPKeyRing {
o := &OpenPGPKeyRing{}
return o
}
func (o *OpenPGPKeyRing) URI() string {
return fmt.Sprintf("%s://%s", o.Type(), "")
}
func (o *OpenPGPKeyRing) SetURI(uri string) error {
return nil
}
func (o *OpenPGPKeyRing) SetParsedURI(uri data.URIParser) error {
return nil
}
func (o *OpenPGPKeyRing) Read(ctx context.Context) (yamlData []byte, err error) {
pemReader := io.NopCloser(strings.NewReader(o.Keyring))
o.entities, err = openpgp.ReadArmoredKeyRing(pemReader)
return
}
func (o *OpenPGPKeyRing) Load(r io.Reader) (err error) {
err = codec.NewYAMLDecoder(r).Decode(o)
if err == nil {
_, err = o.Read(context.Background())
}
return err
}
func (o *OpenPGPKeyRing) LoadYAML(yamlData string) (err error) {
err = codec.NewYAMLStringDecoder(yamlData).Decode(o)
slog.Info("OpenPGP.LoadYAML()", "keyring", len(o.Keyring), "err", err)
if err == nil {
_, err = o.Read(context.Background())
}
return err
}
func (o *OpenPGPKeyRing) UnmarshalJSON(data []byte) error {
if unmarshalErr := json.Unmarshal(data, o); unmarshalErr != nil {
return unmarshalErr
}
//o.NewReadConfigCommand()
return nil
}
func (o *OpenPGPKeyRing) UnmarshalYAML(value *yaml.Node) error {
type decodeOpenPGP OpenPGPKeyRing
if unmarshalErr := value.Decode((*decodeOpenPGP)(o)); unmarshalErr != nil {
return unmarshalErr
}
return nil
}
func (o *OpenPGPKeyRing) Clone() data.Configuration {
jsonGeneric, _ := json.Marshal(o)
clone := NewOpenPGPKeyRing()
if unmarshalErr := json.Unmarshal(jsonGeneric, &clone); unmarshalErr != nil {
panic(unmarshalErr)
}
return clone
}
func (o *OpenPGPKeyRing) Type() string {
return "openpgp"
}
func (o *OpenPGPKeyRing) GetEntityIndex(key string) (index int, field string, err error) {
values := strings.SplitN(key, ".", 2)
if len(values) == 2 {
if index, err = strconv.Atoi(values[0]); err == nil {
field = values[1]
}
} else {
err = data.ErrUnknownConfigurationKey
}
return
}
func (o *OpenPGPKeyRing) GetValue(name string) (result any, err error) {
index, field, err := o.GetEntityIndex(name)
if len(o.entities) > index && err == nil {
switch field {
case "PublicKey":
return o.entities[index].PrimaryKey, err
case "PrivateKey":
return o.entities[index].PrimaryKey, err
}
}
err = data.ErrUnknownConfigurationKey
return
}
// Expected key: 0.PrivateKey
func (o *OpenPGPKeyRing) Has(key string) (ok bool) {
index, field, err := o.GetEntityIndex(key)
if len(o.entities) > index && err == nil {
switch field {
case "PublicKey":
ok = o.entities[index].PrimaryKey != nil
case "PrivateKey":
ok = o.entities[index].PrimaryKey != nil
}
}
return
}

View File

@ -0,0 +1,131 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"github.com/stretchr/testify/assert"
"testing"
"fmt"
)
var (
privateKey = `
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
=BAfD
-----END PGP PRIVATE KEY BLOCK-----
`
publicKey = `
-----BEGIN PGP PUBLIC KEY BLOCK-----
xsBNBGhI/VUBCADDVSm3mKY5JsncMMJFV0zELMrmip7dkK3vvMvVWVmMHiC4akDH
WPxUdWNQPjE2e5HGF9Ebg0c7gu634VG470MnTzFdPV6f5zA+yJfdKrLq7gpl9QGW
jFLIeK/l4xc+MIpOE1rD9WqYUUw2IYY8YANYq4yB36rq41VuZps/adI9Go5IhfcU
3SVb7o7pa/gWE0FVu9ze31j2agC8FIKHgB++7bmYgbAQz5Qi1qgtG0Kn25QUacJ6
Akm2+h4w3SQCR6HLRV2BO29x9mFBszf2KQ7DW2VNiGyUuQQ3m8v2ZidG/11ff6U6
ad5tvr/8sYr5jOnKEJyDP9v9yQ04cU94GmsPABEBAAHNL1Rlc3RVc2VyMSAoVGVz
dFVzZXIxKSA8dGVzdHVzZXJAcm9zc2tlZW4uaG91c2U+wsCKBBMBCAA+BQJoSP1V
CRDfB5I2FtNl1xYhBADOnA8Aah3FXyl/m98HkjYW02XXAhsDAh4BAhkBAgsHAhUI
AxYAAgMnBwIAAGKLB/9whwgfLkoTYI+Q0Te2uZXl7FPyW0t4tbUSoI1aiVw+ymND
V+wuqYsjYDob2MyK/TAFkSCqZhBIRbGLRJtwzQwkF/amGuelHSSBX3LdxK/sp2UU
+zv/NBEP1LlUNKchuxpdBPYjKHdbOLPoKqFfdCujSxpWTIeKCwvnDtuP+PlsjUgc
afxMx+cVwe77AZ1Fi5CBD9Dr7nNsLovywrSiszyVh5eT0pbcu8Elf5oYQNT1q3ms
aqrw95shlGQiwVgrfTvRCDeZnKafHwMUoh48otsNI9aS0HpChi+JNd+PLe/Wlsj5
x+q0sHc+5/24zQvN0Wx9fPE4/7z8Pr+CUhAxjI7hzsBNBGhI/VUBCACfahoi2GRx
OfOqJ4ZN8Te+4bJeOQdzTofAisfCU736q+MvEKtAuY72vzWdERWpn7XABmtkfII8
Xyya8iHKuEGCETy0YoZ15GLBgSHhLnlN2U/4BkrUjGvUjZWquW22gfFB1q8ZdSGw
pg8hrspY/b+nnAGdawq+hfE6YRQTmG5tkBrctxfxglBnKzezmL1a3arTHWf2SRlL
eAWNI4sACryP/5plD6+9Z9YXTKeDlXnga6BzB80y7F2g0cPdtX20dWAGx4irq2I2
SxlABvWMZrdm2Upr6yLHD2zTpbf1FkKFUw9j0lrL1fMq0/j9zbKJsTh6CtOlF04s
AeoEV9SZ36GvABEBAAHCwHYEGAEIACoFAmhI/VUJEN8HkjYW02XXFiEEAM6cDwBq
HcVfKX+b3weSNhbTZdcCGwwAAFxWB/9Af7qpYKldDz5iNYQluFoPfyHAVlOpiakf
hvnu7hijTEgB1frByKu2m+k4m7gANBgfWldsYi1feWBVn6fspXuCc0k7opmQgol7
ueTrsGnNB7U0l14HcKSTLZ2c/Jt7ZS6aXXjxpYPq0/5lt8+5sRkXxHI9HhAx6gK/
RJCNITRt/V/gT4YWWbz8VHgkPIfHFsjr8brVhm4V/+JKZAG4eSNQp4QrTpdIjdfa
1rd91qffyrWhPNTQErE2jFOXSU/OojdDjGJ7pwr0qj4FHACVL3/oqYog2NrUWf92
QcwwWrEZNZUpJEMUB5q+tOoZ40ll0A8lsvABULZDqWveYpRQF+rQ
=jBvZ
-----END PGP PUBLIC KEY BLOCK-----
`
)
func TestNewOpenPGPKeyRingConfig(t *testing.T) {
p := NewOpenPGPKeyRing()
assert.NotNil(t, p)
}
func TestNewOpenPGPKeyRingConfigYAML(t *testing.T) {
p := NewOpenPGPKeyRing()
assert.NotNil(t, p)
config := fmt.Sprintf(`
keyring: |-
%s
%s
`, publicKey, privateKey)
yamlErr := p.LoadYAML(config)
assert.Nil(t, yamlErr)
/*
crt, err := p.GetValue("catemplate")
assert.Nil(t, err)
assert.Equal(t, []string{"RKH"}, crt.(*x509.Certificate).Subject.Organization)
*/
}

View File

@ -0,0 +1,135 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"context"
"encoding/json"
"github.com/ProtonMail/go-crypto/openpgp"
"io"
"strings"
"strconv"
"gopkg.in/yaml.v3"
"decl/internal/data"
"decl/internal/codec"
"fmt"
"log/slog"
)
type OpenPGPSignature struct {
KeyRing string `json:"keyring,omitempty" yaml:"keyring,omitempty"`
Signature string `json:"signature,omitempty" yaml:"signature,omitempty"`
entities openpgp.EntityList
}
func NewOpenPGPSignature() *OpenPGPSignature {
o := &OpenPGPSignature{}
return o
}
func (o *OpenPGPSignature) URI() string {
return fmt.Sprintf("%s://%s", o.Type(), "")
}
func (o *OpenPGPSignature) SetURI(uri string) error {
return nil
}
func (o *OpenPGPSignature) SetParsedURI(uri data.URIParser) error {
return nil
}
func (o *OpenPGPSignature) Read(ctx context.Context) (yamlData []byte, err error) {
pemReader := io.NopCloser(strings.NewReader(o.KeyRing))
o.entities, err = openpgp.ReadArmoredKeyRing(pemReader)
return
}
func (o *OpenPGPSignature) Load(r io.Reader) (err error) {
err = codec.NewYAMLDecoder(r).Decode(o)
if err == nil {
_, err = o.Read(context.Background())
}
return err
}
func (o *OpenPGPSignature) LoadYAML(yamlData string) (err error) {
err = codec.NewYAMLStringDecoder(yamlData).Decode(o)
slog.Info("OpenPGP.LoadYAML()", "keyring", len(o.KeyRing), "err", err)
if err == nil {
_, err = o.Read(context.Background())
}
return err
}
func (o *OpenPGPSignature) UnmarshalJSON(data []byte) error {
if unmarshalErr := json.Unmarshal(data, o); unmarshalErr != nil {
return unmarshalErr
}
//o.NewReadConfigCommand()
return nil
}
func (o *OpenPGPSignature) UnmarshalYAML(value *yaml.Node) error {
type decodeOpenPGP OpenPGPSignature
if unmarshalErr := value.Decode((*decodeOpenPGP)(o)); unmarshalErr != nil {
return unmarshalErr
}
return nil
}
func (o *OpenPGPSignature) Clone() data.Configuration {
jsonGeneric, _ := json.Marshal(o)
clone := NewOpenPGPSignature()
if unmarshalErr := json.Unmarshal(jsonGeneric, &clone); unmarshalErr != nil {
panic(unmarshalErr)
}
return clone
}
func (o *OpenPGPSignature) Type() string {
return "openpgp"
}
func (o *OpenPGPSignature) GetEntityIndex(key string) (index int, field string, err error) {
values := strings.SplitN(key, ".", 2)
if len(values) == 2 {
if index, err = strconv.Atoi(values[0]); err == nil {
field = values[1]
}
} else {
err = data.ErrUnknownConfigurationKey
}
return
}
func (o *OpenPGPSignature) GetValue(name string) (result any, err error) {
index, field, err := o.GetEntityIndex(name)
if len(o.entities) > index && err == nil {
switch field {
case "PublicKey":
return o.entities[index].PrimaryKey, err
case "PrivateKey":
return o.entities[index].PrimaryKey, err
}
}
err = data.ErrUnknownConfigurationKey
return
}
// Expected key: 0.PrivateKey
func (o *OpenPGPSignature) Has(key string) (ok bool) {
index, field, err := o.GetEntityIndex(key)
if len(o.entities) > index && err == nil {
switch field {
case "PublicKey":
ok = o.entities[index].PrimaryKey != nil
case "PrivateKey":
ok = o.entities[index].PrimaryKey != nil
}
}
return
}

View File

@ -0,0 +1,33 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package config
import (
"context"
"github.com/stretchr/testify/assert"
"testing"
"fmt"
)
func TestNewOpenPGPSignatureConfig(t *testing.T) {
p := NewOpenPGPSignature()
assert.NotNil(t, p)
}
func TestNewOpenPGPSignatureConfigYAML(t *testing.T) {
p := NewOpenPGPSignature()
assert.NotNil(t, p)
config := fmt.Sprintf(`
keyring: |-
%s
%s
`, publicKey, privateKey)
yamlErr := p.LoadYAML(config)
assert.Nil(t, yamlErr)
p.Read(context.Background())
}

View File

@ -0,0 +1,9 @@
{
"$id": "openpgp.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "openpgp",
"type": "object",
"required": [ "path", "filetype" ],
"properties": {
}
}

View File

@ -40,6 +40,7 @@ func NewSystem() *System {
s["importpath"] = []string {
"/etc/jx/lib",
}
s["keyringpath"] = "/etc/jx/pgp/keyring.asc"
return &s
}

View File

@ -0,0 +1,13 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package data
import (
)
var (
)
type Originator interface {
GetContentReadWriter() ContentReadWriter
}

View File

@ -38,7 +38,6 @@ type Resource interface {
Crudder
Validator
Clone() Resource
SetResourceMapper(ResourceMapper)
}
type Declaration interface {

View File

@ -149,12 +149,16 @@ func (j *JxFile) Extract(resourceSource data.Resource, filter data.ElementSelect
}
uri := resourceSource.URI()
if folio.DocumentRegistry.HasDocument(folio.URI(uri)) {
uri = fmt.Sprintf("%s?index=%d", uri, j.index)
documentIndexUri := fmt.Sprintf("%s?index=%d", uri, j.index)
doc = folio.DocumentRegistry.NewDocument(folio.URI(documentIndexUri))
if ! folio.DocumentRegistry.HasDocument(folio.URI(uri)) {
folio.DocumentRegistry.SetDocument(folio.URI(uri), doc.(*folio.Document))
doc.(*folio.Document).SetURI(uri)
}
doc = folio.DocumentRegistry.NewDocument(folio.URI(uri))
err = j.decoder.Decode(doc)
slog.Info("JxFile.Extract()", "doc", doc, "jxfile", j, "error", err)
slog.Info("JxFile.Extract()", "uri", uri, "doc", doc, "jxfile", j, "error", err)
j.index++
if err != nil {
return

View File

@ -73,7 +73,7 @@ func (d *Declaration) SetDocument(newDocument *Document) {
d.document = newDocument
d.SetConfig(d.document.config)
d.ResourceTypes = d.document.Types()
d.Attributes.SetResourceMapper(d.document.uris)
d.Attributes.(ResourceMapSetter).SetResourceMapper(d.document.uris)
}
func (d *Declaration) ResolveId(ctx context.Context) string {
@ -209,6 +209,11 @@ func (d *Declaration) Apply(stateTransition string) (result error) {
return result
}
result = stater.Trigger("read")
case "restart": // XXX should only work for a process type resource
if result = stater.Trigger("restart"); result != nil {
return result
}
result = stater.Trigger("read")
default:
return fmt.Errorf("%w: %s on %s", ErrUnknownStateTransition, stateTransition, d.Attributes.URI())
case "create", "present":
@ -249,7 +254,7 @@ func (d *Declaration) SetConfig(configDoc data.Document) {
return
}
if d.Config != "" { // XXX
panic(fmt.Sprintf("failed setting config: %s", d.Config))
panic(fmt.Errorf("%w: failed setting config: %s", data.ErrConfigUndefined, d.Config))
}
}
@ -396,6 +401,10 @@ func (d *Declaration) UnmarshalJSON(jsonData []byte) (err error) {
return
}
func (d *Declaration) GetContentReadWriter() data.ContentReadWriter {
return d.Resource().(data.ContentReadWriter)
}
/*
func (l *LuaWorker) Receive(m message.Envelope) {
s := m.Sender()

View File

@ -37,7 +37,7 @@ type Document struct {
Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"`
Imports []URI `json:"imports,omitempty" yaml:"imports,omitempty"`
Errors []string `json:"error,omitempty" yaml:"error,omitempty"`
uris mapper.Store[string, data.Declaration]
uris mapper.Store[URI, *Declaration]
ResourceDeclarations []*Declaration `json:"resources,omitempty" yaml:"resources,omitempty"`
configNames mapper.Store[string, data.Block] `json:"-" yaml:"-"`
Configurations []*Block `json:"configurations,omitempty" yaml:"configurations,omitempty"`
@ -57,7 +57,7 @@ func NewDocument(r *Registry) *Document {
return &Document{
Registry: r,
Format: codec.FormatYaml,
uris: mapper.New[string, data.Declaration](),
uris: mapper.New[URI, *Declaration](),
configNames: mapper.New[string, data.Block](),
importPaths: NewSearchPath(configImportPath.GetStringSlice()),
}
@ -111,24 +111,24 @@ func (d *Document) Filter(filter data.DeclarationSelector) []data.Declaration {
}
func (d *Document) Has(key string) bool {
return d.uris.Has(key)
return d.uris.Has(URI(key))
}
func (d *Document) Get(key string) (any, bool) {
return d.uris.Get(key)
return d.uris.Get(URI(key))
}
func (d *Document) Set(key string, value any) {
d.uris.Set(key, value.(data.Declaration))
d.uris.Set(URI(key), value.(*Declaration))
}
func (d *Document) Delete(key string) {
d.uris.Delete(key)
d.uris.Delete(URI(key))
}
func (d *Document) GetResource(uri string) *Declaration {
if decl, ok := d.uris[uri]; ok {
return decl.(*Declaration)
if decl, ok := d.uris[URI(uri)]; ok {
return decl
}
return nil
}
@ -236,17 +236,17 @@ func (d *Document) GetSchemaFiles() (schemaFs fs.FS) {
return
}
schemaFs, _ = d.Registry.Schemas.Get(d.Registry.DefaultSchema)
slog.Info("Document.GetSchemaFiles()", "schemaFs", schemaFs)
slog.Info("Document.GetSchemaFiles() default schema", "schema", d.Registry.DefaultSchema, "schemaFs", schemaFs)
return
}
func (d *Document) Validate() error {
jsonDocument, jsonErr := d.JSON()
slog.Info("Document.Validate() json", "err", jsonErr)
slog.Info("Document.Validate() convert to json", "err", jsonErr)
if jsonErr == nil {
s := schema.New("document", d.GetSchemaFiles())
err := s.Validate(string(jsonDocument))
slog.Info("Document.Validate()", "error", err)
slog.Info("Document.Validate() validate schema", "error", err)
if err != nil {
return err
}
@ -356,12 +356,12 @@ func (d *Document) MapConfigurationURI(uri string, block data.Block) {
}
*/
func (d *Document) MapResourceURI(uri string, declaration data.Declaration) {
d.uris[uri] = declaration
func (d *Document) MapResourceURI(uri string, declaration *Declaration) {
d.uris[URI(uri)] = declaration
}
func (d *Document) UnMapResourceURI(uri string) {
d.uris.Delete(uri)
d.uris.Delete(URI(uri))
}
func (d *Document) AddDeclaration(declaration data.Declaration) {
@ -371,7 +371,7 @@ func (d *Document) AddDeclaration(declaration data.Declaration) {
d.ResourceDeclarations = append(d.ResourceDeclarations, decl)
d.MapResourceURI(uri, declaration)
d.MapResourceURI(uri, decl)
decl.SetDocument(d)
d.Registry.DeclarationMap[decl] = d
}
@ -597,22 +597,38 @@ func (d *Document) Diff(with data.Document, output io.Writer) (returnOutput stri
/*
func (d *Document) UnmarshalValue(value *DocumentType) error {
d.Requires = value.Requires
}
*/
func (d *Document) UnmarshalLoadDependencies() (err error) {
defer func() {
if r := recover(); r != nil {
if err != nil {
err = fmt.Errorf("%s - %w", r, err)
} else {
err = fmt.Errorf("%s", r)
}
}
}()
err = d.loadImports()
d.assignConfigurationsDocument()
d.assignResourcesDocument()
return
}
func (d *Document) UnmarshalYAML(value *yaml.Node) (err error) {
type decodeDocument Document
/*
t := &DocumentType{}
if unmarshalDocumentErr := value.Decode(t); unmarshalDocumentErr != nil {
return unmarshalDocumentErr
}
*/
if unmarshalResourcesErr := value.Decode((*decodeDocument)(d)); unmarshalResourcesErr != nil {
return unmarshalResourcesErr
}
err = d.loadImports()
d.assignConfigurationsDocument()
d.assignResourcesDocument()
err = d.UnmarshalLoadDependencies()
return
}
@ -622,12 +638,39 @@ func (d *Document) UnmarshalJSON(data []byte) (err error) {
if unmarshalDocumentErr := json.Unmarshal(data, t); unmarshalDocumentErr != nil {
return unmarshalDocumentErr
}
err = d.loadImports()
d.assignConfigurationsDocument()
d.assignResourcesDocument()
err = d.UnmarshalLoadDependencies()
return
}
func (d *Document) AddError(e error) {
d.Errors = append(d.Errors, e.Error())
}
func (d *Document) GetContentReadWriter() data.ContentReadWriter {
return d.URI
}
func (d *Document) GetContent(w io.Writer) (contentReader io.ReadCloser, err error) {
slog.Info("Document.GetContent()")
var buf strings.Builder
err = codec.FormatYaml.Serialize(d, &buf)
contentReader = io.NopCloser(strings.NewReader(buf.String()))
if w != nil {
copyBuffer := make([]byte, 32 * 1024)
_, writeErr := io.CopyBuffer(w, contentReader, copyBuffer)
if writeErr != nil {
return nil, fmt.Errorf("File.GetContent(): CopyBuffer failed %v %v: %w", w, contentReader, writeErr)
}
return nil, nil
}
return
}
func (d *Document) SetContent(r io.Reader) error {
return d.LoadReader(io.NopCloser(r), codec.FormatYaml)
}

View File

@ -8,6 +8,7 @@ _ "gopkg.in/yaml.v3"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
"decl/internal/data"
"decl/internal/transport"
"io"
"net/url"
"path/filepath"
@ -89,9 +90,10 @@ func NewMockResource(typename string, stater machine.Stater) (m *MockResource) {
InjectUpdate: func(context.Context) error { return nil },
InjectDelete: func(context.Context) error { return nil },
InjectUseConfig: func(data.ConfigurationValueGetter) {},
InjectSetResourceMapper: func(data.ResourceMapper) {},
InjectSetResourceMapper: func(ResourceMapper) {},
InjectURI: func() string { return fmt.Sprintf("%s://bar", typename) },
InjectNotify: func(*machine.EventMessage) {},
InjectContentReaderStream: func() (*transport.Reader, error) { return nil, nil },
}
m.InjectInit = func(u data.URIParser) error {
if u != nil {

View File

@ -11,6 +11,7 @@ _ "fmt"
"decl/internal/data"
"decl/internal/codec"
"io"
"decl/internal/transport"
)
type MockResource struct {
@ -34,9 +35,11 @@ type MockResource struct {
InjectUpdate func(context.Context) error `json:"-" yaml:"-"`
InjectDelete func(context.Context) error `json:"-" yaml:"-"`
InjectStateMachine func() machine.Stater `json:"-" yaml:"-"`
InjectSetResourceMapper func(data.ResourceMapper) `json:"-" yaml:"-"`
InjectSetResourceMapper func(ResourceMapper) `json:"-" yaml:"-"`
InjectUseConfig func(data.ConfigurationValueGetter) `json:"-" yaml:"-"`
InjectNotify func(*machine.EventMessage) `json:"-" yaml:"-"`
InjectContentReaderStream func() (*transport.Reader, error) `json:"-" yaml:"-"`
InjectContentWriterStream func() (*transport.Writer, error) `json:"-" yaml:"-"`
}
func (m *MockResource) Clone() data.Resource {
@ -67,7 +70,7 @@ func (m *MockResource) ResolveId(ctx context.Context) string {
return m.InjectResolveId(ctx)
}
func (m *MockResource) SetResourceMapper(rm data.ResourceMapper) {
func (m *MockResource) SetResourceMapper(rm ResourceMapper) {
m.InjectSetResourceMapper(rm)
}
@ -139,6 +142,14 @@ func (m *MockResource) Notify(em *machine.EventMessage) {
m.InjectNotify(em)
}
func (m *MockResource) ContentReaderStream() (*transport.Reader, error) {
return m.InjectContentReaderStream()
}
func (m *MockResource) ContentWriterStream() (*transport.Writer, error) {
return m.InjectContentWriterStream()
}
func (m *MockResource) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, m); err != nil {
return err

229
internal/folio/ref.go Normal file
View File

@ -0,0 +1,229 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"encoding/json"
"gopkg.in/yaml.v3"
"decl/internal/mapper"
"decl/internal/data"
"decl/internal/transport"
"net/url"
"log/slog"
"errors"
"fmt"
"io"
)
var (
ErrRefMapperMismatch = errors.New("Ref type does not the provided mapper")
)
type Ref struct {
Uri URI `json:"uri" yaml:"uri"`
RefType ReferenceType `json:"type,omitempty" yaml:"type,omitempty"`
documentMapper mapper.Store[URI, *Document]
resourceMapper mapper.Store[URI, *Declaration]
}
type decodeRef Ref
func NewRef() *Ref {
return &Ref {
}
}
func (r *ReferenceType) UnmarshalValue(value string) error {
switch value {
case string(ReferenceTypeResource), string(ReferenceTypeDocument):
*r = ReferenceType(value)
default:
*r = ReferenceTypeResource
//return ErrInvalidReferenceType
}
return nil
}
func (r *ReferenceType) UnmarshalJSON(jsonData []byte) error {
var s string
if unmarshalReferenceTypeErr := json.Unmarshal(jsonData, &s); unmarshalReferenceTypeErr != nil {
return unmarshalReferenceTypeErr
}
return r.UnmarshalValue(s)
}
func (r *ReferenceType) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
return r.UnmarshalValue(s)
}
func (r *Ref) UnmarshalValue(value *decodeRef) error {
slog.Info("Ref.UnmarshalValue", "decode", value)
r.Uri = value.Uri
switch value.RefType {
case ReferenceTypeResource:
r.RefType = value.RefType
case ReferenceTypeDocument:
r.RefType = value.RefType
r.SetMapper(DocumentRegistry.UriMap)
default:
r.RefType = ReferenceTypeResource
//return ErrInvalidReferenceType
}
slog.Info("Ref.UnmarshalValue", "stored", *r)
return nil
}
func (r *Ref) UnmarshalJSON(jsonData []byte) error {
decodeJsonToRef := &decodeRef{}
if unmarshalReferenceTypeErr := json.Unmarshal(jsonData, decodeJsonToRef); unmarshalReferenceTypeErr != nil {
return unmarshalReferenceTypeErr
}
return r.UnmarshalValue(decodeJsonToRef)
}
func (r *Ref) UnmarshalYAML(value *yaml.Node) error {
decodeYamlToRef := &decodeRef{}
if err := value.Decode(decodeYamlToRef); err != nil {
return err
}
return r.UnmarshalValue(decodeYamlToRef)
}
func (r *Ref) SetMapper(m any) error {
var ok bool
switch r.RefType {
case ReferenceTypeResource:
if r.resourceMapper, ok = m.(mapper.Store[URI, *Declaration]); ! ok {
return fmt.Errorf("%w - %T is not a %s", ErrRefMapperMismatch, m, r.RefType)
}
case ReferenceTypeDocument:
if r.documentMapper, ok = m.(mapper.Store[URI, *Document]); ! ok {
return fmt.Errorf("%w - %T is not a %s", ErrRefMapperMismatch, m, r.RefType)
}
}
return nil
}
func Dereference[T ReferenceTypes](uri URI, look mapper.Map[URI, T]) T {
if uri != "" && look != nil {
if v, ok := look.Get(uri); ok {
slog.Info("Ref::Dereference()", "value", v, "mapper", look)
return v
}
}
return nil
}
func (r *Ref) Lookup(look any) ContentReadWriter {
slog.Info("Ref.Lookup()", "ref", r, "mapper", look)
switch r.RefType {
case ReferenceTypeResource:
if resourceDeclaration := Dereference(r.Uri, look.(mapper.Store[URI, *Declaration])); resourceDeclaration != nil {
return resourceDeclaration.GetContentReadWriter()
}
case ReferenceTypeDocument:
if document := Dereference(r.Uri, look.(mapper.Store[URI, *Document])); document != nil {
return document.GetContentReadWriter()
}
}
return r.Uri
}
func (r *Ref) Dereference(look any) any {
switch r.RefType {
case ReferenceTypeResource:
if look == nil {
return Dereference(r.Uri, r.resourceMapper)
} else {
return Dereference(r.Uri, look.(mapper.Store[URI, *Declaration]))
}
case ReferenceTypeDocument:
if look == nil {
return Dereference(r.Uri, r.documentMapper)
} else {
return Dereference(r.Uri, look.(mapper.Store[URI, *Document]))
}
}
return nil
}
func (r *Ref) DereferenceDefault() any {
switch r.RefType {
case ReferenceTypeResource:
return Dereference(r.Uri, r.resourceMapper)
case ReferenceTypeDocument:
return Dereference(r.Uri, r.documentMapper)
}
return nil
}
func (r *Ref) Parse() data.URIParser {
return r.Uri.Parse()
}
func (r *Ref) Exists() bool {
return r.Uri.Exists()
}
func (r *Ref) ContentReaderStream() (*transport.Reader, error) {
return r.Uri.ContentReaderStream()
}
func (r *Ref) ContentWriterStream() (*transport.Writer, error) {
return r.Uri.ContentWriterStream()
}
func (r *Ref) GetContent(w io.Writer) (contentReader io.ReadCloser, err error) {
target := r.DereferenceDefault()
if targetContent, ok := target.(data.ContentGetSetter); ok {
return targetContent.GetContent(w)
}
return nil, fmt.Errorf("Ref target does not support ContentGetSetter: %s, %s, %#v", r.RefType, r.Uri, target)
}
func (r *Ref) SetContent(contentReader io.Reader) error {
target := r.DereferenceDefault()
if targetContent, ok := target.(data.ContentGetSetter); ok {
return targetContent.SetContent(contentReader)
}
return fmt.Errorf("Ref target does not support ContentGetSetter: %s, %s, %#v", r.RefType, r.Uri, target)
}
func (r *Ref) Reader() (reader io.ReadCloser, err error) {
switch r.RefType {
case ReferenceTypeResource:
return r.ContentReaderStream()
case ReferenceTypeDocument:
reader, err = r.GetContent(nil)
if reader == nil {
return r.ContentReaderStream()
}
return
}
return nil, ErrInvalidReferenceType
}
func (r *Ref) String() string {
return r.Uri.String()
}
func (r *Ref) SetURL(url *url.URL) {
r.Uri.SetURL(url)
}
func (r *Ref) Extension() (string, string) {
return r.Uri.Extension()
}
func (r *Ref) IsEmpty() bool {
return r.Uri.IsEmpty()
}
func (r *Ref) FindIn(s *SearchPath) {
r.Uri.FindIn(s)
}

View File

@ -0,0 +1,67 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"github.com/stretchr/testify/assert"
"testing"
"fmt"
_ "decl/internal/data"
"decl/internal/mapper"
"decl/internal/codec"
"log/slog"
"strings"
"io"
)
func TestReference(t *testing.T) {
f := NewFooResource()
resourceMapper := mapper.New[URI, *Declaration]()
f.Name = string(TempDir)
f.Size = 10
f.MockResource.InjectURI = func() string { return fmt.Sprintf("%s://%s", "foo", f.Name) }
d := NewDeclaration()
d.Type = "foo"
d.Attributes = f
resourceMapper[URI(d.URI())] = d
slog.Info("TestReference", "declaration", d, "mapper", resourceMapper)
var fooRef *Ref = NewRef()
fooRef.RefType = ReferenceTypeResource
fooRef.Uri = URI(fmt.Sprintf("foo://%s", string(TempDir)))
u := fooRef.Uri.Parse().URL()
assert.Equal(t, "foo", u.Scheme)
assert.True(t, fooRef.Uri.Exists())
fromRef := fooRef.Lookup(resourceMapper)
assert.NotNil(t, fromRef)
}
func TestDocumentRef(t *testing.T) {
docUri := fmt.Sprintf("file://%s/doc.yaml", TempDir)
DocumentRegistry.ResourceTypes = TestResourceTypes
document := `
---
resources:
- type: foo
attributes:
name: "testfoo"
size: 10022
`
TempDir.CreateFile("doc.yaml", document)
d := DocumentRegistry.NewDocument(URI(docUri))
assert.NotNil(t, d)
docReader := io.NopCloser(strings.NewReader(document))
e := d.LoadReader(docReader, codec.FormatYaml)
assert.Nil(t, e)
var docRef *Ref = NewRef()
docRef.RefType = ReferenceTypeDocument
docRef.Uri = URI(docUri)
u := docRef.Uri.Parse().URL()
assert.Equal(t, "file", u.Scheme)
assert.Equal(t, d, docRef.Dereference(DocumentRegistry.UriMap).(*Document))
}

133
internal/folio/referrer.go Normal file
View File

@ -0,0 +1,133 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package folio
import (
"encoding/json"
"gopkg.in/yaml.v3"
"net/url"
"decl/internal/data"
"decl/internal/mapper"
"errors"
)
var (
// ErrInvalidURI error = errors.New("Invalid URI")
ErrInvalidReferenceType error = errors.New("invalid ReferenceType value")
)
type ReferenceTypes interface {
*Document | *Declaration
}
type RefMap[T ReferenceTypes] mapper.Store[string, T]
type ReferenceMapper[T ReferenceTypes] interface {
mapper.Map[URI, T]
}
type ResourceMapper ReferenceMapper[*Declaration]
type DocumentMapper ReferenceMapper[*Document]
type ReferenceType string
type RefType[T ReferenceTypes, TypedReferenceMapper ReferenceMapper[T]] ReferenceType
type ReferenceURI[T ReferenceTypes] URI
type DocumentURI ReferenceURI[*Document]
type ResourceURI ReferenceURI[*Declaration]
const (
ReferenceTypeResource ReferenceType = "resource"
RefTypeResource RefType[*Declaration, mapper.Store[URI, *Declaration]] = RefType[*Declaration, mapper.Store[URI, *Declaration]](ReferenceTypeResource)
ReferenceTypeDocument ReferenceType = "document"
RefTypeDocument RefType[*Document, mapper.Store[URI, *Document]] = RefType[*Document, mapper.Store[URI, *Document]](ReferenceTypeDocument)
)
type CommonReferrer interface {
ReferenceType() ReferenceType
Parse() *url.URL
Exists() bool
data.ContentReadWriter
IsEmpty() bool
}
// interface for a reference
type Referrer[T ReferenceTypes, TypedReferenceMapper ReferenceMapper[T]] interface {
CommonReferrer
Lookup(TypedReferenceMapper) ContentReadWriter
GetContentReadWriter(TypedReferenceMapper) ContentReadWriter
Dereference(TypedReferenceMapper) T
}
/*
func (r ReferenceType) URIType() any {
switch r {
case ReferenceTypeDocument:
return ReferenceURI[*Document]
case ReferenceTypeResource:
return ReferenceURI[*Declaration]
}
}
*/
func (r *RefType[T, TypedReferenceMapper]) UnmarshalValue(value string) error {
switch value {
case string(ReferenceTypeResource), string(ReferenceTypeDocument):
*r = RefType[T, TypedReferenceMapper](value)
return nil
default:
return ErrInvalidReferenceType
}
}
func (r *RefType[T, TypedReferenceMapper]) UnmarshalJSON(data []byte) error {
var s string
if unmarshalReferenceTypeErr := json.Unmarshal(data, &s); unmarshalReferenceTypeErr != nil {
return unmarshalReferenceTypeErr
}
return r.UnmarshalValue(s)
}
func (r *RefType[T, TypedReferenceMapper]) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
return r.UnmarshalValue(s)
}
func (r *RefType[T, TypedReferenceMapper]) Dereference(uri URI, look TypedReferenceMapper) (result T) {
result = nil
if uri != "" {
if v,ok := look.Get(uri); ok {
result = v
}
}
return
}
func RefURI[T ReferenceTypes, RT RefType[T, ReferenceMapper[T]], RU ReferenceURI[T]](uri URI, reftype RT) (refuri RU) {
refuri = RU(uri)
return
}
/*
func (r *ReferenceURI[T]) GetContentReadWriter(mapper TypedReferenceMapper) ContentReadWriter {
if URI(r).String() == "" {
return nil
}
if mapper != nil {
if v,ok := mapper.Get(string(r)); ok {
return v.(data.Originator).GetContentReadWriter()
}
}
return r
}
*/
type ResourceMapSetter interface {
SetResourceMapper(ResourceMapper)
}
func NewResourceMapper() ResourceMapper {
return mapper.New[URI, *Declaration]()
}

View File

@ -60,7 +60,8 @@ func (r *Registry) HasDocument(key URI) bool {
}
func (r *Registry) GetDocument(key URI) (*Document, bool) {
return r.UriMap.Get(key)
document, result := r.UriMap.Get(key)
return document, result
}
func (r *Registry) SetDocument(key URI, value *Document) {
@ -72,7 +73,7 @@ func (r *Registry) NewDocument(uri URI) (doc *Document) {
doc.SetURI(string(uri))
r.Documents = append(r.Documents, doc)
if uri != "" {
r.UriMap[uri] = doc
r.UriMap.Set(uri, doc)
}
return
}

View File

@ -5,6 +5,7 @@ package folio
import (
"decl/internal/transport"
"decl/internal/data"
"decl/internal/mapper"
"errors"
"log/slog"
"net/url"
@ -31,25 +32,26 @@ type ContentReadWriter interface {
type ResourceReference URI
// Return a Content ReadWriter for the resource referred to.
func (r ResourceReference) Lookup(look data.ResourceMapper) ContentReadWriter {
func (r ResourceReference) Lookup(look mapper.Map[URI, *Declaration]) ContentReadWriter {
if string(r) == "" {
return nil
}
slog.Info("ResourceReference.Lookup()", "resourcereference", r, "resourcemapper", look)
if look != nil {
if v,ok := look.Get(string(r)); ok {
if v,ok := look.Get(URI(r)); ok {
slog.Info("ResourceReference.Lookup()", "resourcereference", r, "result", v.Resource())
return v.Resource().(ContentReadWriter)
}
}
return r
}
func (r ResourceReference) Dereference(look data.ResourceMapper) data.Resource {
func (r ResourceReference) Dereference(look mapper.Map[URI, *Declaration]) data.Resource {
slog.Info("ResourceReference.Dereference()", "resourcereference", r, "resourcemapper", look)
if look != nil {
if v,ok := look.Get(string(r)); ok {
if v,ok := look.Get(URI(r)); ok {
slog.Info("ResourceReference.Dereference()", "resourcereference", r, "result", v)
return v.(*Declaration).Attributes
return v.Attributes
}
}
return nil
@ -74,3 +76,7 @@ func (r ResourceReference) ContentWriterStream() (*transport.Writer, error) {
func (r ResourceReference) IsEmpty() bool {
return URI(r).IsEmpty()
}
func (r *ResourceReference) FindIn(s *SearchPath) {
(*URI)(r).FindIn(s)
}

View File

@ -6,19 +6,18 @@ import (
"github.com/stretchr/testify/assert"
"testing"
"fmt"
"decl/internal/data"
"decl/internal/mapper"
)
func TestResourceReference(t *testing.T) {
f := NewFooResource()
resourceMapper := mapper.New[string, data.Declaration]()
resourceMapper := mapper.New[URI, *Declaration]()
f.Name = string(TempDir)
f.Size = 10
d := NewDeclaration()
d.Type = "foo"
d.Attributes = f
resourceMapper[d.URI()] = d
resourceMapper[URI(d.URI())] = d
var foo ResourceReference = ResourceReference(fmt.Sprintf("foo://%s", string(TempDir)))
u := foo.Parse()

View File

@ -0,0 +1,17 @@
{
"$id": "ref.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ref",
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Type of object referred to",
"enum": [ "resource", "document" ]
},
"uri": {
"type": "string",
"description": "URI of the object referred to"
}
}
}

View File

@ -9,6 +9,7 @@ import (
"path/filepath"
"decl/internal/data"
"decl/internal/folio"
_ "decl/internal/mapper"
"log/slog"
"errors"
)
@ -31,7 +32,7 @@ type Common struct {
State string `json:"state,omitempty" yaml:"state,omitempty"`
config data.ConfigurationValueGetter
Resources data.ResourceMapper `json:"-" yaml:"-"`
Resources folio.ResourceMapper `json:"-" yaml:"-"`
Errors []error `json:"-" yaml:"-"`
}
@ -53,7 +54,7 @@ func (c *Common) ContentType() string {
return c.exttype
}
func (c *Common) SetResourceMapper(resources data.ResourceMapper) {
func (c *Common) SetResourceMapper(resources folio.ResourceMapper) {
c.Resources = resources
}
@ -158,6 +159,7 @@ func (c *Common) IsResourceInconsistent() (result bool) {
func (c *Common) AddError(err error) (error) {
if err != nil {
slog.Info("Common.AddError()", "errors", c.Errors, "err", err)
c.Errors = append(c.Errors, err)
}
return err

View File

@ -32,15 +32,22 @@ _ "os/exec"
"decl/internal/containerlog"
"bytes"
_ "encoding/base64"
"time"
"errors"
)
const (
ContainerTypeName TypeName = "container"
)
var (
ErrContainerWaitTimeOut = errors.New("Container wait timed out waiting for state")
)
type ContainerClient interface {
ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error)
ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error
ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error
ContainerList(context.Context, container.ListOptions) ([]types.Container, error)
ContainerInspect(context.Context, string) (types.ContainerJSON, error)
ContainerRemove(context.Context, string, container.RemoveOptions) error
@ -91,7 +98,6 @@ type Container struct {
Stderr string `json:"stderr,omitempty" yaml:"stderr,omitempty"`
apiClient ContainerClient
Resources data.ResourceMapper `json:"-" yaml:"-"`
}
func init() {
@ -137,10 +143,6 @@ func (c *Container) SetParsedURI(u data.URIParser) (err error) {
return
}
func (c *Container) SetResourceMapper(resources data.ResourceMapper) {
c.Resources = resources
}
func (c *Container) Clone() data.Resource {
return &Container {
Id: c.Id,
@ -233,7 +235,6 @@ func (c *Container) Notify(m *machine.EventMessage) {
panic(createErr)
}
case "start_delete":
slog.Info("Container.Notify()", "event", "start_delete")
if deleteErr := c.Delete(ctx); deleteErr == nil {
if triggerErr := c.StateMachine().Trigger("deleted"); triggerErr == nil {
return
@ -245,6 +246,26 @@ func (c *Container) Notify(m *machine.EventMessage) {
c.Common.State = "present"
panic(deleteErr)
}
case "restarting":
if restartErr := c.Restart(ctx); restartErr == nil {
if triggerErr := c.StateMachine().Trigger("restarted"); triggerErr == nil {
return
} else {
_ = c.AddError(triggerErr)
}
} else {
_ = c.AddError(restartErr)
if c.IsResourceInconsistent() {
if triggerErr := c.StateMachine().Trigger("restart-failed"); triggerErr == nil {
panic(restartErr)
} else {
_ = c.AddError(triggerErr)
panic(fmt.Errorf("%w - %w", restartErr, triggerErr))
}
}
_ = c.StateMachine().Trigger("notexists")
panic(restartErr)
}
case "present", "created", "read":
c.Common.State = "present"
case "running":
@ -475,10 +496,10 @@ func (c *Container) Read(ctx context.Context) ([]byte, error) {
func (c *Container) Inspect(ctx context.Context, containerID string) error {
containerJSON, err := c.apiClient.ContainerInspect(ctx, containerID)
if client.IsErrNotFound(err) {
if client.IsErrNotFound(err) || containerJSON.State == nil {
c.Common.State = "absent"
} else {
c.Common.State = "present"
c.Common.State = containerJSON.State.Status
c.Id = containerJSON.ID
if c.Name == "" {
if containerJSON.Name[0] == '/' {
@ -583,3 +604,64 @@ func (c *Container) ResolveId(ctx context.Context) string {
}
return ""
}
type WaitCondition func(state *types.ContainerState) bool
func (c *Container) wait(ctx context.Context, untilstate WaitCondition) error {
statusCh := make(chan bool)
timeoutCtx, cancel := context.WithTimeout(ctx, 60 * time.Second)
defer cancel()
go func() {
for {
if containerJSON, err := c.apiClient.ContainerInspect(ctx, c.Id); err == nil {
statusCh <- untilstate(containerJSON.State)
return
}
time.Sleep(2 * time.Second)
}
}()
select {
case <-timeoutCtx.Done():
return ErrContainerWaitTimeOut
case <-statusCh:
}
return nil
}
func (c *Container) Restart(ctx context.Context) (err error) {
if err = c.apiClient.ContainerRestart(ctx, c.Id, container.StopOptions{}); err != nil {
return err
}
if err = c.wait(ctx, func(state *types.ContainerState) bool {
return state.Running
}); err != nil {
return err
}
/*
waitTimeout := 60 * time.Second
interval := 2 * time.Second
deadline := time.Now().Add(waitTimeout)
for {
if time.Now().After(deadline) {
panic("")
}
if state.Running {
if state.Health != nil {
fmt.Println("Health status:", state.Health.Status)
if state.Health.Status == "healthy" {
fmt.Println("Container is healthy and running.")
break
}
} else {
fmt.Println("Container is running (no health check defined).")
break
}
}
time.Sleep(interval)
}
*/
return nil
}

View File

@ -93,7 +93,6 @@ type ContainerImage struct {
outputWriter strings.Builder `json:"-" yaml:"-"`
apiClient ContainerImageClient
Resources data.ResourceMapper `json:"-" yaml:"-"`
contextDocument data.Document `json:"-" yaml:"-"`
ConverterTypes data.TypesRegistry[data.Converter] `json:"-" yaml:"-"`
@ -193,10 +192,6 @@ func (c *ContainerImage) NormalizePath() error {
return nil
}
func (c *ContainerImage) SetResourceMapper(resources data.ResourceMapper) {
c.Resources = resources
}
func (c *ContainerImage) Clone() data.Resource {
return &ContainerImage {
Common: c.Common,

View File

@ -214,8 +214,8 @@ attributes:
_, readErr := contextFile.Resource().Read(context.Background())
assert.Nil(t, readErr)
c.Resources = data.NewResourceMapper()
c.Resources.Set(contextDirUri, contextFile)
c.Resources = folio.NewResourceMapper()
c.Resources.Set(folio.URI(contextDirUri), contextFile)
d, contextErr := c.ContextDocument()
assert.Nil(t, contextErr)

View File

@ -49,7 +49,6 @@ type ContainerNetwork struct {
Created time.Time `json:"created" yaml:"created"`
apiClient ContainerNetworkClient
Resources data.ResourceMapper `json:"-" yaml:"-"`
}
func init() {
@ -92,10 +91,6 @@ func (n *ContainerNetwork) NormalizePath() error {
return nil
}
func (n *ContainerNetwork) SetResourceMapper(resources data.ResourceMapper) {
n.Resources = resources
}
func (n *ContainerNetwork) Clone() data.Resource {
return &ContainerNetwork {
Common: n.Common.Clone(),

View File

@ -5,6 +5,7 @@ package resource
import (
"context"
"decl/tests/mocks"
"decl/internal/codec"
_ "encoding/json"
_ "fmt"
"github.com/docker/docker/api/types"
@ -20,6 +21,7 @@ _ "os"
"strings"
"testing"
"bytes"
"time"
)
func TestNewContainerResource(t *testing.T) {
@ -47,6 +49,9 @@ func TestReadContainer(t *testing.T) {
ID: "123456789abc",
Name: "test",
Image: "alpine",
State: &types.ContainerState{
Status: "running",
},
}}, nil
},
InjectContainerWait: func(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
@ -62,18 +67,19 @@ func TestReadContainer(t *testing.T) {
}
c := NewContainer(m)
assert.NotEqual(t, nil, c)
assert.NotNil(t, c)
e := c.LoadDecl(decl)
assert.Equal(t, nil, e)
assert.Nil(t, e)
assert.Equal(t, "testcontainer", c.Name)
resourceYaml, readContainerErr := c.Read(ctx)
assert.Equal(t, nil, readContainerErr)
assert.Nil(t, readContainerErr)
assert.Greater(t, len(resourceYaml), 0)
assert.Equal(t, "running", c.State)
}
func TestCreateContainer(t *testing.T) {
func TestCreateDeleteContainer(t *testing.T) {
m := &mocks.MockContainerClient{
InjectContainerCreate: func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) {
return container.CreateResponse{ID: "abcdef012", Warnings: []string{}}, nil
@ -148,3 +154,119 @@ func TestContainerLogOutput(t *testing.T) {
assert.Equal(t, "done.", c.Stdout)
}
func TestWaitContainer(t *testing.T) {
mockState := &types.ContainerState{
Status: "",
}
m := &mocks.MockContainerClient{
InjectContainerInspect: func(ctx context.Context, containerID string) (types.ContainerJSON, error) {
return types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
ID: "abcdef012",
Name: "testcontainer",
Image: "alpine",
State: mockState,
}}, nil
},
InjectContainerCreate: func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) {
mockState.Status = "created"
return container.CreateResponse{ID: "abcdef012", Warnings: []string{}}, nil
},
InjectContainerStop: func(context.Context, string, container.StopOptions) error {
return nil
},
InjectContainerRemove: func(context.Context, string, container.RemoveOptions) error {
return nil
},
InjectContainerWait: func(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
var res container.WaitResponse
resChan := make(chan container.WaitResponse)
errChan := make(chan error, 1)
go func() { resChan <- res }()
return resChan, errChan
},
InjectContainerLogs: func(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("done.")), nil
},
}
c := NewContainer(m)
assert.Nil(t, c.LoadString(`
name: "testcontainer"
image: "alpine"
state: present
`, codec.FormatYaml))
assert.Nil(t, c.Apply())
assert.Equal(t, "testcontainer", c.Name)
assert.Nil(t, c.wait(context.Background(), func(state *types.ContainerState) bool {
return state.Status == "running"
}))
}
func TestRestartContainer(t *testing.T) {
mockState := &types.ContainerState{
Status: "",
}
m := &mocks.MockContainerClient{
InjectContainerInspect: func(ctx context.Context, containerID string) (types.ContainerJSON, error) {
return types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
ID: "abcdef012",
Name: "testcontainer",
Image: "alpine",
State: mockState,
}}, nil
},
InjectContainerCreate: func(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) {
mockState.Status = "created"
return container.CreateResponse{ID: "abcdef012", Warnings: []string{}}, nil
},
InjectContainerStop: func(context.Context, string, container.StopOptions) error {
return nil
},
InjectContainerRemove: func(context.Context, string, container.RemoveOptions) error {
return nil
},
InjectContainerRestart: func(ctx context.Context, containerID string, options container.StopOptions) error {
go func() {
time.Sleep(100 * time.Millisecond)
mockState.Status = "running"
mockState.Running = true
}()
return nil
},
InjectContainerWait: func(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
var res container.WaitResponse
resChan := make(chan container.WaitResponse)
errChan := make(chan error, 1)
go func() { resChan <- res }()
return resChan, errChan
},
InjectContainerLogs: func(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("done.")), nil
},
}
c := NewContainer(m)
assert.Nil(t, c.LoadString(`
name: "testcontainer"
image: "alpine"
state: present
`, codec.FormatYaml))
assert.Equal(t, "testcontainer", c.Name)
assert.Nil(t, c.Apply())
c.State = "present" // overwrite the state
c.StateMachine().Trigger("restart")
assert.Equal(t, "running", c.State)
assert.Nil(t, c.Apply())
}

View File

@ -42,7 +42,6 @@ type ContainerVolume struct {
volume.Volume `json:",inline" yaml:",inline"`
apiClient ContainerVolumeClient
Resources data.ResourceMapper `json:"-" yaml:"-"`
}
func init() {
@ -85,10 +84,6 @@ func (v *ContainerVolume) NormalizePath() error {
return nil
}
func (v *ContainerVolume) SetResourceMapper(resources data.ResourceMapper) {
v.Resources = resources
}
func (v *ContainerVolume) Clone() data.Resource {
return &ContainerVolume {
Common: v.Common.Clone(),

View File

@ -34,8 +34,6 @@ type Exec struct {
ReadTemplate *command.Command `yaml:"read,omitempty" json:"read,omitempty"`
UpdateTemplate *command.Command `yaml:"update,omitempty" json:"update,omitempty"`
DeleteTemplate *command.Command `yaml:"delete,omitempty" json:"delete,omitempty"`
Resources data.ResourceMapper `yaml:"-" json:"-"`
}
func init() {
@ -63,10 +61,6 @@ func NewExec() *Exec {
return e
}
func (x *Exec) SetResourceMapper(resources data.ResourceMapper) {
x.Resources = resources
}
func (x *Exec) Clone() data.Resource {
return &Exec {
Common: x.Common.Clone(),

View File

@ -35,7 +35,16 @@ func TestExecApplyResourceTransformation(t *testing.T) {
}
func TestReadExec(t *testing.T) {
x := NewExec()
decl := `
read:
path: ls
args:
- -al
`
assert.Nil(t, x.LoadDecl(decl))
assert.Equal(t, "ls", x.ReadTemplate.Path)
assert.Equal(t, command.CommandArg("-al"), x.ReadTemplate.Args[0])
}
func TestReadExecError(t *testing.T) {

View File

@ -48,7 +48,6 @@ type Group struct {
ReadCommand *command.Command `json:"-" yaml:"-"`
UpdateCommand *command.Command `json:"-" yaml:"-"`
DeleteCommand *command.Command `json:"-" yaml:"-"`
Resources data.ResourceMapper `json:"-" yaml:"-"`
groupStatus *user.Group `json:"-" yaml:"-"`
}
@ -102,10 +101,6 @@ func (g *Group) NormalizePath() error {
return nil
}
func (g *Group) SetResourceMapper(resources data.ResourceMapper) {
g.Resources = resources
}
func (g *Group) Clone() data.Resource {
newg := &Group {
Common: g.Common,

View File

@ -91,7 +91,6 @@ type HTTP struct {
LastModified time.Time `json:"lastmodified,omitempty" yaml:"lastmodified,omitempty"`
Size int64 `yaml:"size,omitempty" json:"size,omitempty"`
SignatureValue string `yaml:"signature,omitempty" json:"signature,omitempty"`
Resources data.ResourceMapper `yaml:"-" json:"-"`
reader *transport.Reader `yaml:"-" json:"-"`
writer *transport.ReadWriter `yaml:"-" json:"-"`
}
@ -127,10 +126,6 @@ func (h *HTTP) NormalizePath() error {
return nil
}
func (h *HTTP) SetResourceMapper(resources data.ResourceMapper) {
h.Resources = resources
}
func (h *HTTP) Open() (err error) {
u := h.Common.parsedURI
if u == nil {

View File

@ -136,8 +136,6 @@ type Iptable struct {
ReadChainCommand *command.Command `yaml:"-" json:"-"`
UpdateCommand *command.Command `yaml:"-" json:"-"`
DeleteCommand *command.Command `yaml:"-" json:"-"`
Resources data.ResourceMapper `yaml:"-" json:"-"`
}
@ -185,10 +183,6 @@ func (i *Iptable) Init(u data.URIParser) (err error) {
return
}
func (i *Iptable) SetResourceMapper(resources data.ResourceMapper) {
i.Resources = resources
}
func (i *Iptable) Clone() data.Resource {
newIpt := &Iptable {
Common: i.Common,
@ -286,13 +280,13 @@ func (i *Iptable) Notify(m *machine.EventMessage) {
// Set the chain ID and update the mapped URI
func (i *Iptable) SetId(id uint) {
if i.Id != id {
uri := i.URI()
uri := folio.URI(i.URI())
i.Id = id
decl, ok := i.Resources.Get(uri)
if ok {
i.Resources.Delete(uri)
}
i.Resources.Set(i.URI(), decl)
i.Resources.Set(folio.URI(i.URI()), decl)
}
}

View File

@ -20,7 +20,7 @@ _ "syscall"
"testing"
_ "time"
"decl/internal/command"
"decl/internal/data"
"decl/internal/folio"
)
func TestNewIptableResource(t *testing.T) {
@ -167,7 +167,7 @@ func TestIptableRuleExtractorById(t *testing.T) {
func TestIptableRuleExtractorByFlags(t *testing.T) {
ipt := NewIptable()
ipt.Resources = data.NewResourceMapper()
ipt.Resources = folio.NewResourceMapper()
assert.NotNil(t, ipt)
ipt.Table = IptableName("filter")
ipt.Chain = IptableChain("FOO")

View File

@ -0,0 +1,34 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
"context"
_ "gopkg.in/yaml.v3"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/transport"
"decl/internal/ext"
"strings"
"io"
)
func NewMockBufferResource(id string, target *strings.Builder) *MockResource {
return &MockResource {
InjectType: func() string { return "buffer" },
InjectResolveId: func(ctx context.Context) string { return id },
InjectRead: func(ctx context.Context) ([]byte, error) { return nil,nil },
InjectLoadDecl: func(string) error { return nil },
InjectApply: func() error { return nil },
InjectStateMachine: func() machine.Stater { return nil },
InjectContentWriterStream: func() (*transport.Writer, error) {
w := &transport.Writer{}
w.SetStream(ext.WriteNopCloser(target))
return w, nil
},
InjectContentReaderStream: func() (*transport.Reader, error) {
r := &transport.Reader{}
r.SetStream(io.NopCloser(strings.NewReader(target.String())))
return r, nil
},
}
}

View File

@ -9,9 +9,13 @@ _ "gopkg.in/yaml.v3"
_ "fmt"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/data"
"decl/internal/codec"
"decl/internal/transport"
"io"
)
type MockResource struct {
*Common `json:",inline" yaml:",inline"`
InjectURI func() string
InjectType func() string
InjectResolveId func(ctx context.Context) string
@ -20,6 +24,9 @@ type MockResource struct {
InjectApply func() error
InjectRead func(context.Context) ([]byte, error)
InjectStateMachine func() machine.Stater
InjectContentType func() string
InjectContentReaderStream func() (*transport.Reader, error)
InjectContentWriterStream func() (*transport.Writer, error)
}
func (m *MockResource) Clone() data.Resource {
@ -68,3 +75,64 @@ func (m *MockResource) UnmarshalJSON(data []byte) error {
}
return nil
}
/*
func (m *MockResource) SetParsedURI(URIParser) error {
return nil
}
*/
func (m *MockResource) UseConfig(config data.ConfigurationValueGetter) {
}
func (m *MockResource) LoadString(string, codec.Format) (error) {
return nil
}
func (m *MockResource) Load([]byte, codec.Format) (error) {
return nil
}
func (m *MockResource) LoadReader(io.ReadCloser, codec.Format) (error) {
return nil
}
func (m *MockResource) Create(context.Context) error {
return nil
}
func (m *MockResource) Update(context.Context) error {
return nil
}
func (m *MockResource) Delete(context.Context) error {
return nil
}
func (m *MockResource) ReadStat() error {
return nil
}
func (m *MockResource) ContentType() string {
if m.InjectContentType == nil {
return ""
}
return m.InjectContentType()
}
func (m *MockResource) ContentReaderStream() (*transport.Reader, error) {
return m.InjectContentReaderStream()
}
func (m *MockResource) ContentWriterStream() (*transport.Writer, error) {
return m.InjectContentWriterStream()
}
func (m *MockResource) GetContent(w io.Writer) (contentReader io.ReadCloser, err error) {
return nil, nil
}
func (m *MockResource) SetContent(r io.Reader) error {
return nil
}

View File

@ -96,8 +96,6 @@ type NetworkRoute struct {
ReadCommand *command.Command `yaml:"-" json:"-"`
UpdateCommand *command.Command `yaml:"-" json:"-"`
DeleteCommand *command.Command `yaml:"-" json:"-"`
Resources data.ResourceMapper `json:"-" yaml:"-"`
}
func NewNetworkRoute() *NetworkRoute {
@ -119,10 +117,6 @@ func (n *NetworkRoute) NormalizePath() error {
return nil
}
func (n *NetworkRoute) SetResourceMapper(resources data.ResourceMapper) {
n.Resources = resources
}
func (n *NetworkRoute) Clone() data.Resource {
newn := &NetworkRoute {
Common: n.Common,

View File

@ -28,6 +28,7 @@ import (
var (
ErrOpenPGPEncryptionFailure error = errors.New("OpenPGP encryption failure")
ErrOpenPGPDecryptionFailure error = errors.New("OpenPGP decryption failure")
)
const (
@ -65,6 +66,7 @@ func NewOpenPGPKeyRing() (o *OpenPGPKeyRing) {
Common: NewCommon(OpenPGPKeyRingTypeName, false),
Bits: 2048,
}
o.Common.NormalizePath = o.NormalizePath
return
}
@ -81,7 +83,6 @@ func (o *OpenPGPKeyRing) NormalizePath() error {
return nil
}
func (o *OpenPGPKeyRing) Validate() (err error) {
var keyringJson []byte
if keyringJson, err = o.JSON(); err == nil {
@ -124,8 +125,29 @@ func (o *OpenPGPKeyRing) EncryptPrivateKey(entity *openpgp.Entity) error {
return nil
}
func (o *OpenPGPKeyRing) DecryptPrivateKey(entity *openpgp.Entity) error {
if o.config != nil && entity.PrivateKey != nil {
passphraseConfig, _ := o.config.GetValue("passphrase")
passphrase := []byte(passphraseConfig.(string))
if len(passphrase) > 0 {
if decryptErr := entity.PrivateKey.Decrypt(passphrase); decryptErr != nil {
return fmt.Errorf("%w private key: %w", ErrOpenPGPDecryptionFailure, decryptErr)
}
for _, subkey := range entity.Subkeys {
if decryptErr := subkey.PrivateKey.Encrypt(passphrase); decryptErr != nil {
return fmt.Errorf("%w subkey (private key): %w", ErrOpenPGPDecryptionFailure, decryptErr)
}
}
}
}
return nil
}
func (o *OpenPGPKeyRing) Create(ctx context.Context) (err error) {
var entity *openpgp.Entity
var keyRingFileStream io.WriteCloser
var keyRingBuffer bytes.Buffer
cfg := o.Config()
entity, err = openpgp.NewEntity(o.Name, o.Comment, o.Email, cfg)
o.entityList = append(o.entityList, entity)
@ -142,25 +164,37 @@ func (o *OpenPGPKeyRing) Create(ctx context.Context) (err error) {
return
}
if len(o.KeyRing) == 0 {
var keyringBuffer bytes.Buffer
if publicKeyWriter, err := armor.Encode(&keyringBuffer, openpgp.PublicKeyType, nil); err == nil {
if err = entity.Serialize(publicKeyWriter); err == nil {
}
publicKeyWriter.Close()
}
keyringBuffer.WriteString("\n")
if privateKeyWriter, err := armor.Encode(&keyringBuffer, openpgp.PrivateKeyType, nil); err == nil {
if len(o.KeyRing) == 0 { // XXX this should probably always overwrite the value of KeyRing
if privateKeyWriter, err := armor.Encode(&keyRingBuffer, openpgp.PrivateKeyType, nil); err == nil {
if err = entity.SerializePrivateWithoutSigning(privateKeyWriter, nil); err == nil {
} else {
slog.Error("Failed writing privatekey", "err", err)
}
privateKeyWriter.Close()
keyRingBuffer.WriteString("\n")
}
keyringBuffer.WriteString("\n")
if publicKeyWriter, err := armor.Encode(&keyRingBuffer, openpgp.PublicKeyType, nil); err == nil {
if err = entity.Serialize(publicKeyWriter); err == nil {
o.KeyRing = keyringBuffer.String()
} else {
slog.Error("Failed writing public key", "err", err)
}
publicKeyWriter.Close()
keyRingBuffer.WriteString("\n")
}
o.KeyRing = keyRingBuffer.String()
}
if len(o.KeyRingRef) > 0 {
keyRingFileStream, _ = o.KeyRingRef.Lookup(o.Resources).ContentWriterStream()
defer keyRingFileStream.Close()
if _, writeErr := keyRingFileStream.Write([]byte(o.KeyRing)); writeErr != nil {
err = writeErr
}
}
return
}
@ -333,6 +367,8 @@ func (o *OpenPGPKeyRing) ResolveId(ctx context.Context) string {
if e := o.NormalizePath(); e != nil {
panic(e)
}
o.GetIdentityFromKeyRing()
o.Common.Path = fmt.Sprintf("%s/%s/%s", o.Name, o.Comment, o.Email)
return o.Common.Path
}
@ -363,6 +399,33 @@ func (o *OpenPGPKeyRing) ReadStat() (err error) {
return
}
func (o *OpenPGPKeyRing) GetIdentityFromKeyRing() error {
if len(o.KeyRing) > 0 {
if keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(o.KeyRing))); err != nil {
return err
} else {
for _, entity := range keyring {
for identityName, identity := range entity.Identities {
slog.Info("OpenPGPKeyRing.GetIdentityFromKeyRing()", "identity", identityName)
if identity.UserId != nil {
if len(identity.UserId.Name) > 0 {
o.Name = identity.UserId.Name
}
if len(identity.UserId.Comment) > 0 {
o.Comment = identity.UserId.Comment
}
if len(identity.UserId.Email) > 0 {
o.Email = identity.UserId.Email
}
return nil
}
}
}
}
}
return nil
}
func (o *OpenPGPKeyRing) Read(ctx context.Context) (yamlData []byte, err error) {
var keyringReader io.ReadCloser
statErr := o.ReadStat()
@ -371,11 +434,19 @@ func (o *OpenPGPKeyRing) Read(ctx context.Context) (yamlData []byte, err error)
}
if keyringReader, err = o.GetContent(nil); err == nil {
defer keyringReader.Close()
if krData, krErr := io.ReadAll(keyringReader); krErr == nil {
o.KeyRing = string(krData)
o.entityList, err = openpgp.ReadArmoredKeyRing(strings.NewReader(o.KeyRing))
} else {
err = krErr
return nil, fmt.Errorf("%w - %w: %s", ErrResourceStateAbsent, krErr, o.Path)
}
}
o.GetIdentityFromKeyRing()
for i := range o.entityList {
if decryptErr := o.DecryptPrivateKey(o.entityList[i]); decryptErr != nil {
return nil, fmt.Errorf("%w - %w: %s", ErrResourceStateAbsent, decryptErr, o.Path)
}
}
@ -408,19 +479,24 @@ func (o *OpenPGPKeyRing) readThru() (contentReader io.ReadCloser, err error) {
return
}
func (o *OpenPGPKeyRing) URI() string { return string(o.Common.URI()) }
func (o *OpenPGPKeyRing) URI() string {
return fmt.Sprintf("%s://%s/%s/%s", o.Type(), o.Name, o.Comment, o.Email)
}
func (o *OpenPGPKeyRing) Type() string { return "openpgp-keyring" }
func (o *OpenPGPKeyRing) Type() string { return string(OpenPGPKeyRingTypeName) }
func (o *OpenPGPKeyRing) ContentReaderStream() (*transport.Reader, error) {
if len(o.KeyRing) == 0 && ! o.KeyRingRef.IsEmpty() {
//if len(o.KeyRing) == 0 && ! o.KeyRingRef.IsEmpty() {
if ! o.KeyRingRef.IsEmpty() {
slog.Info("OpenPGPKeyRing.ContentReaderStream()", "keyring", o)
return o.KeyRingRef.Lookup(nil).ContentReaderStream()
}
return nil, fmt.Errorf("Cannot provide transport reader for string content")
}
func (o *OpenPGPKeyRing) ContentWriterStream() (*transport.Writer, error) {
if len(o.KeyRing) == 0 && ! o.KeyRingRef.IsEmpty() {
//if len(o.KeyRing) == 0 && ! o.KeyRingRef.IsEmpty() {
if ! o.KeyRingRef.IsEmpty() {
return o.KeyRingRef.Lookup(nil).ContentWriterStream()
}
return nil, fmt.Errorf("Cannot provide transport writer for string content")

View File

@ -8,6 +8,112 @@ import (
"testing"
"fmt"
"decl/internal/data"
"decl/internal/codec"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"bytes"
"log/slog"
"io"
"os"
"strings"
"path/filepath"
"io/ioutil"
)
var(
TestUserKeyPublic string = `
-----BEGIN PGP PUBLIC KEY BLOCK-----
xsBNBGhI/VUBCADDVSm3mKY5JsncMMJFV0zELMrmip7dkK3vvMvVWVmMHiC4akDH
WPxUdWNQPjE2e5HGF9Ebg0c7gu634VG470MnTzFdPV6f5zA+yJfdKrLq7gpl9QGW
jFLIeK/l4xc+MIpOE1rD9WqYUUw2IYY8YANYq4yB36rq41VuZps/adI9Go5IhfcU
3SVb7o7pa/gWE0FVu9ze31j2agC8FIKHgB++7bmYgbAQz5Qi1qgtG0Kn25QUacJ6
Akm2+h4w3SQCR6HLRV2BO29x9mFBszf2KQ7DW2VNiGyUuQQ3m8v2ZidG/11ff6U6
ad5tvr/8sYr5jOnKEJyDP9v9yQ04cU94GmsPABEBAAHNL1Rlc3RVc2VyMSAoVGVz
dFVzZXIxKSA8dGVzdHVzZXJAcm9zc2tlZW4uaG91c2U+wsCKBBMBCAA+BQJoSP1V
CRDfB5I2FtNl1xYhBADOnA8Aah3FXyl/m98HkjYW02XXAhsDAh4BAhkBAgsHAhUI
AxYAAgMnBwIAAGKLB/9whwgfLkoTYI+Q0Te2uZXl7FPyW0t4tbUSoI1aiVw+ymND
V+wuqYsjYDob2MyK/TAFkSCqZhBIRbGLRJtwzQwkF/amGuelHSSBX3LdxK/sp2UU
+zv/NBEP1LlUNKchuxpdBPYjKHdbOLPoKqFfdCujSxpWTIeKCwvnDtuP+PlsjUgc
afxMx+cVwe77AZ1Fi5CBD9Dr7nNsLovywrSiszyVh5eT0pbcu8Elf5oYQNT1q3ms
aqrw95shlGQiwVgrfTvRCDeZnKafHwMUoh48otsNI9aS0HpChi+JNd+PLe/Wlsj5
x+q0sHc+5/24zQvN0Wx9fPE4/7z8Pr+CUhAxjI7hzsBNBGhI/VUBCACfahoi2GRx
OfOqJ4ZN8Te+4bJeOQdzTofAisfCU736q+MvEKtAuY72vzWdERWpn7XABmtkfII8
Xyya8iHKuEGCETy0YoZ15GLBgSHhLnlN2U/4BkrUjGvUjZWquW22gfFB1q8ZdSGw
pg8hrspY/b+nnAGdawq+hfE6YRQTmG5tkBrctxfxglBnKzezmL1a3arTHWf2SRlL
eAWNI4sACryP/5plD6+9Z9YXTKeDlXnga6BzB80y7F2g0cPdtX20dWAGx4irq2I2
SxlABvWMZrdm2Upr6yLHD2zTpbf1FkKFUw9j0lrL1fMq0/j9zbKJsTh6CtOlF04s
AeoEV9SZ36GvABEBAAHCwHYEGAEIACoFAmhI/VUJEN8HkjYW02XXFiEEAM6cDwBq
HcVfKX+b3weSNhbTZdcCGwwAAFxWB/9Af7qpYKldDz5iNYQluFoPfyHAVlOpiakf
hvnu7hijTEgB1frByKu2m+k4m7gANBgfWldsYi1feWBVn6fspXuCc0k7opmQgol7
ueTrsGnNB7U0l14HcKSTLZ2c/Jt7ZS6aXXjxpYPq0/5lt8+5sRkXxHI9HhAx6gK/
RJCNITRt/V/gT4YWWbz8VHgkPIfHFsjr8brVhm4V/+JKZAG4eSNQp4QrTpdIjdfa
1rd91qffyrWhPNTQErE2jFOXSU/OojdDjGJ7pwr0qj4FHACVL3/oqYog2NrUWf92
QcwwWrEZNZUpJEMUB5q+tOoZ40ll0A8lsvABULZDqWveYpRQF+rQ
=jBvZ
-----END PGP PUBLIC KEY BLOCK-----
`
TestUserKeyPair string = `
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
=BAfD
-----END PGP PRIVATE KEY BLOCK-----
`
)
func TestNewOpenPGPKeyRingResource(t *testing.T) {
@ -106,3 +212,81 @@ func TestReadKeyRing(t *testing.T) {
assert.Contains(t, testKeyRing.entityList[0].Identities, "TestUser (TestUser) <matthewrich.conf@gmail.com>")
}
func TestDecryptKeyPairRing(t *testing.T) {
keyReader := bytes.NewBufferString(TestUserKeyPair)
block, err := armor.Decode(keyReader)
assert.NotEqual(t, err, io.EOF)
assert.Nil(t, err)
assert.False(t, block.Type != openpgp.PublicKeyType && block.Type != openpgp.PrivateKeyType)
slog.Info("TestDecryptKeyPairRing", "block", block)
entityList, err := openpgp.ReadKeyRing(block.Body)
assert.True(t, entityList[0].PrivateKey.Encrypted)
// entityList, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(TestUserKeyPair))
slog.Info("TestDecryptKeyPairRing", "entity", entityList[0], "err", err)
assert.Nil(t, err)
assert.NotNil(t, entityList)
entityList[0].PrivateKey.Decrypt([]byte("foo"))
assert.False(t, entityList[0].PrivateKey.Encrypted)
/*
for _, subkey := range entity.Subkeys {
if decryptErr := subkey.PrivateKey.Encrypt(passphrase); decryptErr != nil {
return fmt.Errorf("%w subkey (private key): %w", ErrOpenPGPDecryptionFailure, decryptErr)
}
}
*/
}
func TestReadKeyPairRing(t *testing.T) {
ctx := context.Background()
testDataDir := "../../tests/data/openpgp-keyring"
files, err := os.ReadDir(testDataDir)
assert.Nil(t, err)
for _, entry := range files {
if entry.IsDir() {
continue
}
if ! strings.HasSuffix(strings.ToLower(entry.Name()), ".asc") {
continue
}
path := filepath.Join(testDataDir, entry.Name())
fileData, err := ioutil.ReadFile(path)
declarationAttributes := strings.Builder{}
enc := codec.NewYAMLEncoder(&declarationAttributes)
type kr struct {
KeyRing string `yaml:"keyring"`
}
assert.Nil(t, enc.Encode(&kr{ KeyRing: string(fileData) }))
testKeyRing := NewOpenPGPKeyRing()
testKeyRing.UseConfig(MockConfigValueGetter(func(key string) (any, error) {
switch key {
case "passphrase":
return "foo", nil
}
return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key)
}))
e := testKeyRing.LoadDecl(declarationAttributes.String())
assert.Nil(t, e)
y, err := testKeyRing.Read(ctx)
assert.Nil(t, err)
assert.NotNil(t, y)
assert.Greater(t, len(testKeyRing.entityList), 0)
if strings.HasSuffix(strings.ToLower(entry.Name()), "_private.asc") {
assert.NotNil(t, testKeyRing.entityList[0].PrivateKey)
}
}
}

View File

@ -27,8 +27,11 @@ import (
)
var (
ErrSignatureFailedDecodingSignature error = errors.New("Failed decoding signature")
ErrSignatureWriterFailed error = errors.New("Failed creating signature writer")
ErrArmoredWriterFailed error = errors.New("Failed to create armored writer")
ErrSignatureVerificationUnknownEntity error = errors.New("Signature uses unknown entity")
ErrSignatureMissing error = errors.New("Signature value undefined")
)
const (
@ -48,14 +51,17 @@ func init() {
}
type OpenPGPSignature struct {
*Common `json:",inline" yaml:",inline"`
*Common `json:"-,inline" yaml:"-,inline"`
stater machine.Stater `json:"-" yaml:"-"`
Signature string `json:"signature,omitempty" yaml:"signature,omitempty"`
KeyRingRef folio.ResourceReference `json:"keyringref,omitempty" yaml:"keyringref,omitempty"`
SourceRef folio.ResourceReference `json:"soureref,omitempty" yaml:"sourceref,omitempty"`
SignatureRef folio.ResourceReference `json:"signatureref,omitempty" yaml:"signatureref,omitempty"`
Signed string `json:"signed,omitempty" yaml:"signed,omitempty"`
KeyRingRef folio.Ref `json:"keyringref,omitempty" yaml:"keyringref,omitempty"`
SourceRef folio.Ref `json:"soureref,omitempty" yaml:"sourceref,omitempty"`
SignatureRef folio.Ref `json:"signatureref,omitempty" yaml:"signatureref,omitempty"`
signatureBlock *armor.Block
message *openpgp.MessageDetails
entityList openpgp.EntityList
}
@ -90,10 +96,32 @@ func (o *OpenPGPSignature) StateMachine() machine.Stater {
}
func (o *OpenPGPSignature) URI() string {
return string(o.Common.URI())
parsedSourceUri := o.SourceRef.Uri.Parse().URL()
return fmt.Sprintf("%s://%s", o.Type(), parsedSourceUri.Host + parsedSourceUri.RequestURI())
}
func (o *OpenPGPSignature) DecryptPrivateKey(entity *openpgp.Entity) error {
if o.config != nil {
passphraseConfig, _ := o.config.GetValue("passphrase")
passphrase := []byte(passphraseConfig.(string))
if len(passphrase) > 0 {
slog.Info("OpenPGPSignature.DecryptPrivateKey", "passphrase", passphrase, "entity", entity)
if decryptErr := entity.PrivateKey.Decrypt(passphrase); decryptErr != nil {
return fmt.Errorf("%w private key: %w", ErrOpenPGPDecryptionFailure, decryptErr)
}
for _, subkey := range entity.Subkeys {
if decryptErr := subkey.PrivateKey.Encrypt(passphrase); decryptErr != nil {
return fmt.Errorf("%w subkey (private key): %w", ErrOpenPGPDecryptionFailure, decryptErr)
}
}
}
}
return nil
}
func (o *OpenPGPSignature) SigningEntity() (err error) {
if len(o.entityList) < 1 {
slog.Info("OpenPGPSignature.SigningEntity() - Loading KeyRing", "keyring", o.KeyRingRef)
if o.KeyRingRef.IsEmpty() {
var keyringConfig any
if keyringConfig, err = o.config.GetValue("keyring"); err == nil {
@ -102,29 +130,60 @@ func (o *OpenPGPSignature) SigningEntity() (err error) {
} else {
ringFileStream, _ := o.KeyRingRef.Lookup(o.Resources).ContentReaderStream()
defer ringFileStream.Close()
o.entityList, err = openpgp.ReadArmoredKeyRing(ringFileStream)
slog.Info("OpenPGPSignature.SigningEntity()", "entities", o.entityList[0])
}
for i := range o.entityList {
if decryptErr := o.DecryptPrivateKey(o.entityList[i]); decryptErr != nil {
err = decryptErr
}
}
}
return
}
func (o *OpenPGPSignature) Sign(message io.Reader, w io.Writer) (err error) {
var writer io.WriteCloser
entity := o.entityList[0]
if writer, err = openpgp.Sign(w, entity, nil, o.Config()); err == nil {
defer writer.Close()
_, err = io.Copy(writer, message)
} else {
if err = openpgp.DetachSign(w, entity, message, o.Config()); err != nil {
err = fmt.Errorf("%w: %w", ErrSignatureWriterFailed, err)
}
return
}
func (o *OpenPGPSignature) Verify(message io.Reader) (err error) {
if len(o.Signature) < 1 || o.signatureBlock == nil {
return fmt.Errorf("%w - %d", ErrSignatureMissing, len(o.Signature))
}
slog.Info("OpenPGPSignature.Verify()", "signature", o.Signature, "block", o.signatureBlock)
var entity *openpgp.Entity
entity, err = openpgp.CheckDetachedSignature(o.entityList, message, o.signatureBlock.Body, o.Config())
if entity == nil {
slog.Info("OpenPGPSignature.Verify() check signature failed", "signature", o.signatureBlock, "err", err)
return fmt.Errorf("%w: %w", ErrSignatureVerificationUnknownEntity, err)
}
return
}
func (o *OpenPGPSignature) Create(ctx context.Context) (err error) {
if err = o.SigningEntity(); err == nil {
var sourceReadStream io.ReadCloser
switch o.SourceRef.RefType {
case folio.ReferenceTypeDocument:
//sourceReadStream, err = o.SourceRef.Lookup(folio.DocumentRegistry.UriMap).ContentReaderStream()
sourceReadStream, err = o.SourceRef.Reader()
srcData, _ := io.ReadAll(sourceReadStream)
sourceReadStream.Close()
o.Signed = string(srcData)
sourceReadStream, err = o.SourceRef.Reader()
default:
sourceReadStream, err = o.SourceRef.Lookup(o.Resources).ContentReaderStream()
}
defer sourceReadStream.Close()
var signatureStream, armoredWriter io.WriteCloser
if o.SignatureRef.IsEmpty() {
@ -138,9 +197,9 @@ func (o *OpenPGPSignature) Create(ctx context.Context) (err error) {
if armoredWriter, err = armor.Encode(signatureStream, openpgp.SignatureType, nil); err != nil {
err = fmt.Errorf("%w: %w", ErrArmoredWriterFailed, err)
}
defer armoredWriter.Close()
err = o.Sign(sourceReadStream, armoredWriter)
armoredWriter.Close()
}
return
}
@ -196,8 +255,10 @@ func (o *OpenPGPSignature) Notify(m *machine.EventMessage) {
_ = o.AddError(triggerErr)
}
} else {
slog.Info("OpenPGPSignature.Notify()", "dest", "start_read", "err", readErr)
_ = o.AddError(readErr)
if o.IsResourceInconsistent() {
slog.Info("OpenPGPSignature.Notify()", "dest", "read-failed", "machine", o.StateMachine())
if triggerErr := o.StateMachine().Trigger("read-failed"); triggerErr == nil {
panic(readErr)
} else {
@ -320,16 +381,16 @@ func (o *OpenPGPSignature) ResolveId(ctx context.Context) string {
}
func (o *OpenPGPSignature) GetContentSourceRef() string {
return string(o.SignatureRef)
return o.SignatureRef.String()
}
func (o *OpenPGPSignature) SetContentSourceRef(uri string) {
o.SignatureRef = folio.ResourceReference(uri)
o.SignatureRef.Uri = folio.URI(uri)
}
func (o *OpenPGPSignature) SignatureRefStat() (info fs.FileInfo, err error) {
err = fmt.Errorf("%w: %s", ErrResourceStateAbsent, o.SignatureRef)
if len(o.SignatureRef) > 0 {
err = fmt.Errorf("%w: %s", ErrResourceStateAbsent, o.SignatureRef.Uri)
if len(o.SignatureRef.Uri) > 0 {
rs, _ := o.ContentReaderStream()
defer rs.Close()
return rs.Stat()
@ -346,6 +407,16 @@ func (o *OpenPGPSignature) ReadStat() (err error) {
return err
}
func (o *OpenPGPSignature) SourceRefStat() (info fs.FileInfo, err error) {
err = fmt.Errorf("%w: %s", ErrResourceStateAbsent, o.SourceRef.Uri)
if ! o.SourceRef.IsEmpty() {
rs, _ := o.SourceRef.ContentReaderStream()
defer rs.Close()
return rs.Stat()
}
return
}
func (o *OpenPGPSignature) Update(ctx context.Context) error {
return o.Create(ctx)
}
@ -354,7 +425,69 @@ func (o *OpenPGPSignature) Delete(ctx context.Context) error {
return os.Remove(o.Common.Path)
}
func (o *OpenPGPSignature) Read(ctx context.Context) ([]byte, error) {
func (o *OpenPGPSignature) readSignatureRef() (err error) {
// signatureref
// XXX which takes precedence: the value of Signature from yaml or the value of Signature loaded from SignatureRef?
if (! o.SignatureRef.IsEmpty()) && o.SignatureRef.Exists() {
signatureReader, _ := o.SignatureRef.Lookup(o.Resources).ContentReaderStream()
defer signatureReader.Close()
SignatureData, readErr := io.ReadAll(signatureReader)
if readErr != nil {
return readErr
}
o.Signature = string(SignatureData)
}
return nil
}
func (o *OpenPGPSignature) DecodeSignatureBlock() (err error) {
slog.Info("OpenPGPSignature.DecodeSignatureBlock()", "signature", o.Signature)
if o.signatureBlock, err = armor.Decode(bytes.NewReader([]byte(o.Signature))); o.signatureBlock == nil || err != nil {
err = ErrSignatureFailedDecodingSignature
}
return
}
// Read
// - signature(string/ref) content
// - keyringref -> loads signing entity
// - sourceref to generate a signature (create)
func (o *OpenPGPSignature) Read(ctx context.Context) (yamlData []byte, err error) {
if len(o.Signature) < 1 {
if err = o.readSignatureRef(); err != nil {
panic(err)
}
}
if err = o.DecodeSignatureBlock(); err != nil {
panic(err)
}
slog.Info("OpenPGPSignature.Read() - decodesignatureblock", "sourceref", o.SourceRef, "entityList", o.entityList, "signatureblock", o.signatureBlock)
// sourceref
if _, sourceRefStatErr := o.SourceRefStat(); sourceRefStatErr != nil {
return nil, sourceRefStatErr
}
slog.Info("OpenPGPSignature.Read() - sourceref", "sourceref", o.SourceRef, "entityList", o.entityList, "signatureblock", o.signatureBlock)
if err = o.SigningEntity(); err != nil {
return nil, err
}
slog.Info("OpenPGPSignature.Read() - reading", "sourceref", o.SourceRef, "entityList", o.entityList, "signatureblock", o.signatureBlock)
slog.Info("OpenPGPSignature.Read() - reading sourceref", "openpgp-signature", o)
if sourceRefReader, sourceRefReaderErr := o.SourceRef.Reader(); sourceRefReaderErr != nil {
return nil, sourceRefReaderErr
} else {
defer sourceRefReader.Close()
if err = o.Verify(sourceRefReader); err != nil {
return nil, err
}
}
o.Common.State = "present"
slog.Info("OpenPGPSignature.Read()", "openpgp-signature", o)
return yaml.Marshal(o)
}

View File

@ -10,21 +10,208 @@ import (
"decl/internal/data"
"decl/internal/folio"
"decl/internal/codec"
"decl/internal/ext"
"log"
"io"
"os"
"bytes"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"log/slog"
"path/filepath"
)
func NewTestUserKeys() (data.ResourceMapper, folio.URI) {
uri := "openpgp-keyring://TestUser1/TestUser1/testuser@rosskeen.house"
var mockKeyRingPassphraseConfig MockConfigValueGetter = func(key string) (any, error) {
switch key {
case "passphrase":
return "foo", nil
}
return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key)
}
func DataKeyRing(uri folio.URI) (folio.ReferenceMapper[*folio.Declaration], folio.URI) {
var err error
var reader io.ReadCloser
searchPath := folio.NewSearchPath(folio.ConfigKey("system.importpath").GetStringSlice())
if u := uri.Parse().(*folio.ParsedURI); u != nil {
projectPath := filepath.Dir(filepath.Join(u.Hostname(), u.Path))
if err := searchPath.AddPath(projectPath); err != nil {
panic(err)
}
}
if reader, err = uri.ContentReaderStream(); err == nil {
keyRingDecl := folio.NewDeclaration()
if err = keyRingDecl.LoadReader(reader, codec.FormatYaml); err == nil {
keyRing := keyRingDecl.Resource()
testKeyRingResource := keyRing.(*OpenPGPKeyRing)
testKeyRingResource.KeyRingRef.FindIn(searchPath)
keyRing.UseConfig(mockKeyRingPassphraseConfig)
keyRing.Read(context.Background())
if testKeyRingResource.entityList[0].PrivateKey == nil {
log.Fatal("Keyring does not contain a private key")
}
testKeyRingResource.Resources = folio.NewResourceMapper()
testKeyRingResource.Resources.Set(folio.URI(keyRing.URI()), keyRingDecl)
return testKeyRingResource.Resources, folio.URI(testKeyRingResource.URI())
}
}
log.Fatal(err)
return nil, ""
}
func NewTestUserKeys(name string, comment string, email string) (folio.ReferenceMapper[*folio.Declaration], folio.URI) {
uri := fmt.Sprintf("openpgp-keyring://%s/%s/%s", name, comment, email)
keyRingDecl := folio.NewDeclaration()
keyRingDecl.NewResource(&uri)
ctx := context.Background()
declarationAttributes := fmt.Sprintf(`
name: TestUser1
comment: TestUser1
email: testuser@rosskeen.house
keyringref: file://%s/keyring.asc
`, string(TempDir))
name: %s
comment: %s
email: %s
keyringref: file://%s/keyring_%s.asc
`, name, comment, email, string(TempDir), name)
testKeyRing := keyRingDecl.Resource()
if e := testKeyRing.LoadString(declarationAttributes, codec.FormatYaml); e != nil {
log.Fatal(e)
}
testKeyRing.UseConfig(mockKeyRingPassphraseConfig)
if err := testKeyRing.Create(ctx); err != nil {
log.Fatal(err)
}
testKeyRingResource := testKeyRing.(*OpenPGPKeyRing)
if testKeyRingResource.entityList[0].PrivateKey == nil {
log.Fatal("Keyring does not contain a private key")
}
testKeyRingResource.Resources = folio.NewResourceMapper()
testKeyRingResource.Resources.Set(folio.URI(uri), keyRingDecl)
return testKeyRingResource.Resources, folio.URI(uri)
}
func KeyRingTestUser1() (folio.ReferenceMapper[*folio.Declaration], folio.URI) {
name := "TestUser1"
comment := "TestUser1"
email := "testuser@rosskeen.house"
if e := TempDir.CreateFile("keyring_TestUser1.asc", `
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
=BAfD
-----END PGP PRIVATE KEY BLOCK-----
-----BEGIN PGP PUBLIC KEY BLOCK-----
xsBNBGhI/VUBCADDVSm3mKY5JsncMMJFV0zELMrmip7dkK3vvMvVWVmMHiC4akDH
WPxUdWNQPjE2e5HGF9Ebg0c7gu634VG470MnTzFdPV6f5zA+yJfdKrLq7gpl9QGW
jFLIeK/l4xc+MIpOE1rD9WqYUUw2IYY8YANYq4yB36rq41VuZps/adI9Go5IhfcU
3SVb7o7pa/gWE0FVu9ze31j2agC8FIKHgB++7bmYgbAQz5Qi1qgtG0Kn25QUacJ6
Akm2+h4w3SQCR6HLRV2BO29x9mFBszf2KQ7DW2VNiGyUuQQ3m8v2ZidG/11ff6U6
ad5tvr/8sYr5jOnKEJyDP9v9yQ04cU94GmsPABEBAAHNL1Rlc3RVc2VyMSAoVGVz
dFVzZXIxKSA8dGVzdHVzZXJAcm9zc2tlZW4uaG91c2U+wsCKBBMBCAA+BQJoSP1V
CRDfB5I2FtNl1xYhBADOnA8Aah3FXyl/m98HkjYW02XXAhsDAh4BAhkBAgsHAhUI
AxYAAgMnBwIAAGKLB/9whwgfLkoTYI+Q0Te2uZXl7FPyW0t4tbUSoI1aiVw+ymND
V+wuqYsjYDob2MyK/TAFkSCqZhBIRbGLRJtwzQwkF/amGuelHSSBX3LdxK/sp2UU
+zv/NBEP1LlUNKchuxpdBPYjKHdbOLPoKqFfdCujSxpWTIeKCwvnDtuP+PlsjUgc
afxMx+cVwe77AZ1Fi5CBD9Dr7nNsLovywrSiszyVh5eT0pbcu8Elf5oYQNT1q3ms
aqrw95shlGQiwVgrfTvRCDeZnKafHwMUoh48otsNI9aS0HpChi+JNd+PLe/Wlsj5
x+q0sHc+5/24zQvN0Wx9fPE4/7z8Pr+CUhAxjI7hzsBNBGhI/VUBCACfahoi2GRx
OfOqJ4ZN8Te+4bJeOQdzTofAisfCU736q+MvEKtAuY72vzWdERWpn7XABmtkfII8
Xyya8iHKuEGCETy0YoZ15GLBgSHhLnlN2U/4BkrUjGvUjZWquW22gfFB1q8ZdSGw
pg8hrspY/b+nnAGdawq+hfE6YRQTmG5tkBrctxfxglBnKzezmL1a3arTHWf2SRlL
eAWNI4sACryP/5plD6+9Z9YXTKeDlXnga6BzB80y7F2g0cPdtX20dWAGx4irq2I2
SxlABvWMZrdm2Upr6yLHD2zTpbf1FkKFUw9j0lrL1fMq0/j9zbKJsTh6CtOlF04s
AeoEV9SZ36GvABEBAAHCwHYEGAEIACoFAmhI/VUJEN8HkjYW02XXFiEEAM6cDwBq
HcVfKX+b3weSNhbTZdcCGwwAAFxWB/9Af7qpYKldDz5iNYQluFoPfyHAVlOpiakf
hvnu7hijTEgB1frByKu2m+k4m7gANBgfWldsYi1feWBVn6fspXuCc0k7opmQgol7
ueTrsGnNB7U0l14HcKSTLZ2c/Jt7ZS6aXXjxpYPq0/5lt8+5sRkXxHI9HhAx6gK/
RJCNITRt/V/gT4YWWbz8VHgkPIfHFsjr8brVhm4V/+JKZAG4eSNQp4QrTpdIjdfa
1rd91qffyrWhPNTQErE2jFOXSU/OojdDjGJ7pwr0qj4FHACVL3/oqYog2NrUWf92
QcwwWrEZNZUpJEMUB5q+tOoZ40ll0A8lsvABULZDqWveYpRQF+rQ
=jBvZ
-----END PGP PUBLIC KEY BLOCK-----
`); e != nil {
log.Fatal(e)
}
uri := fmt.Sprintf("openpgp-keyring://%s/%s/%s", name, comment, email)
keyRingDecl := folio.NewDeclaration()
keyRingDecl.NewResource(&uri)
ctx := context.Background()
declarationAttributes := fmt.Sprintf(`
name: %s
comment: %s
email: %s
keyringref: file://%s/keyring_%s.asc
`, name, comment, email, string(TempDir), name)
testKeyRing := keyRingDecl.Resource()
if e := testKeyRing.LoadString(declarationAttributes, codec.FormatYaml); e != nil {
@ -39,96 +226,277 @@ func NewTestUserKeys() (data.ResourceMapper, folio.URI) {
return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key)
}))
if err := testKeyRing.Create(ctx); err != nil {
if _, err := testKeyRing.Read(ctx); err != nil {
log.Fatal(err)
}
return TestResourceMapper(func(key string) (data.Declaration, bool) {
return keyRingDecl, true
}), folio.URI(uri)
testKeyRingResource := testKeyRing.(*OpenPGPKeyRing)
testKeyRingResource.Resources = folio.NewResourceMapper()
if testKeyRingResource.entityList[0].PrivateKey == nil {
log.Fatal("Keyring does not contain a private key")
}
/*
return TestRefMapper[*folio.Declaration](func(key string) (*folio.Declaration, bool) {
switch key {
case uri:
slog.Info("KEYRING", "name", name, "addr", testKeyRing.(*OpenPGPKeyRing).entityList[0].PrivateKey)
return keyRingDecl, true
}
return nil, false
}), folio.URI(uri)
*/
testKeyRingResource.Resources.Set(folio.URI(uri), keyRingDecl)
return testKeyRingResource.Resources, folio.URI(uri)
}
func DataTestKeyRing(keyRingPath string) (folio.ReferenceMapper[*folio.Declaration], folio.URI) {
keyData, err := os.ReadFile(keyRingPath)
if err != nil {
panic(err)
}
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(keyData))
if err != nil {
panic(err)
}
var name, comment, email string
for _, entity := range keyring {
for _, identity := range entity.Identities {
if identity.UserId != nil {
name = identity.UserId.Name
comment = identity.UserId.Comment
email = identity.UserId.Email
}
}
}
uri := fmt.Sprintf("openpgp-keyring://%s/%s/%s", name, comment, email)
keyRingDecl := folio.NewDeclaration()
keyRingDecl.NewResource(&uri)
ctx := context.Background()
declarationAttributes := fmt.Sprintf(`
name: %s
comment: %s
email: %s
keyringref: file://%s
`, name, comment, email, keyRingPath)
testKeyRing := keyRingDecl.Resource()
if e := testKeyRing.LoadString(declarationAttributes, codec.FormatYaml); e != nil {
log.Fatal(e)
}
testKeyRing.UseConfig(MockConfigValueGetter(func(key string) (any, error) {
switch key {
case "passphrase":
return "foo", nil
}
return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key)
}))
if _, err := testKeyRing.Read(ctx); err != nil {
log.Fatal(err)
}
testKeyRingResource := testKeyRing.(*OpenPGPKeyRing)
testKeyRingResource.Resources = folio.NewResourceMapper()
if testKeyRingResource.entityList[0].PrivateKey == nil {
log.Fatal("Keyring does not contain a private key")
}
testKeyRingResource.Resources.Set(folio.URI(uri), keyRingDecl)
return testKeyRingResource.Resources, folio.URI(uri)
}
func TestNewOpenPGPSignatureResource(t *testing.T) {
assert.NotNil(t, NewOpenPGPSignature())
}
func TestCreateSignature(t *testing.T) {
ctx := context.Background()
func TestSigningEntity(t *testing.T) {
m, keyRingUri := NewTestUserKeys("TestUser5", "TestUser5", "testuser5@rosskeen.house")
assert.NotNil(t, m)
assert.Equal(t, folio.URI("openpgp-keyring://TestUser5/TestUser5/testuser5@rosskeen.house"), keyRingUri)
assert.Nil(t, TempDir.CreateFile("signing-test.txt", "test data"))
assert.FileExists(t, fmt.Sprintf("%s/keyring_TestUser5.asc", TempDir))
m, keyRingUri := NewTestUserKeys()
assert.Nil(t, TempDir.CreateFile("test.txt", "test data"))
testUserKeyRingResource := folio.Dereference(keyRingUri, m)
assert.NotNil(t, testUserKeyRingResource)
assert.NotNil(t, testUserKeyRingResource.Resource().(*OpenPGPKeyRing).entityList[0].PrivateKey)
declarationAttributes := fmt.Sprintf(`
keyringref: %s
sourceref: file://%s/test.txt
keyringref:
uri: %s
sourceref:
uri: file://%s/signing-test.txt
`, string(keyRingUri), string(TempDir))
testSignature := NewOpenPGPSignature()
e := testSignature.LoadString(declarationAttributes, codec.FormatYaml)
assert.Nil(t, e)
testSignature.Resources = m
testSignature.UseConfig(MockConfigValueGetter(func(key string) (any, error) {
switch key {
case "passphrase":
return "foo", nil
}
return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key)
}))
assert.False(t, testSignature.KeyRingRef.IsEmpty())
slog.Info("TestSigningEntity", "keyringref", testSignature.KeyRingRef, "resourcemap", testSignature.Resources)
keyRingResource := testSignature.KeyRingRef.Lookup(testSignature.Resources)
assert.NotNil(t, keyRingResource)
ringFileStream, streamErr := keyRingResource.ContentReaderStream()
assert.Nil(t, streamErr)
assert.NotNil(t, ringFileStream)
assert.Nil(t, testSignature.SigningEntity())
assert.NotNil(t, testSignature.entityList)
assert.Len(t, testSignature.entityList, 1)
assert.NotNil(t, testSignature.entityList[0].PrivateKey)
}
func TestSign(t *testing.T) {
//ctx := context.Background()
m, keyRingUri := NewTestUserKeys("TestUser2", "TestUser2", "testuser2@rosskeen.house")
assert.NotNil(t, m)
assert.Equal(t, folio.URI("openpgp-keyring://TestUser2/TestUser2/testuser2@rosskeen.house"), keyRingUri)
assert.Nil(t, TempDir.CreateFile("sign-test.txt", "test data"))
declarationAttributes := fmt.Sprintf(`
keyringref:
uri: %s
sourceref:
uri: file://%s/sign-test.txt
`, string(keyRingUri), string(TempDir))
testSignature := NewOpenPGPSignature()
testSignature.Resources = m
e := testSignature.LoadDecl(declarationAttributes)
assert.Nil(t, testSignature.LoadString(declarationAttributes, codec.FormatYaml))
testSignature.UseConfig(MockConfigValueGetter(func(key string) (any, error) {
switch key {
case "passphrase":
return "foo", nil
}
return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key)
}))
assert.Nil(t, testSignature.SigningEntity())
sourceReadStream, err := testSignature.SourceRef.Lookup(testSignature.Resources).ContentReaderStream()
var signatureStream, armoredWriter io.WriteCloser
assert.True(t, testSignature.SignatureRef.IsEmpty())
var signatureContent bytes.Buffer
signatureStream = ext.WriteNopCloser(&signatureContent)
defer func() { testSignature.Signature = signatureContent.String() }()
if armoredWriter, err = armor.Encode(signatureStream, openpgp.SignatureType, nil); err != nil {
err = fmt.Errorf("%w: %w", ErrArmoredWriterFailed, err)
}
defer armoredWriter.Close()
slog.Info("TESTSIGN KEYRING", "name", "TestUser2", "addr", testSignature.KeyRingRef.Lookup(testSignature.Resources).(*OpenPGPKeyRing))
assert.Len(t, testSignature.entityList, 1)
slog.Info("Signing Entity", "entity", testSignature.entityList[0])
assert.NotNil(t, testSignature.entityList[0].PrivateKey)
assert.Nil(t, testSignature.Sign(sourceReadStream, armoredWriter))
}
func TestCreateSignature(t *testing.T) {
ctx := context.Background()
m, keyRingUri := NewTestUserKeys("TestUser3", "TestUser3", "testuser3@rosskeen.house")
assert.NotNil(t, m)
assert.Equal(t, folio.URI("openpgp-keyring://TestUser3/TestUser3/testuser3@rosskeen.house"), keyRingUri)
assert.Nil(t, TempDir.CreateFile("test3.txt", "test data\n"))
declarationAttributes := fmt.Sprintf(`
keyringref:
uri: %s
sourceref:
uri: file://%s/test3.txt
`, string(keyRingUri), string(TempDir))
testSignature := NewOpenPGPSignature()
testSignature.Resources = m
testSignature.UseConfig(MockConfigValueGetter(func(key string) (any, error) {
switch key {
case "passphrase":
return "foo", nil
}
return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key)
}))
e := testSignature.LoadString(declarationAttributes, codec.FormatYaml)
assert.Nil(t, e)
err := testSignature.Create(ctx)
assert.Nil(t, err)
/*
assert.Greater(t, len(testSignature.entityList), 0)
assert.Contains(t, testSignature.entityList[0].Identities, "TestUser1 (TestUser1) <testuser@rosskeen.house>")
assert.Contains(t, testSignature.Signature, "-----END PGP PUBLIC KEY BLOCK-----")
assert.Contains(t, testSignature.Signature, "-----END PGP PRIVATE KEY BLOCK-----")
*/
assert.Contains(t, testSignature.entityList[0].Identities, "TestUser3 (TestUser3) <testuser3@rosskeen.house>")
assert.NotContains(t, testSignature.Signature, "-----END PGP PUBLIC KEY BLOCK-----")
assert.NotContains(t, testSignature.Signature, "-----END PGP PRIVATE KEY BLOCK-----")
assert.Contains(t, testSignature.Signature, "-----END PGP SIGNATURE-----")
}
/*
func TestReadSignature(t *testing.T) {
ctx := context.Background()
declarationAttributes := `
signature: |-
-----BEGIN PGP PUBLIC KEY BLOCK-----
assert.Nil(t, TempDir.CreateFile("read-test.txt", "test data\n"))
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
+aAQq8u60fWheEIG+xaV3T01zrNRUo6I7xu5kA==
=yFbn
-----END PGP PUBLIC KEY BLOCK-----
`
m, keyRingUri := KeyRingTestUser1()
assert.NotNil(t, m)
assert.Equal(t, folio.URI("openpgp-keyring://TestUser1/TestUser1/testuser@rosskeen.house"), keyRingUri)
declarationAttributes := fmt.Sprintf(`
keyringref:
uri: %s
sourceref:
uri: file://%s/read-test.txt
signature: |-
-----BEGIN PGP SIGNATURE-----
iQEzBAABCgAdFiEEAM6cDwBqHcVfKX+b3weSNhbTZdcFAmhQtmMACgkQ3weSNhbT
ZddNPAf/ZSKVOUXVlIBgXMsw2RqVWYkOzyuWADeOq6P1UHlRN0xxSmOFnHukxstL
zTa7KSpf5y3/DKRSTWldAy8WYFr0mx5hoDy5c83OYZo2oUtFsVYifol4JfZeF+ij
6a2trW17VjtKYzwpSTt/SAaTxKj/6oVevpWykOPfg6V3CCLfnVEuBMhotU91ngbg
wh3R3K7LrVVJ6kYI4RKUeBIx280JZMG0sZ/AuwUdWG6KFavGMbHVzHHD2PMwT53t
xJ5kzWyS9ZWws8r/B33GM9szHplXulhETsd1S3x0/R2MoVcHeAaAa6JwD0y0Kxu6
MuQM3twP32lK4UUqNx4oPHzU5gbirA==
=+dVO
-----END PGP SIGNATURE-----
` , string(keyRingUri), string(TempDir))
testSignature := NewOpenPGPSignature()
testSignature.Resources = m
testSignature.UseConfig(MockConfigValueGetter(func(key string) (any, error) {
switch key {
case "passphrase":
return "foo", nil
}
return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key)
}))
e := testSignature.LoadDecl(declarationAttributes)
assert.Nil(t, e)
y, err := testSignature.Read(ctx)
@ -136,7 +504,175 @@ func TestReadSignature(t *testing.T) {
assert.NotNil(t, y)
assert.Greater(t, len(testSignature.entityList), 0)
assert.Contains(t, testSignature.entityList[0].Identities, "TestUser (TestUser) <matthewrich.conf@gmail.com>")
assert.Contains(t, testSignature.entityList[0].Identities, "TestUser1 (TestUser1) <testuser@rosskeen.house>")
}
func TestSignatureBlock(t *testing.T) {
assert.Nil(t, TempDir.CreateFile("block-test.txt", "test data\n"))
m, keyRingUri := KeyRingTestUser1()
assert.NotNil(t, m)
assert.Equal(t, folio.URI("openpgp-keyring://TestUser1/TestUser1/testuser@rosskeen.house"), keyRingUri)
declarationAttributes := fmt.Sprintf(`
keyringref:
uri: %s
sourceref:
uri: file://%s/block-test.txt
signature: |-
-----BEGIN PGP SIGNATURE-----
iQEzBAABCgAdFiEEAM6cDwBqHcVfKX+b3weSNhbTZdcFAmhQtmMACgkQ3weSNhbT
ZddNPAf/ZSKVOUXVlIBgXMsw2RqVWYkOzyuWADeOq6P1UHlRN0xxSmOFnHukxstL
zTa7KSpf5y3/DKRSTWldAy8WYFr0mx5hoDy5c83OYZo2oUtFsVYifol4JfZeF+ij
6a2trW17VjtKYzwpSTt/SAaTxKj/6oVevpWykOPfg6V3CCLfnVEuBMhotU91ngbg
wh3R3K7LrVVJ6kYI4RKUeBIx280JZMG0sZ/AuwUdWG6KFavGMbHVzHHD2PMwT53t
xJ5kzWyS9ZWws8r/B33GM9szHplXulhETsd1S3x0/R2MoVcHeAaAa6JwD0y0Kxu6
MuQM3twP32lK4UUqNx4oPHzU5gbirA==
=+dVO
-----END PGP SIGNATURE-----
` , string(keyRingUri), string(TempDir))
testSignature := NewOpenPGPSignature()
testSignature.Resources = m
testSignature.UseConfig(MockConfigValueGetter(func(key string) (any, error) {
switch key {
case "passphrase":
return "foo", nil
}
return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key)
}))
e := testSignature.LoadDecl(declarationAttributes)
assert.Nil(t, e)
err := testSignature.DecodeSignatureBlock()
assert.Nil(t, err)
}
func TestVerifySignature(t *testing.T) {
ctx := context.Background()
assert.Nil(t, TempDir.CreateFile("verify-test.txt", "test data\n"))
m, keyRingUri := KeyRingTestUser1()
assert.NotNil(t, m)
assert.Equal(t, folio.URI("openpgp-keyring://TestUser1/TestUser1/testuser@rosskeen.house"), keyRingUri)
declarationAttributes := fmt.Sprintf(`
keyringref:
uri: %s
sourceref:
uri: file://%s/verify-test.txt
signature: |-
-----BEGIN PGP SIGNATURE-----
iQEzBAABCgAdFiEEAM6cDwBqHcVfKX+b3weSNhbTZdcFAmhQtmMACgkQ3weSNhbT
ZddNPAf/ZSKVOUXVlIBgXMsw2RqVWYkOzyuWADeOq6P1UHlRN0xxSmOFnHukxstL
zTa7KSpf5y3/DKRSTWldAy8WYFr0mx5hoDy5c83OYZo2oUtFsVYifol4JfZeF+ij
6a2trW17VjtKYzwpSTt/SAaTxKj/6oVevpWykOPfg6V3CCLfnVEuBMhotU91ngbg
wh3R3K7LrVVJ6kYI4RKUeBIx280JZMG0sZ/AuwUdWG6KFavGMbHVzHHD2PMwT53t
xJ5kzWyS9ZWws8r/B33GM9szHplXulhETsd1S3x0/R2MoVcHeAaAa6JwD0y0Kxu6
MuQM3twP32lK4UUqNx4oPHzU5gbirA==
=+dVO
-----END PGP SIGNATURE-----
` , string(keyRingUri), string(TempDir))
testSignature := NewOpenPGPSignature()
testSignature.Resources = m
testSignature.UseConfig(MockConfigValueGetter(func(key string) (any, error) {
switch key {
case "passphrase":
return "foo", nil
}
return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key)
}))
e := testSignature.LoadDecl(declarationAttributes)
assert.Nil(t, e)
y, err := testSignature.Read(ctx)
assert.Nil(t, err)
assert.NotNil(t, y)
assert.Greater(t, len(testSignature.entityList), 0)
assert.Contains(t, testSignature.entityList[0].Identities, "TestUser1 (TestUser1) <testuser@rosskeen.house>")
assert.Equal(t, "DF07923616D365D7", fmt.Sprintf("%X", testSignature.entityList[0].PrimaryKey.KeyId))
sourceReadStream, sourceReadStreamErr := testSignature.SourceRef.ContentReaderStream()
assert.Nil(t, sourceReadStreamErr)
assert.Nil(t, testSignature.DecodeSignatureBlock())
assert.Nil(t, testSignature.Verify(sourceReadStream))
sourceReadStream.Close()
assert.Nil(t, testSignature.DecodeSignatureBlock())
sourceReadStream2, sourceReadStreamErr2 := testSignature.SourceRef.ContentReaderStream()
assert.Nil(t, sourceReadStreamErr2)
assert.Nil(t, testSignature.Verify(sourceReadStream2))
}
func TestVerifyDocumentSignature(t *testing.T) {
ctx := context.Background()
assert.Nil(t, TempDir.CreateFile("verify-test.txt", "test data\n"))
m, keyRingUri := DataKeyRing("file://../../tests/data/openpgp-keyring/keyring_jx.yaml")
assert.NotNil(t, m)
assert.Equal(t, folio.URI("openpgp-keyring://TestUser1/TestUser1/testuser@rosskeen.house"), keyRingUri)
// load a document
docURI := folio.URI("file://../../examples/signed/ubuntu-dev-read.jx.yaml")
r, readerErr := docURI.ContentReaderStream()
assert.Nil(t, readerErr)
assert.NotNil(t, r)
doc := folio.DocumentRegistry.NewDocument(docURI)
assert.Nil(t, doc.LoadReader(r, codec.FormatYaml))
declarationAttributes := fmt.Sprintf(`
keyringref:
uri: %s
sourceref:
uri: %s
type: document
` , string(keyRingUri), string(docURI))
testSignature := NewOpenPGPSignature()
testSignature.Resources = m
testSignature.UseConfig(mockKeyRingPassphraseConfig)
e := testSignature.LoadDecl(declarationAttributes)
assert.Nil(t, e)
createErr := testSignature.Create(ctx)
assert.Nil(t, createErr)
assert.Greater(t, len(testSignature.entityList), 0)
assert.Contains(t, testSignature.entityList[0].Identities, "TestUser1 (TestUser1) <testuser@rosskeen.house>")
y, err := testSignature.Read(ctx) // XXX throws an error if no signature is present
assert.Nil(t, err)
assert.NotNil(t, y)
assert.Greater(t, len(testSignature.entityList), 0)
assert.Contains(t, testSignature.entityList[0].Identities, "TestUser1 (TestUser1) <testuser@rosskeen.house>")
assert.Equal(t, "FAED7F3BB05EF687", fmt.Sprintf("%X", testSignature.entityList[0].PrimaryKey.KeyId))
sourceReadStream, sourceReadStreamErr := testSignature.SourceRef.Reader()
assert.Nil(t, sourceReadStreamErr)
assert.Nil(t, testSignature.DecodeSignatureBlock())
assert.Nil(t, testSignature.Verify(sourceReadStream))
sourceReadStream.Close()
assert.Nil(t, testSignature.DecodeSignatureBlock())
sourceReadStream2, sourceReadStreamErr2 := testSignature.SourceRef.Reader()
assert.Nil(t, sourceReadStreamErr2)
assert.Nil(t, testSignature.Verify(sourceReadStream2))
}
*/

View File

@ -66,7 +66,6 @@ type Package struct {
ReadCommand *command.Command `yaml:"-" json:"-"`
UpdateCommand *command.Command `yaml:"-" json:"-"`
DeleteCommand *command.Command `yaml:"-" json:"-"`
Resources data.ResourceMapper `yaml:"-" json:"-"`
}
func init() {
@ -123,10 +122,6 @@ func (p *Package) Init(u data.URIParser) error {
return p.SetParsedURI(u)
}
func (p *Package) SetResourceMapper(resources data.ResourceMapper) {
p.Resources = resources
}
func (p *Package) Clone() data.Resource {
newp := &Package {
Common: p.Common.Clone(),

View File

@ -88,7 +88,6 @@ type PKI struct {
Bits int `json:"bits" yaml:"bits"`
EncodingType EncodingType `json:"type" yaml:"type"`
Resources data.ResourceMapper `json:"-" yaml:"-"`
}
func NewPKI() *PKI {
@ -96,11 +95,6 @@ func NewPKI() *PKI {
return p
}
func (k *PKI) SetResourceMapper(resources data.ResourceMapper) {
slog.Info("PKI.SetResourceMapper()", "resources", resources)
k.Resources = resources
}
func (k *PKI) Clone() data.Resource {
return &PKI {
Common: k.Common.Clone(),

View File

@ -12,32 +12,15 @@ _ "gopkg.in/yaml.v3"
_ "log"
_ "os"
"decl/internal/transport"
"decl/internal/ext"
"decl/internal/codec"
"decl/internal/data"
"decl/internal/folio"
"gitea.rosskeen.house/rosskeen.house/machine"
"strings"
"testing"
"path/filepath"
)
type TestResourceMapper func(key string) (data.Declaration, bool)
func (rm TestResourceMapper) Get(key string) (data.Declaration, bool) {
return rm(key)
}
func (rm TestResourceMapper) Has(key string) (ok bool) {
_, ok = rm(key)
return
}
func (rm TestResourceMapper) Set(key string, value data.Declaration) {
}
func (rm TestResourceMapper) Delete(key string) {
}
type StringContentReadWriter func() (any, error)
func (s StringContentReadWriter) ContentWriterStream() (*transport.Writer, error) {
@ -62,8 +45,28 @@ func (s StringContentReadWriter) SetParsedURI(uri data.URIParser) (error) { retu
func (s StringContentReadWriter) URI() (string) { return "" }
func (s StringContentReadWriter) Validate() (error) { return nil }
func (s StringContentReadWriter) ResourceType() data.TypeName { return "" }
func (s StringContentReadWriter) Resource() data.Resource { return nil }
func (s StringContentReadWriter) Resource() data.Resource { return stringContentReadWriterResource { StringContentReadWriter: s } }
type stringContentReadWriterResource struct {
StringContentReadWriter
}
//URI() string { return "" }
//SetParsedURI(URIParser) error { return nil }
func (s stringContentReadWriterResource) Type() string { return "buffer" }
func (s stringContentReadWriterResource) StateMachine() machine.Stater { return nil }
func (s stringContentReadWriterResource) UseConfig(config data.ConfigurationValueGetter) { return }
//ResolveId(context.Context) string { return }
//LoadString(string, codec.Format) (error) { return }
//Load([]byte, codec.Format) (error) { return }
//LoadReader(io.ReadCloser, codec.Format) (error) { return }
func (s stringContentReadWriterResource) Apply() error { return nil }
func (s stringContentReadWriterResource) Create(context.Context) error { return nil }
func (s stringContentReadWriterResource) Read(context.Context) ([]byte, error) { return nil, nil }
func (s stringContentReadWriterResource) Update(context.Context) error { return nil }
func (s stringContentReadWriterResource) Delete(context.Context) error { return nil }
//Validate() error { return }
func (s stringContentReadWriterResource) Clone() data.Resource { return nil }
func (s stringContentReadWriterResource) SetResourceMapper(folio.ResourceMapper) { return }
func TestNewPKIKeysResource(t *testing.T) {
r := NewPKI()
@ -90,29 +93,19 @@ func TestPKIEncodeKeys(t *testing.T) {
r.PublicKey()
assert.NotNil(t, r.publicKey)
r.Resources = TestResourceMapper(func(key string) (data.Declaration, 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.Resources = folio.NewResourceMapper()
privateKeyBuffer := folio.NewDeclaration()
privateKeyBuffer.Attributes = NewMockBufferResource("privatekey", &privateTarget)
r.Resources.Set("buffer://privatekey", privateKeyBuffer)
publicKeyBuffer := folio.NewDeclaration()
publicKeyBuffer.Attributes = NewMockBufferResource("publickey", &publicTarget)
r.Resources.Set("buffer://publickey", publicKeyBuffer)
certKeyBuffer := folio.NewDeclaration()
certKeyBuffer.Attributes = NewMockBufferResource("certificate", &certTarget)
r.Resources.Set("buffer://certificate", certKeyBuffer)
r.PrivateKeyRef = folio.ResourceReference("buffer://privatekey")
r.PublicKeyRef = folio.ResourceReference("buffer://publickey")

View File

@ -3,6 +3,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "certificate",
"type": "object",
"required": [],
"properties": {
"publickey": {
"type": "string"

View File

@ -0,0 +1,9 @@
{
"$id": "constraints.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "constraints",
"type": "object",
"additionalProperties": {
"type": ["string", "number", "object", "array", "boolean", "null"]
}
}

View File

@ -0,0 +1,9 @@
{
"$id": "dependencies.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "dependencies",
"type": "object",
"additionalProperties": {
"$ref": "constraints.schema.json"
}
}

View File

@ -3,9 +3,26 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "document",
"type": "object",
"required": [
],
"required": [],
"properties": {
"source": {
"type": "string",
"description": "Document URI"
},
"format": {
"$ref": "codec.schema.json"
},
"requires": {
"$ref": "dependencies.schema.json"
},
"imports": {
"type": "array",
"description": "List of other documents to import",
"items": {
"type": "string",
"description": "Document URI"
}
},
"configurations": {
"type": "array",
"description": "Configurations list",
@ -31,6 +48,7 @@
{ "$ref": "iptable-declaration.schema.json" },
{ "$ref": "network-route-declaration.schema.json" },
{ "$ref": "openpgp-keyring-declaration.schema.json" },
{ "$ref": "openpgp-signature-declaration.schema.json" },
{ "$ref": "package-declaration.schema.json" },
{ "$ref": "pki-declaration.schema.json" },
{ "$ref": "user-declaration.schema.json" }

View File

@ -10,10 +10,22 @@
"description": "Resource type name.",
"enum": [ "file" ]
},
"transition": {
"$ref": "storagetransition.schema.json"
},
"config": {
"type": "string",
"description": "Config name"
},
"onerror": {
"type": "string",
"description": "error handling strategy",
"enum": [
"stop",
"fail",
"skip"
]
},
"attributes": {
"$ref": "file.schema.json"
}

View File

@ -8,7 +8,7 @@
"type": {
"type": "string",
"description": "Resource type name.",
"enum": [ "file" ]
"enum": [ "openpgp-keyring" ]
},
"config": {
"type": "string",

View File

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

View File

@ -0,0 +1,23 @@
{
"$id": "openpgp-signature.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "openpgp-signature",
"type": "object",
"required": [ ],
"properties": {
"signature": {
"type": "string",
"description": "ASCII-armored PGP signature"
},
"sourceref": {
"$ref": "ref.schema.json"
},
"keyringref": {
"$ref": "ref.schema.json"
},
"signatureref": {
"$ref": "ref.schema.json",
"description": "file content signature uri"
}
}
}

View File

@ -0,0 +1,17 @@
{
"$id": "ref.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ref",
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Type of object referred to",
"enum": [ "resource", "document" ]
},
"uri": {
"type": "string",
"description": "URI of the object referred to"
}
}
}

View File

@ -60,7 +60,6 @@ type User struct {
ReadCommand *command.Command `json:"-" yaml:"-"`
UpdateCommand *command.Command `json:"-" yaml:"-"`
DeleteCommand *command.Command `json:"-" yaml:"-"`
Resources data.ResourceMapper `json:"-" yaml:"-"`
}
func NewUser() *User {
@ -107,10 +106,6 @@ func (u *User) NormalizePath() error {
return nil
}
func (u *User) SetResourceMapper(resources data.ResourceMapper) {
u.Resources = resources
}
func (u *User) Clone() data.Resource {
newu := &User {
Common: u.Common,

View File

@ -20,10 +20,10 @@ type Schema struct {
schema gojsonschema.JSONLoader
}
func New(name string, fs fs.FS) *Schema {
func New(name string, schemaFS fs.FS) *Schema {
path := fmt.Sprintf("file://schemas/%s.schema.json", name)
return &Schema{schema: gojsonschema.NewReferenceLoaderFileSystem(path, http.FS(fs))}
return &Schema{schema: gojsonschema.NewReferenceLoaderFileSystem(path, http.FS(schemaFS))}
//return &Schema{schema: gojsonschema.NewReferenceLoader(path)}
}
@ -36,10 +36,10 @@ func (s *Schema) Validate(source string) error {
slog.Info("schema error", "source", source, "schema", s.schema, "result", result, "err", err)
return err
}
slog.Info("schema.Validate()", "schema", s.schema, "result", result, "err", err)
if !result.Valid() {
schemaErrors := strings.Builder{}
schemaErrors.WriteString(fmt.Sprintf("source: %s schema: %#v ", source, s.schema))
for _, err := range result.Errors() {
schemaErrors.WriteString(err.String() + "\n")
}
@ -56,3 +56,11 @@ func (s *Schema) ValidateSchema() error {
slog.Info("validate schema definition", "schemaloader", sl, "err", schemaErr)
return schemaErr
}
func (s *Schema) Debug(schemaFS fs.FS) {
if files, err := fs.ReadDir(schemaFS, "schemas"); err == nil {
for _, f := range files {
slog.Info("Schema.Debug()", "file", f.Name())
}
}
}