Skip to content

Commit

Permalink
✨ Inject location (#143)
Browse files Browse the repository at this point in the history
Add support for injecting the source code path (to be analyzed) in the
extension metadata. The `Location` field will continue to be injected.
There are providers that do not honor the Location field but instead
expect a `workspaceFolders` array populated. This is an inconsistency in
providers. To support this, the location needs to be templated. A new
`location` variable in the `builtin` namespace may be used by the
extension writer.
Example:
```
metadata:
  provider:
    address: localhost:$(PORT)
    initConfig:
    - providerSpecificConfig:
        lspServerName: generic
        lspServerPath: /usr/local/bin/pylsp
        workspaceFolders:
        - $(builtin.location)                  <-------------- HERE
        dependencyFolders:
        - examples/python/__pycache__
        - examples/python/.venv
    name: python
```

---------

Signed-off-by: Jeff Ortel <[email protected]>
  • Loading branch information
jortel authored Feb 7, 2025
1 parent 5e4a70d commit 57d7c99
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 123 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
docker save -o /tmp/tackle2-addon-analyzer.tar quay.io/konveyor/tackle2-addon-analyzer:latest
- name: Upload tackle2-addon-analyzer image as artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: tackle2-addon-analyzer
path: /tmp/tackle2-addon-analyzer.tar
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ fmt: $(GOIMPORTS)
vet:
go vet $(PKG)

test:
go test -count=1 -v ./cmd/...

# Ensure goimports installed.
$(GOIMPORTS):
go install golang.org/x/tools/cmd/[email protected]
2 changes: 1 addition & 1 deletion cmd/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (r *Analyzer) Run() (issueBuilder *builder.Issues, depBuilder *builder.Deps
// options builds Analyzer options.
func (r *Analyzer) options(output, depOutput string) (options command.Options, err error) {
settings := &Settings{}
err = settings.AppendExtensions()
err = settings.AppendExtensions(&r.Mode)
if err != nil {
return
}
Expand Down
39 changes: 37 additions & 2 deletions cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"testing"

"github.com/konveyor/analyzer-lsp/provider"
"github.com/onsi/gomega"
)

Expand Down Expand Up @@ -130,7 +131,8 @@ func TestIncidentSelector(t *testing.T) {

func TestInjectorDefaults(t *testing.T) {
g := gomega.NewGomegaWithT(t)
inj := ResourceInjector{dict: make(map[string]any)}
inj := ResourceInjector{}
inj.dict = make(map[string]any)
r := &Resource{
Fields: []Field{
{
Expand All @@ -152,7 +154,8 @@ func TestInjectorDefaults(t *testing.T) {

func TestInjectorTypeCast(t *testing.T) {
g := gomega.NewGomegaWithT(t)
inj := ResourceInjector{dict: make(map[string]any)}
inj := ResourceInjector{}
inj.dict = make(map[string]any)
r := &Resource{
Fields: []Field{
{
Expand Down Expand Up @@ -207,3 +210,35 @@ func TestInjectorTypeCast(t *testing.T) {
err = inj.addDefaults(r)
g.Expect(errors.Is(err, &TypeError{})).To(gomega.BeTrue())
}

func TestInject(t *testing.T) {
g := gomega.NewGomegaWithT(t)

key := "location"
path := "/tmp/x"
inj := Injector{}
inj.Use(make(map[string]any))
inj.dict[key] = path
md := &Metadata{}
md.Provider.InitConfig = []provider.InitConfig{
{Location: "$(" + key + ")"},
}
err := inj.Inject(md)
g.Expect(err).To(gomega.BeNil())
g.Expect(md.Provider.InitConfig[0].Location).To(gomega.Equal(path))
}

func TestRawInject(t *testing.T) {
g := gomega.NewGomegaWithT(t)

key := "location"
path := "/tmp/x"
inj := Injector{}
inj.Use(make(map[string]any))
inj.dict[key] = path
md := map[string]any{
"Location": "$(" + key + ")",
}
md2 := inj.inject(md).(map[string]any)
g.Expect(md2["Location"]).To(gomega.Equal(path))
}
241 changes: 144 additions & 97 deletions cmd/injector.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,25 @@ func (e *TypeError) Is(err error) (matched bool) {
return
}

// KeyConflictError reports key redefined errors.
type KeyConflictError struct {
Key string
Value any
}

func (e *KeyConflictError) Error() (s string) {
return fmt.Sprintf(
"Key: '%s' = '%v' cannot be redefined.",
e.Key,
e.Value)
}

func (e *KeyConflictError) Is(err error) (matched bool) {
var inst *KeyConflictError
matched = errors.As(err, &inst)
return
}

// Field injection specification.
type Field struct {
Name string `json:"name"`
Expand Down Expand Up @@ -179,56 +198,141 @@ 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 {
// Injector replaces variables in the object.
// format: $(variable).
type Injector struct {
dict map[string]any
}

// Inject resources into extension metadata.
// Returns injected provider (settings).
func (r *ResourceInjector) Inject(extension *api.Extension) (p *provider.Config, err error) {
mp := r.asMap(extension.Metadata)
md := Metadata{}
err = r.object(mp, &md)
func (r *Injector) Inject(md *Metadata) (err error) {
r.init()
mp := r.asMap(md)
mp = r.inject(mp).(map[string]any)
err = r.object(mp, md)
if err != nil {
return
}
err = r.build(&md)
if err != nil {
return
}

// Use map.
func (r *Injector) Use(d map[string]any) {
r.dict = d
}

// constructor.
func (r *Injector) init() {
if r.dict == nil {
r.dict = make(map[string]any)
}
}

// inject replaces `dict` variables referenced in metadata.
func (r *Injector) inject(in any) (out any) {
if r.dict == nil {
return
}
mp = r.asMap(&md.Provider)
mp = r.inject(mp).(map[string]any)
err = r.object(mp, &md.Provider)
switch node := in.(type) {
case map[string]any:
for k, v := range node {
node[k] = r.inject(v)
}
out = node
case []any:
var injected []any
for _, n := range node {
injected = append(
injected,
r.inject(n))
}
out = injected
case string:
for {
match := KeyRegex.FindStringSubmatch(node)
if len(match) < 3 {
break
}
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:
out = node
}
return
}

// objectMap returns a map for a resource object.
func (r *Injector) asMap(object any) (mp map[string]any) {
b, _ := json.Marshal(object)
mp = make(map[string]any)
_ = json.Unmarshal(b, &mp)
return
}

// objectMap returns a map for a resource object.
func (r *Injector) object(mp map[string]any, object any) (err error) {
b, _ := json.Marshal(mp)
err = json.Unmarshal(b, object)
return
}

// string returns a string representation of a field value.
func (r *Injector) string(object any) (s string) {
if object != nil {
s = fmt.Sprintf("%v", object)
}
return
}

// 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 {
Injector
}

// Inject resources into extension metadata.
func (r *ResourceInjector) Inject(md *Metadata) (err error) {
r.init()
err = r.build(md)
if err != nil {
return
}
p = &md.Provider
err = r.Injector.Inject(md)
return
}

// build builds resource dictionary.
func (r *ResourceInjector) build(md *Metadata) (err error) {
r.dict = make(map[string]any)
application, err := addon.Task.Application()
if err != nil {
return
Expand Down Expand Up @@ -320,6 +424,13 @@ func (r *ResourceInjector) addField(f *Field, v any) (err error) {
return
}
}
if _, found := r.dict[f.Key]; found {
err = &KeyConflictError{
Key: f.Key,
Value: v,
}
return
}
r.dict[f.Key] = v
return
}
Expand All @@ -341,67 +452,3 @@ func (r *ResourceInjector) write(path string, object any) (err error) {
_, err = f.Write([]byte(s))
return
}

// string returns a string representation of a field value.
func (r *ResourceInjector) string(object any) (s string) {
if object != nil {
s = fmt.Sprintf("%v", object)
}
return
}

// objectMap returns a map for a resource object.
func (r *ResourceInjector) asMap(object any) (mp map[string]any) {
b, _ := json.Marshal(object)
mp = make(map[string]any)
_ = json.Unmarshal(b, &mp)
return
}

// objectMap returns a map for a resource object.
func (r *ResourceInjector) object(mp map[string]any, object any) (err error) {
b, _ := json.Marshal(mp)
err = json.Unmarshal(b, object)
return
}

// inject replaces `dict` variables referenced in metadata.
func (r *ResourceInjector) inject(in any) (out any) {
switch node := in.(type) {
case map[string]any:
for k, v := range node {
node[k] = r.inject(v)
}
out = node
case []any:
var injected []any
for _, n := range node {
injected = append(
injected,
r.inject(n))
}
out = injected
case string:
for {
match := KeyRegex.FindStringSubmatch(node)
if len(match) < 3 {
break
}
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:
out = node
}
return
}
9 changes: 7 additions & 2 deletions cmd/mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,15 @@ func (r *Mode) AddOptions(options *command.Options, settings *Settings) (err err
settings.Mode(provider.SourceOnlyAnalysisMode)
options.Add("--no-dependency-rules")
}
return
}

// Location returns the location to be analyzed.
func (r *Mode) Location() (path string) {
if r.Binary {
settings.Location(r.path.binary)
path = r.path.binary
} else {
settings.Location(r.path.appDir)
path = r.path.appDir
}
return
}
Expand Down
Loading

0 comments on commit 57d7c99

Please sign in to comment.