From 8feb7b8d56974ecadce2004ee29835ae665892de Mon Sep 17 00:00:00 2001 From: Matthew Rich Date: Wed, 16 Oct 2024 10:26:42 -0700 Subject: [PATCH] add support of import search paths [doublejynx/jx#7] --- cmd/cli/main.go | 6 +- internal/builtin/documents/config.jx.yaml | 9 + internal/builtin/documents/install.jx.yaml | 16 ++ internal/client/client.go | 16 +- internal/config/certificate.go | 2 +- internal/config/exec.go | 2 +- internal/config/generic.go | 2 +- internal/config/system.go | 5 +- internal/data/document.go | 3 +- internal/data/identifier.go | 22 +- internal/data/resource.go | 5 + internal/data/types.go | 1 + internal/ds/orderedset.go | 63 ++++++ internal/ds/orderedset_test.go | 51 +++++ internal/ds/set.go | 34 +++ internal/ds/set_test.go | 24 +++ internal/ext/file.go | 26 +++ internal/fan/dir.go | 12 +- internal/fan/jx.go | 2 +- internal/folio/block.go | 30 ++- internal/folio/declaration.go | 92 +++++--- internal/folio/declaration_test.go | 1 + internal/folio/document.go | 39 +++- internal/folio/document_test.go | 3 + internal/folio/mock_configuration_test.go | 7 +- internal/folio/mock_foo_resource_test.go | 39 ++-- internal/folio/mock_resource_test.go | 13 +- internal/folio/parseduri.go | 102 +++++++++ internal/folio/registry.go | 7 +- internal/folio/resource.go | 4 +- internal/folio/resourcereference.go | 2 +- internal/folio/searchpath.go | 65 ++++++ internal/folio/searchpath_test.go | 24 +++ internal/folio/uri.go | 14 +- internal/folio/uri_test.go | 8 +- internal/resource/common.go | 61 +++--- internal/resource/common_test.go | 6 +- internal/resource/container.go | 47 ++-- internal/resource/container_image.go | 48 ++--- internal/resource/container_network.go | 62 +++--- internal/resource/exec.go | 28 ++- internal/resource/exec_test.go | 26 +-- internal/resource/file.go | 238 +++++++++------------ internal/resource/file_test.go | 95 ++++++-- internal/resource/group.go | 54 +++-- internal/resource/http.go | 42 ++-- internal/resource/iptables.go | 54 +++-- internal/resource/network_route.go | 97 +++------ internal/resource/package.go | 132 +++++++----- internal/resource/package_test.go | 16 +- internal/resource/pki.go | 88 +------- internal/resource/pki_test.go | 7 +- internal/resource/resource.go | 108 +--------- internal/resource/schema_test.go | 8 +- internal/resource/service.go | 28 ++- internal/resource/service_test.go | 7 +- internal/resource/user.go | 42 ++-- internal/types/types.go | 13 ++ internal/types/types_test.go | 14 ++ 59 files changed, 1255 insertions(+), 817 deletions(-) create mode 100644 internal/builtin/documents/install.jx.yaml create mode 100644 internal/ds/orderedset.go create mode 100644 internal/ds/orderedset_test.go create mode 100644 internal/ds/set.go create mode 100644 internal/ds/set_test.go create mode 100644 internal/ext/file.go create mode 100644 internal/folio/parseduri.go create mode 100644 internal/folio/searchpath.go create mode 100644 internal/folio/searchpath_test.go diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 33b2c6e..ad2a6bf 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -10,10 +10,10 @@ _ "decl/internal/config" _ "decl/internal/resource" _ "decl/internal/fan" "decl/internal/builtin" - _ "errors" +_ "errors" "flag" "fmt" - _ "gopkg.in/yaml.v3" +_ "gopkg.in/yaml.v3" "io" "log/slog" "os" @@ -159,7 +159,7 @@ func main() { DefaultConfigurations, configErr := builtin.BuiltInDocuments() if configErr != nil { - slog.Error("Failed loading default configuration", "error", configErr) + slog.Warn("Failed loading default configuration", "error", configErr) } ConfigDoc.AppendConfigurations(DefaultConfigurations) diff --git a/internal/builtin/documents/config.jx.yaml b/internal/builtin/documents/config.jx.yaml index 3d6853d..0a79629 100644 --- a/internal/builtin/documents/config.jx.yaml +++ b/internal/builtin/documents/config.jx.yaml @@ -25,6 +25,15 @@ resources: group: "jx" mode: "0770" filetype: directory +- type: file + transition: update + config: confdir + attributes: + path: "lib" + owner: "root" + group: "jx" + mode: "0770" + filetype: directory - type: file transition: update config: confdir diff --git a/internal/builtin/documents/install.jx.yaml b/internal/builtin/documents/install.jx.yaml new file mode 100644 index 0000000..e0eefcf --- /dev/null +++ b/internal/builtin/documents/install.jx.yaml @@ -0,0 +1,16 @@ +imports: +- file://documents/config.jx.yaml +configurations: +- name: bindir + values: + prefix: /usr/local/bin +resources: +- type: file + transition: update + config: bindir + attributes: + path: "jx" + owner: "root" + group: "root" + mode: "0755" + sourceref: file://jx diff --git a/internal/client/client.go b/internal/client/client.go index c94a933..a8b8a5c 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -126,7 +126,7 @@ func (a *App) ImportResource(ctx context.Context, uri string) (err error) { a.Documents = append(a.Documents, folio.DocumentRegistry.NewDocument("")) } resourceURI := folio.URI(uri) - u := resourceURI.Parse() + u := resourceURI.Parse().URL() if u == nil { return fmt.Errorf("Failed adding resource: %s", uri) } @@ -147,9 +147,19 @@ func (a *App) ImportResource(ctx context.Context, uri string) (err error) { } func (a *App) ImportSource(uri string) (loadedDocuments []data.Document, err error) { - if loadedDocuments, err = folio.DocumentRegistry.Load(folio.URI(uri)); err == nil && loadedDocuments != nil { - a.Documents = append(a.Documents, loadedDocuments...) + if source := folio.URI(uri).Parse().URL(); source != nil { + if source.Scheme == "" { + source.Scheme = "file" + } + + slog.Info("Client.ImportSource()", "uri", uri, "source", source, "error", err) + if loadedDocuments, err = folio.DocumentRegistry.LoadFromParsedURI(source); err == nil && loadedDocuments != nil { + a.Documents = append(a.Documents, loadedDocuments...) + } + } else { + err = folio.ErrInvalidURI } + slog.Info("Client.ImportSource()", "uri", uri, "error", err) return } diff --git a/internal/config/certificate.go b/internal/config/certificate.go index 53d418f..de6a895 100644 --- a/internal/config/certificate.go +++ b/internal/config/certificate.go @@ -37,7 +37,7 @@ func (c *Certificate) SetURI(uri string) error { return nil } -func (c *Certificate) SetParsedURI(uri *url.URL) error { +func (c *Certificate) SetParsedURI(uri data.URIParser) error { return nil } diff --git a/internal/config/exec.go b/internal/config/exec.go index 396b732..cd46cee 100644 --- a/internal/config/exec.go +++ b/internal/config/exec.go @@ -39,7 +39,7 @@ func (x *Exec) SetURI(uri string) error { return nil } -func (x *Exec) SetParsedURI(uri *url.URL) error { +func (x *Exec) SetParsedURI(uri data.URIParser) error { return nil } diff --git a/internal/config/generic.go b/internal/config/generic.go index 7a27fa1..005f0d2 100644 --- a/internal/config/generic.go +++ b/internal/config/generic.go @@ -35,7 +35,7 @@ func (g *Generic[Value]) SetURI(uri string) error { return nil } -func (g *Generic[Value]) SetParsedURI(uri *url.URL) error { +func (g *Generic[Value]) SetParsedURI(uri data.URIParser) error { return nil } diff --git a/internal/config/system.go b/internal/config/system.go index 885c08b..be193b0 100644 --- a/internal/config/system.go +++ b/internal/config/system.go @@ -37,6 +37,9 @@ func NewSystem() *System { s[k] = v } s.CurrentUser() + s["importpath"] = []string { + "/etc/jx/lib", + } return &s } @@ -59,7 +62,7 @@ func (s *System) SetURI(uri string) error { return nil } -func (s *System) SetParsedURI(uri *url.URL) error { +func (s *System) SetParsedURI(uri data.URIParser) error { return nil } diff --git a/internal/data/document.go b/internal/data/document.go index 8615c7b..5290eb8 100644 --- a/internal/data/document.go +++ b/internal/data/document.go @@ -8,7 +8,6 @@ import ( "decl/internal/codec" "io" "decl/internal/mapper" - "net/url" ) var ( @@ -44,7 +43,7 @@ type Document interface { mapper.Mapper NewResource(uri string) (Resource, error) - NewResourceFromParsedURI(uri *url.URL) (Resource, error) + NewResourceFromParsedURI(uri URIParser) (Resource, error) AddDeclaration(Declaration) AddResourceDeclaration(resourceType string, resourceDeclaration Resource) diff --git a/internal/data/identifier.go b/internal/data/identifier.go index 7cf80e5..14fe418 100644 --- a/internal/data/identifier.go +++ b/internal/data/identifier.go @@ -5,16 +5,34 @@ package data import ( "errors" "net/url" + "decl/internal/transport" ) var ( ErrInvalidURI error = errors.New("Invalid URI") ) +type URIParser interface { + URL() *url.URL + NewResource(document Document) (newResource Resource, err error) + ConstructResource(res Resource) (err error) + Converter() (converter Converter, err error) + Exists() bool + + ContentReaderStream() (*transport.Reader, error) + ContentWriterStream() (*transport.Writer, error) + + String() string + SetURL(url *url.URL) + Extension() (string, string) + + ContentType() string + IsEmpty() bool +} + type Identifier interface { URI() string - SetURI(string) error - SetParsedURI(*url.URL) error + SetParsedURI(URIParser) error } type DocumentElement interface { diff --git a/internal/data/resource.go b/internal/data/resource.go index 1baa400..d98b909 100644 --- a/internal/data/resource.go +++ b/internal/data/resource.go @@ -22,6 +22,11 @@ type StateTransformer interface { Apply() error } +// Used by the resource factory to initialize new resources. +type ResourceInitializer interface { + Init(uri URIParser) error +} + type Resource interface { Identifier Type() string diff --git a/internal/data/types.go b/internal/data/types.go index e4af876..aa99c22 100644 --- a/internal/data/types.go +++ b/internal/data/types.go @@ -11,6 +11,7 @@ type Factory[Product comparable] func(*url.URL) Product type TypesRegistry[Product comparable] interface { New(uri string) (result Product, err error) NewFromParsedURI(uri *url.URL) (result Product, err error) + NewFromType(typename string) (result Product, err error) Has(typename string) bool //Get(string) Factory[Product] } diff --git a/internal/ds/orderedset.go b/internal/ds/orderedset.go new file mode 100644 index 0000000..b0b4351 --- /dev/null +++ b/internal/ds/orderedset.go @@ -0,0 +1,63 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package ds + +import ( + "log/slog" +) + +type OrderedSet[Value comparable] struct { + Values []*Value + elements map[Value]int +} + +func NewOrderedSet[Value comparable]() *OrderedSet[Value] { + return &OrderedSet[Value]{ elements: make(map[Value]int), Values: make([]*Value, 0, 10) } +} + +func (s *OrderedSet[Value]) Add(value Value) { + slog.Info("OrderedSet.Add", "key", value, "s", s) + s.Values = append(s.Values, &value) + s.elements[value] = len(s.Values) + slog.Info("OrderedSet.Add", "key", value, "s", s, "v", &s.Values) +} + +func (s *OrderedSet[Value]) Delete(key Value) { + slog.Info("OrderedSet.Delete", "key", key, "s", s, "size", len(s.Values)) + if i, ok := s.elements[key]; ok { + i-- + s.Values[i] = nil + delete(s.elements, key) + } +} + +func (s *OrderedSet[Value]) Contains(value Value) (result bool) { + slog.Info("OrderedSet.Contains", "key", value, "s", s, "size", len(s.Values), "v", &s.Values) + _, result = s.elements[value] + return +} + +func (s *OrderedSet[Value]) Len() int { + return len(s.elements) +} + +func (s *OrderedSet[Value]) AddItems(value []Value) { + for _, v := range value { + s.Add(v) + } +} + +func (s *OrderedSet[Value]) Items() []*Value { + slog.Info("OrderedSet.Items - start", "s", s) + result := make([]*Value, 0, len(s.elements) - 1) + for _, v := range s.Values { + slog.Info("OrderedSet.Items", "value", v) + if v != nil { + result = append(result, v) + s.elements[*v] = len(result) + } + } + slog.Info("OrderedSet.Items", "s", s, "result", result) + s.Values = result + return result +} diff --git a/internal/ds/orderedset_test.go b/internal/ds/orderedset_test.go new file mode 100644 index 0000000..4063b43 --- /dev/null +++ b/internal/ds/orderedset_test.go @@ -0,0 +1,51 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package ds + +import ( + "github.com/stretchr/testify/assert" + "testing" + "log/slog" +) + +var ( +) + +func TestNewOrderedSet(t *testing.T) { + s := NewOrderedSet[string]() + assert.NotNil(t, s) + + testValues := []string{ + "foo", + "bar", + "baz", + "quuz", + } + + for _,value := range testValues { + + s.Add(value) + + + slog.Info("TestNewOrderedSet - ADD", "item", value, "s", s) + + assert.True(t, s.Contains(value)) + slog.Info("TestNewOrderedSet - CONTAINS", "s", s) + + for x, item := range s.Items() { + slog.Info("TestNewOrderedSet", "item", item, "s", s) + assert.Equal(t, testValues[x], *item) + } + + } + s.Delete("bar") + + expectedValues := []string { + "foo", + "baz", + "quuz", + } + for x, item := range s.Items() { + assert.Equal(t, expectedValues[x], *item) + } +} diff --git a/internal/ds/set.go b/internal/ds/set.go new file mode 100644 index 0000000..f8599b7 --- /dev/null +++ b/internal/ds/set.go @@ -0,0 +1,34 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package ds + +import ( +) + +type Set[Value comparable] map[Value]bool + +func NewSet[Value comparable]() Set[Value] { + return make(map[Value]bool) +} + +func (s Set[Value]) Add(value Value) { + s[value] = true +} + +func (s Set[Value]) Delete(value Value) { + delete(s, value) +} + +func (s Set[Value]) Contains(value Value) bool { + return s[value] +} + +func (s Set[Value]) Len() int { + return len(s) +} + +func (s Set[Value]) AddSlice(value []Value) { + for _, v := range value { + s.Add(v) + } +} diff --git a/internal/ds/set_test.go b/internal/ds/set_test.go new file mode 100644 index 0000000..bbb2f16 --- /dev/null +++ b/internal/ds/set_test.go @@ -0,0 +1,24 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package ds + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +var ( +) + +func TestNewSet(t *testing.T) { + s := NewSet[string]() + assert.NotNil(t, s) + s["foo"] = true + assert.True(t, s.Contains("foo")) + + s.Add("bar") + + assert.True(t, s.Contains("bar")) + + +} diff --git a/internal/ext/file.go b/internal/ext/file.go new file mode 100644 index 0000000..67af4cc --- /dev/null +++ b/internal/ext/file.go @@ -0,0 +1,26 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package ext + +import ( + "os" + "path/filepath" +) + +type FilePath string + +func (f *FilePath) Exists() bool { + _, err := os.Stat(string(*f)) + return !os.IsNotExist(err) +} + +func (f *FilePath) Add(relative string) { + newPath := filepath.Join(string(*f), relative) + *f = FilePath(newPath) +} + + +func (f FilePath) Abs() FilePath { + result, _ := filepath.Abs(string(f)) + return FilePath(result) +} diff --git a/internal/fan/dir.go b/internal/fan/dir.go index bac7d25..f6811ed 100644 --- a/internal/fan/dir.go +++ b/internal/fan/dir.go @@ -105,12 +105,6 @@ func (d *Dir) Emit(document data.Document, filter data.ElementSelector) (resourc return nil, ErrEmptyDocument } - dirFileDeclaration := folio.NewDeclaration() - dirFileDeclaration.Type = "file" - if err = dirFileDeclaration.NewResource(nil); err != nil { - return - } - parentPaths := make(map[string]int) var containingDirectoryPath string for _,res := range document.Filter(func(d data.Declaration) bool { @@ -130,8 +124,12 @@ func (d *Dir) Emit(document data.Document, filter data.ElementSelector) (resourc containingDirectoryPath, _ = d.isParent(&parentPaths, parent, containingDirectoryPath) } + uri := fmt.Sprintf("file://%s", containingDirectoryPath) - if err = dirFileDeclaration.SetURI(uri); err != nil { + + dirFileDeclaration := folio.NewDeclaration() + dirFileDeclaration.Type = "file" + if err = dirFileDeclaration.NewResource(&uri); err != nil { return } diff --git a/internal/fan/jx.go b/internal/fan/jx.go index 06f3a96..43a26c5 100644 --- a/internal/fan/jx.go +++ b/internal/fan/jx.go @@ -189,7 +189,7 @@ func (j *JxFile) ExtractMany(resourceSource data.Resource, filter data.ElementSe func (j *JxFile) targetResource() (target data.Resource, err error) { if j.emitResource == nil { - targetUrl := j.Uri.Parse() + targetUrl := j.Uri.Parse().URL() targetUrl.Scheme = "file" q := targetUrl.Query() q.Set("format", string(j.Format)) diff --git a/internal/folio/block.go b/internal/folio/block.go index 6adce36..87638a0 100644 --- a/internal/folio/block.go +++ b/internal/folio/block.go @@ -15,6 +15,32 @@ import ( "decl/internal/schema" ) +type ConfigKey string + +// Lookup a config value using block.key identifier (E.g. system.GOOS) +func (c ConfigKey) GetValue() (value any, err error) { + fields := strings.SplitN(string(c), ".", 2) + if configBlock, ok := DocumentRegistry.ConfigNameMap.Get(fields[0]); ok && len(fields) > 1 { + return configBlock.GetValue(fields[1]) + } else { + return nil, fmt.Errorf("%w - %s", data.ErrUnknownConfigurationType, c) + } +} + +func (c ConfigKey) Get() any { + if v, err := c.GetValue(); err == nil { + return v + } + return nil +} + +func (c ConfigKey) GetStringSlice() []string { + if v, err := c.GetValue(); err == nil { + return v.([]string) + } + return nil +} + type BlockType struct { Name string `json:"name" yaml:"name"` Type TypeName `json:"type" yaml:"type"` @@ -135,9 +161,9 @@ func (b *Block) SetURI(uri string) (err error) { return } -func (b *Block) SetParsedURI(uri *url.URL) (err error) { +func (b *Block) SetParsedURI(uri data.URIParser) (err error) { if b.Values == nil { - if err = b.NewConfigurationFromParsedURI(uri); err != nil { + if err = b.NewConfigurationFromParsedURI(uri.URL()); err != nil { return } } diff --git a/internal/folio/declaration.go b/internal/folio/declaration.go index 99609d9..926e546 100644 --- a/internal/folio/declaration.go +++ b/internal/folio/declaration.go @@ -11,12 +11,10 @@ _ "errors" "gopkg.in/yaml.v3" "log/slog" _ "gitea.rosskeen.house/rosskeen.house/machine" -//_ "gitea.rosskeen.house/pylon/luaruntime" + "gitea.rosskeen.house/pylon/luaruntime" "decl/internal/codec" "decl/internal/data" "decl/internal/schema" - "net/url" - "runtime/debug" "errors" ) @@ -33,6 +31,7 @@ type DeclarationType struct { OnError OnError `json:"onerror,omitempty" yaml:"onerror,omitempty"` Error string `json:"error,omitempty" yaml:"error,omitempty"` Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"` + On *Events `json:"on,omitempty" yaml:"on,omitempty"` } type Declaration struct { @@ -43,14 +42,15 @@ type Declaration struct { OnError OnError `json:"onerror,omitempty" yaml:"onerror,omitempty"` Error string `json:"error,omitempty" yaml:"error,omitempty"` Requires Dependencies `json:"requires,omitempty" yaml:"requires,omitempty"` -// runtime luaruntime.LuaRunner + On *Events `json:"on,omitempty" yaml:"on,omitempty"` + runtime luaruntime.LuaRunner document *Document configBlock data.Block ResourceTypes data.TypesRegistry[data.Resource] `json:"-" yaml:"-"` } func NewDeclaration() *Declaration { - return &Declaration{ ResourceTypes: DocumentRegistry.ResourceTypes } + return &Declaration{ ResourceTypes: DocumentRegistry.ResourceTypes, runtime: luaruntime.New() } } func NewDeclarationFromDocument(document *Document) *Declaration { @@ -98,6 +98,7 @@ func (d *Declaration) Clone() data.Declaration { //runtime: luaruntime.New(), Config: d.Config, Requires: d.Requires, + On: d.On, } } @@ -137,11 +138,12 @@ func (d *Declaration) Validate() (err error) { return err } -func (d *Declaration) NewResourceFromParsedURI(u *url.URL) (err error) { +func (d *Declaration) NewResourceFromParsedURI(u data.URIParser) (err error) { if u == nil { - d.Attributes, err = d.ResourceTypes.NewFromParsedURI(&url.URL{ Scheme: string(d.Type) }) + d.Attributes, err = d.ResourceTypes.NewFromType(string(d.Type)) } else { - if d.Attributes, err = d.ResourceTypes.NewFromParsedURI(u); err == nil { + parsed := u.URL() + if d.Attributes, err = d.ResourceTypes.NewFromParsedURI(parsed); err == nil { err = d.Attributes.SetParsedURI(u) } } @@ -149,15 +151,17 @@ func (d *Declaration) NewResourceFromParsedURI(u *url.URL) (err error) { } func (d *Declaration) NewResource(uri *string) (err error) { + slog.Info("Declaration.NewResource()") if d.ResourceTypes == nil { panic(fmt.Errorf("Undefined type registry: unable to create new resource %s", *uri)) } + if uri == nil { - d.Attributes, err = d.ResourceTypes.New(fmt.Sprintf("%s://", d.Type)) + d.Attributes, err = d.ResourceTypes.NewFromType(string(d.Type)) } else { - if d.Attributes, err = d.ResourceTypes.New(*uri); err == nil { - err = d.Attributes.SetURI(*uri) - } + slog.Info("Declaration.NewResource()", "uri", *uri) + parsedURI := URI(*uri).Parse() + d.Attributes, err = d.ResourceTypes.NewFromParsedURI(parsedURI.URL()) } return } @@ -169,7 +173,6 @@ func (d *Declaration) Resource() data.Resource { func (d *Declaration) Apply(stateTransition string) (result error) { defer func() { if r := recover(); r != nil { - slog.Debug("Declaration.Apply()", "stacktrace", string(debug.Stack())) slog.Info("Declaration.Apply()", "error", r, "resourceerror", d.Error) if d.Error != "" { result = fmt.Errorf("%s - %s", r, d.Error) @@ -254,7 +257,7 @@ func (d *Declaration) SetURI(uri string) (err error) { if d.Attributes == nil { err = d.NewResource(&uri) } else { - err = d.Attributes.SetURI(uri) + err = d.Attributes.SetParsedURI(URI(uri).Parse()) } if err != nil { return err @@ -268,7 +271,7 @@ func (d *Declaration) SetURI(uri string) (err error) { return } -func (d *Declaration) SetParsedURI(uri *url.URL) (err error) { +func (d *Declaration) SetParsedURI(uri data.URIParser) (err error) { slog.Info("Declaration.SetParsedURI()", "uri", uri, "declaration", d) if d.Attributes == nil { err = d.NewResourceFromParsedURI(uri) @@ -298,7 +301,9 @@ func (d *Declaration) UnmarshalValue(value *DeclarationType) error { d.OnError = value.OnError d.Error = value.Error d.Requires = value.Requires - newResource, resourceErr := d.ResourceTypes.New(fmt.Sprintf("%s://", value.Type)) + d.On = value.On + newResource, resourceErr := d.ResourceTypes.NewFromType(string(value.Type)) + slog.Info("Declaration.UnmarshalValue", "value", value, "error", resourceErr, "type", value.Type, "resource", newResource, "resourcetypes", d.ResourceTypes) if resourceErr != nil { slog.Info("Declaration.UnmarshalValue", "value", value, "error", resourceErr) @@ -306,10 +311,30 @@ func (d *Declaration) UnmarshalValue(value *DeclarationType) error { } d.Attributes = newResource d.configBlock = d.Config.GetBlock() + + if d.On != nil { + if handler, ok := (*d.On)[EventTypeLoad]; ok { + stackSize := d.runtime.Api().GetTop() + if e := d.runtime.LoadScriptFromString(string(handler)); e != nil { + d.Error = e.Error() + } + returnsCount := d.runtime.Api().GetTop() - stackSize + if ! d.runtime.Api().IsNil(-1) { + if returnsCount == 0 { + // return nil + } else { + if lr,le := d.runtime.CopyReturnValuesFromCall(int(returnsCount)); le == nil { + slog.Info("Event.Load", "result", lr, "error", le) + } + } + } + } + } + return nil } -func (d *Declaration) UnmarshalYAML(value *yaml.Node) error { +func (d *Declaration) UnmarshalYAML(value *yaml.Node) (err error) { if d.ResourceTypes == nil { d.ResourceTypes = DocumentRegistry.ResourceTypes } @@ -331,10 +356,17 @@ func (d *Declaration) UnmarshalYAML(value *yaml.Node) error { if unmarshalResourceErr := resourceAttrs.Attributes.Decode(d.Attributes); unmarshalResourceErr != nil { return unmarshalResourceErr } - return nil + + if i, ok := d.Attributes.(data.ResourceInitializer); ok { + err = i.Init(nil) + } else { + err = fmt.Errorf("failed to execute init") + } + + return } -func (d *Declaration) UnmarshalJSON(jsonData []byte) error { +func (d *Declaration) UnmarshalJSON(jsonData []byte) (err error) { if d.ResourceTypes == nil { d.ResourceTypes = DocumentRegistry.ResourceTypes } @@ -354,24 +386,14 @@ func (d *Declaration) UnmarshalJSON(jsonData []byte) error { return unmarshalAttributesErr } - return nil -} + if i, ok := d.Attributes.(data.ResourceInitializer); ok { + err = i.Init(nil) + } else { + err = fmt.Errorf("failed to execute init") + } -/* -func (d *Declaration) MarshalJSON() ([]byte, error) { - buf := new(bytes.Buffer) - buf.WriteByte('"') - buf.WriteString("value")) - buf.WriteByte('"') - return buf.Bytes(), nil + return } -*/ - -/* -func (d *Declaration) MarshalYAML() (any, error) { - return d, nil -} -*/ /* func (l *LuaWorker) Receive(m message.Envelope) { diff --git a/internal/folio/declaration_test.go b/internal/folio/declaration_test.go index 1de5f6e..34cfbe1 100644 --- a/internal/folio/declaration_test.go +++ b/internal/folio/declaration_test.go @@ -57,6 +57,7 @@ func TestNewResourceDeclarationType(t *testing.T) { e := resourceDeclaration.LoadString(decl, codec.FormatYaml) assert.Nil(t, e) + assert.Equal(t, TypeName("foo"), resourceDeclaration.Type) assert.NotNil(t, resourceDeclaration.Attributes) } diff --git a/internal/folio/document.go b/internal/folio/document.go index 19fcae1..ee4f390 100644 --- a/internal/folio/document.go +++ b/internal/folio/document.go @@ -10,7 +10,6 @@ import ( "io/fs" _ "os" "log/slog" - "net/url" "github.com/sters/yaml-diff/yamldiff" "strings" "decl/internal/codec" @@ -19,6 +18,7 @@ _ "decl/internal/types" "decl/internal/data" "decl/internal/schema" "context" + "path/filepath" ) type DocumentType struct { @@ -44,13 +44,23 @@ type Document struct { config data.Document Registry *Registry `json:"-" yaml:"-"` failedResources int `json:"-" yaml:"-"` + importPaths *SearchPath `json:"-" yaml:"-"` } func NewDocument(r *Registry) *Document { if r == nil { r = DocumentRegistry } - return &Document{ Registry: r, Format: codec.FormatYaml, uris: mapper.New[string, data.Declaration](), configNames: mapper.New[string, data.Block]() } + + var configImportPath ConfigKey = "system.importpath" + + return &Document{ + Registry: r, + Format: codec.FormatYaml, + uris: mapper.New[string, data.Declaration](), + configNames: mapper.New[string, data.Block](), + importPaths: NewSearchPath(configImportPath.GetStringSlice()), + } } func (d *Document) GetURI() string { @@ -59,6 +69,19 @@ func (d *Document) GetURI() string { func (d *Document) SetURI(uri string) { d.URI = URI(uri) + d.AddProjectPath() +} + +func (d *Document) AddProjectPath() { + exists := d.URI.Exists() + if exists { + if u := d.URI.Parse().(*ParsedURI); u != nil { + projectPath := filepath.Dir(filepath.Join(u.Hostname(), u.Path)) + if err := d.importPaths.AddPath(projectPath); err != nil { + panic(err) + } + } + } } func (d *Document) Types() data.TypesRegistry[data.Resource] { @@ -146,7 +169,15 @@ func (d *Document) ImportedDocuments() (documents []data.Document) { func (d *Document) loadImports() (err error) { for _, uri := range d.Imports { if ! DocumentRegistry.HasDocument(uri) { - if _, err = DocumentRegistry.Load(uri); err != nil { + var load URI = uri + if ! load.Exists() { + foundURI := d.importPaths.FindURI(load) + if foundURI != "" { + load = foundURI + } + } + slog.Info("Document.loadImports()", "load", load, "uri", uri, "importpaths", d.importPaths, "doc", d.URI) + if _, err = DocumentRegistry.Load(load); err != nil { return } } @@ -390,7 +421,7 @@ func (d *Document) NewResourceFromURI(uri URI) (newResource data.Resource, err e return d.NewResourceFromParsedURI(uri.Parse()) } -func (d *Document) NewResourceFromParsedURI(uri *url.URL) (newResource data.Resource, err error) { +func (d *Document) NewResourceFromParsedURI(uri data.URIParser) (newResource data.Resource, err error) { if uri == nil { return nil, fmt.Errorf("%w: %s", ErrUnknownResourceType, uri) } diff --git a/internal/folio/document_test.go b/internal/folio/document_test.go index 5a5d1cf..b75b2b5 100644 --- a/internal/folio/document_test.go +++ b/internal/folio/document_test.go @@ -84,6 +84,9 @@ resources: f.Name = "mytestresource" f.Size = 3 + + assert.Nil(t, f.Init(nil)) + d.AddResourceDeclaration("foo", f) ey := d.Generate(&documentYaml) diff --git a/internal/folio/mock_configuration_test.go b/internal/folio/mock_configuration_test.go index 24ae471..3caf8cc 100644 --- a/internal/folio/mock_configuration_test.go +++ b/internal/folio/mock_configuration_test.go @@ -3,14 +3,13 @@ package folio import ( - "context" -_ "gopkg.in/yaml.v3" + "context" +_ "gopkg.in/yaml.v3" "encoding/json" _ "fmt" "decl/internal/data" "decl/internal/codec" "io" - "net/url" ) type MockConfiguration struct { @@ -38,7 +37,7 @@ func (m *MockConfiguration) SetURI(uri string) error { return nil } -func (m *MockConfiguration) SetParsedURI(uri *url.URL) error { +func (m *MockConfiguration) SetParsedURI(uri data.URIParser) error { return nil } diff --git a/internal/folio/mock_foo_resource_test.go b/internal/folio/mock_foo_resource_test.go index e3efdc9..0018ff6 100644 --- a/internal/folio/mock_foo_resource_test.go +++ b/internal/folio/mock_foo_resource_test.go @@ -17,22 +17,30 @@ _ "gopkg.in/yaml.v3" func RegisterMocks() { TestResourceTypes.Register([]string{"foo"}, func(u *url.URL) data.Resource { f := NewFooResource() - f.Name = filepath.Join(u.Hostname(), u.Path) + if u != nil { + f.Name = filepath.Join(u.Hostname(), u.Path) + } return f }) TestResourceTypes.Register([]string{"bar"}, func(u *url.URL) data.Resource { f := NewBarResource() - f.Name = filepath.Join(u.Hostname(), u.Path) + if u != nil { + f.Name = filepath.Join(u.Hostname(), u.Path) + } return f }) TestResourceTypes.Register([]string{"testuser"}, func(u *url.URL) data.Resource { f := NewTestuserResource() - f.Name = filepath.Join(u.Hostname(), u.Path) + if u != nil { + f.Name = filepath.Join(u.Hostname(), u.Path) + } return f }) TestResourceTypes.Register([]string{"file"}, func(u *url.URL) data.Resource { f := NewFileResource() - f.Name = filepath.Join(u.Hostname(), u.Path) + if u != nil { + f.Name = filepath.Join(u.Hostname(), u.Path) + } return f }) } @@ -40,23 +48,20 @@ func RegisterMocks() { type MockFoo struct { stater machine.Stater `json:"-" yaml:"-"` - *MockResource `json:"-" yaml:"-"` - Name string `json:"name" yaml:"name"` + *MockResource `json:",inline" yaml:",inline"` Size int `json:"size" yaml:"size"` } type MockBar struct { stater machine.Stater `json:"-" yaml:"-"` - *MockResource `json:"-" yaml:"-"` - Name string `json:"name" yaml:"name"` + *MockResource `json:",inline" yaml:",inline"` Size int `json:"size,omitempty" yaml:"size,omitempty"` Owner string `json:"owner,omitempty" yaml:"owner,omitempty"` } type MockTestuser struct { stater machine.Stater `json:"-" yaml:"-"` - *MockResource `json:"-" yaml:"-"` - Name string `json:"name" yaml:"name"` + *MockResource `json:",inline" yaml:",inline"` Uid string `json:"uid" yaml:"uid"` Group string `json:"group" yaml:"group"` Home string `json:"home" yaml:"home"` @@ -64,13 +69,12 @@ type MockTestuser struct { type MockFile struct { stater machine.Stater `json:"-" yaml:"-"` - *MockResource `json:"-" yaml:"-"` - Name string `json:"name" yaml:"name"` + *MockResource `json:",inline" yaml:",inline"` Size int `json:"size" yaml:"size"` } -func NewMockResource(typename string, stater machine.Stater) *MockResource { - return &MockResource { +func NewMockResource(typename string, stater machine.Stater) (m *MockResource) { + m = &MockResource { InjectType: func() string { return typename }, InjectResolveId: func(ctx context.Context) string { return "bar" }, InjectLoadDecl: func(string) error { return nil }, @@ -89,6 +93,13 @@ func NewMockResource(typename string, stater machine.Stater) *MockResource { InjectURI: func() string { return fmt.Sprintf("%s://bar", typename) }, InjectNotify: func(*machine.EventMessage) {}, } + m.InjectInit = func(u data.URIParser) error { + if u != nil { + m.Name = filepath.Join(u.URL().Hostname(), u.URL().Path) + } + return nil + } + return m } func NewFooResource() *MockFoo { diff --git a/internal/folio/mock_resource_test.go b/internal/folio/mock_resource_test.go index 7e8f0a1..a56ae64 100644 --- a/internal/folio/mock_resource_test.go +++ b/internal/folio/mock_resource_test.go @@ -3,18 +3,19 @@ package folio import ( - "context" -_ "gopkg.in/yaml.v3" + "context" +_ "gopkg.in/yaml.v3" "encoding/json" _ "fmt" "gitea.rosskeen.house/rosskeen.house/machine" "decl/internal/data" "decl/internal/codec" "io" - "net/url" ) type MockResource struct { + Name string `json:"name" yaml:"name"` + InjectInit func(data.URIParser) error `json:"-" yaml:"-"` InjectURI func() string `json:"-" yaml:"-"` InjectType func() string `json:"-" yaml:"-"` InjectResolveId func(ctx context.Context) string `json:"-" yaml:"-"` @@ -54,7 +55,7 @@ func (m *MockResource) SetURI(uri string) error { return nil } -func (m *MockResource) SetParsedURI(uri *url.URL) error { +func (m *MockResource) SetParsedURI(uri data.URIParser) error { return nil } @@ -126,6 +127,10 @@ func (m *MockResource) Apply() error { return m.InjectApply() } +func (m *MockResource) Init(u data.URIParser) (error) { + return m.InjectInit(u) +} + func (m *MockResource) Type() string { return m.InjectType() } diff --git a/internal/folio/parseduri.go b/internal/folio/parseduri.go new file mode 100644 index 0000000..d570efb --- /dev/null +++ b/internal/folio/parseduri.go @@ -0,0 +1,102 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "net/url" + "decl/internal/transport" + "decl/internal/data" + "decl/internal/identifier" + "strings" +) + + +var ( +) + +type ParsedURI url.URL + +func NewParsedURI(uri URI) *ParsedURI { + return uri.Parse().(*ParsedURI) +} + +func CastParsedURI(u *url.URL) *ParsedURI { + return (*ParsedURI)(u) +} + +func (uri *ParsedURI) URL() *url.URL { + return (*url.URL)(uri) +} + +func (uri *ParsedURI) Hostname() string { + return (*url.URL)(uri).Hostname() +} + +func (uri *ParsedURI) NewResource(document data.Document) (newResource data.Resource, err error) { + if document == nil { + declaration := NewDeclaration() + if err = declaration.NewResourceFromParsedURI(uri); err == nil { + return declaration.Attributes, err + } + } else { + newResource, err = document.NewResourceFromParsedURI(uri) + return + } + return +} + +func (uri *ParsedURI) ConstructResource(res data.Resource) (err error) { + if ri, ok := res.(data.ResourceInitializer); ok { + err = ri.Init(uri) + } else { + err = res.SetParsedURI(uri) + } + return +} + +func (uri *ParsedURI) Converter() (converter data.Converter, err error) { + return DocumentRegistry.ConverterTypes.NewFromParsedURI(uri.URL()) +} + +func (uri *ParsedURI) Exists() bool { + return transport.Exists(uri.URL()) +} + +func (uri *ParsedURI) ContentReaderStream() (*transport.Reader, error) { + return transport.NewReader(uri.URL()) +} + +func (uri *ParsedURI) ContentWriterStream() (*transport.Writer, error) { + return transport.NewWriter(uri.URL()) +} + +func (uri *ParsedURI) String() string { + return uri.URL().String() +} + +func (uri *ParsedURI) SetURL(url *url.URL) { + *uri = *(*ParsedURI)(url) +} + +func (uri *ParsedURI) Extension() (string, string) { + return (identifier.ID)(uri.Path).Extension() +} + +func (uri *ParsedURI) ContentType() string { + var ext strings.Builder + exttype, fileext := uri.Extension() + if fileext == "" { + return exttype + } + ext.WriteString(exttype) + ext.WriteRune('.') + ext.WriteString(fileext) + return ext.String() +} + +func (uri *ParsedURI) IsEmpty() bool { + if uri == nil || (identifier.ID)(uri.String()).IsEmpty() { + return true + } + return false +} diff --git a/internal/folio/registry.go b/internal/folio/registry.go index 3f92567..500a4a2 100644 --- a/internal/folio/registry.go +++ b/internal/folio/registry.go @@ -69,7 +69,7 @@ func (r *Registry) SetDocument(key URI, value *Document) { func (r *Registry) NewDocument(uri URI) (doc *Document) { doc = NewDocument(r) - doc.URI = uri + doc.SetURI(string(uri)) r.Documents = append(r.Documents, doc) if uri != "" { r.UriMap[uri] = doc @@ -148,3 +148,8 @@ func (r *Registry) LoadFromURL(uri *url.URL) (documents []data.Document, err err documents = make([]data.Document, 0, 10) return r.AppendParsedURI(uri, documents) } + +func (r *Registry) LoadFromParsedURI(uri *url.URL) (documents []data.Document, err error) { + documents = make([]data.Document, 0, 10) + return r.AppendParsedURI(uri, documents) +} diff --git a/internal/folio/resource.go b/internal/folio/resource.go index 9e3c889..fb489f2 100644 --- a/internal/folio/resource.go +++ b/internal/folio/resource.go @@ -15,11 +15,11 @@ var ( func NewResourceFromParsedURI(u *url.URL, document data.Document) (newResource data.Resource, err error) { if document == nil { declaration := NewDeclaration() - if err = declaration.NewResourceFromParsedURI(u); err == nil { + if err = declaration.NewResourceFromParsedURI((*ParsedURI)(u)); err == nil { return declaration.Attributes, err } } else { - newResource, err = document.NewResourceFromParsedURI(u) + newResource, err = document.NewResourceFromParsedURI((*ParsedURI)(u)) return } return diff --git a/internal/folio/resourcereference.go b/internal/folio/resourcereference.go index 52be7e7..8faabd2 100644 --- a/internal/folio/resourcereference.go +++ b/internal/folio/resourcereference.go @@ -56,7 +56,7 @@ func (r ResourceReference) Dereference(look data.ResourceMapper) data.Resource { } func (r ResourceReference) Parse() *url.URL { - return URI(r).Parse() + return URI(r).Parse().URL() } func (r ResourceReference) Exists() bool { diff --git a/internal/folio/searchpath.go b/internal/folio/searchpath.go new file mode 100644 index 0000000..d98eb1f --- /dev/null +++ b/internal/folio/searchpath.go @@ -0,0 +1,65 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "decl/internal/ext" + "decl/internal/ds" + "errors" + "fmt" + "log/slog" + "path/filepath" + "decl/internal/data" +) + +var ( + ErrSearchPathNotExist error = errors.New("Search path does not exist.") +) + +// A search path should find a file in list of paths + +type SearchPath struct { + paths *ds.OrderedSet[string] +} + +func NewSearchPath(paths []string) (s *SearchPath) { + s = &SearchPath{ paths: ds.NewOrderedSet[string]() } + s.paths.AddItems(paths) + return +} + +func (s *SearchPath) AddPath(path string) error { + if newPath := ext.FilePath(path).Abs(); newPath.Exists() { + s.paths.Add(string(newPath)) + } else { + return fmt.Errorf("%w: %s", ErrSearchPathNotExist, path) + } + return nil +} + +func (s *SearchPath) Find(relative string) string { + pathValues := s.paths.Items() + for i := len(pathValues) - 1; i >= 0; i-- { + p := *pathValues[i] + searchPath := ext.FilePath(p) + searchPath.Add(relative) + slog.Info("SearchPath.Find()", "searchpath", p, "file", relative, "target", searchPath) + if searchPath.Exists() { + return string(searchPath) + } + } + return "" +} + +func (s *SearchPath) FindParsedURI(uri data.URIParser) (result URI) { + u := uri.URL() + filePath := filepath.Join(u.Hostname(), u.Path) + if absPath := s.Find(filePath); absPath != "" { + result = URI(fmt.Sprintf("%s://%s", u.Scheme, string(absPath))) + } + return +} + +func (s *SearchPath) FindURI(uri URI) (result URI) { + return s.FindParsedURI(uri.Parse()) +} diff --git a/internal/folio/searchpath_test.go b/internal/folio/searchpath_test.go new file mode 100644 index 0000000..e8c021c --- /dev/null +++ b/internal/folio/searchpath_test.go @@ -0,0 +1,24 @@ +// Copyright 2024 Matthew Rich . All rights reserved. + +package folio + +import ( + "github.com/stretchr/testify/assert" + "testing" + "os" +) + +func TestSearchPath(t *testing.T) { + + assert.Nil(t, TempDir.CreateFile("test.jx.yaml", "")) + + sp := NewSearchPath(nil) + assert.NotNil(t, sp) + + assert.Nil(t, sp.AddPath(string(TempDir))) + + absPath := sp.Find("test.jx.yaml") + _, err := os.Stat(absPath) + assert.True(t, !os.IsNotExist(err)) + +} diff --git a/internal/folio/uri.go b/internal/folio/uri.go index e946f51..07a4341 100644 --- a/internal/folio/uri.go +++ b/internal/folio/uri.go @@ -35,14 +35,24 @@ func (u URI) NewResource(document data.Document) (newResource data.Resource, err return } +func (u URI) ConstructResource(res data.Resource) (err error) { + parsedURI := u.Parse() + if ri, ok := res.(data.ResourceInitializer); ok { + err = ri.Init(parsedURI) + } else { + err = res.SetParsedURI(parsedURI) + } + return +} + func (u URI) Converter() (converter data.Converter, err error) { return DocumentRegistry.ConverterTypes.New(string(u)) } -func (u URI) Parse() *url.URL { +func (u URI) Parse() data.URIParser { url, e := url.Parse(string(u)) if e == nil { - return url + return (*ParsedURI)(url) } return nil } diff --git a/internal/folio/uri_test.go b/internal/folio/uri_test.go index 1c21f56..4b1f82a 100644 --- a/internal/folio/uri_test.go +++ b/internal/folio/uri_test.go @@ -10,17 +10,17 @@ import ( func TestURI(t *testing.T) { var file URI = URI(fmt.Sprintf("file://%s", TempDir)) - u := file.Parse() + u := file.Parse().URL() assert.Equal(t, "file", u.Scheme) assert.True(t, file.Exists()) file = URI(fmt.Sprintf("0file:_/%s", TempDir)) - u = file.Parse() - assert.Nil(t, u) + x := file.Parse() + assert.Nil(t, x) } func TestURISetURL(t *testing.T) { var file URI = URI(fmt.Sprintf("file://%s", TempDir)) - u := file.Parse() + u := file.Parse().URL() var fileFromURL URI fileFromURL.SetURL(u) assert.Equal(t, fileFromURL, file) diff --git a/internal/resource/common.go b/internal/resource/common.go index 0c952af..f614229 100644 --- a/internal/resource/common.go +++ b/internal/resource/common.go @@ -20,9 +20,9 @@ type Common struct { NormalizePath UriNormalize `json:"-" yaml:"-"` includeQueryParamsInURI bool `json:"-" yaml:"-"` resourceType TypeName `json:"-" yaml:"-"` - Uri folio.URI `json:"uri,omitempty" yaml:"uri,omitempty"` parsedURI *url.URL `json:"-" yaml:"-"` Path string `json:"path,omitempty" yaml:"path,omitempty"` + absPath string `json:"-" yaml:"-"` exttype string `json:"-" yaml:"-"` fileext string `json:"-" yaml:"-"` @@ -57,9 +57,13 @@ func (c *Common) SetResourceMapper(resources data.ResourceMapper) { func (c *Common) Clone() *Common { return &Common { - Uri: c.Uri, + SchemeCheck: c.SchemeCheck, + NormalizePath: c.NormalizePath, + includeQueryParamsInURI: c.includeQueryParamsInURI, + resourceType: c.resourceType, parsedURI: c.parsedURI, Path: c.Path, + absPath: c.absPath, exttype: c.exttype, fileext: c.fileext, normalizePath: c.normalizePath, @@ -77,41 +81,35 @@ func (c *Common) URIPath() string { return c.Path } -func (c *Common) URI() string { - return string(c.Uri) +func (c *Common) URI() folio.URI { + slog.Info("Common.URI", "parsed", c.parsedURI, "debug", debug.Stack()) + return folio.URI(c.parsedURI.String()) } -func (c *Common) SetURI(uri string) (err error) { - c.SetURIFromString(uri) - err = c.SetParsedURI(c.Uri.Parse()) - return -} - -func (c *Common) SetURIFromString(uri string) { - c.Uri = folio.URI(uri) - c.exttype, c.fileext = c.Uri.Extension() -} - -func (c *Common) SetParsedURI(u *url.URL) (err error) { +func (c *Common) SetParsedURI(u data.URIParser) (err error) { if u != nil { - slog.Info("Common.SetParsedURI()", "parsed", u, "uri", c.Uri) - if c.Uri.IsEmpty() { - c.SetURIFromString(u.String()) - } - c.parsedURI = u + + slog.Info("Common.SetParsedURI()", "parsed", u, "uri", c) + + c.parsedURI = u.URL() + + c.exttype, c.fileext = u.Extension() if c.SchemeCheck(c.parsedURI.Scheme) { if c.includeQueryParamsInURI { c.Path = filepath.Join(c.parsedURI.Hostname(), c.parsedURI.RequestURI()) } else { c.Path = filepath.Join(c.parsedURI.Hostname(), c.parsedURI.Path) } + if c.absPath, err = filepath.Abs(c.Path); err != nil { + return + } if err = c.NormalizePath(); err != nil { return } return } } - err = fmt.Errorf("%w: %s is not a %s resource, parsed: %t", ErrInvalidResourceURI, c.Uri, c.Type(), (u != nil)) + err = fmt.Errorf("%w: %s is not a %s resource, parsed: %t", ErrInvalidResourceURI, c.URI(), c.Type(), (u != nil)) return } @@ -120,26 +118,27 @@ func (c *Common) UseConfig(config data.ConfigurationValueGetter) { } func (c *Common) ResolveId(ctx context.Context) string { - if e := c.NormalizePath(); e != nil { - panic(e) + var err error + if c.absPath, err = filepath.Abs(c.Path); err != nil { + panic(err) + } + if err = c.NormalizePath(); err != nil { + panic(err) } return c.Path } -func (c *Common) NormalizeFilePath() error { +// Common path normalization for a file resource. +func (c *Common) NormalizeFilePath() (err error) { if c.config != nil { if prefixPath, configErr := c.config.GetValue("prefix"); configErr == nil { c.Path = filepath.Join(prefixPath.(string), c.Path) } } if c.normalizePath { - filePath, fileAbsErr := filepath.Abs(c.Path) - if fileAbsErr == nil { - c.Path = filePath - } - return fileAbsErr + c.Path = c.absPath } - return nil + return } func (c *Common) Type() string { return string(c.resourceType) } diff --git a/internal/resource/common_test.go b/internal/resource/common_test.go index 9fd186d..e5a5c8f 100644 --- a/internal/resource/common_test.go +++ b/internal/resource/common_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" "testing" "net/url" + "decl/internal/folio" ) func TestNewCommon(t *testing.T) { @@ -16,16 +17,15 @@ func TestNewCommon(t *testing.T) { func TestCommon(t *testing.T) { expectedCommon := NewCommon(FileTypeName, false) expectedCommon.resourceType = "file" - expectedCommon.Uri = "file:///tmp/foo" expectedCommon.parsedURI = &url.URL{ Scheme: "file", Path: "/tmp/foo"} expectedCommon.Path = "/tmp/foo" - for _, v := range []struct{ uri string; expected *Common }{ + for _, v := range []struct{ uri folio.URI; expected *Common }{ { uri: "file:///tmp/foo", expected: expectedCommon }, }{ c := NewCommon(FileTypeName, false) c.resourceType = "file" - assert.Nil(t, c.SetURI(v.uri)) + assert.Nil(t, c.SetParsedURI(v.uri.Parse())) assert.Equal(t, v.expected.resourceType , c.resourceType) assert.Equal(t, v.expected.Path, c.Path) assert.Equal(t, v.expected.parsedURI.Scheme, c.parsedURI.Scheme) diff --git a/internal/resource/container.go b/internal/resource/container.go index 46e5e09..12a4443 100644 --- a/internal/resource/container.go +++ b/internal/resource/container.go @@ -16,7 +16,7 @@ import ( "github.com/docker/go-connections/nat" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "gopkg.in/yaml.v3" - _ "gopkg.in/yaml.v3" +_ "gopkg.in/yaml.v3" "log/slog" "net/url" _ "os" @@ -28,6 +28,7 @@ _ "os/exec" "gitea.rosskeen.house/rosskeen.house/machine" "decl/internal/codec" "decl/internal/data" + "decl/internal/folio" ) const ( @@ -50,7 +51,6 @@ type Container struct { stater machine.Stater `yaml:"-" json:"-"` Id string `json:"ID,omitempty" yaml:"ID,omitempty"` Name string `json:"name" yaml:"name"` -// Path string `json:"path" yaml:"path"` Cmd []string `json:"cmd,omitempty" yaml:"cmd,omitempty"` Entrypoint strslice.StrSlice `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty"` Args []string `json:"args,omitempty" yaml:"args,omitempty"` @@ -81,21 +81,19 @@ type Container struct { NetworkSettings *NetworkSettings */ -// State string `yaml:"state,omitempty" json:"state,omitempty"` - -// config ConfigurationValueGetter apiClient ContainerClient -// Resources data.ResourceMapper `json:"-" yaml:"-"` + Resources data.ResourceMapper `json:"-" yaml:"-"` } func init() { - ResourceTypes.Register([]string{"container"}, func(u *url.URL) data.Resource { - c := NewContainer(nil) - c.Name = filepath.Join(u.Hostname(), u.Path) - if err := c.Common.SetParsedURI(u); err == nil { - return c + ResourceTypes.Register([]string{"container"}, func(u *url.URL) (c data.Resource) { + c = NewContainer(nil) + if u != nil { + if err := folio.CastParsedURI(u).ConstructResource(c); err != nil { + panic(err) + } } - return nil + return }) } @@ -114,6 +112,22 @@ func NewContainer(containerClientApi ContainerClient) *Container { } } +func (c *Container) Init(u data.URIParser) error { + if u == nil { + u = folio.URI(c.URI()).Parse() + } + uri := u.URL() + c.Name = filepath.Join(uri.Hostname(), uri.Path) + return c.SetParsedURI(u) +} + +func (c *Container) SetParsedURI(u data.URIParser) (err error) { + if err = c.Common.SetParsedURI(u); err == nil { + c.Name = filepath.Join(c.Common.parsedURI.Hostname(), c.Common.parsedURI.Path) + } + return +} + func (c *Container) SetResourceMapper(resources data.ResourceMapper) { c.Resources = resources } @@ -122,7 +136,7 @@ func (c *Container) Clone() data.Resource { return &Container { Id: c.Id, Name: c.Name, - Common: c.Common, + Common: c.Common.Clone(), Cmd: c.Cmd, Entrypoint: c.Entrypoint, Args: c.Args, @@ -146,7 +160,6 @@ func (c *Container) Clone() data.Resource { SizeRw: c.SizeRw, SizeRootFs: c.SizeRootFs, Networks: c.Networks, -// State: c.State, apiClient: c.apiClient, } } @@ -158,10 +171,6 @@ func (c *Container) StateMachine() machine.Stater { return c.stater } -func (c *Container) UseConfig(config data.ConfigurationValueGetter) { - c.config = config -} - func (c *Container) JSON() ([]byte, error) { return json.Marshal(c) } @@ -429,7 +438,7 @@ func (c *Container) Type() string { return "container" } func (c *Container) ResolveId(ctx context.Context) string { var err error - if err = c.Common.SetURI(c.URI()); err != nil { + if err = c.Common.SetParsedURI(folio.URI(c.URI()).Parse()); err != nil { triggerErr := c.StateMachine().Trigger("notexists") panic(fmt.Errorf("%w: %s %s, %w", err, c.Type(), c.Name, triggerErr)) } diff --git a/internal/resource/container_image.go b/internal/resource/container_image.go index fd88fc8..d0ee72e 100644 --- a/internal/resource/container_image.go +++ b/internal/resource/container_image.go @@ -101,11 +101,14 @@ type ContainerImage struct { } func init() { - folio.DocumentRegistry.ResourceTypes.Register([]string{"container-image"}, func(u *url.URL) data.Resource { - c := NewContainerImage(nil) - c.Name = ContainerImageNameFromURI(u) - slog.Info("NewContainerImage", "container", c) - return c + folio.DocumentRegistry.ResourceTypes.Register([]string{"container-image"}, func(u *url.URL) (c data.Resource) { + c = NewContainerImage(nil) + if u != nil { + if err := folio.CastParsedURI(u).ConstructResource(c); err != nil { + panic(err) + } + } + return }) } @@ -129,6 +132,15 @@ func NewContainerImage(containerClientApi ContainerImageClient) *ContainerImage return c } +func (c *ContainerImage) Init(u data.URIParser) (err error) { + if u == nil { + u = folio.URI(c.URI()).Parse() + } + err = c.SetParsedURI(u) + c.Name = ContainerImageNameFromURI(u.URL()) + return +} + func (c *ContainerImage) RegistryAuthConfig() (authConfig registry.AuthConfig, err error) { if c.Common.config != nil { var configValue any @@ -255,24 +267,6 @@ func (c *ContainerImage) URI() string { return URIFromContainerImageName(c.Name) } -/* -func (c *ContainerImage) SetURI(uri string) error { - resourceUri, e := url.Parse(uri) - if e == nil { - if resourceUri.Scheme == c.Type() { - c.Name = strings.Join([]string{resourceUri.Hostname(), resourceUri.RequestURI()}, ":") - } else { - e = fmt.Errorf("%w: %s is not a %s", ErrInvalidResourceURI, uri, c.Type()) - } - } - return e -} - -func (c *ContainerImage) UseConfig(config data.ConfigurationValueGetter) { - c.config = config -} -*/ - func (c *ContainerImage) JSON() ([]byte, error) { return json.Marshal(c) } @@ -795,12 +789,6 @@ func (c *ContainerImage) Delete(ctx context.Context) error { _, err := c.apiClient.ImageRemove(ctx, c.Id, options) return err -/* -for _, img := range deletedImages { -fmt.Printf("Deleted image: %s\n", img.Deleted) -fmt.Printf("Untagged image: %s\n", img.Untagged) -} -*/ } func (c *ContainerImage) Type() string { return "container-image" } @@ -827,5 +815,3 @@ func (c *ContainerImage) ResolveId(ctx context.Context) string { } return c.Id } - - diff --git a/internal/resource/container_network.go b/internal/resource/container_network.go index 2e3fa64..c11a15c 100644 --- a/internal/resource/container_network.go +++ b/internal/resource/container_network.go @@ -15,7 +15,6 @@ _ "log/slog" "net/url" _ "os" _ "os/exec" - "path/filepath" _ "strings" "encoding/json" "io" @@ -39,26 +38,29 @@ type ContainerNetworkClient interface { } type ContainerNetwork struct { - *Common `json:",inline" yaml:",inline"` - stater machine.Stater `json:"-" yaml:"-"` - Id string `json:"ID,omitempty" yaml:"ID,omitempty"` - Name string `json:"name" yaml:"name"` - Driver string `json:"driver,omitempty" yaml:"driver,omitempty"` - EnableIPv6 bool `json:"enableipv6,omitempty" yaml:"enableipv6,omitempty"` - Internal bool `json:"internal,omitempty" yaml:"internal,omitempty"` - Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` - Created time.Time `json:"created" yaml:"created"` - //State string `yaml:"state"` + *Common `json:",inline" yaml:",inline"` + stater machine.Stater `json:"-" yaml:"-"` + Id string `json:"ID,omitempty" yaml:"ID,omitempty"` + Name string `json:"name" yaml:"name"` + Driver string `json:"driver,omitempty" yaml:"driver,omitempty"` + EnableIPv6 bool `json:"enableipv6,omitempty" yaml:"enableipv6,omitempty"` + Internal bool `json:"internal,omitempty" yaml:"internal,omitempty"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + Created time.Time `json:"created" yaml:"created"` - apiClient ContainerNetworkClient - Resources data.ResourceMapper `json:"-" yaml:"-"` + apiClient ContainerNetworkClient + Resources data.ResourceMapper `json:"-" yaml:"-"` } func init() { - folio.DocumentRegistry.ResourceTypes.Register([]string{"container-network"}, func(u *url.URL) data.Resource { - n := NewContainerNetwork(nil) - n.Name = filepath.Join(u.Hostname(), u.Path) - return n + folio.DocumentRegistry.ResourceTypes.Register([]string{"container-network"}, func(u *url.URL) (n data.Resource) { + n = NewContainerNetwork(nil) + if u != nil { + if err := folio.CastParsedURI(u).ConstructResource(n); err != nil { + panic(err) + } + } + return }) } @@ -79,6 +81,13 @@ func NewContainerNetwork(containerClientApi ContainerNetworkClient) (cn *Contain return cn } +func (n *ContainerNetwork) Init(u data.URIParser) error { + if u == nil { + u = folio.URI(n.URI()).Parse() + } + return n.SetParsedURI(u) +} + func (n *ContainerNetwork) NormalizePath() error { return nil } @@ -92,7 +101,6 @@ func (n *ContainerNetwork) Clone() data.Resource { Common: n.Common, Id: n.Id, Name: n.Name, - //State: n.State, apiClient: n.apiClient, } } @@ -154,24 +162,6 @@ func (n *ContainerNetwork) URI() string { return fmt.Sprintf("container-network://%s", n.Name) } -/* -func (n *ContainerNetwork) SetURI(uri string) error { - resourceUri, e := url.Parse(uri) - if e == nil { - if resourceUri.Scheme == n.Type() { - n.Name, e = filepath.Abs(filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI())) - } else { - e = fmt.Errorf("%w: %s is not a %s", ErrInvalidResourceURI, uri, n.Type()) - } - } - return e -} - -func (n *ContainerNetwork) UseConfig(config data.ConfigurationValueGetter) { - n.config = config -} -*/ - func (n *ContainerNetwork) JSON() ([]byte, error) { return json.Marshal(n) } diff --git a/internal/resource/exec.go b/internal/resource/exec.go index 1b3eb05..20eea02 100644 --- a/internal/resource/exec.go +++ b/internal/resource/exec.go @@ -39,9 +39,22 @@ type Exec struct { } func init() { - folio.DocumentRegistry.ResourceTypes.Register([]string{"exec"}, func(u *url.URL) data.Resource { + folio.DocumentRegistry.ResourceTypes.Register([]string{"exec"}, func(u *url.URL) (res data.Resource) { x := NewExec() - return x + res = x + if u != nil { + uri := folio.CastParsedURI(u) + if ri, ok := res.(data.ResourceInitializer); ok { + if err := ri.Init(uri); err != nil { + panic(err) + } + } else { + if err := x.SetParsedURI(uri); err != nil { + panic(err) + } + } + } + return }) } @@ -56,7 +69,7 @@ func (x *Exec) SetResourceMapper(resources data.ResourceMapper) { func (x *Exec) Clone() data.Resource { return &Exec { - Common: x.Common, + Common: x.Common.Clone(), Id: x.Id, CreateTemplate: x.CreateTemplate, ReadTemplate: x.ReadTemplate, @@ -76,13 +89,16 @@ func (x *Exec) URI() string { return fmt.Sprintf("exec://%s", x.Id) } -func (x *Exec) SetURI(uri string) (err error) { - err = x.Common.SetURI(uri) +func (x *Exec) Init(u data.URIParser) (err error) { + if u == nil { + u = folio.URI(x.URI()).Parse() + } + err = x.SetParsedURI(u) x.Id = x.Common.Path return } -func (x *Exec) SetParsedURI(uri *url.URL) (err error) { +func (x *Exec) SetParsedURI(uri data.URIParser) (err error) { err = x.Common.SetParsedURI(uri) x.Id = x.Common.Path return diff --git a/internal/resource/exec_test.go b/internal/resource/exec_test.go index 20a992e..720d3ae 100644 --- a/internal/resource/exec_test.go +++ b/internal/resource/exec_test.go @@ -4,20 +4,21 @@ package resource import ( - _ "context" - _ "encoding/json" - _ "fmt" +_ "context" +_ "encoding/json" +_ "fmt" "github.com/stretchr/testify/assert" - _ "gopkg.in/yaml.v3" - _ "io" - _ "log" - _ "net/http" - _ "net/http/httptest" - _ "net/url" - _ "os" - _ "strings" +_ "gopkg.in/yaml.v3" +_ "io" +_ "log" +_ "net/http" +_ "net/http/httptest" +_ "net/url" +_ "os" +_ "strings" "testing" "decl/internal/command" + "decl/internal/folio" ) func TestNewExecResource(t *testing.T) { @@ -55,9 +56,10 @@ func TestCreateExec(t *testing.T) { } func TestExecSetURI(t *testing.T) { + var uri folio.URI = "exec://12345_key" x := NewExec() assert.NotNil(t, x) - e := x.SetURI("exec://" + "12345_key") + e := x.SetParsedURI(uri.Parse()) assert.Nil(t, e) assert.Equal(t, "exec", x.Type()) assert.Equal(t, "12345_key", x.Path) diff --git a/internal/resource/file.go b/internal/resource/file.go index 59ec671..454b8a5 100644 --- a/internal/resource/file.go +++ b/internal/resource/file.go @@ -14,7 +14,6 @@ import ( "net/url" "os" "os/user" - "path/filepath" "strconv" "syscall" "time" @@ -56,19 +55,14 @@ var ErrInvalidFileGroup error = errors.New("Unknown Group") type FileMode string func init() { - folio.DocumentRegistry.ResourceTypes.Register([]string{"file"}, func(u *url.URL) data.Resource { + folio.DocumentRegistry.ResourceTypes.Register([]string{"file"}, func(u *url.URL) (res data.Resource) { f := NewFile() - f.parsedURI = u - //f.Uri.SetURL(u) - f.Path = filepath.Join(u.Hostname(), u.Path) - f.exttype, f.fileext = f.Uri.Extension() - - slog.Info("folio.DocumentRegistry.ResourceTypes.Register()()", "url", u, "file", f) -/* - if absPath, err := filepath.Abs(f.Path); err == nil { - f.Filesystem = os.DirFS(filepath.Dir(absPath)) + slog.Info("FileFactory", "uri", u) + if u != nil { + if err := folio.CastParsedURI(u).ConstructResource(f); err != nil { + panic(err) + } } -*/ return f }) } @@ -83,18 +77,12 @@ The `SerializeContent` the flag allows forcing the content to be serialized in t */ type File struct { - Uri folio.URI `json:"uri,omitempty" yaml:"uri,omitempty"` - parsedURI *url.URL `json:"-" yaml:"-"` + *Common `json:",inline" yaml:",inline"` Filesystem fs.FS `json:"-" yaml:"-"` - exttype string `json:"-" yaml:"-"` - fileext string `json:"-" yaml:"-"` stater machine.Stater `json:"-" yaml:"-"` - normalizePath bool `json:"-" yaml:"-"` - absPath string `json:"-" yaml:"-"` basePath int `json:"-" yaml:"-"` - Path string `json:"path" yaml:"path"` Owner string `json:"owner" yaml:"owner"` Group string `json:"group" yaml:"group"` Mode FileMode `json:"mode" yaml:"mode"` @@ -109,10 +97,8 @@ type File struct { Size int64 `json:"size,omitempty" yaml:"size,omitempty"` Target string `json:"target,omitempty" yaml:"target,omitempty"` FileType FileType `json:"filetype" yaml:"filetype"` - State string `json:"state,omitempty" yaml:"state,omitempty"` SerializeContent bool `json:"serializecontent,omitempty" yaml:"serializecontent,omitempty"` GzipContent bool `json:"gzipcontent,omitempty" yaml:"gzipcontent,omitempty"` - config data.ConfigurationValueGetter Resources data.ResourceMapper `json:"-" yaml:"-"` } @@ -123,29 +109,48 @@ type ResourceFileInfo struct { func NewFile() *File { currentUser, _ := user.Current() group, _ := user.LookupGroupId(currentUser.Gid) - f := &File{ normalizePath: false, Owner: currentUser.Username, Group: group.Name, Mode: "0644", FileType: RegularFile, SerializeContent: false } + f := &File{ + Common: NewCommon(FileTypeName, true), + Owner: currentUser.Username, + Group: group.Name, + Mode: "0644", + FileType: RegularFile, + SerializeContent: false, + } + f.PathNormalization(false) slog.Info("NewFile()", "file", f) return f } func NewNormalizedFile() *File { f := NewFile() - f.normalizePath = true + f.PathNormalization(true) return f } +func (f *File) Init(u data.URIParser) error { + if u == nil { + u = folio.URI(f.URI()).Parse() + } + return f.SetParsedURI(u) +} + +func (f *File) NormalizePath() error { + return f.Common.NormalizePath() +} + func (f *File) ContentType() string { - var ext strings.Builder - if f.parsedURI.Scheme != "file" { - return f.parsedURI.Scheme - } - if f.fileext == "" { - return f.exttype - } - ext.WriteString(f.exttype) - ext.WriteRune('.') - ext.WriteString(f.fileext) - return ext.String() + var ext strings.Builder + if f.parsedURI.Scheme != "file" { + return f.parsedURI.Scheme + } + if f.fileext == "" { + return f.exttype + } + ext.WriteString(f.exttype) + ext.WriteRune('.') + ext.WriteString(f.fileext) + return ext.String() } func (f *File) SetResourceMapper(resources data.ResourceMapper) { @@ -154,13 +159,7 @@ func (f *File) SetResourceMapper(resources data.ResourceMapper) { func (f *File) Clone() data.Resource { return &File { - Uri: f.Uri, - parsedURI: f.parsedURI, - exttype: f.exttype, - fileext: f.fileext, - normalizePath: f.normalizePath, - absPath: f.absPath, - Path: f.Path, + Common: f.Common.Clone(), Owner: f.Owner, Group: f.Group, Mode: f.Mode, @@ -172,7 +171,6 @@ func (f *File) Clone() data.Resource { Size: f.Size, Target: f.Target, FileType: f.FileType, - State: f.State, } } @@ -189,26 +187,26 @@ func (f *File) Notify(m *machine.EventMessage) { switch m.On { case machine.ENTERSTATEEVENT: switch m.Dest { - case "start_stat": - if statErr := f.ReadStat(); statErr == nil { - if triggerErr := f.StateMachine().Trigger("exists"); triggerErr == nil { - return - } - } else { - if triggerErr := f.StateMachine().Trigger("notexists"); triggerErr == nil { - return - } + case "start_stat": + if statErr := f.ReadStat(); statErr == nil { + if triggerErr := f.StateMachine().Trigger("exists"); triggerErr == nil { + return } - case "start_read": - if _,readErr := f.Read(ctx); readErr == nil { - if triggerErr := f.StateMachine().Trigger("state_read"); triggerErr == nil { - return - } else { - f.State = "absent" - panic(triggerErr) - } } else { - f.State = "absent" + if triggerErr := f.StateMachine().Trigger("notexists"); triggerErr == nil { + return + } + } + case "start_read": + if _,readErr := f.Read(ctx); readErr == nil { + if triggerErr := f.StateMachine().Trigger("state_read"); triggerErr == nil { + return + } else { + f.Common.State = "absent" + panic(triggerErr) + } + } else { + f.Common.State = "absent" if ! errors.Is(readErr, ErrResourceStateAbsent) { panic(readErr) } @@ -219,7 +217,7 @@ func (f *File) Notify(m *machine.EventMessage) { return } } else { - f.State = "absent" + f.Common.State = "absent" panic(e) } case "start_update": @@ -227,10 +225,10 @@ func (f *File) Notify(m *machine.EventMessage) { if triggerErr := f.stater.Trigger("updated"); triggerErr == nil { return } else { - f.State = "absent" + f.Common.State = "absent" } } else { - f.State = "absent" + f.Common.State = "absent" panic(updateErr) } case "start_delete": @@ -238,17 +236,17 @@ func (f *File) Notify(m *machine.EventMessage) { if triggerErr := f.StateMachine().Trigger("deleted"); triggerErr == nil { return } else { - f.State = "present" + f.Common.State = "present" panic(triggerErr) } } else { - f.State = "present" + f.Common.State = "present" panic(deleteErr) } case "absent": - f.State = "absent" + f.Common.State = "absent" case "present", "created", "read": - f.State = "present" + f.Common.State = "present" } case machine.EXITSTATEEVENT: } @@ -258,12 +256,8 @@ func (f *File) SetGzipContent(flag bool) { f.GzipContent = flag } -func (f *File) PathNormalization(flag bool) { - f.normalizePath = flag -} - func (f *File) FilePath() string { - return f.Path + return f.Common.Path } func (f *File) SetFS(fsys fs.FS) { @@ -271,63 +265,23 @@ func (f *File) SetFS(fsys fs.FS) { } func (f *File) URI() string { - return fmt.Sprintf("file://%s", f.Path) + return fmt.Sprintf("file://%s", f.Common.Path) } func (f *File) RelativePath() string { - return f.Path[f.basePath:] + return f.Common.Path[f.basePath:] } func (f *File) SetBasePath(index int) { - if index < len(f.Path) { + if index < len(f.Common.Path) { f.basePath = index } } -func (f *File) SetURI(uri string) (err error) { - slog.Info("File.SetURI()", "uri", uri, "file", f, "parsed", f.parsedURI) - f.SetURIFromString(uri) - err = f.SetParsedURI(f.Uri.Parse()) - return -} - func (f *File) DetectGzip() bool { return (f.parsedURI.Query().Get("gzip") == "true" || f.fileext == "gz" || f.exttype == "tgz" || f.exttype == "gz" || f.fileext == "tgz" ) } -func (f *File) SetURIFromString(uri string) { - f.Uri = folio.URI(uri) - f.exttype, f.fileext = f.Uri.Extension() -} - -func (f *File) SetParsedURI(u *url.URL) (err error) { - if u != nil { - if u.Scheme == "" { - u.Scheme = "file" - f.Uri = "" - } - if f.Uri.IsEmpty() { - f.SetURIFromString(u.String()) - } - slog.Info("File.SetParsedURI()", "parsed", u, "path", f.Path) - f.parsedURI = u - if f.parsedURI.Scheme == "file" { - f.Path = filepath.Join(f.parsedURI.Hostname(), f.parsedURI.Path) - slog.Info("File.SetParsedURI()", "path", f.Path) - if err = f.NormalizePath(); err != nil { - return - } - return - } - } - err = fmt.Errorf("%w: %s is not a file", ErrInvalidResourceURI, f.Uri) - return -} - -func (f *File) UseConfig(config data.ConfigurationValueGetter) { - f.config = config -} - func (f *File) JSON() ([]byte, error) { return json.Marshal(f) } @@ -343,7 +297,7 @@ func (f *File) Validate() (err error) { func (f *File) Apply() error { ctx := context.Background() - switch f.State { + switch f.Common.State { case "absent": return f.Delete(ctx) case "present": @@ -385,9 +339,10 @@ func (f *File) ResolveId(ctx context.Context) string { if e := f.NormalizePath(); e != nil { panic(e) } - return f.Path + return f.Common.Path } +/* func (f *File) NormalizePath() (err error) { if f.config != nil { if prefixPath, configErr := f.config.GetValue("prefix"); configErr == nil { @@ -399,6 +354,7 @@ func (f *File) NormalizePath() (err error) { } return } +*/ func (f *File) GetContentSourceRef() string { return string(f.ContentSourceRef) @@ -411,7 +367,7 @@ func (f *File) SetContentSourceRef(uri string) { func (f *File) Stat() (info fs.FileInfo, err error) { if _, ok := f.Filesystem.(embed.FS); ok { - info, err = fs.Stat(f.Filesystem, f.Path) + info, err = fs.Stat(f.Filesystem, f.Common.Path) } else { info, err = os.Lstat(f.absPath) } @@ -471,12 +427,12 @@ func (f *File) Create(ctx context.Context) error { switch f.FileType { case SymbolicLinkFile: - linkErr := os.Symlink(f.Target, f.Path) + linkErr := os.Symlink(f.Target, f.Common.Path) if linkErr != nil { return linkErr } case DirectoryFile: - if mkdirErr := os.MkdirAll(f.Path, mode); mkdirErr != nil { + if mkdirErr := os.MkdirAll(f.Common.Path, mode); mkdirErr != nil { return mkdirErr } default: @@ -504,7 +460,8 @@ func (f *File) Create(ctx context.Context) error { }) var createdFileWriter io.WriteCloser - createdFile, fileErr := os.Create(f.Path) + createdFile, fileErr := os.Create(f.Common.Path) + slog.Info("File.Create(): os.Create()", "path", f.Common.Path, "error", fileErr) if fileErr != nil { return fileErr } @@ -518,6 +475,7 @@ func (f *File) Create(ctx context.Context) error { defer createdFile.Close() + slog.Info("File.Create(): Chmod()", "path", f.Common.Path, "mode", mode) if chmodErr := createdFile.Chmod(mode); chmodErr != nil { return chmodErr } @@ -529,16 +487,20 @@ func (f *File) Create(ctx context.Context) error { f.Sha256 = fmt.Sprintf("%x", hash.Sum(nil)) if !f.Mtime.IsZero() && !f.Atime.IsZero() { - if chtimesErr := os.Chtimes(f.Path, f.Atime, f.Mtime); chtimesErr != nil { + slog.Info("File.Create(): Chtimes()", "path", f.Common.Path, "atime", f.Atime, "mtime", f.Mtime) + if chtimesErr := os.Chtimes(f.Common.Path, f.Atime, f.Mtime); chtimesErr != nil { return chtimesErr } + } else { + slog.Info("File.Create(): Chtimes() SKIPPED", "path", f.Common.Path, "atime", f.Atime, "mtime", f.Mtime) } } + slog.Info("File.Create(): Chown()", "path", f.Common.Path, "uid", uid, "gid", gid) - if chownErr := os.Chown(f.Path, uid, gid); chownErr != nil { + if chownErr := os.Chown(f.Common.Path, uid, gid); chownErr != nil { return chownErr } - f.State = "present" + f.Common.State = "present" return nil } @@ -547,7 +509,7 @@ func (f *File) Update(ctx context.Context) error { } func (f *File) Delete(ctx context.Context) error { - return os.Remove(f.Path) + return os.Remove(f.Common.Path) } func (f *File) UpdateContentAttributes() { @@ -603,11 +565,11 @@ func (f *File) ContentSourceRefStat() (info fs.FileInfo) { func (f *File) ReadStat() (err error) { var info fs.FileInfo - slog.Info("ReadStat()", "filesystem", f.Filesystem, "path", f.Path) + slog.Info("ReadStat()", "filesystem", f.Filesystem, "path", f.Common.Path) info, err = f.Stat() - slog.Info("ReadStat()", "filesystem", f.Filesystem, "path", f.Path, "info", info, "error", err) + slog.Info("ReadStat()", "filesystem", f.Filesystem, "path", f.Common.Path, "info", info, "error", err) if err == nil { _ = f.SetFileInfo(info) @@ -621,9 +583,9 @@ func (f *File) ReadStat() (err error) { } - slog.Info("ReadStat()", "stat", info, "path", f.Path) + slog.Info("ReadStat()", "stat", info, "path", f.Common.Path) if err != nil { - f.State = "absent" + f.Common.State = "absent" return } @@ -631,27 +593,29 @@ func (f *File) ReadStat() (err error) { } func (f *File) open() (file io.ReadCloser, err error) { - slog.Info("open()", "file", f.Path, "fs", f.Filesystem) + slog.Info("open()", "file", f.Common.Path, "fs", f.Filesystem) if _, ok := f.Filesystem.(embed.FS); ok { - file, err = f.Filesystem.Open(f.Path) + file, err = f.Filesystem.Open(f.Common.Path) } else { - file, err = os.Open(f.Path) + file, err = os.Open(f.Common.Path) } if f.GzipContent && f.DetectGzip() { file, err = gzip.NewReader(file) } - slog.Info("open()", "file", f.Path, "error", err) + slog.Info("open()", "file", f.Common.Path, "error", err) return } func (f *File) Read(ctx context.Context) ([]byte, error) { + /* if normalizePathErr := f.NormalizePath(); normalizePathErr != nil { return nil, normalizePathErr } - + */ + statErr := f.ReadStat() if statErr != nil { - return nil, fmt.Errorf("%w - %w", ErrResourceStateAbsent, statErr) + return nil, fmt.Errorf("%w - %w: %s", ErrResourceStateAbsent, statErr, f.Path) } switch f.FileType { @@ -671,13 +635,13 @@ func (f *File) Read(ctx context.Context) ([]byte, error) { f.Sha256 = fmt.Sprintf("%x", sha256.Sum256(fileContent)) } case SymbolicLinkFile: - linkTarget, pathErr := os.Readlink(f.Path) + linkTarget, pathErr := os.Readlink(f.Common.Path) if pathErr != nil { return nil, pathErr } f.Target = linkTarget } - f.State = "present" + f.Common.State = "present" return yaml.Marshal(f) } diff --git a/internal/resource/file_test.go b/internal/resource/file_test.go index e844ee9..b4dc022 100644 --- a/internal/resource/file_test.go +++ b/internal/resource/file_test.go @@ -4,15 +4,15 @@ package resource import ( "context" - _ "encoding/json" +_ "encoding/json" "fmt" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v3" "io" - _ "log" - _ "net/http" - _ "net/http/httptest" - _ "net/url" +_ "log" +_ "net/http" +_ "net/http/httptest" +_ "net/url" "os" "path/filepath" "strings" @@ -23,6 +23,7 @@ import ( "io/fs" "decl/internal/codec" "decl/internal/data" + "decl/internal/folio" "log/slog" ) @@ -37,7 +38,8 @@ func TestNewFileNormalized(t *testing.T) { f := NewNormalizedFile() assert.NotNil(t, f) - assert.Nil(t, f.SetURI("file://" + indirectFile)) + f.Path = indirectFile + assert.Nil(t, f.Init(nil)) assert.NotEqual(t, indirectFile, f.Path) assert.Equal(t, absFilePath, f.Path) @@ -84,13 +86,21 @@ func TestReadFile(t *testing.T) { testFile := NewFile() e := testFile.LoadDecl(decl) assert.Nil(t, e) + + assert.Equal(t, "present", testFile.Common.State) + assert.Equal(t, file, testFile.Common.Path) + applyErr := testFile.Apply() assert.Nil(t, applyErr) + assert.FileExists(t, file) + f := NewFile() - assert.NotEqual(t, nil, f) + assert.NotNil(t, f) f.Path = file + assert.Nil(t, f.Init(nil)) + r, e := f.Read(ctx) assert.Nil(t, e) assert.Equal(t, ProcessTestUserName, f.Owner) @@ -118,7 +128,8 @@ func TestUseConfig(t *testing.T) { return nil, data.ErrUnknownConfigurationKey })) - assert.Nil(t, f.SetURI(fmt.Sprintf("file://%s", file))) + uri := folio.URI(fmt.Sprintf("file://%s", file)) + assert.Nil(t, f.Init(uri.Parse())) assert.Equal(t, filepath.Join("/tmp", file), f.FilePath()) } @@ -263,8 +274,8 @@ func TestFileSetURI(t *testing.T) { file, _ := filepath.Abs(TempDir.FilePath("testuri.txt")) f := NewFile() assert.NotNil(t, f) - e := f.SetURI("file://" + file) - assert.Nil(t, e) + uri := folio.URI("file://" + file).Parse() + assert.Nil(t, f.Init(uri)) assert.Equal(t, "file", f.Type()) assert.Equal(t, file, f.Path) } @@ -302,6 +313,7 @@ func TestFileUpdateAttributesFromFileInfo(t *testing.T) { func TestFileReadStat(t *testing.T) { ctx := context.Background() + link := TempDir.FilePath("link.txt") linkTargetFile := TempDir.FilePath("testuri.txt") @@ -309,8 +321,8 @@ func TestFileReadStat(t *testing.T) { assert.NotNil(t, f) f.Path = linkTargetFile - e := f.NormalizePath() - assert.Nil(t, e) + f.PathNormalization(true) + assert.Nil(t, f.Init(nil)) statErr := f.ReadStat() assert.Error(t, statErr) @@ -324,8 +336,9 @@ func TestFileReadStat(t *testing.T) { l := NewFile() assert.NotNil(t, l) + l.PathNormalization(true) + assert.Nil(t, l.Init(nil)) - assert.Nil(t, l.NormalizePath()) l.FileType = SymbolicLinkFile l.Path = link l.Target = linkTargetFile @@ -340,6 +353,9 @@ func TestFileReadStat(t *testing.T) { testRead := NewFile() testRead.Path = link + + assert.Nil(t, testRead.Init(nil)) + _,testReadErr := testRead.Read(ctx) assert.Nil(t, testReadErr) assert.Equal(t, linkTargetFile, testRead.Target) @@ -355,6 +371,8 @@ func TestFileResourceFileInfo(t *testing.T) { f.Mode = "0600" f.Content = "some test data" f.State = "present" + assert.Nil(t, f.Init(nil)) + assert.Nil(t, f.Apply()) _, readErr := f.Read(context.Background()) @@ -378,20 +396,45 @@ func TestFileClone(t *testing.T) { assert.NotNil(t, f) f.Path = testFile + assert.Nil(t, f.Init(nil)) f.Mode = "0600" f.State = "present" assert.Nil(t, f.Apply()) - + + origin := time.Now() + _,readErr := f.Read(ctx) assert.Nil(t, readErr) time.Sleep(100 * time.Millisecond) + assert.Greater(t, origin, f.Mtime) clone := f.Clone().(*File) - assert.Equal(t, f, clone) - clone.Mtime = time.Time{} + assert.Equal(t, f.Common.Path, clone.Common.Path) + assert.Equal(t, f.Common.absPath, clone.Common.absPath) + assert.Equal(t, f.Common.parsedURI, clone.Common.parsedURI) + assert.Equal(t, f.Common.exttype, clone.Common.exttype) + assert.Equal(t, f.Common.fileext, clone.Common.fileext) + assert.Equal(t, f.Common.State, clone.Common.State) + assert.Equal(t, f.Size, clone.Size) + assert.Equal(t, f.Owner, clone.Owner) + assert.Equal(t, f.Group, clone.Group) + assert.Equal(t, f.Mode, clone.Mode) + assert.Equal(t, f.Atime, clone.Atime) + assert.Equal(t, f.Mtime, clone.Mtime) + assert.Equal(t, f.Ctime, clone.Ctime) + assert.Equal(t, f.Content, clone.Content) + assert.Equal(t, f.Sha256, clone.Sha256) + + clone.Mtime = time.Now() clone.Path = testCloneFile + assert.Nil(t, clone.Init(nil)) + + assert.NotEqual(t, f.absPath, clone.absPath) + + slog.Info("TestFileClone", "clone", clone) assert.Nil(t, clone.Apply()) + slog.Info("TestFileClone - applied mtime change", "clone", clone) _,updateReadErr := f.Read(ctx) assert.Nil(t, updateReadErr) @@ -399,7 +442,8 @@ func TestFileClone(t *testing.T) { _, cloneReadErr := clone.Read(ctx) assert.Nil(t, cloneReadErr) - fmt.Printf("file %#v\nclone %#v\n", f, clone) + slog.Info("TestFileClone - read mtime change", "orig", f.Mtime, "clone", clone.Mtime) + fmt.Printf("file %#v\n %#v\nclone %#v\n %#v\n", f, f.Common, clone, clone.Common) assert.NotEqual(t, f.Mtime, clone.Mtime) } @@ -412,12 +456,16 @@ func TestFileErrors(t *testing.T) { stater := f.StateMachine() f.Path = testFile + assert.Nil(t, f.Init(nil)) f.Mode = "631" assert.Nil(t, stater.Trigger("create")) - + + assert.FileExists(t, f.Path) + read := NewFile() readStater := read.StateMachine() read.Path = testFile + assert.Nil(t, read.Init(nil)) assert.Nil(t, readStater.Trigger("read")) assert.Equal(t, FileMode("0631"), read.Mode) @@ -534,7 +582,8 @@ func TestFilePathURI(t *testing.T) { e := f.LoadDecl(decl) assert.Nil(t, e) assert.Equal(t, "", f.FilePath()) - assert.ErrorContains(t, f.Validate(), "path: String length must be greater than or equal to 1") +// assert.ErrorContains(t, f.Validate(), "path: String length must be greater than or equal to 1") + assert.ErrorContains(t, f.Validate(), "path is required") } func TestFileAbsent(t *testing.T) { @@ -593,8 +642,8 @@ func TestFileSetURIError(t *testing.T) { file := TempDir.FilePath("fooread.txt") f := NewFile() assert.NotNil(t, f) - e := f.SetURI("foo://" + file) - assert.NotNil(t, e) + uri := folio.URI("foo://" + file).Parse() + e := f.Init(uri) assert.ErrorIs(t, e, ErrInvalidResourceURI) } @@ -602,8 +651,8 @@ func TestFileContentType(t *testing.T) { file := TempDir.FilePath("fooread.txt") f := NewFile() assert.NotNil(t, f) - e := f.SetURI("file://" + file) - assert.Nil(t, e) + uri := folio.URI("file://" + file).Parse() + assert.Nil(t, f.Init(uri)) assert.Equal(t, "txt", f.ContentType()) } diff --git a/internal/resource/group.go b/internal/resource/group.go index 51203bc..fc7717e 100644 --- a/internal/resource/group.go +++ b/internal/resource/group.go @@ -48,7 +48,6 @@ type Group struct { ReadCommand *command.Command `json:"-" yaml:"-"` UpdateCommand *command.Command `json:"-" yaml:"-"` DeleteCommand *command.Command `json:"-" yaml:"-"` - //State string `json:"state,omitempty" yaml:"state,omitempty"` config data.ConfigurationValueGetter Resources data.ResourceMapper `json:"-" yaml:"-"` groupStatus *user.Group `json:"-" yaml:"-"` @@ -62,18 +61,14 @@ func NewGroup() (g *Group) { } func init() { - folio.DocumentRegistry.ResourceTypes.Register([]string{"group"}, func(u *url.URL) data.Resource { - group := NewGroup() - group.Name = u.Hostname() - group.GID = LookupGIDString(u.Hostname()) - if _, addGroupPathErr := exec.LookPath("addgroup"); addGroupPathErr == nil { - group.GroupType = GroupTypeAddGroup + folio.DocumentRegistry.ResourceTypes.Register([]string{"group"}, func(u *url.URL) (group data.Resource) { + group = NewGroup() + if u != nil { + if err := folio.CastParsedURI(u).ConstructResource(group); err != nil { + panic(err) + } } - if _, pathErr := exec.LookPath("groupadd"); pathErr == nil { - group.GroupType = GroupTypeGroupAdd - } - group.CreateCommand, group.ReadCommand, group.UpdateCommand, group.DeleteCommand = group.GroupType.NewCRUD() - return group + return }) } @@ -87,6 +82,25 @@ func FindSystemGroupType() GroupType { return GroupTypeAddGroup } +func (g *Group) Init(u data.URIParser) error { + var initializeURI folio.URI + if u == nil { + u = folio.URI(g.URI()).Parse() + } + uri := u.URL() + g.Name = uri.Hostname() + g.GID = LookupGIDString(uri.Hostname()) + if _, addGroupPathErr := exec.LookPath("addgroup"); addGroupPathErr == nil { + g.GroupType = GroupTypeAddGroup + } + if _, pathErr := exec.LookPath("groupadd"); pathErr == nil { + g.GroupType = GroupTypeGroupAdd + } + g.CreateCommand, g.ReadCommand, g.UpdateCommand, g.DeleteCommand = g.GroupType.NewCRUD() + initializeURI.SetURL(uri) + return g.SetURI(string(initializeURI)) +} + func (g *Group) NormalizePath() error { return nil } @@ -170,26 +184,10 @@ func (g *Group) Notify(m *machine.EventMessage) { } } -func (g *Group) SetURI(uri string) error { - resourceUri, e := url.Parse(uri) - if e == nil { - if resourceUri.Scheme == "group" { - g.Name = resourceUri.Hostname() - } else { - e = fmt.Errorf("%w: %s is not a group", ErrInvalidResourceURI, uri) - } - } - return e -} - func (g *Group) URI() string { return fmt.Sprintf("group://%s", g.Name) } -func (g *Group) UseConfig(config data.ConfigurationValueGetter) { - g.config = config -} - func (g *Group) ResolveId(ctx context.Context) string { return LookupUIDString(g.Name) } diff --git a/internal/resource/http.go b/internal/resource/http.go index 5d41aa0..4ae0f58 100644 --- a/internal/resource/http.go +++ b/internal/resource/http.go @@ -60,16 +60,11 @@ func init() { } func HTTPFactory(u *url.URL) data.Resource { - var err error h := NewHTTP() - - slog.Info("HTTP.Factory", "http", h, "url", u) - if err = h.SetParsedURI(u); err != nil { - panic(err) - } - - if err = h.Open(); err != nil { - panic(err) + if u != nil { + if err := folio.CastParsedURI(u).ConstructResource(h); err != nil { + panic(err) + } } return h } @@ -118,6 +113,16 @@ func (h *HTTP) SchemeCheck(scheme string) bool { return false } +func (h *HTTP) Init(u data.URIParser) (err error) { + if u == nil { + u = folio.URI(h.URI()).Parse() + } + if err = h.SetParsedURI(u); err == nil { + err = h.Open() + } + return +} + func (h *HTTP) NormalizePath() error { return nil } @@ -228,16 +233,9 @@ func (h *HTTP) URI() string { return h.Endpoint.String() } -func (h *HTTP) SetURI(uri string) (err error) { - if err = h.Common.SetURI(uri); err == nil { - h.Endpoint = h.Common.Uri - } - return -} - -func (h *HTTP) SetParsedURI(u *url.URL) (err error) { +func (h *HTTP) SetParsedURI(u data.URIParser) (err error) { if err = h.Common.SetParsedURI(u); err == nil { - h.Endpoint = h.Common.Uri + h.Endpoint = h.Common.URI() } return } @@ -343,14 +341,14 @@ func (h *HTTP) Apply() error { func (h *HTTP) Load(docData []byte, f codec.Format) (err error) { if err = f.StringDecoder(string(docData)).Decode(h); err == nil { - err = h.Common.SetURI(string(h.Endpoint)) + err = h.Common.SetParsedURI(folio.URI(h.Endpoint).Parse()) } return } func (h *HTTP) LoadReader(r io.ReadCloser, f codec.Format) (err error) { if err = f.Decoder(r).Decode(h); err == nil { - err = h.Common.SetURI(string(h.Endpoint)) + err = h.Common.SetParsedURI(folio.URI(h.Endpoint).Parse()) //err = h.setParsedURI(h.Endpoint) } return @@ -358,7 +356,7 @@ func (h *HTTP) LoadReader(r io.ReadCloser, f codec.Format) (err error) { func (h *HTTP) LoadString(docData string, f codec.Format) (err error) { if err = f.StringDecoder(docData).Decode(h); err == nil { - err = h.Common.SetURI(string(h.Endpoint)) + err = h.Common.SetParsedURI(folio.URI(h.Endpoint).Parse()) //err = h.setParsedURI(h.Endpoint) } return @@ -369,7 +367,7 @@ func (h *HTTP) LoadDecl(yamlResourceDeclaration string) error { } func (h *HTTP) ResolveId(ctx context.Context) string { - _ = h.Common.SetURI(h.Endpoint.String()) + _ = h.Common.SetParsedURI(folio.URI(h.Endpoint).Parse()) slog.Info("HTTP.ResolveId()", "uri", h.Endpoint.String()) return h.Endpoint.String() } diff --git a/internal/resource/iptables.go b/internal/resource/iptables.go index a8ecfba..9f2b140 100644 --- a/internal/resource/iptables.go +++ b/internal/resource/iptables.go @@ -30,20 +30,11 @@ const ( func init() { folio.DocumentRegistry.ResourceTypes.Register([]string{"iptable"}, func(u *url.URL) data.Resource { i := NewIptable() - i.Table = IptableName(u.Hostname()) - if len(u.Path) > 0 { - fields := strings.FieldsFunc(u.Path, func(c rune) bool { return c == '/' }) - slog.Info("iptables factory", "iptable", i, "uri", u, "fields", fields, "number_fields", len(fields)) - if len(fields) > 0 { - i.Chain = IptableChain(fields[0]) - if len(fields) < 3 { - i.ResourceType = IptableTypeChain - } else { - i.ResourceType = IptableTypeRule - id, _ := strconv.ParseUint(fields[1], 10, 32) - i.SetId(uint(id)) - } + if u != nil { + if err := folio.CastParsedURI(u).ConstructResource(i); err != nil { + panic(err) } + i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD() } return i @@ -168,6 +159,32 @@ func NewIptable() (i *Iptable) { return } +func (i *Iptable) Init(u data.URIParser) (err error) { + if u == nil { + u = folio.URI(i.URI()).Parse() + } + uri := u.URL() + err = i.SetParsedURI(u) + + i.Table = IptableName(uri.Hostname()) + if len(uri.Path) > 0 { + fields := strings.FieldsFunc(uri.Path, func(c rune) bool { return c == '/' }) + slog.Info("iptables factory", "iptable", i, "uri", uri, "fields", fields, "number_fields", len(fields)) + if len(fields) > 0 { + i.Chain = IptableChain(fields[0]) + if len(fields) < 3 { + i.ResourceType = IptableTypeChain + } else { + i.ResourceType = IptableTypeRule + id, _ := strconv.ParseUint(fields[1], 10, 32) + i.SetId(uint(id)) + } + } + i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD() + } + return +} + func (i *Iptable) SetResourceMapper(resources data.ResourceMapper) { i.Resources = resources } @@ -284,7 +301,7 @@ func (i *Iptable) URI() string { } -func (i *Iptable) SetParsedURI(uri *url.URL) (err error) { +func (i *Iptable) SetParsedURI(uri data.URIParser) (err error) { if err = i.Common.SetParsedURI(uri); err == nil { err = i.setFieldsFromPath() } @@ -312,14 +329,7 @@ func (i *Iptable) setFieldsFromPath() (err error) { i.Id = uint(id) } } else { - err = fmt.Errorf("%w: %s is not an iptable rule", ErrInvalidResourceURI, i.Common.Uri) - } - return -} - -func (i *Iptable) SetURI(uri string) (err error) { - if err = i.Common.SetURI(uri); err == nil { - err = i.setFieldsFromPath() + err = fmt.Errorf("%w: %s is not an iptable rule", ErrInvalidResourceURI, i.Common.URI()) } return } diff --git a/internal/resource/network_route.go b/internal/resource/network_route.go index ca4f017..b2670aa 100644 --- a/internal/resource/network_route.go +++ b/internal/resource/network_route.go @@ -26,52 +26,17 @@ const ( ) func init() { - folio.DocumentRegistry.ResourceTypes.Register([]string{"route"}, func(u *url.URL) data.Resource { - n := NewNetworkRoute() + folio.DocumentRegistry.ResourceTypes.Register([]string{"route"}, func(u *url.URL) (n data.Resource) { + n = NewNetworkRoute() + if u != nil { + if err := folio.CastParsedURI(u).ConstructResource(n); err != nil { + panic(err) + } + } return n }) } -/* - -ROUTE := NODE_SPEC [ INFO_SPEC ] -NODE_SPEC := [ TYPE ] PREFIX [ tos TOS ] - [ table TABLE_ID ] [ proto RTPROTO ] - [ scope SCOPE ] [ metric METRIC ] - [ ttl-propagate { enabled | disabled } ] -INFO_SPEC := { NH | nhid ID } OPTIONS FLAGS [ nexthop NH ]... - -NH := [ encap ENCAPTYPE ENCAPHDR ] [ via [ FAMILY ] ADDRESS ] - [ dev STRING ] [ weight NUMBER ] NHFLAGS -FAMILY := [ inet | inet6 | mpls | bridge | link ] -OPTIONS := FLAGS [ mtu NUMBER ] [ advmss NUMBER ] [ as [ to ] ADDRESS ] - [ rtt TIME ] [ rttvar TIME ] [ reordering NUMBER ] - [ window NUMBER ] [ cwnd NUMBER ] [ initcwnd NUMBER ] - [ ssthresh NUMBER ] [ realms REALM ] [ src ADDRESS ] - [ rto_min TIME ] [ hoplimit NUMBER ] [ initrwnd NUMBER ] - [ features FEATURES ] [ quickack BOOL ] [ congctl NAME ] - [ pref PREF ] [ expires TIME ] [ fastopen_no_cookie BOOL ] -NHFLAGS := [ onlink | pervasive ] -PREF := [ low | medium | high ] -TIME := NUMBER[s|ms] -BOOL := [1|0] -FEATURES := ecn -ENCAPTYPE := [ mpls | ip | ip6 | seg6 | seg6local | rpl | ioam6 ] -ENCAPHDR := [ MPLSLABEL | SEG6HDR | SEG6LOCAL | IOAM6HDR ] -SEG6HDR := [ mode SEGMODE ] segs ADDR1,ADDRi,ADDRn [hmac HMACKEYID] [cleanup] -SEGMODE := [ encap | inline ] -SEG6LOCAL := action ACTION [ OPTIONS ] [ count ] -ACTION := { End | End.X | End.T | End.DX2 | End.DX6 | End.DX4 | - End.DT6 | End.DT4 | End.DT46 | End.B6 | End.B6.Encaps | - End.BM | End.S | End.AS | End.AM | End.BPF } -OPTIONS := OPTION [ OPTIONS ] -OPTION := { srh SEG6HDR | nh4 ADDR | nh6 ADDR | iif DEV | oif DEV | - table TABLEID | vrftable TABLEID | endpoint PROGNAME } -IOAM6HDR := trace prealloc type IOAM6_TRACE_TYPE ns IOAM6_NAMESPACE size IOAM6_TRACE_SIZE -ROUTE_GET_FLAGS := [ fibmatch ] - -*/ - type NetworkRouteType string const ( @@ -115,25 +80,24 @@ const ( // Manage the state of network routes type NetworkRoute struct { - *Common `json:",inline" yaml:",inline"` - stater machine.Stater `json:"-" yaml:"-"` - Id string - To string `json:"to" yaml:"to"` - Interface string `json:"interface" yaml:"interface"` - Gateway string `json:"gateway" yaml:"gateway"` - Metric uint `json:"metric" yaml:"metric"` - Rtid NetworkRouteTableId `json:"rtid" yaml:"rtid"` - RouteType NetworkRouteType `json:"routetype" yaml:"routetype"` - Scope NetworkRouteScope `json:"scope" yaml:"scope"` - Proto NetworkRouteProto `json:"proto" yaml:"proto"` + *Common `json:",inline" yaml:",inline"` + stater machine.Stater `json:"-" yaml:"-"` + Id string + To string `json:"to" yaml:"to"` + Interface string `json:"interface" yaml:"interface"` + Gateway string `json:"gateway" yaml:"gateway"` + Metric uint `json:"metric" yaml:"metric"` + Rtid NetworkRouteTableId `json:"rtid" yaml:"rtid"` + RouteType NetworkRouteType `json:"routetype" yaml:"routetype"` + Scope NetworkRouteScope `json:"scope" yaml:"scope"` + Proto NetworkRouteProto `json:"proto" yaml:"proto"` - CreateCommand *command.Command `yaml:"-" json:"-"` - ReadCommand *command.Command `yaml:"-" json:"-"` - UpdateCommand *command.Command `yaml:"-" json:"-"` - DeleteCommand *command.Command `yaml:"-" json:"-"` + CreateCommand *command.Command `yaml:"-" json:"-"` + ReadCommand *command.Command `yaml:"-" json:"-"` + UpdateCommand *command.Command `yaml:"-" json:"-"` + DeleteCommand *command.Command `yaml:"-" json:"-"` - config data.ConfigurationValueGetter - Resources data.ResourceMapper `json:"-" yaml:"-"` + Resources data.ResourceMapper `json:"-" yaml:"-"` } func NewNetworkRoute() *NetworkRoute { @@ -144,6 +108,13 @@ func NewNetworkRoute() *NetworkRoute { return n } +func (n *NetworkRoute) Init(u data.URIParser) (err error) { + if u == nil { + u = folio.URI(n.URI()).Parse() + } + return n.SetParsedURI(u) +} + func (n *NetworkRoute) NormalizePath() error { return nil } @@ -217,14 +188,6 @@ func (n *NetworkRoute) URI() string { return fmt.Sprintf("route://%s", n.Id) } -func (n *NetworkRoute) SetURI(uri string) error { - return nil -} - -func (n *NetworkRoute) UseConfig(config data.ConfigurationValueGetter) { - n.config = config -} - func (n *NetworkRoute) Validate() error { return fmt.Errorf("failed") } diff --git a/internal/resource/package.go b/internal/resource/package.go index a249255..d3806c6 100644 --- a/internal/resource/package.go +++ b/internal/resource/package.go @@ -23,6 +23,9 @@ import ( "decl/internal/tempdir" ) +const ( + PackageTypeName TypeName = "package" +) var ( PackageTempDir tempdir.Path = "jx_package_resource" ) @@ -50,7 +53,8 @@ var SupportedPackageTypes []PackageType = []PackageType{PackageTypeApk, PackageT var SystemPackageType PackageType = FindSystemPackageType() type Package struct { - stater machine.Stater `yaml:"-" json:"-"` + *Common `yaml:",inline" json:",inline"` + stater machine.Stater `yaml:"-" json:"-"` Source string `yaml:"source,omitempty" json:"source,omitempty"` Name string `yaml:"name" json:"name"` Required string `json:"required,omitempty" yaml:"required,omitempty"` @@ -62,18 +66,12 @@ type Package struct { ReadCommand *command.Command `yaml:"-" json:"-"` UpdateCommand *command.Command `yaml:"-" json:"-"` DeleteCommand *command.Command `yaml:"-" json:"-"` - // state attributes - State string `yaml:"state,omitempty" json:"state,omitempty"` - config data.ConfigurationValueGetter Resources data.ResourceMapper `yaml:"-" json:"-"` } func init() { folio.DocumentRegistry.ResourceTypes.Register([]string{"package", string(PackageTypeApk), string(PackageTypeApt), string(PackageTypeDeb), string(PackageTypeDnf), string(PackageTypeRpm), string(PackageTypePip), string(PackageTypeYum)}, func(u *url.URL) data.Resource { - p := NewPackage() - e := p.SetParsedURI(u) - slog.Info("PackageFactory SetParsedURI()", "error", e) - return p + return ConstructNewPackage(u) }) } @@ -88,7 +86,41 @@ func FindSystemPackageType() PackageType { } func NewPackage() *Package { - return &Package{ PackageType: SystemPackageType } + return &Package{ + Common: NewCommon(PackageTypeName, true), + PackageType: SystemPackageType, + } +} + +func ConstructNewPackage(uri *url.URL) (p *Package) { + p = NewPackage() + if uri != nil { + if err := folio.CastParsedURI(uri).ConstructResource(p); err != nil { + panic(err) + } + } + return +} + +func (p *Package) Init(u data.URIParser) error { + if u == nil { + u = folio.URI(p.URI()).Parse() + } + uri := u.URL() + + p.Name = filepath.Join(uri.Hostname(), uri.Path) + p.Version = uri.Query().Get("version") + if p.Version == "" { + p.Version = "latest" + } + indicatedPackageType := PackageType(uri.Query().Get("type")) + if indicatedPackageType.Validate() != nil { + p.PackageType = SystemPackageType + } + + p.CreateCommand, p.ReadCommand, p.UpdateCommand, p.DeleteCommand = p.PackageType.NewCRUD() + + return p.SetParsedURI(u) } func (p *Package) SetResourceMapper(resources data.ResourceMapper) { @@ -97,11 +129,11 @@ func (p *Package) SetResourceMapper(resources data.ResourceMapper) { func (p *Package) Clone() data.Resource { newp := &Package { + Common: p.Common.Clone(), Name: p.Name, Required: p.Required, Version: p.Version, PackageType: p.PackageType, - State: p.State, } newp.CreateCommand, newp.ReadCommand, newp.UpdateCommand, newp.DeleteCommand = newp.PackageType.NewCRUD() return newp @@ -135,11 +167,11 @@ func (p *Package) Notify(m *machine.EventMessage) { if triggerErr := p.StateMachine().Trigger("state_read"); triggerErr == nil { return } else { - p.State = "absent" + p.Common.State = "absent" panic(triggerErr) } } else { - p.State = "absent" + p.Common.State = "absent" if ! errors.Is(readErr, ErrResourceStateAbsent) { panic(readErr) } @@ -164,17 +196,17 @@ func (p *Package) Notify(m *machine.EventMessage) { if triggerErr := p.StateMachine().Trigger("deleted"); triggerErr == nil { return } else { - p.State = "present" + p.Common.State = "present" panic(triggerErr) } } else { - p.State = "present" + p.Common.State = "present" panic(deleteErr) } case "absent": - p.State = "absent" + p.Common.State = "absent" case "present", "created", "updated", "read": - p.State = "present" + p.Common.State = "present" } case machine.EXITSTATEEVENT: } @@ -192,36 +224,25 @@ func (p *Package) URI() string { } -func (p *Package) SetURI(uri string) error { - resourceUri, e := url.Parse(uri) - if e == nil { - e = p.SetParsedURI(resourceUri) - } - return e -} - -func (p *Package) SetParsedURI(uri *url.URL) (err error) { - if uri.Scheme == "package" { - p.Name = filepath.Join(uri.Hostname(), uri.Path) - p.Version = uri.Query().Get("version") +func (p *Package) SetParsedURI(uri data.URIParser) (err error) { + u := uri.URL() + if u.Scheme == "package" { + p.Name = filepath.Join(u.Hostname(), u.Path) + p.Version = u.Query().Get("version") if p.Version == "" { p.Version = "latest" } - indicatedPackageType := PackageType(uri.Query().Get("type")) + indicatedPackageType := PackageType(u.Query().Get("type")) if indicatedPackageType.Validate() != nil { p.PackageType = SystemPackageType } } else { - err = fmt.Errorf("%w: %s is not a package resource ", ErrInvalidResourceURI, uri.String()) + err = fmt.Errorf("%w: %s is not a package resource ", ErrInvalidResourceURI, u.String()) } p.CreateCommand, p.ReadCommand, p.UpdateCommand, p.DeleteCommand = p.PackageType.NewCRUD() return } -func (p *Package) UseConfig(config data.ConfigurationValueGetter) { - p.config = config -} - func (p *Package) JSON() ([]byte, error) { return json.Marshal(p) } @@ -331,6 +352,9 @@ func (p *Package) LoadDecl(yamlResourceDeclaration string) error { func (p *Package) Type() string { return "package" } func (p *Package) Read(ctx context.Context) (resourceYaml []byte, err error) { + if p.Version == "latest" { + p.Version = "" + } if p.ReadCommand.Exists() { var out []byte out, err = p.ReadCommand.Execute(p) @@ -495,10 +519,10 @@ func NewApkReadCommand() *command.Command { if packageName == p.Name { p.Name = packageName p.Version = packageVersion - p.State = "present" + p.Common.State = "present" } else { slog.Info("NewApkReadCommand().Extrctor() mismatch", "name", p.Name, "parsed", packageName) - p.State = "absent" + p.Common.State = "absent" } return nil } @@ -554,7 +578,7 @@ func NewApkReadPackagesCommand() *command.Command { packageName := strings.Join(packageFields[0:numberOfFields - 2], "-") packageVersion := strings.Join(packageFields[numberOfFields - 2:numberOfFields - 1], "-") p.Name = packageName - p.State = "present" + p.Common.State = "present" p.Version = packageVersion } } @@ -595,16 +619,16 @@ func NewAptReadCommand() *command.Command { switch key { case "Package": if value != p.Name { - p.State = "absent" + p.Common.State = "absent" return nil } case "Status": statusFields := strings.SplitN(value, " ", 3) if len(statusFields) > 1 { if statusFields[2] == "installed" { - p.State = "present" + p.Common.State = "present" } else { - p.State = "absent" + p.Common.State = "absent" } } case "Version": @@ -671,7 +695,7 @@ func NewAptReadPackagesCommand() *command.Command { packageName := packageFields[0] packageVersion := installedPackage[1] p.Name = packageName - p.State = "present" + p.Common.State = "present" p.Version = packageVersion p.PackageType = PackageTypeApt } @@ -711,16 +735,16 @@ func NewDebReadCommand() *command.Command { switch key { case "Package": if value != p.Name { - p.State = "absent" + p.Common.State = "absent" return nil } case "Status": statusFields := strings.SplitN(value, " ", 3) if len(statusFields) > 1 { if statusFields[2] == "installed" { - p.State = "present" + p.Common.State = "present" } else { - p.State = "absent" + p.Common.State = "absent" } } case "Version": @@ -787,7 +811,7 @@ func NewDebReadPackagesCommand() *command.Command { p.Version = packageVersionFields[0] } p.Name = packageName - p.State = "present" + p.Common.State = "present" p.PackageType = PackageTypeDeb lineIndex++ } @@ -833,7 +857,7 @@ func NewDnfReadCommand() *command.Command { packageName := strings.TrimSpace(strings.Join(packageNameField[0:lenName - 1], ".")) if packageName == p.Name { - p.State = "present" + p.Common.State = "present" packageVersionField := strings.Split(fields[1], ":") if len(packageVersionField) > 1 { //packageEpoch := strings.TrimSpace(packageVersionField[0]) @@ -846,7 +870,7 @@ func NewDnfReadCommand() *command.Command { slog.Info("DnfReadCommaond.Extract()", "package", packageName, "package", p) } } - p.State = "absent" + p.Common.State = "absent" slog.Info("Extract()", "package", p) return nil } @@ -907,7 +931,7 @@ func NewDnfReadPackagesCommand() *command.Command { p.Version = packageVersionFields[0] } p.Name = packageName - p.State = "present" + p.Common.State = "present" p.PackageType = PackageTypeDnf lineIndex++ } @@ -949,13 +973,13 @@ func NewRpmReadCommand() *command.Command { packageVersion := strings.Join(packageFields[numberOfFields - 2:numberOfFields - 1], "-") slog.Info("Package[RPM].Extract()", "name", packageName, "version", packageVersion, "package", p) if packageName == p.Name { - p.State = "present" + p.Common.State = "present" p.Version = packageVersion return nil } } } - p.State = "absent" + p.Common.State = "absent" slog.Info("Extract()", "package", p) return nil } @@ -1013,13 +1037,13 @@ func NewPipReadCommand() *command.Command { packageName := packageFields[0] packageVersion := packageFields[1] if packageName == p.Name { - p.State = "present" + p.Common.State = "present" p.Version = packageVersion return nil } } } - p.State = "absent" + p.Common.State = "absent" slog.Info("Extract()", "package", p) return nil } @@ -1083,7 +1107,7 @@ func NewYumReadCommand() *command.Command { //packageArch := strings.TrimSpace(packageNameField[1]) if packageName == p.Name { - p.State = "present" + p.Common.State = "present" packageVersionField := strings.Split(fields[1], ":") //packageEpoch := strings.TrimSpace(packageVersionField[0]) packageVersion := strings.TrimSpace(packageVersionField[1]) @@ -1091,7 +1115,7 @@ func NewYumReadCommand() *command.Command { return nil } } - p.State = "absent" + p.Common.State = "absent" slog.Info("Extract()", "package", p) return nil } diff --git a/internal/resource/package_test.go b/internal/resource/package_test.go index de41390..468519d 100644 --- a/internal/resource/package_test.go +++ b/internal/resource/package_test.go @@ -4,19 +4,12 @@ package resource import ( "context" -_ "encoding/json" "fmt" "github.com/stretchr/testify/assert" -_ "gopkg.in/yaml.v3" -_ "io" "log/slog" -_ "net/http" -_ "net/http/httptest" -_ "net/url" -_ "os" -_ "strings" "testing" "decl/internal/command" + "decl/internal/folio" ) func TestNewPackageResource(t *testing.T) { @@ -354,6 +347,8 @@ type: %s assert.NotNil(t, p) loadErr := p.LoadDecl(decl) assert.Nil(t, loadErr) + assert.Nil(t, p.Init(nil)) + p.ReadCommand = SystemPackageType.NewReadCommand() /* p.ReadCommand.Executor = func(value any) ([]byte, error) { @@ -363,6 +358,7 @@ type: %s p.ResolveId(ctx) yaml, readErr := p.Read(ctx) assert.ErrorIs(t, readErr, ErrResourceStateAbsent) + slog.Info("TestReadPackageError", "package", p, "common", p.Common, "yaml", yaml) assert.YAMLEq(t, expected, string(yaml)) slog.Info("read()", "yaml", yaml) assert.Equal(t, "", p.Version) @@ -375,7 +371,9 @@ func TestCreatePackage(t *testing.T) { func TestPackageSetURI(t *testing.T) { p := NewPackage() assert.NotNil(t, p) - e := p.SetURI("package://" + "12345_key?type=apk") + + uri := folio.URI("package://" + "12345_key?type=apk").Parse() + e := p.Init(uri) assert.Nil(t, e) assert.Equal(t, "package", p.Type()) assert.Equal(t, "12345_key", p.Name) diff --git a/internal/resource/pki.go b/internal/resource/pki.go index 479223b..0d53465 100644 --- a/internal/resource/pki.go +++ b/internal/resource/pki.go @@ -44,11 +44,16 @@ var ErrPKIInvalidEncodingType error = errors.New("Invalid EncodingType") var ErrPKIFailedDecodingPemBlock error = errors.New("Failed decoding pem block") func init() { - ResourceTypes.Register([]string{"pki"}, func(u *url.URL) data.Resource { + ResourceTypes.Register([]string{"pki"}, func(u *url.URL) (data.Resource) { k := NewPKI() - ref := folio.ResourceReference(filepath.Join(u.Hostname(), u.Path)) - if len(ref) > 0 { - k.PrivateKeyRef = ref + if u != nil { + if err := folio.CastParsedURI(u).ConstructResource(k); err != nil { + panic(err) + } + ref := folio.ResourceReference(filepath.Join(u.Hostname(), u.Path)) + if len(ref) > 0 { + k.PrivateKeyRef = ref + } } return k }) @@ -83,8 +88,6 @@ type PKI struct { Bits int `json:"bits" yaml:"bits"` EncodingType EncodingType `json:"type" yaml:"type"` - //State string `json:"state,omitempty" yaml:"state,omitempty"` - config data.ConfigurationValueGetter Resources data.ResourceMapper `json:"-" yaml:"-"` } @@ -102,7 +105,6 @@ func (k *PKI) Clone() data.Resource { return &PKI { Common: k.Common.Clone(), EncodingType: k.EncodingType, - //State: k.State, } } @@ -185,22 +187,6 @@ func (k *PKI) URI() string { return fmt.Sprintf("pki://%s", filepath.Join(u.Hostname(), u.RequestURI())) } -func (k *PKI) SetURI(uri string) error { - resourceUri, e := url.Parse(uri) - if e == nil { - if resourceUri.Scheme == "pki" { - k.PrivateKeyRef = folio.ResourceReference(fmt.Sprintf("pki://%s", filepath.Join(resourceUri.Hostname(), resourceUri.Path))) - } else { - e = fmt.Errorf("%w: %s is not a cert", ErrInvalidResourceURI, uri) - } - } - return e -} - -func (k *PKI) UseConfig(config data.ConfigurationValueGetter) { - k.config = config -} - func (k *PKI) Validate() error { return fmt.Errorf("failed") } @@ -494,62 +480,6 @@ func (k *PKI) Update(ctx context.Context) (err error) { func (k *PKI) Type() string { return "pki" } -/* -func (k *PKI) UnmarshalValue(value *PKIContent) error { - d.Type = value.Type - d.Transition = value.Transition - d.Config = value.Config - newResource, resourceErr := ResourceTypes.New(fmt.Sprintf("%s://", value.Type)) - if resourceErr != nil { - slog.Info("Declaration.UnmarshalValue", "value", value, "error", resourceErr) - return resourceErr - } - d.Attributes = newResource - return nil -} - -func (k *PKI) UnmarshalYAML(value *yaml.Node) error { - t := &DeclarationType{} - if unmarshalResourceTypeErr := value.Decode(t); unmarshalResourceTypeErr != nil { - return unmarshalResourceTypeErr - } - - if err := d.UnmarshalValue(t); err != nil { - return err - } - - resourceAttrs := struct { - Attributes yaml.Node `json:"attributes"` - }{} - if unmarshalAttributesErr := value.Decode(&resourceAttrs); unmarshalAttributesErr != nil { - return unmarshalAttributesErr - } - if unmarshalResourceErr := resourceAttrs.Attributes.Decode(d.Attributes); unmarshalResourceErr != nil { - return unmarshalResourceErr - } - return nil -} - -func (d *Declaration) UnmarshalJSON(data []byte) error { - t := &DeclarationType{} - if unmarshalResourceTypeErr := json.Unmarshal(data, t); unmarshalResourceTypeErr != nil { - return unmarshalResourceTypeErr - } - - if err := d.UnmarshalValue(t); err != nil { - return err - } - - resourceAttrs := struct { - Attributes Resource `json:"attributes"` - }{Attributes: d.Attributes} - if unmarshalAttributesErr := json.Unmarshal(data, &resourceAttrs); unmarshalAttributesErr != nil { - return unmarshalAttributesErr - } -return nil -} -*/ - func (t *EncodingType) UnmarshalValue(value string) error { switch value { case string(EncodingTypePem): diff --git a/internal/resource/pki_test.go b/internal/resource/pki_test.go index 96a1970..946b35e 100644 --- a/internal/resource/pki_test.go +++ b/internal/resource/pki_test.go @@ -4,12 +4,12 @@ package resource import ( "context" -_ "encoding/json" +_ "encoding/json" "fmt" "github.com/stretchr/testify/assert" _ "gopkg.in/yaml.v3" "io" -_ "log" +_ "log" _ "os" "decl/internal/transport" "decl/internal/ext" @@ -19,7 +19,6 @@ _ "os" "strings" "testing" "path/filepath" - "net/url" ) type TestResourceMapper func(key string) (data.Declaration, bool) @@ -59,7 +58,7 @@ func (s StringContentReadWriter) LoadString(docData string, f codec.Format) (err func (s StringContentReadWriter) LoadDecl(yamlResourceDeclaration string) error { return nil } func (s StringContentReadWriter) ResolveId(ctx context.Context) (string) { return "" } func (s StringContentReadWriter) SetURI(uri string) (error) { return nil } -func (s StringContentReadWriter) SetParsedURI(uri *url.URL) (error) { return nil } +func (s StringContentReadWriter) SetParsedURI(uri data.URIParser) (error) { return nil } func (s StringContentReadWriter) URI() (string) { return "" } func (s StringContentReadWriter) Validate() (error) { return nil } func (s StringContentReadWriter) ResourceType() data.TypeName { return "" } diff --git a/internal/resource/resource.go b/internal/resource/resource.go index 7b14668..6096271 100644 --- a/internal/resource/resource.go +++ b/internal/resource/resource.go @@ -20,67 +20,6 @@ var ( ) type ResourceReference string -/* -type ResourceSelector func(r *Declaration) bool - -type Resource interface { - Type() string - StateMachine() machine.Stater - URI() string - SetURI(string) error - UseConfig(config ConfigurationValueGetter) - ResolveId(context.Context) string - ResourceLoader - StateTransformer - ResourceReader - ResourceValidator - Clone() Resource - SetResourceMapper(resources ResourceMapper) -} - -type ContentReader interface { - ContentReaderStream() (*transport.Reader, error) -} - -type ContentWriter interface { - ContentWriterStream() (*transport.Writer, error) -} - -type ContentReadWriter interface { - ContentReader - ContentWriter -} - -type ResourceValidator interface { - Validate() error -} - -type ResourceCreator interface { - Create(context.Context) error -} - -type ResourceReader interface { - Read(context.Context) ([]byte, error) -} - -type ResourceUpdater interface { - Update(context.Context) error -} - -type ResourceDeleter interface { - Delete(context.Context) error -} - -type ResourceDecoder struct { -} - -type ResourceCrudder struct { - ResourceCreator - ResourceReader - ResourceUpdater - ResourceDeleter -} -*/ func NewResource(uri string) data.Resource { r, e := ResourceTypes.New(uri) @@ -90,50 +29,17 @@ func NewResource(uri string) data.Resource { return nil } -/* - -// Return a Content ReadWriter for the resource referred to. -func (r ResourceReference) Lookup(look data.ResourceMapper) data.ContentReadWriter { - slog.Info("ResourceReference.Lookup()", "resourcereference", r, "resourcemapper", look) - if look != nil { - if v,ok := look.Get(string(r)); ok { - return v.(data.ContentReadWriter) +func ResourceConstructor(res data.Resource, uri data.URIParser) (err error) { + if uri != nil { + return uri.ConstructResource(res) + } else { + if ri, ok := res.(data.ResourceInitializer); ok { + return ri.Init(uri) } } - return r + return } -func (r ResourceReference) Dereference(look data.ResourceMapper) data.Resource { - slog.Info("ResourceReference.Dereference()", "resourcereference", r, "resourcemapper", look) - if look != nil { - if v,ok := look.Get(string(r)); ok { - return v.(*Declaration).Attributes - } - } - return nil -} - -func (r ResourceReference) Parse() *url.URL { - u, e := url.Parse(string(r)) - if e == nil { - return u - } - return nil -} - -func (r ResourceReference) Exists() bool { - return transport.ExistsURI(string(r)) -} - -func (r ResourceReference) ContentReaderStream() (*transport.Reader, error) { - return transport.NewReaderURI(string(r)) -} - -func (r ResourceReference) ContentWriterStream() (*transport.Writer, error) { - return transport.NewWriterURI(string(r)) -} -*/ - func StorageMachine(sub machine.Subscriber) machine.Stater { // start_destroy -> absent -> start_create -> present -> start_destroy stater := machine.New("unknown") diff --git a/internal/resource/schema_test.go b/internal/resource/schema_test.go index 988a2d7..32abe44 100644 --- a/internal/resource/schema_test.go +++ b/internal/resource/schema_test.go @@ -58,6 +58,8 @@ func TestSchemaValidateJSON(t *testing.T) { testFile := NewFile() e := testFile.LoadDecl(decl) assert.Nil(t, e) + assert.Equal(t, file, testFile.ResolveId(ctx)) + fileApplyErr := testFile.Apply() assert.Nil(t, fileApplyErr) @@ -71,8 +73,10 @@ func TestSchemaValidateJSON(t *testing.T) { assert.NotNil(t, f) f.Path = file + assert.Nil(t, f.Init(nil)) r, e := f.Read(ctx) assert.Nil(t, e) + assert.Equal(t, ProcessTestUserName, f.Owner) info, statErr := os.Stat(file) @@ -86,8 +90,8 @@ func TestSchemaValidateJSON(t *testing.T) { } func TestSchemaValidateSchema(t *testing.T) { - s := NewSchema("document") - assert.NotNil(t, s) + s := NewSchema("document") + assert.NotNil(t, s) assert.Nil(t, s.ValidateSchema()) } diff --git a/internal/resource/service.go b/internal/resource/service.go index edf529d..20e9ecc 100644 --- a/internal/resource/service.go +++ b/internal/resource/service.go @@ -15,6 +15,7 @@ import ( "decl/internal/codec" "decl/internal/data" "decl/internal/command" + "decl/internal/folio" "encoding/json" "strings" "errors" @@ -64,8 +65,9 @@ type Service struct { func init() { ResourceTypes.Register([]string{"service"}, func(u *url.URL) data.Resource { s := NewService() - s.Name = filepath.Join(u.Hostname(), u.Path) - s.CreateCommand, s.ReadCommand, s.UpdateCommand, s.DeleteCommand = s.ServiceManagerType.NewCRUD() + if err := folio.CastParsedURI(u).ConstructResource(s); err != nil { + panic(err) + } return s }) } @@ -87,6 +89,17 @@ func NewService() (s *Service) { return } +func (s *Service) Init(u data.URIParser) (err error) { + if u == nil { + u = folio.URI(s.URI()).Parse() + } + uri := u.URL() + err = s.SetParsedURI(u) + s.Name = filepath.Join(uri.Hostname(), uri.Path) + s.CreateCommand, s.ReadCommand, s.UpdateCommand, s.DeleteCommand = s.ServiceManagerType.NewCRUD() + return +} + func (s *Service) NormalizePath() error { return nil } @@ -147,7 +160,7 @@ func (s *Service) URI() string { return fmt.Sprintf("service://%s", s.Name) } -func (s *Service) SetParsedURI(uri *url.URL) (err error) { +func (s *Service) SetParsedURI(uri data.URIParser) (err error) { if err = s.Common.SetParsedURI(uri); err == nil { err = s.setFieldsFromPath() } @@ -158,14 +171,7 @@ func (s *Service) setFieldsFromPath() (err error) { if len(s.Common.Path) > 0 { s.Name = s.Common.Path } else { - err = fmt.Errorf("%w: %s is not an iptable rule", ErrInvalidResourceURI, s.Common.Uri) - } - return -} - -func (s *Service) SetURI(uri string) (err error) { - if err = s.Common.SetURI(uri); err == nil { - err = s.setFieldsFromPath() + err = fmt.Errorf("%w: %s is not an iptable rule", ErrInvalidResourceURI, s.Common.URI()) } return } diff --git a/internal/resource/service_test.go b/internal/resource/service_test.go index 7e6c6b7..2a9ea50 100644 --- a/internal/resource/service_test.go +++ b/internal/resource/service_test.go @@ -4,11 +4,10 @@ package resource import ( "context" -_ "decl/tests/mocks" "decl/internal/command" -_ "fmt" "github.com/stretchr/testify/assert" "testing" + "decl/internal/folio" ) func TestNewServiceResource(t *testing.T) { @@ -18,7 +17,9 @@ func TestNewServiceResource(t *testing.T) { func TestUriServiceResource(t *testing.T) { c := NewService() - assert.Nil(t, c.SetURI("service://ssh")) + uri := folio.URI("service://ssh").Parse() + + assert.Nil(t, c.Init(uri)) assert.Equal(t, "ssh", c.Name) } diff --git a/internal/resource/user.go b/internal/resource/user.go index 206daf0..48b46b5 100644 --- a/internal/resource/user.go +++ b/internal/resource/user.go @@ -72,12 +72,14 @@ func NewUser() *User { } func init() { - folio.DocumentRegistry.ResourceTypes.Register([]string{"user"}, func(u *url.URL) data.Resource { - user := NewUser() - user.Name = u.Hostname() - user.UID = LookupUIDString(u.Hostname()) - user.CreateCommand, user.ReadCommand, user.UpdateCommand, user.DeleteCommand = user.UserType.NewCRUD() - return user + folio.DocumentRegistry.ResourceTypes.Register([]string{"user"}, func(u *url.URL) (user data.Resource) { + user = NewUser() + if u != nil { + if err := folio.CastParsedURI(u).ConstructResource(user); err != nil { + panic(err) + } + } + return }) } @@ -91,6 +93,18 @@ func FindSystemUserType() UserType { return UserTypeShadow } +func (u *User) Init(uri data.URIParser) error { + var initializeURI folio.URI + if uri == nil { + uri = folio.URI(u.URI()).Parse() + } + initializeURI.SetURL(uri.URL()) + u.Name = uri.URL().Hostname() + u.UID = LookupUIDString(uri.URL().Hostname()) + u.CreateCommand, u.ReadCommand, u.UpdateCommand, u.DeleteCommand = u.UserType.NewCRUD() + return u.SetURI(string(initializeURI)) +} + func (u *User) NormalizePath() error { return nil } @@ -189,26 +203,10 @@ func (u *User) Notify(m *machine.EventMessage) { } } -func (u *User) SetURI(uri string) error { - resourceUri, e := url.Parse(uri) - if e == nil { - if resourceUri.Scheme == "user" { - u.Name = resourceUri.Hostname() - } else { - e = fmt.Errorf("%w: %s is not a user", ErrInvalidResourceURI, uri) - } - } - return e -} - func (u *User) URI() string { return fmt.Sprintf("user://%s", u.Name) } -func (u *User) UseConfig(config data.ConfigurationValueGetter) { - u.config = config -} - func (u *User) ResolveId(ctx context.Context) string { if u.config != nil { if configUser, configUserErr := u.config.GetValue("user"); configUserErr == nil && u.Name == "self" { diff --git a/internal/types/types.go b/internal/types/types.go index e3f042d..345475e 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -105,6 +105,19 @@ func (t *Types[Product]) NewFromParsedURI(u *url.URL) (result Product, err error return } +// Returns an uninitialized resource for the given type +func (t *Types[Product]) NewFromType(typename string) (result Product, err error) { + if r, ok := t.registry[typename]; ok { + if result = r(nil); result != any(nil) { + return result, nil + } else { + return result, fmt.Errorf("%w: factory failed creating %s", ErrInvalidProduct, typename) + } + } + err = fmt.Errorf("%w: %s", ErrUnknownType, typename) + return +} + func (t *Types[Product]) Has(typename string) bool { if _, ok := t.registry[typename]; ok { return true diff --git a/internal/types/types_test.go b/internal/types/types_test.go index c457a12..95d58c6 100644 --- a/internal/types/types_test.go +++ b/internal/types/types_test.go @@ -61,3 +61,17 @@ func TestTypeName(t *testing.T) { assert.Equal(t, "file", string(fTypeName.Name)) } */ + +func TestNewFromType(t *testing.T) { + testTypes := New[string]() + assert.NotNil(t, testTypes) + + testTypes.Register([]string{"foo"}, func(*url.URL) string { return "test" }) + + r, e := testTypes.NewFromType("foo") + assert.Nil(t, e) + assert.Equal(t, "test", r) + failr, faile := testTypes.NewFromType("bar") + assert.ErrorIs(t, faile, ErrUnknownType) + assert.Equal(t, "", failr) +}