Skip to content
Open
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
31 changes: 31 additions & 0 deletions api/v1alpha1/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package v1alpha1

import (
"strings"

"github.com/IBM/ibm-licensing-operator/api/v1alpha1/features"
)

Expand Down Expand Up @@ -47,6 +49,11 @@ type Features struct {
// +optional
NamespaceScopeEnabled *bool `json:"nssEnabled,omitempty"`

// Comma-separated list of namespaces to exclude during aggregation. Regex patterns are also supported.
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Exclude Namespaces",xDescriptors="urn:alm:descriptor:com.tectonic.ui:text"
// +optional
ExcludeNamespace string `json:"excludeNamespace,omitempty"`

// Enables node CPU capping. When false, the operand will skip calls to the Kubernetes node API; node-capping is not applied and metrics may exceed
// real node capacity. Defaults to true.
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Node CPU Capping Enabled",xDescriptors="urn:alm:descriptor:com.tectonic.ui:booleanSwitch"
Expand Down Expand Up @@ -84,6 +91,30 @@ func (spec *IBMLicensingSpec) IsNamespaceScopeEnabled() bool {
return spec.HaveFeatures() && spec.Features.NamespaceScopeEnabled != nil && *spec.Features.NamespaceScopeEnabled
}

func (spec *IBMLicensingSpec) GetSanitizedExcludeNamespace() string {
if !spec.HaveFeatures() {
return ""
}

namespacesLookup := map[string]struct{}{}
dedupedNamespaces := []string{}

for namespace := range strings.SplitSeq(spec.Features.ExcludeNamespace, ",") {
trimmedNamespace := strings.TrimSpace(namespace)

if trimmedNamespace == "" {
continue
}

if _, ok := namespacesLookup[trimmedNamespace]; !ok {
namespacesLookup[trimmedNamespace] = struct{}{}
dedupedNamespaces = append(dedupedNamespaces, trimmedNamespace)
}
}

return strings.Join(dedupedNamespaces, ",")
}

func (spec *IBMLicensingSpec) IsKubeRBACAuthEnabled() bool {
if !spec.HaveFeatures() || spec.Features.KubeRBACAuthEnabled == nil {
return true
Expand Down
54 changes: 54 additions & 0 deletions api/v1alpha1/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,57 @@ func TestIsNodeCpuCappingEnabledExplicitFalse(t *testing.T) {
assert.False(t, spec.IsNodeCpuCappingEnabled(),
"NodeCpuCappingEnabled=false should return false.")
}

func TestGetSanitizedExcludeNamespaceNoFeatures(t *testing.T) {
spec := &IBMLicensingSpec{}
assert.Equal(t, "", spec.GetSanitizedExcludeNamespace(),
"No features block should return empty string.")
}

func TestGetSanitizedExcludeNamespaceEmptyString(t *testing.T) {
spec := &IBMLicensingSpec{Features: &Features{ExcludeNamespace: ""}}
assert.Equal(t, "", spec.GetSanitizedExcludeNamespace(),
"Empty ExcludeNamespace should return empty string.")
}

func TestGetSanitizedExcludeNamespaceSingleNamespace(t *testing.T) {
spec := &IBMLicensingSpec{Features: &Features{ExcludeNamespace: "namespace-a"}}
assert.Equal(t, "namespace-a", spec.GetSanitizedExcludeNamespace(),
"Single namespace should be returned as-is.")
}

func TestGetSanitizedExcludeNamespaceMultipleNamespaces(t *testing.T) {
spec := &IBMLicensingSpec{Features: &Features{ExcludeNamespace: "namespace-a,namespace-b,namespace-c"}}
assert.Equal(t, "namespace-a,namespace-b,namespace-c", spec.GetSanitizedExcludeNamespace(),
"Multiple distinct namespaces should all be returned.")
}

func TestGetSanitizedExcludeNamespaceTrimsWhitespace(t *testing.T) {
spec := &IBMLicensingSpec{Features: &Features{ExcludeNamespace: " namespace-a , namespace-b , namespace-c "}}
assert.Equal(t, "namespace-a,namespace-b,namespace-c", spec.GetSanitizedExcludeNamespace(),
"Whitespace around namespace names should be trimmed.")
}

func TestGetSanitizedExcludeNamespaceRemovesDuplicates(t *testing.T) {
spec := &IBMLicensingSpec{Features: &Features{ExcludeNamespace: "namespace-a,namespace-b,namespace-a"}}
assert.Equal(t, "namespace-a,namespace-b", spec.GetSanitizedExcludeNamespace(),
"Duplicate namespaces should be removed, preserving first-seen order.")
}

func TestGetSanitizedExcludeNamespaceRemovesEmptyEntries(t *testing.T) {
spec := &IBMLicensingSpec{Features: &Features{ExcludeNamespace: "namespace-a,,namespace-b,"}}
assert.Equal(t, "namespace-a,namespace-b", spec.GetSanitizedExcludeNamespace(),
"Empty entries from consecutive commas or trailing comma should be removed.")
}

func TestGetSanitizedExcludeNamespaceRegexPattern(t *testing.T) {
spec := &IBMLicensingSpec{Features: &Features{ExcludeNamespace: "products-[a-zA-Z]+,database-[0-9]+"}}
assert.Equal(t, "products-[a-zA-Z]+,database-[0-9]+", spec.GetSanitizedExcludeNamespace(),
"Regex patterns should be preserved as-is.")
}

func TestGetSanitizedExcludeNamespaceWhitespaceOnlyEntry(t *testing.T) {
spec := &IBMLicensingSpec{Features: &Features{ExcludeNamespace: "namespace-a, ,namespace-b"}}
assert.Equal(t, "namespace-a,namespace-b", spec.GetSanitizedExcludeNamespace(),
"Whitespace-only entries should be treated as empty and dropped.")
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ metadata:
categories: Monitoring
certified: "false"
containerImage: icr.io/cpopen/ibm-licensing-operator:4.2.24
createdAt: "2026-05-11T09:09:00Z"
createdAt: "2026-07-17T11:06:24Z"
description: The IBM Licensing Operator provides a Kubernetes CRD-Based API to monitor the license usage of products.
features.operators.openshift.io/disconnected: "true"
features.operators.openshift.io/fips-compliant: "true"
Expand Down Expand Up @@ -219,6 +219,11 @@ spec:
path: features.auth.urlBasedEnabled
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:hidden
- description: Comma-separated list of namespaces to exclude during aggregation. Regex patterns are also supported.
displayName: Exclude Namespaces
path: features.excludeNamespace
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:text
- description: Configure if you have HyperThreading (HT) or Symmetrical Multi-Threading (SMT) enabled
displayName: Hyper Threading
path: features.hyperThreading
Expand Down
4 changes: 4 additions & 0 deletions bundle/manifests/operator.ibm.com_ibmlicensings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ spec:
description: Enables access to License Service custom resources.
Defaults to true.
type: boolean
excludeNamespace:
description: Comma-separated list of namespaces to exclude during
aggregation. Regex patterns are also supported.
type: string
hyperThreading:
description: Configure if you have HyperThreading (HT) or Symmetrical
Multi-Threading (SMT) enabled
Expand Down
4 changes: 4 additions & 0 deletions config/crd/bases/operator.ibm.com_ibmlicensings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ spec:
description: Enables access to License Service custom resources.
Defaults to true.
type: boolean
excludeNamespace:
description: Comma-separated list of namespaces to exclude during
aggregation. Regex patterns are also supported.
type: string
hyperThreading:
description: Configure if you have HyperThreading (HT) or Symmetrical
Multi-Threading (SMT) enabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ spec:
path: features.auth.urlBasedEnabled
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:hidden
- description: Comma-separated list of namespaces to exclude during aggregation.
Regex patterns are also supported.
displayName: Exclude Namespaces
path: features.excludeNamespace
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:text
- description: Configure if you have HyperThreading (HT) or Symmetrical Multi-Threading
(SMT) enabled
displayName: Hyper Threading
Expand Down
8 changes: 8 additions & 0 deletions controllers/resources/service/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
softwareCentralDefaultFrequency = "5 0 * * *"
)

//nolint:gocyclo
func getLicensingEnvironmentVariables(spec operatorv1alpha1.IBMLicensingSpec) []corev1.EnvVar {
var httpsEnableString = strconv.FormatBool(spec.HTTPSEnable)
var environmentVariables = []corev1.EnvVar{
Expand Down Expand Up @@ -152,6 +153,13 @@ func getLicensingEnvironmentVariables(spec operatorv1alpha1.IBMLicensingSpec) []
})
}
}
excludeNamespace := spec.GetSanitizedExcludeNamespace()
if excludeNamespace != "" && !spec.IsNamespaceScopeEnabled() {
environmentVariables = append(environmentVariables, corev1.EnvVar{
Name: "EXCLUDE_NAMESPACE",
Value: excludeNamespace,
})
}
if spec.ChargebackRetentionPeriod != nil {
environmentVariables = append(environmentVariables, corev1.EnvVar{
Name: "CONTRIBUTIONS_DATA_RETENTION",
Expand Down
68 changes: 68 additions & 0 deletions controllers/resources/service/containers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,74 @@ func TestGetLicensingEnvironmentVariablesCustomResourcesEnabledExplicitFalse(t *
"CustomResourcesEnabled=false, CUSTOM_RESOURCES_ENABLED=false should be added to Licensing pod.")
}

func TestGetLicensingEnvironmentVariablesExcludeNamespaceSet(t *testing.T) {
spec := operatorv1alpha1.IBMLicensingSpec{
InstanceNamespace: "namespace",
Datasource: "datacollector",
Features: &operatorv1alpha1.Features{
ExcludeNamespace: "ns-a,ns-b",
},
}

envVars := getLicensingEnvironmentVariables(spec)
assert.Contains(t, envVars, corev1.EnvVar{Name: "EXCLUDE_NAMESPACE", Value: "ns-a,ns-b"},
"ExcludeNamespace set with NSS disabled, EXCLUDE_NAMESPACE should be added to Licensing pod.")
}

func TestGetLicensingEnvironmentVariablesExcludeNamespaceNotSetWhenNSSEnabled(t *testing.T) {
t.Setenv("WATCH_NAMESPACE", "ibm-licensing")

spec := operatorv1alpha1.IBMLicensingSpec{
InstanceNamespace: "namespace",
Datasource: "datacollector",
Features: &operatorv1alpha1.Features{
ExcludeNamespace: "ns-a,ns-b",
NamespaceScopeEnabled: ptr.To(true),
},
}

envVars := getLicensingEnvironmentVariables(spec)
assert.False(t, ContainsEnvVar(envVars, "EXCLUDE_NAMESPACE"),
"NSS is enabled, EXCLUDE_NAMESPACE should not be added to Licensing pod.")
}

func TestGetLicensingEnvironmentVariablesExcludeNamespaceNotSetWhenEmpty(t *testing.T) {
spec := operatorv1alpha1.IBMLicensingSpec{
InstanceNamespace: "namespace",
Datasource: "datacollector",
Features: &operatorv1alpha1.Features{ExcludeNamespace: ""},
}

envVars := getLicensingEnvironmentVariables(spec)
assert.False(t, ContainsEnvVar(envVars, "EXCLUDE_NAMESPACE"),
"ExcludeNamespace is empty, EXCLUDE_NAMESPACE should not be added to Licensing pod.")
}

func TestGetLicensingEnvironmentVariablesExcludeNamespaceNotSetWhenFeaturesNil(t *testing.T) {
spec := operatorv1alpha1.IBMLicensingSpec{
InstanceNamespace: "namespace",
Datasource: "datacollector",
}

envVars := getLicensingEnvironmentVariables(spec)
assert.False(t, ContainsEnvVar(envVars, "EXCLUDE_NAMESPACE"),
"Features is nil, EXCLUDE_NAMESPACE should not be added to Licensing pod.")
}

func TestGetLicensingEnvironmentVariablesExcludeNamespaceSanitized(t *testing.T) {
spec := operatorv1alpha1.IBMLicensingSpec{
InstanceNamespace: "namespace",
Datasource: "datacollector",
Features: &operatorv1alpha1.Features{
ExcludeNamespace: " ns-a , ns-b , ns-a ",
},
}

envVars := getLicensingEnvironmentVariables(spec)
assert.Contains(t, envVars, corev1.EnvVar{Name: "EXCLUDE_NAMESPACE", Value: "ns-a,ns-b"},
"ExcludeNamespace with whitespace and duplicates should be sanitized before being set as env var.")
}

func Contains[T comparable](s []T, e T) bool {
for _, v := range s {
if v == e {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,10 @@ spec:
customResourcesEnabled:
description: Enables access to License Service custom resources. Defaults to true.
type: boolean
excludeNamespace:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change was not autogenerated, added CRD parameter was hand-picked and moved here to helm CRD

description: Comma-separated list of namespaces to exclude
during aggregation. Regex patterns are also supported.
type: string
hyperThreading:
description: Configure if you have HyperThreading (HT) or Symmetrical Multi-Threading (SMT) enabled
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ibmLicensing:
httpsEnable: true
features:
nssEnabled: false
excludeNamespace: ""

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to have "" as default or maybe just hide excludeNamespace param in values.yaml?

nodeCpuCappingEnabled: true
kubeRBACAuthEnabled: true
operandRequestsEnabled: true
Expand Down
8 changes: 8 additions & 0 deletions helm-no-operator/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ spec:
value: {{ .Values.ibmLicensing.spec.features.kubeRBACAuthEnabled | quote }}
- name: CUSTOM_RESOURCES_ENABLED
value: {{ .Values.ibmLicensing.spec.features.customResourcesEnabled | quote }}
{{- if and (not .Values.ibmLicensing.spec.features.nssEnabled) (.Values.ibmLicensing.spec.features.excludeNamespace) }}
- name: EXCLUDE_NAMESPACE
value: {{ .Values.ibmLicensing.spec.features.excludeNamespace | quote }}
{{- end }}
Comment on lines +69 to +72

@Michal-Szczygiel Michal-Szczygiel Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helm chart follows the logic of the operator here: create EXCLUDE_NAMESPACE only when nssEnabled=false and excludeNamespace not empty (warning: no sanitization in helm version)

image: {{ .Values.global.imagePullPrefix }}/{{ .Values.ibmLicensing.imageRegistryNamespaceOperand }}/ibm-licensing:{{ .Values.ibmLicensing.ibmLicensingVersion }}
imagePullPolicy: IfNotPresent
livenessProbe:
Expand Down Expand Up @@ -158,6 +162,10 @@ spec:
value: {{ .Values.ibmLicensing.spec.features.kubeRBACAuthEnabled | quote }}
- name: CUSTOM_RESOURCES_ENABLED
value: {{ .Values.ibmLicensing.spec.features.customResourcesEnabled | quote }}
{{- if and (not .Values.ibmLicensing.spec.features.nssEnabled) (.Values.ibmLicensing.spec.features.excludeNamespace) }}
- name: EXCLUDE_NAMESPACE
value: {{ .Values.ibmLicensing.spec.features.excludeNamespace | quote }}
{{- end }}
Comment on lines +165 to +168

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image: {{ .Values.global.imagePullPrefix }}/{{ .Values.ibmLicensing.imageRegistryNamespaceOperand }}/ibm-licensing:{{ .Values.ibmLicensing.ibmLicensingVersion }}
imagePullPolicy: IfNotPresent
name: ocp-check-secret
Expand Down
1 change: 1 addition & 0 deletions helm-no-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ ibmLicensing:
enableInstanaMetricCollection: false
features:
nssEnabled: false
excludeNamespace: ""

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to have "" as default or maybe just hide excludeNamespace param in values.yaml?

nodeCpuCappingEnabled: true
kubeRBACAuthEnabled: true
customResourcesEnabled: true
Expand Down