Skip to content

feat(secretmanager): add Cloud SQL managed rotation and secret type samples - #3347

Open
suvidha-malaviya wants to merge 4 commits into
GoogleCloudPlatform:mainfrom
suvidha-malaviya:cloudsql-managed-rotation
Open

suvidha-malaviya wants to merge 4 commits into
GoogleCloudPlatform:mainfrom
suvidha-malaviya:cloudsql-managed-rotation

Conversation

@suvidha-malaviya

@suvidha-malaviya suvidha-malaviya commented Sep 30, 2026 •

Copy link
Copy Markdown

Adds C# samples and integration tests for Secret Manager Cloud SQL managed rotation and secret types.

Regional samples

  • CreateRegionalSecretWithCloudSqlCredentials: creates a secret of type CLOUD_SQL_DB_CREDENTIALS.
  • EnableRegionalSecretManagedRotation: links the secret to a Cloud SQL instance and user, and creates version 1.
  • RotateRegionalSecret: triggers a rotation on demand.
  • UpdateRegionalSecretWithManagedRotationSchedule: sets the recurring schedule. The update mask names rotation.next_rotation_time and rotation.rotation_period rather than the whole rotation message.
  • GetRegionalSecretType

Global samples

  • CreateSecretWithType: takes the secret type as a parameter.
  • GetSecretType

Other changes

  • Bumps Google.Cloud.SecretManager.V1 from 2.6.0 to 2.8.0, the first version with managed rotation support.
  • RegionalSecretManagerFixture now grants and revokes roles/cloudsql.admin for each test secret's own identity, because the grant is per-secret.

…amples

Add C# samples and tests for creating a Cloud SQL credentials secret, enabling, rotating and scheduling managed rotation, and creating/getting secrets by secret type. Bump Google.Cloud.SecretManager.V1 to 2.8.0.
@product-auto-label product-auto-label Bot added api: secretmanager Issues related to the Secret Manager API. samples Issues that are directly related to samples. labels Sep 30, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces new C# sample codes and integration tests for Google Cloud Secret Manager, specifically covering regional secrets, secret types, and managed rotation for Cloud SQL database credentials. The review feedback highlights several critical issues in the test suite and test fixture where created secrets and IAM role bindings could be leaked in the GCP project if assertions fail or exceptions are thrown during setup. It is highly recommended to wrap these operations in try-finally or try-catch blocks to guarantee robust resource cleanup.

