diff --git a/docs/dynamic-plugins.md b/docs/dynamic-plugins.md index 5bac74a59..aaa771df1 100644 --- a/docs/dynamic-plugins.md +++ b/docs/dynamic-plugins.md @@ -84,19 +84,51 @@ The extraction directory can be configured via the `CATALOG_ENTITIES_EXTRACT_DIR More details in [Catalog Entities Extraction](https://github.com/redhat-developer/rhdh/blob/main/docs/dynamic-plugins/installing-plugins.md#catalog-entities-extraction). +## Supported Package URL Formats + +| Format | Type | Description | +|--------|------|-------------| +| `ref://plugin-name` | Catalog reference | Look up plugin by name, returns full package URL | +| `oci://...{{inherit}}` | Catalog reference | Look up plugin by name, returns full package URL | +| `oci://...` | Direct link | OCI image reference (no resolution) | +| `https://...` | Direct link | HTTPS URL to plugin archive | +| `http://...` | Direct link | HTTP URL to plugin archive | +| `./path` | Direct link | Local filesystem path | + ## Plugin URL References -The operator supports special URL reference syntax in plugin package URLs, allowing users to reference versions or plugins from the default configuration. +The operator optionally supports special URL reference syntax in plugin package URLs, allowing users to reference plugins from the default configuration by name. + +TODO: document Operator Dynamic Plugins processing mode **Operator behavior:** - The operator resolves all references during ConfigMap merge (before passing to the init container) - If a reference cannot be resolved, the operator returns an error and the Backstage CR will not reconcile +- Both reference types use **name-based matching** - only the plugin name matters for lookup + +### Ref Reference (`ref://`) + +Look up a plugin by name and use its full package URL from the default configuration. + +```yaml +plugins: + - package: "ref://backstage-plugin-catalog" + pluginConfig: + # your config overrides +``` ### Inherit Reference (`:{{inherit}}`) -Allows inheriting version (tag or digest) from default plugins. Useful when overriding plugin settings without hardcoding versions. +Look up a plugin by name and use its full package URL from the default configuration. The registry/path in your URL is ignored - only the plugin name matters for matching. + +```yaml +plugins: + # These all match the same base plugin (backstage-plugin-catalog): + - package: "oci://quay.io/rhdh/backstage-plugin-catalog:{{inherit}}" + - package: "oci://any-registry/path/backstage-plugin-catalog:{{inherit}}" +``` -For syntax details and examples, see [OCI Package Version Inheritance](https://github.com/redhat-developer/rhdh/blob/main/docs/dynamic-plugins/installing-plugins.md#oci-package-version-inheritance). +**Since v0.11.0:** Both `ref://` and `:{{inherit}}` use name-based matching (plugin name only, registry/path ignored). This behavior is slightly different from what is described in [OCI Package Version Inheritance](https://github.com/redhat-developer/rhdh/blob/main/docs/dynamic-plugins/installing-plugins.md#oci-package-version-inheritance) which documents the RHDH init-container behavior (full URL matching). ## Dynamic plugins dependency management diff --git a/examples/dyna-plugins.yaml b/examples/dyna-plugins.yaml new file mode 100644 index 000000000..ede81afbf --- /dev/null +++ b/examples/dyna-plugins.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: dynamic-plugins +data: + dynamic-plugins.yaml: | + plugins: + - package: 'ref://backstage-community-plugin-catalog-backend-module-keycloak-dynamic' + enabled: true +--- +apiVersion: rhdh.redhat.com/v1alpha5 +kind: Backstage +metadata: + name: bs1 +spec: + application: + dynamicPluginsConfigMapName: dynamic-plugins + diff --git a/pkg/model/default-config_test.go b/pkg/model/default-config_test.go index 2c1a084a1..8e44b67a1 100644 --- a/pkg/model/default-config_test.go +++ b/pkg/model/default-config_test.go @@ -68,7 +68,7 @@ func TestMergeDynamicPluginsFunction(t *testing.T) { // Find plugin-b to verify it was overridden var pluginB *DynaPlugin for i := range config.Plugins { - if config.Plugins[i].Package == "plugin-b" { + if config.Plugins[i].Package == "./plugin-b" { pluginB = &config.Plugins[i] break } diff --git a/pkg/model/dynamic-plugins-reference.go b/pkg/model/dynamic-plugins-reference.go index 284040474..33cf56639 100644 --- a/pkg/model/dynamic-plugins-reference.go +++ b/pkg/model/dynamic-plugins-reference.go @@ -7,29 +7,54 @@ import ( const inheritSuffix = ":{{inherit}}" const refPrefix = "ref://" +const ociPrefix = "oci://" +const httpsPrefix = "https://" +const httpPrefix = "http://" +const localPrefix = "./" // resolveReferences resolves all reference types in plugin package URLs. -// Currently supports: -// - {{inherit}}: inherits version/digest from base plugins -// - ref://: references another plugin by name (TODO) +// +// Supported package URL formats: +// +// Catalog references (resolved by plugin name lookup): +// - ref://plugin-name: Returns full package URL from base plugins matching by name. +// Example: ref://backstage-plugin-catalog → oci://quay.io/rhdh/backstage-plugin-catalog@sha256:abc123 +// - oci://...{{inherit}}: Inherits version/digest from base plugins matching by name. +// The registry/path in the user URL is ignored - only the plugin name matters. +// Example: oci://any-registry/backstage-plugin-catalog:{{inherit}} → oci://quay.io/rhdh/backstage-plugin-catalog@sha256:abc123 +// User can override !plugin-path: oci://x/plugin:{{inherit}}!custom-path → uses custom-path instead of base's path +// +// Direct links (no resolution needed): +// - oci://...: OCI image reference +// - https://...: HTTPS URL to plugin archive +// - http://...: HTTP URL to plugin archive +// - ./path: Local filesystem path +// +// Any other prefix returns an error. func resolveReferences(plugins []DynaPlugin, basePlugins []DynaPlugin) ([]DynaPlugin, error) { resolved := make([]DynaPlugin, len(plugins)) copy(resolved, plugins) - // Build lookup map for base plugins (used by inherit resolver) - baseURLMap := buildBaseURLMap(basePlugins) - for i := range resolved { plugin := &resolved[i] + if plugin.Package == "" { + continue + } + var err error switch { - case strings.Contains(plugin.Package, inheritSuffix): - resolved[i].Package, err = resolveInheritReference(plugin.Package, baseURLMap) case strings.HasPrefix(plugin.Package, refPrefix): + // Catalog search by name resolved[i].Package, err = resolveRefReference(plugin.Package, basePlugins) - default: + case strings.Contains(plugin.Package, inheritSuffix): + // Catalog search by name, inherit version/digest + resolved[i].Package, err = resolveInheritReference(plugin.Package, basePlugins) + case plugin.IsDirectLink(): + // Direct link - no resolution needed continue + default: + return nil, fmt.Errorf("unsupported package URL format %q: must start with oci://, https://, http://, ./ or use ref:// for catalog lookup", plugin.Package) } if err != nil { @@ -40,26 +65,21 @@ func resolveReferences(plugins []DynaPlugin, basePlugins []DynaPlugin) ([]DynaPl return resolved, nil } -// buildBaseURLMap creates a lookup map from base URL to full package URL. -func buildBaseURLMap(basePlugins []DynaPlugin) map[string]string { - baseURLMap := make(map[string]string) - for i := range basePlugins { - plugin := &basePlugins[i] - if plugin.Package == "" { - continue - } - baseURL := plugin.BaseURL() - if baseURL != "" { - baseURLMap[baseURL] = plugin.Package - } - } - return baseURLMap +// IsDirectLink returns true if the package URL is a direct link that doesn't need resolution. +func (p *DynaPlugin) IsDirectLink() bool { + return strings.HasPrefix(p.Package, ociPrefix) || + strings.HasPrefix(p.Package, httpsPrefix) || + strings.HasPrefix(p.Package, httpPrefix) || + strings.HasPrefix(p.Package, localPrefix) } -// resolveInheritReference resolves a single {{inherit}} reference. -// For example, oci://registry/plugin:{{inherit}} will be replaced with -// oci://registry/plugin@sha256:abc123 if found in baseURLMap. -func resolveInheritReference(packageURL string, baseURLMap map[string]string) (string, error) { +// resolveInheritReference resolves a single {{inherit}} reference by looking up plugin by name. +// The registry and path in the user's URL are ignored - only the plugin name (last path component) matters. +// +// Examples: +// - oci://any-registry/path/plugin-foo:{{inherit}} matches base plugin oci://quay.io/rhdh/plugin-foo@sha256:abc +// - oci://x/plugin-foo:{{inherit}}!custom-path uses base's version but user's plugin-path +func resolveInheritReference(packageURL string, basePlugins []DynaPlugin) (string, error) { // Parse package to extract !plugin-path suffix if present var pluginPath string if idx := strings.LastIndex(packageURL, "!"); idx != -1 { @@ -67,67 +87,155 @@ func resolveInheritReference(packageURL string, baseURLMap map[string]string) (s packageURL = packageURL[:idx] } - // Extract base URL (strip :{{inherit}}) - baseURL := strings.Replace(packageURL, inheritSuffix, "", 1) + // Extract plugin name from the package URL (strip :{{inherit}} first) + tempPackage := strings.Replace(packageURL, inheritSuffix, "", 1) + tempPlugin := DynaPlugin{Package: tempPackage} + pluginName := tempPlugin.Name() - // Look up the full URL in basePlugins - fullURL, found := baseURLMap[baseURL] - if !found { - return "", fmt.Errorf("cannot resolve {{inherit}} reference: no matching plugin found for base URL %q in default plugins", baseURL) + if pluginName == "" { + return "", fmt.Errorf("cannot resolve {{inherit}} reference: unable to extract plugin name from %q", packageURL) } - // If user specified !plugin-path, use it; otherwise use full default URL - if pluginPath != "" { - // Extract image part from default (without !plugin-path) - if idx := strings.LastIndex(fullURL, "!"); idx != -1 { - fullURL = fullURL[:idx] + // Look up the plugin by name in basePlugins + for i := range basePlugins { + plugin := &basePlugins[i] + if plugin.Package == "" { + continue + } + if plugin.Name() == pluginName { + fullURL := plugin.Package + + // If user specified !plugin-path, use it; otherwise use full default URL + if pluginPath != "" { + // Extract image part from default (without !plugin-path) + if idx := strings.LastIndex(fullURL, "!"); idx != -1 { + fullURL = fullURL[:idx] + } + return fullURL + pluginPath, nil + } + + return fullURL, nil } - return fullURL + pluginPath, nil } - return fullURL, nil + return "", fmt.Errorf("cannot resolve {{inherit}} reference: no plugin named %q found in default plugins", pluginName) } // resolveRefReference resolves a ref:// reference by looking up plugin by name. -// For example, ref://my-plugin will be replaced with the full package URL -// of the plugin named "my-plugin" in basePlugins. +// Returns the full package URL from basePlugins for the plugin with matching name. +// +// Example: ref://backstage-plugin-catalog → oci://quay.io/rhdh/backstage-plugin-catalog@sha256:abc123 func resolveRefReference(packageURL string, basePlugins []DynaPlugin) (string, error) { - // TODO: implement ref:// resolution - // ref:// should look up the plugin by name in basePlugins - return "", fmt.Errorf("ref:// references are not yet implemented: %s", packageURL) + // Extract the plugin name from ref:// + refName := strings.TrimPrefix(packageURL, refPrefix) + if refName == "" { + return "", fmt.Errorf("invalid ref:// reference: empty plugin name in %q", packageURL) + } + + // Look up the plugin by name in basePlugins + for i := range basePlugins { + plugin := &basePlugins[i] + if plugin.Package == "" { + continue + } + if plugin.Name() == refName { + return plugin.Package, nil + } + } + + return "", fmt.Errorf("cannot resolve ref:// reference: no plugin named %q found in default plugins", refName) } -// BaseURL extracts the base URL from a plugin package URL -// by removing the tag or digest suffix. +// Name extracts the plugin name from the package URL. // For example: -// - oci://registry/plugin:tag -> oci://registry/plugin -// - oci://registry/plugin@sha256:abc -> oci://registry/plugin -// - ./local/path -> ./local/path (unchanged) -func (p *DynaPlugin) BaseURL() string { +// - oci://quay.io/rhdh/backstage-plugin-techdocs@sha256:abc -> backstage-plugin-techdocs +// - oci://quay.io/rhdh/backstage-plugin-techdocs:1.0.0 -> backstage-plugin-techdocs +// - https://example.com/path/backstage-plugin-foo-1.0.0.tgz -> backstage-plugin-foo +// - ./dynamic-plugins/dist/backstage-plugin-techdocs -> backstage-plugin-techdocs +func (p *DynaPlugin) Name() string { packageURL := p.Package - // Only process OCI URLs - if !strings.HasPrefix(packageURL, "oci://") { - return packageURL - } - - // Strip !plugin-path suffix first if present + // Strip !plugin-path suffix if present if idx := strings.LastIndex(packageURL, "!"); idx != -1 { packageURL = packageURL[:idx] } - // Handle OCI URLs with digest (@sha256:...) - if idx := strings.LastIndex(packageURL, "@"); idx != -1 { - return packageURL[:idx] + // Handle OCI URLs + if strings.HasPrefix(packageURL, ociPrefix) { + // Remove oci:// prefix + url := strings.TrimPrefix(packageURL, ociPrefix) + + // Remove digest (@sha256:...) + if idx := strings.LastIndex(url, "@"); idx != -1 { + url = url[:idx] + } + + // Require a path component (must have "/") - registry-only URLs are invalid + idx := strings.LastIndex(url, "/") + if idx == -1 { + return "" + } + + // Extract the last path component (the image name, possibly with tag) + imageName := url[idx+1:] + if imageName == "" { + return "" + } + + // Remove tag (:tag) from image name only (not port from registry) + if idx := strings.LastIndex(imageName, ":"); idx != -1 { + imageName = imageName[:idx] + } + + return imageName } - // Handle OCI URLs with tag (:tag) - schemeEnd := len("oci://") - rest := packageURL[schemeEnd:] - if idx := strings.LastIndex(rest, ":"); idx != -1 { - return packageURL[:schemeEnd+idx] + // Handle HTTP(S) URLs + if strings.HasPrefix(packageURL, httpsPrefix) || strings.HasPrefix(packageURL, httpPrefix) { + // Remove scheme + url := strings.TrimPrefix(packageURL, httpsPrefix) + url = strings.TrimPrefix(url, httpPrefix) + + // Extract the last path component + if idx := strings.LastIndex(url, "/"); idx != -1 { + url = url[idx+1:] + } + + // Strip query string if present + if idx := strings.Index(url, "?"); idx != -1 { + url = url[:idx] + } + + // Strip common archive extensions + url = strings.TrimSuffix(url, ".tgz") + url = strings.TrimSuffix(url, ".tar.gz") + + // Strip version suffix (e.g., -1.0.0, -1.2.3-beta) + url = stripVersionSuffix(url) + + return url + } + + // Handle local paths (./path/to/plugin-name) + if strings.HasPrefix(packageURL, localPrefix) { + if idx := strings.LastIndex(packageURL, "/"); idx != -1 { + return packageURL[idx+1:] + } + return packageURL } - // No tag or digest found - return packageURL + // Unknown protocol - return empty string + return "" +} + +// stripVersionSuffix removes a trailing version suffix from a plugin name. +// For example: backstage-plugin-foo-1.0.0 -> backstage-plugin-foo +func stripVersionSuffix(name string) string { + // Look for pattern: - which typically starts a version + for i := len(name) - 1; i >= 0; i-- { + if name[i] == '-' && i+1 < len(name) && name[i+1] >= '0' && name[i+1] <= '9' { + return name[:i] + } + } + return name } diff --git a/pkg/model/dynamic-plugins-reference_test.go b/pkg/model/dynamic-plugins-reference_test.go index 390a4ef85..7a5912b32 100644 --- a/pkg/model/dynamic-plugins-reference_test.go +++ b/pkg/model/dynamic-plugins-reference_test.go @@ -6,73 +6,11 @@ import ( "github.com/stretchr/testify/assert" ) -func TestBaseURL(t *testing.T) { - tests := []struct { - name string - package_ string - expected string - }{ - { - name: "OCI with digest", - package_: "oci://quay.io/rhdh/plugin@sha256:abc123", - expected: "oci://quay.io/rhdh/plugin", - }, - { - name: "OCI with tag", - package_: "oci://quay.io/rhdh/plugin:v1.0.0", - expected: "oci://quay.io/rhdh/plugin", - }, - { - name: "OCI with digest and plugin path", - package_: "oci://quay.io/rhdh/plugin@sha256:abc123!my-plugin", - expected: "oci://quay.io/rhdh/plugin", - }, - { - name: "OCI with tag and plugin path", - package_: "oci://quay.io/rhdh/plugin:v1.0.0!my-plugin", - expected: "oci://quay.io/rhdh/plugin", - }, - { - name: "OCI with inherit suffix", - package_: "oci://quay.io/rhdh/plugin:{{inherit}}", - expected: "oci://quay.io/rhdh/plugin", - }, - { - name: "OCI with inherit suffix and plugin path", - package_: "oci://quay.io/rhdh/plugin:{{inherit}}!my-plugin", - expected: "oci://quay.io/rhdh/plugin", - }, - { - name: "OCI without tag or digest", - package_: "oci://quay.io/rhdh/plugin", - expected: "oci://quay.io/rhdh/plugin", - }, - { - name: "Local path", - package_: "./dynamic-plugins/dist/my-plugin", - expected: "./dynamic-plugins/dist/my-plugin", - }, - { - name: "NPM package", - package_: "@backstage/plugin-catalog", - expected: "@backstage/plugin-catalog", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - plugin := DynaPlugin{Package: tt.package_} - result := plugin.BaseURL() - assert.Equal(t, tt.expected, result) - }) - } -} - func TestResolveInheritReference(t *testing.T) { - baseURLMap := map[string]string{ - "oci://quay.io/rhdh/plugin-a": "oci://quay.io/rhdh/plugin-a@sha256:abc123!plugin-a-path", - "oci://quay.io/rhdh/plugin-b": "oci://quay.io/rhdh/plugin-b@sha256:def456", - "oci://registry.access.redhat.com/rh": "oci://registry.access.redhat.com/rh@sha256:xyz789!rh-plugin", + basePlugins := []DynaPlugin{ + {Package: "oci://quay.io/rhdh/plugin-a@sha256:abc123!plugin-a-path"}, + {Package: "oci://quay.io/rhdh/plugin-b@sha256:def456"}, + {Package: "oci://registry.access.redhat.com/plugin-c@sha256:xyz789!rh-plugin"}, } tests := []struct { @@ -101,6 +39,11 @@ func TestResolveInheritReference(t *testing.T) { packageURL: "oci://quay.io/rhdh/plugin-b:{{inherit}}!my-plugin", expected: "oci://quay.io/rhdh/plugin-b@sha256:def456!my-plugin", }, + { + name: "inherit with different registry - matches by name", + packageURL: "oci://other-registry.io/different/plugin-c:{{inherit}}", + expected: "oci://registry.access.redhat.com/plugin-c@sha256:xyz789!rh-plugin", + }, { name: "inherit with no matching base - error", packageURL: "oci://quay.io/rhdh/unknown:{{inherit}}", @@ -110,7 +53,7 @@ func TestResolveInheritReference(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := resolveInheritReference(tt.packageURL, baseURLMap) + result, err := resolveInheritReference(tt.packageURL, basePlugins) if tt.expectError { assert.Error(t, err) @@ -181,6 +124,20 @@ func TestResolveReferences(t *testing.T) { }, expectError: true, }, + { + name: "unsupported protocol - error", + plugins: []DynaPlugin{ + {Package: "ftp://server.example.com/plugin"}, + }, + expectError: true, + }, + { + name: "npm package not supported - error", + plugins: []DynaPlugin{ + {Package: "@backstage/plugin-catalog"}, + }, + expectError: true, + }, } for _, tt := range tests { @@ -233,6 +190,121 @@ plugins: assert.NotContains(t, mergedData, "{{inherit}}") } +func TestResolveRefReference(t *testing.T) { + basePlugins := []DynaPlugin{ + {Package: "oci://quay.io/rhdh/backstage-plugin-foo@sha256:abc123"}, + {Package: "oci://quay.io/rhdh/backstage-plugin-bar@sha256:def456!plugin-path"}, + } + + tests := []struct { + name string + packageURL string + expected string + expectError bool + }{ + { + name: "ref to OCI plugin", + packageURL: "ref://backstage-plugin-foo", + expected: "oci://quay.io/rhdh/backstage-plugin-foo@sha256:abc123", + }, + { + name: "ref to OCI plugin with path", + packageURL: "ref://backstage-plugin-bar", + expected: "oci://quay.io/rhdh/backstage-plugin-bar@sha256:def456!plugin-path", + }, + { + name: "ref to non-existent plugin", + packageURL: "ref://unknown-plugin", + expectError: true, + }, + { + name: "empty ref", + packageURL: "ref://", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := resolveRefReference(tt.packageURL, basePlugins) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestName(t *testing.T) { + tests := []struct { + name string + package_ string + expected string + }{ + { + name: "OCI with digest", + package_: "oci://quay.io/rhdh/backstage-plugin-foo@sha256:abc123", + expected: "backstage-plugin-foo", + }, + { + name: "OCI with tag", + package_: "oci://quay.io/rhdh/backstage-plugin-bar:v1.0.0", + expected: "backstage-plugin-bar", + }, + { + name: "OCI with registry port and no tag", + package_: "oci://localhost:5000/path/my-plugin", + expected: "my-plugin", + }, + { + name: "OCI with registry port and tag", + package_: "oci://localhost:5000/path/my-plugin:v1.0.0", + expected: "my-plugin", + }, + { + name: "OCI registry-only URL returns empty", + package_: "oci://localhost:5000", + expected: "", + }, + { + name: "OCI registry with trailing slash returns empty", + package_: "oci://localhost:5000/", + expected: "", + }, + { + name: "HTTPS with version and tgz", + package_: "https://example.com/plugins/backstage-plugin-foo-1.0.0.tgz", + expected: "backstage-plugin-foo", + }, + { + name: "HTTPS with tar.gz", + package_: "https://example.com/path/my-plugin-2.3.4.tar.gz", + expected: "my-plugin", + }, + { + name: "HTTP URL", + package_: "http://registry.example.com/backstage-plugin-bar-0.1.0.tgz", + expected: "backstage-plugin-bar", + }, + { + name: "Local path", + package_: "./dynamic-plugins/dist/backstage-plugin-techdocs", + expected: "backstage-plugin-techdocs", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + plugin := DynaPlugin{Package: tt.package_} + result := plugin.Name() + assert.Equal(t, tt.expected, result) + }) + } +} + func TestMergePluginsDataWithInheritError(t *testing.T) { t.Setenv(OperatorDPProcessingEnvVar, "true") diff --git a/pkg/model/dynamic-plugins_test.go b/pkg/model/dynamic-plugins_test.go index 7d790ccd1..1275fa60c 100644 --- a/pkg/model/dynamic-plugins_test.go +++ b/pkg/model/dynamic-plugins_test.go @@ -206,7 +206,7 @@ func TestWithDynamicPluginsDeps(t *testing.T) { yamlData := `"dynamic-plugins.yaml": | plugins: - - package: "plugin-a" + - package: "./plugin-a" disabled: false dependencies: - ref: "dependency-1" @@ -384,25 +384,25 @@ func TestMergeDynamicPlugins(t *testing.T) { // Sample model ConfigMap modelData := ` plugins: - - package: "plugin-a" + - package: "./plugin-a" integrity: "sha256-abc123" disabled: false pluginConfig: key1: "value1" dependencies: - ref: "dependency-1" - - package: "plugin-b" + - package: "./plugin-b" integrity: "sha256-def456" disabled: true pluginConfig: key2: "value2" dependencies: - ref: "dependency-2" - - package: "plugin-c" + - package: "./plugin-c" integrity: "sha256-ghi789" pluginConfig: key3: "value3" - - package: "plugin-d" + - package: "./plugin-d" disabled: true integrity: "sha256-ddd" pluginConfig: @@ -425,14 +425,14 @@ includes: // Sample spec data specData := ` plugins: - - package: "plugin-a" + - package: "./plugin-a" integrity: "sha256-overridden" pluginConfig: key1: "overridden" dependencies: - ref: "dependency-3" - - package: "plugin-d" - - package: "plugin-e" + - package: "./plugin-d" + - package: "./plugin-e" includes: - "include-2" @@ -454,9 +454,7 @@ includes: assert.Equal(t, 5, len(mergedConfig.Plugins)) // Validate plugin-a (overridden by specData) - //pluginA := mergedConfig.Plugins[0] - //assert.Equal(t, "plugin-a", pluginA.Package) - pluginA := findPluginByPackage(mergedConfig.Plugins, "plugin-a") + pluginA := findPluginByPackage(mergedConfig.Plugins, "./plugin-a") assert.NotNil(t, pluginA) assert.Equal(t, "sha256-overridden", pluginA.Integrity) assert.False(t, pluginA.IsDisabled()) @@ -465,29 +463,25 @@ includes: assert.Equal(t, "dependency-3", pluginA.Dependencies[0].Ref) // Validate plugin-b (disabled, from modelDp) - pluginB := findPluginByPackage(mergedConfig.Plugins, "plugin-b") + pluginB := findPluginByPackage(mergedConfig.Plugins, "./plugin-b") assert.NotNil(t, pluginB) assert.True(t, pluginB.IsDisabled()) // Validate plugin-c (from modelDp, as plugin-b is disabled) - //pluginC := mergedConfig.Plugins[1] - pluginC := findPluginByPackage(mergedConfig.Plugins, "plugin-c") + pluginC := findPluginByPackage(mergedConfig.Plugins, "./plugin-c") assert.NotNil(t, pluginC) - //assert.Equal(t, "plugin-c", pluginC.Package) assert.Equal(t, "sha256-ghi789", pluginC.Integrity) assert.Equal(t, "value3", pluginC.PluginConfig["key3"]) - //pluginD := mergedConfig.Plugins[2] - pluginD := findPluginByPackage(mergedConfig.Plugins, "plugin-d") + pluginD := findPluginByPackage(mergedConfig.Plugins, "./plugin-d") assert.NotNil(t, pluginD) - //assert.Equal(t, "plugin-d", pluginD.Package) assert.Equal(t, "sha256-ddd", pluginD.Integrity) // Validate merged includes assert.ElementsMatch(t, []string{"include-1", "include-2"}, mergedConfig.Includes) // Marshal the merged configuration into YAML - marshalledE, err := yaml.Marshal(findPluginByPackage(mergedConfig.Plugins, "plugin-e")) + marshalledE, err := yaml.Marshal(findPluginByPackage(mergedConfig.Plugins, "./plugin-e")) assert.NoError(t, err) // Validate that the marshalled string omits empty fields @@ -526,12 +520,12 @@ func TestMergePluginsEnabledDisabledBackwardCompat(t *testing.T) { t.Run("legacy disabled overlay overrides enabled base", func(t *testing.T) { base := ` plugins: - - package: "plugin-a" + - package: "./plugin-a" enabled: true ` overlay := ` plugins: - - package: "plugin-a" + - package: "./plugin-a" disabled: true ` merged, err := MergePluginsData(base, overlay) @@ -541,7 +535,7 @@ plugins: err = yaml.Unmarshal([]byte(merged), &config) assert.NoError(t, err) - plugin := findPluginByPackage(config.Plugins, "plugin-a") + plugin := findPluginByPackage(config.Plugins, "./plugin-a") assert.NotNil(t, plugin) assert.True(t, plugin.IsDisabled(), "legacy disabled: true should override enabled: true") }) @@ -550,12 +544,12 @@ plugins: t.Run("enabled overlay overrides disabled base", func(t *testing.T) { base := ` plugins: - - package: "plugin-a" + - package: "./plugin-a" disabled: true ` overlay := ` plugins: - - package: "plugin-a" + - package: "./plugin-a" enabled: true ` merged, err := MergePluginsData(base, overlay) @@ -565,7 +559,7 @@ plugins: err = yaml.Unmarshal([]byte(merged), &config) assert.NoError(t, err) - plugin := findPluginByPackage(config.Plugins, "plugin-a") + plugin := findPluginByPackage(config.Plugins, "./plugin-a") assert.NotNil(t, plugin) assert.False(t, plugin.IsDisabled(), "enabled: true should override disabled: true") }) @@ -574,13 +568,13 @@ plugins: t.Run("overlay without activation fields preserves base", func(t *testing.T) { base := ` plugins: - - package: "plugin-a" + - package: "./plugin-a" disabled: true integrity: "sha256-abc" ` overlay := ` plugins: - - package: "plugin-a" + - package: "./plugin-a" integrity: "sha256-overridden" ` merged, err := MergePluginsData(base, overlay) @@ -590,7 +584,7 @@ plugins: err = yaml.Unmarshal([]byte(merged), &config) assert.NoError(t, err) - plugin := findPluginByPackage(config.Plugins, "plugin-a") + plugin := findPluginByPackage(config.Plugins, "./plugin-a") assert.NotNil(t, plugin) assert.True(t, plugin.IsDisabled(), "base disabled: true should be preserved when overlay omits both fields") assert.Equal(t, "sha256-overridden", plugin.Integrity) @@ -600,9 +594,9 @@ plugins: t.Run("legacy disabled-only config works", func(t *testing.T) { base := ` plugins: - - package: "plugin-a" + - package: "./plugin-a" disabled: false - - package: "plugin-b" + - package: "./plugin-b" disabled: true ` merged, err := MergePluginsData(base, "") @@ -612,9 +606,9 @@ plugins: err = yaml.Unmarshal([]byte(merged), &config) assert.NoError(t, err) - pluginA := findPluginByPackage(config.Plugins, "plugin-a") + pluginA := findPluginByPackage(config.Plugins, "./plugin-a") assert.False(t, pluginA.IsDisabled()) - pluginB := findPluginByPackage(config.Plugins, "plugin-b") + pluginB := findPluginByPackage(config.Plugins, "./plugin-b") assert.True(t, pluginB.IsDisabled()) }) } @@ -637,7 +631,7 @@ includes: specData := ` plugins: - - package: "plugin-a" + - package: "./plugin-a" includes: [] ` mergedData, err := MergePluginsData(defDynamicPlugins.ConfigMap.Data[DynamicPluginsFile], specData) @@ -656,7 +650,7 @@ func TestClearDeps(t *testing.T) { // Sample model (default) ConfigMap modelData := ` plugins: - - package: "plugin-a" + - package: "./plugin-a" disabled: true pluginConfig: key1: "value1" @@ -678,7 +672,7 @@ plugins: // Sample spec data, remove default deps and override plugin-a to be enabled specData := ` plugins: - - package: "plugin-a" + - package: "./plugin-a" pluginConfig: key1: "overridden" dependencies: [] diff --git a/pkg/model/flavour_test.go b/pkg/model/flavour_test.go index 8a46aeaa3..6dedfa9de 100644 --- a/pkg/model/flavour_test.go +++ b/pkg/model/flavour_test.go @@ -66,8 +66,8 @@ func TestFlavoursWithDefaultsEnabled(t *testing.T) { dynamicPlugins := model.GetRuntimeObject(DynamicPluginsKey).(*DynamicPlugins) err = yaml.Unmarshal([]byte(dynamicPlugins.ConfigMap.Data[DynamicPluginsFile]), &dpConfig) assert.NoError(t, err) - assert.NotNil(t, findPluginByPackage(dpConfig.Plugins, "plugin-base")) - assert.NotNil(t, findPluginByPackage(dpConfig.Plugins, "plugin-flavor1")) + assert.NotNil(t, findPluginByPackage(dpConfig.Plugins, "./plugin-base")) + assert.NotNil(t, findPluginByPackage(dpConfig.Plugins, "./plugin-flavor1")) } func TestFlavoursWithExplicitEnabled(t *testing.T) { @@ -115,9 +115,9 @@ func TestFlavoursWithExplicitEnabled(t *testing.T) { var dpConfig DynaPluginsConfig err = yaml.Unmarshal([]byte(model.GetRuntimeObject(DynamicPluginsKey).(*DynamicPlugins).ConfigMap.Data[DynamicPluginsFile]), &dpConfig) assert.NoError(t, err) - assert.NotNil(t, findPluginByPackage(dpConfig.Plugins, "plugin-base")) - assert.NotNil(t, findPluginByPackage(dpConfig.Plugins, "plugin-flavor2")) - assert.NotNil(t, findPluginByPackage(dpConfig.Plugins, "plugin-flavor1")) + assert.NotNil(t, findPluginByPackage(dpConfig.Plugins, "./plugin-base")) + assert.NotNil(t, findPluginByPackage(dpConfig.Plugins, "./plugin-flavor2")) + assert.NotNil(t, findPluginByPackage(dpConfig.Plugins, "./plugin-flavor1")) } func TestFlavoursWithDefaultDisabled(t *testing.T) { @@ -159,8 +159,8 @@ func TestFlavoursWithDefaultDisabled(t *testing.T) { var dpConfig DynaPluginsConfig err = yaml.Unmarshal([]byte(model.GetRuntimeObject(DynamicPluginsKey).(*DynamicPlugins).ConfigMap.Data[DynamicPluginsFile]), &dpConfig) assert.NoError(t, err) - assert.NotNil(t, findPluginByPackage(dpConfig.Plugins, "plugin-base")) - assert.Nil(t, findPluginByPackage(dpConfig.Plugins, "plugin-flavor1")) + assert.NotNil(t, findPluginByPackage(dpConfig.Plugins, "./plugin-base")) + assert.Nil(t, findPluginByPackage(dpConfig.Plugins, "./plugin-flavor1")) } func TestFlavoursOnlyNoBase(t *testing.T) { @@ -196,8 +196,8 @@ func TestFlavoursOnlyNoBase(t *testing.T) { var dpConfig DynaPluginsConfig err = yaml.Unmarshal([]byte(model.GetRuntimeObject(DynamicPluginsKey).(*DynamicPlugins).ConfigMap.Data[DynamicPluginsFile]), &dpConfig) assert.NoError(t, err) - assert.Nil(t, findPluginByPackage(dpConfig.Plugins, "plugin-base"), "base plugin should NOT exist") - assert.NotNil(t, findPluginByPackage(dpConfig.Plugins, "plugin-flavor3"), "flavor3 plugin should exist") + assert.Nil(t, findPluginByPackage(dpConfig.Plugins, "./plugin-base"), "base plugin should NOT exist") + assert.NotNil(t, findPluginByPackage(dpConfig.Plugins, "./plugin-flavor3"), "flavor3 plugin should exist") } func TestFlavoursWithEmptyArray(t *testing.T) { diff --git a/pkg/model/testdata/dynamic-plugins-base.yaml b/pkg/model/testdata/dynamic-plugins-base.yaml index 47c8ac59f..4857e3f1b 100644 --- a/pkg/model/testdata/dynamic-plugins-base.yaml +++ b/pkg/model/testdata/dynamic-plugins-base.yaml @@ -7,8 +7,8 @@ data: includes: - dynamic-plugins.default.yaml plugins: - - package: "plugin-a" + - package: "./plugin-a" enabled: true - - package: "plugin-b" + - package: "./plugin-b" enabled: false integrity: "sha512-base" diff --git a/pkg/model/testdata/dynamic-plugins-overlay.yaml b/pkg/model/testdata/dynamic-plugins-overlay.yaml index b5ebe35d0..70b23eecc 100644 --- a/pkg/model/testdata/dynamic-plugins-overlay.yaml +++ b/pkg/model/testdata/dynamic-plugins-overlay.yaml @@ -7,8 +7,8 @@ data: includes: - dynamic-plugins.custom.yaml plugins: - - package: "plugin-b" + - package: "./plugin-b" enabled: true integrity: "sha512-overlay" - - package: "plugin-c" + - package: "./plugin-c" enabled: true diff --git a/pkg/model/testdata/testflavours-nobase/default-config/flavours/flavor3/dynamic-plugins.yaml b/pkg/model/testdata/testflavours-nobase/default-config/flavours/flavor3/dynamic-plugins.yaml index 5ce8b73e6..76fd74f7e 100644 --- a/pkg/model/testdata/testflavours-nobase/default-config/flavours/flavor3/dynamic-plugins.yaml +++ b/pkg/model/testdata/testflavours-nobase/default-config/flavours/flavor3/dynamic-plugins.yaml @@ -5,5 +5,5 @@ metadata: data: "dynamic-plugins.yaml": | plugins: - - package: "plugin-flavor3" + - package: "./plugin-flavor3" enabled: true diff --git a/pkg/model/testdata/testflavours/default-config/dynamic-plugins.yaml b/pkg/model/testdata/testflavours/default-config/dynamic-plugins.yaml index 16cad0afb..06ce0dd8a 100644 --- a/pkg/model/testdata/testflavours/default-config/dynamic-plugins.yaml +++ b/pkg/model/testdata/testflavours/default-config/dynamic-plugins.yaml @@ -7,5 +7,5 @@ data: includes: - dynamic-plugins.default.yaml plugins: - - package: "plugin-base" + - package: "./plugin-base" enabled: true diff --git a/pkg/model/testdata/testflavours/default-config/flavours/flavor1/dynamic-plugins.yaml b/pkg/model/testdata/testflavours/default-config/flavours/flavor1/dynamic-plugins.yaml index d0f1e9623..6b62cb6e6 100644 --- a/pkg/model/testdata/testflavours/default-config/flavours/flavor1/dynamic-plugins.yaml +++ b/pkg/model/testdata/testflavours/default-config/flavours/flavor1/dynamic-plugins.yaml @@ -5,5 +5,5 @@ metadata: data: "dynamic-plugins.yaml": | plugins: - - package: "plugin-flavor1" + - package: "./plugin-flavor1" enabled: true diff --git a/pkg/model/testdata/testflavours/default-config/flavours/flavor2/dynamic-plugins.yaml b/pkg/model/testdata/testflavours/default-config/flavours/flavor2/dynamic-plugins.yaml index f3d963818..9afb182a9 100644 --- a/pkg/model/testdata/testflavours/default-config/flavours/flavor2/dynamic-plugins.yaml +++ b/pkg/model/testdata/testflavours/default-config/flavours/flavor2/dynamic-plugins.yaml @@ -5,5 +5,5 @@ metadata: data: "dynamic-plugins.yaml": | plugins: - - package: "plugin-flavor2" + - package: "./plugin-flavor2" enabled: true