Repository navigation
feat(secretmanager): add Cloud SQL managed rotation and secret type samples - #3347
suvidha-malaviya wants to merge 4 commits into
Conversation
…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.
There was a problem hiding this comment.
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.
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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);
}
}| 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); | ||
| } |
There was a problem hiding this comment.
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);
}
}| [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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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);
}
}
}| [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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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);
}
}
}| [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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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);
}
}
}| 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; | ||
| } |
There was a problem hiding this comment.
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;
}|
Here is the summary of changes. You are about to add 7 region tags.
This comment is generated by snippet-bot.
|
…ation samples Align comments with other samples, simplify summaries and clarify rotation schedule behavior.
YashSaraf11
left a comment
There was a problem hiding this comment.
Reviewed the Cloud SQL Managed Rotation and Secret Type .NET samples. Left inline comments categorized into [Must], [Warning], and [Nit].
| _fixture.DeleteSecret(secret.SecretName); | ||
| } | ||
| if (member != null) | ||
| { | ||
| _fixture.RevokeCloudSqlRole(member); |
There was a problem hiding this comment.
[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.
| _fixture.DeleteSecret(secret.SecretName); | ||
| } | ||
| if (member != null) | ||
| { | ||
| _fixture.RevokeCloudSqlRole(member); |
There was a problem hiding this comment.
[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.
| _fixture.DeleteSecret(secret.SecretName); | ||
| } | ||
| if (member != null) | ||
| { | ||
| _fixture.RevokeCloudSqlRole(member); |
There was a problem hiding this comment.
[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.
| FieldMask fieldMask = new FieldMask | ||
| { | ||
| Paths = { "rotation.next_rotation_time", "rotation.rotation_period" } | ||
| }; |
There was a problem hiding this comment.
[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 |
There was a problem hiding this comment.
[Nit] Consider using Secret.Types.SecretType.AccessKey as the default parameter value to match the other language samples.
Adds C# samples and integration tests for Secret Manager Cloud SQL managed rotation and secret types.
Regional samples
CreateRegionalSecretWithCloudSqlCredentials: creates a secret of typeCLOUD_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 namesrotation.next_rotation_timeandrotation.rotation_periodrather than the wholerotationmessage.GetRegionalSecretTypeGlobal samples
CreateSecretWithType: takes the secret type as a parameter.GetSecretTypeOther changes
Google.Cloud.SecretManager.V1from 2.6.0 to 2.8.0, the first version with managed rotation support.RegionalSecretManagerFixturenow grants and revokesroles/cloudsql.adminfor each test secret's own identity, because the grant is per-secret.