add ref attribute type

This commit is contained in:
Matthew Rich 2025-08-25 03:51:34 +00:00
parent fb544a455c
commit 2d4234c6a0
21 changed files with 1497 additions and 206 deletions

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

@ -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))
}

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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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")