Skip to content
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
15 changes: 15 additions & 0 deletions internal/operator-controller/applier/boxcutter.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"maps"
"slices"
"strings"
"unicode/utf8"

"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
Expand Down Expand Up @@ -40,6 +41,9 @@ import (

const (
ClusterExtensionRevisionRetentionLimit = 5

maxBundleConfigAnnotationBytes = 50 * 1024 // 50KB
bundleConfigTruncatedSuffix = "<truncated due to size limit>"
)

type ClusterExtensionRevisionGenerator interface {
Expand Down Expand Up @@ -206,6 +210,17 @@ func (r *SimpleRevisionGenerator) buildClusterExtensionRevision(
}
annotations[labels.ServiceAccountNameKey] = ext.Spec.ServiceAccount.Name
annotations[labels.ServiceAccountNamespaceKey] = ext.Spec.Namespace
if ext.Spec.Config != nil && ext.Spec.Config.Inline != nil {
value := string(ext.Spec.Config.Inline.Raw)
if len(value) > maxBundleConfigAnnotationBytes {
maxContent := maxBundleConfigAnnotationBytes - len(bundleConfigTruncatedSuffix)
for maxContent > 0 && !utf8.RuneStart(value[maxContent]) {
maxContent--
}
value = value[:maxContent] + bundleConfigTruncatedSuffix
}
annotations[labels.BundleConfigKey] = value
}

phases := PhaseSort(objects)

Expand Down
71 changes: 71 additions & 0 deletions internal/operator-controller/applier/boxcutter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -481,6 +482,76 @@ func Test_SimpleRevisionGenerator_PropagatesProgressDeadlineMinutes(t *testing.T
}
}

func Test_SimpleRevisionGenerator_BundleConfigAnnotation(t *testing.T) {
r := &FakeManifestProvider{
GetFn: func(b fs.FS, e *ocv1.ClusterExtension) ([]client.Object, error) {
return []client.Object{}, nil
},
}

b := applier.SimpleRevisionGenerator{
Scheme: k8scheme.Scheme,
ManifestProvider: r,
}

for name, tc := range map[string]struct {
config *ocv1.ClusterExtensionConfig
wantAnnot bool
wantValue string
}{
"annotation set when inline config is present": {
config: &ocv1.ClusterExtensionConfig{
ConfigType: ocv1.ClusterExtensionConfigTypeInline,
Inline: &apiextensionsv1.JSON{Raw: []byte(`{"watchNamespace":"ns1"}`)},
},
wantAnnot: true,
wantValue: `{"watchNamespace":"ns1"}`,
},
"annotation not set when config is nil": {
config: nil,
wantAnnot: false,
},
"annotation not set when inline is nil": {
config: &ocv1.ClusterExtensionConfig{
ConfigType: ocv1.ClusterExtensionConfigTypeInline,
},
wantAnnot: false,
},
"annotation is truncated when inline config exceeds 50KB": {
config: &ocv1.ClusterExtensionConfig{
ConfigType: ocv1.ClusterExtensionConfigTypeInline,
Inline: &apiextensionsv1.JSON{Raw: []byte(`{"key":"` + strings.Repeat("x", 50*1024) + `"}`)},
},
wantAnnot: true,
wantValue: (`{"key":"` + strings.Repeat("x", 50*1024))[:50*1024-len("<truncated due to size limit>")] + "<truncated due to size limit>",
},
} {
t.Run(name, func(t *testing.T) {
ext := &ocv1.ClusterExtension{
ObjectMeta: metav1.ObjectMeta{
Name: "test-extension",
},
Spec: ocv1.ClusterExtensionSpec{
Namespace: "test-namespace",
ServiceAccount: ocv1.ServiceAccountReference{Name: "test-sa"},
Config: tc.config,
},
}
rev, err := b.GenerateRevision(t.Context(), dummyBundle, ext, map[string]string{}, map[string]string{})
require.NoError(t, err)

val, ok := rev.Annotations[labels.BundleConfigKey]
if tc.wantAnnot {
require.True(t, ok, "expected bundle-config annotation to be present")
require.Equal(t, tc.wantValue, val)
require.LessOrEqual(t, len(val), 50*1024, "annotation value must not exceed 50KB")
} else {
require.False(t, ok, "expected bundle-config annotation to not be present")
}
})
}
}

func Test_SimpleRevisionGenerator_Failure(t *testing.T) {
r := &FakeManifestProvider{
GetFn: func(b fs.FS, e *ocv1.ClusterExtension) ([]client.Object, error) {
Expand Down
7 changes: 7 additions & 0 deletions internal/operator-controller/labels/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ const (
// ClusterExtensionRevision operations is preserved.
ServiceAccountNamespaceKey = "olm.operatorframework.io/service-account-namespace"

// BundleConfigKey is the annotation key used to record the inline bundle
// configuration from the owning ClusterExtension. It is applied as an
// annotation on ClusterExtensionRevision resources when the ClusterExtension
// has .spec.config.inline set. The value is a JSON string of the configuration.
// Values over 50KB are truncated to avoid overrunning metadata size.
BundleConfigKey = "olm.operatorframework.io/bundle-config"

// MigratedFromHelmKey is the label key used to mark ClusterExtensionRevisions
// that were created during migration from Helm releases. This label is used
// to distinguish migrated revisions from those created by normal Boxcutter operation.
Expand Down
30 changes: 30 additions & 0 deletions test/e2e/features/install.feature
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,36 @@ Feature: Install ClusterExtension
And ClusterExtensionRevision "${NAME}-1" has label "olm.operatorframework.io/owner-kind" with value "ClusterExtension"
And ClusterExtensionRevision "${NAME}-1" has label "olm.operatorframework.io/owner-name" with value "${NAME}"

@BoxcutterRuntime
Scenario: ClusterExtensionRevision is annotated with bundle config when inline config is set
When ClusterExtension is applied
"""
apiVersion: olm.operatorframework.io/v1
kind: ClusterExtension
metadata:
name: ${NAME}
spec:
namespace: ${TEST_NAMESPACE}
serviceAccount:
name: olm-sa
config:
configType: Inline
inline:
watchNamespace: ${TEST_NAMESPACE}
source:
sourceType: Catalog
catalog:
packageName: own-namespace-operator
selector:
matchLabels:
"olm.operatorframework.io/metadata.name": test-catalog
"""
Then ClusterExtension is rolled out
And ClusterExtensionRevision "${NAME}-1" contains annotation "olm.operatorframework.io/bundle-config" with value
"""
{"watchNamespace":"${TEST_NAMESPACE}"}
"""

@DeploymentConfig
Scenario: deploymentConfig nodeSelector is applied to the operator deployment
When ClusterExtension is applied
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/steps/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ func ClusterExtensionRevisionHasAnnotationWithValue(ctx context.Context, revisio
revisionName = substituteScenarioVars(strings.TrimSpace(revisionName), sc)
expectedValue := ""
if annotationValue != nil {
expectedValue = annotationValue.Content
expectedValue = substituteScenarioVars(strings.TrimSpace(annotationValue.Content), sc)
}
waitFor(ctx, func() bool {
obj, err := getResource("clusterextensionrevision", revisionName, "")
Expand Down
Loading