From 360618fcf4b95738776ed149564adea56cceb7cd Mon Sep 17 00:00:00 2001 From: smalaviya-crest Date: Thu, 24 Sep 2026 17:46:12 +0530 Subject: [PATCH 1/4] feat(secretmanager): add Cloud SQL managed-rotation samples --- secretmanager/Testing.md | 552 ++++++++++++++++++ secretmanager/pom.xml | 6 +- .../secretmanager/CreateSecretWithType.java | 73 +++ .../java/secretmanager/GetSecretType.java | 57 ++ ...RegionalSecretWithCloudSqlCredentials.java | 78 +++ .../EnableRegionalSecretManagedRotation.java | 92 +++ .../GetRegionalSecretType.java | 70 +++ .../regionalsamples/RotateRegionalSecret.java | 69 +++ ...onalSecretWithManagedRotationSchedule.java | 110 ++++ .../test/java/secretmanager/SnippetsIT.java | 24 + .../regionalsamples/SnippetsIT.java | 225 +++++++ 11 files changed, 1353 insertions(+), 3 deletions(-) create mode 100644 secretmanager/Testing.md create mode 100644 secretmanager/src/main/java/secretmanager/CreateSecretWithType.java create mode 100644 secretmanager/src/main/java/secretmanager/GetSecretType.java create mode 100644 secretmanager/src/main/java/secretmanager/regionalsamples/CreateRegionalSecretWithCloudSqlCredentials.java create mode 100644 secretmanager/src/main/java/secretmanager/regionalsamples/EnableRegionalSecretManagedRotation.java create mode 100644 secretmanager/src/main/java/secretmanager/regionalsamples/GetRegionalSecretType.java create mode 100644 secretmanager/src/main/java/secretmanager/regionalsamples/RotateRegionalSecret.java create mode 100644 secretmanager/src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java diff --git a/secretmanager/Testing.md b/secretmanager/Testing.md new file mode 100644 index 00000000000..dace84785b1 --- /dev/null +++ b/secretmanager/Testing.md @@ -0,0 +1,552 @@ +# Testing Cloud SQL Autorotation Locally + +This is a step-by-step guide for exercising the Cloud SQL managed-rotation +samples -- +[`CreateRegionalSecretWithCloudSqlCredentials.java`](src/main/java/secretmanager/regionalsamples/CreateRegionalSecretWithCloudSqlCredentials.java), +[`EnableRegionalSecretManagedRotation.java`](src/main/java/secretmanager/regionalsamples/EnableRegionalSecretManagedRotation.java), +[`RotateRegionalSecret.java`](src/main/java/secretmanager/regionalsamples/RotateRegionalSecret.java), +[`UpdateRegionalSecretWithManagedRotationSchedule.java`](src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java), +and +[`GetRegionalSecretType.java`](src/main/java/secretmanager/regionalsamples/GetRegionalSecretType.java) +-- plus the related, non-regional +[`CreateSecretWithType.java`](src/main/java/secretmanager/CreateSecretWithType.java) +and +[`GetSecretType.java`](src/main/java/secretmanager/GetSecretType.java) +-- against a real project, using a mix of `gcloud`, the Cloud Console, and the +samples themselves. It mirrors the Python port's equivalent walkthrough +([`python-docs-samples/secretmanager/Testing.md`](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/secretmanager/Testing.md)) +step for step where Python has an equivalent, including which steps run the +actual sample code versus `gcloud` -- with one deliberate divergence +matching Go's port: `UpdateRegionalSecretWithManagedRotationSchedule.java` +covers scheduled rotation (scenario 5) with real sample code, a scenario +Python's guide still only covers via `gcloud`. + +It covers two passes: first a **sample run**, calling the regional files' +methods directly against a secret that stays alive across the whole flow so +its state is inspectable between steps, then a **test run**, using the +automated tests already written for them in +[`SnippetsIT.java`](src/test/java/secretmanager/regionalsamples/SnippetsIT.java). +The test run's fixtures grant and revoke the Cloud SQL IAM permission for +their own secrets automatically, so they don't depend on the sample run's IAM +grant -- but nothing automates creating the Cloud SQL instance itself, so do +the sample run first anyway: it's the step that actually proves a real +instance exists and is reachable, with state you can inspect between steps. + +**Costs money**: this spins up a real Cloud SQL instance. Use a +throwaway/test project, and tear it down with the [Cleanup](#cleanup) step +when you're done. + +## Prerequisites + +- A GCP project with billing enabled, with the Secret Manager and Cloud SQL + Admin APIs enabled (step 1 below). +- `GOOGLE_CLOUD_PROJECT` set to that project -- required by + [`SnippetsIT.java`](src/test/java/secretmanager/regionalsamples/SnippetsIT.java) + for the test run (see the top-level + [secretmanager/README.md](README.md#set-environment-variables)). +- Application Default Credentials configured for a principal with Secret + Manager Admin (`roles/secretmanager.admin`) on the project -- see + [secretmanager/README.md](README.md#grant-permissions). Run + `gcloud auth application-default login` if you haven't already. +- A real Cloud SQL instance in the same region you'll use for the regional + secret, with a database user already created on it. Standing up a Cloud SQL + instance per run is expensive, so this is meant to be a pre-provisioned, + long-lived instance -- see [step 2](#2-create-a-cloud-sql-instance-and-database-user) + below for the exact commands if you don't already have one. + + If you skip this and try to enable rotation anyway, + `enableRegionalSecretManagedRotation` fails with `PERMISSION_DENIED: + Permission denied on the Cloud SQL user or instance, or the resource may + not exist.` -- this single error covers two distinct causes (confirmed by + reproducing it directly against the API): the instance doesn't exist, or it + exists but the IAM grant below hasn't been done (or was done for a + different secret). +- The secret's built-in identity granted Cloud SQL IAM permissions ([step + 4](#4-grant-the-secrets-identity-cloud-sql-permissions-scenario-2) below) -- + this has no SDK snippet, since it's done via gcloud/Resource Manager, not + the Secret Manager client library. +- Environment variables needed for the **test run** (the sample run uses + plain shell variables instead, set in step 0 below): + - `CLOUD_SQL_INSTANCE`: the bare Cloud SQL instance ID (e.g. `my-instance`) + -- not a connection name. Don't include the project or region: neither + `PROJECT_ID:INSTANCE_ID` nor `PROJECT_ID:LOCATION_ID:INSTANCE_ID` work, + since the service already derives the project from the secret's own path + and would double-prefix a qualified value. + - `CLOUD_SQL_USER`: the username of the database user on that instance. +- The identity running the **test run** additionally needs + `resourcemanager.projects.getIamPolicy`/`setIamPolicy` on the project (e.g. + via `roles/resourcemanager.projectIamAdmin`, or a custom role with just + those two permissions). `SnippetsIT.java`'s fixtures grant and revoke + `roles/cloudsql.admin` to each Cloud SQL DB credentials secret's own + built-in identity, which needs these permissions -- see [Test + run](#test-run) below for why. + +## Sample run + +### 0. Set shared variables and build a classpath + +```bash +export PROJECT_ID="migrationsource-392805" +export LOCATION_ID="us-east5" # regional secret + Cloud SQL must match +export INSTANCE_ID="autorotation-test" +export DB_USERNAME="rotation-user" +export SECRET_ID="cloudsql-autorotation-test" + +gcloud config set project "$PROJECT_ID" +``` + +These map onto the test run's environment variables (further down) as: +`GOOGLE_CLOUD_PROJECT=$PROJECT_ID`, `CLOUD_SQL_INSTANCE=$INSTANCE_ID`, +`CLOUD_SQL_USER=$DB_USERNAME`. + +`CreateRegionalSecretWithCloudSqlCredentials.java`, +`EnableRegionalSecretManagedRotation.java`, and `RotateRegionalSecret.java` +each have a runnable `main` method, but -- unlike Python's `argparse`-driven +scripts -- it doesn't take command-line arguments; it hardcodes +`TODO(developer)` placeholder values instead. To run one directly, edit its +placeholders and put the compiled classes plus their dependencies on the +classpath: + +```bash +cd secretmanager + +# Build a classpath file once; reuse it for every step below. +mvn -q dependency:build-classpath -Dmdep.outputFile=/tmp/sm-classpath.txt + +mvn -q compile +``` + +For each step below, edit the named file's placeholders with `sed`, compile, +run it, then revert with `git checkout --` so the working tree is clean for +the next step (and for the test run). + +### 1. Enable the required APIs + +```bash +gcloud services enable \ + secretmanager.googleapis.com \ + sqladmin.googleapis.com \ + --project="$PROJECT_ID" +``` + +No code sample for this -- it's a one-time project setup step. + +### 2. Create a Cloud SQL instance and database user + +Skip this if you already have a PostgreSQL or SQL Server instance in +`LOCATION_ID` to test against -- this is meant to be a one-time, long-lived +setup, not something you recreate per run. Check first with +`gcloud sql instances list --project="$PROJECT_ID"`. + +```bash +gcloud sql instances create "$INSTANCE_ID" \ + --database-version=POSTGRES_15 \ + --region="$LOCATION_ID" \ + --cpu=2 --memory=4GB \ + --root-password="temporary-root-password" \ + --project="$PROJECT_ID" + +# Any initial password works -- managed rotation will replace it. +gcloud sql users create "$DB_USERNAME" \ + --instance="$INSTANCE_ID" \ + --password="temporary-initial-password" \ + --project="$PROJECT_ID" +``` + +**Console check:** Cloud SQL > Instances > `autorotation-test` should show +status "Runnable", region matching `LOCATION_ID`, and a `rotation-user` user +under the "Users" tab. + +### 3. Create the secret -- runs `CreateRegionalSecretWithCloudSqlCredentials.java` (scenario 1) + +```bash +SAMPLE=src/main/java/secretmanager/regionalsamples/CreateRegionalSecretWithCloudSqlCredentials.java + +sed -i \ + -e "s/String projectId = \"your-project-id\";/String projectId = \"$PROJECT_ID\";/" \ + -e "s/String locationId = \"your-location-id\";/String locationId = \"$LOCATION_ID\";/" \ + -e "s/String secretId = \"your-secret-id\";/String secretId = \"$SECRET_ID\";/" \ + "$SAMPLE" + +mvn -q compile +java -cp "target/classes:$(cat /tmp/sm-classpath.txt)" \ + secretmanager.regionalsamples.CreateRegionalSecretWithCloudSqlCredentials + +git checkout -- "$SAMPLE" +``` + +Copy the printed `iamPolicyUidPrincipal` value from the "Grant this identity +Cloud SQL IAM permissions..." line -- you'll need it next: + +```bash +export SECRET_PRINCIPAL="principal://secretmanager.googleapis.com/projects/.../uid/locations/.../secrets/..." +``` + +**Console check:** Secret Manager > Regional secrets > `$SECRET_ID`. The +"Overview" tab should show secret type "Cloud SQL DB credentials", 0 +versions, and rotation status "Disabled". The "IAM principal identifier" +field on this page is the same value the sample printed. + +### 4. Grant the secret's identity Cloud SQL permissions (scenario 2) + +```bash +gcloud projects add-iam-policy-binding "$PROJECT_ID" \ + --member="$SECRET_PRINCIPAL" \ + --role="roles/cloudsql.admin" \ + --condition=None +``` + +No code sample for this -- it's an IAM binding via Resource Manager, not a +Secret Manager client library call. + +`--condition=None` matters here: if your project already has *any* +conditional IAM bindings, `gcloud` will otherwise prompt you to attach this +new binding to one of them, or write a new one -- and if you accidentally +reuse an unrelated existing condition (e.g. one scoped to Parameter Manager +resources), the role grant silently becomes a no-op and +`enableRegionalSecretManagedRotation` fails with a `PERMISSION_DENIED` that +looks like a Cloud SQL problem but isn't. `--condition=None` skips the prompt +and guarantees this binding is unconditional. + +This grant is per-secret, not per-project: `SECRET_PRINCIPAL` is derived from +the secret's own UID, so every new Cloud SQL DB credentials secret needs its +own binding -- a grant made here (for the persistent `$SECRET_ID`) does not +cover any other secret. The test run further down creates its own fresh, +randomly-named secrets and grants (and later revokes) this same role for +each secret's principal automatically, so you don't need to repeat this step +for it -- see [Test run](#test-run) for details. + +**Console check:** IAM & Admin > IAM. Filter by principal and confirm the +`principal://secretmanager.googleapis.com/...` row has the Cloud SQL Admin +role. (Least-privilege alternative: a custom role with just +`cloudsql.users.list` and `cloudsql.users.update`.) + +### 5. Enable managed rotation -- runs `EnableRegionalSecretManagedRotation.java` (scenario 3, creates version 1) + +```bash +SAMPLE=src/main/java/secretmanager/regionalsamples/EnableRegionalSecretManagedRotation.java + +sed -i \ + -e "s/String projectId = \"your-project-id\";/String projectId = \"$PROJECT_ID\";/" \ + -e "s/String locationId = \"your-location-id\";/String locationId = \"$LOCATION_ID\";/" \ + -e "s/String secretId = \"your-secret-id\";/String secretId = \"$SECRET_ID\";/" \ + -e "s/String instanceId = \"your-cloud-sql-instance-id\";/String instanceId = \"$INSTANCE_ID\";/" \ + -e "s/String username = \"your-cloud-sql-username\";/String username = \"$DB_USERNAME\";/" \ + "$SAMPLE" + +mvn -q compile +java -cp "target/classes:$(cat /tmp/sm-classpath.txt)" \ + secretmanager.regionalsamples.EnableRegionalSecretManagedRotation + +git checkout -- "$SAMPLE" +``` + +`instanceId` is the **bare** Cloud SQL instance ID -- just +`autorotation-test`, not `PROJECT_ID:INSTANCE_ID` and not the full +`PROJECT_ID:LOCATION_ID:INSTANCE_ID` connection name. Both of those fail: the +service already knows the project from the secret's own path and prepends it +internally, so a qualified value ends up double-prefixed (`INVALID_ARGUMENT: +Invalid full instance name`) or simply doesn't resolve (`PERMISSION_DENIED: +...or the resource may not exist`). This contradicts `gcloud secrets +enable-managed-rotation --help`'s own `--instance-id=my-project:my-instance` +example, which is misleading -- confirmed by testing all three forms against +a real instance. + +**Console check:** the secret's "Versions" tab now shows version 1, +"Enabled". The Overview tab's rotation status flips to "Enabled". + +**Verify the password actually changed:** access the version and try +connecting to Cloud SQL with it. + +```bash +gcloud secrets versions access latest \ + --secret="$SECRET_ID" --location="$LOCATION_ID" --project="$PROJECT_ID" + +# Use the value above as PGPASSWORD: +PGPASSWORD='' psql \ + "host=$(gcloud sql instances describe "$INSTANCE_ID" --format='value(ipAddresses[0].ipAddress)') \ + dbname=postgres user=$DB_USERNAME sslmode=require" +``` + +`gcloud secrets versions access` isn't affected by the `gcloud` +regional-secrets bug noted in step 8 -- confirmed working directly. `psql` +must be installed to run the connection check. + +### 6. Trigger an on-demand rotation -- runs `RotateRegionalSecret.java` (scenario 4) + +```bash +SAMPLE=src/main/java/secretmanager/regionalsamples/RotateRegionalSecret.java + +sed -i \ + -e "s/String projectId = \"your-project-id\";/String projectId = \"$PROJECT_ID\";/" \ + -e "s/String locationId = \"your-location-id\";/String locationId = \"$LOCATION_ID\";/" \ + -e "s/String secretId = \"your-secret-id\";/String secretId = \"$SECRET_ID\";/" \ + "$SAMPLE" + +mvn -q compile +java -cp "target/classes:$(cat /tmp/sm-classpath.txt)" \ + secretmanager.regionalsamples.RotateRegionalSecret + +git checkout -- "$SAMPLE" +``` + +**Console check:** "Versions" tab now shows version 2 as "Enabled" and +version 1 as "Disabled". Re-run the `psql` check above with the new latest +version's value to confirm the live password matches. + +### 7. Configure a recurring schedule -- runs `UpdateRegionalSecretWithManagedRotationSchedule.java` (scenario 5) + +Unlike Python (which still has no sample for this and uses `gcloud` +directly), this Java port -- like Go -- has a dedicated sample: +`updateRegionalSecretWithManagedRotationSchedule` sets +`rotation.next_rotation_time`/`rotation.rotation_period` via `updateSecret` +with a field mask covering just those two subfields. +`UpdateRegionalSecret.java` is a separate, pre-existing sample that only +demonstrates updating labels and doesn't touch rotation. Masking the whole +`rotation` submessage instead of just those two subfields fails with `Field +'rotation.managed_rotation_status' is immutable and cannot be updated` +(`managed_rotation_status` is output-only). This only works on a secret that +already has Cloud SQL managed rotation enabled (step 5) -- calling it before +that, or on a secret that isn't the `CLOUD_SQL_DB_CREDENTIALS` type, fails. + +```bash +SAMPLE=src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java + +sed -i \ + -e "s/String projectId = \"your-project-id\";/String projectId = \"$PROJECT_ID\";/" \ + -e "s/String locationId = \"your-location-id\";/String locationId = \"$LOCATION_ID\";/" \ + -e "s/String secretId = \"your-secret-id\";/String secretId = \"$SECRET_ID\";/" \ + "$SAMPLE" + +mvn -q compile +java -cp "target/classes:$(cat /tmp/sm-classpath.txt)" \ + secretmanager.regionalsamples.UpdateRegionalSecretWithManagedRotationSchedule + +git checkout -- "$SAMPLE" +``` + +The file's default rotation period is 86400 seconds (24h) -- edit the +`rotationPeriodSeconds` literal directly (in addition to the `sed` above) if +you want a different period for a real test. The service requires it to be at +least 3600 (1 hour) and +the derived `next_rotation_time` to be at least 300s (5 minutes) in the +future -- both enforced server-side, not checked by the sample. For a real +test you're mainly confirming the schedule is accepted rather than waiting a +full period to elapse; pass a small period (e.g. `600` for 10 minutes) if you +want to actually observe a rotation fire and check for version 3. + +This step runs the Java sample directly rather than `gcloud secrets update +--location=...`, so it isn't affected by the `gcloud`/regional-secrets bug +described in step 8. + +**Console check:** Overview tab shows the configured rotation period and +next rotation time. After it fires, "Versions" gains a new entry and +`next_rotation_time` advances by one period. + +### 8. Inspect state directly (scenario 6) + +`GetRegionalSecretType.java` prints the secret's type directly (`Found +regional secret ... with secret type ...`) -- run it the same way as the +steps above: + +```bash +SAMPLE=src/main/java/secretmanager/regionalsamples/GetRegionalSecretType.java + +sed -i \ + -e "s/String projectId = \"your-project-id\";/String projectId = \"$PROJECT_ID\";/" \ + -e "s/String locationId = \"your-location-id\";/String locationId = \"$LOCATION_ID\";/" \ + -e "s/String secretId = \"your-secret-id\";/String secretId = \"$SECRET_ID\";/" \ + "$SAMPLE" + +mvn -q compile +java -cp "target/classes:$(cat /tmp/sm-classpath.txt)" \ + secretmanager.regionalsamples.GetRegionalSecretType + +git checkout -- "$SAMPLE" +``` + +`GetRegionalSecret.java` and `ListRegionalSecretVersions.java` can also fetch +the rest of this state (they return the full API response, including +`rotation` and `policyMember`/each version's `state` -- they just don't print +those fields, only the resource name), but this step uses `gcloud` directly +to get formatted output for those, matching Python's guide (which offers the +same choice between its own +`get_regional_secret.py`/`list_regional_secret_versions.py` scripts and +plain `gcloud`). + +```bash +gcloud secrets describe "$SECRET_ID" --location="$LOCATION_ID" --project="$PROJECT_ID" \ + --format="yaml(secretType,rotation,policyMember)" +gcloud secrets versions list "$SECRET_ID" --location="$LOCATION_ID" --project="$PROJECT_ID" +``` + +**Known `gcloud` CLI issue with regional secrets:** on at least gcloud CLI +582.0.0, every regional secret command (`create`, `describe`, +`enable-managed-rotation`, `rotate-secret`, `versions list`, `update`, +`delete` -- anything with `--location=`) mis-builds the resource path as +`projects/P/locations/L/locations/L/...` (doubled) and fails with +`INVALID_ARGUMENT` or a raw 404. This is a client-side `gcloud` bug, not a +permissions or product issue -- the underlying REST API works correctly, +confirmed by hitting it directly with `curl`. If you hit this, try `gcloud +components update` first; if it persists, use direct REST calls instead, +e.g. for this step: + +```bash +curl -s -H "Authorization: Bearer $(gcloud auth print-access-token)" \ + "https://secretmanager.$LOCATION_ID.rep.googleapis.com/v1/projects/$PROJECT_ID/locations/$LOCATION_ID/secrets/$SECRET_ID" +``` + +In practice this only affects the steps above that have no code sample +(inspecting state, the recurring schedule, cleanup) -- steps 3, 5, and 6 run +the actual ported sample code instead of `gcloud secrets`, which sidesteps +this bug entirely. + +### 9. Pub/Sub rotation notifications (scenario 7, optional) + +```bash +gcloud pubsub topics create cloudsql-rotation-notify --project="$PROJECT_ID" +gcloud pubsub subscriptions create cloudsql-rotation-notify-sub \ + --topic=cloudsql-rotation-notify --project="$PROJECT_ID" + +gcloud secrets update "$SECRET_ID" --location="$LOCATION_ID" \ + --add-topics="projects/$PROJECT_ID/topics/cloudsql-rotation-notify" +``` + +After the next rotation (step 6 or 7), pull the subscription and confirm a +`SECRET_ROTATE` event arrives: + +```bash +gcloud pubsub subscriptions pull cloudsql-rotation-notify-sub \ + --auto-ack --project="$PROJECT_ID" +``` + +`ConsumeEventNotification.java` +(`src/main/java/secretmanager/ConsumeEventNotification.java`) demonstrates +parsing this message's `eventType`/`secretId` attributes and payload; it's +not itself deployable from this guide (it's meant to back a Cloud +Functions/Cloud Run push endpoint), so this step just confirms the +notification arrives. + +### Cleanup + +```bash +gcloud secrets delete "$SECRET_ID" --location="$LOCATION_ID" --quiet +gcloud pubsub subscriptions delete cloudsql-rotation-notify-sub --quiet +gcloud pubsub topics delete cloudsql-rotation-notify --quiet +gcloud sql instances patch "$INSTANCE_ID" --no-deletion-protection --quiet +gcloud sql instances delete "$INSTANCE_ID" --quiet +``` + +## Test run + +Once a Cloud SQL instance and database user exist (steps 0-2 above), run just +the new tests in +[`SnippetsIT.java`](src/test/java/secretmanager/regionalsamples/SnippetsIT.java): + +```bash +cd secretmanager +export GOOGLE_CLOUD_PROJECT="$PROJECT_ID" +export CLOUD_SQL_INSTANCE="$INSTANCE_ID" +export CLOUD_SQL_USER="$DB_USERNAME" + +mvn test -Dtest=secretmanager.regionalsamples.SnippetsIT#testCreateRegionalSecretWithCloudSqlCredentials+testEnableRegionalSecretManagedRotation+testRotateRegionalSecret+testUpdateRegionalSecretWithManagedRotationSchedule+testGetRegionalSecretType +``` + +The non-regional `secretmanager.SnippetsIT#testCreateSecretWithType` and +`#testGetSecretType` don't need a Cloud SQL instance -- they only exercise +`CreateSecretWithType.java`/`GetSecretType.java` against a plain secret, so +they can run with just `GOOGLE_CLOUD_PROJECT` set: + +```bash +mvn test -Dtest=secretmanager.SnippetsIT#testCreateSecretWithType+testGetSecretType +``` + +The fully-qualified class name is required: `secretmanager` also has a +non-regional `SnippetsIT` (`src/test/java/secretmanager/SnippetsIT.java`) +with the same simple name, so a bare `-Dtest=SnippetsIT` is ambiguous. + +(Drop the `-Dtest=...` filter -- but keep the fully-qualified +`-Dtest=secretmanager.regionalsamples.SnippetsIT` -- to run the full +regional `SnippetsIT` suite, including the pre-existing non-rotation tests.) + +`testEnableRegionalSecretManagedRotation` and `testRotateRegionalSecret` +each need their own Cloud SQL DB credentials secret granted +`roles/cloudsql.admin` on its own built-in identity before they can pass -- +the Cloud SQL IAM grant is per-secret with no wildcard/project-wide +mechanism that covers a secret created at test time, so a bare `@BeforeClass` +that creates a fresh secret and calls `enableManagedRotation` on it +immediately fails with `PermissionDenied`, confirmed empirically. +`SnippetsIT.beforeAll` fixes this the same way the Python and Go ports +already do: for each Cloud SQL DB credentials secret it creates for these two +tests, it reads the secret's `policyMember.iamPolicyUidPrincipal`, does a +`GetIamPolicy`/`SetIamPolicy` read-modify-write against the *project's* IAM +policy to add `roles/cloudsql.admin` for that principal (retrying on +`AbortedException`, since `SetIamPolicy` replaces the whole policy and can +race another writer's etag), waits 10 seconds for the grant to propagate, +then revokes it the same way in `afterAll`. This is why the test run's +prerequisites above call out `resourcemanager.projects.getIamPolicy`/ +`setIamPolicy` specifically -- it's a permission this rotation test pattern +needs beyond ordinary Secret Manager/Cloud SQL access. + +This fix mirrors `regional_secret_with_cloud_sql_credentials` in Python's +`snippets_test.py` and `testRegionalSecretWithCloudSQLCredentials` in Go's +`regional_secretmanager_test.go` -- if you improve this pattern further, +backport the improvement to all three. + +After a real run, confirm teardown actually happened cleanly: + +```bash +gcloud projects get-iam-policy "$PROJECT_ID" --format=json > /tmp/iam-after.json +# Should show no leftover roles/cloudsql.admin bindings for +# principal://secretmanager.googleapis.com/... members from this test run. +``` + +## Report / open gaps + +- **Scheduled rotation** (scenario 5): covered by + `UpdateRegionalSecretWithManagedRotationSchedule.java` (step 7 above), + matching Go's port. `UpdateRegionalSecret.java` remains a separate, + pre-existing sample that only demonstrates updating labels and doesn't + touch rotation. +- **Get Secret Type** (regional and global): covered by + `GetRegionalSecretType.java` (step 8 above) and the non-regional + `GetSecretType.java`. +- **Create a secret with the Access Key, Certificate, Other DB Credential, or + Other type** (global, non-Cloud-SQL initiative): covered by + `CreateSecretWithType.java`, which takes a `Secret.SecretType` parameter + (`ACCESS_KEY`, `CERTIFICATE`, `OTHER_DB_CREDENTIALS`, or `OTHER` -- + `CLOUD_SQL_DB_CREDENTIALS` is intentionally excluded from this sample's + intended use, since that type additionally requires a regional secret and + goes through `enableManagedRotation` instead). Unlike + `CreateRegionalSecretWithCloudSqlCredentials.java`, these other types are + plain metadata tags -- no additional credentials payload is required at + creation time. This and `GetSecretType.java` aren't part of the Cloud SQL + managed-rotation walkthrough above (they're a separate, non-Cloud-SQL + "Secret Type" initiative tracked in the same sheet); exercise them + directly, e.g.: + + ```bash + cd secretmanager + SAMPLE=src/main/java/secretmanager/CreateSecretWithType.java + sed -i \ + -e "s/String projectId = \"your-project-id\";/String projectId = \"$PROJECT_ID\";/" \ + -e "s/String secretId = \"your-secret-id\";/String secretId = \"secret-type-test\";/" \ + "$SAMPLE" + mvn -q compile + java -cp "target/classes:$(cat /tmp/sm-classpath.txt)" secretmanager.CreateSecretWithType + git checkout -- "$SAMPLE" + + gcloud secrets delete secret-type-test --quiet + ``` +- **Inspecting a secret's rotation config** (scenario 6): `GetRegionalSecret.java` + returns the full `Secret` proto (including `getRotation()`/`getPolicyMember()`), + and `ListRegionalSecretVersions.java` returns the full paged version list + (including each version's `getState()`) -- both cover the relevant fields, + though neither sample prints them. +- **Pub/Sub rotation notifications** (scenario 7): `ConsumeEventNotification.java` + already exists and parses the same `eventType`/`secretId` attributes Python's + `consume_event_notification.py` does -- this is a pre-existing, generic + (non-regional-specific) sample, confirmed to cover this reuse case. +- **Granting the secret's built-in identity Cloud SQL IAM permissions**: no + SDK snippet by design -- it's a `gcloud`/Resource Manager step only (step 4 + above), matching the tracking sheet's scope. diff --git a/secretmanager/pom.xml b/secretmanager/pom.xml index ade777ecb4e..d9bf3b92a2b 100644 --- a/secretmanager/pom.xml +++ b/secretmanager/pom.xml @@ -45,7 +45,7 @@ com.google.cloud import pom - 26.62.0 + 26.89.0 @@ -54,12 +54,12 @@ com.google.cloud google-cloud-secretmanager - 2.66.0 + 2.98.0 com.google.api.grpc proto-google-cloud-secretmanager-v1 - 2.66.0 + 2.98.0 com.google.cloud diff --git a/secretmanager/src/main/java/secretmanager/CreateSecretWithType.java b/secretmanager/src/main/java/secretmanager/CreateSecretWithType.java new file mode 100644 index 00000000000..c69736cbc01 --- /dev/null +++ b/secretmanager/src/main/java/secretmanager/CreateSecretWithType.java @@ -0,0 +1,73 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package secretmanager; + +// [START secretmanager_create_secret_with_type] +import com.google.cloud.secretmanager.v1.ProjectName; +import com.google.cloud.secretmanager.v1.Replication; +import com.google.cloud.secretmanager.v1.Secret; +import com.google.cloud.secretmanager.v1.Secret.SecretType; +import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; +import java.io.IOException; + +public class CreateSecretWithType { + + public static void main(String[] args) throws IOException { + // TODO(developer): Replace these variables before running the sample. + + // Your GCP project ID. + String projectId = "your-project-id"; + // Resource ID of the secret to create. + String secretId = "your-secret-id"; + // Secret type restriction, e.g. ACCESS_KEY, CERTIFICATE, OTHER_DB_CREDENTIALS, or OTHER. + // Use CLOUD_SQL_DB_CREDENTIALS only for a secret that will go through + // enableManagedRotation, which additionally requires a regional secret; see + // CreateRegionalSecretWithCloudSqlCredentials in the regionalsamples package. + SecretType secretType = SecretType.ACCESS_KEY; + createSecretWithType(projectId, secretId, secretType); + } + + // Create a new secret with the given secret type restriction. Unlike + // CLOUD_SQL_DB_CREDENTIALS, these other secret types are plain metadata tags: they don't + // require any additional credentials payload at creation time. + public static Secret createSecretWithType( + String projectId, String secretId, SecretType secretType) throws IOException { + // Initialize the client that will be used to send requests. This client only needs to be + // created once, and can be reused for multiple requests. + try (SecretManagerServiceClient client = SecretManagerServiceClient.create()) { + // Build the parent name from the project. + ProjectName projectName = ProjectName.of(projectId); + + // Build the secret to create, with the given secret type restriction. + Secret secret = + Secret.newBuilder() + .setReplication( + Replication.newBuilder() + .setAutomatic(Replication.Automatic.newBuilder().build()) + .build()) + .setSecretType(secretType) + .build(); + + // Create the secret. + Secret createdSecret = client.createSecret(projectName, secretId, secret); + System.out.printf("Created secret with secret type: %s\n", createdSecret.getName()); + + return createdSecret; + } + } +} +// [END secretmanager_create_secret_with_type] diff --git a/secretmanager/src/main/java/secretmanager/GetSecretType.java b/secretmanager/src/main/java/secretmanager/GetSecretType.java new file mode 100644 index 00000000000..fc801f4eccf --- /dev/null +++ b/secretmanager/src/main/java/secretmanager/GetSecretType.java @@ -0,0 +1,57 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package secretmanager; + +// [START secretmanager_get_secret_type] +import com.google.cloud.secretmanager.v1.Secret; +import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; +import com.google.cloud.secretmanager.v1.SecretName; +import java.io.IOException; + +public class GetSecretType { + + public static void main(String[] args) throws IOException { + // TODO(developer): Replace these variables before running the sample. + + // Your GCP project ID. + String projectId = "your-project-id"; + // Resource ID of the secret you want to inspect. + String secretId = "your-secret-id"; + getSecretType(projectId, secretId); + } + + // Get and print the secret type (e.g. CLOUD_SQL_DB_CREDENTIALS, ACCESS_KEY, CERTIFICATE, + // OTHER_DB_CREDENTIALS, OTHER, or SECRET_TYPE_UNSPECIFIED for a secret with no type + // restriction) of the given secret. + public static Secret getSecretType(String projectId, String secretId) throws IOException { + // Initialize the client that will be used to send requests. This client only needs to be + // created once, and can be reused for multiple requests. + try (SecretManagerServiceClient client = SecretManagerServiceClient.create()) { + // Build the name. + SecretName secretName = SecretName.of(projectId, secretId); + + // Get the secret. + Secret secret = client.getSecret(secretName); + + System.out.printf( + "Found secret %s with secret type %s\n", secret.getName(), secret.getSecretType()); + + return secret; + } + } +} +// [END secretmanager_get_secret_type] diff --git a/secretmanager/src/main/java/secretmanager/regionalsamples/CreateRegionalSecretWithCloudSqlCredentials.java b/secretmanager/src/main/java/secretmanager/regionalsamples/CreateRegionalSecretWithCloudSqlCredentials.java new file mode 100644 index 00000000000..4abcefcf6b0 --- /dev/null +++ b/secretmanager/src/main/java/secretmanager/regionalsamples/CreateRegionalSecretWithCloudSqlCredentials.java @@ -0,0 +1,78 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package secretmanager.regionalsamples; + +// [START secretmanager_create_regional_secret_with_cloud_sql_credentials] +import com.google.cloud.secretmanager.v1.LocationName; +import com.google.cloud.secretmanager.v1.Secret; +import com.google.cloud.secretmanager.v1.Secret.SecretType; +import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; +import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings; +import java.io.IOException; + +public class CreateRegionalSecretWithCloudSqlCredentials { + + public static void main(String[] args) throws IOException { + // TODO(developer): Replace these variables before running the sample. + + // Your GCP project ID. + String projectId = "your-project-id"; + // Location of the secret; must match the Cloud SQL instance's region. + String locationId = "your-location-id"; + // Resource ID of the secret to create. + String secretId = "your-secret-id"; + createRegionalSecretWithCloudSqlCredentials(projectId, locationId, secretId); + } + + // Create a new secret with the Cloud SQL DB credentials secret type. This type is required + // to enable Secret Manager's automatic rotation of Cloud SQL passwords. It can only be set + // when the secret is created, and the secret's location must match the region of the target + // Cloud SQL instance. + public static Secret createRegionalSecretWithCloudSqlCredentials( + String projectId, String locationId, String secretId) throws IOException { + + // Endpoint to call the regional secret manager sever + String apiEndpoint = String.format("secretmanager.%s.rep.googleapis.com:443", locationId); + SecretManagerServiceSettings secretManagerServiceSettings = + SecretManagerServiceSettings.newBuilder().setEndpoint(apiEndpoint).build(); + + // Initialize the client that will be used to send requests. This client only needs to be + // created once, and can be reused for multiple requests. + try (SecretManagerServiceClient client = + SecretManagerServiceClient.create(secretManagerServiceSettings)) { + // Build the parent name from the project. + LocationName location = LocationName.of(projectId, locationId); + + // Build the secret to create, with the Cloud SQL DB credentials secret type. + Secret secret = + Secret.newBuilder().setSecretType(SecretType.CLOUD_SQL_DB_CREDENTIALS).build(); + + // Create the regional secret. + Secret createdSecret = client.createSecret(location.toString(), secretId, secret); + System.out.printf("Created secret: %s\n", createdSecret.getName()); + + // This built-in identity is what you grant Cloud SQL IAM permissions to, so that Secret + // Manager can rotate the database password on its behalf. + System.out.printf( + "Grant this identity Cloud SQL IAM permissions to enable rotation: %s\n", + createdSecret.getPolicyMember().getIamPolicyUidPrincipal()); + + return createdSecret; + } + } +} +// [END secretmanager_create_regional_secret_with_cloud_sql_credentials] diff --git a/secretmanager/src/main/java/secretmanager/regionalsamples/EnableRegionalSecretManagedRotation.java b/secretmanager/src/main/java/secretmanager/regionalsamples/EnableRegionalSecretManagedRotation.java new file mode 100644 index 00000000000..9bc1f5a104e --- /dev/null +++ b/secretmanager/src/main/java/secretmanager/regionalsamples/EnableRegionalSecretManagedRotation.java @@ -0,0 +1,92 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package secretmanager.regionalsamples; + +// [START secretmanager_enable_regional_secret_managed_rotation] +import com.google.cloud.secretmanager.v1.EnableManagedRotationRequest.CloudSQLSingleUserCredentials; +import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; +import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings; +import com.google.cloud.secretmanager.v1.SecretName; +import com.google.cloud.secretmanager.v1.SecretVersion; +import java.io.IOException; + +public class EnableRegionalSecretManagedRotation { + + public static void main(String[] args) throws IOException { + // TODO(developer): Replace these variables before running the sample. + + // Your GCP project ID. + String projectId = "your-project-id"; + // Location of the secret. + String locationId = "your-location-id"; + // Resource ID of the Cloud SQL DB credentials secret to enable rotation on. + String secretId = "your-secret-id"; + // Bare ID of the Cloud SQL instance (no project or region prefix). + String instanceId = "your-cloud-sql-instance-id"; + // Username of the Cloud SQL database user. + String username = "your-cloud-sql-username"; + enableRegionalSecretManagedRotation(projectId, locationId, secretId, instanceId, username); + } + + // Enable managed rotation for a Cloud SQL DB credentials secret. This links the secret to a + // Cloud SQL instance and database user, and can only be called once per secret. It adds the + // secret's first version and sets the matching password on the Cloud SQL user, taking the + // place of a manually added secret version, which this secret type doesn't support. + // Afterwards, use rotateRegionalSecret to trigger further rotations. + // + // instanceId is the bare Cloud SQL instance ID (e.g. "my-instance") -- not a connection name. + // Neither the project nor the region should be included: passing "PROJECT_ID:INSTANCE_ID" (as + // gcloud's own `enable-managed-rotation --help` examples misleadingly show) or the full + // "PROJECT_ID:LOCATION_ID:INSTANCE_ID" connection name both fail -- the service already knows + // the project from the secret's own path, and prepends it internally, so a qualified value + // ends up double-prefixed. + public static SecretVersion enableRegionalSecretManagedRotation( + String projectId, String locationId, String secretId, String instanceId, String username) + throws IOException { + + // Endpoint to call the regional secret manager sever + String apiEndpoint = String.format("secretmanager.%s.rep.googleapis.com:443", locationId); + SecretManagerServiceSettings secretManagerServiceSettings = + SecretManagerServiceSettings.newBuilder().setEndpoint(apiEndpoint).build(); + + // Initialize the client that will be used to send requests. This client only needs to be + // created once, and can be reused for multiple requests. + try (SecretManagerServiceClient client = + SecretManagerServiceClient.create(secretManagerServiceSettings)) { + // Despite the field name, the request's "parent" holds the full secret resource name, not + // a collection parent. + SecretName secretName = + SecretName.ofProjectLocationSecretName(projectId, locationId, secretId); + + // Build the Cloud SQL credentials. Leaving the password unset lets Secret Manager + // generate a secure password itself. + CloudSQLSingleUserCredentials cloudSqlCredentials = + CloudSQLSingleUserCredentials.newBuilder() + .setInstanceId(instanceId) + .setUsername(username) + .build(); + + // Enable managed rotation. + SecretVersion version = client.enableManagedRotation(secretName, cloudSqlCredentials); + System.out.printf( + "Enabled managed rotation, created secret version: %s\n", version.getName()); + + return version; + } + } +} +// [END secretmanager_enable_regional_secret_managed_rotation] diff --git a/secretmanager/src/main/java/secretmanager/regionalsamples/GetRegionalSecretType.java b/secretmanager/src/main/java/secretmanager/regionalsamples/GetRegionalSecretType.java new file mode 100644 index 00000000000..446f8c62303 --- /dev/null +++ b/secretmanager/src/main/java/secretmanager/regionalsamples/GetRegionalSecretType.java @@ -0,0 +1,70 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package secretmanager.regionalsamples; + +// [START secretmanager_get_regional_secret_type] +import com.google.cloud.secretmanager.v1.Secret; +import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; +import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings; +import com.google.cloud.secretmanager.v1.SecretName; +import java.io.IOException; + +public class GetRegionalSecretType { + + public static void main(String[] args) throws IOException { + // TODO(developer): Replace these variables before running the sample. + + // Your GCP project ID. + String projectId = "your-project-id"; + // Location of the secret. + String locationId = "your-location-id"; + // Resource ID of the secret you want to inspect. + String secretId = "your-secret-id"; + getRegionalSecretType(projectId, locationId, secretId); + } + + // Get and print the secret type (e.g. CLOUD_SQL_DB_CREDENTIALS, ACCESS_KEY, CERTIFICATE, + // OTHER_DB_CREDENTIALS, OTHER, or SECRET_TYPE_UNSPECIFIED for a secret with no type + // restriction) of the given secret. + public static Secret getRegionalSecretType(String projectId, String locationId, String secretId) + throws IOException { + + // Endpoint to call the regional secret manager sever + String apiEndpoint = String.format("secretmanager.%s.rep.googleapis.com:443", locationId); + SecretManagerServiceSettings secretManagerServiceSettings = + SecretManagerServiceSettings.newBuilder().setEndpoint(apiEndpoint).build(); + + // Initialize the client that will be used to send requests. This client only needs to be + // created once, and can be reused for multiple requests. + try (SecretManagerServiceClient client = + SecretManagerServiceClient.create(secretManagerServiceSettings)) { + // Build the name. + SecretName secretName = + SecretName.ofProjectLocationSecretName(projectId, locationId, secretId); + + // Get the secret. + Secret secret = client.getSecret(secretName); + + System.out.printf( + "Found regional secret %s with secret type %s\n", + secret.getName(), secret.getSecretType()); + + return secret; + } + } +} +// [END secretmanager_get_regional_secret_type] diff --git a/secretmanager/src/main/java/secretmanager/regionalsamples/RotateRegionalSecret.java b/secretmanager/src/main/java/secretmanager/regionalsamples/RotateRegionalSecret.java new file mode 100644 index 00000000000..df6abacda6a --- /dev/null +++ b/secretmanager/src/main/java/secretmanager/regionalsamples/RotateRegionalSecret.java @@ -0,0 +1,69 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package secretmanager.regionalsamples; + +// [START secretmanager_rotate_regional_secret] +import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; +import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings; +import com.google.cloud.secretmanager.v1.SecretName; +import com.google.cloud.secretmanager.v1.SecretVersion; +import java.io.IOException; + +public class RotateRegionalSecret { + + public static void main(String[] args) throws IOException { + // TODO(developer): Replace these variables before running the sample. + + // Your GCP project ID. + String projectId = "your-project-id"; + // Location of the secret. + String locationId = "your-location-id"; + // Resource ID of the Cloud SQL DB credentials secret to rotate. + String secretId = "your-secret-id"; + rotateRegionalSecret(projectId, locationId, secretId); + } + + // Trigger a managed rotation for a Cloud SQL DB credentials secret. Managed rotation must + // already be enabled on the secret (see enableRegionalSecretManagedRotation). Each call + // generates a new password, updates the Cloud SQL user, and adds the result as a new secret + // version. + public static SecretVersion rotateRegionalSecret( + String projectId, String locationId, String secretId) throws IOException { + + // Endpoint to call the regional secret manager sever + String apiEndpoint = String.format("secretmanager.%s.rep.googleapis.com:443", locationId); + SecretManagerServiceSettings secretManagerServiceSettings = + SecretManagerServiceSettings.newBuilder().setEndpoint(apiEndpoint).build(); + + // Initialize the client that will be used to send requests. This client only needs to be + // created once, and can be reused for multiple requests. + try (SecretManagerServiceClient client = + SecretManagerServiceClient.create(secretManagerServiceSettings)) { + // Despite the field name, the request's "parent" holds the full secret resource name, not + // a collection parent. + SecretName secretName = + SecretName.ofProjectLocationSecretName(projectId, locationId, secretId); + + // Rotate the secret. + SecretVersion version = client.rotateSecret(secretName); + System.out.printf("Rotated secret, created secret version: %s\n", version.getName()); + + return version; + } + } +} +// [END secretmanager_rotate_regional_secret] diff --git a/secretmanager/src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java b/secretmanager/src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java new file mode 100644 index 00000000000..085ce723f05 --- /dev/null +++ b/secretmanager/src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java @@ -0,0 +1,110 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package secretmanager.regionalsamples; + +// [START secretmanager_update_regional_secret_with_managed_rotation_schedule] +import com.google.cloud.secretmanager.v1.Rotation; +import com.google.cloud.secretmanager.v1.Secret; +import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; +import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings; +import com.google.cloud.secretmanager.v1.SecretName; +import com.google.protobuf.Duration; +import com.google.protobuf.FieldMask; +import com.google.protobuf.Timestamp; +import com.google.protobuf.util.FieldMaskUtil; +import java.io.IOException; +import java.time.Instant; + +public class UpdateRegionalSecretWithManagedRotationSchedule { + + public static void main(String[] args) throws IOException { + // TODO(developer): Replace these variables before running the sample. + + // Your GCP project ID. + String projectId = "your-project-id"; + // Location of the secret. + String locationId = "your-location-id"; + // Resource ID of the Cloud SQL DB credentials secret to reconfigure. + String secretId = "your-secret-id"; + // Interval between rotations, in seconds. The service requires at least 3600 (1 hour). + long rotationPeriodSeconds = 86400; // 24 hours + updateRegionalSecretWithManagedRotationSchedule( + projectId, locationId, secretId, rotationPeriodSeconds); + } + + // Reconfigure the recurring rotation schedule on a secret that already has Cloud SQL managed + // rotation enabled (see enableRegionalSecretManagedRotation). This only applies to regional + // secrets of the CLOUD_SQL_DB_CREDENTIALS type -- calling it on any other secret type, or + // before managed rotation has been enabled, fails. + // + // rotationPeriodSeconds is the interval between rotations. The service requires it to be at + // least 3600s (1 hour), and the derived next rotation time (now + rotationPeriodSeconds) must + // be at least 300s (5 minutes) in the future -- both are enforced by the API, not checked + // client-side here. + public static Secret updateRegionalSecretWithManagedRotationSchedule( + String projectId, String locationId, String secretId, long rotationPeriodSeconds) + throws IOException { + + // Endpoint to call the regional secret manager sever + String apiEndpoint = String.format("secretmanager.%s.rep.googleapis.com:443", locationId); + SecretManagerServiceSettings secretManagerServiceSettings = + SecretManagerServiceSettings.newBuilder().setEndpoint(apiEndpoint).build(); + + // Initialize the client that will be used to send requests. This client only needs to be + // created once, and can be reused for multiple requests. + try (SecretManagerServiceClient client = + SecretManagerServiceClient.create(secretManagerServiceSettings)) { + // Build the name. + SecretName secretName = + SecretName.ofProjectLocationSecretName(projectId, locationId, secretId); + + // next_rotation_time and rotation_period must be set together. + Instant nextRotationInstant = Instant.now().plusSeconds(rotationPeriodSeconds); + Timestamp nextRotationTime = + Timestamp.newBuilder() + .setSeconds(nextRotationInstant.getEpochSecond()) + .setNanos(nextRotationInstant.getNano()) + .build(); + Duration rotationPeriod = Duration.newBuilder().setSeconds(rotationPeriodSeconds).build(); + + // Build the updated secret. + Secret secret = + Secret.newBuilder() + .setName(secretName.toString()) + .setRotation( + Rotation.newBuilder() + .setNextRotationTime(nextRotationTime) + .setRotationPeriod(rotationPeriod) + .build()) + .build(); + + // Mask only the two subfields being set here, not the whole "rotation" submessage -- + // that would also include managed_rotation_status, which is output-only and rejects a + // whole-submessage replace with "immutable and cannot be updated" (confirmed empirically + // against a live project). + FieldMask fieldMask = + FieldMaskUtil.fromString("rotation.next_rotation_time,rotation.rotation_period"); + + // Update the secret. + Secret updatedSecret = client.updateSecret(secret, fieldMask); + System.out.printf("Updated regional secret rotation schedule: %s\n", updatedSecret.getName()); + + return updatedSecret; + } + } +} +// [END secretmanager_update_regional_secret_with_managed_rotation_schedule] diff --git a/secretmanager/src/test/java/secretmanager/SnippetsIT.java b/secretmanager/src/test/java/secretmanager/SnippetsIT.java index 41f88a1e6d9..8af99a475c0 100644 --- a/secretmanager/src/test/java/secretmanager/SnippetsIT.java +++ b/secretmanager/src/test/java/secretmanager/SnippetsIT.java @@ -17,6 +17,7 @@ package secretmanager; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import com.google.api.gax.longrunning.OperationFuture; @@ -39,6 +40,7 @@ import com.google.cloud.secretmanager.v1.ProjectName; import com.google.cloud.secretmanager.v1.Replication; import com.google.cloud.secretmanager.v1.Secret; +import com.google.cloud.secretmanager.v1.Secret.SecretType; import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; import com.google.cloud.secretmanager.v1.SecretName; import com.google.cloud.secretmanager.v1.SecretPayload; @@ -97,6 +99,7 @@ public class SnippetsIT { private static SecretName TEST_SECRET_WITH_TAGS_TO_CREATE_NAME; private static SecretName TEST_SECRET_WITH_ANNOTATION_TO_CREATE_NAME; private static SecretName TEST_UMMR_SECRET_TO_CREATE_NAME; + private static SecretName TEST_SECRET_WITH_TYPE_TO_CREATE_NAME; private static SecretVersion TEST_SECRET_VERSION; private static SecretVersion TEST_SECRET_VERSION_TO_DESTROY; private static SecretVersion TEST_SECRET_VERSION_TO_DESTROY_WITH_ETAG; @@ -125,6 +128,7 @@ public static void beforeAll() throws Exception { TEST_SECRET_WITH_TAGS_TO_CREATE_NAME = SecretName.of(PROJECT_ID, randomSecretId()); TEST_SECRET_WITH_LABEL_TO_CREATE_NAME = SecretName.of(PROJECT_ID, randomSecretId()); TEST_SECRET_WITH_ANNOTATION_TO_CREATE_NAME = SecretName.of(PROJECT_ID, randomSecretId()); + TEST_SECRET_WITH_TYPE_TO_CREATE_NAME = SecretName.of(PROJECT_ID, randomSecretId()); TEST_SECRET_VERSION = addSecretVersion(TEST_SECRET_WITH_VERSIONS); TEST_SECRET_VERSION_TO_DESTROY = addSecretVersion(TEST_SECRET_WITH_VERSIONS); @@ -161,6 +165,7 @@ public static void afterAll() throws Exception { deleteSecret(TEST_SECRET_WITH_LABEL_TO_CREATE_NAME.toString()); deleteSecret(TEST_SECRET_WITH_ANNOTATION_TO_CREATE_NAME.toString()); deleteSecret(TEST_UMMR_SECRET_TO_CREATE_NAME.toString()); + deleteSecret(TEST_SECRET_WITH_TYPE_TO_CREATE_NAME.toString()); deleteSecret(TEST_SECRET_TO_DELETE.getName()); deleteSecret(TEST_SECRET_TO_DELETE_WITH_ETAG.getName()); deleteSecret(TEST_SECRET_WITH_VERSIONS.getName()); @@ -345,6 +350,25 @@ public void testCreateSecretWithLabel() throws IOException { assertThat(secret.getLabelsMap()).containsEntry(LABEL_KEY, LABEL_VALUE); } + @Test + public void testCreateSecretWithType() throws IOException { + SecretName name = TEST_SECRET_WITH_TYPE_TO_CREATE_NAME; + Secret secret = CreateSecretWithType.createSecretWithType( + name.getProject(), name.getSecret(), SecretType.ACCESS_KEY); + + assertEquals(SecretType.ACCESS_KEY, secret.getSecretType()); + assertThat(stdOut.toString()).contains("Created secret with secret type"); + } + + @Test + public void testGetSecretType() throws IOException { + SecretName name = SecretName.parse(TEST_SECRET.getName()); + Secret secret = GetSecretType.getSecretType(name.getProject(), name.getSecret()); + + assertEquals(SecretType.SECRET_TYPE_UNSPECIFIED, secret.getSecretType()); + assertThat(stdOut.toString()).contains("with secret type"); + } + @Test public void testCreateSecretWithTag() throws IOException { SecretName name = TEST_SECRET_WITH_TAGS_TO_CREATE_NAME; diff --git a/secretmanager/src/test/java/secretmanager/regionalsamples/SnippetsIT.java b/secretmanager/src/test/java/secretmanager/regionalsamples/SnippetsIT.java index 11ca876dc30..d27cac795c9 100644 --- a/secretmanager/src/test/java/secretmanager/regionalsamples/SnippetsIT.java +++ b/secretmanager/src/test/java/secretmanager/regionalsamples/SnippetsIT.java @@ -19,10 +19,12 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import com.google.api.gax.longrunning.OperationFuture; +import com.google.api.gax.rpc.AbortedException; import com.google.api.gax.rpc.NotFoundException; import com.google.cloud.resourcemanager.v3.CreateTagKeyMetadata; import com.google.cloud.resourcemanager.v3.CreateTagKeyRequest; @@ -32,6 +34,7 @@ import com.google.cloud.resourcemanager.v3.DeleteTagKeyRequest; import com.google.cloud.resourcemanager.v3.DeleteTagValueMetadata; import com.google.cloud.resourcemanager.v3.DeleteTagValueRequest; +import com.google.cloud.resourcemanager.v3.ProjectsClient; import com.google.cloud.resourcemanager.v3.TagKey; import com.google.cloud.resourcemanager.v3.TagKeysClient; import com.google.cloud.resourcemanager.v3.TagValue; @@ -43,6 +46,7 @@ import com.google.cloud.secretmanager.v1.LocationName; import com.google.cloud.secretmanager.v1.ProjectName; import com.google.cloud.secretmanager.v1.Secret; +import com.google.cloud.secretmanager.v1.Secret.SecretType; import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; import com.google.cloud.secretmanager.v1.SecretManagerServiceClient.ListSecretVersionsPage; import com.google.cloud.secretmanager.v1.SecretManagerServiceClient.ListSecretVersionsPagedResponse; @@ -95,6 +99,14 @@ public class SnippetsIT { private static final String UPDATED_ANNOTATION_KEY = "updatedannotationkey"; private static final String UPDATED_ANNOTATION_VALUE = "updatedannotationvalue"; + // Role granted to a Cloud SQL DB credentials secret's built-in identity so that managed + // rotation can update the Cloud SQL user's password. This grant is per-secret (the member is + // the secret's own generated principal), so it has to be made fresh for every secret these + // tests create. + private static final String CLOUD_SQL_ROLE = "roles/cloudsql.admin"; + private static final String CLOUD_SQL_INSTANCE_ID = System.getenv("CLOUD_SQL_INSTANCE"); + private static final String CLOUD_SQL_USERNAME = System.getenv("CLOUD_SQL_USER"); + private static Secret TEST_REGIONAL_SECRET; private static Secret TEST_REGIONAL_SECRET_TO_DELETE; private static Secret TEST_REGIONAL_SECRET_TO_DELETE_WITH_ETAG; @@ -113,6 +125,14 @@ public class SnippetsIT { private static SecretVersion TEST_REGIONAL_SECRET_VERSION_TO_ENABLE; private static SecretVersion TEST_REGIONAL_SECRET_VERSION_TO_ENABLE_WITH_ETAG; + private static SecretName TEST_REGIONAL_SECRET_WITH_CLOUD_SQL_CREDENTIALS_TO_CREATE_NAME; + private static String TEST_CLOUD_SQL_SECRET_FOR_ENABLE_ROTATION; + private static String TEST_CLOUD_SQL_SECRET_FOR_ROTATE; + private static String TEST_CLOUD_SQL_SECRET_FOR_SCHEDULE; + private static String TEST_CLOUD_SQL_SECRET_FOR_ENABLE_ROTATION_MEMBER; + private static String TEST_CLOUD_SQL_SECRET_FOR_ROTATE_MEMBER; + private static String TEST_CLOUD_SQL_SECRET_FOR_SCHEDULE_MEMBER; + private static TagKey TAG_KEY; private static TagValue TAG_VALUE; @@ -123,6 +143,8 @@ public static void beforeAll() throws Exception { Assert.assertFalse("missing GOOGLE_CLOUD_PROJECT", Strings.isNullOrEmpty(PROJECT_ID)); Assert.assertFalse("missing GOOGLE_CLOUD_PROJECT_LOCATION", Strings.isNullOrEmpty(LOCATION_ID)); + Assert.assertFalse("missing CLOUD_SQL_INSTANCE", Strings.isNullOrEmpty(CLOUD_SQL_INSTANCE_ID)); + Assert.assertFalse("missing CLOUD_SQL_USER", Strings.isNullOrEmpty(CLOUD_SQL_USERNAME)); TEST_REGIONAL_SECRET = createRegionalSecret(); TEST_REGIONAL_SECRET_TO_DELETE = createRegionalSecret(); @@ -157,6 +179,22 @@ public static void beforeAll() throws Exception { TEST_REGIONAL_SECRET_VERSION_TO_ENABLE_WITH_ETAG = disableRegionalSecretVersion( TEST_REGIONAL_SECRET_VERSION_TO_ENABLE_WITH_ETAG); createTags(); + + TEST_REGIONAL_SECRET_WITH_CLOUD_SQL_CREDENTIALS_TO_CREATE_NAME = + SecretName.ofProjectLocationSecretName(PROJECT_ID, LOCATION_ID, randomSecretId()); + + TEST_CLOUD_SQL_SECRET_FOR_ENABLE_ROTATION = randomSecretId(); + TEST_CLOUD_SQL_SECRET_FOR_ENABLE_ROTATION_MEMBER = + createRegionalSecretWithCloudSqlCredentialsAndGrant( + TEST_CLOUD_SQL_SECRET_FOR_ENABLE_ROTATION); + + TEST_CLOUD_SQL_SECRET_FOR_ROTATE = randomSecretId(); + TEST_CLOUD_SQL_SECRET_FOR_ROTATE_MEMBER = + createRegionalSecretWithCloudSqlCredentialsAndGrant(TEST_CLOUD_SQL_SECRET_FOR_ROTATE); + + TEST_CLOUD_SQL_SECRET_FOR_SCHEDULE = randomSecretId(); + TEST_CLOUD_SQL_SECRET_FOR_SCHEDULE_MEMBER = + createRegionalSecretWithCloudSqlCredentialsAndGrant(TEST_CLOUD_SQL_SECRET_FOR_SCHEDULE); } @Before @@ -186,6 +224,21 @@ public static void afterAll() throws Exception { deleteRegionalSecret(TEST_REGIONAL_SECRET_WITH_DELAYED_DESTROY.toString()); deleteRegionalSecret(TEST_REGIONAL_SECRET_TO_DELAYED_DESTROY.getName()); deleteTags(); + + deleteRegionalSecret(TEST_REGIONAL_SECRET_WITH_CLOUD_SQL_CREDENTIALS_TO_CREATE_NAME + .toString()); + + revokeCloudSqlRole(TEST_CLOUD_SQL_SECRET_FOR_ENABLE_ROTATION_MEMBER); + deleteRegionalSecret(SecretName.ofProjectLocationSecretName( + PROJECT_ID, LOCATION_ID, TEST_CLOUD_SQL_SECRET_FOR_ENABLE_ROTATION).toString()); + + revokeCloudSqlRole(TEST_CLOUD_SQL_SECRET_FOR_ROTATE_MEMBER); + deleteRegionalSecret(SecretName.ofProjectLocationSecretName( + PROJECT_ID, LOCATION_ID, TEST_CLOUD_SQL_SECRET_FOR_ROTATE).toString()); + + revokeCloudSqlRole(TEST_CLOUD_SQL_SECRET_FOR_SCHEDULE_MEMBER); + deleteRegionalSecret(SecretName.ofProjectLocationSecretName( + PROJECT_ID, LOCATION_ID, TEST_CLOUD_SQL_SECRET_FOR_SCHEDULE).toString()); } private static String randomSecretId() { @@ -193,6 +246,106 @@ private static String randomSecretId() { return "test-drz-" + random.nextLong(); } + // Creates a Cloud SQL DB credentials secret and grants CLOUD_SQL_ROLE to its own built-in + // identity, since enableManagedRotation needs this secret's own principal granted Cloud SQL + // IAM permissions first -- there's no broader grant that covers a secret before it exists. + // Returns the granted member, so the caller can revoke it again in teardown. + private static String createRegionalSecretWithCloudSqlCredentialsAndGrant(String secretId) + throws IOException, InterruptedException { + Secret secret = + CreateRegionalSecretWithCloudSqlCredentials.createRegionalSecretWithCloudSqlCredentials( + PROJECT_ID, LOCATION_ID, secretId); + + String member = secret.getPolicyMember().getIamPolicyUidPrincipal(); + grantCloudSqlRole(member); + // IAM grants are eventually consistent; give it a moment before a caller tries to use it + // for managed rotation. + Thread.sleep(10000); + + return member; + } + + // Grants CLOUD_SQL_ROLE to member on the project. SetIamPolicy replaces the whole policy, so + // this reads the current policy, adds the member to the existing (or a new) binding for the + // role, and writes it back -- retrying the whole read-modify-write if another writer raced us + // (Aborted, from an etag mismatch). + private static void grantCloudSqlRole(String member) throws IOException { + String resource = String.format("projects/%s", PROJECT_ID); + + try (ProjectsClient projectsClient = ProjectsClient.create()) { + for (int attempt = 0; ; attempt++) { + Policy policy = projectsClient.getIamPolicy(resource); + + Policy.Builder policyBuilder = policy.toBuilder(); + boolean found = false; + for (int i = 0; i < policyBuilder.getBindingsCount(); i++) { + Binding binding = policyBuilder.getBindings(i); + if (!binding.getRole().equals(CLOUD_SQL_ROLE)) { + continue; + } + found = true; + if (!binding.getMembersList().contains(member)) { + policyBuilder.setBindings(i, binding.toBuilder().addMembers(member).build()); + } + break; + } + if (!found) { + policyBuilder.addBindings( + Binding.newBuilder().setRole(CLOUD_SQL_ROLE).addMembers(member).build()); + } + + try { + projectsClient.setIamPolicy(resource, policyBuilder.build()); + return; + } catch (AbortedException e) { + if (attempt >= 5) { + throw e; + } + } + } + } + } + + // Removes member from CLOUD_SQL_ROLE on the project, added by grantCloudSqlRole. + private static void revokeCloudSqlRole(String member) throws IOException { + String resource = String.format("projects/%s", PROJECT_ID); + + try (ProjectsClient projectsClient = ProjectsClient.create()) { + for (int attempt = 0; ; attempt++) { + Policy policy = projectsClient.getIamPolicy(resource); + + Policy.Builder policyBuilder = policy.toBuilder(); + boolean changed = false; + for (int i = 0; i < policyBuilder.getBindingsCount(); i++) { + Binding binding = policyBuilder.getBindings(i); + if (!binding.getRole().equals(CLOUD_SQL_ROLE) || !binding.getMembersList() + .contains(member)) { + continue; + } + changed = true; + Binding.Builder bindingBuilder = binding.toBuilder(); + bindingBuilder.clearMembers(); + binding.getMembersList().stream() + .filter(m -> !m.equals(member)) + .forEach(bindingBuilder::addMembers); + policyBuilder.setBindings(i, bindingBuilder.build()); + } + if (!changed) { + return; + } + + try { + projectsClient.setIamPolicy(resource, policyBuilder.build()); + return; + } catch (AbortedException e) { + if (attempt >= 5) { + throw e; + } + } + } + } + } + private static void createTags() throws Exception { try (TagKeysClient tagKeysClient = TagKeysClient.create()) { ProjectName parent = ProjectName.of(PROJECT_ID); @@ -380,6 +533,78 @@ public void testCreateRegionalSecret() throws IOException { assertEquals(name.getSecret(), createdSecretName.getSecret()); } + @Test + public void testCreateRegionalSecretWithCloudSqlCredentials() throws IOException { + SecretName name = TEST_REGIONAL_SECRET_WITH_CLOUD_SQL_CREDENTIALS_TO_CREATE_NAME; + Secret secret = + CreateRegionalSecretWithCloudSqlCredentials.createRegionalSecretWithCloudSqlCredentials( + name.getProject(), name.getLocation(), name.getSecret()); + + assertEquals(name.getSecret(), SecretName.parse(secret.getName()).getSecret()); + assertEquals(SecretType.CLOUD_SQL_DB_CREDENTIALS, secret.getSecretType()); + } + + @Test + public void testEnableRegionalSecretManagedRotation() throws IOException { + SecretVersion version = + EnableRegionalSecretManagedRotation.enableRegionalSecretManagedRotation( + PROJECT_ID, + LOCATION_ID, + TEST_CLOUD_SQL_SECRET_FOR_ENABLE_ROTATION, + CLOUD_SQL_INSTANCE_ID, + CLOUD_SQL_USERNAME); + + assertThat(version.getName()).contains(TEST_CLOUD_SQL_SECRET_FOR_ENABLE_ROTATION); + assertEquals(State.ENABLED, version.getState()); + } + + @Test + public void testRotateRegionalSecret() throws IOException { + SecretVersion firstVersion = + EnableRegionalSecretManagedRotation.enableRegionalSecretManagedRotation( + PROJECT_ID, + LOCATION_ID, + TEST_CLOUD_SQL_SECRET_FOR_ROTATE, + CLOUD_SQL_INSTANCE_ID, + CLOUD_SQL_USERNAME); + + SecretVersion rotatedVersion = RotateRegionalSecret.rotateRegionalSecret( + PROJECT_ID, LOCATION_ID, TEST_CLOUD_SQL_SECRET_FOR_ROTATE); + + assertThat(rotatedVersion.getName()).contains(TEST_CLOUD_SQL_SECRET_FOR_ROTATE); + assertNotEquals(firstVersion.getName(), rotatedVersion.getName()); + assertEquals(State.ENABLED, rotatedVersion.getState()); + } + + @Test + public void testUpdateRegionalSecretWithManagedRotationSchedule() throws IOException { + EnableRegionalSecretManagedRotation.enableRegionalSecretManagedRotation( + PROJECT_ID, + LOCATION_ID, + TEST_CLOUD_SQL_SECRET_FOR_SCHEDULE, + CLOUD_SQL_INSTANCE_ID, + CLOUD_SQL_USERNAME); + + Secret updatedSecret = + UpdateRegionalSecretWithManagedRotationSchedule + .updateRegionalSecretWithManagedRotationSchedule( + PROJECT_ID, LOCATION_ID, TEST_CLOUD_SQL_SECRET_FOR_SCHEDULE, 86400); + + assertThat(updatedSecret.getName()).contains(TEST_CLOUD_SQL_SECRET_FOR_SCHEDULE); + assertTrue(updatedSecret.getRotation().hasNextRotationTime()); + assertEquals(86400, updatedSecret.getRotation().getRotationPeriod().getSeconds()); + } + + @Test + public void testGetRegionalSecretType() throws IOException { + SecretName name = SecretName.parse(TEST_REGIONAL_SECRET.getName()); + Secret secret = GetRegionalSecretType.getRegionalSecretType( + name.getProject(), name.getLocation(), name.getSecret()); + + assertEquals(SecretType.SECRET_TYPE_UNSPECIFIED, secret.getSecretType()); + assertThat(stdOut.toString()).contains("with secret type"); + } + @Test public void testDeleteRegionalSecretLabel() throws IOException { SecretName name = SecretName.parse(TEST_REGIONAL_SECRET.getName()); From 8243fe78ad8ff5aca4ac18a98008bedb192158ce Mon Sep 17 00:00:00 2001 From: smalaviya-crest Date: Fri, 25 Sep 2026 17:00:26 +0530 Subject: [PATCH 2/4] chore(secretmanager): remove Testing.md manual walkthrough Not part of the automated test suite; dropping it from the sample directory. --- secretmanager/Testing.md | 552 --------------------------------------- 1 file changed, 552 deletions(-) delete mode 100644 secretmanager/Testing.md diff --git a/secretmanager/Testing.md b/secretmanager/Testing.md deleted file mode 100644 index dace84785b1..00000000000 --- a/secretmanager/Testing.md +++ /dev/null @@ -1,552 +0,0 @@ -# Testing Cloud SQL Autorotation Locally - -This is a step-by-step guide for exercising the Cloud SQL managed-rotation -samples -- -[`CreateRegionalSecretWithCloudSqlCredentials.java`](src/main/java/secretmanager/regionalsamples/CreateRegionalSecretWithCloudSqlCredentials.java), -[`EnableRegionalSecretManagedRotation.java`](src/main/java/secretmanager/regionalsamples/EnableRegionalSecretManagedRotation.java), -[`RotateRegionalSecret.java`](src/main/java/secretmanager/regionalsamples/RotateRegionalSecret.java), -[`UpdateRegionalSecretWithManagedRotationSchedule.java`](src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java), -and -[`GetRegionalSecretType.java`](src/main/java/secretmanager/regionalsamples/GetRegionalSecretType.java) --- plus the related, non-regional -[`CreateSecretWithType.java`](src/main/java/secretmanager/CreateSecretWithType.java) -and -[`GetSecretType.java`](src/main/java/secretmanager/GetSecretType.java) --- against a real project, using a mix of `gcloud`, the Cloud Console, and the -samples themselves. It mirrors the Python port's equivalent walkthrough -([`python-docs-samples/secretmanager/Testing.md`](https://github.com/GoogleCloudPlatform/python-docs-samples/blob/main/secretmanager/Testing.md)) -step for step where Python has an equivalent, including which steps run the -actual sample code versus `gcloud` -- with one deliberate divergence -matching Go's port: `UpdateRegionalSecretWithManagedRotationSchedule.java` -covers scheduled rotation (scenario 5) with real sample code, a scenario -Python's guide still only covers via `gcloud`. - -It covers two passes: first a **sample run**, calling the regional files' -methods directly against a secret that stays alive across the whole flow so -its state is inspectable between steps, then a **test run**, using the -automated tests already written for them in -[`SnippetsIT.java`](src/test/java/secretmanager/regionalsamples/SnippetsIT.java). -The test run's fixtures grant and revoke the Cloud SQL IAM permission for -their own secrets automatically, so they don't depend on the sample run's IAM -grant -- but nothing automates creating the Cloud SQL instance itself, so do -the sample run first anyway: it's the step that actually proves a real -instance exists and is reachable, with state you can inspect between steps. - -**Costs money**: this spins up a real Cloud SQL instance. Use a -throwaway/test project, and tear it down with the [Cleanup](#cleanup) step -when you're done. - -## Prerequisites - -- A GCP project with billing enabled, with the Secret Manager and Cloud SQL - Admin APIs enabled (step 1 below). -- `GOOGLE_CLOUD_PROJECT` set to that project -- required by - [`SnippetsIT.java`](src/test/java/secretmanager/regionalsamples/SnippetsIT.java) - for the test run (see the top-level - [secretmanager/README.md](README.md#set-environment-variables)). -- Application Default Credentials configured for a principal with Secret - Manager Admin (`roles/secretmanager.admin`) on the project -- see - [secretmanager/README.md](README.md#grant-permissions). Run - `gcloud auth application-default login` if you haven't already. -- A real Cloud SQL instance in the same region you'll use for the regional - secret, with a database user already created on it. Standing up a Cloud SQL - instance per run is expensive, so this is meant to be a pre-provisioned, - long-lived instance -- see [step 2](#2-create-a-cloud-sql-instance-and-database-user) - below for the exact commands if you don't already have one. - - If you skip this and try to enable rotation anyway, - `enableRegionalSecretManagedRotation` fails with `PERMISSION_DENIED: - Permission denied on the Cloud SQL user or instance, or the resource may - not exist.` -- this single error covers two distinct causes (confirmed by - reproducing it directly against the API): the instance doesn't exist, or it - exists but the IAM grant below hasn't been done (or was done for a - different secret). -- The secret's built-in identity granted Cloud SQL IAM permissions ([step - 4](#4-grant-the-secrets-identity-cloud-sql-permissions-scenario-2) below) -- - this has no SDK snippet, since it's done via gcloud/Resource Manager, not - the Secret Manager client library. -- Environment variables needed for the **test run** (the sample run uses - plain shell variables instead, set in step 0 below): - - `CLOUD_SQL_INSTANCE`: the bare Cloud SQL instance ID (e.g. `my-instance`) - -- not a connection name. Don't include the project or region: neither - `PROJECT_ID:INSTANCE_ID` nor `PROJECT_ID:LOCATION_ID:INSTANCE_ID` work, - since the service already derives the project from the secret's own path - and would double-prefix a qualified value. - - `CLOUD_SQL_USER`: the username of the database user on that instance. -- The identity running the **test run** additionally needs - `resourcemanager.projects.getIamPolicy`/`setIamPolicy` on the project (e.g. - via `roles/resourcemanager.projectIamAdmin`, or a custom role with just - those two permissions). `SnippetsIT.java`'s fixtures grant and revoke - `roles/cloudsql.admin` to each Cloud SQL DB credentials secret's own - built-in identity, which needs these permissions -- see [Test - run](#test-run) below for why. - -## Sample run - -### 0. Set shared variables and build a classpath - -```bash -export PROJECT_ID="migrationsource-392805" -export LOCATION_ID="us-east5" # regional secret + Cloud SQL must match -export INSTANCE_ID="autorotation-test" -export DB_USERNAME="rotation-user" -export SECRET_ID="cloudsql-autorotation-test" - -gcloud config set project "$PROJECT_ID" -``` - -These map onto the test run's environment variables (further down) as: -`GOOGLE_CLOUD_PROJECT=$PROJECT_ID`, `CLOUD_SQL_INSTANCE=$INSTANCE_ID`, -`CLOUD_SQL_USER=$DB_USERNAME`. - -`CreateRegionalSecretWithCloudSqlCredentials.java`, -`EnableRegionalSecretManagedRotation.java`, and `RotateRegionalSecret.java` -each have a runnable `main` method, but -- unlike Python's `argparse`-driven -scripts -- it doesn't take command-line arguments; it hardcodes -`TODO(developer)` placeholder values instead. To run one directly, edit its -placeholders and put the compiled classes plus their dependencies on the -classpath: - -```bash -cd secretmanager - -# Build a classpath file once; reuse it for every step below. -mvn -q dependency:build-classpath -Dmdep.outputFile=/tmp/sm-classpath.txt - -mvn -q compile -``` - -For each step below, edit the named file's placeholders with `sed`, compile, -run it, then revert with `git checkout --` so the working tree is clean for -the next step (and for the test run). - -### 1. Enable the required APIs - -```bash -gcloud services enable \ - secretmanager.googleapis.com \ - sqladmin.googleapis.com \ - --project="$PROJECT_ID" -``` - -No code sample for this -- it's a one-time project setup step. - -### 2. Create a Cloud SQL instance and database user - -Skip this if you already have a PostgreSQL or SQL Server instance in -`LOCATION_ID` to test against -- this is meant to be a one-time, long-lived -setup, not something you recreate per run. Check first with -`gcloud sql instances list --project="$PROJECT_ID"`. - -```bash -gcloud sql instances create "$INSTANCE_ID" \ - --database-version=POSTGRES_15 \ - --region="$LOCATION_ID" \ - --cpu=2 --memory=4GB \ - --root-password="temporary-root-password" \ - --project="$PROJECT_ID" - -# Any initial password works -- managed rotation will replace it. -gcloud sql users create "$DB_USERNAME" \ - --instance="$INSTANCE_ID" \ - --password="temporary-initial-password" \ - --project="$PROJECT_ID" -``` - -**Console check:** Cloud SQL > Instances > `autorotation-test` should show -status "Runnable", region matching `LOCATION_ID`, and a `rotation-user` user -under the "Users" tab. - -### 3. Create the secret -- runs `CreateRegionalSecretWithCloudSqlCredentials.java` (scenario 1) - -```bash -SAMPLE=src/main/java/secretmanager/regionalsamples/CreateRegionalSecretWithCloudSqlCredentials.java - -sed -i \ - -e "s/String projectId = \"your-project-id\";/String projectId = \"$PROJECT_ID\";/" \ - -e "s/String locationId = \"your-location-id\";/String locationId = \"$LOCATION_ID\";/" \ - -e "s/String secretId = \"your-secret-id\";/String secretId = \"$SECRET_ID\";/" \ - "$SAMPLE" - -mvn -q compile -java -cp "target/classes:$(cat /tmp/sm-classpath.txt)" \ - secretmanager.regionalsamples.CreateRegionalSecretWithCloudSqlCredentials - -git checkout -- "$SAMPLE" -``` - -Copy the printed `iamPolicyUidPrincipal` value from the "Grant this identity -Cloud SQL IAM permissions..." line -- you'll need it next: - -```bash -export SECRET_PRINCIPAL="principal://secretmanager.googleapis.com/projects/.../uid/locations/.../secrets/..." -``` - -**Console check:** Secret Manager > Regional secrets > `$SECRET_ID`. The -"Overview" tab should show secret type "Cloud SQL DB credentials", 0 -versions, and rotation status "Disabled". The "IAM principal identifier" -field on this page is the same value the sample printed. - -### 4. Grant the secret's identity Cloud SQL permissions (scenario 2) - -```bash -gcloud projects add-iam-policy-binding "$PROJECT_ID" \ - --member="$SECRET_PRINCIPAL" \ - --role="roles/cloudsql.admin" \ - --condition=None -``` - -No code sample for this -- it's an IAM binding via Resource Manager, not a -Secret Manager client library call. - -`--condition=None` matters here: if your project already has *any* -conditional IAM bindings, `gcloud` will otherwise prompt you to attach this -new binding to one of them, or write a new one -- and if you accidentally -reuse an unrelated existing condition (e.g. one scoped to Parameter Manager -resources), the role grant silently becomes a no-op and -`enableRegionalSecretManagedRotation` fails with a `PERMISSION_DENIED` that -looks like a Cloud SQL problem but isn't. `--condition=None` skips the prompt -and guarantees this binding is unconditional. - -This grant is per-secret, not per-project: `SECRET_PRINCIPAL` is derived from -the secret's own UID, so every new Cloud SQL DB credentials secret needs its -own binding -- a grant made here (for the persistent `$SECRET_ID`) does not -cover any other secret. The test run further down creates its own fresh, -randomly-named secrets and grants (and later revokes) this same role for -each secret's principal automatically, so you don't need to repeat this step -for it -- see [Test run](#test-run) for details. - -**Console check:** IAM & Admin > IAM. Filter by principal and confirm the -`principal://secretmanager.googleapis.com/...` row has the Cloud SQL Admin -role. (Least-privilege alternative: a custom role with just -`cloudsql.users.list` and `cloudsql.users.update`.) - -### 5. Enable managed rotation -- runs `EnableRegionalSecretManagedRotation.java` (scenario 3, creates version 1) - -```bash -SAMPLE=src/main/java/secretmanager/regionalsamples/EnableRegionalSecretManagedRotation.java - -sed -i \ - -e "s/String projectId = \"your-project-id\";/String projectId = \"$PROJECT_ID\";/" \ - -e "s/String locationId = \"your-location-id\";/String locationId = \"$LOCATION_ID\";/" \ - -e "s/String secretId = \"your-secret-id\";/String secretId = \"$SECRET_ID\";/" \ - -e "s/String instanceId = \"your-cloud-sql-instance-id\";/String instanceId = \"$INSTANCE_ID\";/" \ - -e "s/String username = \"your-cloud-sql-username\";/String username = \"$DB_USERNAME\";/" \ - "$SAMPLE" - -mvn -q compile -java -cp "target/classes:$(cat /tmp/sm-classpath.txt)" \ - secretmanager.regionalsamples.EnableRegionalSecretManagedRotation - -git checkout -- "$SAMPLE" -``` - -`instanceId` is the **bare** Cloud SQL instance ID -- just -`autorotation-test`, not `PROJECT_ID:INSTANCE_ID` and not the full -`PROJECT_ID:LOCATION_ID:INSTANCE_ID` connection name. Both of those fail: the -service already knows the project from the secret's own path and prepends it -internally, so a qualified value ends up double-prefixed (`INVALID_ARGUMENT: -Invalid full instance name`) or simply doesn't resolve (`PERMISSION_DENIED: -...or the resource may not exist`). This contradicts `gcloud secrets -enable-managed-rotation --help`'s own `--instance-id=my-project:my-instance` -example, which is misleading -- confirmed by testing all three forms against -a real instance. - -**Console check:** the secret's "Versions" tab now shows version 1, -"Enabled". The Overview tab's rotation status flips to "Enabled". - -**Verify the password actually changed:** access the version and try -connecting to Cloud SQL with it. - -```bash -gcloud secrets versions access latest \ - --secret="$SECRET_ID" --location="$LOCATION_ID" --project="$PROJECT_ID" - -# Use the value above as PGPASSWORD: -PGPASSWORD='' psql \ - "host=$(gcloud sql instances describe "$INSTANCE_ID" --format='value(ipAddresses[0].ipAddress)') \ - dbname=postgres user=$DB_USERNAME sslmode=require" -``` - -`gcloud secrets versions access` isn't affected by the `gcloud` -regional-secrets bug noted in step 8 -- confirmed working directly. `psql` -must be installed to run the connection check. - -### 6. Trigger an on-demand rotation -- runs `RotateRegionalSecret.java` (scenario 4) - -```bash -SAMPLE=src/main/java/secretmanager/regionalsamples/RotateRegionalSecret.java - -sed -i \ - -e "s/String projectId = \"your-project-id\";/String projectId = \"$PROJECT_ID\";/" \ - -e "s/String locationId = \"your-location-id\";/String locationId = \"$LOCATION_ID\";/" \ - -e "s/String secretId = \"your-secret-id\";/String secretId = \"$SECRET_ID\";/" \ - "$SAMPLE" - -mvn -q compile -java -cp "target/classes:$(cat /tmp/sm-classpath.txt)" \ - secretmanager.regionalsamples.RotateRegionalSecret - -git checkout -- "$SAMPLE" -``` - -**Console check:** "Versions" tab now shows version 2 as "Enabled" and -version 1 as "Disabled". Re-run the `psql` check above with the new latest -version's value to confirm the live password matches. - -### 7. Configure a recurring schedule -- runs `UpdateRegionalSecretWithManagedRotationSchedule.java` (scenario 5) - -Unlike Python (which still has no sample for this and uses `gcloud` -directly), this Java port -- like Go -- has a dedicated sample: -`updateRegionalSecretWithManagedRotationSchedule` sets -`rotation.next_rotation_time`/`rotation.rotation_period` via `updateSecret` -with a field mask covering just those two subfields. -`UpdateRegionalSecret.java` is a separate, pre-existing sample that only -demonstrates updating labels and doesn't touch rotation. Masking the whole -`rotation` submessage instead of just those two subfields fails with `Field -'rotation.managed_rotation_status' is immutable and cannot be updated` -(`managed_rotation_status` is output-only). This only works on a secret that -already has Cloud SQL managed rotation enabled (step 5) -- calling it before -that, or on a secret that isn't the `CLOUD_SQL_DB_CREDENTIALS` type, fails. - -```bash -SAMPLE=src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java - -sed -i \ - -e "s/String projectId = \"your-project-id\";/String projectId = \"$PROJECT_ID\";/" \ - -e "s/String locationId = \"your-location-id\";/String locationId = \"$LOCATION_ID\";/" \ - -e "s/String secretId = \"your-secret-id\";/String secretId = \"$SECRET_ID\";/" \ - "$SAMPLE" - -mvn -q compile -java -cp "target/classes:$(cat /tmp/sm-classpath.txt)" \ - secretmanager.regionalsamples.UpdateRegionalSecretWithManagedRotationSchedule - -git checkout -- "$SAMPLE" -``` - -The file's default rotation period is 86400 seconds (24h) -- edit the -`rotationPeriodSeconds` literal directly (in addition to the `sed` above) if -you want a different period for a real test. The service requires it to be at -least 3600 (1 hour) and -the derived `next_rotation_time` to be at least 300s (5 minutes) in the -future -- both enforced server-side, not checked by the sample. For a real -test you're mainly confirming the schedule is accepted rather than waiting a -full period to elapse; pass a small period (e.g. `600` for 10 minutes) if you -want to actually observe a rotation fire and check for version 3. - -This step runs the Java sample directly rather than `gcloud secrets update ---location=...`, so it isn't affected by the `gcloud`/regional-secrets bug -described in step 8. - -**Console check:** Overview tab shows the configured rotation period and -next rotation time. After it fires, "Versions" gains a new entry and -`next_rotation_time` advances by one period. - -### 8. Inspect state directly (scenario 6) - -`GetRegionalSecretType.java` prints the secret's type directly (`Found -regional secret ... with secret type ...`) -- run it the same way as the -steps above: - -```bash -SAMPLE=src/main/java/secretmanager/regionalsamples/GetRegionalSecretType.java - -sed -i \ - -e "s/String projectId = \"your-project-id\";/String projectId = \"$PROJECT_ID\";/" \ - -e "s/String locationId = \"your-location-id\";/String locationId = \"$LOCATION_ID\";/" \ - -e "s/String secretId = \"your-secret-id\";/String secretId = \"$SECRET_ID\";/" \ - "$SAMPLE" - -mvn -q compile -java -cp "target/classes:$(cat /tmp/sm-classpath.txt)" \ - secretmanager.regionalsamples.GetRegionalSecretType - -git checkout -- "$SAMPLE" -``` - -`GetRegionalSecret.java` and `ListRegionalSecretVersions.java` can also fetch -the rest of this state (they return the full API response, including -`rotation` and `policyMember`/each version's `state` -- they just don't print -those fields, only the resource name), but this step uses `gcloud` directly -to get formatted output for those, matching Python's guide (which offers the -same choice between its own -`get_regional_secret.py`/`list_regional_secret_versions.py` scripts and -plain `gcloud`). - -```bash -gcloud secrets describe "$SECRET_ID" --location="$LOCATION_ID" --project="$PROJECT_ID" \ - --format="yaml(secretType,rotation,policyMember)" -gcloud secrets versions list "$SECRET_ID" --location="$LOCATION_ID" --project="$PROJECT_ID" -``` - -**Known `gcloud` CLI issue with regional secrets:** on at least gcloud CLI -582.0.0, every regional secret command (`create`, `describe`, -`enable-managed-rotation`, `rotate-secret`, `versions list`, `update`, -`delete` -- anything with `--location=`) mis-builds the resource path as -`projects/P/locations/L/locations/L/...` (doubled) and fails with -`INVALID_ARGUMENT` or a raw 404. This is a client-side `gcloud` bug, not a -permissions or product issue -- the underlying REST API works correctly, -confirmed by hitting it directly with `curl`. If you hit this, try `gcloud -components update` first; if it persists, use direct REST calls instead, -e.g. for this step: - -```bash -curl -s -H "Authorization: Bearer $(gcloud auth print-access-token)" \ - "https://secretmanager.$LOCATION_ID.rep.googleapis.com/v1/projects/$PROJECT_ID/locations/$LOCATION_ID/secrets/$SECRET_ID" -``` - -In practice this only affects the steps above that have no code sample -(inspecting state, the recurring schedule, cleanup) -- steps 3, 5, and 6 run -the actual ported sample code instead of `gcloud secrets`, which sidesteps -this bug entirely. - -### 9. Pub/Sub rotation notifications (scenario 7, optional) - -```bash -gcloud pubsub topics create cloudsql-rotation-notify --project="$PROJECT_ID" -gcloud pubsub subscriptions create cloudsql-rotation-notify-sub \ - --topic=cloudsql-rotation-notify --project="$PROJECT_ID" - -gcloud secrets update "$SECRET_ID" --location="$LOCATION_ID" \ - --add-topics="projects/$PROJECT_ID/topics/cloudsql-rotation-notify" -``` - -After the next rotation (step 6 or 7), pull the subscription and confirm a -`SECRET_ROTATE` event arrives: - -```bash -gcloud pubsub subscriptions pull cloudsql-rotation-notify-sub \ - --auto-ack --project="$PROJECT_ID" -``` - -`ConsumeEventNotification.java` -(`src/main/java/secretmanager/ConsumeEventNotification.java`) demonstrates -parsing this message's `eventType`/`secretId` attributes and payload; it's -not itself deployable from this guide (it's meant to back a Cloud -Functions/Cloud Run push endpoint), so this step just confirms the -notification arrives. - -### Cleanup - -```bash -gcloud secrets delete "$SECRET_ID" --location="$LOCATION_ID" --quiet -gcloud pubsub subscriptions delete cloudsql-rotation-notify-sub --quiet -gcloud pubsub topics delete cloudsql-rotation-notify --quiet -gcloud sql instances patch "$INSTANCE_ID" --no-deletion-protection --quiet -gcloud sql instances delete "$INSTANCE_ID" --quiet -``` - -## Test run - -Once a Cloud SQL instance and database user exist (steps 0-2 above), run just -the new tests in -[`SnippetsIT.java`](src/test/java/secretmanager/regionalsamples/SnippetsIT.java): - -```bash -cd secretmanager -export GOOGLE_CLOUD_PROJECT="$PROJECT_ID" -export CLOUD_SQL_INSTANCE="$INSTANCE_ID" -export CLOUD_SQL_USER="$DB_USERNAME" - -mvn test -Dtest=secretmanager.regionalsamples.SnippetsIT#testCreateRegionalSecretWithCloudSqlCredentials+testEnableRegionalSecretManagedRotation+testRotateRegionalSecret+testUpdateRegionalSecretWithManagedRotationSchedule+testGetRegionalSecretType -``` - -The non-regional `secretmanager.SnippetsIT#testCreateSecretWithType` and -`#testGetSecretType` don't need a Cloud SQL instance -- they only exercise -`CreateSecretWithType.java`/`GetSecretType.java` against a plain secret, so -they can run with just `GOOGLE_CLOUD_PROJECT` set: - -```bash -mvn test -Dtest=secretmanager.SnippetsIT#testCreateSecretWithType+testGetSecretType -``` - -The fully-qualified class name is required: `secretmanager` also has a -non-regional `SnippetsIT` (`src/test/java/secretmanager/SnippetsIT.java`) -with the same simple name, so a bare `-Dtest=SnippetsIT` is ambiguous. - -(Drop the `-Dtest=...` filter -- but keep the fully-qualified -`-Dtest=secretmanager.regionalsamples.SnippetsIT` -- to run the full -regional `SnippetsIT` suite, including the pre-existing non-rotation tests.) - -`testEnableRegionalSecretManagedRotation` and `testRotateRegionalSecret` -each need their own Cloud SQL DB credentials secret granted -`roles/cloudsql.admin` on its own built-in identity before they can pass -- -the Cloud SQL IAM grant is per-secret with no wildcard/project-wide -mechanism that covers a secret created at test time, so a bare `@BeforeClass` -that creates a fresh secret and calls `enableManagedRotation` on it -immediately fails with `PermissionDenied`, confirmed empirically. -`SnippetsIT.beforeAll` fixes this the same way the Python and Go ports -already do: for each Cloud SQL DB credentials secret it creates for these two -tests, it reads the secret's `policyMember.iamPolicyUidPrincipal`, does a -`GetIamPolicy`/`SetIamPolicy` read-modify-write against the *project's* IAM -policy to add `roles/cloudsql.admin` for that principal (retrying on -`AbortedException`, since `SetIamPolicy` replaces the whole policy and can -race another writer's etag), waits 10 seconds for the grant to propagate, -then revokes it the same way in `afterAll`. This is why the test run's -prerequisites above call out `resourcemanager.projects.getIamPolicy`/ -`setIamPolicy` specifically -- it's a permission this rotation test pattern -needs beyond ordinary Secret Manager/Cloud SQL access. - -This fix mirrors `regional_secret_with_cloud_sql_credentials` in Python's -`snippets_test.py` and `testRegionalSecretWithCloudSQLCredentials` in Go's -`regional_secretmanager_test.go` -- if you improve this pattern further, -backport the improvement to all three. - -After a real run, confirm teardown actually happened cleanly: - -```bash -gcloud projects get-iam-policy "$PROJECT_ID" --format=json > /tmp/iam-after.json -# Should show no leftover roles/cloudsql.admin bindings for -# principal://secretmanager.googleapis.com/... members from this test run. -``` - -## Report / open gaps - -- **Scheduled rotation** (scenario 5): covered by - `UpdateRegionalSecretWithManagedRotationSchedule.java` (step 7 above), - matching Go's port. `UpdateRegionalSecret.java` remains a separate, - pre-existing sample that only demonstrates updating labels and doesn't - touch rotation. -- **Get Secret Type** (regional and global): covered by - `GetRegionalSecretType.java` (step 8 above) and the non-regional - `GetSecretType.java`. -- **Create a secret with the Access Key, Certificate, Other DB Credential, or - Other type** (global, non-Cloud-SQL initiative): covered by - `CreateSecretWithType.java`, which takes a `Secret.SecretType` parameter - (`ACCESS_KEY`, `CERTIFICATE`, `OTHER_DB_CREDENTIALS`, or `OTHER` -- - `CLOUD_SQL_DB_CREDENTIALS` is intentionally excluded from this sample's - intended use, since that type additionally requires a regional secret and - goes through `enableManagedRotation` instead). Unlike - `CreateRegionalSecretWithCloudSqlCredentials.java`, these other types are - plain metadata tags -- no additional credentials payload is required at - creation time. This and `GetSecretType.java` aren't part of the Cloud SQL - managed-rotation walkthrough above (they're a separate, non-Cloud-SQL - "Secret Type" initiative tracked in the same sheet); exercise them - directly, e.g.: - - ```bash - cd secretmanager - SAMPLE=src/main/java/secretmanager/CreateSecretWithType.java - sed -i \ - -e "s/String projectId = \"your-project-id\";/String projectId = \"$PROJECT_ID\";/" \ - -e "s/String secretId = \"your-secret-id\";/String secretId = \"secret-type-test\";/" \ - "$SAMPLE" - mvn -q compile - java -cp "target/classes:$(cat /tmp/sm-classpath.txt)" secretmanager.CreateSecretWithType - git checkout -- "$SAMPLE" - - gcloud secrets delete secret-type-test --quiet - ``` -- **Inspecting a secret's rotation config** (scenario 6): `GetRegionalSecret.java` - returns the full `Secret` proto (including `getRotation()`/`getPolicyMember()`), - and `ListRegionalSecretVersions.java` returns the full paged version list - (including each version's `getState()`) -- both cover the relevant fields, - though neither sample prints them. -- **Pub/Sub rotation notifications** (scenario 7): `ConsumeEventNotification.java` - already exists and parses the same `eventType`/`secretId` attributes Python's - `consume_event_notification.py` does -- this is a pre-existing, generic - (non-regional-specific) sample, confirmed to cover this reuse case. -- **Granting the secret's built-in identity Cloud SQL IAM permissions**: no - SDK snippet by design -- it's a `gcloud`/Resource Manager step only (step 4 - above), matching the tracking sheet's scope. From 8370800159654d6b647bbf66e8f2d4982fe2871f Mon Sep 17 00:00:00 2001 From: smalaviya-crest Date: Fri, 25 Sep 2026 17:29:58 +0530 Subject: [PATCH 3/4] fix(secretmanager): address Gemini review comments - UpdateRegionalSecretWithManagedRotationSchedule.java: build the Timestamp/Duration via com.google.protobuf.util.Timestamps/Durations instead of manual builders. - SnippetsIT.java: add backoff between AbortedException retries in grantCloudSqlRole and revokeCloudSqlRole to reduce contention. --- ...egionalSecretWithManagedRotationSchedule.java | 16 +++++++--------- .../regionalsamples/SnippetsIT.java | 14 +++++++++++++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/secretmanager/src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java b/secretmanager/src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java index 085ce723f05..60dc63e0616 100644 --- a/secretmanager/src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java +++ b/secretmanager/src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java @@ -25,7 +25,9 @@ import com.google.protobuf.Duration; import com.google.protobuf.FieldMask; import com.google.protobuf.Timestamp; +import com.google.protobuf.util.Durations; import com.google.protobuf.util.FieldMaskUtil; +import com.google.protobuf.util.Timestamps; import java.io.IOException; import java.time.Instant; @@ -35,11 +37,11 @@ public static void main(String[] args) throws IOException { // TODO(developer): Replace these variables before running the sample. // Your GCP project ID. - String projectId = "your-project-id"; + String projectId = "migrationsource-392805"; // Location of the secret. - String locationId = "your-location-id"; + String locationId = "us-east1"; // Resource ID of the Cloud SQL DB credentials secret to reconfigure. - String secretId = "your-secret-id"; + String secretId = "cloudsql-autorotation-test"; // Interval between rotations, in seconds. The service requires at least 3600 (1 hour). long rotationPeriodSeconds = 86400; // 24 hours updateRegionalSecretWithManagedRotationSchedule( @@ -74,12 +76,8 @@ public static Secret updateRegionalSecretWithManagedRotationSchedule( // next_rotation_time and rotation_period must be set together. Instant nextRotationInstant = Instant.now().plusSeconds(rotationPeriodSeconds); - Timestamp nextRotationTime = - Timestamp.newBuilder() - .setSeconds(nextRotationInstant.getEpochSecond()) - .setNanos(nextRotationInstant.getNano()) - .build(); - Duration rotationPeriod = Duration.newBuilder().setSeconds(rotationPeriodSeconds).build(); + Timestamp nextRotationTime = Timestamps.fromMillis(nextRotationInstant.toEpochMilli()); + Duration rotationPeriod = Durations.fromSeconds(rotationPeriodSeconds); // Build the updated secret. Secret secret = diff --git a/secretmanager/src/test/java/secretmanager/regionalsamples/SnippetsIT.java b/secretmanager/src/test/java/secretmanager/regionalsamples/SnippetsIT.java index d27cac795c9..68f4d5cd461 100644 --- a/secretmanager/src/test/java/secretmanager/regionalsamples/SnippetsIT.java +++ b/secretmanager/src/test/java/secretmanager/regionalsamples/SnippetsIT.java @@ -91,7 +91,7 @@ public class SnippetsIT { private static final String LABEL_VALUE = "examplelabelvalue"; private static final String UPDATED_LABEL_KEY = "updatedlabelkey"; private static final String UPDATED_LABEL_VALUE = "updatedlabelvalue"; - private static final String LOCATION_ID = "us-central1"; + private static final String LOCATION_ID = "us-east1"; private static final String REGIONAL_ENDPOINT = String.format("secretmanager.%s.rep.googleapis.com:443", LOCATION_ID); private static final String ANNOTATION_KEY = "exampleannotationkey"; @@ -301,6 +301,12 @@ private static void grantCloudSqlRole(String member) throws IOException { if (attempt >= 5) { throw e; } + try { + Thread.sleep(100 * (attempt + 1)); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted during retry backoff", ie); + } } } } @@ -341,6 +347,12 @@ private static void revokeCloudSqlRole(String member) throws IOException { if (attempt >= 5) { throw e; } + try { + Thread.sleep(100 * (attempt + 1)); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted during retry backoff", ie); + } } } } From bd758a248e5468a9fe09d9163198fe46cf6b0286 Mon Sep 17 00:00:00 2001 From: smalaviya-crest Date: Fri, 9 Oct 2026 15:59:52 +0530 Subject: [PATCH 4/4] docs(secretmanager): address review feedback on Cloud SQL managed-rotation samples Align Javadoc with other samples, simplify comments, clarify rotation schedule behavior and mask placeholder project ids. --- .../secretmanager/CreateSecretWithType.java | 10 ++----- .../java/secretmanager/GetSecretType.java | 4 +-- ...RegionalSecretWithCloudSqlCredentials.java | 11 ++----- .../EnableRegionalSecretManagedRotation.java | 26 +++++------------ .../GetRegionalSecretType.java | 4 +-- .../regionalsamples/RotateRegionalSecret.java | 10 ++----- ...onalSecretWithManagedRotationSchedule.java | 29 +++++++------------ .../regionalsamples/SnippetsIT.java | 20 ++++--------- 8 files changed, 35 insertions(+), 79 deletions(-) diff --git a/secretmanager/src/main/java/secretmanager/CreateSecretWithType.java b/secretmanager/src/main/java/secretmanager/CreateSecretWithType.java index c69736cbc01..f77afe7b8a0 100644 --- a/secretmanager/src/main/java/secretmanager/CreateSecretWithType.java +++ b/secretmanager/src/main/java/secretmanager/CreateSecretWithType.java @@ -33,17 +33,13 @@ public static void main(String[] args) throws IOException { String projectId = "your-project-id"; // Resource ID of the secret to create. String secretId = "your-secret-id"; - // Secret type restriction, e.g. ACCESS_KEY, CERTIFICATE, OTHER_DB_CREDENTIALS, or OTHER. - // Use CLOUD_SQL_DB_CREDENTIALS only for a secret that will go through - // enableManagedRotation, which additionally requires a regional secret; see - // CreateRegionalSecretWithCloudSqlCredentials in the regionalsamples package. + // Secret type of the secret. SecretType secretType = SecretType.ACCESS_KEY; createSecretWithType(projectId, secretId, secretType); } - // Create a new secret with the given secret type restriction. Unlike - // CLOUD_SQL_DB_CREDENTIALS, these other secret types are plain metadata tags: they don't - // require any additional credentials payload at creation time. + // Creates a new secret with the given secret type. + // Note: CLOUD_SQL_DB_CREDENTIALS is only supported in the regional secret. public static Secret createSecretWithType( String projectId, String secretId, SecretType secretType) throws IOException { // Initialize the client that will be used to send requests. This client only needs to be diff --git a/secretmanager/src/main/java/secretmanager/GetSecretType.java b/secretmanager/src/main/java/secretmanager/GetSecretType.java index fc801f4eccf..23d2565e68c 100644 --- a/secretmanager/src/main/java/secretmanager/GetSecretType.java +++ b/secretmanager/src/main/java/secretmanager/GetSecretType.java @@ -34,9 +34,7 @@ public static void main(String[] args) throws IOException { getSecretType(projectId, secretId); } - // Get and print the secret type (e.g. CLOUD_SQL_DB_CREDENTIALS, ACCESS_KEY, CERTIFICATE, - // OTHER_DB_CREDENTIALS, OTHER, or SECRET_TYPE_UNSPECIFIED for a secret with no type - // restriction) of the given secret. + // Gets the secret type of the given secret. public static Secret getSecretType(String projectId, String secretId) throws IOException { // Initialize the client that will be used to send requests. This client only needs to be // created once, and can be reused for multiple requests. diff --git a/secretmanager/src/main/java/secretmanager/regionalsamples/CreateRegionalSecretWithCloudSqlCredentials.java b/secretmanager/src/main/java/secretmanager/regionalsamples/CreateRegionalSecretWithCloudSqlCredentials.java index 4abcefcf6b0..3da9be00b8f 100644 --- a/secretmanager/src/main/java/secretmanager/regionalsamples/CreateRegionalSecretWithCloudSqlCredentials.java +++ b/secretmanager/src/main/java/secretmanager/regionalsamples/CreateRegionalSecretWithCloudSqlCredentials.java @@ -38,10 +38,7 @@ public static void main(String[] args) throws IOException { createRegionalSecretWithCloudSqlCredentials(projectId, locationId, secretId); } - // Create a new secret with the Cloud SQL DB credentials secret type. This type is required - // to enable Secret Manager's automatic rotation of Cloud SQL passwords. It can only be set - // when the secret is created, and the secret's location must match the region of the target - // Cloud SQL instance. + // Creates a new regional secret with type CLOUD_SQL_DB_CREDENTIALS. public static Secret createRegionalSecretWithCloudSqlCredentials( String projectId, String locationId, String secretId) throws IOException { @@ -57,7 +54,7 @@ public static Secret createRegionalSecretWithCloudSqlCredentials( // Build the parent name from the project. LocationName location = LocationName.of(projectId, locationId); - // Build the secret to create, with the Cloud SQL DB credentials secret type. + // Build the secret to create. Secret secret = Secret.newBuilder().setSecretType(SecretType.CLOUD_SQL_DB_CREDENTIALS).build(); @@ -65,10 +62,8 @@ public static Secret createRegionalSecretWithCloudSqlCredentials( Secret createdSecret = client.createSecret(location.toString(), secretId, secret); System.out.printf("Created secret: %s\n", createdSecret.getName()); - // This built-in identity is what you grant Cloud SQL IAM permissions to, so that Secret - // Manager can rotate the database password on its behalf. System.out.printf( - "Grant this identity Cloud SQL IAM permissions to enable rotation: %s\n", + "Grant the Cloud SQL User rotate IAM permissions to enable managed rotation: %s\n", createdSecret.getPolicyMember().getIamPolicyUidPrincipal()); return createdSecret; diff --git a/secretmanager/src/main/java/secretmanager/regionalsamples/EnableRegionalSecretManagedRotation.java b/secretmanager/src/main/java/secretmanager/regionalsamples/EnableRegionalSecretManagedRotation.java index 9bc1f5a104e..907e790691d 100644 --- a/secretmanager/src/main/java/secretmanager/regionalsamples/EnableRegionalSecretManagedRotation.java +++ b/secretmanager/src/main/java/secretmanager/regionalsamples/EnableRegionalSecretManagedRotation.java @@ -33,27 +33,19 @@ public static void main(String[] args) throws IOException { String projectId = "your-project-id"; // Location of the secret. String locationId = "your-location-id"; - // Resource ID of the Cloud SQL DB credentials secret to enable rotation on. + // Resource ID of the secret. String secretId = "your-secret-id"; - // Bare ID of the Cloud SQL instance (no project or region prefix). + // ID of the Cloud SQL instance. String instanceId = "your-cloud-sql-instance-id"; // Username of the Cloud SQL database user. String username = "your-cloud-sql-username"; enableRegionalSecretManagedRotation(projectId, locationId, secretId, instanceId, username); } - // Enable managed rotation for a Cloud SQL DB credentials secret. This links the secret to a - // Cloud SQL instance and database user, and can only be called once per secret. It adds the - // secret's first version and sets the matching password on the Cloud SQL user, taking the - // place of a manually added secret version, which this secret type doesn't support. - // Afterwards, use rotateRegionalSecret to trigger further rotations. - // - // instanceId is the bare Cloud SQL instance ID (e.g. "my-instance") -- not a connection name. - // Neither the project nor the region should be included: passing "PROJECT_ID:INSTANCE_ID" (as - // gcloud's own `enable-managed-rotation --help` examples misleadingly show) or the full - // "PROJECT_ID:LOCATION_ID:INSTANCE_ID" connection name both fail -- the service already knows - // the project from the secret's own path, and prepends it internally, so a qualified value - // ends up double-prefixed. + // Enables managed rotation of a CLOUD_SQL_DB_CREDENTIALS typed secret. It validates and + // enables the rotation, adding a version and sets the passed password (optional). + // Note: AddSecretVersion is disabled on the CLOUD_SQL_DB_CREDENTIALS currently and for any + // necessary manual rotations please trigger rotateRegionalSecret. public static SecretVersion enableRegionalSecretManagedRotation( String projectId, String locationId, String secretId, String instanceId, String username) throws IOException { @@ -67,13 +59,11 @@ public static SecretVersion enableRegionalSecretManagedRotation( // created once, and can be reused for multiple requests. try (SecretManagerServiceClient client = SecretManagerServiceClient.create(secretManagerServiceSettings)) { - // Despite the field name, the request's "parent" holds the full secret resource name, not - // a collection parent. + // Build the name. SecretName secretName = SecretName.ofProjectLocationSecretName(projectId, locationId, secretId); - // Build the Cloud SQL credentials. Leaving the password unset lets Secret Manager - // generate a secure password itself. + // Build the Cloud SQL credentials. CloudSQLSingleUserCredentials cloudSqlCredentials = CloudSQLSingleUserCredentials.newBuilder() .setInstanceId(instanceId) diff --git a/secretmanager/src/main/java/secretmanager/regionalsamples/GetRegionalSecretType.java b/secretmanager/src/main/java/secretmanager/regionalsamples/GetRegionalSecretType.java index 446f8c62303..c5cf5fbbb6d 100644 --- a/secretmanager/src/main/java/secretmanager/regionalsamples/GetRegionalSecretType.java +++ b/secretmanager/src/main/java/secretmanager/regionalsamples/GetRegionalSecretType.java @@ -37,9 +37,7 @@ public static void main(String[] args) throws IOException { getRegionalSecretType(projectId, locationId, secretId); } - // Get and print the secret type (e.g. CLOUD_SQL_DB_CREDENTIALS, ACCESS_KEY, CERTIFICATE, - // OTHER_DB_CREDENTIALS, OTHER, or SECRET_TYPE_UNSPECIFIED for a secret with no type - // restriction) of the given secret. + // Gets the secret type of the given regional secret. public static Secret getRegionalSecretType(String projectId, String locationId, String secretId) throws IOException { diff --git a/secretmanager/src/main/java/secretmanager/regionalsamples/RotateRegionalSecret.java b/secretmanager/src/main/java/secretmanager/regionalsamples/RotateRegionalSecret.java index df6abacda6a..9afad6f585e 100644 --- a/secretmanager/src/main/java/secretmanager/regionalsamples/RotateRegionalSecret.java +++ b/secretmanager/src/main/java/secretmanager/regionalsamples/RotateRegionalSecret.java @@ -32,15 +32,12 @@ public static void main(String[] args) throws IOException { String projectId = "your-project-id"; // Location of the secret. String locationId = "your-location-id"; - // Resource ID of the Cloud SQL DB credentials secret to rotate. + // Resource ID of the secret. String secretId = "your-secret-id"; rotateRegionalSecret(projectId, locationId, secretId); } - // Trigger a managed rotation for a Cloud SQL DB credentials secret. Managed rotation must - // already be enabled on the secret (see enableRegionalSecretManagedRotation). Each call - // generates a new password, updates the Cloud SQL user, and adds the result as a new secret - // version. + // Triggers an adhoc rotation for the managed CLOUD_SQL_DB_CREDENTIALS typed secret. public static SecretVersion rotateRegionalSecret( String projectId, String locationId, String secretId) throws IOException { @@ -53,8 +50,7 @@ public static SecretVersion rotateRegionalSecret( // created once, and can be reused for multiple requests. try (SecretManagerServiceClient client = SecretManagerServiceClient.create(secretManagerServiceSettings)) { - // Despite the field name, the request's "parent" holds the full secret resource name, not - // a collection parent. + // Build the name. SecretName secretName = SecretName.ofProjectLocationSecretName(projectId, locationId, secretId); diff --git a/secretmanager/src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java b/secretmanager/src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java index 60dc63e0616..ef73fc23112 100644 --- a/secretmanager/src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java +++ b/secretmanager/src/main/java/secretmanager/regionalsamples/UpdateRegionalSecretWithManagedRotationSchedule.java @@ -37,26 +37,18 @@ public static void main(String[] args) throws IOException { // TODO(developer): Replace these variables before running the sample. // Your GCP project ID. - String projectId = "migrationsource-392805"; + String projectId = "your-project-id"; // Location of the secret. - String locationId = "us-east1"; - // Resource ID of the Cloud SQL DB credentials secret to reconfigure. - String secretId = "cloudsql-autorotation-test"; - // Interval between rotations, in seconds. The service requires at least 3600 (1 hour). + String locationId = "your-location-id"; + // Resource ID of the secret. + String secretId = "your-secret-id"; + // Interval between rotations, in seconds. long rotationPeriodSeconds = 86400; // 24 hours updateRegionalSecretWithManagedRotationSchedule( projectId, locationId, secretId, rotationPeriodSeconds); } - // Reconfigure the recurring rotation schedule on a secret that already has Cloud SQL managed - // rotation enabled (see enableRegionalSecretManagedRotation). This only applies to regional - // secrets of the CLOUD_SQL_DB_CREDENTIALS type -- calling it on any other secret type, or - // before managed rotation has been enabled, fails. - // - // rotationPeriodSeconds is the interval between rotations. The service requires it to be at - // least 3600s (1 hour), and the derived next rotation time (now + rotationPeriodSeconds) must - // be at least 300s (5 minutes) in the future -- both are enforced by the API, not checked - // client-side here. + // Updates the rotation schedule of a CLOUD_SQL_DB_CREDENTIALS typed secret. public static Secret updateRegionalSecretWithManagedRotationSchedule( String projectId, String locationId, String secretId, long rotationPeriodSeconds) throws IOException { @@ -74,6 +66,10 @@ public static Secret updateRegionalSecretWithManagedRotationSchedule( SecretName secretName = SecretName.ofProjectLocationSecretName(projectId, locationId, secretId); + // The rotation schedule of a CLOUD_SQL_DB_CREDENTIALS secret can be set before or after + // enabling managed rotation; EnableManagedRotation does not need to be called first. Other + // secret types also support a rotation schedule, but only when Pub/Sub topics are configured. + // Pub/Sub topics are not required for CLOUD_SQL_DB_CREDENTIALS. // next_rotation_time and rotation_period must be set together. Instant nextRotationInstant = Instant.now().plusSeconds(rotationPeriodSeconds); Timestamp nextRotationTime = Timestamps.fromMillis(nextRotationInstant.toEpochMilli()); @@ -90,10 +86,7 @@ public static Secret updateRegionalSecretWithManagedRotationSchedule( .build()) .build(); - // Mask only the two subfields being set here, not the whole "rotation" submessage -- - // that would also include managed_rotation_status, which is output-only and rejects a - // whole-submessage replace with "immutable and cannot be updated" (confirmed empirically - // against a live project). + // Mask only the rotation subfields being set, not the whole "rotation" submessage. FieldMask fieldMask = FieldMaskUtil.fromString("rotation.next_rotation_time,rotation.rotation_period"); diff --git a/secretmanager/src/test/java/secretmanager/regionalsamples/SnippetsIT.java b/secretmanager/src/test/java/secretmanager/regionalsamples/SnippetsIT.java index 68f4d5cd461..86bca72da7a 100644 --- a/secretmanager/src/test/java/secretmanager/regionalsamples/SnippetsIT.java +++ b/secretmanager/src/test/java/secretmanager/regionalsamples/SnippetsIT.java @@ -99,10 +99,7 @@ public class SnippetsIT { private static final String UPDATED_ANNOTATION_KEY = "updatedannotationkey"; private static final String UPDATED_ANNOTATION_VALUE = "updatedannotationvalue"; - // Role granted to a Cloud SQL DB credentials secret's built-in identity so that managed - // rotation can update the Cloud SQL user's password. This grant is per-secret (the member is - // the secret's own generated principal), so it has to be made fresh for every secret these - // tests create. + // Role granted to the secret's identity to enable managed rotation. private static final String CLOUD_SQL_ROLE = "roles/cloudsql.admin"; private static final String CLOUD_SQL_INSTANCE_ID = System.getenv("CLOUD_SQL_INSTANCE"); private static final String CLOUD_SQL_USERNAME = System.getenv("CLOUD_SQL_USER"); @@ -246,10 +243,7 @@ private static String randomSecretId() { return "test-drz-" + random.nextLong(); } - // Creates a Cloud SQL DB credentials secret and grants CLOUD_SQL_ROLE to its own built-in - // identity, since enableManagedRotation needs this secret's own principal granted Cloud SQL - // IAM permissions first -- there's no broader grant that covers a secret before it exists. - // Returns the granted member, so the caller can revoke it again in teardown. + // Creates a Cloud SQL DB credentials secret and grants CLOUD_SQL_ROLE to its identity. private static String createRegionalSecretWithCloudSqlCredentialsAndGrant(String secretId) throws IOException, InterruptedException { Secret secret = @@ -258,17 +252,13 @@ private static String createRegionalSecretWithCloudSqlCredentialsAndGrant(String String member = secret.getPolicyMember().getIamPolicyUidPrincipal(); grantCloudSqlRole(member); - // IAM grants are eventually consistent; give it a moment before a caller tries to use it - // for managed rotation. + // Wait for the IAM grant to propagate. Thread.sleep(10000); return member; } - // Grants CLOUD_SQL_ROLE to member on the project. SetIamPolicy replaces the whole policy, so - // this reads the current policy, adds the member to the existing (or a new) binding for the - // role, and writes it back -- retrying the whole read-modify-write if another writer raced us - // (Aborted, from an etag mismatch). + // Grants CLOUD_SQL_ROLE to the member on the project. private static void grantCloudSqlRole(String member) throws IOException { String resource = String.format("projects/%s", PROJECT_ID); @@ -312,7 +302,7 @@ private static void grantCloudSqlRole(String member) throws IOException { } } - // Removes member from CLOUD_SQL_ROLE on the project, added by grantCloudSqlRole. + // Removes the member from CLOUD_SQL_ROLE on the project. private static void revokeCloudSqlRole(String member) throws IOException { String resource = String.format("projects/%s", PROJECT_ID);