Skip to content
Merged
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
59 changes: 37 additions & 22 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -179,15 +179,15 @@ run: manifests generate fmt vet ## Run a controller from your host.
# If you wish to build the manager image targeting other platforms you can use the --platform flag.
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
.PHONY: docker-build
docker-build: ## Build docker image with the manager.
.PHONY: image-build
image-build: ## Build operator image.
$(CONTAINER_TOOL) build -t ${IMG} .

.PHONY: docker-push
docker-push: ## Push docker image with the manager.
.PHONY: image-push
image-push: ## Push operator image.
$(CONTAINER_TOOL) push ${IMG}

# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple
# PLATFORMS defines the target platforms for the operator image be built to provide support to multiple
# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/
# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/
Expand All @@ -198,10 +198,10 @@ PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
docker-buildx: ## Build and push docker image for the manager for cross-platform support
# copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
- $(CONTAINER_TOOL) buildx create --name external-secrets-operator-builder
$(CONTAINER_TOOL) buildx use external-secrets-operator-builder
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
- $(CONTAINER_TOOL) buildx rm external-secrets-operator-builder
- docker buildx create --name external-secrets-operator-builder
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The docker is used instead of $(CONTAINER_TOOL) because, by default $(CONTAINER_TOOL) is set to podman, and buildx is supported by docker. Hence just here docker is hardcoded.

docker buildx use external-secrets-operator-builder
- docker buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
- docker buildx rm external-secrets-operator-builder
rm Dockerfile.cross

.PHONY: build-installer
Expand Down Expand Up @@ -240,7 +240,7 @@ LOCALBIN ?= $(shell pwd)/bin
$(LOCALBIN):
mkdir -p $(LOCALBIN)

## Location to story temp outputs
## Location to store temp outputs
OUTPUTS_PATH ?= $(shell pwd)/_output
$(OUTPUTS_PATH):
mkdir -p $(OUTPUTS_PATH)
Expand Down Expand Up @@ -293,16 +293,16 @@ govulncheck: $(LOCALBIN) ## Download govulncheck locally if necessary.
ginkgo: $(LOCALBIN) ## Download ginkgo locally if necessary.
$(call go-install-tool,$(GINKGO),github.com/onsi/ginkgo/v2/ginkgo)

# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
# go-install-tool will 'go install' any package with custom target and name of the binary.
# $1 - target path with name of binary
# $2 - package url which can be installed
define go-install-tool
@{ \
set -e; \
package=$(2) ;\
echo "Downloading $${package}" ;\
echo "Installing $${package}" ;\
rm -f $(1) || true ;\
GOBIN=$(LOCALBIN) go install $${package} ;\
GOBIN=$(LOCALBIN) GOFLAGS="-mod=vendor" go install $${package} ;\
}
endef

Expand Down Expand Up @@ -352,11 +352,11 @@ bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metada

.PHONY: bundle-build
bundle-build: ## Build the bundle image.
docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) .
$(CONTAINER_TOOL) build -f bundle.Dockerfile -t $(BUNDLE_IMG) .

.PHONY: bundle-push
bundle-push: ## Push the bundle image.
$(MAKE) docker-push IMG=$(BUNDLE_IMG)
$(CONTAINER_TOOL) push $(BUNDLE_IMG)

.PHONY: opm
OPM = $(LOCALBIN)/opm
Expand Down Expand Up @@ -392,12 +392,12 @@ endif
# https://github.com/operator-framework/community-operators/blob/7f1438c/docs/packaging-operator.md#updating-your-existing-operator
.PHONY: catalog-build
catalog-build: opm ## Build a catalog image.
$(OPM) index add --container-tool docker --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT)
$(OPM) index add --container-tool $(CONTAINER_TOOL) --mode semver --tag $(CATALOG_IMG) --bundles $(BUNDLE_IMGS) $(FROM_INDEX_OPT)

