From 6149a02f2ecfeb7fe9294561687cba55c3f01322 Mon Sep 17 00:00:00 2001 From: eshalev Date: Sun, 8 Feb 2026 10:06:38 +0200 Subject: [PATCH 1/5] Test for first commit --- internal/cyberark/dataupload/dataupload.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/cyberark/dataupload/dataupload.go b/internal/cyberark/dataupload/dataupload.go index 0d5bcc08..29304f77 100644 --- a/internal/cyberark/dataupload/dataupload.go +++ b/internal/cyberark/dataupload/dataupload.go @@ -73,6 +73,10 @@ type Snapshot struct { ServiceAccounts []runtime.Object `json:"serviceaccounts"` // ConfigMaps is a list of ConfigMap resources in the cluster. ConfigMaps []runtime.Object `json:"configmaps"` + // ExternalSecrets is a list of ExternalSecret resources in the cluster. + ExternalSecrets []runtime.Object `json:"externalsecrets"` + // SecretStores is a list of SecretStore resources in the cluster. + SecretStores []runtime.Object `json:"secretstores"` // Roles is a list of Role resources in the cluster. Roles []runtime.Object `json:"roles"` // ClusterRoles is a list of ClusterRole resources in the cluster. From 811b7c4113019076376bc3bbdab747b61f69737e Mon Sep 17 00:00:00 2001 From: eshalev Date: Sun, 8 Feb 2026 10:26:39 +0200 Subject: [PATCH 2/5] ESO Features --- .../disco-agent/templates/configmap.yaml | 14 ++ .../__snapshot__/configmap_test.yaml.snap | 56 +++++ examples/machinehub.yaml | 19 ++ examples/machinehub/input.json | 12 ++ hack/ark/external-secret.yaml | 25 +++ hack/ark/secret-store.yaml | 20 ++ hack/ark/test-e2e.sh | 7 + pkg/client/client_cyberark.go | 6 + ...lient_cyberark_convertdatareadings_test.go | 200 ++++++++++++++++++ pkg/client/client_cyberark_test.go | 2 + 10 files changed, 361 insertions(+) create mode 100644 hack/ark/external-secret.yaml create mode 100644 hack/ark/secret-store.yaml diff --git a/deploy/charts/disco-agent/templates/configmap.yaml b/deploy/charts/disco-agent/templates/configmap.yaml index 6ecb9007..0b97b985 100644 --- a/deploy/charts/disco-agent/templates/configmap.yaml +++ b/deploy/charts/disco-agent/templates/configmap.yaml @@ -117,3 +117,17 @@ data: version: v1 label-selectors: - conjur.org/name=conjur-connect-configmap + - kind: k8s-dynamic + name: ark/externalsecrets + config: + resource-type: + group: external-secrets.io + version: v1 + resource: externalsecrets + - kind: k8s-dynamic + name: ark/secretstores + config: + resource-type: + group: external-secrets.io + version: v1 + resource: secretstores diff --git a/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap b/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap index 4016aaee..a827d931 100644 --- a/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap +++ b/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap @@ -105,6 +105,20 @@ custom-cluster-description: version: v1 label-selectors: - conjur.org/name=conjur-connect-configmap + - kind: k8s-dynamic + name: ark/externalsecrets + config: + resource-type: + group: external-secrets.io + version: v1 + resource: externalsecrets + - kind: k8s-dynamic + name: ark/secretstores + config: + resource-type: + group: external-secrets.io + version: v1 + resource: secretstores kind: ConfigMap metadata: labels: @@ -222,6 +236,20 @@ custom-cluster-name: version: v1 label-selectors: - conjur.org/name=conjur-connect-configmap + - kind: k8s-dynamic + name: ark/externalsecrets + config: + resource-type: + group: external-secrets.io + version: v1 + resource: externalsecrets + - kind: k8s-dynamic + name: ark/secretstores + config: + resource-type: + group: external-secrets.io + version: v1 + resource: secretstores kind: ConfigMap metadata: labels: @@ -339,6 +367,20 @@ custom-period: version: v1 label-selectors: - conjur.org/name=conjur-connect-configmap + - kind: k8s-dynamic + name: ark/externalsecrets + config: + resource-type: + group: external-secrets.io + version: v1 + resource: externalsecrets + - kind: k8s-dynamic + name: ark/secretstores + config: + resource-type: + group: external-secrets.io + version: v1 + resource: secretstores kind: ConfigMap metadata: labels: @@ -456,6 +498,20 @@ defaults: version: v1 label-selectors: - conjur.org/name=conjur-connect-configmap + - kind: k8s-dynamic + name: ark/externalsecrets + config: + resource-type: + group: external-secrets.io + version: v1 + resource: externalsecrets + - kind: k8s-dynamic + name: ark/secretstores + config: + resource-type: + group: external-secrets.io + version: v1 + resource: secretstores kind: ConfigMap metadata: labels: diff --git a/examples/machinehub.yaml b/examples/machinehub.yaml index 4d851966..2d5f6704 100644 --- a/examples/machinehub.yaml +++ b/examples/machinehub.yaml @@ -139,3 +139,22 @@ data-gatherers: version: v1 label-selectors: - conjur.org/name=conjur-connect-configmap + +# Gather External Secrets Operator ExternalSecret resources +- name: ark/externalsecrets + kind: k8s-dynamic + config: + resource-type: + group: external-secrets.io + version: v1 + resource: externalsecrets + +# Gather External Secrets Operator SecretStore resources +- name: ark/secretstores + kind: k8s-dynamic + config: + resource-type: + group: external-secrets.io + version: v1 + resource: secretstores + diff --git a/examples/machinehub/input.json b/examples/machinehub/input.json index 8067cef7..ea8d2beb 100644 --- a/examples/machinehub/input.json +++ b/examples/machinehub/input.json @@ -159,5 +159,17 @@ "data": { "items": [] } + }, + { + "data-gatherer": "ark/externalsecrets", + "data": { + "items": [] + } + }, + { + "data-gatherer": "ark/secretstores", + "data": { + "items": [] + } } ] diff --git a/hack/ark/external-secret.yaml b/hack/ark/external-secret.yaml new file mode 100644 index 00000000..5f6b72f8 --- /dev/null +++ b/hack/ark/external-secret.yaml @@ -0,0 +1,25 @@ +# Sample ExternalSecret for e2e testing +# This is a minimal ExternalSecret CR that will be discovered by the agent. +# Note: This requires the External Secrets Operator CRDs to be installed, +# but does not require a working secrets backend. +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: e2e-test-external-secret + namespace: default + labels: + app.kubernetes.io/name: e2e-test + app.kubernetes.io/component: external-secret +spec: + refreshInterval: 1h + secretStoreRef: + name: e2e-test-secret-store + kind: SecretStore + target: + name: e2e-test-synced-secret + creationPolicy: Owner + data: + - secretKey: example-key + remoteRef: + key: dummy/path/to/secret + property: password diff --git a/hack/ark/secret-store.yaml b/hack/ark/secret-store.yaml new file mode 100644 index 00000000..883be16c --- /dev/null +++ b/hack/ark/secret-store.yaml @@ -0,0 +1,20 @@ +# Sample SecretStore for e2e testing +# This is a minimal SecretStore CR that will be discovered by the agent. +# Note: This requires the External Secrets Operator CRDs to be installed, +# but does not require a working secrets backend. +apiVersion: external-secrets.io/v1 +kind: SecretStore +metadata: + name: e2e-test-secret-store + namespace: default + labels: + app.kubernetes.io/name: e2e-test + app.kubernetes.io/component: secret-store +spec: + provider: + # Fake provider configuration - this won't actually work but allows the CR to be created + fake: + data: + - key: dummy/path/to/secret + value: dummy-value + version: "1" diff --git a/hack/ark/test-e2e.sh b/hack/ark/test-e2e.sh index e24487d3..8b221e1e 100755 --- a/hack/ark/test-e2e.sh +++ b/hack/ark/test-e2e.sh @@ -80,6 +80,13 @@ kubectl create secret generic e2e-sample-secret-$(date '+%s') \ # in the ark/configmaps data gatherer (conjur.org/name=conjur-connect-configmap). kubectl apply -f "${root_dir}/hack/ark/conjur-connect-configmap.yaml" +# Create sample External Secrets Operator resources that will be discovered by the agent +# +# These require the ESO CRDs to be installed in the cluster. If the CRDs are not +# installed, these commands will fail but the e2e test can still proceed. +kubectl apply -f "${root_dir}/hack/ark/secret-store.yaml" || echo "Warning: SecretStore CRD not installed, skipping" +kubectl apply -f "${root_dir}/hack/ark/external-secret.yaml" || echo "Warning: ExternalSecret CRD not installed, skipping" + # We use a non-existent tag and omit the `--version` flag, to work around a Helm # v4 bug. See: https://github.com/helm/helm/issues/31600 helm upgrade agent "oci://${ARK_CHART}:NON_EXISTENT_TAG@${ARK_CHART_DIGEST}" \ diff --git a/pkg/client/client_cyberark.go b/pkg/client/client_cyberark.go index 735313bd..818cb213 100644 --- a/pkg/client/client_cyberark.go +++ b/pkg/client/client_cyberark.go @@ -221,6 +221,12 @@ var defaultExtractorFunctions = map[string]func(*api.DataReading, *dataupload.Sn "ark/configmaps": func(r *api.DataReading, s *dataupload.Snapshot) error { return extractResourceListFromReading(r, &s.ConfigMaps) }, + "ark/externalsecrets": func(r *api.DataReading, s *dataupload.Snapshot) error { + return extractResourceListFromReading(r, &s.ExternalSecrets) + }, + "ark/secretstores": func(r *api.DataReading, s *dataupload.Snapshot) error { + return extractResourceListFromReading(r, &s.SecretStores) + }, } // convertDataReadings processes a list of DataReadings using the provided diff --git a/pkg/client/client_cyberark_convertdatareadings_test.go b/pkg/client/client_cyberark_convertdatareadings_test.go index a0fc2c27..cc4fdde0 100644 --- a/pkg/client/client_cyberark_convertdatareadings_test.go +++ b/pkg/client/client_cyberark_convertdatareadings_test.go @@ -419,6 +419,206 @@ func TestConvertDataReadings_ConfigMaps(t *testing.T) { assert.Equal(t, "default", cm2.GetNamespace()) } +// TestConvertDataReadings_ExternalSecrets tests that externalsecrets are correctly converted. +func TestConvertDataReadings_ExternalSecrets(t *testing.T) { + extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{ + "ark/discovery": extractClusterIDAndServerVersionFromReading, + "ark/externalsecrets": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error { + return extractResourceListFromReading(reading, &snapshot.ExternalSecrets) + }, + } + + readings := []*api.DataReading{ + { + DataGatherer: "ark/discovery", + Data: &api.DiscoveryData{ + ClusterID: "test-cluster-id", + ServerVersion: &version.Info{ + GitVersion: "v1.21.0", + }, + }, + }, + { + DataGatherer: "ark/externalsecrets", + Data: &api.DynamicData{ + Items: []*api.GatheredResource{ + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "external-secrets.io/v1", + "kind": "ExternalSecret", + "metadata": map[string]any{ + "name": "my-external-secret", + "namespace": "default", + }, + "spec": map[string]any{ + "refreshInterval": "1h", + "secretStoreRef": map[string]any{ + "name": "my-secret-store", + "kind": "SecretStore", + }, + }, + }, + }, + }, + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "external-secrets.io/v1", + "kind": "ExternalSecret", + "metadata": map[string]any{ + "name": "another-external-secret", + "namespace": "production", + }, + "spec": map[string]any{ + "refreshInterval": "30m", + }, + }, + }, + }, + // Deleted externalsecret should be ignored + { + DeletedAt: api.Time{Time: time.Now()}, + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "external-secrets.io/v1", + "kind": "ExternalSecret", + "metadata": map[string]any{ + "name": "deleted-external-secret", + "namespace": "default", + }, + }, + }, + }, + }, + }, + }, + } + + var snapshot dataupload.Snapshot + err := convertDataReadings(extractorFunctions, readings, &snapshot) + require.NoError(t, err) + + // Verify the snapshot contains the expected data + assert.Equal(t, "test-cluster-id", snapshot.ClusterID) + assert.Equal(t, "v1.21.0", snapshot.K8SVersion) + require.Len(t, snapshot.ExternalSecrets, 2, "should have 2 externalsecrets (deleted one should be excluded)") + + // Verify the first externalsecret + es1, ok := snapshot.ExternalSecrets[0].(*unstructured.Unstructured) + require.True(t, ok, "externalsecret should be unstructured") + assert.Equal(t, "ExternalSecret", es1.GetKind()) + assert.Equal(t, "my-external-secret", es1.GetName()) + assert.Equal(t, "default", es1.GetNamespace()) + + // Verify the second externalsecret + es2, ok := snapshot.ExternalSecrets[1].(*unstructured.Unstructured) + require.True(t, ok, "externalsecret should be unstructured") + assert.Equal(t, "ExternalSecret", es2.GetKind()) + assert.Equal(t, "another-external-secret", es2.GetName()) + assert.Equal(t, "production", es2.GetNamespace()) +} + +// TestConvertDataReadings_SecretStores tests that secretstores are correctly converted. +func TestConvertDataReadings_SecretStores(t *testing.T) { + extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{ + "ark/discovery": extractClusterIDAndServerVersionFromReading, + "ark/secretstores": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error { + return extractResourceListFromReading(reading, &snapshot.SecretStores) + }, + } + + readings := []*api.DataReading{ + { + DataGatherer: "ark/discovery", + Data: &api.DiscoveryData{ + ClusterID: "test-cluster-id", + ServerVersion: &version.Info{ + GitVersion: "v1.21.0", + }, + }, + }, + { + DataGatherer: "ark/secretstores", + Data: &api.DynamicData{ + Items: []*api.GatheredResource{ + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "external-secrets.io/v1", + "kind": "SecretStore", + "metadata": map[string]any{ + "name": "my-secret-store", + "namespace": "default", + }, + "spec": map[string]any{ + "provider": map[string]any{ + "fake": map[string]any{}, + }, + }, + }, + }, + }, + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "external-secrets.io/v1", + "kind": "SecretStore", + "metadata": map[string]any{ + "name": "aws-secret-store", + "namespace": "production", + }, + "spec": map[string]any{ + "provider": map[string]any{ + "aws": map[string]any{}, + }, + }, + }, + }, + }, + // Deleted secretstore should be ignored + { + DeletedAt: api.Time{Time: time.Now()}, + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "external-secrets.io/v1", + "kind": "SecretStore", + "metadata": map[string]any{ + "name": "deleted-secret-store", + "namespace": "default", + }, + }, + }, + }, + }, + }, + }, + } + + var snapshot dataupload.Snapshot + err := convertDataReadings(extractorFunctions, readings, &snapshot) + require.NoError(t, err) + + // Verify the snapshot contains the expected data + assert.Equal(t, "test-cluster-id", snapshot.ClusterID) + assert.Equal(t, "v1.21.0", snapshot.K8SVersion) + require.Len(t, snapshot.SecretStores, 2, "should have 2 secretstores (deleted one should be excluded)") + + // Verify the first secretstore + ss1, ok := snapshot.SecretStores[0].(*unstructured.Unstructured) + require.True(t, ok, "secretstore should be unstructured") + assert.Equal(t, "SecretStore", ss1.GetKind()) + assert.Equal(t, "my-secret-store", ss1.GetName()) + assert.Equal(t, "default", ss1.GetNamespace()) + + // Verify the second secretstore + ss2, ok := snapshot.SecretStores[1].(*unstructured.Unstructured) + require.True(t, ok, "secretstore should be unstructured") + assert.Equal(t, "SecretStore", ss2.GetKind()) + assert.Equal(t, "aws-secret-store", ss2.GetName()) + assert.Equal(t, "production", ss2.GetNamespace()) +} + // TestConvertDataReadings_ServiceAccounts tests that serviceaccounts are correctly converted. func TestConvertDataReadings_ServiceAccounts(t *testing.T) { extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{ diff --git a/pkg/client/client_cyberark_test.go b/pkg/client/client_cyberark_test.go index 9a963300..9448c4d5 100644 --- a/pkg/client/client_cyberark_test.go +++ b/pkg/client/client_cyberark_test.go @@ -80,6 +80,8 @@ var defaultDynamicDatagathererNames = []string{ "ark/secrets", "ark/serviceaccounts", "ark/configmaps", + "ark/externalsecrets", + "ark/secretstores", "ark/roles", "ark/clusterroles", "ark/rolebindings", From 698b8613f8abfcc8621afb7d6db58f00bc30b864 Mon Sep 17 00:00:00 2001 From: eshalev Date: Mon, 9 Feb 2026 16:35:35 +0200 Subject: [PATCH 3/5] include the cluster-scoped resources: clusterexternalsecrets and clustersecretstores. --- deploy/charts/disco-agent/templates/configmap.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/deploy/charts/disco-agent/templates/configmap.yaml b/deploy/charts/disco-agent/templates/configmap.yaml index 0b97b985..88393aaf 100644 --- a/deploy/charts/disco-agent/templates/configmap.yaml +++ b/deploy/charts/disco-agent/templates/configmap.yaml @@ -131,3 +131,17 @@ data: group: external-secrets.io version: v1 resource: secretstores + - kind: k8s-dynamic + name: ark/clusterexternalsecrets + config: + resource-type: + group: external-secrets.io + version: v1 + resource: clusterexternalsecrets + - kind: k8s-dynamic + name: ark/clustersecretstores + config: + resource-type: + group: external-secrets.io + version: v1 + resource: clustersecretstores From 280cbd54184267d4c2813ca6e0bb36bd00f7d897 Mon Sep 17 00:00:00 2001 From: eshalev Date: Mon, 9 Feb 2026 18:27:59 +0200 Subject: [PATCH 4/5] adding clusterexternalsecrets and clustersecretstores support --- .../disco-agent/templates/configmap.yaml | 4 +-- examples/machinehub.yaml | 17 ++++++++++++ examples/machinehub/input.json | 12 +++++++++ hack/ark/cluster-external-secret.yaml | 27 +++++++++++++++++++ hack/ark/cluster-secret-store.yaml | 18 +++++++++++++ hack/ark/test-e2e.sh | 22 +++++++++++---- internal/cyberark/dataupload/dataupload.go | 6 ++++- pkg/client/client_cyberark.go | 6 +++++ pkg/client/client_cyberark_test.go | 2 ++ 9 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 hack/ark/cluster-external-secret.yaml create mode 100644 hack/ark/cluster-secret-store.yaml diff --git a/deploy/charts/disco-agent/templates/configmap.yaml b/deploy/charts/disco-agent/templates/configmap.yaml index 88393aaf..83116e8a 100644 --- a/deploy/charts/disco-agent/templates/configmap.yaml +++ b/deploy/charts/disco-agent/templates/configmap.yaml @@ -136,12 +136,12 @@ data: config: resource-type: group: external-secrets.io - version: v1 + version: v1beta1 resource: clusterexternalsecrets - kind: k8s-dynamic name: ark/clustersecretstores config: resource-type: group: external-secrets.io - version: v1 + version: v1beta1 resource: clustersecretstores diff --git a/examples/machinehub.yaml b/examples/machinehub.yaml index 2d5f6704..6813573f 100644 --- a/examples/machinehub.yaml +++ b/examples/machinehub.yaml @@ -158,3 +158,20 @@ data-gatherers: version: v1 resource: secretstores +# Gather External Secrets Operator ClusterExternalSecret resources +- name: ark/clusterexternalsecrets + kind: k8s-dynamic + config: + resource-type: + group: external-secrets.io + version: v1beta1 + resource: clusterexternalsecrets + +# Gather External Secrets Operator ClusterSecretStore resources +- name: ark/clustersecretstores + kind: k8s-dynamic + config: + resource-type: + group: external-secrets.io + version: v1beta1 + resource: clustersecretstores diff --git a/examples/machinehub/input.json b/examples/machinehub/input.json index ea8d2beb..a298a38e 100644 --- a/examples/machinehub/input.json +++ b/examples/machinehub/input.json @@ -171,5 +171,17 @@ "data": { "items": [] } + }, + { + "data-gatherer": "ark/clusterexternalsecrets", + "data": { + "items": [] + } + }, + { + "data-gatherer": "ark/clustersecretstores", + "data": { + "items": [] + } } ] diff --git a/hack/ark/cluster-external-secret.yaml b/hack/ark/cluster-external-secret.yaml new file mode 100644 index 00000000..b5abb45e --- /dev/null +++ b/hack/ark/cluster-external-secret.yaml @@ -0,0 +1,27 @@ +# Sample ClusterExternalSecret for e2e testing +# This is a minimal ClusterExternalSecret CR that will be discovered by the agent. +# This is a cluster-scoped resource that can create ExternalSecrets in multiple namespaces. +apiVersion: external-secrets.io/v1beta1 +kind: ClusterExternalSecret +metadata: + name: e2e-test-cluster-external-secret + labels: + app.kubernetes.io/name: e2e-test + app.kubernetes.io/component: cluster-external-secret +spec: + refreshInterval: 1h + externalSecretSpec: + secretStoreRef: + name: e2e-test-cluster-secret-store + kind: ClusterSecretStore + target: + name: e2e-test-synced-secret + creationPolicy: Owner + data: + - secretKey: example-key + remoteRef: + key: dummy/path/to/secret + property: password + namespaceSelector: + matchLabels: + environment: test diff --git a/hack/ark/cluster-secret-store.yaml b/hack/ark/cluster-secret-store.yaml new file mode 100644 index 00000000..a3ffe380 --- /dev/null +++ b/hack/ark/cluster-secret-store.yaml @@ -0,0 +1,18 @@ +# Sample ClusterSecretStore for e2e testing +# This is a minimal ClusterSecretStore CR that will be discovered by the agent. +# This is a cluster-scoped resource that can be referenced by ExternalSecrets in any namespace. +apiVersion: external-secrets.io/v1beta1 +kind: ClusterSecretStore +metadata: + name: e2e-test-cluster-secret-store + labels: + app.kubernetes.io/name: e2e-test + app.kubernetes.io/component: cluster-secret-store +spec: + provider: + # Fake provider configuration - this won't actually work but allows the CR to be created + fake: + data: + - key: dummy/path/to/secret + value: dummy-value + version: "1" diff --git a/hack/ark/test-e2e.sh b/hack/ark/test-e2e.sh index 8b221e1e..3c5eeb67 100755 --- a/hack/ark/test-e2e.sh +++ b/hack/ark/test-e2e.sh @@ -80,12 +80,24 @@ kubectl create secret generic e2e-sample-secret-$(date '+%s') \ # in the ark/configmaps data gatherer (conjur.org/name=conjur-connect-configmap). kubectl apply -f "${root_dir}/hack/ark/conjur-connect-configmap.yaml" -# Create sample External Secrets Operator resources that will be discovered by the agent +# Install External Secrets Operator CRDs and controller # -# These require the ESO CRDs to be installed in the cluster. If the CRDs are not -# installed, these commands will fail but the e2e test can still proceed. -kubectl apply -f "${root_dir}/hack/ark/secret-store.yaml" || echo "Warning: SecretStore CRD not installed, skipping" -kubectl apply -f "${root_dir}/hack/ark/external-secret.yaml" || echo "Warning: ExternalSecret CRD not installed, skipping" +# This is required for the agent to discover ExternalSecret and SecretStore resources. +echo "Installing External Secrets Operator..." +helm repo add external-secrets https://charts.external-secrets.io +helm repo update +helm upgrade --install external-secrets \ + external-secrets/external-secrets \ + --namespace external-secrets-system \ + --create-namespace \ + --wait \ + --set installCRDs=true + +# Create sample External Secrets Operator resources that will be discovered by the agent +kubectl apply -f "${root_dir}/hack/ark/secret-store.yaml" +kubectl apply -f "${root_dir}/hack/ark/external-secret.yaml" +kubectl apply -f "${root_dir}/hack/ark/cluster-secret-store.yaml" +kubectl apply -f "${root_dir}/hack/ark/cluster-external-secret.yaml" # We use a non-existent tag and omit the `--version` flag, to work around a Helm # v4 bug. See: https://github.com/helm/helm/issues/31600 diff --git a/internal/cyberark/dataupload/dataupload.go b/internal/cyberark/dataupload/dataupload.go index 29304f77..ee627978 100644 --- a/internal/cyberark/dataupload/dataupload.go +++ b/internal/cyberark/dataupload/dataupload.go @@ -73,10 +73,14 @@ type Snapshot struct { ServiceAccounts []runtime.Object `json:"serviceaccounts"` // ConfigMaps is a list of ConfigMap resources in the cluster. ConfigMaps []runtime.Object `json:"configmaps"` - // ExternalSecrets is a list of ExternalSecret resources in the cluster. + // ExternalSecrets is a list of ExternalSecret resources in the cluster. ExternalSecrets []runtime.Object `json:"externalsecrets"` // SecretStores is a list of SecretStore resources in the cluster. SecretStores []runtime.Object `json:"secretstores"` + // ClusterExternalSecrets is a list of ClusterExternalSecret resources in the cluster. + ClusterExternalSecrets []runtime.Object `json:"clusterexternalsecrets"` + // ClusterSecretStores is a list of ClusterSecretStore resources in the cluster. + ClusterSecretStores []runtime.Object `json:"clustersecretstores"` // Roles is a list of Role resources in the cluster. Roles []runtime.Object `json:"roles"` // ClusterRoles is a list of ClusterRole resources in the cluster. diff --git a/pkg/client/client_cyberark.go b/pkg/client/client_cyberark.go index 818cb213..a5c20ffe 100644 --- a/pkg/client/client_cyberark.go +++ b/pkg/client/client_cyberark.go @@ -227,6 +227,12 @@ var defaultExtractorFunctions = map[string]func(*api.DataReading, *dataupload.Sn "ark/secretstores": func(r *api.DataReading, s *dataupload.Snapshot) error { return extractResourceListFromReading(r, &s.SecretStores) }, + "ark/clusterexternalsecrets": func(r *api.DataReading, s *dataupload.Snapshot) error { + return extractResourceListFromReading(r, &s.ClusterExternalSecrets) + }, + "ark/clustersecretstores": func(r *api.DataReading, s *dataupload.Snapshot) error { + return extractResourceListFromReading(r, &s.ClusterSecretStores) + }, } // convertDataReadings processes a list of DataReadings using the provided diff --git a/pkg/client/client_cyberark_test.go b/pkg/client/client_cyberark_test.go index 9448c4d5..fcb657ec 100644 --- a/pkg/client/client_cyberark_test.go +++ b/pkg/client/client_cyberark_test.go @@ -82,6 +82,8 @@ var defaultDynamicDatagathererNames = []string{ "ark/configmaps", "ark/externalsecrets", "ark/secretstores", + "ark/clusterexternalsecrets", + "ark/clustersecretstores", "ark/roles", "ark/clusterroles", "ark/rolebindings", From ad67d6e7940dee4b474df45a4f9f438245395100 Mon Sep 17 00:00:00 2001 From: eshalev Date: Mon, 9 Feb 2026 18:42:30 +0200 Subject: [PATCH 5/5] Add more support for clusterexternalsecrets and clustersecretstores --- .../__snapshot__/configmap_test.yaml.snap | 56 +++++ ...lient_cyberark_convertdatareadings_test.go | 196 ++++++++++++++++++ 2 files changed, 252 insertions(+) diff --git a/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap b/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap index a827d931..5228b93e 100644 --- a/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap +++ b/deploy/charts/disco-agent/tests/__snapshot__/configmap_test.yaml.snap @@ -119,6 +119,20 @@ custom-cluster-description: group: external-secrets.io version: v1 resource: secretstores + - kind: k8s-dynamic + name: ark/clusterexternalsecrets + config: + resource-type: + group: external-secrets.io + version: v1beta1 + resource: clusterexternalsecrets + - kind: k8s-dynamic + name: ark/clustersecretstores + config: + resource-type: + group: external-secrets.io + version: v1beta1 + resource: clustersecretstores kind: ConfigMap metadata: labels: @@ -250,6 +264,20 @@ custom-cluster-name: group: external-secrets.io version: v1 resource: secretstores + - kind: k8s-dynamic + name: ark/clusterexternalsecrets + config: + resource-type: + group: external-secrets.io + version: v1beta1 + resource: clusterexternalsecrets + - kind: k8s-dynamic + name: ark/clustersecretstores + config: + resource-type: + group: external-secrets.io + version: v1beta1 + resource: clustersecretstores kind: ConfigMap metadata: labels: @@ -381,6 +409,20 @@ custom-period: group: external-secrets.io version: v1 resource: secretstores + - kind: k8s-dynamic + name: ark/clusterexternalsecrets + config: + resource-type: + group: external-secrets.io + version: v1beta1 + resource: clusterexternalsecrets + - kind: k8s-dynamic + name: ark/clustersecretstores + config: + resource-type: + group: external-secrets.io + version: v1beta1 + resource: clustersecretstores kind: ConfigMap metadata: labels: @@ -512,6 +554,20 @@ defaults: group: external-secrets.io version: v1 resource: secretstores + - kind: k8s-dynamic + name: ark/clusterexternalsecrets + config: + resource-type: + group: external-secrets.io + version: v1beta1 + resource: clusterexternalsecrets + - kind: k8s-dynamic + name: ark/clustersecretstores + config: + resource-type: + group: external-secrets.io + version: v1beta1 + resource: clustersecretstores kind: ConfigMap metadata: labels: diff --git a/pkg/client/client_cyberark_convertdatareadings_test.go b/pkg/client/client_cyberark_convertdatareadings_test.go index cc4fdde0..0e8bf8c1 100644 --- a/pkg/client/client_cyberark_convertdatareadings_test.go +++ b/pkg/client/client_cyberark_convertdatareadings_test.go @@ -619,6 +619,202 @@ func TestConvertDataReadings_SecretStores(t *testing.T) { assert.Equal(t, "production", ss2.GetNamespace()) } +// TestConvertDataReadings_ClusterExternalSecrets tests that clusterexternalsecrets are correctly converted. +func TestConvertDataReadings_ClusterExternalSecrets(t *testing.T) { + extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{ + "ark/discovery": extractClusterIDAndServerVersionFromReading, + "ark/clusterexternalsecrets": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error { + return extractResourceListFromReading(reading, &snapshot.ClusterExternalSecrets) + }, + } + + readings := []*api.DataReading{ + { + DataGatherer: "ark/discovery", + Data: &api.DiscoveryData{ + ClusterID: "test-cluster-id", + ServerVersion: &version.Info{ + GitVersion: "v1.21.0", + }, + }, + }, + { + DataGatherer: "ark/clusterexternalsecrets", + Data: &api.DynamicData{ + Items: []*api.GatheredResource{ + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "external-secrets.io/v1beta1", + "kind": "ClusterExternalSecret", + "metadata": map[string]any{ + "name": "my-cluster-external-secret", + }, + "spec": map[string]any{ + "externalSecretSpec": map[string]any{ + "secretStoreRef": map[string]any{ + "name": "my-cluster-secret-store", + "kind": "ClusterSecretStore", + }, + }, + }, + }, + }, + }, + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "external-secrets.io/v1beta1", + "kind": "ClusterExternalSecret", + "metadata": map[string]any{ + "name": "aws-cluster-external-secret", + }, + "spec": map[string]any{ + "externalSecretSpec": map[string]any{ + "secretStoreRef": map[string]any{ + "name": "aws-cluster-secret-store", + "kind": "ClusterSecretStore", + }, + }, + }, + }, + }, + }, + // Deleted clusterexternalsecret should be ignored + { + DeletedAt: api.Time{Time: time.Now()}, + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "external-secrets.io/v1beta1", + "kind": "ClusterExternalSecret", + "metadata": map[string]any{ + "name": "deleted-cluster-external-secret", + }, + }, + }, + }, + }, + }, + }, + } + + var snapshot dataupload.Snapshot + err := convertDataReadings(extractorFunctions, readings, &snapshot) + require.NoError(t, err) + + // Verify the snapshot contains the expected data + assert.Equal(t, "test-cluster-id", snapshot.ClusterID) + assert.Equal(t, "v1.21.0", snapshot.K8SVersion) + require.Len(t, snapshot.ClusterExternalSecrets, 2, "should have 2 clusterexternalsecrets (deleted one should be excluded)") + + // Verify the first clusterexternalsecret + ces1, ok := snapshot.ClusterExternalSecrets[0].(*unstructured.Unstructured) + require.True(t, ok, "clusterexternalsecret should be unstructured") + assert.Equal(t, "ClusterExternalSecret", ces1.GetKind()) + assert.Equal(t, "my-cluster-external-secret", ces1.GetName()) + + // Verify the second clusterexternalsecret + ces2, ok := snapshot.ClusterExternalSecrets[1].(*unstructured.Unstructured) + require.True(t, ok, "clusterexternalsecret should be unstructured") + assert.Equal(t, "ClusterExternalSecret", ces2.GetKind()) + assert.Equal(t, "aws-cluster-external-secret", ces2.GetName()) +} + +// TestConvertDataReadings_ClusterSecretStores tests that clustersecretstores are correctly converted. +func TestConvertDataReadings_ClusterSecretStores(t *testing.T) { + extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{ + "ark/discovery": extractClusterIDAndServerVersionFromReading, + "ark/clustersecretstores": func(reading *api.DataReading, snapshot *dataupload.Snapshot) error { + return extractResourceListFromReading(reading, &snapshot.ClusterSecretStores) + }, + } + + readings := []*api.DataReading{ + { + DataGatherer: "ark/discovery", + Data: &api.DiscoveryData{ + ClusterID: "test-cluster-id", + ServerVersion: &version.Info{ + GitVersion: "v1.21.0", + }, + }, + }, + { + DataGatherer: "ark/clustersecretstores", + Data: &api.DynamicData{ + Items: []*api.GatheredResource{ + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "external-secrets.io/v1beta1", + "kind": "ClusterSecretStore", + "metadata": map[string]any{ + "name": "my-cluster-secret-store", + }, + "spec": map[string]any{ + "provider": map[string]any{ + "fake": map[string]any{}, + }, + }, + }, + }, + }, + { + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "external-secrets.io/v1beta1", + "kind": "ClusterSecretStore", + "metadata": map[string]any{ + "name": "aws-cluster-secret-store", + }, + "spec": map[string]any{ + "provider": map[string]any{ + "aws": map[string]any{}, + }, + }, + }, + }, + }, + // Deleted clustersecretstore should be ignored + { + DeletedAt: api.Time{Time: time.Now()}, + Resource: &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "external-secrets.io/v1beta1", + "kind": "ClusterSecretStore", + "metadata": map[string]any{ + "name": "deleted-cluster-secret-store", + }, + }, + }, + }, + }, + }, + }, + } + + var snapshot dataupload.Snapshot + err := convertDataReadings(extractorFunctions, readings, &snapshot) + require.NoError(t, err) + + // Verify the snapshot contains the expected data + assert.Equal(t, "test-cluster-id", snapshot.ClusterID) + assert.Equal(t, "v1.21.0", snapshot.K8SVersion) + require.Len(t, snapshot.ClusterSecretStores, 2, "should have 2 clustersecretstores (deleted one should be excluded)") + + // Verify the first clustersecretstore + css1, ok := snapshot.ClusterSecretStores[0].(*unstructured.Unstructured) + require.True(t, ok, "clustersecretstore should be unstructured") + assert.Equal(t, "ClusterSecretStore", css1.GetKind()) + assert.Equal(t, "my-cluster-secret-store", css1.GetName()) + + // Verify the second clustersecretstore + css2, ok := snapshot.ClusterSecretStores[1].(*unstructured.Unstructured) + require.True(t, ok, "clustersecretstore should be unstructured") + assert.Equal(t, "ClusterSecretStore", css2.GetKind()) + assert.Equal(t, "aws-cluster-secret-store", css2.GetName()) +} + // TestConvertDataReadings_ServiceAccounts tests that serviceaccounts are correctly converted. func TestConvertDataReadings_ServiceAccounts(t *testing.T) { extractorFunctions := map[string]func(*api.DataReading, *dataupload.Snapshot) error{