Comment on lines +33 to +49
public void CreatesRegionalSecretWithCloudSqlCredentials()
{
SecretName secretName = SecretName.FromProjectLocationSecret(_fixture.ProjectId, _fixture.LocationId, _fixture.RandomId());

// Run the sample code.
Secret result = _sample.CreateRegionalSecretWithCloudSqlCredentials(
projectId: secretName.ProjectId, locationId: secretName.LocationId, secretId: secretName.SecretId);

// Assert that the secret was created with the Cloud SQL secret type.
Assert.Equal(secretName.SecretId, result.SecretName.SecretId);
Assert.Equal(Secret.Types.SecretType.CloudSqlDbCredentials, result.SecretType);
Assert.False(string.IsNullOrEmpty(result.PolicyMember.IamPolicyUidPrincipal));

// Clean the created secret.
_fixture.DeleteSecret(secretName);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If any of the assertions fail, the _fixture.DeleteSecret(secretName) call will be skipped, causing the created secret to be leaked in the GCP project. Wrapping the sample execution and assertions in a try-finally block ensures that the secret is always cleaned up. Additionally, asserting that result.PolicyMember is not null before accessing its properties prevents potential NullReferenceExceptions.

    [Fact]
    public void CreatesRegionalSecretWithCloudSqlCredentials()
    {
        SecretName secretName = SecretName.FromProjectLocationSecret(_fixture.ProjectId, _fixture.LocationId, _fixture.RandomId());
        try
        {
            // Run the sample code.
            Secret result = _sample.CreateRegionalSecretWithCloudSqlCredentials(
              projectId: secretName.ProjectId, locationId: secretName.LocationId, secretId: secretName.SecretId);

            // Assert that the secret was created with the Cloud SQL secret type.
            Assert.Equal(secretName.SecretId, result.SecretName.SecretId);
            Assert.Equal(Secret.Types.SecretType.CloudSqlDbCredentials, result.SecretType);
            Assert.NotNull(result.PolicyMember);
            Assert.False(string.IsNullOrEmpty(result.PolicyMember.IamPolicyUidPrincipal));
        }
        finally
        {
            // Clean the created secret.
            _fixture.DeleteSecret(secretName);
        }
    }

Comment on lines +37 to +51
public void CreatesSecretWithType(Secret.Types.SecretType secretType)
{
SecretName secretName = new SecretName(_fixture.ProjectId, _fixture.RandomId());

// Run the sample code.
Secret result = _sample.CreateSecretWithType(
projectId: secretName.ProjectId, secretId: secretName.SecretId, secretType: secretType);

// Assert that the secret was created with the requested type.
Assert.Equal(secretName.SecretId, result.SecretName.SecretId);
Assert.Equal(secretType, result.SecretType);

// Clean the created secret.
_fixture.DeleteSecret(secretName);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If any of the assertions fail, the _fixture.DeleteSecret(secretName) call will be skipped, causing the created secret to be leaked in the GCP project. Wrapping the sample execution and assertions in a try-finally block ensures that the secret is always cleaned up.

    public void CreatesSecretWithType(Secret.Types.SecretType secretType)
    {
        SecretName secretName = new SecretName(_fixture.ProjectId, _fixture.RandomId());
        try
        {
            // Run the sample code.
            Secret result = _sample.CreateSecretWithType(
              projectId: secretName.ProjectId, secretId: secretName.SecretId, secretType: secretType);

            // Assert that the secret was created with the requested type.
            Assert.Equal(secretName.SecretId, result.SecretName.SecretId);
            Assert.Equal(secretType, result.SecretType);
        }
        finally
        {
            // Clean the created secret.
            _fixture.DeleteSecret(secretName);
        }
    }

Comment on lines +33 to +57
[Fact]
public void EnablesRegionalSecretManagedRotation()
{
Secret secret = _fixture.CreateCloudSqlCredentialsSecret(_fixture.RandomId());
string member = secret.PolicyMember.IamPolicyUidPrincipal;
try
{
// Run the sample code.
SecretVersion version = _sample.EnableRegionalSecretManagedRotation(
projectId: secret.SecretName.ProjectId,
locationId: secret.SecretName.LocationId,
secretId: secret.SecretName.SecretId,
instanceId: _fixture.CloudSqlInstanceId,
username: _fixture.CloudSqlUsername);

// Assert that the first version was created and is enabled.
Assert.Contains(secret.SecretName.SecretId, version.Name);
Assert.Equal(SecretVersion.Types.State.Enabled, version.State);
}
finally
{
_fixture.DeleteSecret(secret.SecretName);
_fixture.RevokeCloudSqlRole(member);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If _fixture.CreateCloudSqlCredentialsSecret succeeds but an exception is thrown before entering the try block (e.g., if secret.PolicyMember is null, causing a NullReferenceException on line 37), the created secret and its IAM role binding will be leaked. Declaring the variables as null and performing the setup inside the try block ensures that cleanup is always executed in the finally block.

    [Fact]
    public void EnablesRegionalSecretManagedRotation()
    {
        Secret secret = null;
        string member = null;
        try
        {
            secret = _fixture.CreateCloudSqlCredentialsSecret(_fixture.RandomId());
            member = secret.PolicyMember.IamPolicyUidPrincipal;

            // Run the sample code.
            SecretVersion version = _sample.EnableRegionalSecretManagedRotation(
              projectId: secret.SecretName.ProjectId,
              locationId: secret.SecretName.LocationId,
              secretId: secret.SecretName.SecretId,
              instanceId: _fixture.CloudSqlInstanceId,
              username: _fixture.CloudSqlUsername);

            // Assert that the first version was created and is enabled.
            Assert.Contains(secret.SecretName.SecretId, version.Name);
            Assert.Equal(SecretVersion.Types.State.Enabled, version.State);
        }
        finally
        {
            if (secret != null)
            {
                _fixture.DeleteSecret(secret.SecretName);
            }
            if (member != null)
            {
                _fixture.RevokeCloudSqlRole(member);
            }
        }
    }

Comment on lines +34 to +61
[Fact]
public void RotatesRegionalSecret()
{
Secret secret = _fixture.CreateCloudSqlCredentialsSecret(_fixture.RandomId());
string member = secret.PolicyMember.IamPolicyUidPrincipal;
try
{
SecretVersion firstVersion = _enableSample.EnableRegionalSecretManagedRotation(
secret.SecretName.ProjectId, secret.SecretName.LocationId, secret.SecretName.SecretId,
_fixture.CloudSqlInstanceId, _fixture.CloudSqlUsername);

// Run the sample code.
SecretVersion rotatedVersion = _sample.RotateRegionalSecret(
projectId: secret.SecretName.ProjectId,
locationId: secret.SecretName.LocationId,
secretId: secret.SecretName.SecretId);

// Assert that rotation created a new, enabled version.
Assert.Contains(secret.SecretName.SecretId, rotatedVersion.Name);
Assert.NotEqual(firstVersion.Name, rotatedVersion.Name);
Assert.Equal(SecretVersion.Types.State.Enabled, rotatedVersion.State);
}
finally
{
_fixture.DeleteSecret(secret.SecretName);
_fixture.RevokeCloudSqlRole(member);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If _fixture.CreateCloudSqlCredentialsSecret succeeds but an exception is thrown before entering the try block (e.g., if secret.PolicyMember is null, causing a NullReferenceException on line 38), the created secret and its IAM role binding will be leaked. Declaring the variables as null and performing the setup inside the try block ensures that cleanup is always executed in the finally block.

    [Fact]
    public void RotatesRegionalSecret()
    {
        Secret secret = null;
        string member = null;
        try
        {
            secret = _fixture.CreateCloudSqlCredentialsSecret(_fixture.RandomId());
            member = secret.PolicyMember.IamPolicyUidPrincipal;

            SecretVersion firstVersion = _enableSample.EnableRegionalSecretManagedRotation(
              secret.SecretName.ProjectId, secret.SecretName.LocationId, secret.SecretName.SecretId,
              _fixture.CloudSqlInstanceId, _fixture.CloudSqlUsername);

            // Run the sample code.
            SecretVersion rotatedVersion = _sample.RotateRegionalSecret(
              projectId: secret.SecretName.ProjectId,
              locationId: secret.SecretName.LocationId,
              secretId: secret.SecretName.SecretId);

            // Assert that rotation created a new, enabled version.
            Assert.Contains(secret.SecretName.SecretId, rotatedVersion.Name);
            Assert.NotEqual(firstVersion.Name, rotatedVersion.Name);
            Assert.Equal(SecretVersion.Types.State.Enabled, rotatedVersion.State);
        }
        finally
        {
            if (secret != null)
            {
                _fixture.DeleteSecret(secret.SecretName);
            }
            if (member != null)
            {
                _fixture.RevokeCloudSqlRole(member);
            }
        }
    }

Comment on lines +36 to +67
[Fact]
public void UpdatesRegionalSecretWithManagedRotationSchedule()
{
Secret secret = _fixture.CreateCloudSqlCredentialsSecret(_fixture.RandomId());
string member = secret.PolicyMember.IamPolicyUidPrincipal;
try
{
_enableSample.EnableRegionalSecretManagedRotation(
secret.SecretName.ProjectId, secret.SecretName.LocationId, secret.SecretName.SecretId,
_fixture.CloudSqlInstanceId, _fixture.CloudSqlUsername);

long rotationPeriodSeconds = 3600;
DateTimeOffset before = DateTimeOffset.UtcNow;

// Run the sample code.
Secret updated = _sample.UpdateRegionalSecretWithManagedRotationSchedule(
projectId: secret.SecretName.ProjectId,
locationId: secret.SecretName.LocationId,
secretId: secret.SecretName.SecretId,
rotationPeriodSeconds: rotationPeriodSeconds);

// Assert that the schedule was applied.
Assert.Equal(rotationPeriodSeconds, updated.Rotation.RotationPeriod.Seconds);
Assert.True(
updated.Rotation.NextRotationTime.ToDateTimeOffset() >= before.AddSeconds(rotationPeriodSeconds).AddSeconds(-1));
}
finally
{
_fixture.DeleteSecret(secret.SecretName);
_fixture.RevokeCloudSqlRole(member);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If _fixture.CreateCloudSqlCredentialsSecret succeeds but an exception is thrown before entering the try block (e.g., if secret.PolicyMember is null, causing a NullReferenceException on line 40), the created secret and its IAM role binding will be leaked. Declaring the variables as null and performing the setup inside the try block ensures that cleanup is always executed in the finally block.

    [Fact]
    public void UpdatesRegionalSecretWithManagedRotationSchedule()
    {
        Secret secret = null;
        string member = null;
        try
        {
            secret = _fixture.CreateCloudSqlCredentialsSecret(_fixture.RandomId());
            member = secret.PolicyMember.IamPolicyUidPrincipal;

            _enableSample.EnableRegionalSecretManagedRotation(
              secret.SecretName.ProjectId, secret.SecretName.LocationId, secret.SecretName.SecretId,
              _fixture.CloudSqlInstanceId, _fixture.CloudSqlUsername);

            long rotationPeriodSeconds = 3600;
            DateTimeOffset before = DateTimeOffset.UtcNow;

            // Run the sample code.
            Secret updated = _sample.UpdateRegionalSecretWithManagedRotationSchedule(
              projectId: secret.SecretName.ProjectId,
              locationId: secret.SecretName.LocationId,
              secretId: secret.SecretName.SecretId,
              rotationPeriodSeconds: rotationPeriodSeconds);

            // Assert that the schedule was applied.
            Assert.Equal(rotationPeriodSeconds, updated.Rotation.RotationPeriod.Seconds);
            Assert.True(
              updated.Rotation.NextRotationTime.ToDateTimeOffset() >= before.AddSeconds(rotationPeriodSeconds).AddSeconds(-1));
        }
        finally
        {
            if (secret != null)
            {
                _fixture.DeleteSecret(secret.SecretName);
            }
            if (member != null)
            {
                _fixture.RevokeCloudSqlRole(member);
            }
        }
    }

Comment on lines +139 to +153
public Secret CreateCloudSqlCredentialsSecret(string secretId)
{
LocationName locationName = new LocationName(ProjectId, LocationId);
Secret secret = Client.CreateSecret(locationName, secretId, new Secret
{
SecretType = Secret.Types.SecretType.CloudSqlDbCredentials,
});

GrantCloudSqlRole(secret.PolicyMember.IamPolicyUidPrincipal);

// IAM grants are eventually consistent; give it a moment before a caller
// tries to use it for managed rotation.
Thread.Sleep(TimeSpan.FromSeconds(10));
return secret;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If GrantCloudSqlRole throws an exception (or if secret.PolicyMember is null), the created secret will be leaked because the caller's cleanup code is not yet active. Wrapping the IAM grant in a try-catch block that deletes the secret on failure ensures robust cleanup. Additionally, a defensive null check on secret.PolicyMember prevents a generic NullReferenceException and provides a clearer error message.

    public Secret CreateCloudSqlCredentialsSecret(string secretId)
    {
        LocationName locationName = new LocationName(ProjectId, LocationId);
        Secret secret = Client.CreateSecret(locationName, secretId, new Secret
        {
            SecretType = Secret.Types.SecretType.CloudSqlDbCredentials,
        });

        try
        {
            if (secret.PolicyMember?.IamPolicyUidPrincipal == null)
            {
                throw new InvalidOperationException("The created regional secret does not have a valid PolicyMember or IamPolicyUidPrincipal.");
            }
            GrantCloudSqlRole(secret.PolicyMember.IamPolicyUidPrincipal);
        }
        catch
        {
            DeleteSecret(secret.SecretName);
            throw;
        }

        // IAM grants are eventually consistent; give it a moment before a caller
        // tries to use it for managed rotation.
        Thread.Sleep(TimeSpan.FromSeconds(10));
        return secret;
    }

@suvidha-malaviya
suvidha-malaviya marked this pull request as ready for review September 30, 2026 06:58
@suvidha-malaviya
suvidha-malaviya requested review from a team as code owners September 30, 2026 06:58
@snippet-bot

snippet-bot Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Here is the summary of changes.

You are about to add 7 region tags.

This comment is generated by snippet-bot.
If you find problems with this result, please file an issue at:
https://github.com/googleapis/repo-automation-bots/issues.
To update this comment, add snippet-bot:force-run label or use the checkbox below:

  • Refresh this comment

…ation samples

Align comments with other samples, simplify summaries and clarify rotation schedule behavior.

@YashSaraf11 YashSaraf11 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the Cloud SQL Managed Rotation and Secret Type .NET samples. Left inline comments categorized into [Must], [Warning], and [Nit].

Comment on lines +59 to +63
_fixture.DeleteSecret(secret.SecretName);
}
if (member != null)
{
_fixture.RevokeCloudSqlRole(member);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Must] In the finally block, call _fixture.RevokeCloudSqlRole(member) before _fixture.DeleteSecret(secret.SecretName) (or wrap each in its own try/finally) so that the project-level roles/cloudsql.admin binding is still revoked even if DeleteSecret throws.

Comment on lines +63 to +67
_fixture.DeleteSecret(secret.SecretName);
}
if (member != null)
{
_fixture.RevokeCloudSqlRole(member);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Must] Call _fixture.RevokeCloudSqlRole(member) before _fixture.DeleteSecret(secret.SecretName) in finally so the project-level IAM binding is still cleaned up if secret deletion throws.

Comment on lines +69 to +73
_fixture.DeleteSecret(secret.SecretName);
}
if (member != null)
{
_fixture.RevokeCloudSqlRole(member);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Must] Call _fixture.RevokeCloudSqlRole(member) before _fixture.DeleteSecret(secret.SecretName) in finally so the project-level IAM binding is still cleaned up if secret deletion throws.

Comment on lines +63 to +66
FieldMask fieldMask = new FieldMask
{
Paths = { "rotation.next_rotation_time", "rotation.rotation_period" }
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Warning] For consistency with UpdateRegionalSecret.cs and UpdateSecret.cs, consider using FieldMask.FromString("rotation.next_rotation_time", "rotation.rotation_period").

public Secret CreateSecretWithType(
string projectId = "my-project",
string secretId = "my-secret",
Secret.Types.SecretType secretType = Secret.Types.SecretType.Other

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Nit] Consider using Secret.Types.SecretType.AccessKey as the default parameter value to match the other language samples.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: secretmanager Issues related to the Secret Manager API. samples Issues that are directly related to samples.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants