From 94ddf258f9ee8d760ab7f29913ad0ada4bb31f25 Mon Sep 17 00:00:00 2001 From: Thomas Krampl Date: Thu, 9 Apr 2026 14:25:04 +0200 Subject: [PATCH] feat: verify instance exists before creating Aiven credentials Check that the OpenSearch or Valkey instance actually exists before proceeding with credential creation. This prevents confusing errors from the Aivenator when the instance name is wrong. - Add instance existence check in CreateOpenSearchCredentials - Add instance existence check in CreateValkeyCredentials - Add integration tests for non-existing instances and environments - Add K8s fixtures for credteam namespace to support existing TTL tests --- integration_tests/aiven_credentials.lua | 116 ++++++++++++++++++ .../dev/credteam/opensearch.yaml | 38 ++++++ .../dev/credteam/valkey.yaml | 35 ++++++ .../persistence/aivencredentials/queries.go | 8 ++ 4 files changed, 197 insertions(+) create mode 100644 integration_tests/k8s_resources/aiven_credentials/dev/credteam/opensearch.yaml create mode 100644 integration_tests/k8s_resources/aiven_credentials/dev/credteam/valkey.yaml diff --git a/integration_tests/aiven_credentials.lua b/integration_tests/aiven_credentials.lua index a8d1d6aaa..e86cdf0a4 100644 --- a/integration_tests/aiven_credentials.lua +++ b/integration_tests/aiven_credentials.lua @@ -4,6 +4,10 @@ local nonMember = User.new("nonmember", "other@user.com") local team = Team.new("credteam", "purpose", "#slack_channel") team:addMember(user) +-- Load K8s fixtures so that instance existence checks pass for known instances. +-- The fixtures provide opensearch-credteam-my-opensearch and valkey-credteam-my-valkey in the "dev" cluster. +Helper.readK8sResources("k8s_resources/aiven_credentials") + -- Authorization: non-member cannot create OpenSearch credentials Test.gql("Non-member cannot create OpenSearch credentials", function(t) t.addHeader("x-user-email", nonMember:email()) @@ -114,6 +118,118 @@ Test.gql("Cannot create credentials for non-existing team", function(t) } end) +-- Instance not found: OpenSearch +Test.gql("Cannot create OpenSearch credentials for non-existing instance", function(t) + t.addHeader("x-user-email", user:email()) + t.query(string.format([[ + mutation { + createOpenSearchCredentials(input: { + teamSlug: "%s" + environmentName: "dev" + instanceName: "does-not-exist" + permission: READ + ttl: "1d" + }) { + credentials { username } + } + } + ]], team:slug())) + + t.check { + errors = { + { + message = Contains("not found"), + path = { "createOpenSearchCredentials" }, + }, + }, + data = Null, + } +end) + +-- Instance not found: Valkey +Test.gql("Cannot create Valkey credentials for non-existing instance", function(t) + t.addHeader("x-user-email", user:email()) + t.query(string.format([[ + mutation { + createValkeyCredentials(input: { + teamSlug: "%s" + environmentName: "dev" + instanceName: "does-not-exist" + permission: READ + ttl: "1d" + }) { + credentials { username } + } + } + ]], team:slug())) + + t.check { + errors = { + { + message = Contains("not found"), + path = { "createValkeyCredentials" }, + }, + }, + data = Null, + } +end) + +-- Instance not found: non-existing environment for OpenSearch +Test.gql("Cannot create OpenSearch credentials in non-existing environment", function(t) + t.addHeader("x-user-email", user:email()) + t.query(string.format([[ + mutation { + createOpenSearchCredentials(input: { + teamSlug: "%s" + environmentName: "prod" + instanceName: "my-opensearch" + permission: READ + ttl: "1d" + }) { + credentials { username } + } + } + ]], team:slug())) + + t.check { + errors = { + { + message = Contains("not found"), + path = { "createOpenSearchCredentials" }, + }, + }, + data = Null, + } +end) + +-- Instance not found: non-existing environment for Valkey +Test.gql("Cannot create Valkey credentials in non-existing environment", function(t) + t.addHeader("x-user-email", user:email()) + t.query(string.format([[ + mutation { + createValkeyCredentials(input: { + teamSlug: "%s" + environmentName: "prod" + instanceName: "my-valkey" + permission: READ + ttl: "1d" + }) { + credentials { username } + } + } + ]], team:slug())) + + t.check { + errors = { + { + message = Contains("not found"), + path = { "createValkeyCredentials" }, + }, + }, + data = Null, + } +end) + -- Input validation: TTL exceeds maximum (OpenSearch) Test.gql("TTL exceeding 30 days is rejected", function(t) t.addHeader("x-user-email", user:email()) diff --git a/integration_tests/k8s_resources/aiven_credentials/dev/credteam/opensearch.yaml b/integration_tests/k8s_resources/aiven_credentials/dev/credteam/opensearch.yaml new file mode 100644 index 000000000..8719a4195 --- /dev/null +++ b/integration_tests/k8s_resources/aiven_credentials/dev/credteam/opensearch.yaml @@ -0,0 +1,38 @@ +--- +apiVersion: aiven.io/v1alpha1 +kind: OpenSearch +metadata: + annotations: + controllers.aiven.io/generation-was-processed: "1" + controllers.aiven.io/instance-is-running: "true" + labels: + app: app-name + name: opensearch-credteam-my-opensearch +spec: + cloudName: google-europe-north1 + connInfoSecretTarget: + name: "" + disk_space: 525G + plan: business-8 + project: nav-dev + projectVpcId: fff21e17-95d5-408b-8df5-15aacf38f5de + tags: + environment: dev + team: credteam + tenant: nav + terminationProtection: true + userConfig: + opensearch_version: "2" +status: + conditions: + - lastTransitionTime: "2023-11-08T10:36:06Z" + message: Instance was created or update on Aiven side + reason: Updated + status: "True" + type: Initialized + - lastTransitionTime: "2024-01-10T09:40:58Z" + message: Instance is running on Aiven side + reason: CheckRunning + status: "True" + type: Running + state: RUNNING diff --git a/integration_tests/k8s_resources/aiven_credentials/dev/credteam/valkey.yaml b/integration_tests/k8s_resources/aiven_credentials/dev/credteam/valkey.yaml new file mode 100644 index 000000000..232b3c06b --- /dev/null +++ b/integration_tests/k8s_resources/aiven_credentials/dev/credteam/valkey.yaml @@ -0,0 +1,35 @@ +--- +apiVersion: aiven.io/v1alpha1 +kind: Valkey +metadata: + annotations: + controllers.aiven.io/generation-was-processed: "1" + controllers.aiven.io/instance-is-running: "true" + labels: + app: app-name + name: valkey-credteam-my-valkey +spec: + cloudName: google-europe-north1 + connInfoSecretTarget: + name: "" + plan: startup-4 + project: nav-dev + projectVpcId: d405e36a-a577-4dce-af0e-6d217fc47a5c + tags: + environment: dev + team: credteam + tenant: nav + terminationProtection: true +status: + conditions: + - lastTransitionTime: "2023-11-20T19:07:04Z" + message: Instance was created or update on Aiven side + reason: Updated + status: "True" + type: Initialized + - lastTransitionTime: "2024-02-23T14:55:47Z" + message: Instance is running on Aiven side + reason: CheckRunning + status: "True" + type: Running + state: RUNNING diff --git a/internal/persistence/aivencredentials/queries.go b/internal/persistence/aivencredentials/queries.go index 0e1cd771e..af662b123 100644 --- a/internal/persistence/aivencredentials/queries.go +++ b/internal/persistence/aivencredentials/queries.go @@ -104,6 +104,10 @@ func createCredentials(ctx context.Context, req credentialRequest) (any, error) } func CreateOpenSearchCredentials(ctx context.Context, input CreateOpenSearchCredentialsInput) (*CreateOpenSearchCredentialsPayload, error) { + if _, err := opensearch.Get(ctx, input.TeamSlug, input.EnvironmentName, input.InstanceName); err != nil { + return nil, err + } + // Strip "opensearch--" prefix if the user provided the full Kubernetes resource name. // The buildSpec already prepends "opensearch--" for the Aivenator. instanceName := strings.TrimPrefix(input.InstanceName, opensearch.NamePrefix(input.TeamSlug)) @@ -154,6 +158,10 @@ func valkeyEnvVarSuffix(instanceName string) string { } func CreateValkeyCredentials(ctx context.Context, input CreateValkeyCredentialsInput) (*CreateValkeyCredentialsPayload, error) { + if _, err := valkey.Get(ctx, input.TeamSlug, input.EnvironmentName, input.InstanceName); err != nil { + return nil, err + } + // Strip "valkey--" prefix if the user provided the full Kubernetes resource name. // Aivenator expects the short instance name and prepends "valkey--" itself. instanceName := strings.TrimPrefix(input.InstanceName, valkey.NamePrefix(input.TeamSlug))