Skip to content

Configuration Support for registry+v1 bundle (w/ Helm aware) #2132

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions api/v1/clusterextension_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package v1

import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -92,6 +93,13 @@ type ClusterExtensionSpec struct {
//
// +optional
Install *ClusterExtensionInstallConfig `json:"install,omitempty"`

// config contains arbitrary JSON configuration values to be applied at render time.
// These values will be merged into the bundle manifests during rendering.
// +optional
// +kubebuilder:validation:Type=object
// +kubebuilder:pruning:PreserveUnknownFields
Config *apiextensionsv1.JSON `json:"config,omitempty"`
}

const SourceTypeCatalog = "Catalog"
Expand Down
4 changes: 4 additions & 0 deletions api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

118 changes: 118 additions & 0 deletions cmd/demo-config/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package main

import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"

"sigs.k8s.io/yaml"

"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/engine"

"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source"
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1"
)

func main() {
helmFlag := flag.Bool("helm", false, "render as Helm chart instead of registry+v1 bundle")
flag.Parse()
if flag.NArg() != 2 {
fmt.Fprintf(os.Stderr, "usage: %s [-helm] <bundle-dir> <install-namespace>\n", os.Args[0])
os.Exit(1)
}
bundleDir := flag.Arg(0)
installNs := flag.Arg(1)

// load optional configuration values from config.yaml
cfg := map[string]interface{}{}
configFile := filepath.Join(bundleDir, "config.yaml")
if data, err := os.ReadFile(configFile); err == nil {
if err := yaml.Unmarshal(data, &cfg); err != nil {
fmt.Fprintf(os.Stderr, "error unmarshalling config file %q: %v\n", configFile, err)
os.Exit(1)
}
}

if *helmFlag {
// ensure a Helm chart is present
// look for Chart.yaml in the specified dir, or try replacing "bundles" -> "charts" if not found
chartPath := filepath.Join(bundleDir, "Chart.yaml")
if _, err := os.Stat(chartPath); err != nil {
// fallback: swap a "bundles" segment to "charts" in the path
parts := strings.Split(bundleDir, string(os.PathSeparator))
for i, p := range parts {
if p == "bundles" {
parts[i] = "charts"
altDir := filepath.Join(parts...)
if _, err2 := os.Stat(filepath.Join(altDir, "Chart.yaml")); err2 == nil {
bundleDir = altDir
chartPath = filepath.Join(bundleDir, "Chart.yaml")
break
}
}
}
if _, err := os.Stat(chartPath); err != nil {
fmt.Fprintf(os.Stderr, "error: helm chart not found in %q: %v\n", bundleDir, err)
os.Exit(1)
}
}
// render with the Helm engine
chrt, err := loader.Load(bundleDir)
if err != nil {
fmt.Fprintf(os.Stderr, "error loading Helm chart: %v\n", err)
os.Exit(1)
}
// load values.yaml and apply any config overrides
values := map[string]interface{}{}
valuesFile := filepath.Join(bundleDir, "values.yaml")
if data, err := os.ReadFile(valuesFile); err == nil {
if err := yaml.Unmarshal(data, &values); err != nil {
fmt.Fprintf(os.Stderr, "error unmarshalling values file %q: %v\n", valuesFile, err)
os.Exit(1)
}
}
for k, v := range cfg {
values[k] = v
}
// render the chart templates using the provided values under the 'Values' key
renderContext := chartutil.Values{"Values": values}
rendered, err := engine.Engine{}.Render(chrt, renderContext)
if err != nil {
fmt.Fprintf(os.Stderr, "error rendering Helm chart: %v\n", err)
os.Exit(1)
}
for name, content := range rendered {
fmt.Printf("---\n# Source: %s\n%s\n", name, content)
}
return
}

// parse registry+v1 bundle
regv1, err := source.FromFS(os.DirFS(bundleDir)).GetBundle()
if err != nil {
fmt.Fprintf(os.Stderr, "error parsing bundle directory: %v\n", err)
os.Exit(1)
}

// render bundle with configuration
opts := []render.Option{render.WithConfig(cfg)}
objs, err := registryv1.Renderer.Render(regv1, installNs, opts...)
if err != nil {
fmt.Fprintf(os.Stderr, "error rendering bundle: %v\n", err)
os.Exit(1)
}

for _, obj := range objs {
data, err := yaml.Marshal(obj)
if err != nil {
fmt.Fprintf(os.Stderr, "error marshaling object: %v\n", err)
os.Exit(1)
}
fmt.Printf("---\n%s", string(data))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ spec:
description: spec is an optional field that defines the desired state
of the ClusterExtension.
properties:
config:
description: |-
config contains arbitrary JSON configuration values to be applied at render time.
These values will be merged into the bundle manifests during rendering.
type: object
x-kubernetes-preserve-unknown-fields: true
install:
description: |-
install is an optional field used to configure the installation options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ spec:
description: spec is an optional field that defines the desired state
of the ClusterExtension.
properties:
config:
description: |-
config contains arbitrary JSON configuration values to be applied at render time.
These values will be merged into the bundle manifests during rendering.
type: object
x-kubernetes-preserve-unknown-fields: true
install:
description: |-
install is an optional field used to configure the installation options
Expand Down
9 changes: 6 additions & 3 deletions docs/tutorials/install-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ For information on determining the ServiceAccount's permission, please see [Deri

## Procedure

1. Create a CR for the Kubernetes extension you want to install:
1. Create a CR for the Kubernetes extension you want to install. You can also specify arbitrary configuration values under `spec.config` (per [RFC: Registry+v1 Configuration Support](../../RFC_Config_registry+v1_bundle_config.md)):

``` yaml title="Example CR"
```yaml title="Example CR"
apiVersion: olm.operatorframework.io/v1
kind: ClusterExtension
metadata:
Expand All @@ -46,8 +46,11 @@ For information on determining the ServiceAccount's permission, please see [Deri
sourceType: Catalog
catalog:
packageName: <package_name>
channels: [<channel1>,<channel2]
channels: [<channel1>,<channel2>]
version: "<version>"
config:
version: "v2.0.0-demo"
name: "demo-configmap"
```

`extension_name`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ type ClusterExtensionSpec struct {
//
// +optional
Install *ClusterExtensionInstallConfig `json:"install,omitempty"`

// config contains arbitrary configuration values to be applied at render time.
// These values will be merged into the bundle manifests during rendering.
// +optional
Config map[string]interface{} `json:"config,omitempty"`
}

const SourceTypeCatalog = "Catalog"
Expand Down
10 changes: 9 additions & 1 deletion internal/operator-controller/applier/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package applier
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -126,7 +127,14 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte
if err != nil {
return nil, "", err
}
values := chartutil.Values{}
// merge in any user-provided config JSON as Helm values
valuesMap := map[string]interface{}{}
if ext.Spec.Config != nil && ext.Spec.Config.Raw != nil {
if err := json.Unmarshal(ext.Spec.Config.Raw, &valuesMap); err != nil {
return nil, "", fmt.Errorf("invalid JSON in spec.config: %w", err)
}
}
values := chartutil.Values{"Values": valuesMap}

post := &postrenderer{
labels: objectLabels,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
rbacv1 "k8s.io/api/rbac/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/utils/ptr"
Expand Down Expand Up @@ -281,6 +282,30 @@ func BundleAdditionalResourcesGenerator(rv1 *bundle.RegistryV1, opts render.Opti

objs = append(objs, obj)
}
// apply overrides from configuration, if present
if opts.Config != nil {
for _, obj := range objs {
if obj.GetObjectKind().GroupVersionKind().Kind == "ConfigMap" {
u, ok := obj.(*unstructured.Unstructured)
if !ok {
continue
}
data, found, err := unstructured.NestedStringMap(u.Object, "data")
if err != nil {
return nil, err
}
if !found {
data = map[string]string{}
}
for k, v := range opts.Config {
data[k] = fmt.Sprintf("%v", v)
}
if err := unstructured.SetNestedStringMap(u.Object, data, "data"); err != nil {
return nil, err
}
}
}
}
return objs, nil
}

Expand Down
9 changes: 9 additions & 0 deletions internal/operator-controller/rukpak/render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ type Options struct {
TargetNamespaces []string
UniqueNameGenerator UniqueNameGenerator
CertificateProvider CertificateProvider
// Config holds arbitrary configuration values provided at render time
Config map[string]interface{}
}

func (o *Options) apply(opts ...Option) *Options {
Expand Down Expand Up @@ -94,6 +96,13 @@ func WithTargetNamespaces(namespaces ...string) Option {
}
}

// WithConfig supplies configuration values to be used during bundle rendering
func WithConfig(cfg map[string]interface{}) Option {
return func(o *Options) {
o.Config = cfg
}
}

func WithUniqueNameGenerator(generator UniqueNameGenerator) Option {
return func(o *Options) {
o.UniqueNameGenerator = generator
Expand Down
6 changes: 6 additions & 0 deletions manifests/experimental-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,12 @@ spec:
description: spec is an optional field that defines the desired state
of the ClusterExtension.
properties:
config:
description: |-
config contains arbitrary JSON configuration values to be applied at render time.
These values will be merged into the bundle manifests during rendering.
type: object
x-kubernetes-preserve-unknown-fields: true
install:
description: |-
install is an optional field used to configure the installation options
Expand Down
6 changes: 6 additions & 0 deletions manifests/experimental.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,12 @@ spec:
description: spec is an optional field that defines the desired state
of the ClusterExtension.
properties:
config:
description: |-
config contains arbitrary JSON configuration values to be applied at render time.
These values will be merged into the bundle manifests during rendering.
type: object
x-kubernetes-preserve-unknown-fields: true
install:
description: |-
install is an optional field used to configure the installation options
Expand Down
6 changes: 6 additions & 0 deletions manifests/standard-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,12 @@ spec:
description: spec is an optional field that defines the desired state
of the ClusterExtension.
properties:
config:
description: |-
config contains arbitrary JSON configuration values to be applied at render time.
These values will be merged into the bundle manifests during rendering.
type: object
x-kubernetes-preserve-unknown-fields: true
install:
description: |-
install is an optional field used to configure the installation options
Expand Down
6 changes: 6 additions & 0 deletions manifests/standard.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,12 @@ spec:
description: spec is an optional field that defines the desired state
of the ClusterExtension.
properties:
config:
description: |-
config contains arbitrary JSON configuration values to be applied at render time.
These values will be merged into the bundle manifests during rendering.
type: object
x-kubernetes-preserve-unknown-fields: true
install:
description: |-
install is an optional field used to configure the installation options
Expand Down
16 changes: 14 additions & 2 deletions test/convert/generate-manifests.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,20 @@ func generateManifests(outputPath, bundleDir, installNamespace, watchNamespace s
os.Exit(1)
}

// Convert RegistryV1 to plain manifests
objs, err := registryv1.Renderer.Render(regv1, installNamespace, render.WithTargetNamespaces(watchNamespace))
// load optional configuration for registry+v1 bundle if present
cfg := map[string]interface{}{}
configFile := filepath.Join(bundleDir, "config.yaml")
if data, err := os.ReadFile(configFile); err == nil {
if err := yaml.Unmarshal(data, &cfg); err != nil {
return fmt.Errorf("error unmarshalling config file %q: %w", configFile, err)
}
}
// Convert RegistryV1 to plain manifests, applying any configuration
opts := []render.Option{render.WithTargetNamespaces(watchNamespace)}
if cfg != nil {
opts = append(opts, render.WithConfig(cfg))
}
objs, err := registryv1.Renderer.Render(regv1, installNamespace, opts...)
if err != nil {
return fmt.Errorf("error converting registry+v1 bundle: %w", err)
}
Expand Down
2 changes: 2 additions & 0 deletions testdata/images/bundles/test-operator/v2.0.0/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
version: "v2.0.0-demo"
name: "demo-configmap"
4 changes: 4 additions & 0 deletions testdata/images/charts/test-operator/v2.0.0/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
apiVersion: v2
name: test-operator
version: 2.0.0
appVersion: v2.0.0
2 changes: 2 additions & 0 deletions testdata/images/charts/test-operator/v2.0.0/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
version: "v2.0.0-demo"
name: "demo-configmap"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: test-configmap
data:
version: "{{ index .Values "version" }}"
name: "{{ index .Values "name" }}"
Loading
Loading