# Push the catalog image.
.PHONY: catalog-push
catalog-push: ## Push a catalog image.
$(MAKE) docker-push IMG=$(CATALOG_IMG)
$(CONTAINER_TOOL) push $(CATALOG_IMG)

## verify the changes are working as expected.
.PHONY: verify
Expand All @@ -419,15 +419,30 @@ docs: crd-ref-docs

## perform vulnerabilities scan using govulncheck.
.PHONY: govulnscan
#The ignored vulnerabilities are not in the operator code, but in the vendored packages.
# The ignored vulnerabilities are not in the operator code, but in the vendored packages.
# Each vulnerability ID corresponds to a specific issue that has been reviewed and deemed
# acceptable for the current vendored dependencies.
# - https://pkg.go.dev/vuln/GO-2025-3956
# - https://pkg.go.dev/vuln/GO-2025-3547
# - https://pkg.go.dev/vuln/GO-2025-3521
KNOWN_VULNERABILITIES:="GO-2025-3547|GO-2025-3521|GO-2025-3956|GO-2025-3915"
KNOWN_VULNERABILITIES=GO-2025-3956|GO-2025-3547|GO-2025-3521
govulnscan: govulncheck $(OUTPUTS_PATH) ## Run govulncheck
- $(GOVULNCHECK) ./... > $(OUTPUTS_PATH)/govulcheck.results 2>&1
$(eval reported_vulnerabilities = $(strip $(shell grep "pkg.go.dev" $(OUTPUTS_PATH)/govulcheck.results | ([ -n $KNOWN_VULNERABILITIES ] && grep -Ev $(KNOWN_VULNERABILITIES) || cat) | wc -l)))
@(if [ $(reported_vulnerabilities) -ne 0 ]; then echo -e "\n-- ERROR -- $(reported_vulnerabilities) new vulnerabilities reported, please check\n"; exit 1; fi)
@echo "Running govulncheck vulnerability scan..."
@$(GOVULNCHECK) ./... > $(OUTPUTS_PATH)/govulcheck.results 2>&1 || true
@grep -q "pkg.go.dev" $(OUTPUTS_PATH)/govulcheck.results || { \
echo "-- ERROR -- govulncheck may have failed to run; see $(OUTPUTS_PATH)/govulcheck.results"; exit 1; }
@echo "Filtering known vulnerabilities and counting new ones..."
$(eval reported_vulnerabilities = $(strip $(shell grep "pkg.go.dev" $(OUTPUTS_PATH)/govulcheck.results | grep -Ev "$(KNOWN_VULNERABILITIES)" | wc -l)))
@echo "Found $(reported_vulnerabilities) new vulnerabilities (excluding known issues)"
@(if [ $(reported_vulnerabilities) -ne 0 ]; then \
echo ""; \
echo "-- ERROR -- $(reported_vulnerabilities) new vulnerabilities reported"; \
echo "Please review $(OUTPUTS_PATH)/govulcheck.results for details"; \
echo ""; \
exit 1; \
else \
echo "✓ Vulnerability scan passed - no new issues found"; \
fi)

