diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 8733945..86cf4f0 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -1,6 +1,7 @@ package main import ( + "errors" "testing" "github.com/onsi/gomega" @@ -126,3 +127,83 @@ func TestIncidentSelector(t *testing.T) { selector = scope.incidentSelector() g.Expect("(!package||package=a||package=b) && !(package=C||package=D)").To(gomega.Equal(selector)) } + +func TestInjectorDefaults(t *testing.T) { + g := gomega.NewGomegaWithT(t) + inj := ResourceInjector{dict: make(map[string]any)} + r := &Resource{ + Fields: []Field{ + { + Name: "Name", + Key: "person.name", + Default: "Elmer", + }, + { + Name: "Age", + Key: "person.age", + }, + }, + } + err := inj.addDefaults(r) + g.Expect(err).To(gomega.BeNil()) + g.Expect(inj.dict[r.Fields[0].Key]).To(gomega.Equal(r.Fields[0].Default)) + g.Expect(inj.dict[r.Fields[1].Key]).To(gomega.BeNil()) +} + +func TestInjectorTypeCast(t *testing.T) { + g := gomega.NewGomegaWithT(t) + inj := ResourceInjector{dict: make(map[string]any)} + r := &Resource{ + Fields: []Field{ + { + Name: "Name", + Key: "person.name", + Default: "Elmer", + }, + { + Name: "Age", + Key: "person.age", + Type: "integer", + Default: "18", + }, + { + Name: "Resident", + Key: "person.resident", + Type: "boolean", + Default: "true", + }, + { + Name: "Member", + Key: "person.member", + Type: "boolean", + Default: 1, + }, + { + Name: "One", + Key: "person.one", + Type: "integer", + Default: true, + }, + }, + } + err := inj.addDefaults(r) + g.Expect(err).To(gomega.BeNil()) + g.Expect(inj.dict[r.Fields[0].Key]).To(gomega.Equal(r.Fields[0].Default)) + g.Expect(inj.dict[r.Fields[1].Key]).To(gomega.Equal(18)) + g.Expect(inj.dict[r.Fields[2].Key]).To(gomega.BeTrue()) + g.Expect(inj.dict[r.Fields[3].Key]).To(gomega.BeTrue()) + g.Expect(inj.dict[r.Fields[4].Key]).To(gomega.Equal(1)) + + // cast error. + inj.dict = make(map[string]any) + r.Fields = append( + r.Fields, + Field{ + Name: "Resident", + Key: "person.parent", + Type: "integer", + Default: "true", + }) + err = inj.addDefaults(r) + g.Expect(errors.Is(err, &TypeError{})).To(gomega.BeTrue()) +} diff --git a/cmd/injector.go b/cmd/injector.go index 819d39a..faf17ae 100644 --- a/cmd/injector.go +++ b/cmd/injector.go @@ -7,6 +7,7 @@ import ( "os" pathlib "path" "regexp" + "strconv" "strings" "github.com/konveyor/analyzer-lsp/provider" @@ -14,6 +15,7 @@ import ( "github.com/konveyor/tackle2-hub/nas" ) +// KeyRegex $(variable) var ( KeyRegex = regexp.MustCompile(`(\$\()([^)]+)(\))`) ) @@ -49,11 +51,92 @@ func (e *FieldNotMatched) Is(err error) (matched bool) { return } +// TypeError used to report resource field cast error. +type TypeError struct { + Field *Field + Reason string + Object any +} + +func (e *TypeError) Error() (s string) { + return fmt.Sprintf( + "Resource injector: cast failed. field=%s type=%s reason=%s, object:%v", + e.Field.Name, + e.Field.Type, + e.Reason, + e.Object) +} + +func (e *TypeError) Is(err error) (matched bool) { + var inst *TypeError + matched = errors.As(err, &inst) + return +} + // Field injection specification. type Field struct { - Name string `json:"name"` - Path string `json:"path"` - Key string `json:"key"` + Name string `json:"name"` + Path string `json:"path"` + Key string `json:"key"` + Type string `json:"type"` + Default any `json:"default"` +} + +// cast returns object cast as defined by Field.Type. +func (f *Field) cast(object any) (cast any, err error) { + cast = object + if f.Type == "" { + return + } + defer func() { + if err != nil { + err = &TypeError{ + Field: f, + Reason: err.Error(), + Object: object, + } + } + }() + switch strings.ToLower(f.Type) { + case "string": + cast = fmt.Sprintf("%v", object) + case "integer": + switch x := object.(type) { + case int, + int8, + int16, + int32, + int64: + cast = x + case bool: + cast = 0 + if x { + cast = 1 + } + case string: + cast, err = strconv.Atoi(x) + default: + err = errors.New("expected: integer|boolean|string") + } + case "boolean": + switch x := object.(type) { + case bool: + cast = x + case int, + int8, + int16, + int32, + int64: + cast = x != 0 + case string: + cast, err = strconv.ParseBool(x) + default: + err = errors.New("expected: integer|boolean|string") + } + default: + err = errors.New("expected: integer|boolean|string") + } + return } // Resource injection specification. @@ -97,8 +180,27 @@ func (p *ParsedSelector) With(s string) { } // ResourceInjector inject resources into extension metadata. +// Example: +// metadata: +// provider: +// address: localhost:$(PORT) +// initConfig: +// - providerSpecificConfig: +// mavenInsecure: $(maven.insecure) +// mavenSettingsFile: $(maven.settings.path) +// name: java +// resources: +// - selector: identity:kind=maven +// fields: +// - key: maven.settings.path +// name: settings +// path: /shared/creds/maven/settings.xml +// - selector: setting:key=mvn.insecure.enabled +// fields: +// - key: maven.insecure +// name: value type ResourceInjector struct { - dict map[string]string + dict map[string]any } // Inject resources into extension metadata. @@ -126,11 +228,14 @@ func (r *ResourceInjector) Inject(extension *api.Extension) (p *provider.Config, // build builds resource dictionary. func (r *ResourceInjector) build(md *Metadata) (err error) { - r.dict = make(map[string]string) + r.dict = make(map[string]any) application, err := addon.Task.Application() if err != nil { return } + for _, resource := range md.Resources { + err = r.addDefaults(&resource) + } for _, resource := range md.Resources { parsed := ParsedSelector{} parsed.With(resource.Selector) @@ -165,6 +270,20 @@ func (r *ResourceInjector) build(md *Metadata) (err error) { return } +// addDefaults adds defaults when specified. +func (r *ResourceInjector) addDefaults(resource *Resource) (err error) { + for _, f := range resource.Fields { + if f.Default == nil { + continue + } + err = r.addField(&f, f.Default) + if err != nil { + return + } + } + return +} + // add the resource fields specified in the injector. func (r *ResourceInjector) add(resource *Resource, object any) (err error) { mp := r.asMap(object) @@ -177,30 +296,49 @@ func (r *ResourceInjector) add(resource *Resource, object any) (err error) { } return } - fv := r.string(v) - if f.Path != "" { - err = r.write(f.Path, fv) - if err != nil { - return - } - fv = f.Path + err = r.addField(&f, v) + if err != nil { + return } - r.dict[f.Key] = fv } return } +// addField adds field to the dict. +// When field has a path defined, the values is written to the +// file and the dict[key] = path. +func (r *ResourceInjector) addField(f *Field, v any) (err error) { + if f.Path != "" { + err = r.write(f.Path, v) + if err != nil { + return + } + v = f.Path + } else { + v, err = f.cast(v) + if err != nil { + return + } + } + r.dict[f.Key] = v + return +} + // write a resource field value to a file. -func (r *ResourceInjector) write(path string, s string) (err error) { +func (r *ResourceInjector) write(path string, object any) (err error) { err = nas.MkDir(pathlib.Dir(path), 0755) if err != nil { return } f, err := os.Create(path) - if err == nil { - _, err = f.Write([]byte(s)) - _ = f.Close() + if err != nil { + return } + defer func() { + _ = f.Close() + }() + s := r.string(object) + _, err = f.Write([]byte(s)) return } @@ -249,11 +387,17 @@ func (r *ResourceInjector) inject(in any) (out any) { if len(match) < 3 { break } - node = strings.Replace( - node, - match[0], - r.dict[match[2]], - -1) + v := r.dict[match[2]] + if len(node) > len(match[0]) { + node = strings.Replace( + node, + match[0], + r.string(v), + -1) + } else { + out = v + return + } } out = node default: