diff --git a/internal/data/originator.go b/internal/data/originator.go new file mode 100644 index 0000000..8065ef7 --- /dev/null +++ b/internal/data/originator.go @@ -0,0 +1,13 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package data + +import ( +) + +var ( +) + +type Originator interface { + GetContentReadWriter() ContentReadWriter +} diff --git a/internal/data/resource.go b/internal/data/resource.go index 6510bb1..dd77a5b 100644 --- a/internal/data/resource.go +++ b/internal/data/resource.go @@ -38,7 +38,6 @@ type Resource interface { Crudder Validator Clone() Resource - SetResourceMapper(ResourceMapper) } type Declaration interface { diff --git a/internal/folio/declaration.go b/internal/folio/declaration.go index 9ac817a..e7e2bb9 100644 --- a/internal/folio/declaration.go +++ b/internal/folio/declaration.go @@ -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() diff --git a/internal/folio/document.go b/internal/folio/document.go index 8cf8bc8..90ca354 100644 --- a/internal/folio/document.go +++ b/internal/folio/document.go @@ -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) +} + diff --git a/internal/folio/mock_foo_resource_test.go b/internal/folio/mock_foo_resource_test.go index 0018ff6..f093e5b 100644 --- a/internal/folio/mock_foo_resource_test.go +++ b/internal/folio/mock_foo_resource_test.go @@ -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 { diff --git a/internal/folio/mock_resource_test.go b/internal/folio/mock_resource_test.go index a56ae64..45c9a7d 100644 --- a/internal/folio/mock_resource_test.go +++ b/internal/folio/mock_resource_test.go @@ -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 diff --git a/internal/folio/ref.go b/internal/folio/ref.go new file mode 100644 index 0000000..a1858f1 --- /dev/null +++ b/internal/folio/ref.go @@ -0,0 +1,229 @@ +// Copyright 2024 Matthew Rich . 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) +} diff --git a/internal/folio/ref_test.go b/internal/folio/ref_test.go new file mode 100644 index 0000000..01805cb --- /dev/null +++ b/internal/folio/ref_test.go @@ -0,0 +1,67 @@ +// Copyright 2024 Matthew Rich . 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)) +} diff --git a/internal/folio/registry.go b/internal/folio/registry.go index 500a4a2..5407d29 100644 --- a/internal/folio/registry.go +++ b/internal/folio/registry.go @@ -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 } diff --git a/internal/folio/resourcereference.go b/internal/folio/resourcereference.go index 89fdd0d..3309d1f 100644 --- a/internal/folio/resourcereference.go +++ b/internal/folio/resourcereference.go @@ -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) +} diff --git a/internal/folio/resourcereference_test.go b/internal/folio/resourcereference_test.go index 012b0d3..78abd8a 100644 --- a/internal/folio/resourcereference_test.go +++ b/internal/folio/resourcereference_test.go @@ -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() diff --git a/internal/folio/schemas/foo.schema.json b/internal/folio/schemas/foo.schema.json index f91eee0..bd3a15b 100644 --- a/internal/folio/schemas/foo.schema.json +++ b/internal/folio/schemas/foo.schema.json @@ -7,8 +7,8 @@ "properties": { "name": { "type": "string", - "description": "foo name", - "minLength": 1 + "description": "foo name", + "minLength": 1 }, "size": { "type": "integer", diff --git a/internal/folio/schemas/ref.schema.json b/internal/folio/schemas/ref.schema.json new file mode 100644 index 0000000..94b2d26 --- /dev/null +++ b/internal/folio/schemas/ref.schema.json @@ -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" + } + } +} diff --git a/internal/resource/container_image_test.go b/internal/resource/container_image_test.go index 6d7e5ef..7806b27 100644 --- a/internal/resource/container_image_test.go +++ b/internal/resource/container_image_test.go @@ -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) diff --git a/internal/resource/container_test.go b/internal/resource/container_test.go index c907f47..fb2bf73 100644 --- a/internal/resource/container_test.go +++ b/internal/resource/container_test.go @@ -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()) +} diff --git a/internal/resource/exec_test.go b/internal/resource/exec_test.go index 720d3ae..125248b 100644 --- a/internal/resource/exec_test.go +++ b/internal/resource/exec_test.go @@ -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) { diff --git a/internal/resource/mock_buffer_resource_test.go b/internal/resource/mock_buffer_resource_test.go new file mode 100644 index 0000000..42c0b17 --- /dev/null +++ b/internal/resource/mock_buffer_resource_test.go @@ -0,0 +1,34 @@ +// Copyright 2024 Matthew Rich . 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 + }, + } +} diff --git a/internal/resource/mock_resource_test.go b/internal/resource/mock_resource_test.go index 6fad0a6..a1c89d3 100644 --- a/internal/resource/mock_resource_test.go +++ b/internal/resource/mock_resource_test.go @@ -9,25 +9,32 @@ _ "gopkg.in/yaml.v3" _ "fmt" "gitea.rosskeen.house/rosskeen.house/machine" "decl/internal/data" + "decl/internal/codec" + "decl/internal/transport" + "io" ) type MockResource struct { - InjectURI func() string - InjectType func() string - InjectResolveId func(ctx context.Context) string - InjectLoadDecl func(string) error - InjectValidate func() error - InjectApply func() error - InjectRead func(context.Context) ([]byte, error) - InjectStateMachine func() machine.Stater + *Common `json:",inline" yaml:",inline"` + InjectURI func() string + InjectType func() string + InjectResolveId func(ctx context.Context) string + InjectLoadDecl func(string) error + InjectValidate func() error + 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 { - return nil + return nil } func (m *MockResource) StateMachine() machine.Stater { - return nil + return nil } func (m *MockResource) SetURI(uri string) error { @@ -35,15 +42,15 @@ func (m *MockResource) SetURI(uri string) error { } func (m *MockResource) URI() string { - return m.InjectURI() + return m.InjectURI() } func (m *MockResource) ResolveId(ctx context.Context) string { - return m.InjectResolveId(ctx) + return m.InjectResolveId(ctx) } func (m *MockResource) LoadDecl(yamlResourceDeclaration string) error { - return m.InjectLoadDecl(yamlResourceDeclaration) + return m.InjectLoadDecl(yamlResourceDeclaration) } func (m *MockResource) Validate() error { @@ -51,20 +58,81 @@ func (m *MockResource) Validate() error { } func (m *MockResource) Apply() error { - return m.InjectApply() + return m.InjectApply() } func (m *MockResource) Read(ctx context.Context) ([]byte, error) { - return m.InjectRead(ctx) + return m.InjectRead(ctx) } func (m *MockResource) Type() string { - return m.InjectType() + return m.InjectType() } func (m *MockResource) UnmarshalJSON(data []byte) error { - if err := json.Unmarshal(data, m); err != nil { - return err - } - return nil + if err := json.Unmarshal(data, m); err != nil { + return err + } + 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 } diff --git a/internal/resource/openpgp_signature.go b/internal/resource/openpgp_signature.go index 8a3978f..85fbdf8 100644 --- a/internal/resource/openpgp_signature.go +++ b/internal/resource/openpgp_signature.go @@ -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"` - 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"` + *Common `json:"-,inline" yaml:"-,inline"` + stater machine.Stater `json:"-" yaml:"-"` + Signature string `json:"signature,omitempty" yaml:"signature,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,41 +96,94 @@ 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 o.KeyRingRef.IsEmpty() { - var keyringConfig any - if keyringConfig, err = o.config.GetValue("keyring"); err == nil { - o.entityList = keyringConfig.(openpgp.EntityList) + 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 { + o.entityList = keyringConfig.(openpgp.EntityList) + } + } 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 + } } - } else { - ringFileStream, _ := o.KeyRingRef.Lookup(o.Resources).ContentReaderStream() - defer ringFileStream.Close() - o.entityList, err = openpgp.ReadArmoredKeyRing(ringFileStream) } 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 - sourceReadStream, err = o.SourceRef.Lookup(o.Resources).ContentReaderStream() + + 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) } diff --git a/internal/resource/openpgp_signature_test.go b/internal/resource/openpgp_signature_test.go index 0594952..e7b901a 100644 --- a/internal/resource/openpgp_signature_test.go +++ b/internal/resource/openpgp_signature_test.go @@ -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) ") - 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) ") + 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 := ` + assert.Nil(t, TempDir.CreateFile("read-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/read-test.txt signature: |- - -----BEGIN PGP PUBLIC KEY BLOCK----- + -----BEGIN PGP SIGNATURE----- - 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 - +aAQq8u60fWheEIG+xaV3T01zrNRUo6I7xu5kA== - =yFbn - -----END PGP PUBLIC KEY BLOCK----- -` + 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) ") + assert.Contains(t, testSignature.entityList[0].Identities, "TestUser1 (TestUser1) ") +} + +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) ") + 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) ") + + 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) ") + 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)) } -*/ + diff --git a/internal/resource/pki_test.go b/internal/resource/pki_test.go index 946b35e..6998fe6 100644 --- a/internal/resource/pki_test.go +++ b/internal/resource/pki_test.go @@ -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")