Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(generator): generate a SetIamPolicy() overload with an OCC loop #7276

Merged
merged 2 commits into from
Sep 7, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions generator/integration_tests/golden/golden_thing_admin_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

#include "generator/integration_tests/golden/golden_thing_admin_client.h"
#include <memory>
#include "generator/integration_tests/golden/golden_thing_admin_options.h"
#include "generator/integration_tests/golden/internal/golden_thing_admin_option_defaults.h"
#include <thread>

namespace google {
namespace cloud {
Expand Down Expand Up @@ -79,6 +82,28 @@ GoldenThingAdminClient::SetIamPolicy(std::string const& resource, google::iam::v
return connection_->SetIamPolicy(request);
}

StatusOr<google::iam::v1::Policy>
GoldenThingAdminClient::SetIamPolicy(std::string const& resource, IamUpdater const& updater, Options options) {
internal::CheckExpectedOptions<GoldenThingAdminBackoffPolicyOption>(options, __func__);
options = golden_internal::GoldenThingAdminDefaultOptions(std::move(options));
auto backoff_policy = options.get<GoldenThingAdminBackoffPolicyOption>()->clone();
for (;;) {
auto recent = GetIamPolicy(resource);
if (!recent) {
return recent.status();
}
auto policy = updater(*std::move(recent));
if (!policy) {
return Status(StatusCode::kCancelled, "updater did not yield a policy");
}
auto result = SetIamPolicy(resource, *std::move(policy));
if (result || result.status().code() != StatusCode::kAborted) {
return result;
}
std::this_thread::sleep_for(backoff_policy->OnCompletion());
}
}

StatusOr<google::iam::v1::Policy>
GoldenThingAdminClient::GetIamPolicy(std::string const& resource) {
google::iam::v1::GetIamPolicyRequest request;
Expand Down
16 changes: 16 additions & 0 deletions generator/integration_tests/golden/golden_thing_admin_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
#include "google/cloud/polling_policy.h"
#include "google/cloud/status_or.h"
#include "google/cloud/version.h"
#include "google/cloud/iam_updater.h"
#include "google/cloud/options.h"
#include <google/longrunning/operations.grpc.pb.h>
#include <memory>

Expand Down Expand Up @@ -161,6 +163,20 @@ class GoldenThingAdminClient {
StatusOr<google::iam::v1::Policy>
SetIamPolicy(std::string const& resource, google::iam::v1::Policy const& policy);

/**
* Updates the IAM policy for @p resource using an optimistic concurrency control loop.
*
* The loop fetches the current policy for @p resource, and passes it to @p updater, which should return the new policy. This new policy should use the current etag so that the read-modify-write cycle can detect races and rerun the update when there is a mismatch. If the new policy does not have an etag, the existing policy will be blindly overwritten. If @p updater does not yield a policy, the control loop is terminated and kCancelled is returned.
*
* @param resource Required. The resource for which the policy is being specified. See the operation documentation for the appropriate value for this field.
* @param updater Required. Functor to map the current policy to a new one.
* @param options Optional. Options to control the loop. Expected options are:
* - `GoldenThingAdminBackoffPolicyOption`
* @return google::iam::v1::Policy
*/
StatusOr<google::iam::v1::Policy>
SetIamPolicy(std::string const& resource, IamUpdater const& updater, Options options = {});

/**
* Gets the access control policy for a database or backup resource.
* Returns an empty policy if a database or backup exists but does not have a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "google/cloud/internal/time_utils.h"
#include "google/cloud/testing_util/is_proto_equal.h"
#include "google/cloud/testing_util/status_matchers.h"
#include "generator/integration_tests/golden/golden_thing_admin_options.h"
#include "generator/integration_tests/golden/mocks/mock_golden_thing_admin_connection.h"
#include <google/iam/v1/policy.pb.h>
#include <google/protobuf/util/field_mask_util.h>
Expand All @@ -29,10 +30,12 @@ namespace golden {
inline namespace GOOGLE_CLOUD_CPP_GENERATED_NS {
namespace {

using ::google::cloud::testing_util::IsOk;
using ::google::cloud::testing_util::IsProtoEqual;
using ::google::cloud::testing_util::StatusIs;
using ::testing::ElementsAre;
using ::testing::HasSubstr;
using ::testing::Not;

TEST(GoldenThingAdminClientTest, CopyMoveEquality) {
auto conn1 = std::make_shared<golden_mocks::MockGoldenThingAdminConnection>();
Expand Down Expand Up @@ -269,6 +272,167 @@ TEST(GoldenThingAdminClientTest, SetIamPolicy) {
EXPECT_STATUS_OK(response);
}

TEST(GoldenThingAdminClientTest, SetIamPolicyUpdater) {
auto mock = std::make_shared<golden_mocks::MockGoldenThingAdminConnection>();
std::string expected_database =
"/projects/test-project/instances/test-instance/databases/test-db";
std::string etag_old = "\007\005\313\113\361\351\232\005";
std::string etag_new = "\007\005\313\113\366\143\244\343";
EXPECT_CALL(*mock, GetIamPolicy)
.WillOnce([expected_database,
etag_old](::google::iam::v1::GetIamPolicyRequest const& r) {
EXPECT_EQ(expected_database, r.resource());
google::iam::v1::Policy response;
response.set_etag(etag_old);
return response;
});
EXPECT_CALL(*mock, SetIamPolicy)
.WillOnce([expected_database, etag_old,
etag_new](::google::iam::v1::SetIamPolicyRequest const& r) {
EXPECT_EQ(expected_database, r.resource());
EXPECT_EQ(etag_old, r.policy().etag());
google::iam::v1::Policy response = r.policy();
response.set_etag(etag_new);
return response;
});
GoldenThingAdminClient client(std::move(mock));
auto response = client.SetIamPolicy(
expected_database, [etag_old](::google::iam::v1::Policy policy) {
EXPECT_EQ(etag_old, policy.etag());
return policy;
});
ASSERT_THAT(response, IsOk());
EXPECT_EQ(response->etag(), etag_new);
}

TEST(GoldenThingAdminClientTest, SetIamPolicyUpdaterGetFailure) {
auto mock = std::make_shared<golden_mocks::MockGoldenThingAdminConnection>();
std::string expected_database =
"/projects/test-project/instances/test-instance/databases/test-db";
EXPECT_CALL(*mock, GetIamPolicy)
.WillOnce(
[expected_database](::google::iam::v1::GetIamPolicyRequest const& r) {
EXPECT_EQ(expected_database, r.resource());
return Status(StatusCode::kPermissionDenied, "uh-oh");
});
GoldenThingAdminClient client(std::move(mock));
auto response = client.SetIamPolicy(expected_database,
[](::google::iam::v1::Policy policy) {
EXPECT_TRUE(false);
return policy;
});
ASSERT_THAT(response, Not(IsOk()));
EXPECT_THAT(response,
StatusIs(StatusCode::kPermissionDenied, HasSubstr("uh-oh")));
}

TEST(GoldenThingAdminClientTest, SetIamPolicyUpdaterCancelled) {
auto mock = std::make_shared<golden_mocks::MockGoldenThingAdminConnection>();
std::string expected_database =
"/projects/test-project/instances/test-instance/databases/test-db";
std::string etag_old = "\007\005\313\113\374\306\126\007";
EXPECT_CALL(*mock, GetIamPolicy)
.WillOnce([expected_database,
etag_old](::google::iam::v1::GetIamPolicyRequest const& r) {
EXPECT_EQ(expected_database, r.resource());
google::iam::v1::Policy response;
response.set_etag(etag_old);
return response;
});
GoldenThingAdminClient client(std::move(mock));
auto response = client.SetIamPolicy(
expected_database, [etag_old](::google::iam::v1::Policy const& policy) {
EXPECT_EQ(etag_old, policy.etag());
return absl::nullopt;
});
ASSERT_THAT(response, Not(IsOk()));
EXPECT_THAT(response, StatusIs(StatusCode::kCancelled));
}

TEST(GoldenThingAdminClientTest, SetIamPolicyUpdaterSetFailure) {
auto mock = std::make_shared<golden_mocks::MockGoldenThingAdminConnection>();
std::string expected_database =
"/projects/test-project/instances/test-instance/databases/test-db";
std::string etag_old = "\007\005\313\113\377\272\224\367";
EXPECT_CALL(*mock, GetIamPolicy)
.WillOnce([expected_database,
etag_old](::google::iam::v1::GetIamPolicyRequest const& r) {
EXPECT_EQ(expected_database, r.resource());
google::iam::v1::Policy response;
response.set_etag(etag_old);
return response;
});
EXPECT_CALL(*mock, SetIamPolicy)
.WillOnce([expected_database,
etag_old](::google::iam::v1::SetIamPolicyRequest const& r) {
EXPECT_EQ(expected_database, r.resource());
EXPECT_EQ(etag_old, r.policy().etag());
return Status(StatusCode::kPermissionDenied, "uh-oh");
});
GoldenThingAdminClient client(std::move(mock));
auto response = client.SetIamPolicy(
expected_database, [etag_old](::google::iam::v1::Policy policy) {
EXPECT_EQ(etag_old, policy.etag());
return policy;
});
ASSERT_THAT(response, Not(IsOk()));
EXPECT_THAT(response,
StatusIs(StatusCode::kPermissionDenied, HasSubstr("uh-oh")));
}

TEST(GoldenThingAdminClientTest, SetIamPolicyUpdaterRerun) {
auto mock = std::make_shared<golden_mocks::MockGoldenThingAdminConnection>();
std::string expected_database =
"/projects/test-project/instances/test-instance/databases/test-db";
std::string etag_old = "\007\005\313\114\002\240\006\225";
std::string etag_new = "\007\005\313\114\005\046\113\243";
std::string etag_rerun = "\007\005\313\114\007\252\023\045";
EXPECT_CALL(*mock, GetIamPolicy)
.WillOnce([expected_database,
etag_old](::google::iam::v1::GetIamPolicyRequest const& r) {
EXPECT_EQ(expected_database, r.resource());
google::iam::v1::Policy response;
response.set_etag(etag_old);
return response;
})
.WillOnce([expected_database,
etag_new](::google::iam::v1::GetIamPolicyRequest const& r) {
EXPECT_EQ(expected_database, r.resource());
google::iam::v1::Policy response;
response.set_etag(etag_new);
return response;
});
EXPECT_CALL(*mock, SetIamPolicy)
.WillOnce([expected_database,
etag_old](::google::iam::v1::SetIamPolicyRequest const& r) {
EXPECT_EQ(expected_database, r.resource());
EXPECT_EQ(etag_old, r.policy().etag());
return Status(StatusCode::kAborted,
"There were concurrent policy changes."
" Please retry the whole read-modify-write with "
"exponential backoff.");
})
.WillOnce([expected_database, etag_new,
etag_rerun](::google::iam::v1::SetIamPolicyRequest const& r) {
EXPECT_EQ(expected_database, r.resource());
EXPECT_EQ(etag_new, r.policy().etag());
google::iam::v1::Policy response = r.policy();
response.set_etag(etag_rerun);
return response;
});
GoldenThingAdminClient client(std::move(mock));
auto options = Options{}.set<GoldenThingAdminBackoffPolicyOption>(
ExponentialBackoffPolicy(std::chrono::seconds::zero(),
std::chrono::seconds::zero(), 2)
.clone());
auto response = client.SetIamPolicy(
expected_database,
[](::google::iam::v1::Policy policy) { return policy; },
std::move(options));
ASSERT_THAT(response, IsOk());
EXPECT_EQ(response->etag(), etag_rerun);
}

TEST(GoldenThingAdminClientTest, GetIamPolicy) {
auto mock = std::make_shared<golden_mocks::MockGoldenThingAdminConnection>();
std::string expected_database =
Expand Down
Loading