From c9301d6eaeb9e809fcde0a972aa38686080716fa Mon Sep 17 00:00:00 2001 From: justinsb Date: Wed, 22 May 2024 16:15:46 -0400 Subject: [PATCH 1/2] Support setting IAM on PrivateCACAPool This uses our direct actuation framework. --- .../controllerbuilder/template/controller.go | 5 + docs/releasenotes/release-1.120.md | 2 + go.mod | 3 +- go.sum | 2 + .../direct/alloydb/cluster_controller.go | 4 + .../direct/apikeys/apikeyskey_controller.go | 4 + .../cloudbuild/workerpool_controller.go | 4 + .../directbase/directbase_controller.go | 3 +- .../direct/directbase/interfaces.go | 3 + pkg/controller/direct/export.go | 86 ++----- .../gkehub/featuremembership_controller.go | 4 + pkg/controller/direct/iam.go | 189 ++++++++++++++++ .../direct/logging/logmetric_controller.go | 29 +++ .../monitoringdashboard_controller.go | 30 +++ pkg/controller/direct/privateca/client.go | 70 ++++++ .../privateca/privatecapool_controller.go | 212 ++++++++++++++++++ pkg/controller/direct/privateca/utils.go | 64 ++++++ .../direct/references/projectref.go | 14 ++ pkg/controller/direct/register/register.go | 1 + pkg/controller/direct/registry/references.go | 98 ++++++++ pkg/controller/direct/registry/registry.go | 17 ++ .../resourcemanager/tagkey_controller.go | 4 + pkg/controller/iam/iamclient/iamclient.go | 33 ++- pkg/k8s/errors.go | 2 + pkg/webhook/iam_validator.go | 6 + tests/e2e/unified_test.go | 6 +- 26 files changed, 820 insertions(+), 75 deletions(-) create mode 100644 pkg/controller/direct/iam.go create mode 100644 pkg/controller/direct/privateca/client.go create mode 100644 pkg/controller/direct/privateca/privatecapool_controller.go create mode 100644 pkg/controller/direct/privateca/utils.go create mode 100644 pkg/controller/direct/registry/references.go diff --git a/dev/tools/controllerbuilder/template/controller.go b/dev/tools/controllerbuilder/template/controller.go index cbb4054017..94fb7e16cd 100644 --- a/dev/tools/controllerbuilder/template/controller.go +++ b/dev/tools/controllerbuilder/template/controller.go @@ -126,6 +126,11 @@ func (m *model) AdapterForObject(ctx context.Context, reader client.Reader, u *u }, nil } +func (m *model) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) { + // TODO: Support URLs + return nil, nil +} + type Adapter struct { resourceID string projectID string diff --git a/docs/releasenotes/release-1.120.md b/docs/releasenotes/release-1.120.md index 39c575ff12..2bda5f1566 100644 --- a/docs/releasenotes/release-1.120.md +++ b/docs/releasenotes/release-1.120.md @@ -4,6 +4,8 @@ * ... +* IAM configuration can now be applied to `PrivateCACAPool`, using our direct-actuation approach. + * Special shout-outs to ... for their contributions to this release. TODO: list contributors with `git log v1.120.0... | grep Merge | grep from | awk '{print $6}' | cut -d '/' -f 1 | sort | uniq` diff --git a/go.mod b/go.mod index b6ddc0e8eb..c656ecae7b 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,12 @@ require ( cloud.google.com/go/apikeys v1.1.7 cloud.google.com/go/cloudbuild v1.16.1 cloud.google.com/go/compute v1.27.0 + cloud.google.com/go/iam v1.1.8 cloud.google.com/go/monitoring v1.19.0 cloud.google.com/go/profiler v0.4.0 cloud.google.com/go/resourcemanager v1.9.7 cloud.google.com/go/securesourcemanager v0.1.5 + cloud.google.com/go/security v1.17.0 contrib.go.opencensus.io/exporter/prometheus v0.1.0 github.com/GoogleCloudPlatform/declarative-resource-client-library v1.62.0 github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp v0.0.0-20240614222432-4bde5b345380 @@ -77,7 +79,6 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect cloud.google.com/go/bigtable v1.25.0 // indirect cloud.google.com/go/compute/metadata v0.3.0 // indirect - cloud.google.com/go/iam v1.1.8 // indirect cloud.google.com/go/longrunning v0.5.7 // indirect dario.cat/mergo v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect diff --git a/go.sum b/go.sum index 5810256fd0..7cb24da921 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,8 @@ cloud.google.com/go/resourcemanager v1.9.7 h1:SdvD0PaPX60+yeKoSe16mawFpM0EPuiPPi cloud.google.com/go/resourcemanager v1.9.7/go.mod h1:cQH6lJwESufxEu6KepsoNAsjrUtYYNXRwxm4QFE5g8A= cloud.google.com/go/securesourcemanager v0.1.5 h1:+6x067eHPHyDU8ed+ybNEWudtngaz/bAoehhyy5bO5M= cloud.google.com/go/securesourcemanager v0.1.5/go.mod h1:RTBWXAILmlm91TsDBmKUzUevfjB1HSXo85nsF8JEWjc= +cloud.google.com/go/security v1.17.0 h1:u4RCnEQPvlrrnFRFinU0T3WsjtrsQErkWBfqTM5oUQI= +cloud.google.com/go/security v1.17.0/go.mod h1:eSuFs0SlBv1gWg7gHIoF0hYOvcSwJCek/GFXtgO6aA0= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= diff --git a/pkg/controller/direct/alloydb/cluster_controller.go b/pkg/controller/direct/alloydb/cluster_controller.go index bd3b06cccc..7af87444f2 100644 --- a/pkg/controller/direct/alloydb/cluster_controller.go +++ b/pkg/controller/direct/alloydb/cluster_controller.go @@ -110,6 +110,10 @@ func (m *clusterModel) AdapterForObject(ctx context.Context, reader client.Reade }, nil } +func (m *clusterModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) { + return nil, nil +} + // adapter implements the Adapter interface. var _ directbase.Adapter = &clusterAdapter{} diff --git a/pkg/controller/direct/apikeys/apikeyskey_controller.go b/pkg/controller/direct/apikeys/apikeyskey_controller.go index 5d39a7a422..c2993ee16f 100644 --- a/pkg/controller/direct/apikeys/apikeyskey_controller.go +++ b/pkg/controller/direct/apikeys/apikeyskey_controller.go @@ -147,6 +147,10 @@ func (m *model) AdapterForObject(ctx context.Context, reader client.Reader, u *u }, nil } +func (m *model) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) { + return nil, nil +} + // Find implements the Adapter interface. func (a *adapter) Find(ctx context.Context) (bool, error) { if a.keyID == "" { diff --git a/pkg/controller/direct/cloudbuild/workerpool_controller.go b/pkg/controller/direct/cloudbuild/workerpool_controller.go index e471c3de6f..60123b67e7 100644 --- a/pkg/controller/direct/cloudbuild/workerpool_controller.go +++ b/pkg/controller/direct/cloudbuild/workerpool_controller.go @@ -127,6 +127,10 @@ func (m *model) AdapterForObject(ctx context.Context, reader client.Reader, u *u }, nil } +func (m *model) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) { + return nil, nil +} + type Adapter struct { resourceID string projectID string diff --git a/pkg/controller/direct/directbase/directbase_controller.go b/pkg/controller/direct/directbase/directbase_controller.go index c9daa53447..cc85739fea 100644 --- a/pkg/controller/direct/directbase/directbase_controller.go +++ b/pkg/controller/direct/directbase/directbase_controller.go @@ -23,7 +23,6 @@ import ( "github.com/GoogleCloudPlatform/k8s-config-connector/operator/pkg/apis/core/v1beta1" "github.com/GoogleCloudPlatform/k8s-config-connector/operator/pkg/kccstate" - kcciamclient "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/iam/iamclient" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/jitter" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/lifecyclehandler" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/metrics" @@ -254,7 +253,7 @@ func (r *reconcileContext) doReconcile(ctx context.Context, u *unstructured.Unst } if !k8s.HasAbandonAnnotation(u) { if _, err := adapter.Delete(ctx); err != nil { - if !errors.Is(err, kcciamclient.ErrNotFound) && !k8s.IsReferenceNotFoundError(err) { + if !errors.Is(err, k8s.ErrIAMNotFound) && !k8s.IsReferenceNotFoundError(err) { if unwrappedErr, ok := lifecyclehandler.CausedByUnresolvableDeps(err); ok { logger.Info(unwrappedErr.Error(), "resource", k8s.GetNamespacedName(u)) resource, err := toK8sResource(u) diff --git a/pkg/controller/direct/directbase/interfaces.go b/pkg/controller/direct/directbase/interfaces.go index 38d7fb4d8f..7d070b2285 100644 --- a/pkg/controller/direct/directbase/interfaces.go +++ b/pkg/controller/direct/directbase/interfaces.go @@ -26,6 +26,9 @@ type Model interface { // AdapterForObject builds an operation object for reconciling the object u. // If there are references, AdapterForObject should dereference them before returning (using reader) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (Adapter, error) + + // AdapterForURL builds an operation object for exporting the object u. + AdapterForURL(ctx context.Context, url string) (Adapter, error) } // Adapter performs a single reconciliation on a single object. diff --git a/pkg/controller/direct/export.go b/pkg/controller/direct/export.go index 6139c8438c..c17e43e826 100644 --- a/pkg/controller/direct/export.go +++ b/pkg/controller/direct/export.go @@ -17,88 +17,36 @@ package direct import ( "context" "fmt" - "strings" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/client" ) // Export attempts to export the resource specified by url. // The url format should match the Cloud-Asset-Inventory format: https://cloud.google.com/asset-inventory/docs/resource-name-format // If url is not recognized or not implemented by a direct controller, this returns (nil, nil) func Export(ctx context.Context, url string, config *config.ControllerConfig) (*unstructured.Unstructured, error) { - if strings.HasPrefix(url, "//logging.googleapis.com/") { - tokens := strings.Split(strings.TrimPrefix(url, "//logging.googleapis.com/"), "/") - if len(tokens) == 4 && tokens[0] == "projects" && tokens[2] == "metrics" { - model, err := registry.GetModel(schema.GroupKind{Group: "logging.cnrm.cloud.google.com", Kind: "LoggingLogMetric"}) - if err != nil { - return nil, err - } - in := &unstructured.Unstructured{} - in.SetName(tokens[3]) - if err := unstructured.SetNestedField(in.Object, tokens[1], "spec", "projectRef", "external"); err != nil { - return nil, err - } - - var reader client.Reader // TODO: Create erroring reader? - a, err := model.AdapterForObject(ctx, reader, in) - if err != nil { - return nil, err - } - found, err := a.Find(ctx) - if err != nil { - return nil, err - } - if !found { - return nil, fmt.Errorf("resource %q is not found", url) - } - - u, err := a.Export(ctx) - if err != nil { - return nil, err - } - - return u, nil - } + adapter, err := registry.AdapterForURL(ctx, url) + if err != nil { + return nil, err } + if adapter != nil { + found, err := adapter.Find(ctx) + if err != nil { + return nil, err + } + if !found { + return nil, fmt.Errorf("resource %q is not found", url) + } - //monitoring.googleapis.com/projects/PROJECT_NUMBER/dashboards/DASHBOARD_ID - if strings.HasPrefix(url, "//monitoring.googleapis.com/") { - tokens := strings.Split(strings.TrimPrefix(url, "//monitoring.googleapis.com/"), "/") - if len(tokens) == 4 && tokens[0] == "projects" && tokens[2] == "dashboards" { - model, err := registry.GetModel(schema.GroupKind{Group: "monitoring.cnrm.cloud.google.com", Kind: "MonitoringDashboard"}) - if err != nil { - return nil, err - } - in := &unstructured.Unstructured{} - in.SetName(tokens[3]) - if err := unstructured.SetNestedField(in.Object, tokens[1], "spec", "projectRef", "external"); err != nil { - return nil, err - } - - var reader client.Reader // TODO: Create erroring reader? - a, err := model.AdapterForObject(ctx, reader, in) - if err != nil { - return nil, err - } - found, err := a.Find(ctx) - if err != nil { - return nil, err - } - if !found { - return nil, fmt.Errorf("resource %q is not found", url) - } - - u, err := a.Export(ctx) - if err != nil { - return nil, err - } - - return u, nil + u, err := adapter.Export(ctx) + if err != nil { + return nil, err } + + return u, nil } + return nil, nil } diff --git a/pkg/controller/direct/gkehub/featuremembership_controller.go b/pkg/controller/direct/gkehub/featuremembership_controller.go index 7063abd897..1fce73f15a 100644 --- a/pkg/controller/direct/gkehub/featuremembership_controller.go +++ b/pkg/controller/direct/gkehub/featuremembership_controller.go @@ -117,6 +117,10 @@ func (m *gkeHubModel) AdapterForObject(ctx context.Context, reader client.Reader }, nil } +func (m *gkeHubModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) { + return nil, nil +} + func resolveIAMReferences(ctx context.Context, reader client.Reader, obj *krm.GKEHubFeatureMembership) error { spec := obj.Spec if spec.Configmanagement != nil && spec.Configmanagement.ConfigSync != nil { diff --git a/pkg/controller/direct/iam.go b/pkg/controller/direct/iam.go new file mode 100644 index 0000000000..31bb65b360 --- /dev/null +++ b/pkg/controller/direct/iam.go @@ -0,0 +1,189 @@ +// Copyright 2024 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 direct + +import ( + "context" + "fmt" + + "cloud.google.com/go/iam/apiv1/iampb" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/iam/v1beta1" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type IAMAdapter interface { + GetIAMPolicy(ctx context.Context) (*iampb.Policy, error) + SetIAMPolicy(ctx context.Context, policy *iampb.Policy) (*iampb.Policy, error) +} + +// GetIAMPolicyMember returns the actual IAMPolicyMember for the specified member and referenced resource. +func GetIAMPolicyMember(ctx context.Context, reader client.Reader, want *v1beta1.IAMPolicyMember, memberID v1beta1.Member) (*v1beta1.IAMPolicyMember, error) { + adapter, err := registry.AdapterForReference(ctx, reader, want.GetNamespace(), want.Spec.ResourceReference) + if err != nil { + return nil, fmt.Errorf("building adapter: %w", err) + } + iamAdapter, ok := adapter.(IAMAdapter) + if !ok { + return nil, fmt.Errorf("adapter does not implement IAMAdapter") + } + + policy, err := iamAdapter.GetIAMPolicy(ctx) + if err != nil { + return nil, fmt.Errorf("getting IAM policy: %w", err) + } + + actual := &v1beta1.IAMPolicyMember{} + actual.ObjectMeta = want.ObjectMeta + actual.Spec = v1beta1.IAMPolicyMemberSpec{ + ResourceReference: want.Spec.ResourceReference, + } + + actual.Spec.Member = memberID + + for _, binding := range policy.Bindings { + if binding.Role != want.Spec.Role { + continue + } + for _, member := range binding.Members { + if member == string(memberID) { + actual.Spec.Role = want.Spec.Role + } + } + } + return actual, nil +} + +// SetIAMPolicyMember will update the IAM policy for the specified member +func SetIAMPolicyMember(ctx context.Context, reader client.Reader, want *v1beta1.IAMPolicyMember, memberID v1beta1.Member) (*v1beta1.IAMPolicyMember, error) { + adapter, err := registry.AdapterForReference(ctx, reader, want.GetNamespace(), want.Spec.ResourceReference) + if err != nil { + return nil, fmt.Errorf("building adapter: %w", err) + } + iamAdapter, ok := adapter.(IAMAdapter) + if !ok { + return nil, fmt.Errorf("adapter does not implement IAMAdapter") + } + + policy, err := iamAdapter.GetIAMPolicy(ctx) + if err != nil { + return nil, fmt.Errorf("getting IAM policy: %w", err) + } + + var binding *iampb.Binding + for _, b := range policy.Bindings { + if b.Role != want.Spec.Role { + continue + } + binding = b + } + + if binding == nil { + binding = &iampb.Binding{ + Role: want.Spec.Role, + } + policy.Bindings = append(policy.Bindings, binding) + } + + hasMember := false + for _, member := range binding.Members { + if member == string(memberID) { + hasMember = true + } + } + latest := policy + if !hasMember { + binding.Members = append(binding.Members, string(memberID)) + newPolicy, err := iamAdapter.SetIAMPolicy(ctx, policy) + if err != nil { + return nil, fmt.Errorf("setting IAM policy: %w", err) + } + latest = newPolicy + } + + actual := &v1beta1.IAMPolicyMember{} + actual.ObjectMeta = want.ObjectMeta + actual.Spec = v1beta1.IAMPolicyMemberSpec{ + ResourceReference: want.Spec.ResourceReference, + } + + actual.Spec.Member = memberID + + for _, binding := range latest.Bindings { + if binding.Role != want.Spec.Role { + continue + } + for _, member := range binding.Members { + if member == string(memberID) { + actual.Spec.Role = want.Spec.Role + } + } + } + return actual, nil +} + +// DeleteIAMPolicyMember will remove the specified member for the IAM policy for a resource +func DeleteIAMPolicyMember(ctx context.Context, reader client.Reader, want *v1beta1.IAMPolicyMember, removeMember v1beta1.Member) error { + log := klog.FromContext(ctx) + + adapter, err := registry.AdapterForReference(ctx, reader, want.GetNamespace(), want.Spec.ResourceReference) + if err != nil { + return fmt.Errorf("building adapter: %w", err) + } + iamAdapter, ok := adapter.(IAMAdapter) + if !ok { + return fmt.Errorf("adapter does not implement IAMAdapter") + } + + policy, err := iamAdapter.GetIAMPolicy(ctx) + if err != nil { + return fmt.Errorf("getting IAM policy: %w", err) + } + + var binding *iampb.Binding + for _, b := range policy.Bindings { + if b.Role != want.Spec.Role { + continue + } + binding = b + } + + if binding == nil { + return nil + } + + var newMembers []string + removedMember := false + for _, member := range binding.Members { + if member == string(removeMember) { + removedMember = true + continue + } + newMembers = append(newMembers, member) + } + binding.Members = newMembers + + if !removedMember { + return nil + } + newPolicy, err := iamAdapter.SetIAMPolicy(ctx, policy) + if err != nil { + return fmt.Errorf("setting IAM policy: %w", err) + } + + log.Info("updated iam policy to remove member", "updatedPolicy", newPolicy, "member", removeMember) + return nil +} diff --git a/pkg/controller/direct/logging/logmetric_controller.go b/pkg/controller/direct/logging/logmetric_controller.go index 4d8fa584e5..e01c96ea7e 100644 --- a/pkg/controller/direct/logging/logmetric_controller.go +++ b/pkg/controller/direct/logging/logmetric_controller.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "reflect" + "strings" api "google.golang.org/api/logging/v2" corev1 "k8s.io/api/core/v1" @@ -108,6 +109,34 @@ func (m *logMetricModel) AdapterForObject(ctx context.Context, reader client.Rea }, nil } +func (m *logMetricModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) { + // Format: //logging.googleapis.com/projects//metrics/ + if !strings.HasPrefix(url, "//logging.googleapis.com/") { + return nil, nil + } + + tokens := strings.Split(strings.TrimPrefix(url, "//logging.googleapis.com/"), "/") + if len(tokens) == 4 && tokens[0] == "projects" && tokens[2] == "metrics" { + gcpClient, err := newGCPClient(ctx, m.config) + if err != nil { + return nil, err + } + + projectMetricsService, err := gcpClient.newProjectMetricsService(ctx) + if err != nil { + return nil, err + } + + return &logMetricAdapter{ + projectID: tokens[1], + resourceID: tokens[3], + logMetricClient: projectMetricsService, + }, nil + } + + return nil, nil +} + func (a *logMetricAdapter) Find(ctx context.Context) (bool, error) { if a.resourceID == "" { return false, nil diff --git a/pkg/controller/direct/monitoring/monitoringdashboard_controller.go b/pkg/controller/direct/monitoring/monitoringdashboard_controller.go index c04c129a01..a9a165bc77 100644 --- a/pkg/controller/direct/monitoring/monitoringdashboard_controller.go +++ b/pkg/controller/direct/monitoring/monitoringdashboard_controller.go @@ -17,6 +17,7 @@ package monitoring import ( "context" "fmt" + "strings" api "cloud.google.com/go/monitoring/dashboard/apiv1" pb "cloud.google.com/go/monitoring/dashboard/apiv1/dashboardpb" @@ -113,6 +114,35 @@ func (m *dashboardModel) AdapterForObject(ctx context.Context, kube client.Reade }, nil } +func (m *dashboardModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) { + // Format: //monitoring.googleapis.com/projects/PROJECT_NUMBER/dashboards/DASHBOARD_ID + if !strings.HasPrefix(url, "//monitoring.googleapis.com/") { + return nil, nil + } + + tokens := strings.Split(strings.TrimPrefix(url, "//monitoring.googleapis.com/"), "/") + if len(tokens) == 4 && tokens[0] == "projects" && tokens[2] == "dashboards" { + gcpClient, err := newGCPClient(ctx, m.config) + if err != nil { + return nil, fmt.Errorf("building gcp client: %w", err) + } + + dashboardsClient, err := gcpClient.newDashboardsClient(ctx) + if err != nil { + return nil, err + } + + return &dashboardAdapter{ + projectID: tokens[1], + resourceID: tokens[3], + dashboardsClient: dashboardsClient, + }, nil + } + + return nil, nil + +} + // Find implements the Adapter interface. func (a *dashboardAdapter) Find(ctx context.Context) (bool, error) { if a.resourceID == "" { diff --git a/pkg/controller/direct/privateca/client.go b/pkg/controller/direct/privateca/client.go new file mode 100644 index 0000000000..ae228ffd7a --- /dev/null +++ b/pkg/controller/direct/privateca/client.go @@ -0,0 +1,70 @@ +// Copyright 2024 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 privateca + +import ( + "context" + "fmt" + + api "cloud.google.com/go/security/privateca/apiv1" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config" + "google.golang.org/api/option" +) + +type gcpClient struct { + config config.ControllerConfig +} + +func newGCPClient(ctx context.Context, config *config.ControllerConfig) (*gcpClient, error) { + gcpClient := &gcpClient{ + config: *config, + } + return gcpClient, nil +} + +func (m *gcpClient) options() ([]option.ClientOption, error) { + var opts []option.ClientOption + // TODO: Support for useragent + if m.config.UserAgent != "" { + opts = append(opts, option.WithUserAgent(m.config.UserAgent)) + } + if m.config.HTTPClient != nil { + // TODO: Set UserAgent in this scenario (error is: WithHTTPClient is incompatible with gRPC dial options) + opts = append(opts, option.WithHTTPClient(m.config.HTTPClient)) + } + if m.config.UserProjectOverride && m.config.BillingProject != "" { + opts = append(opts, option.WithQuotaProject(m.config.BillingProject)) + } + + // TODO: support endpoints? + // if m.config.Endpoint != "" { + // opts = append(opts, option.WithEndpoint(m.config.Endpoint)) + // } + + return opts, nil +} + +func (m *gcpClient) newCertificateAuthorityClient(ctx context.Context) (*api.CertificateAuthorityClient, error) { + opts, err := m.options() + if err != nil { + return nil, err + } + service, err := api.NewCertificateAuthorityRESTClient(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("building service for certificate authority: %w", err) + } + + return service, nil +} diff --git a/pkg/controller/direct/privateca/privatecapool_controller.go b/pkg/controller/direct/privateca/privatecapool_controller.go new file mode 100644 index 0000000000..5b8e4befae --- /dev/null +++ b/pkg/controller/direct/privateca/privatecapool_controller.go @@ -0,0 +1,212 @@ +// Copyright 2024 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 privateca + +import ( + "context" + "fmt" + "strings" + + iampb "cloud.google.com/go/iam/apiv1/iampb" + api "cloud.google.com/go/security/privateca/apiv1" + pb "cloud.google.com/go/security/privateca/apiv1/privatecapb" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + krm "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/privateca/v1beta1" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/references" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry" +) + +func init() { + registry.RegisterModel(krm.PrivateCACAPoolGVK, newCAPoolModel) +} + +func newCAPoolModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) { + gcpClient, err := newGCPClient(ctx, config) + if err != nil { + return nil, fmt.Errorf("building GCP client: %w", err) + } + return &caPoolModel{gcpClient: gcpClient}, nil +} + +type caPoolModel struct { + *gcpClient +} + +// model implements the Model interface. +var _ directbase.Model = &caPoolModel{} + +type caPoolAdapter struct { + projectID string + location string + caPoolID string + + desired *krm.PrivateCACAPool + actual *pb.CaPool + caClient *api.CertificateAuthorityClient +} + +var _ directbase.Adapter = &caPoolAdapter{} + +// AdapterForObject implements the Model interface. +func (m *caPoolModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) { + caClient, err := m.newCertificateAuthorityClient(ctx) + if err != nil { + return nil, err + } + + obj := &krm.PrivateCACAPool{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil { + return nil, fmt.Errorf("error converting to %T: %w", obj, err) + } + + resourceID := ValueOf(obj.Spec.ResourceID) + if resourceID == "" { + resourceID = obj.GetName() + } + if resourceID == "" { + return nil, fmt.Errorf("cannot resolve resource ID") + } + + location := obj.Spec.Location + if location == "" { + return nil, fmt.Errorf("cannot resolve location") + } + + projectRef, err := references.ResolveProject(ctx, reader, obj, references.AsProjectRef(&obj.Spec.ProjectRef)) + if err != nil { + return nil, err + } + projectID := projectRef.ProjectID + if projectID == "" { + return nil, fmt.Errorf("cannot resolve project") + } + + return &caPoolAdapter{ + caPoolID: resourceID, + location: location, + projectID: projectID, + desired: obj, + caClient: caClient, + }, nil +} + +func (m *caPoolModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) { + // Format is //privateca.googleapis.com/projects/PROJECT_ID/locations/LOCATION/caPools/CA_POOL_ID + + if !strings.HasPrefix(url, "//privateca.googleapis.com/") { + return nil, nil + } + + tokens := strings.Split(strings.TrimPrefix(url, "//privateca.googleapis.com/"), "/") + if len(tokens) == 6 && tokens[0] == "projects" && tokens[2] == "locations" && tokens[4] == "caPools" { + caClient, err := m.newCertificateAuthorityClient(ctx) + if err != nil { + return nil, err + } + + return &caPoolAdapter{ + projectID: tokens[1], + location: tokens[3], + caPoolID: tokens[5], + caClient: caClient, + }, nil + } + + return nil, nil +} + +// Delete implements the Adapter interface. +func (a *caPoolAdapter) Delete(ctx context.Context) (bool, error) { + return false, fmt.Errorf("not implemented") +} + +// Create implements the Adapter interface. +func (a *caPoolAdapter) Create(ctx context.Context, u *unstructured.Unstructured) error { + return fmt.Errorf("not implemented") +} + +// Update implements the Adapter interface. +func (a *caPoolAdapter) Update(ctx context.Context, u *unstructured.Unstructured) error { + return fmt.Errorf("not implemented") +} + +// Export implements the Adapter interface. +func (a *caPoolAdapter) Export(ctx context.Context) (*unstructured.Unstructured, error) { + return nil, fmt.Errorf("not implemented") +} + +// Find implements the Adapter interface. +func (a *caPoolAdapter) Find(ctx context.Context) (bool, error) { + if a.caPoolID == "" { + return false, nil + } + + req := &pb.GetCaPoolRequest{ + Name: a.fullyQualifiedName(), + } + logMetric, err := a.caClient.GetCaPool(ctx, req) + if err != nil { + if IsNotFound(err) { + return false, nil + } + return false, fmt.Errorf("getting logMetric %q: %w", a.fullyQualifiedName(), err) + } + + a.actual = logMetric + + return true, nil +} + +func (a *caPoolAdapter) GetIAMPolicy(ctx context.Context) (*iampb.Policy, error) { + if a.caPoolID == "" { + return nil, fmt.Errorf("cannot get iam policy for missing resource") + } + + req := &iampb.GetIamPolicyRequest{ + Resource: a.fullyQualifiedName(), + } + policy, err := a.caClient.GetIamPolicy(ctx, req) + if err != nil { + return nil, fmt.Errorf("getting iam policy for %q: %w", a.fullyQualifiedName(), err) + } + + return policy, nil +} + +func (a *caPoolAdapter) SetIAMPolicy(ctx context.Context, policy *iampb.Policy) (*iampb.Policy, error) { + if a.caPoolID == "" { + return nil, fmt.Errorf("cannot get iam policy for missing resource") + } + + req := &iampb.SetIamPolicyRequest{ + Resource: a.fullyQualifiedName(), + Policy: policy, + } + newPolicy, err := a.caClient.SetIamPolicy(ctx, req) + if err != nil { + return nil, fmt.Errorf("setting iam policy for %q: %w", a.fullyQualifiedName(), err) + } + + return newPolicy, nil +} + +func (a *caPoolAdapter) fullyQualifiedName() string { + return fmt.Sprintf("projects/%s/locations/%s/caPools/%s", a.projectID, a.location, a.caPoolID) +} diff --git a/pkg/controller/direct/privateca/utils.go b/pkg/controller/direct/privateca/utils.go new file mode 100644 index 0000000000..e01a625ce2 --- /dev/null +++ b/pkg/controller/direct/privateca/utils.go @@ -0,0 +1,64 @@ +// Copyright 2024 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 privateca + +import ( + "errors" + + "github.com/googleapis/gax-go/v2/apierror" + "k8s.io/klog/v2" +) + +// todo acpana: add to factor out to top level package +// todo acpana: begin +func ValueOf[T any](p *T) T { + var v T + if p != nil { + v = *p + } + return v +} + +// LazyPtr returns a pointer to v, unless it is the empty value, in which case it returns nil. +// It is essentially the inverse of ValueOf, though it is lossy +// because we can't tell nil and empty apart without a pointer. +func LazyPtr[T comparable](v T) *T { + var defaultValue T + if v == defaultValue { + return nil + } + return &v +} + +// IsNotFound returns true if the given error is an HTTP 404. +func IsNotFound(err error) bool { + return HasHTTPCode(err, 404) +} + +// HasHTTPCode returns true if the given error is an HTTP response with the given code. +func HasHTTPCode(err error, code int) bool { + if err == nil { + return false + } + apiError := &apierror.APIError{} + if errors.As(err, &apiError) { + if apiError.HTTPCode() == code { + return true + } + } else { + klog.Warningf("unexpected error type %T", err) + } + return false +} diff --git a/pkg/controller/direct/references/projectref.go b/pkg/controller/direct/references/projectref.go index 435b46d263..8295d47761 100644 --- a/pkg/controller/direct/references/projectref.go +++ b/pkg/controller/direct/references/projectref.go @@ -20,6 +20,7 @@ import ( "strings" refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/k8s/v1alpha1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -92,3 +93,16 @@ func ResolveProject(ctx context.Context, reader client.Reader, src client.Object ProjectID: projectID, }, nil } + +// AsProjectRef converts a generic ResourceRef into a ProjectRef +func AsProjectRef(in *v1alpha1.ResourceRef) *refs.ProjectRef { + if in == nil { + return nil + } + return &refs.ProjectRef{ + Namespace: in.Namespace, + Name: in.Name, + External: in.External, + Kind: in.Kind, + } +} diff --git a/pkg/controller/direct/register/register.go b/pkg/controller/direct/register/register.go index 23a9957548..fd38591091 100644 --- a/pkg/controller/direct/register/register.go +++ b/pkg/controller/direct/register/register.go @@ -21,5 +21,6 @@ import ( _ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/gkehub" _ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/logging" _ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/monitoring" + _ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/privateca" _ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/resourcemanager" ) diff --git a/pkg/controller/direct/registry/references.go b/pkg/controller/direct/registry/references.go new file mode 100644 index 0000000000..53b2a878c8 --- /dev/null +++ b/pkg/controller/direct/registry/references.go @@ -0,0 +1,98 @@ +// Copyright 2024 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 registry + +import ( + "context" + "fmt" + "strings" + + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/iam/v1beta1" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func AdapterForReference(ctx context.Context, reader client.Reader, sourceNamespace string, resourceRef v1beta1.ResourceReference) (directbase.Adapter, error) { + obj := &unstructured.Unstructured{} + + var gk schema.GroupKind + switch resourceRef.Kind { + default: + gk = resourceRef.GroupVersionKind().GroupKind() + } + + if gk.Group == "" { + return nil, fmt.Errorf("cannot find group for reference %v (must set apiVersion)", resourceRef) + } + + if resourceRef.External != "" { + uri := "" + if !strings.HasPrefix(uri, "//") { + switch gk.Group { + case "privateca.cnrm.google.com": + uri = "//privateca.googleapis.com/" + resourceRef.External + default: + return nil, fmt.Errorf("unknown format for external reference for %v: %q", gk, resourceRef.External) + } + } + + adapter, err := AdapterForURL(ctx, uri) + if err != nil { + return nil, fmt.Errorf("resolving %q: %w", uri, err) + } + if adapter == nil { + return nil, fmt.Errorf("unknown format for external reference for %v: %q", gk, resourceRef.External) + } + return adapter, nil + } + + model, err := GetModel(gk) + if err != nil { + return nil, fmt.Errorf("cannot handle references to %v (in direct controller)", gk) + } + + gvk, ok := PreferredGVK(gk) + if !ok { + return nil, fmt.Errorf("preferred GVK is not known for %v", gk) + } + + obj.SetGroupVersionKind(gvk) + nn := types.NamespacedName{ + Namespace: resourceRef.Namespace, + Name: resourceRef.Name, + } + if nn.Namespace == "" { + nn.Namespace = sourceNamespace + } + + if err := reader.Get(ctx, nn, obj); err != nil { + if apierrors.IsNotFound(err) { + return nil, k8s.NewReferenceNotFoundError(gvk, nn) + } + return nil, fmt.Errorf("error retrieving resource '%v' with GroupVersionKind '%v': %w", nn, gvk, err) + } + + adapter, err := model.AdapterForObject(ctx, reader, obj) + if err != nil { + return nil, fmt.Errorf("building adapter: %w", err) + } + + return adapter, nil +} diff --git a/pkg/controller/direct/registry/registry.go b/pkg/controller/direct/registry/registry.go index 3a12265fc5..8b05546ef6 100644 --- a/pkg/controller/direct/registry/registry.go +++ b/pkg/controller/direct/registry/registry.go @@ -53,6 +53,23 @@ func PreferredGVK(gk schema.GroupKind) (schema.GroupVersionKind, bool) { return registration.gvk, true } +// AdapterForURL will return a directbase.Adapter bound to the resource specified by the URL, +// or (nil, nil) if it is not recognized. +func AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) { + for _, registration := range singleton.registrations { + if registration.model == nil { + return nil, fmt.Errorf("registry was not initialized") + } + adapter, err := registration.model.AdapterForURL(ctx, url) + if err != nil { + return nil, err + } + if adapter != nil { + return adapter, nil + } + } + return nil, nil +} func Init(ctx context.Context, config *config.ControllerConfig) error { for _, registration := range singleton.registrations { model, err := registration.factory(ctx, config) diff --git a/pkg/controller/direct/resourcemanager/tagkey_controller.go b/pkg/controller/direct/resourcemanager/tagkey_controller.go index 39bd057340..cbe3484552 100644 --- a/pkg/controller/direct/resourcemanager/tagkey_controller.go +++ b/pkg/controller/direct/resourcemanager/tagkey_controller.go @@ -94,6 +94,10 @@ func (m *tagKeyModel) AdapterForObject(ctx context.Context, reader client.Reader }, nil } +func (m *tagKeyModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) { + return nil, nil +} + // Find implements the Adapter interface. func (a *tagKeyAdapter) Find(ctx context.Context) (bool, error) { if a.resourceID == "" { diff --git a/pkg/controller/iam/iamclient/iamclient.go b/pkg/controller/iam/iamclient/iamclient.go index 7573e16f7e..0b7fbb4fdc 100644 --- a/pkg/controller/iam/iamclient/iamclient.go +++ b/pkg/controller/iam/iamclient/iamclient.go @@ -20,11 +20,14 @@ import ( "regexp" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/iam/v1beta1" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/conversion" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader" mmdcl "github.com/GoogleCloudPlatform/declarative-resource-client-library/dcl" dcliam "github.com/GoogleCloudPlatform/declarative-resource-client-library/services/google/iam" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s" tfschema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" @@ -54,7 +57,7 @@ const ( ) var ( - ErrNotFound = fmt.Errorf("IAM resource does not exist") + ErrNotFound = k8s.ErrIAMNotFound logger = klog.Log.WithName("iamclient") ProjectGVK = schema.GroupVersionKind{ @@ -91,6 +94,7 @@ var idTemplateVarsRegex = regexp.MustCompile(`{{[a-z]([a-zA-Z0-9\-_.]*[a-zA-Z0-9 type IAMClient struct { TFIAMClient *TFIAMClient DCLIAMClient *DCLIAMClient + kubeClient client.Client } func New(tfProvider *tfschema.Provider, @@ -114,11 +118,21 @@ func New(tfProvider *tfschema.Provider, iamClient := IAMClient{ TFIAMClient: &tfIAMClient, DCLIAMClient: &dclIAMClient, + kubeClient: kubeClient, } return &iamClient } func (c *IAMClient) SetPolicyMember(ctx context.Context, policyMember *v1beta1.IAMPolicyMember) (*v1beta1.IAMPolicyMember, error) { + if registry.IsIAMDirect(policyMember.Spec.ResourceReference.GroupVersionKind().GroupKind()) { + id, err := ResolveMemberIdentity(ctx, policyMember.Spec.Member, policyMember.Spec.MemberFrom, policyMember.GetNamespace(), c.TFIAMClient) + if err != nil { + return nil, err + } + + return direct.SetIAMPolicyMember(ctx, c.kubeClient, policyMember, v1beta1.Member(id)) + } + if c.isDCLBasedIAMResource(policyMember) { return c.DCLIAMClient.SetPolicyMember(ctx, c.TFIAMClient, policyMember) } @@ -126,6 +140,14 @@ func (c *IAMClient) SetPolicyMember(ctx context.Context, policyMember *v1beta1.I } func (c *IAMClient) GetPolicyMember(ctx context.Context, policyMember *v1beta1.IAMPolicyMember) (*v1beta1.IAMPolicyMember, error) { + if registry.IsIAMDirect(policyMember.Spec.ResourceReference.GroupVersionKind().GroupKind()) { + id, err := ResolveMemberIdentity(ctx, policyMember.Spec.Member, policyMember.Spec.MemberFrom, policyMember.GetNamespace(), c.TFIAMClient) + if err != nil { + return nil, err + } + + return direct.GetIAMPolicyMember(ctx, c.kubeClient, policyMember, v1beta1.Member(id)) + } if c.isDCLBasedIAMResource(policyMember) { return c.DCLIAMClient.GetPolicyMember(ctx, c.TFIAMClient, policyMember) } @@ -133,6 +155,15 @@ func (c *IAMClient) GetPolicyMember(ctx context.Context, policyMember *v1beta1.I } func (c *IAMClient) DeletePolicyMember(ctx context.Context, policyMember *v1beta1.IAMPolicyMember) error { + if registry.IsIAMDirect(policyMember.Spec.ResourceReference.GroupVersionKind().GroupKind()) { + id, err := ResolveMemberIdentity(ctx, policyMember.Spec.Member, policyMember.Spec.MemberFrom, policyMember.GetNamespace(), c.TFIAMClient) + if err != nil { + return err + } + + return direct.DeleteIAMPolicyMember(ctx, c.kubeClient, policyMember, v1beta1.Member(id)) + } + if c.isDCLBasedIAMResource(policyMember) { return c.DCLIAMClient.DeletePolicyMember(ctx, c.TFIAMClient, policyMember) diff --git a/pkg/k8s/errors.go b/pkg/k8s/errors.go index 1763ee9bbc..f095c15392 100644 --- a/pkg/k8s/errors.go +++ b/pkg/k8s/errors.go @@ -87,6 +87,8 @@ func IsReferenceNotFoundError(err error) bool { return ok } +var ErrIAMNotFound = fmt.Errorf("IAM resource does not exist") + type SecretNotFoundError struct { Secret types.NamespacedName } diff --git a/pkg/webhook/iam_validator.go b/pkg/webhook/iam_validator.go index d815e3c16a..adf4dc850c 100644 --- a/pkg/webhook/iam_validator.go +++ b/pkg/webhook/iam_validator.go @@ -21,6 +21,7 @@ import ( "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/iam/v1beta1" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry" kcciamclient "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/iam/iamclient" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/extension" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/metadata" @@ -292,6 +293,7 @@ func (a *iamValidatorHandler) tfValidateIAMPartialPolicy(partialPolicy *v1beta1. func (a *iamValidatorHandler) dclValidateIAMPolicyMember(policyMember *v1beta1.IAMPolicyMember) admission.Response { resourceRef := policyMember.Spec.ResourceReference + // Check that DCL-based resource supports IAMPolicy dclSchema, resp := getDCLSchema(resourceRef.GroupVersionKind(), a.serviceMetadataLoader, a.schemaLoader) if !resp.Allowed { @@ -301,6 +303,10 @@ func (a *iamValidatorHandler) dclValidateIAMPolicyMember(policyMember *v1beta1.I if err != nil { return admission.Errored(http.StatusInternalServerError, err) } + // Beginnings of direct IAM support: direct-IAM added to existing DCL resource + if registry.IsIAMDirect(resourceRef.GroupVersionKind().GroupKind()) { + supportsIAM = true + } if !supportsIAM { return admission.Errored(http.StatusForbidden, fmt.Errorf("GroupVersionKind %v does not support IAM Policy Member", resourceRef.GroupVersionKind())) } diff --git a/tests/e2e/unified_test.go b/tests/e2e/unified_test.go index 64902de1eb..802714c0dd 100644 --- a/tests/e2e/unified_test.go +++ b/tests/e2e/unified_test.go @@ -549,6 +549,9 @@ func runScenario(ctx context.Context, t *testing.T, testPause bool, fixture reso addReplacement("insertTime", "2024-04-01T12:34:56.123456Z") addReplacement("user", "user@example.com") + // Specific to IAM/policy + addReplacement("policy.etag", "abcdef0123A=") + // Specific to vertexai addReplacement("blobStoragePathPrefix", "cloud-ai-platform-00000000-1111-2222-3333-444444444444") addReplacement("response.blobStoragePathPrefix", "cloud-ai-platform-00000000-1111-2222-3333-444444444444") @@ -607,12 +610,11 @@ func runScenario(ctx context.Context, t *testing.T, testPause bool, fixture reso addReplacement("serverCaCert.expirationTime", "2024-04-01T12:34:56.123456Z") // Specific to KMS - - addReplacement("policy.etag", "abcdef0123A=") addSetStringReplacement(".cryptoKeyVersions[].createTime", "2024-04-01T12:34:56.123456Z") addSetStringReplacement(".cryptoKeyVersions[].generateTime", "2024-04-01T12:34:56.123456Z") addReplacement("destroyTime", "2024-04-01T12:34:56.123456Z") addReplacement("generateTime", "2024-04-01T12:34:56.123456Z") + // Replace any empty values in LROs; this is surprisingly difficult to fix in mockgcp // // "response": { From 257598e6e9ccdd633f9404105d61bc910610cc31 Mon Sep 17 00:00:00 2001 From: justinsb Date: Wed, 22 May 2024 19:32:16 -0400 Subject: [PATCH 2/2] tests: add test for IAM for PrivateCACAPool --- ...ated_object_privatecacapooliam.golden.yaml | 30 + .../privatecacapooliam/_http.log | 818 ++++++++++++++++++ .../privatecacapooliam/create.yaml | 27 + .../privatecacapooliam/dependencies.yaml | 100 +++ 4 files changed, 975 insertions(+) create mode 100644 pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/_generated_object_privatecacapooliam.golden.yaml create mode 100644 pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/_http.log create mode 100644 pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/create.yaml create mode 100644 pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/dependencies.yaml diff --git a/pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/_generated_object_privatecacapooliam.golden.yaml b/pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/_generated_object_privatecacapooliam.golden.yaml new file mode 100644 index 0000000000..c9a58a241e --- /dev/null +++ b/pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/_generated_object_privatecacapooliam.golden.yaml @@ -0,0 +1,30 @@ +apiVersion: iam.cnrm.cloud.google.com/v1beta1 +kind: IAMPolicyMember +metadata: + annotations: + cnrm.cloud.google.com/state-into-spec: merge + finalizers: + - cnrm.cloud.google.com/finalizer + - cnrm.cloud.google.com/deletion-defender + generation: 1 + labels: + cnrm-test: "true" + name: iampolicymember-${uniqueId} + namespace: ${uniqueId} +spec: + memberFrom: + serviceAccountRef: + name: privatecacapool-dep + resourceRef: + apiVersion: privateca.cnrm.cloud.google.com/v1beta1 + kind: PrivateCACAPool + name: privatecacapool-${uniqueId} + role: roles/privateca.admin +status: + conditions: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: The resource is up to date + reason: UpToDate + status: "True" + type: Ready + observedGeneration: 1 diff --git a/pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/_http.log b/pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/_http.log new file mode 100644 index 0000000000..7869a11f7f --- /dev/null +++ b/pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/_http.log @@ -0,0 +1,818 @@ +GET https://iam.googleapis.com/v1/projects/${projectId}/serviceAccounts/capool-${uniqueId}@${projectId}.iam.gserviceaccount.com?alt=json&prettyPrint=false +User-Agent: google-api-go-client/0.5 Terraform/ (+https://www.terraform.io) Terraform-Plugin-SDK/2.10.1 terraform-provider-google-beta/kcc/controller-manager + +404 Not Found +Cache-Control: private +Content-Type: application/json; charset=UTF-8 +Server: ESF +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Xss-Protection: 0 + +{ + "error": { + "code": 404, + "errors": [ + { + "domain": "global", + "message": "Unknown service account", + "reason": "notFound" + } + ], + "message": "Unknown service account", + "status": "NOT_FOUND" + } +} + +--- + +POST https://iam.googleapis.com/v1/projects/${projectId}/serviceAccounts?alt=json&prettyPrint=false +Content-Type: application/json +User-Agent: google-api-go-client/0.5 Terraform/ (+https://www.terraform.io) Terraform-Plugin-SDK/2.10.1 terraform-provider-google-beta/kcc/controller-manager + +{ + "accountId": "capool-${uniqueId}", + "serviceAccount": { + "displayName": "ExampleGSA" + } +} + +200 OK +Cache-Control: private +Content-Type: application/json; charset=UTF-8 +Server: ESF +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Xss-Protection: 0 + +{ + "displayName": "ExampleGSA", + "email": "capool-${uniqueId}@${projectId}.iam.gserviceaccount.com", + "etag": "abcdef0123A=", + "name": "projects/${projectId}/serviceAccounts/capool-${uniqueId}@${projectId}.iam.gserviceaccount.com", + "oauth2ClientId": "888888888888888888888", + "projectId": "${projectId}", + "uniqueId": "111111111111111111111" +} + +--- + +GET https://iam.googleapis.com/v1/projects/${projectId}/serviceAccounts/capool-${uniqueId}@${projectId}.iam.gserviceaccount.com?alt=json&prettyPrint=false +User-Agent: google-api-go-client/0.5 Terraform/ (+https://www.terraform.io) Terraform-Plugin-SDK/2.10.1 terraform-provider-google-beta/kcc/controller-manager + +200 OK +Cache-Control: private +Content-Type: application/json; charset=UTF-8 +Server: ESF +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Xss-Protection: 0 + +{ + "displayName": "ExampleGSA", + "email": "capool-${uniqueId}@${projectId}.iam.gserviceaccount.com", + "etag": "abcdef0123A=", + "name": "projects/${projectId}/serviceAccounts/capool-${uniqueId}@${projectId}.iam.gserviceaccount.com", + "oauth2ClientId": "888888888888888888888", + "projectId": "${projectId}", + "uniqueId": "111111111111111111111" +} + +--- + +GET https://privateca.googleapis.com/v1/projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}?alt=json +Content-Type: application/json +User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 + +404 Not Found +Cache-Control: private +Content-Type: application/json; charset=UTF-8 +Server: ESF +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Xss-Protection: 0 + +{ + "error": { + "code": 404, + "message": "Resource 'projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}' was not found", + "status": "NOT_FOUND" + } +} + +--- + +POST https://privateca.googleapis.com/v1/projects/${projectId}/locations/us-central1/caPools?alt=json&caPoolId=privatecacapool-${uniqueId} +Content-Type: application/json +User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 + +{ + "issuancePolicy": { + "allowedIssuanceModes": { + "allowConfigBasedIssuance": false, + "allowCsrBasedIssuance": true + }, + "allowedKeyTypes": [ + { + "rsa": { + "maxModulusSize": 128, + "minModulusSize": 64 + } + }, + { + "ellipticCurve": { + "signatureAlgorithm": "ECDSA_P384" + } + } + ], + "baselineValues": { + "additionalExtensions": [ + { + "critical": false, + "objectId": { + "objectIdPath": [ + 1, + 7 + ] + }, + "value": "c3RyaW5nCg==" + } + ], + "aiaOcspServers": [ + "string" + ], + "caOptions": { + "isCa": false, + "maxIssuerPathLength": 7 + }, + "keyUsage": { + "baseKeyUsage": { + "certSign": false, + "contentCommitment": false, + "crlSign": false, + "dataEncipherment": false, + "decipherOnly": false, + "digitalSignature": false, + "encipherOnly": false, + "keyAgreement": false, + "keyEncipherment": false + }, + "extendedKeyUsage": { + "clientAuth": false, + "codeSigning": false, + "emailProtection": false, + "ocspSigning": false, + "serverAuth": false, + "timeStamping": false + }, + "unknownExtendedKeyUsages": [ + { + "objectIdPath": [ + 1, + 7 + ] + } + ] + }, + "policyIds": [ + { + "objectIdPath": [ + 1, + 7 + ] + } + ] + }, + "identityConstraints": { + "allowSubjectAltNamesPassthrough": false, + "allowSubjectPassthrough": false, + "celExpression": { + "description": "Always false", + "expression": "false", + "location": "devops.ca_pool.json", + "title": "Sample expression" + } + }, + "maximumLifetime": "43200s", + "passthroughExtensions": { + "additionalExtensions": [ + { + "objectIdPath": [ + 1, + 7 + ] + } + ], + "knownExtensions": [ + "BASE_KEY_USAGE" + ] + } + }, + "labels": { + "cnrm-test": "true", + "label-two": "value-two", + "managed-by-cnrm": "true" + }, + "name": "projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}", + "tier": "ENTERPRISE" +} + +200 OK +Cache-Control: private +Content-Type: application/json; charset=UTF-8 +Server: ESF +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Xss-Protection: 0 + +{ + "metadata": { + "@type": "type.googleapis.com/google.cloud.security.privateca.v1.OperationMetadata", + "apiVersion": "v1", + "createTime": "2024-04-01T12:34:56.123456Z", + "target": "projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}", + "verb": "create" + }, + "name": "projects/${projectId}/locations/us-central1/operations/${operationID}" +} + +--- + +GET https://privateca.googleapis.com/v1/projects/${projectId}/locations/us-central1/operations/${operationID}?alt=json +Content-Type: application/json +User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 + +200 OK +Cache-Control: private +Content-Type: application/json; charset=UTF-8 +Server: ESF +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Xss-Protection: 0 + +{ + "done": true, + "metadata": { + "@type": "type.googleapis.com/google.cloud.security.privateca.v1.OperationMetadata", + "apiVersion": "v1", + "createTime": "2024-04-01T12:34:56.123456Z", + "endTime": "2024-04-01T12:34:56.123456Z", + "target": "projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}", + "verb": "create" + }, + "name": "projects/${projectId}/locations/us-central1/operations/${operationID}", + "response": { + "@type": "type.googleapis.com/google.cloud.security.privateca.v1.CaPool", + "issuancePolicy": { + "allowedIssuanceModes": { + "allowCsrBasedIssuance": true + }, + "allowedKeyTypes": [ + { + "rsa": { + "maxModulusSize": "128", + "minModulusSize": "64" + } + }, + { + "ellipticCurve": { + "signatureAlgorithm": "ECDSA_P384" + } + } + ], + "baselineValues": { + "additionalExtensions": [ + { + "objectId": { + "objectIdPath": [ + 1, + 7 + ] + }, + "value": "c3RyaW5nCg==" + } + ], + "aiaOcspServers": [ + "string" + ], + "caOptions": { + "isCa": false, + "maxIssuerPathLength": 7 + }, + "keyUsage": { + "unknownExtendedKeyUsages": [ + { + "objectIdPath": [ + 1, + 7 + ] + } + ] + }, + "policyIds": [ + { + "objectIdPath": [ + 1, + 7 + ] + } + ] + }, + "identityConstraints": { + "allowSubjectAltNamesPassthrough": false, + "allowSubjectPassthrough": false, + "celExpression": { + "description": "Always false", + "expression": "false", + "location": "devops.ca_pool.json", + "title": "Sample expression" + } + }, + "maximumLifetime": "43200s", + "passthroughExtensions": { + "additionalExtensions": [ + { + "objectIdPath": [ + 1, + 7 + ] + } + ], + "knownExtensions": [ + "BASE_KEY_USAGE" + ] + } + }, + "labels": { + "cnrm-test": "true", + "label-two": "value-two", + "managed-by-cnrm": "true" + }, + "name": "projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}", + "tier": "ENTERPRISE" + } +} + +--- + +GET https://privateca.googleapis.com/v1/projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}?alt=json +Content-Type: application/json +User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 + +200 OK +Cache-Control: private +Content-Type: application/json; charset=UTF-8 +Server: ESF +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Xss-Protection: 0 + +{ + "issuancePolicy": { + "allowedIssuanceModes": { + "allowCsrBasedIssuance": true + }, + "allowedKeyTypes": [ + { + "rsa": { + "maxModulusSize": "128", + "minModulusSize": "64" + } + }, + { + "ellipticCurve": { + "signatureAlgorithm": "ECDSA_P384" + } + } + ], + "baselineValues": { + "additionalExtensions": [ + { + "objectId": { + "objectIdPath": [ + 1, + 7 + ] + }, + "value": "c3RyaW5nCg==" + } + ], + "aiaOcspServers": [ + "string" + ], + "caOptions": { + "isCa": false, + "maxIssuerPathLength": 7 + }, + "keyUsage": { + "unknownExtendedKeyUsages": [ + { + "objectIdPath": [ + 1, + 7 + ] + } + ] + }, + "policyIds": [ + { + "objectIdPath": [ + 1, + 7 + ] + } + ] + }, + "identityConstraints": { + "allowSubjectAltNamesPassthrough": false, + "allowSubjectPassthrough": false, + "celExpression": { + "description": "Always false", + "expression": "false", + "location": "devops.ca_pool.json", + "title": "Sample expression" + } + }, + "maximumLifetime": "43200s", + "passthroughExtensions": { + "additionalExtensions": [ + { + "objectIdPath": [ + 1, + 7 + ] + } + ], + "knownExtensions": [ + "BASE_KEY_USAGE" + ] + } + }, + "labels": { + "cnrm-test": "true", + "label-two": "value-two", + "managed-by-cnrm": "true" + }, + "name": "projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}", + "tier": "ENTERPRISE" +} + +--- + +GET https://privateca.googleapis.com/v1/projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}:getIamPolicy?%24alt=json%3Benum-encoding%3Dint +Content-Type: application/json +x-goog-request-params: resource=projects%2F${projectId}%2Flocations%2Fus-central1%2FcaPools%2Fprivatecacapool-${uniqueId} + + + +{ + "etag": "abcdef0123A=", + "version": 3 +} + +--- + +POST https://privateca.googleapis.com/v1/projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}:setIamPolicy?%24alt=json%3Benum-encoding%3Dint +Content-Type: application/json +x-goog-request-params: resource=projects%2F${projectId}%2Flocations%2Fus-central1%2FcaPools%2Fprivatecacapool-${uniqueId} + +{ + "policy": { + "bindings": [ + { + "members": [ + "serviceAccount:capool-${uniqueId}@${projectId}.iam.gserviceaccount.com" + ], + "role": "roles/privateca.admin" + } + ], + "etag": "abcdef0123A=", + "version": 3 + }, + "resource": "projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}" +} + + + +{ + "bindings": [ + { + "members": [ + "serviceAccount:capool-${uniqueId}@${projectId}.iam.gserviceaccount.com" + ], + "role": "roles/privateca.admin" + } + ], + "etag": "abcdef0123A=", + "version": 3 +} + +--- + +GET https://privateca.googleapis.com/v1/projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}:getIamPolicy?%24alt=json%3Benum-encoding%3Dint +Content-Type: application/json +x-goog-request-params: resource=projects%2F${projectId}%2Flocations%2Fus-central1%2FcaPools%2Fprivatecacapool-${uniqueId} + + + +{ + "bindings": [ + { + "members": [ + "serviceAccount:capool-${uniqueId}@${projectId}.iam.gserviceaccount.com" + ], + "role": "roles/privateca.admin" + } + ], + "etag": "abcdef0123A=", + "version": 3 +} + +--- + +POST https://privateca.googleapis.com/v1/projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}:setIamPolicy?%24alt=json%3Benum-encoding%3Dint +Content-Type: application/json +x-goog-request-params: resource=projects%2F${projectId}%2Flocations%2Fus-central1%2FcaPools%2Fprivatecacapool-${uniqueId} + +{ + "policy": { + "bindings": [ + { + "role": "roles/privateca.admin" + } + ], + "etag": "abcdef0123A=", + "version": 3 + }, + "resource": "projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}" +} + + + +{ + "bindings": [ + { + "role": "roles/privateca.admin" + } + ], + "etag": "abcdef0123A=", + "version": 3 +} + +--- + +GET https://privateca.googleapis.com/v1/projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}?alt=json +Content-Type: application/json +User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 + +200 OK +Cache-Control: private +Content-Type: application/json; charset=UTF-8 +Server: ESF +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Xss-Protection: 0 + +{ + "issuancePolicy": { + "allowedIssuanceModes": { + "allowCsrBasedIssuance": true + }, + "allowedKeyTypes": [ + { + "rsa": { + "maxModulusSize": "128", + "minModulusSize": "64" + } + }, + { + "ellipticCurve": { + "signatureAlgorithm": "ECDSA_P384" + } + } + ], + "baselineValues": { + "additionalExtensions": [ + { + "objectId": { + "objectIdPath": [ + 1, + 7 + ] + }, + "value": "c3RyaW5nCg==" + } + ], + "aiaOcspServers": [ + "string" + ], + "caOptions": { + "isCa": false, + "maxIssuerPathLength": 7 + }, + "keyUsage": { + "unknownExtendedKeyUsages": [ + { + "objectIdPath": [ + 1, + 7 + ] + } + ] + }, + "policyIds": [ + { + "objectIdPath": [ + 1, + 7 + ] + } + ] + }, + "identityConstraints": { + "allowSubjectAltNamesPassthrough": false, + "allowSubjectPassthrough": false, + "celExpression": { + "description": "Always false", + "expression": "false", + "location": "devops.ca_pool.json", + "title": "Sample expression" + } + }, + "maximumLifetime": "43200s", + "passthroughExtensions": { + "additionalExtensions": [ + { + "objectIdPath": [ + 1, + 7 + ] + } + ], + "knownExtensions": [ + "BASE_KEY_USAGE" + ] + } + }, + "labels": { + "cnrm-test": "true", + "label-two": "value-two", + "managed-by-cnrm": "true" + }, + "name": "projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}", + "tier": "ENTERPRISE" +} + +--- + +DELETE https://privateca.googleapis.com/v1/projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}?alt=json +Content-Type: application/json +User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 + +200 OK +Cache-Control: private +Content-Type: application/json; charset=UTF-8 +Server: ESF +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Xss-Protection: 0 + +{ + "metadata": { + "@type": "type.googleapis.com/google.cloud.security.privateca.v1.OperationMetadata", + "apiVersion": "v1", + "createTime": "2024-04-01T12:34:56.123456Z", + "target": "projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}", + "verb": "delete" + }, + "name": "projects/${projectId}/locations/us-central1/operations/${operationID}" +} + +--- + +GET https://privateca.googleapis.com/v1/projects/${projectId}/locations/us-central1/operations/${operationID}?alt=json +Content-Type: application/json +User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 + +200 OK +Cache-Control: private +Content-Type: application/json; charset=UTF-8 +Server: ESF +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Xss-Protection: 0 + +{ + "done": true, + "metadata": { + "@type": "type.googleapis.com/google.cloud.security.privateca.v1.OperationMetadata", + "apiVersion": "v1", + "createTime": "2024-04-01T12:34:56.123456Z", + "endTime": "2024-04-01T12:34:56.123456Z", + "target": "projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}", + "verb": "delete" + }, + "name": "projects/${projectId}/locations/us-central1/operations/${operationID}", + "response": { + "@type": "type.googleapis.com/google.protobuf.Empty" + } +} + +--- + +GET https://privateca.googleapis.com/v1/projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}?alt=json +Content-Type: application/json +User-Agent: kcc/controller-manager DeclarativeClientLib/0.0.1 + +404 Not Found +Cache-Control: private +Content-Type: application/json; charset=UTF-8 +Server: ESF +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Xss-Protection: 0 + +{ + "error": { + "code": 404, + "message": "Resource 'projects/${projectId}/locations/us-central1/caPools/privatecacapool-${uniqueId}' was not found", + "status": "NOT_FOUND" + } +} + +--- + +GET https://iam.googleapis.com/v1/projects/${projectId}/serviceAccounts/capool-${uniqueId}@${projectId}.iam.gserviceaccount.com?alt=json&prettyPrint=false +User-Agent: google-api-go-client/0.5 Terraform/ (+https://www.terraform.io) Terraform-Plugin-SDK/2.10.1 terraform-provider-google-beta/kcc/controller-manager + +200 OK +Cache-Control: private +Content-Type: application/json; charset=UTF-8 +Server: ESF +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Xss-Protection: 0 + +{ + "displayName": "ExampleGSA", + "email": "capool-${uniqueId}@${projectId}.iam.gserviceaccount.com", + "etag": "abcdef0123A=", + "name": "projects/${projectId}/serviceAccounts/capool-${uniqueId}@${projectId}.iam.gserviceaccount.com", + "oauth2ClientId": "888888888888888888888", + "projectId": "${projectId}", + "uniqueId": "111111111111111111111" +} + +--- + +DELETE https://iam.googleapis.com/v1/projects/${projectId}/serviceAccounts/capool-${uniqueId}@${projectId}.iam.gserviceaccount.com?alt=json&prettyPrint=false +User-Agent: google-api-go-client/0.5 Terraform/ (+https://www.terraform.io) Terraform-Plugin-SDK/2.10.1 terraform-provider-google-beta/kcc/controller-manager + +200 OK +Cache-Control: private +Content-Type: application/json; charset=UTF-8 +Server: ESF +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Xss-Protection: 0 + +{} \ No newline at end of file diff --git a/pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/create.yaml b/pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/create.yaml new file mode 100644 index 0000000000..ef95aa99c2 --- /dev/null +++ b/pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/create.yaml @@ -0,0 +1,27 @@ +# Copyright 2022 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. + +apiVersion: iam.cnrm.cloud.google.com/v1beta1 +kind: IAMPolicyMember +metadata: + name: iampolicymember-${uniqueId} +spec: + memberFrom: + serviceAccountRef: + name: privatecacapool-dep + role: roles/privateca.admin + resourceRef: + apiVersion: privateca.cnrm.cloud.google.com/v1beta1 + kind: PrivateCACAPool + name: privatecacapool-${uniqueId} diff --git a/pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/dependencies.yaml b/pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/dependencies.yaml new file mode 100644 index 0000000000..3e39e2f121 --- /dev/null +++ b/pkg/test/resourcefixture/testdata/basic/privateca/v1beta1/privatecacapool/privatecacapooliam/dependencies.yaml @@ -0,0 +1,100 @@ +# Copyright 2022 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. + +apiVersion: iam.cnrm.cloud.google.com/v1beta1 +kind: IAMServiceAccount +metadata: + name: privatecacapool-dep +spec: + displayName: ExampleGSA + resourceID: capool-${uniqueId} + +--- + +apiVersion: privateca.cnrm.cloud.google.com/v1beta1 +kind: PrivateCACAPool +metadata: + labels: + label-two: "value-two" + name: privatecacapool-${uniqueId} +spec: + projectRef: + external: projects/${projectId} + location: "us-central1" + tier: ENTERPRISE + issuancePolicy: + allowedKeyTypes: + - rsa: + minModulusSize: 64 + maxModulusSize: 128 + - ellipticCurve: + signatureAlgorithm: ECDSA_P384 + maximumLifetime: 43200s + allowedIssuanceModes: + allowCsrBasedIssuance: true + allowConfigBasedIssuance: false + baselineValues: + keyUsage: + baseKeyUsage: + digitalSignature: false + contentCommitment: false + keyEncipherment: false + dataEncipherment: false + keyAgreement: false + certSign: false + crlSign: false + encipherOnly: false + decipherOnly: false + extendedKeyUsage: + serverAuth: false + clientAuth: false + codeSigning: false + emailProtection: false + timeStamping: false + ocspSigning: false + unknownExtendedKeyUsages: + - objectIdPath: + - 1 + - 7 + caOptions: + isCa: false + maxIssuerPathLength: 7 + policyIds: + - objectIdPath: + - 1 + - 7 + aiaOcspServers: + - string + additionalExtensions: + - objectId: + objectIdPath: + - 1 + - 7 + critical: false + value: c3RyaW5nCg== + identityConstraints: + celExpression: + title: Sample expression + description: Always false + expression: 'false' + location: devops.ca_pool.json + allowSubjectPassthrough: false + allowSubjectAltNamesPassthrough: false + passthroughExtensions: + knownExtensions: + - BASE_KEY_USAGE + additionalExtensions: + - objectIdPath: + - 1 + - 7