# Utilize controller-runtime provided envtest for API integration test
.PHONY: test-apis ## Run only the api integration tests.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ metadata:
categories: Security
console.openshift.io/disable-operand-delete: "true"
containerImage: openshift.io/external-secrets-operator:latest
createdAt: "2025-10-09T11:13:16Z"
createdAt: "2025-10-09T14:41:51Z"
features.operators.openshift.io/cnf: "false"
features.operators.openshift.io/cni: "false"
features.operators.openshift.io/csi: "false"
Expand Down Expand Up @@ -756,12 +756,9 @@ spec:
initialDelaySeconds: 5
periodSeconds: 10
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 10m
memory: 64Mi
cpu: 100m
memory: 1Gi
securityContext:
allowPrivilegeEscalation: false
capabilities:
Expand Down
6 changes: 5 additions & 1 deletion cmd/external-secrets-operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,11 @@ func main() {
metricsServerOptions.KeyName = metricsKeyFileName
}
metricsTLSOpts = append(metricsTLSOpts, func(c *tls.Config) {
certPool := x509.NewCertPool()
certPool, err := x509.SystemCertPool()
if err != nil {
setupLog.Info("unable to load system certificate pool", "error", err)
certPool = x509.NewCertPool()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

[claude-generated] Critical: Hard exit on SystemCertPool() failure could break deployments. Consider adding a fallback:

certPool, err := x509.SystemCertPool()
if err != nil {
    setupLog.Info("system cert pool unavailable, using empty pool", "error", err)
    certPool = x509.NewCertPool()
}

Copy link
Contributor

Choose a reason for hiding this comment

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

like claude mentioned above, is it really a fatal error if SystemCertPool() cannot be fetched that we need to do a hard exit?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I feel yes, atleast in RHEL which is our base image, the system CA certificates will always be present, and if at all failure occurs it would be a genuine failure and we don't want proceed I think.

openshiftCACert, err := os.ReadFile(openshiftCACertificateFile)
if err != nil {
setupLog.Error(err, "failed to read OpenShift CA certificate")
Expand Down
7 changes: 2 additions & 5 deletions config/manager/manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,8 @@ spec:
initialDelaySeconds: 5
periodSeconds: 10
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 10m
memory: 64Mi
cpu: 100m
memory: 1Gi
Copy link
Contributor

Choose a reason for hiding this comment

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

[claude-generated] Critical: CPU request increased from 10m to 100m (10x) and memory from 64Mi to 1Gi (16x). Please provide:

  • Performance test results showing why 1Gi memory is needed
  • CPU utilization metrics from production/staging
  • Impact analysis on resource-constrained clusters

Copy link
Contributor

Choose a reason for hiding this comment

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

inline with above claude comment, curious to know 1Gi memory need? Most of it is cache or is there any other significant use of memory in this operator?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was decided based on the observation added in this comment. With just one additional day-2 operator, the memory reached 512Mib when the operator started. And I think it's safe to keep 1Gi considering the large clusters.

Copy link
Contributor

Choose a reason for hiding this comment

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

i have not seen the memory go beyond 62MiB for the operator POD.. i started a k top pod in while loop and then installed the operator and created the ExternalSecretsConfig CR on my GCP cluster.

while true; do kubectl -n external-secrets-operator top pod; done
No resources found in external-secrets-operator namespace.
No resources found in external-secrets-operator namespace.
// Installed operator from console
error: metrics not available yet
error: metrics not available yet
// Operator came up
NAME                                                            CPU(cores)   MEMORY(bytes)
external-secrets-operator-controller-manager-5685dc9cf7-kghkn   34m          62Mi
NAME                                                            CPU(cores)   MEMORY(bytes)
external-secrets-operator-controller-manager-5685dc9cf7-kghkn   34m          62Mi
...
...
external-secrets-operator-controller-manager-5685dc9cf7-kghkn   1m           57Mi
NAME                                                            CPU(cores)   MEMORY(bytes)
external-secrets-operator-controller-manager-5685dc9cf7-kghkn   1m           57Mi
...
...
// Created ExternalSecretsConfig
external-secrets-operator-controller-manager-5685dc9cf7-kghkn   9m           58Mi
NAME                                                            CPU(cores)   MEMORY(bytes)
external-secrets-operator-controller-manager-5685dc9cf7-kghkn   9m           58Mi

Also, we already had limit as 128Mi which went through QA testing.
In addition, i tested with 50Mi limit which as expected crashed with OOM.

k -n external-secrets-operator get po
NAME                                                            READY   STATUS             RESTARTS      AGE
external-secrets-operator-controller-manager-85b4888895-4pqjc   0/1     CrashLoopBackOff   2 (20s ago)   77s
k -n external-secrets-operator get po -o yaml | grep -A2 -B2 OOM
          exitCode: 137
          finishedAt: "2025-10-10T11:41:08Z"
          reason: OOMKilled
          startedAt: "2025-10-10T11:40:50Z"
      name: manager

But even with 64Mi limit there are no OOMKilled.
Thus i feel 128Mi could be a safe resource request allowing for easier POD scheduling than to have it as 1Gi, unless i am missing something here.

serviceAccountName: controller-manager
terminationGracePeriodSeconds: 10
15 changes: 8 additions & 7 deletions pkg/controller/common/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,16 +224,17 @@ func deploymentSpecModified(desired, fetched *appsv1.Deployment) bool {
return true
}
for _, desiredVolume := range desired.Spec.Template.Spec.Volumes {
if desiredVolume.Secret != nil && desiredVolume.Secret.Items != nil {
if desiredVolume.Secret != nil {
for _, fetchedVolume := range fetched.Spec.Template.Spec.Volumes {
if !reflect.DeepEqual(desiredVolume.Secret.Items, fetchedVolume.Secret.Items) {
return true
}
if desiredVolume.Secret.SecretName != fetchedVolume.Secret.SecretName {
return true
if desiredVolume.Name == fetchedVolume.Name {
if !reflect.DeepEqual(desiredVolume.Secret.Items, fetchedVolume.Secret.Items) {
return true
}
if !reflect.DeepEqual(desiredVolume.Secret.SecretName, fetchedVolume.Secret.SecretName) {
return true
}
}
}

}
}

Expand Down
5 changes: 5 additions & 0 deletions pkg/controller/external_secrets/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ func (r *Reconciler) createOrApplyCertificate(esc *operatorv1alpha1.ExternalSecr
func (r *Reconciler) getCertificateObject(esc *operatorv1alpha1.ExternalSecretsConfig, resourceLabels map[string]string, fileName string) (*certmanagerv1.Certificate, error) {
certificate := common.DecodeCertificateObjBytes(assets.MustAsset(fileName))

// update the secret name in the Certificate resource of the webhook component.
if fileName == webhookCertificateAssetName {
certificate.Spec.SecretName = certmanagerTLSSecretWebhook
}

updateNamespace(certificate, esc)
common.UpdateResourceLabels(certificate, resourceLabels)

Expand Down
4 changes: 4 additions & 0 deletions pkg/controller/external_secrets/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ const (
// externalsecretsDefaultNamespace is the namespace where the `external-secrets` operand required resources
// will be created, when ExternalSecretsConfig.Spec.Namespace is not set.
externalsecretsDefaultNamespace = "external-secrets"

// certmanagerTLSSecretWebhook is the TLS secret created by cert-manager for the webhook component. A different
// name is used to avoiding clash with the secret created by the inbuilt cert-controller component.
Comment on lines +52 to +53
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix grammar in comment.

The comment contains a grammatical error.

Apply this diff:

-	// certmanagerTLSSecretWebhook is the TLS secret created by cert-manager for the webhook component. A different
-	// name is used to avoiding clash with the secret created by the inbuilt cert-controller component.
+	// certmanagerTLSSecretWebhook is the TLS secret created by cert-manager for the webhook component. A different
+	// name is used to avoid clash with the secret created by the inbuilt cert-controller component.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// certmanagerTLSSecretWebhook is the TLS secret created by cert-manager for the webhook component. A different
// name is used to avoiding clash with the secret created by the inbuilt cert-controller component.
// certmanagerTLSSecretWebhook is the TLS secret created by cert-manager for the webhook component. A different
// name is used to avoid clash with the secret created by the inbuilt cert-controller component.
🤖 Prompt for AI Agents
In pkg/controller/external_secrets/constants.go around lines 52 to 53, the
comment has a grammatical error; update the sentence "A different name is used
to avoiding clash with the secret created by the inbuilt cert-controller
component." to a correct form such as "A different name is used to avoid a clash
with the secret created by the inbuilt cert-controller component." (or "to avoid
clashing with the secret...") so the comment reads clearly and grammatically
correct.

certmanagerTLSSecretWebhook = "external-secrets-webhook-cm"
)

var (
Expand Down
54 changes: 34 additions & 20 deletions pkg/controller/external_secrets/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ func (r *Reconciler) getDeploymentObject(assetName string, esc *operatorv1alpha1
checkInterval = esc.Spec.ApplicationConfig.WebhookConfig.CertificateCheckInterval.Duration.String()
}
updateWebhookContainerSpec(deployment, image, logLevel, checkInterval)
updateWebhookVolumeConfig(deployment, esc)
case certControllerDeploymentAssetName:
updateCertControllerContainerSpec(deployment, image, logLevel)
case bitwardenDeploymentAssetName:
Expand Down Expand Up @@ -302,20 +303,31 @@ func (r *Reconciler) updateImageInStatus(esc *operatorv1alpha1.ExternalSecretsCo

// argument list for external-secrets deployment resource
func updateContainerSpec(deployment *appsv1.Deployment, esc *operatorv1alpha1.ExternalSecretsConfig, image, logLevel string) {
namespace := getOperatingNamespace(esc)
var (
enableClusterStoreArgFmt = "--enable-cluster-store-reconciler=%s"
enableClusterExternalSecretsArgFmt = "--enable-cluster-external-secret-reconciler=%s"
)

args := []string{
"--concurrent=1",
"--metrics-addr=:8080",
fmt.Sprintf("--loglevel=%s", logLevel),
"--zap-time-encoding=epoch",
"--enable-leader-election=true",
"--enable-cluster-store-reconciler=true",
"--enable-cluster-external-secret-reconciler=true",
"--enable-push-secret-reconciler=true",
}

// when spec.appConfig.operatingNamespace is configured, which is for restricting the
// external-secrets custom resource reconcile scope to specified namespace, the reconciliation
// of cluster scoped custom resources must also be disabled.
namespace := getOperatingNamespace(esc)
if namespace != "" {
args = append(args, fmt.Sprintf("--namespace=%s", namespace))
args = append(args, fmt.Sprintf("--namespace=%s", namespace),
fmt.Sprintf(enableClusterStoreArgFmt, "false"),
fmt.Sprintf(enableClusterExternalSecretsArgFmt, "false"))
} else {
args = append(args, fmt.Sprintf(enableClusterStoreArgFmt, "true"),
fmt.Sprintf(enableClusterExternalSecretsArgFmt, "true"))
}

for i, container := range deployment.Spec.Template.Spec.Containers {
Expand Down Expand Up @@ -399,27 +411,29 @@ func updateBitwardenVolumeConfig(deployment *appsv1.Deployment, esc *operatorv1a
}
}

func updateWebhookVolumeConfig(deployment *appsv1.Deployment, esc *operatorv1alpha1.ExternalSecretsConfig) {
if isCertManagerConfigEnabled(esc) {
updateSecretVolumeConfig(deployment, "certs", certmanagerTLSSecretWebhook)
}
}

func updateSecretVolumeConfig(deployment *appsv1.Deployment, volumeName, secretName string) {
volumeExists := false
for i := range deployment.Spec.Template.Spec.Volumes {
if deployment.Spec.Template.Spec.Volumes[i].Name == volumeName {
volumeExists = true
}
if deployment.Spec.Template.Spec.Volumes[i].Secret == nil {
deployment.Spec.Template.Spec.Volumes[i].Secret = &corev1.SecretVolumeSource{}
if deployment.Spec.Template.Spec.Volumes[i].Secret == nil {
deployment.Spec.Template.Spec.Volumes[i].Secret = &corev1.SecretVolumeSource{}
}
deployment.Spec.Template.Spec.Volumes[i].Secret.SecretName = secretName
return
}
deployment.Spec.Template.Spec.Volumes[i].Secret.SecretName = secretName
break
}

if !volumeExists {
deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: secretName,
},
deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: secretName,
},
})
}
},
})
}