diff --git a/deploy/charts/disco-agent/templates/configmap.yaml b/deploy/charts/disco-agent/templates/configmap.yaml index 6ecb9007..83116e8a 100644 --- a/deploy/charts/disco-agent/templates/configmap.yaml +++ b/deploy/charts/disco-agent/templates/configmap.yaml @@ -117,3 +117,31 @@ 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 + - 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 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..5228b93e 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,34 @@ 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: 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: @@ -222,6 +250,34 @@ 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: 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: @@ -339,6 +395,34 @@ 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: 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: @@ -456,6 +540,34 @@ 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: 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/examples/machinehub.yaml b/examples/machinehub.yaml index 4d851966..6813573f 100644 --- a/examples/machinehub.yaml +++ b/examples/machinehub.yaml @@ -139,3 +139,39 @@ 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 + +# 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 8067cef7..a298a38e 100644 --- a/examples/machinehub/input.json +++ b/examples/machinehub/input.json @@ -159,5 +159,29 @@ "data": { "items": [] } + }, + { + "data-gatherer": "ark/externalsecrets", + "data": { + "items": [] + } + }, + { + "data-gatherer": "ark/secretstores", + "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/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..3c5eeb67 100755 --- a/hack/ark/test-e2e.sh +++ b/hack/ark/test-e2e.sh @@ -80,6 +80,25 @@ 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" +# Install External Secrets Operator CRDs and controller +# +# 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 helm upgrade agent "oci://${ARK_CHART}:NON_EXISTENT_TAG@${ARK_CHART_DIGEST}" \ diff --git a/internal/cyberark/dataupload/dataupload.go b/internal/cyberark/dataupload/dataupload.go index 0d5bcc08..ee627978 100644 --- a/internal/cyberark/dataupload/dataupload.go +++ b/internal/cyberark/dataupload/dataupload.go @@ -73,6 +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 []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 735313bd..a5c20ffe 100644 --- a/pkg/client/client_cyberark.go +++ b/pkg/client/client_cyberark.go @@ -221,6 +221,18 @@ 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) + }, + "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_convertdatareadings_test.go b/pkg/client/client_cyberark_convertdatareadings_test.go index a0fc2c27..0e8bf8c1 100644 --- a/pkg/client/client_cyberark_convertdatareadings_test.go +++ b/pkg/client/client_cyberark_convertdatareadings_test.go @@ -419,6 +419,402 @@ 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_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{ diff --git a/pkg/client/client_cyberark_test.go b/pkg/client/client_cyberark_test.go index 9a963300..fcb657ec 100644 --- a/pkg/client/client_cyberark_test.go +++ b/pkg/client/client_cyberark_test.go @@ -80,6 +80,10 @@ var defaultDynamicDatagathererNames = []string{ "ark/secrets", "ark/serviceaccounts", "ark/configmaps", + "ark/externalsecrets", + "ark/secretstores", + "ark/clusterexternalsecrets", + "ark/clustersecretstores", "ark/roles", "ark/clusterroles", "ark/rolebindings",