From 3c25704ec1c7a8e23d0a64a97b961fabb06d1fe4 Mon Sep 17 00:00:00 2001 From: justinsb Date: Mon, 14 Aug 2023 09:11:05 -0400 Subject: [PATCH 1/5] APIKeysKey: Create direct controller --- pkg/controller/config.go | 34 ++ .../direct/apikeys/apikeyskey_controller.go | 282 +++++++++++++ .../direct/apikeys/apikeyskey_test.go | 149 +++++++ pkg/controller/direct/apikeys/utils.go | 51 +++ pkg/controller/direct/debug/format.go | 40 ++ .../directbase/directbase_controller.go | 394 ++++++++++++++++++ .../direct/directbase/interfaces.go | 50 +++ pkg/controller/direct/mappings/assertions.go | 29 ++ pkg/controller/direct/mappings/build.go | 253 +++++++++++ pkg/controller/direct/mappings/convert.go | 93 +++++ pkg/controller/direct/mappings/field.go | 51 +++ pkg/controller/direct/mappings/fieldpath.go | 71 ++++ pkg/controller/direct/mappings/jsontag.go | 67 +++ pkg/controller/direct/mappings/map.go | 84 ++++ pkg/controller/direct/mappings/point.go | 68 +++ .../direct/mappings/protounionfield.go | 93 +++++ pkg/controller/direct/mappings/structfield.go | 82 ++++ pkg/controller/direct/mappings/type.go | 184 ++++++++ pkg/controller/direct/mappings/validation.go | 251 +++++++++++ pkg/controller/kccmanager/kccmanager.go | 24 +- .../mocktests/secretmanager_secret_test.go | 8 +- .../registration/registration_controller.go | 20 +- pkg/dcl/clientconfig/config.go | 21 +- 23 files changed, 2367 insertions(+), 32 deletions(-) create mode 100644 pkg/controller/config.go create mode 100644 pkg/controller/direct/apikeys/apikeyskey_controller.go create mode 100644 pkg/controller/direct/apikeys/apikeyskey_test.go create mode 100644 pkg/controller/direct/apikeys/utils.go create mode 100644 pkg/controller/direct/debug/format.go create mode 100644 pkg/controller/direct/directbase/directbase_controller.go create mode 100644 pkg/controller/direct/directbase/interfaces.go create mode 100644 pkg/controller/direct/mappings/assertions.go create mode 100644 pkg/controller/direct/mappings/build.go create mode 100644 pkg/controller/direct/mappings/convert.go create mode 100644 pkg/controller/direct/mappings/field.go create mode 100644 pkg/controller/direct/mappings/fieldpath.go create mode 100644 pkg/controller/direct/mappings/jsontag.go create mode 100644 pkg/controller/direct/mappings/map.go create mode 100644 pkg/controller/direct/mappings/point.go create mode 100644 pkg/controller/direct/mappings/protounionfield.go create mode 100644 pkg/controller/direct/mappings/structfield.go create mode 100644 pkg/controller/direct/mappings/type.go create mode 100644 pkg/controller/direct/mappings/validation.go diff --git a/pkg/controller/config.go b/pkg/controller/config.go new file mode 100644 index 0000000000..afea4241b7 --- /dev/null +++ b/pkg/controller/config.go @@ -0,0 +1,34 @@ +// 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 controller + +import "net/http" + +type Config struct { + UserAgent string + + // UserProjectOverride provides the option to use the resource project for preconditions, quota, and billing, + // instead of the project the credentials belong to; false by default + UserProjectOverride bool + + // BillingProject is the project used by the TF provider and DCL client to determine preconditions, + // quota, and billing if UserProjectOverride is set to true. If this field is empty, + // but UserProjectOverride is set to true, resource project will be used. + BillingProject string + + // HTTPClient allows us to specify the HTTP client to use with DCL. + // This is particularly useful in mocks/tests. + HTTPClient *http.Client +} diff --git a/pkg/controller/direct/apikeys/apikeyskey_controller.go b/pkg/controller/direct/apikeys/apikeyskey_controller.go new file mode 100644 index 0000000000..deb4a2c78f --- /dev/null +++ b/pkg/controller/direct/apikeys/apikeyskey_controller.go @@ -0,0 +1,282 @@ +// 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 apikeys + +import ( + "context" + "fmt" + "reflect" + + api "cloud.google.com/go/apikeys/apiv2" + pb "cloud.google.com/go/apikeys/apiv2/apikeyspb" + "google.golang.org/api/option" + "google.golang.org/protobuf/types/known/fieldmaskpb" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/manager" + + krm "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/apikeys/v1alpha1" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase" + + . "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/mappings" //nolint:revive +) + +// AddKeyReconciler creates a new controller and adds it to the Manager. +// The Manager will set fields on the Controller and start it when the Manager is started. +func AddKeyReconciler(mgr manager.Manager, config *controller.Config) error { + gvk := krm.APIKeysKeyGVK + + return directbase.Add(mgr, gvk, &model{config: *config}) +} + +type model struct { + config controller.Config +} + +// model implements the Model interface. +var _ directbase.Model = &model{} + +var keyMapping = NewMapping(&pb.Key{}, &krm.APIKeysKey{}, + Spec("displayName"), + Spec("restrictions"), + Status("uid"), + Ignore("createTime"), + Ignore("updateTime"), + Ignore("deleteTime"), + Ignore("etag"), + Ignore("name"), // TODO: Should be ResourceID? + Ignore("annotations"), // TODO: Should not ignore +). + MapNested(&pb.Restrictions{}, &krm.KeyRestrictions{}, "apiTargets", + "androidKeyRestrictions", "browserKeyRestrictions", "iosKeyRestrictions", "serverKeyRestrictions"). + MapNested(&pb.AndroidKeyRestrictions{}, &krm.KeyAndroidKeyRestrictions{}, "allowedApplications"). + MapNested(&pb.AndroidApplication{}, &krm.KeyAllowedApplications{}, "packageName", "sha1Fingerprint"). + MapNested(&pb.ApiTarget{}, &krm.KeyApiTargets{}, "methods", "service"). + MapNested(&pb.BrowserKeyRestrictions{}, &krm.KeyBrowserKeyRestrictions{}, "allowedReferrers"). + MapNested(&pb.IosKeyRestrictions{}, &krm.KeyIosKeyRestrictions{}, "allowedBundleIds"). + MapNested(&pb.ServerKeyRestrictions{}, &krm.KeyServerKeyRestrictions{}, "allowedIps"). + MustBuild() + +type adapter struct { + projectID string + location string + keyID string + + desired *krm.APIKeysKey + actual *krm.APIKeysKey + + gcp *api.Client +} + +// adapter implements the Adapter interface. +var _ directbase.Adapter = &adapter{} + +func (m *model) client(ctx context.Context) (*api.Client, error) { + var opts []option.ClientOption + if m.config.UserAgent != "" { + opts = append(opts, option.WithUserAgent(m.config.UserAgent)) + } + if m.config.HTTPClient != nil { + 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)) + // } + + gcpClient, err := api.NewRESTClient(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("building apikeys client: %w", err) + } + return gcpClient, err +} + +// AdapterForObject implements the Model interface. +func (m *model) AdapterForObject(ctx context.Context, u *unstructured.Unstructured) (directbase.Adapter, error) { + gcp, err := m.client(ctx) + if err != nil { + return nil, err + } + + // TODO: Just fetch this object? + obj := &krm.APIKeysKey{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil { + return nil, fmt.Errorf("error converting to %T: %w", obj, err) + } + + projectID := obj.Spec.ProjectRef.External + if projectID == "" { + return nil, fmt.Errorf("unable to determine project") + } + + // TODO: Use name or request resourceID to be set on create? + keyID := ValueOf(obj.Spec.ResourceID) + if keyID == "" { + return nil, fmt.Errorf("unable to determine resourceID") + } + + // This is a slightly unusual resource; the location is in the URL, + // but the location is always "global". + location := "global" + + return &adapter{ + projectID: projectID, + location: location, + keyID: keyID, + desired: obj, + gcp: gcp, + }, nil +} + +// Find implements the Adapter interface. +func (a *adapter) Find(ctx context.Context) (bool, error) { + if a.keyID == "" { + return false, nil + } + + req := &pb.GetKeyRequest{ + Name: a.fullyQualifiedName(), + } + key, err := a.gcp.GetKey(ctx, req) + if err != nil { + if IsNotFound(err) { + klog.Warningf("key was not found: %v", err) + return false, nil + } + return false, err + } + + u := &krm.APIKeysKey{} + if err := keyMapping.Map(key, u); err != nil { + return false, err + } + a.actual = u + + return true, nil +} + +// Delete implements the Adapter interface. +func (a *adapter) Delete(ctx context.Context) (bool, error) { + // TODO: Delete via status selfLink? + req := &pb.DeleteKeyRequest{ + Name: a.fullyQualifiedName(), + } + op, err := a.gcp.DeleteKey(ctx, req) + if err != nil { + if IsNotFound(err) { + return false, nil + } + return false, fmt.Errorf("deleting key: %w", err) + } + + if _, err := op.Wait(ctx); err != nil { + return false, fmt.Errorf("waiting for key deletion to complete: %w", err) + } + + return true, nil +} + +func (a *adapter) buildCreateRequest() (*pb.CreateKeyRequest, error) { + // You can configure only the `display_name`, `restrictions`, and + // `annotations` fields. + desired := &pb.Key{} + if err := keyMapping.Map(a.desired, desired); err != nil { + return nil, err + } + + return &pb.CreateKeyRequest{ + Parent: fmt.Sprintf("projects/%s/locations/%s", a.projectID, a.location), + KeyId: a.keyID, + Key: desired, + }, nil +} + +// Create implements the Adapter interface. +func (a *adapter) Create(ctx context.Context, u *unstructured.Unstructured) error { + log := klog.FromContext(ctx) + log.V(2).Info("creating object", "u", u) + + req, err := a.buildCreateRequest() + if err != nil { + return err + } + + op, err := a.gcp.CreateKey(ctx, req) + if err != nil { + return fmt.Errorf("creating key: %w", err) + } + // TODO: Is the resourceID returned if it is dynamically created? Maybe we should create the UUID? + created, err := op.Wait(ctx) + if err != nil { + return fmt.Errorf("waiting for key creation: %w", err) + } + log.V(2).Info("created key", "key", created) + // TODO: Return created object + return nil +} + +// Update implements the Adapter interface. +func (a *adapter) Update(ctx context.Context) (*unstructured.Unstructured, error) { + // TODO: Skip updates if no changes + // TODO: Where/how do we want to enforce immutability? + updateMask := &fieldmaskpb.FieldMask{} + + // TODO: I think we can do this with a helper + if !reflect.DeepEqual(a.desired.Spec.DisplayName, a.actual.Spec.DisplayName) { + updateMask.Paths = append(updateMask.Paths, "display_name") + } + if !reflect.DeepEqual(a.desired.Spec.Restrictions, a.actual.Spec.Restrictions) { + updateMask.Paths = append(updateMask.Paths, "restrictions") + } + + // TODO: Annotations + // if !reflect.DeepEqual(a.desired.Annotations, a.actual.Annotations) { + // updateMask.Paths = append(updateMask.Paths, "annotations") + // } + + if len(updateMask.Paths) == 0 { + klog.Warningf("unexpected empty update mask, desired: %v, actual: %v", a.desired, a.actual) + return nil, nil + } + + key := &pb.Key{} + if err := keyMapping.Map(a.desired, key); err != nil { + return nil, err + } + + req := &pb.UpdateKeyRequest{ + Key: key, + UpdateMask: updateMask, + } + + req.Key.Name = a.fullyQualifiedName() + + _, err := a.gcp.UpdateKey(ctx, req) + if err != nil { + return nil, err + } + // TODO: Return updated object + return nil, nil +} + +func (a *adapter) fullyQualifiedName() string { + return fmt.Sprintf("projects/%s/locations/%s/keys/%s", a.projectID, a.location, a.keyID) +} diff --git a/pkg/controller/direct/apikeys/apikeyskey_test.go b/pkg/controller/direct/apikeys/apikeyskey_test.go new file mode 100644 index 0000000000..0e3b179ccd --- /dev/null +++ b/pkg/controller/direct/apikeys/apikeyskey_test.go @@ -0,0 +1,149 @@ +// 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 apikeys + +import ( + "encoding/json" + "strings" + "testing" + + pb "cloud.google.com/go/apikeys/apiv2/apikeyspb" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/apikeys/v1alpha1" + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/testing/protocmp" + "sigs.k8s.io/yaml" +) + +func TestBuildCreate(t *testing.T) { + b := ` +apiVersion: apikeys.cnrm.cloud.google.com/v1alpha1 +kind: APIKeysKey +spec: + displayName: "Human readable name" + projectRef: + external: "projects/example-project" + resourceID: sample + restrictions: + serverKeyRestrictions: + allowedIps: + - 1.2.3.4 + - 5.6.7.8 +` + + u := &v1alpha1.APIKeysKey{} + if err := yaml.Unmarshal([]byte(b), u); err != nil { + t.Fatalf("error parsing object: %v", err) + } + t.Logf("u: %+v", u) + + a := &adapter{} + a.desired = u + a.projectID = "example-project" + a.location = "global" + req, err := a.buildCreateRequest() + if err != nil { + t.Fatalf("error building create: %v", err) + } + + j, err := protojson.Marshal(req) + if err != nil { + t.Fatalf("error converting proto to json: %v", err) + } + + out := make(map[string]any) + if err := json.Unmarshal(j, &out); err != nil { + t.Fatalf("error parsing json: %v", err) + } + + outYAML, err := yaml.Marshal(out) + if err != nil { + t.Fatalf("error marshaling yaml: %v", err) + } + + got := string(outYAML) + + want := ` +key: + displayName: Human readable name + restrictions: + serverKeyRestrictions: + allowedIps: + - 1.2.3.4 + - 5.6.7.8 +parent: projects/example-project/locations/global +` + + got = strings.TrimSpace(got) + want = strings.TrimSpace(want) + + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("unexpected diff: %v", diff) + } +} + +func TestMapping(t *testing.T) { + originalKRM := &v1alpha1.APIKeysKey{} + originalKRM.Spec.Restrictions = &v1alpha1.KeyRestrictions{ + AndroidKeyRestrictions: &v1alpha1.KeyAndroidKeyRestrictions{ + AllowedApplications: []v1alpha1.KeyAllowedApplications{ + { + Sha1Fingerprint: "sha1-fingerprint", + PackageName: "package-name", + }, + { + Sha1Fingerprint: "sha1-fingerprint-2", + PackageName: "package-name-2", + }, + }, + }, + } + + gotProto := &pb.Key{} + if err := keyMapping.Map(originalKRM, gotProto); err != nil { + t.Fatalf("error mapping: %v", err) + } + + wantProto := &pb.Key{ + Restrictions: &pb.Restrictions{ + ClientRestrictions: &pb.Restrictions_AndroidKeyRestrictions{ + AndroidKeyRestrictions: &pb.AndroidKeyRestrictions{ + AllowedApplications: []*pb.AndroidApplication{ + { + Sha1Fingerprint: "sha1-fingerprint", + PackageName: "package-name", + }, + { + Sha1Fingerprint: "sha1-fingerprint-2", + PackageName: "package-name-2", + }, + }, + }, + }, + }, + } + if diff := cmp.Diff(gotProto, wantProto, protocmp.Transform()); diff != "" { + t.Errorf("unexpected diff: %v", diff) + } + + gotKRM := &v1alpha1.APIKeysKey{} + if err := keyMapping.Map(gotProto, gotKRM); err != nil { + t.Fatalf("error mapping: %v", err) + } + + if diff := cmp.Diff(gotKRM, originalKRM); diff != "" { + t.Errorf("unexpected diff: %v", diff) + } +} diff --git a/pkg/controller/direct/apikeys/utils.go b/pkg/controller/direct/apikeys/utils.go new file mode 100644 index 0000000000..03ff82487e --- /dev/null +++ b/pkg/controller/direct/apikeys/utils.go @@ -0,0 +1,51 @@ +// 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 apikeys + +import ( + "errors" + + "github.com/googleapis/gax-go/v2/apierror" + "k8s.io/klog/v2" +) + +func ValueOf[T any](p *T) T { + var v T + if p != nil { + v = *p + } + return v +} + +// 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 +} + +// IsNotFound returns true if the given error is an HTTP 404. +func IsNotFound(err error) bool { + return HasHTTPCode(err, 404) +} diff --git a/pkg/controller/direct/debug/format.go b/pkg/controller/direct/debug/format.go new file mode 100644 index 0000000000..874e06b4c2 --- /dev/null +++ b/pkg/controller/direct/debug/format.go @@ -0,0 +1,40 @@ +// 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. + +package debug + +import ( + "encoding/json" + "fmt" +) + +// DeferredJSON is a helper that delays JSON formatting until/unless it is needed. +type DeferredJSON struct { + O interface{} +} + +// String is the method that is called to format the object. +func (d DeferredJSON) String() string { + b, err := json.Marshal(d.O) + if err != nil { + return fmt.Sprintf("", err) + } + return string(b) +} + +// JSON is a helper that prints the object in JSON format. +// We use lazy-evaluation to avoid calling json.Marshal unless it is actually needed. +func JSON(o interface{}) DeferredJSON { + return DeferredJSON{o} +} diff --git a/pkg/controller/direct/directbase/directbase_controller.go b/pkg/controller/direct/directbase/directbase_controller.go new file mode 100644 index 0000000000..7ac050c099 --- /dev/null +++ b/pkg/controller/direct/directbase/directbase_controller.go @@ -0,0 +1,394 @@ +// 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. + +package directbase + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + 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" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/predicate" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/ratelimiter" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/resourcewatcher" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/execution" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util" + + "golang.org/x/sync/semaphore" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + crcontroller "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +// Add creates a new controller for reconciling objects of the specified GVK, delegating actual resource reconciliation to the provided Model. +func Add(mgr manager.Manager, gvk schema.GroupVersionKind, model Model) error { + immediateReconcileRequests := make(chan event.GenericEvent, k8s.ImmediateReconcileRequestsBufferSize) + resourceWatcherRoutines := semaphore.NewWeighted(k8s.MaxNumResourceWatcherRoutines) + reconciler, err := NewReconciler(mgr, immediateReconcileRequests, resourceWatcherRoutines, gvk, model) + if err != nil { + return err + } + return add(mgr, reconciler) +} + +// NewReconciler returns a new reconcile.Reconciler. +func NewReconciler(mgr manager.Manager, immediateReconcileRequests chan event.GenericEvent, resourceWatcherRoutines *semaphore.Weighted, + gvk schema.GroupVersionKind, model Model) (*DirectReconciler, error) { + + controllerName := strings.ToLower(gvk.Kind) + "-controller" + + // var logger = log.Log.WithName(controllerName) + + r := DirectReconciler{ + LifecycleHandler: lifecyclehandler.NewLifecycleHandler( + mgr.GetClient(), + mgr.GetEventRecorderFor(controllerName), + ), + Client: mgr.GetClient(), + //iamClient: iamclient.New(provider, smLoader, mgr.GetClient(), converter, dclConfig), + config: mgr.GetConfig(), + immediateReconcileRequests: immediateReconcileRequests, + resourceWatcherRoutines: resourceWatcherRoutines, + scheme: mgr.GetScheme(), + gvk: gvk, + model: model, + controllerName: controllerName, + ReconcilerMetrics: metrics.ReconcilerMetrics{ + ResourceNameLabel: metrics.ResourceNameLabel, + }, + } + return &r, nil +} + +// add adds a new Controller to mgr with r as the reconcile.Reconciler. +func add(mgr manager.Manager, r *DirectReconciler) error { + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(r.gvk) + + _, err := builder. + ControllerManagedBy(mgr). + Named(r.controllerName). + WithOptions(crcontroller.Options{MaxConcurrentReconciles: k8s.ControllerMaxConcurrentReconciles, RateLimiter: ratelimiter.NewRateLimiter()}). + WatchesRawSource(&source.Channel{Source: r.immediateReconcileRequests}, &handler.EnqueueRequestForObject{}). + For(obj, builder.OnlyMetadata, builder.WithPredicates(predicate.UnderlyingResourceOutOfSyncPredicate{})). + Build(r) + if err != nil { + return fmt.Errorf("error creating new controller: %w", err) + } + return nil +} + +var _ reconcile.Reconciler = &DirectReconciler{} + +// DirectReconciler is a reconciler for reconciling resources that support the Model/Adapter pattern. +// It is currently an adaptation of the existing terraform based-reconciler, and thus uses things like k8s.Resource. +// TODO: Move away from k8s.Resource to unstructured.Unstructured. +type DirectReconciler struct { + lifecyclehandler.LifecycleHandler + client.Client + metrics.ReconcilerMetrics + gvk schema.GroupVersionKind + model Model + scheme *runtime.Scheme + config *rest.Config + // Fields used for triggering reconciliations when dependencies are ready + immediateReconcileRequests chan event.GenericEvent + resourceWatcherRoutines *semaphore.Weighted // Used to cap number of goroutines watching unready dependencies + + controllerName string +} + +type reconcileContext struct { + gvk schema.GroupVersionKind + Reconciler *DirectReconciler + NamespacedName types.NamespacedName +} + +// Reconcile checks k8s for the current state of the resource. +func (r *DirectReconciler) Reconcile(ctx context.Context, request reconcile.Request) (result reconcile.Result, err error) { + logger := log.FromContext(ctx) + + logger.Info("Running reconcile", "resource", request.NamespacedName) + startTime := time.Now() + ctx, cancel := context.WithTimeout(ctx, k8s.ReconcileDeadline) + defer cancel() + r.RecordReconcileWorkers(ctx, r.gvk) + defer r.AfterReconcile() + defer r.RecordReconcileMetrics(ctx, r.gvk, request.Namespace, request.Name, startTime, &err) + + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(r.gvk) + if err := r.Get(ctx, request.NamespacedName, obj); err != nil { + if apierrors.IsNotFound(err) { + // Object not found, return. Created objects are automatically garbage collected. + // For additional cleanup logic use finalizers. + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, err + } + runCtx := &reconcileContext{ + Reconciler: r, + gvk: r.gvk, + NamespacedName: request.NamespacedName, + } + requeue, err := runCtx.doReconcile(ctx, obj) + if err != nil { + return reconcile.Result{}, err + } + if requeue { + return reconcile.Result{Requeue: true}, nil + } + jitteredPeriod, err := jitter.GenerateJitteredReenqueuePeriod(r.gvk, nil, nil, obj) + if err != nil { + return reconcile.Result{}, err + } + logger.Info("successfully finished reconcile", "resource", request.NamespacedName, "time to next reconciliation", jitteredPeriod) + return reconcile.Result{RequeueAfter: jitteredPeriod}, nil +} + +func (r *reconcileContext) doReconcile(ctx context.Context, u *unstructured.Unstructured) (requeue bool, err error) { + logger := log.FromContext(ctx) + + adapter, err := r.Reconciler.model.AdapterForObject(ctx, u) + if err != nil { + return false, r.handleUpdateFailed(ctx, u, err) + } + + defer execution.RecoverWithInternalError(&err) + if !u.GetDeletionTimestamp().IsZero() { + if !k8s.HasFinalizer(u, k8s.ControllerFinalizerName) { + // Resource has no controller finalizer; no finalization necessary + return false, nil + } + if k8s.HasFinalizer(u, k8s.DeletionDefenderFinalizerName) { + // deletion defender has not yet been finalized; requeuing + logger.Info("deletion defender has not yet been finalized; requeuing", "resource", k8s.GetNamespacedName(u)) + return true, nil + } + if !k8s.HasAbandonAnnotation(u) { + if _, err := adapter.Delete(ctx); err != nil { + if !errors.Is(err, kcciamclient.ErrNotFound) && !k8s.IsReferenceNotFoundError(err) { + if unwrappedErr, ok := lifecyclehandler.CausedByUnresolvableDeps(err); ok { + logger.Info(unwrappedErr.Error(), "resource", k8s.GetNamespacedName(u)) + resource, err := toK8sResource(u) + if err != nil { + return false, fmt.Errorf("error converting k8s resource while handling unresolvable dependencies event: %w", err) + } + // Requeue resource for reconciliation with exponential backoff applied + return true, r.Reconciler.HandleUnresolvableDeps(ctx, resource, unwrappedErr) + } + return false, r.handleDeleteFailed(ctx, u, err) + } + } + } + return false, r.handleDeleted(ctx, u) + } + + existsAlready, err := adapter.Find(ctx) + if err != nil { + if unwrappedErr, ok := lifecyclehandler.CausedByUnresolvableDeps(err); ok { + logger.Info(unwrappedErr.Error(), "resource", k8s.GetNamespacedName(u)) + return r.handleUnresolvableDeps(ctx, u, unwrappedErr) + } + return false, r.handleUpdateFailed(ctx, u, err) + } + k8s.EnsureFinalizers(u, k8s.ControllerFinalizerName, k8s.DeletionDefenderFinalizerName) + + // set the etag to an empty string, since IAMPolicy is the authoritative intent, KCC wants to overwrite the underlying policy regardless + //policy.Spec.Etag = "" + + if !existsAlready { + if err := adapter.Create(ctx, u); err != nil { + if unwrappedErr, ok := lifecyclehandler.CausedByUnresolvableDeps(err); ok { + logger.Info(unwrappedErr.Error(), "resource", k8s.GetNamespacedName(u)) + return r.handleUnresolvableDeps(ctx, u, unwrappedErr) + } + return false, r.handleUpdateFailed(ctx, u, fmt.Errorf("error creating: %w", err)) + } + } else { + if _, err = adapter.Update(ctx); err != nil { + if unwrappedErr, ok := lifecyclehandler.CausedByUnresolvableDeps(err); ok { + logger.Info(unwrappedErr.Error(), "resource", k8s.GetNamespacedName(u)) + return r.handleUnresolvableDeps(ctx, u, unwrappedErr) + } + return false, r.handleUpdateFailed(ctx, u, fmt.Errorf("error updating: %w", err)) + } + } + if isAPIServerUpdateRequired(u) { + return false, r.handleUpToDate(ctx, u) + } + return false, nil +} + +func (r *reconcileContext) handleUpToDate(ctx context.Context, u *unstructured.Unstructured) error { + resource, err := toK8sResource(u) + if err != nil { + return fmt.Errorf("error converting to k8s resource while handling %v event: %w", k8s.UpToDate, err) + } + return r.Reconciler.HandleUpToDate(ctx, resource) +} + +func (r *reconcileContext) handleUpdateFailed(ctx context.Context, policy *unstructured.Unstructured, origErr error) error { + logger := log.FromContext(ctx) + + resource, err := toK8sResource(policy) + if err != nil { + logger.Error(err, "error converting to k8s resource while handling event", + "resource", k8s.GetNamespacedName(policy), "event", k8s.UpdateFailed) + return fmt.Errorf("Update call failed: %w", origErr) + } + return r.Reconciler.HandleUpdateFailed(ctx, resource, origErr) +} + +func (r *reconcileContext) handleDeleted(ctx context.Context, policy *unstructured.Unstructured) error { + resource, err := toK8sResource(policy) + if err != nil { + return fmt.Errorf("error converting to k8s resource while handling %v event: %w", k8s.Deleted, err) + } + return r.Reconciler.HandleDeleted(ctx, resource) +} + +func (r *reconcileContext) handleDeleteFailed(ctx context.Context, policy *unstructured.Unstructured, origErr error) error { + logger := log.FromContext(ctx) + + resource, err := toK8sResource(policy) + if err != nil { + logger.Error(err, "error converting to k8s resource while handling event", + "resource", k8s.GetNamespacedName(policy), "event", k8s.DeleteFailed) + return fmt.Errorf(k8s.DeleteFailedMessageTmpl, origErr) + } + return r.Reconciler.HandleDeleteFailed(ctx, resource, origErr) +} + +func (r *DirectReconciler) supportsImmediateReconciliations() bool { + return r.immediateReconcileRequests != nil +} + +func (r *reconcileContext) handleUnresolvableDeps(ctx context.Context, policy *unstructured.Unstructured, origErr error) (requeue bool, err error) { + logger := log.FromContext(ctx) + + resource, err := toK8sResource(policy) + if err != nil { + return false, fmt.Errorf("error converting to k8s resource while handling unresolvable dependencies event: %w", err) + } + refGVK, refNN, ok := lifecyclehandler.CausedByUnreadyOrNonexistentResourceRefs(origErr) + if !ok || !r.Reconciler.supportsImmediateReconciliations() { + // Requeue resource for reconciliation with exponential backoff applied + return true, r.Reconciler.HandleUnresolvableDeps(ctx, resource, origErr) + } + // Check that the number of active resource watches + // does not exceed the controller's cap. If the + // capacity is not exceeded, The number of active + // resource watches is incremented by one and a watch + // is started + if !r.Reconciler.resourceWatcherRoutines.TryAcquire(1) { + // Requeue resource for reconciliation with exponential backoff applied + return true, r.Reconciler.HandleUnresolvableDeps(ctx, resource, origErr) + } + // Create a logger for ResourceWatcher that contains info + // about the referencing resource. This is done since the + // messages logged by ResourceWatcher only include the + // information of the resource it is watching by default. + watcherLogger := logger.WithValues( + "referencingResource", resource.GetNamespacedName(), + "referencingResourceGVK", resource.GroupVersionKind()) + watcher, err := resourcewatcher.New(r.Reconciler.config, watcherLogger) + if err != nil { + return false, r.Reconciler.HandleUpdateFailed(ctx, resource, fmt.Errorf("error initializing new resourcewatcher: %w", err)) + } + + logger = logger.WithValues( + "resource", resource.GetNamespacedName(), + "resourceGVK", resource.GroupVersionKind(), + "reference", refNN, + "referenceGVK", refGVK) + go func() { + // Decrement the count of active resource watches after + // the watch finishes + defer r.Reconciler.resourceWatcherRoutines.Release(1) + timeoutPeriod := jitter.GenerateWatchJitteredTimeoutPeriod() + ctx, cancel := context.WithTimeout(context.TODO(), timeoutPeriod) + defer cancel() + logger.Info("starting wait with timeout on resource's reference", "timeout", timeoutPeriod) + if err := watcher.WaitForResourceToBeReady(ctx, refNN, refGVK); err != nil { + logger.Error(err, "error while waiting for resource's reference to be ready") + return + } + logger.Info("enqueuing resource for immediate reconciliation now that its reference is ready") + r.Reconciler.enqueueForImmediateReconciliation(resource.GetNamespacedName()) + }() + + // Do not requeue resource for immediate reconciliation. Wait for either + // the next periodic reconciliation or for the referenced resource to be ready (which + // triggers a reconciliation), whichever comes first. + return false, r.Reconciler.HandleUnresolvableDeps(ctx, resource, origErr) +} + +// enqueueForImmediateReconciliation enqueues the given resource for immediate +// reconciliation. Note that this function only takes in the name and namespace +// of the resource and not its GVK since the controller instance that this +// reconcile instance belongs to can only reconcile resources of one GVK. +func (r *DirectReconciler) enqueueForImmediateReconciliation(resourceNN types.NamespacedName) { + genEvent := event.GenericEvent{} + genEvent.Object = &unstructured.Unstructured{} + genEvent.Object.SetNamespace(resourceNN.Namespace) + genEvent.Object.SetName(resourceNN.Name) + r.immediateReconcileRequests <- genEvent +} + +func isAPIServerUpdateRequired(_ *unstructured.Unstructured) bool { + // TODO: even in the event of an actual update to GCP, this function will + // return false because the condition comparison doesn't account for time. + // conditions := []condition.Condition{ + // k8s.NewCustomReadyCondition(corev1.ConditionTrue, k8s.UpToDate, k8s.UpToDateMessage), + // } + + // TODO: Implement these checks + return true + // if !k8s.ConditionSlicesEqual(policy.Status.Conditions, conditions) { + // return true + // } + // if policy.Status.ObservedGeneration != policy.GetGeneration() { + // return true + // } + // return false +} + +func toK8sResource(policy *unstructured.Unstructured) (*k8s.Resource, error) { + resource := k8s.Resource{} + if err := util.Marshal(policy, &resource); err != nil { + return nil, fmt.Errorf("error marshalling to k8s resource: %w", err) + } + return &resource, nil +} diff --git a/pkg/controller/direct/directbase/interfaces.go b/pkg/controller/direct/directbase/interfaces.go new file mode 100644 index 0000000000..9a4a305c13 --- /dev/null +++ b/pkg/controller/direct/directbase/interfaces.go @@ -0,0 +1,50 @@ +// 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 directbase + +import ( + "context" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// Model is the entry-point for our per-object reconcilers +type Model interface { + // AdapterForObject builds an operation object for reconciling the object u + AdapterForObject(ctx context.Context, u *unstructured.Unstructured) (Adapter, error) +} + +// Adapter performs a single reconciliation on a single object. +// It is built using AdapterForObject. +type Adapter interface { + // Delete removes the GCP object. + // This can be called without calling Find. + // It returns (true, nil) if the object was deleted, + // and (false, nil) if the object was not found but should be presumed deleted. + // In an error, the state is not fully determined - a delete might be in progress. + Delete(ctx context.Context) (deleted bool, err error) + + // Find must be called as the first operation (unless we are deleting). + // It returns whether the corresponding GCP object was found. + Find(ctx context.Context) (found bool, err error) + + // Create creates a new GCP object. + // This should only be called when Find has previously returned false. + Create(ctx context.Context, u *unstructured.Unstructured) error + + // Update updates an existing GCP object. + // This should only be called when Find has previously returned true. + Update(ctx context.Context) (*unstructured.Unstructured, error) +} diff --git a/pkg/controller/direct/mappings/assertions.go b/pkg/controller/direct/mappings/assertions.go new file mode 100644 index 0000000000..96357fbd8a --- /dev/null +++ b/pkg/controller/direct/mappings/assertions.go @@ -0,0 +1,29 @@ +// 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 mappings + +import "k8s.io/klog/v2" + +// AssertEnabled is true if we are running with assertions enabled. +// Like traditional assertions, assertions are runtime checks that +// will panic the program, but we only run those checks if assertion +// checks are enabled. +var AssertEnabled = true + +func AssertFail() { + if AssertEnabled { + klog.Fatalf("assertion failed") + } +} diff --git a/pkg/controller/direct/mappings/build.go b/pkg/controller/direct/mappings/build.go new file mode 100644 index 0000000000..6035b2a404 --- /dev/null +++ b/pkg/controller/direct/mappings/build.go @@ -0,0 +1,253 @@ +// 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 mappings + +import ( + "errors" + "fmt" + "os" + "reflect" + + "k8s.io/klog/v2" +) + +// Mapping holds the logic for mapping a particular resource, +// including sub-object types. +type Mapping struct { + ResourceCloudType *reflectType + ResourceKRMType *reflectType + + Mappings []typeMapping +} + +// Map will convert an object from "in" to "out", mapping object subfields recurisvely. +func (m *Mapping) Map(in any, out any) error { + inVal := reflect.ValueOf(in) + outVal := reflect.ValueOf(out) + + inPoint := m.newPoint(inVal) + outPoint := m.newPoint(outVal) + + for _, typeMapping := range m.Mappings { + if typeMapping.FromType() != inPoint.t.rt { + continue + } + + if typeMapping.ToType() != outPoint.t.rt { + continue + } + + return typeMapping.Map(inPoint, outPoint) + } + + for _, typeMapping := range m.Mappings { + klog.Infof("mapping from %q -> %q", typeMapping.FromType(), typeMapping.ToType()) + } + + return fmt.Errorf("type mapping not found for %q -> %q", inPoint.t, outPoint.t) +} + +// FieldMapping is the base interface for per-field mappings. +type FieldMapping interface { + // TODO: We should have something here, because right now everything is a FieldMapping + // BuildFieldMapping(m *TypeMapping) (*fieldMapping, error) +} + +// statusField describes a mapping from a proto top-level field into a KRM status field. +type statusField struct { + ID string +} + +// Status fields are a proto top-level field but nested under Status in KRM. +func Status(id string) FieldMapping { + return &statusField{ID: id} +} + +// statusField describes a mapping from a proto top-level field into a KRM spec field. +type specField struct { + ID string +} + +// Status fields are a proto top-level field but nested under Spec in KRM. +func Spec(id string) FieldMapping { + return &specField{ID: id} +} + +// ignoreField describes a field that should be actively ignored, we know it should not be automatically mapped. +// The machinery may still process it directly. +type ignoreField struct { + ID FieldID +} + +// Ignore fields should not be mapped automatically. +// They are different from TODO fields, in that we have actively determined they should not be mapped. +func Ignore(id string) FieldMapping { + return &ignoreField{ID: toFieldID(id)} +} + +// MappingBuilder allows for fluid construction of a Mapping +type MappingBuilder struct { + mapping *Mapping + errors []error +} + +// Build "finalizes" the mapping, and returns the constructed mapping. +func (b *MappingBuilder) Build() (*Mapping, error) { + if len(b.errors) != 0 { + return nil, errors.Join(b.errors...) + } + + if AssertEnabled { + errs := b.mapping.Validate() + if len(errs) != 0 { + for _, err := range errs { + fmt.Fprintf(os.Stderr, "%v\n", err.Message) + if err.Proposal != "" { + fmt.Fprintf(os.Stderr, " %v\n", err.Proposal) + } + } + AssertFail() + } + } + + return b.mapping, nil +} + +// MustBuild is like Build, but panics on error. +func (b *MappingBuilder) MustBuild() *Mapping { + m, err := b.Build() + if err != nil { + klog.Fatalf("error building mapping: %v", err) + } + return m +} + +// NewMapping starts a new mappingBuilder, for converting proto <-> KRM. +func NewMapping(cloudObj any, krmObj any, fields ...FieldMapping) *MappingBuilder { + cloudVal := reflect.ValueOf(cloudObj) + krmVal := reflect.ValueOf(krmObj) + resourceCloudType := typeOf(cloudVal.Type()) + resourceKRMType := typeOf(krmVal.Type()) + + m := &Mapping{ + ResourceCloudType: resourceCloudType, + ResourceKRMType: resourceKRMType, + } + + b := &MappingBuilder{ + mapping: m, + } + + b = b.addKRMToCloudMapping(resourceKRMType, resourceCloudType, true, fields...) + b = b.addCloudToKRMMappin(resourceCloudType, resourceKRMType, true, fields...) + return b + +} + +// MapNested describes how a nested subobject should be mapped, when it is encountered in this context. +func (b *MappingBuilder) MapNested(cloudObj any, krmObj any, fields ...FieldMapping) *MappingBuilder { + cloudVal := reflect.ValueOf(cloudObj) + krmVal := reflect.ValueOf(krmObj) + resourceCloudType := typeOf(cloudVal.Type()) + resourceKRMType := typeOf(krmVal.Type()) + + b = b.addKRMToCloudMapping(resourceKRMType, resourceCloudType, false, fields...) + b = b.addCloudToKRMMappin(resourceCloudType, resourceKRMType, false, fields...) + return b +} + +// addKRMToCloudMapping will add a mapping for mapping from KRM to cloud objects. +func (b *MappingBuilder) addKRMToCloudMapping(inType *reflectType, outType *reflectType, hasSpecStatus bool, fields ...FieldMapping) *MappingBuilder { + createMapping := &structTypeMapping{ + scope: b.mapping, + inType: inType, + outType: outType, + hasSpecStatus: hasSpecStatus, + } + b.mapping.Mappings = append(b.mapping.Mappings, createMapping) + + for _, field := range fields { + switch field := field.(type) { + case *specField: + createMapping.fields = append(createMapping.fields, &fieldMapping{ + InPath: parseFieldPath("spec." + field.ID), + OutPath: parseFieldPath(field.ID), + }) + + case *statusField: + createMapping.fields = append(createMapping.fields, &fieldMapping{ + InPath: parseFieldPath("status." + field.ID), + OutPath: parseFieldPath(field.ID), + }) + + // For simple 1:1 mappings, we can just accept a string + case string: + createMapping.fields = append(createMapping.fields, &fieldMapping{ + InPath: parseFieldPath(field), + OutPath: parseFieldPath(field), + }) + + case *ignoreField: + createMapping.ignore = append(createMapping.ignore, field) + + default: + klog.Fatalf("unhandled field type %T", field) + } + } + + return b +} + +// addCloudToKRMMappin will add a mapping for mapping from Cloud to KRM objects. +func (b *MappingBuilder) addCloudToKRMMappin(inType *reflectType, outType *reflectType, hasSpecStatus bool, fields ...FieldMapping) *MappingBuilder { + createMapping := &structTypeMapping{ + scope: b.mapping, + inType: inType, + outType: outType, + hasSpecStatus: hasSpecStatus, + } + b.mapping.Mappings = append(b.mapping.Mappings, createMapping) + + for _, field := range fields { + switch field := field.(type) { + case *specField: + createMapping.fields = append(createMapping.fields, &fieldMapping{ + InPath: parseFieldPath(field.ID), + OutPath: parseFieldPath("spec." + field.ID), + }) + + case *statusField: + createMapping.fields = append(createMapping.fields, &fieldMapping{ + InPath: parseFieldPath(field.ID), + OutPath: parseFieldPath("status." + field.ID), + }) + + // For simple 1:1 mappings, we can just accept a string + case string: + createMapping.fields = append(createMapping.fields, &fieldMapping{ + InPath: parseFieldPath(field), + OutPath: parseFieldPath(field), + }) + + case *ignoreField: + createMapping.ignore = append(createMapping.ignore, field) + + default: + klog.Fatalf("unhandled field type %T", field) + } + } + + return b +} diff --git a/pkg/controller/direct/mappings/convert.go b/pkg/controller/direct/mappings/convert.go new file mode 100644 index 0000000000..4a3b950f71 --- /dev/null +++ b/pkg/controller/direct/mappings/convert.go @@ -0,0 +1,93 @@ +// 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 mappings + +import ( + "fmt" + "reflect" +) + +// convert will convert from the value src to destType, supporting assignment to a field of destType. +func (m *Mapping) convert(src reflect.Value, destType reflect.Type) (reflect.Value, error) { + if src.Kind() == reflect.Pointer { + if src.IsNil() { + // Nothing to set + return reflect.Value{}, nil + } + src = src.Elem() + } + + if src.Kind() == reflect.Slice { + if src.IsNil() { + // Nothing to set + return reflect.Value{}, nil + } + dest := reflect.New(destType).Elem() + n := src.Len() + for i := 0; i < n; i++ { + srcElem := src.Index(i) + destElem, err := m.convert(srcElem, destType.Elem()) + if err != nil { + return reflect.Value{}, fmt.Errorf("converting slice element: %w", err) + } + // TODO: What if destElem is not valid + dest = reflect.Append(dest, destElem) + } + return dest, nil + } + + srcType := src.Type() + + switch srcType.String() { + case "string": + v := src.String() + switch destType.String() { + case "string": + return reflect.ValueOf(v), nil + case "*string": + // When copying to an optional string, skip empty values + if v == "" { + return reflect.Value{}, nil + } + return reflect.ValueOf(&v), nil + } + } + + if src.CanInterface() { + srcVal := src + if srcVal.Kind() == reflect.Struct { + if !srcVal.CanAddr() { + return reflect.Value{}, fmt.Errorf("cannot address struct") + } + srcVal = srcVal.Addr() + } + var destVal reflect.Value + if destType.Kind() == reflect.Pointer { + destVal = reflect.New(destType.Elem()) + } else { + destVal = reflect.New(destType) + } + if err := m.Map(srcVal.Interface(), destVal.Interface()); err != nil { + return reflect.Value{}, err + } + // Match pointer/non-pointer + if destType.Kind() != reflect.Pointer { + destVal = destVal.Elem() + } + return destVal, nil + } + + return reflect.Value{}, fmt.Errorf("conversion from %v to %v not implemented", srcType.String(), destType.String()) +} diff --git a/pkg/controller/direct/mappings/field.go b/pkg/controller/direct/mappings/field.go new file mode 100644 index 0000000000..432c0c497b --- /dev/null +++ b/pkg/controller/direct/mappings/field.go @@ -0,0 +1,51 @@ +// 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 mappings + +import "reflect" + +// Field represents a field in an object type. +type Field interface { + // ID returns the unique ID of the field, typically the value used in JSON serialization. + ID() FieldID + + // Type returns the type of the field, using our reflectType mapper. + Type() *reflectType + + // getValue returns a handle to the field value of the specified parent. + getValue(parent *point) *point + + // setValue sets + setValue(parent *point, src reflect.Value) error + + // getJSONKey returns the key we will normally use for JSON serialization. + // This is normally the same as the ID value. + // This is used for suggesting a go field declaration in validation. + getJSONKey() string + + // isRequired returns true if the field is required in JSON. + // This is used for suggesting a go field declaration in validation. + isRequired() bool +} + +// FieldID is the unique identifier for a field within an object type. +type FieldID string + +// toFieldID converts a string to a FieldID. +func toFieldID(id string) FieldID { + // TODO: Do we want to normalize this here? + // return FieldID(strings.ToLower(id)) + return FieldID(id) +} diff --git a/pkg/controller/direct/mappings/fieldpath.go b/pkg/controller/direct/mappings/fieldpath.go new file mode 100644 index 0000000000..53bc86a874 --- /dev/null +++ b/pkg/controller/direct/mappings/fieldpath.go @@ -0,0 +1,71 @@ +// 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 mappings + +import ( + "fmt" + "reflect" + "strings" +) + +// fieldPath is a sequence of field ids, forming a nested path such as `spec.size` +type fieldPath struct { + fields []FieldID +} + +// newFieldPath builds a fieldPath. +func newFieldPath(fields ...FieldID) *fieldPath { + return &fieldPath{fields: fields} +} + +// String returns a user-friendly value. +func (f *fieldPath) String() string { + var s []string + for _, v := range f.fields { + s = append(s, string(v)) + } + return strings.Join(s, ".") +} + +// FindPoint will return a point representing a nested child field of parent +func (f *fieldPath) FindPoint(parent *point) *point { + p := parent + for _, field := range f.fields { + p = p.Child(field) + } + return p +} + +// SetValue will set the nested field in parent to the value v +func (f *fieldPath) SetValue(parent *point, v reflect.Value) error { + p := parent + n := len(f.fields) + for i := 0; i < n-1; i++ { + p = p.Child(f.fields[i]) + if p == nil { + return fmt.Errorf("unable to find path %v", f) + } + } + return p.SetValue(f.fields[n-1], v) +} + +// parseFieldPath builds a new fieldPath +func parseFieldPath(s string) *fieldPath { + var out []FieldID + for _, v := range strings.Split(s, ".") { + out = append(out, toFieldID(v)) + } + return newFieldPath(out...) +} diff --git a/pkg/controller/direct/mappings/jsontag.go b/pkg/controller/direct/mappings/jsontag.go new file mode 100644 index 0000000000..7eeb03187d --- /dev/null +++ b/pkg/controller/direct/mappings/jsontag.go @@ -0,0 +1,67 @@ +// 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 mappings + +import ( + "reflect" + "strings" + "unicode" +) + +// getJSONFieldTag is a helper to extract the json key for a field, from the reflection StructField. +func getJSONFieldTag(f *reflect.StructField) string { + // Handle protobuf tags. + // example: `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" + protoTag := f.Tag.Get("protobuf") + for _, v := range strings.Split(protoTag, ",") { + if strings.HasPrefix(v, "json=") { + return strings.TrimPrefix(v, "json=") + } + } + + jsonTag := f.Tag.Get("json") + switch jsonTag { + case "-": + // Not marshaled + return "" + + case "": + break + + default: + parts := strings.Split(jsonTag, ",") + name := parts[0] + if name == "" { + name = f.Name + } + return name + } + + name := f.Name + return fallbackGoFieldToFieldID(name) +} + +// fallbackGoFieldToFieldID converts a go field into the json equivalent. +func fallbackGoFieldToFieldID(fieldName string) string { + var out []rune + for i, r := range fieldName { + if i == 0 { + r = unicode.ToLower(r) + } + out = append(out, r) + } + s := string(out) + return s +} diff --git a/pkg/controller/direct/mappings/map.go b/pkg/controller/direct/mappings/map.go new file mode 100644 index 0000000000..8aa3101764 --- /dev/null +++ b/pkg/controller/direct/mappings/map.go @@ -0,0 +1,84 @@ +// 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 mappings + +import "reflect" + +// typeMapping is a mapping from one type to another. +type typeMapping interface { + // FromType returns the type we will convert from + FromType() reflect.Type + // ToType returns the type we will convert to. + ToType() reflect.Type + + // Map performs the actual converstion from one value to another. + Map(in *point, out *point) error +} + +// structTypeMapping is a typeMapping for mapping between struct fields +type structTypeMapping struct { + //scope is the parent Mapping context of which we are part. + scope *Mapping + + // inType is the type we will convert from. + inType *reflectType + // outType is the type we will convert to. + outType *reflectType + + // hasSpecStatus is a hint for validation that this mapping is a top-level mapping. + hasSpecStatus bool + + // fields defines the mapping of fields + fields []*fieldMapping + + // ignore is a list of fields we should ignore. + // This is kept around for validation. + ignore []*ignoreField +} + +var _ typeMapping = &structTypeMapping{} + +// FromType implements typeMapping +func (m *structTypeMapping) FromType() reflect.Type { + return m.inType.rt +} + +// ToType implements typeMapping +func (m *structTypeMapping) ToType() reflect.Type { + return m.outType.rt +} + +// fieldMapping describes the field we should read in the input, and write in the output. +// In future, we could add value transformations here, but we haven't needed one yet. +type fieldMapping struct { + InPath *fieldPath + OutPath *fieldPath +} + +// Map implements typeMapping +func (m *structTypeMapping) Map(in *point, out *point) error { + for _, mapping := range m.fields { + inPoint := mapping.InPath.FindPoint(in) + + srcVal := inPoint.GetValue() + + if err := mapping.OutPath.SetValue(out, srcVal); err != nil { + return err + } + + } + + return nil +} diff --git a/pkg/controller/direct/mappings/point.go b/pkg/controller/direct/mappings/point.go new file mode 100644 index 0000000000..bb52f49316 --- /dev/null +++ b/pkg/controller/direct/mappings/point.go @@ -0,0 +1,68 @@ +// 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 mappings + +import ( + "fmt" + "reflect" + + "k8s.io/klog/v2" +) + +// point holds a pointer to a field in an object, ensuring we can set it. +type point struct { + scope *Mapping + rv reflect.Value + t *reflectType +} + +// newPoint builds a new point for the specified reflect.Value. +func (m *Mapping) newPoint(rv reflect.Value) *point { + t := typeOf(rv.Type()) + + return &point{ + scope: m, + rv: rv, + t: t, + } +} + +// Child gets a point for the specified child field. +func (p *point) Child(id FieldID) *point { + if p == nil { + return nil + } + field := p.t.findField(id) + if field == nil { + klog.Warningf("unable to find field %q in %v", id, p.t) + return nil + } + return field.getValue(p) +} + +// SetValue sets the value of the specified child field. +func (p *point) SetValue(field FieldID, value reflect.Value) error { + f := p.t.findField(field) + if f == nil { + return fmt.Errorf("unable to find field %q in %v", field, p.t) + } + + return f.setValue(p, value) +} + +// GetValue gets the current value of the point. +func (p *point) GetValue() reflect.Value { + return p.rv +} diff --git a/pkg/controller/direct/mappings/protounionfield.go b/pkg/controller/direct/mappings/protounionfield.go new file mode 100644 index 0000000000..9967f17f55 --- /dev/null +++ b/pkg/controller/direct/mappings/protounionfield.go @@ -0,0 +1,93 @@ +// 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 mappings + +import ( + "fmt" + "reflect" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// protoOneOfField is a Field that is backed by a oneof value in proto. +type protoOneOfField struct { + t *reflectType + jsonKey string + fd protoreflect.FieldDescriptor +} + +var _ Field = &protoOneOfField{} + +// ID implements the Field interface. +func (f *protoOneOfField) ID() FieldID { + fieldID := toFieldID(f.jsonKey) + return fieldID +} + +// getJSONKey implements the Field interface. +func (f *protoOneOfField) getJSONKey() string { + return f.jsonKey +} + +// Type implements the Field interface. +func (f *protoOneOfField) Type() *reflectType { + return f.t +} + +// isRequired implements the Field interface. +func (f *protoOneOfField) isRequired() bool { + return false +} + +// getValue implements the Field interface. +func (f *protoOneOfField) getValue(p *point) *point { + protoMsg := p.rv.Interface().(proto.Message) + fdVal := protoMsg.ProtoReflect().Get(f.fd) + if !fdVal.IsValid() { + return nil + } + obj := fdVal.Message().Interface() + rv := reflect.ValueOf(obj) + out := p.scope.newPoint(rv) + return out +} + +// setValue implements the Field interface. +func (f *protoOneOfField) setValue(p *point, srcVal reflect.Value) error { + if srcVal.IsNil() { + return nil + } + + protoMsg := p.rv.Interface().(proto.Message) + fdVal := protoMsg.ProtoReflect().Get(f.fd) + + fdObj := fdVal.Message().New() + + destType := reflect.TypeOf(fdObj.Interface()) + + destVal, err := p.scope.convert(srcVal, destType) + if err != nil { + return fmt.Errorf("converting %v to %v: %w", srcVal.Type(), destType, err) + } + if !destVal.IsValid() { + return nil + } + + destMsg := destVal.Interface().(proto.Message) + protoMsg.ProtoReflect().Set(f.fd, protoreflect.ValueOfMessage(destMsg.ProtoReflect())) + + return nil +} diff --git a/pkg/controller/direct/mappings/structfield.go b/pkg/controller/direct/mappings/structfield.go new file mode 100644 index 0000000000..fc7d285157 --- /dev/null +++ b/pkg/controller/direct/mappings/structfield.go @@ -0,0 +1,82 @@ +// 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 mappings + +import ( + "fmt" + "reflect" +) + +// structField is a "normal" Field within a structure. +// This is the common implementation of Field. +type structField struct { + f *reflect.StructField + + jsonKey string +} + +var _ Field = &structField{} + +// ID implements the Field interface. +func (f *structField) ID() FieldID { + fieldID := toFieldID(f.jsonKey) + return fieldID +} + +// getJSONKey implements the Field interface. +func (f *structField) getJSONKey() string { + return f.jsonKey +} + +// Type implements the Field interface. +func (f *structField) Type() *reflectType { + return allTypes.get(f.f.Type) +} + +// isRequired implements the Field interface. +func (f *structField) isRequired() bool { + return false +} + +// getValue implements the Field interface. +func (f *structField) getValue(p *point) *point { + structVal := p.rv + if structVal.Kind() == reflect.Ptr { + structVal = structVal.Elem() + } + rv := structVal.FieldByIndex(f.f.Index) + out := p.scope.newPoint(rv) + return out +} + +// setValue implements the Field interface. +func (f *structField) setValue(p *point, srcVal reflect.Value) error { + structVal := p.rv + if structVal.Kind() == reflect.Ptr { + structVal = structVal.Elem() + } + + fieldVal := structVal.FieldByIndex(f.f.Index) + destType := fieldVal.Type() + destVal, err := p.scope.convert(srcVal, destType) + if err != nil { + return fmt.Errorf("converting %v to %v: %w", srcVal.Type(), destType, err) + } + if !destVal.IsValid() { + return nil + } + fieldVal.Set(destVal) + return nil +} diff --git a/pkg/controller/direct/mappings/type.go b/pkg/controller/direct/mappings/type.go new file mode 100644 index 0000000000..90c70aeeec --- /dev/null +++ b/pkg/controller/direct/mappings/type.go @@ -0,0 +1,184 @@ +// 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 mappings + +import ( + "reflect" + "sync" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "k8s.io/klog/v2" +) + +// reflectType is a wrapper around reflect.Type, that adds a cache of our Fields. +type reflectType struct { + rt reflect.Type + + fields map[FieldID]Field +} + +// typeCache is a cache of the types we have built. +type typeCache struct { + mutex sync.Mutex + types map[reflect.Type]*reflectType +} + +// allTypes is a global cache of reflectType. +var allTypes typeCache + +// get returns the cached reflectType, or builds one if it does not exist. +func (c *typeCache) get(t reflect.Type) *reflectType { + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.types == nil { + c.types = make(map[reflect.Type]*reflectType) + } + + if tt, ok := c.types[t]; ok { + return tt + } + + tt := &reflectType{rt: t} + c.types[t] = tt + return tt +} + +// typeOf returns the reflectType for the given type. +func typeOf(t reflect.Type) *reflectType { + return allTypes.get(t) +} + +// String returns a human friendly name for the type. +func (t *reflectType) String() string { + prefix := "" + reflectType := t.rt + + for { + if reflectType.Kind() == reflect.Ptr { + prefix = "*" + prefix + reflectType = reflectType.Elem() + } else if reflectType.Kind() == reflect.Slice { + prefix = "[]" + prefix + reflectType = reflectType.Elem() + } else { + break + } + } + + return prefix + reflectType.PkgPath() + "." + reflectType.Name() +} + +// lookupField returns the field for the given path. +// This is used in validation. +func (t *reflectType) lookupField(fields *fieldPath) Field { + n := len(fields.fields) + p := t.findField(fields.fields[0]) + if p == nil { + return nil + } + for i := 1; i < n; i++ { + f := p.Type().findField(fields.fields[i]) + if f == nil { + return nil + } + p = f + } + return p +} + +// Fields returns our Fields for the given type. +// We have some special handling for e.g. proto oneof fields, +// we are trying to look at the representation of the objects in e.g. JSON +func (t *reflectType) Fields() map[FieldID]Field { + if t.fields != nil { + return t.fields + } + var fieldList []Field + reflectType := t.rt + + protoMessageInterfaceType := reflect.TypeOf((*proto.Message)(nil)).Elem() + + // proto oneof (union) fields have a complicated structure, + // but can easily be discovered and get/set through protoreflect + if reflectType.Implements(protoMessageInterfaceType) { + obj := reflect.New(reflectType).Elem().Interface().(proto.Message) + + descriptor := obj.ProtoReflect().Descriptor() + + fields := descriptor.Fields() + for i := 0; i < fields.Len(); i++ { + fd := fields.Get(i) + + oneOf := fd.ContainingOneof() + if oneOf == nil { + continue + } + + fdObj := obj.ProtoReflect().Get(fd) + + // klog.Infof("found oneof %s", oneOf) + + field := &protoOneOfField{fd: fd} + field.jsonKey = fd.JSONName() + switch fd.Kind() { + case protoreflect.MessageKind: + t := reflect.TypeOf(fdObj.Message().Interface()) + field.t = allTypes.get(t) + default: + klog.Fatalf("cannot handle oneof member field %v", fd) + } + + fieldList = append(fieldList, field) + } + } + + if reflectType.Kind() == reflect.Ptr { + reflectType = reflectType.Elem() + } + n := reflectType.NumField() + for i := 0; i < n; i++ { + rf := reflectType.Field(i) + if !rf.IsExported() { + continue + } + // Skip proto oneof (as we handled them above) + oneOfTag := rf.Tag.Get("protobuf_oneof") + if oneOfTag != "" { + continue + } + f := &structField{f: &rf, jsonKey: getJSONFieldTag(&rf)} + fieldList = append(fieldList, f) + } + + fields := make(map[FieldID]Field) + for _, f := range fieldList { + id := f.ID() + if fields[id] != nil { + klog.Fatalf("duplicate field %s", f.ID()) + } + fields[id] = f + } + + t.fields = fields + return fields +} + +// findField returns the field with the specified ID +func (t *reflectType) findField(id FieldID) Field { + f := t.Fields()[id] + return f +} diff --git a/pkg/controller/direct/mappings/validation.go b/pkg/controller/direct/mappings/validation.go new file mode 100644 index 0000000000..8311d7aecc --- /dev/null +++ b/pkg/controller/direct/mappings/validation.go @@ -0,0 +1,251 @@ +// 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 mappings + +import ( + "fmt" + "os" + "reflect" + "strings" + "unicode" + + "k8s.io/klog/v2" +) + +// ValidationError is the type for one of our validation findings. +type ValidationError struct { + Message string + Proposal string +} + +// Validate will perform some checks on the mapping. +// We always look for invalid mappings, where (for example) the field does not exist on one or both sides. +// If CHECK_COVERAGE is passed, it will look for missing mappings. +func (m *Mapping) Validate() []ValidationError { + var errors []ValidationError + for _, typeMapping := range m.Mappings { + checkMissing := (os.Getenv("CHECK_COVERAGE") != "") + switch typeMapping := typeMapping.(type) { + case *structTypeMapping: + errors = append(errors, typeMapping.validate(checkMissing)...) + default: + klog.Fatalf("unhandled type mapping %T", typeMapping) + } + } + return errors +} + +// validate performs validation on a structTypeMapping +func (m *structTypeMapping) validate(checkMissing bool) []ValidationError { + var errors []ValidationError + + for _, mapping := range m.fields { + inField := m.inType.lookupField(mapping.InPath) + outField := m.outType.lookupField(mapping.OutPath) + + if inField == nil { + err := ValidationError{Message: fmt.Sprintf("field %s not found in input type %v", mapping.InPath, m.inType)} + if outField != nil { + err.Proposal = buildGoField(outField) + } + + errors = append(errors, err) + continue + } + if outField == nil { + proposal := buildGoField(inField) + + klog.Infof("outType.Fields = %v", m.outType.Fields()) + + errors = append(errors, ValidationError{ + Message: fmt.Sprintf("field %s not found in output type %v", mapping.OutPath, m.outType), + Proposal: proposal, + }) + continue + } + + } + + if checkMissing { + + if m.hasSpecStatus { + specField := m.inType.lookupField(parseFieldPath("spec")) + statusField := m.inType.lookupField(parseFieldPath("status")) + if specField != nil && statusField != nil { + specType := specField.Type() + statusType := statusField.Type() + + for _, cloudField := range m.outType.Fields() { + id := cloudField.ID() + + ignore := false + for _, ignoreField := range m.ignore { + if ignoreField.ID == id { + ignore = true + } + } + if ignore { + continue + } + specField := specType.lookupField(newFieldPath(id)) + statusField := statusType.lookupField(newFieldPath(id)) + + if specField == nil && statusField == nil { + proposal := buildGoField(cloudField) + + errors = append(errors, ValidationError{ + Message: fmt.Sprintf("field %s not found in KRM spec nor status %v", id, m.inType), + Proposal: proposal, + }) + continue + } + } + } + } else { + for _, outField := range m.outType.Fields() { + id := outField.ID() + + found := false + for _, mapping := range m.fields { + if len(mapping.OutPath.fields) == 1 && mapping.OutPath.fields[0] == id { + found = true + } + } + if found { + continue + } + + ignore := false + for _, ignoreField := range m.ignore { + if ignoreField.ID == id { + ignore = true + } + } + if ignore { + continue + } + errors = append(errors, ValidationError{ + Message: fmt.Sprintf("field %s is not mapped in %v", id, m.outType), + // Proposal: proposal, + }) + } + + for _, inField := range m.inType.Fields() { + id := inField.ID() + + found := false + for _, mapping := range m.fields { + if len(mapping.InPath.fields) == 1 && mapping.InPath.fields[0] == id { + found = true + } + } + if found { + continue + } + + ignore := false + for _, ignoreField := range m.ignore { + if ignoreField.ID == id { + ignore = true + } + } + if ignore { + continue + } + errors = append(errors, ValidationError{ + Message: fmt.Sprintf("field %s is not mapped in %v", id, m.inType), + }) + } + } + + } + + return errors +} + +// buildGoField builds go code that could be used to add the relevant field. +func buildGoField(f Field) string { + jsonName := f.getJSONKey() + + fieldName := jsonToGoFieldName(jsonName) + fieldType := convertToGoType(f.Type().rt) + jsonTag := jsonName + jsonTag += ",omitempty" + + requiredTag := "" + if f.isRequired() { + requiredTag = "true" + } + + tags := []string{} + if jsonTag != "" { + tags = append(tags, fmt.Sprintf("json:%q", jsonTag)) + } + if requiredTag != "" { + tags = append(tags, fmt.Sprintf("required:%q", requiredTag)) + } + + fieldTags := "" + if len(tags) != 0 { + fieldTags = " `" + strings.Join(tags, " ") + "`" + } + + proposal := fmt.Sprintf("%s %s%s", fieldName, fieldType, fieldTags) + return proposal +} + +// jsonToGoFieldName converts a json name into its go field name equivalent, +// used to suggest go code. +func jsonToGoFieldName(jsonName string) string { + var out []rune + for i, r := range jsonName { + if i == 0 { + r = unicode.ToUpper(r) + } + out = append(out, r) + } + return string(out) +} + +// convertToGoType builds the go type name for the specified type, +// used to suggest go code. +func convertToGoType(t reflect.Type) string { + fieldGoType := t + switch fieldGoType.Kind() { + case reflect.Slice: + return "[]" + convertToGoType(t.Elem()) + case reflect.Ptr: + return "*" + convertToGoType(t.Elem()) + case reflect.Struct: + return t.Name() + case reflect.String: + return "string" + case reflect.Bool: + return "bool" + case reflect.Uint8: + return "uint8" + case reflect.Int32: + return "int32" + case reflect.Int64: + return "int64" + case reflect.Map: + return "map[todo]todo" + case reflect.Interface: + return "interface{}" + default: + klog.Fatalf("unsupported kind in convertToGoType %v", fieldGoType.Kind()) + return "" + } +} diff --git a/pkg/controller/kccmanager/kccmanager.go b/pkg/controller/kccmanager/kccmanager.go index 5bf5e5776c..d36c628e41 100644 --- a/pkg/controller/kccmanager/kccmanager.go +++ b/pkg/controller/kccmanager/kccmanager.go @@ -20,6 +20,7 @@ import ( "net/http" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/kccmanager/nocache" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/registration" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/clientconfig" @@ -85,6 +86,9 @@ func New(ctx context.Context, restConfig *rest.Config, config Config) (manager.M // Disable the cache. The cache causes problems in namespaced mode when trying // to read resources in our system namespace. opts.NewClient = nocache.NoCacheClientFunc + opts.BaseContext = func() context.Context { + return ctx + } mgr, err := manager.New(restConfig, opts) if err != nil { return nil, fmt.Errorf("error creating new manager: %w", err) @@ -115,12 +119,12 @@ func New(ctx context.Context, restConfig *rest.Config, config Config) (manager.M serviceMetadataLoader := dclmetadata.New() dclConverter := dclconversion.New(dclSchemaLoader, serviceMetadataLoader) - dclOptions := clientconfig.Options{ - UserProjectOverride: config.UserProjectOverride, - BillingProject: config.BillingProject, - HTTPClient: config.HTTPClient, - UserAgent: gcp.KCCUserAgent, - } + dclOptions := clientconfig.Options{} + dclOptions.UserProjectOverride = config.UserProjectOverride + dclOptions.BillingProject = config.BillingProject + dclOptions.HTTPClient = config.HTTPClient + dclOptions.UserAgent = gcp.KCCUserAgent + dclConfig, err := clientconfig.New(ctx, dclOptions) if err != nil { return nil, fmt.Errorf("error creating a DCL client config: %w", err) @@ -130,9 +134,15 @@ func New(ctx context.Context, restConfig *rest.Config, config Config) (manager.M if err != nil { return nil, fmt.Errorf("error constructing new state into spec value: %w", err) } + controllerConfig := &controller.Config{ + UserProjectOverride: config.UserProjectOverride, + BillingProject: config.BillingProject, + HTTPClient: config.HTTPClient, + UserAgent: gcp.KCCUserAgent, + } // Register the registration controller, which will dynamically create controllers for // all our resources. - if err := registration.Add(mgr, provider, smLoader, dclConfig, dclConverter, registration.RegisterDefaultController, []k8s.Defaulter{stateIntoSpecDefaulter}); err != nil { + if err := registration.Add(mgr, provider, smLoader, dclConfig, dclConverter, registration.RegisterDefaultController(controllerConfig), []k8s.Defaulter{stateIntoSpecDefaulter}); err != nil { return nil, fmt.Errorf("error adding registration controller: %w", err) } return mgr, nil diff --git a/pkg/controller/mocktests/secretmanager_secret_test.go b/pkg/controller/mocktests/secretmanager_secret_test.go index d8a27c0b64..bd39de6ae9 100644 --- a/pkg/controller/mocktests/secretmanager_secret_test.go +++ b/pkg/controller/mocktests/secretmanager_secret_test.go @@ -121,10 +121,10 @@ func TestSecretManagerSecretVersion(t *testing.T) { t.Fatalf("error from tfprovider.New: %v", err) } t.Logf("creating dclconfig") - dclConfig, err := clientconfig.New(h.Ctx, clientconfig.Options{ - UserAgent: "kcc/dev", - HTTPClient: gcpHTTPClient, - }) + dclOptions := clientconfig.Options{} + dclOptions.UserAgent = "kcc/dev" + dclOptions.HTTPClient = gcpHTTPClient + dclConfig, err := clientconfig.New(h.Ctx, dclOptions) if err != nil { t.Fatalf("error from clientconfig.New: %v", err) } diff --git a/pkg/controller/registration/registration_controller.go b/pkg/controller/registration/registration_controller.go index ddaf713ea2..7b82d2d676 100644 --- a/pkg/controller/registration/registration_controller.go +++ b/pkg/controller/registration/registration_controller.go @@ -20,8 +20,10 @@ import ( "sync" "time" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller" dclcontroller "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/dcl" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/deletiondefender" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/apikeys" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/gsakeysecretgenerator" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/iam/auditconfig" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/iam/partialpolicy" @@ -40,7 +42,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" + crcontroller "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" klog "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -68,8 +70,8 @@ func Add(mgr manager.Manager, p *tfschema.Provider, smLoader *servicemappingload registrationFunc: regFunc, defaulters: defaulters, } - c, err := controller.New(controllerName, mgr, - controller.Options{ + c, err := crcontroller.New(controllerName, mgr, + crcontroller.Options{ Reconciler: r, MaxConcurrentReconciles: k8s.ControllerMaxConcurrentReconciles, }) @@ -158,7 +160,13 @@ func isServiceAccountKeyCRD(crd *apiextensions.CustomResourceDefinition) bool { return crd.Spec.Group == serviceAccountKeyAPIGroup && crd.Spec.Names.Kind == serviceAccountKeyKind } -func RegisterDefaultController(r *ReconcileRegistration, crd *apiextensions.CustomResourceDefinition, gvk schema.GroupVersionKind) (k8s.SchemaReferenceUpdater, error) { +func RegisterDefaultController(config *controller.Config) registrationFunc { //nolint:revive + return func(r *ReconcileRegistration, crd *apiextensions.CustomResourceDefinition, gvk schema.GroupVersionKind) (k8s.SchemaReferenceUpdater, error) { + return registerDefaultController(r, config, crd, gvk) + } +} + +func registerDefaultController(r *ReconcileRegistration, config *controller.Config, crd *apiextensions.CustomResourceDefinition, gvk schema.GroupVersionKind) (k8s.SchemaReferenceUpdater, error) { if _, ok := k8s.IgnoredKindList[crd.Spec.Names.Kind]; ok { return nil, nil } @@ -181,6 +189,10 @@ func RegisterDefaultController(r *ReconcileRegistration, crd *apiextensions.Cust if err := auditconfig.Add(r.mgr, r.provider, r.smLoader, r.dclConverter, r.dclConfig, r.defaulters); err != nil { return nil, err } + case "APIKeysKey": + if err := apikeys.AddKeyReconciler(r.mgr, config); err != nil { + return nil, err + } default: // register controllers for dcl-based CRDs if val, ok := crd.Labels[k8s.DCL2CRDLabel]; ok && val == "true" { diff --git a/pkg/dcl/clientconfig/config.go b/pkg/dcl/clientconfig/config.go index 2dc00848da..c8b7b91fd4 100644 --- a/pkg/dcl/clientconfig/config.go +++ b/pkg/dcl/clientconfig/config.go @@ -22,6 +22,7 @@ import ( "path/filepath" "time" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/logger" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gcp" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s" @@ -36,20 +37,7 @@ import ( var nonretryable = dcl.Retryability{Retryable: false} type Options struct { - UserAgent string - - // UserProjectOverride provides the option to use the resource project for preconditions, quota, and billing, - // instead of the project the credentials belong to; false by default - UserProjectOverride bool - - // BillingProject is the project used by the TF provider and DCL client to determine preconditions, - // quota, and billing if UserProjectOverride is set to true. If this field is empty, - // but UserProjectOverride is set to true, resource project will be used. - BillingProject string - - // HTTPClient allows us to specify the HTTP client to use with DCL. - // This is particularly useful in mocks/tests. - HTTPClient *http.Client + controller.Config } func New(ctx context.Context, opt Options) (*dcl.Config, error) { @@ -104,9 +92,8 @@ func NewForIntegrationTest() *dcl.Config { eventSinks = append(eventSinks, test.NewDirectoryEventSink(outputDir)) } - opt := Options{ - UserAgent: "kcc/dev", - } + opt := Options{} + opt.UserAgent = "kcc/dev" // Log DCL requests if len(eventSinks) != 0 { From ff20a650a19b4c97019f33189dc3ee45b364d737 Mon Sep 17 00:00:00 2001 From: justinsb Date: Wed, 31 Jan 2024 12:40:00 -0500 Subject: [PATCH 2/5] Update mockgcp for apikeys We want some operations support --- mockgcp/common/operations/operations.go | 7 ++++++- mockgcp/mock_http_roundtrip.go | 4 ++-- mockgcp/mockapikeys/key.go | 4 +++- mockgcp/mockapikeys/service.go | 10 ++++++---- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/mockgcp/common/operations/operations.go b/mockgcp/common/operations/operations.go index ab8cd0ace6..0a1a680a75 100644 --- a/mockgcp/common/operations/operations.go +++ b/mockgcp/common/operations/operations.go @@ -171,7 +171,12 @@ func (s *Operations) DoneLRO(ctx context.Context, prefix string, metadata proto. } func rewriteTypes(any *anypb.Any) { - // Fix our mockgcp hack + // Fix our mockgcp hack: + // The protobuf libraries get upset if we have two proto message types + // with the same proto path, but different go paths. + // The go client SDK for GCP uses the protos for some services, + // so we need to "get out of the way" to avoid conflicts. + // We rename our protos from google. => mockgcp. if strings.HasPrefix(any.TypeUrl, "type.googleapis.com/mockgcp.") { any.TypeUrl = "type.googleapis.com/google." + strings.TrimPrefix(any.TypeUrl, "type.googleapis.com/mockgcp.") } diff --git a/mockgcp/mock_http_roundtrip.go b/mockgcp/mock_http_roundtrip.go index fe11455446..caba8a8547 100644 --- a/mockgcp/mock_http_roundtrip.go +++ b/mockgcp/mock_http_roundtrip.go @@ -291,8 +291,8 @@ func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) body["email"] = "test@example.com" response.StatusCode = 200 } else { - log.Printf("Expect host name invalid or does not match the actual host. " + - "Please verify the ExpectedHost in service.go and retry.") + log.Printf("host name %q not known. "+ + "Please verify the ExpectedHost in service.go and retry.", req.Host) } if body != nil { diff --git a/mockgcp/mockapikeys/key.go b/mockgcp/mockapikeys/key.go index 2773fc1bfa..7b9ff3dbab 100644 --- a/mockgcp/mockapikeys/key.go +++ b/mockgcp/mockapikeys/key.go @@ -169,5 +169,7 @@ func (s *APIKeysV2) DeleteKey(ctx context.Context, req *pb.DeleteKeyRequest) (*l return nil, err } - return s.operations.NewLRO(ctx) + return s.operations.StartLRO(ctx, "", nil, func() (proto.Message, error) { + return deleted, nil + }) } diff --git a/mockgcp/mockapikeys/service.go b/mockgcp/mockapikeys/service.go index e2583fabca..85795969f4 100644 --- a/mockgcp/mockapikeys/service.go +++ b/mockgcp/mockapikeys/service.go @@ -19,10 +19,10 @@ import ( "net/http" "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common" + "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/httpmux" "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/operations" "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/common/projects" "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/pkg/storage" - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" "sigs.k8s.io/controller-runtime/pkg/client" @@ -61,9 +61,11 @@ func (s *MockService) Register(grpcServer *grpc.Server) { } func (s *MockService) NewHTTPMux(ctx context.Context, conn *grpc.ClientConn) (http.Handler, error) { - mux := runtime.NewServeMux() - - if err := pb.RegisterApiKeysHandler(ctx, mux, conn); err != nil { + mux, err := httpmux.NewServeMux(ctx, conn, + pb.RegisterApiKeysHandler, + s.operations.RegisterOperationsPath("/v2/operations/{name}"), + ) + if err != nil { return nil, err } From 166896f8e5b769c20f6084a777b179985fffeca2 Mon Sep 17 00:00:00 2001 From: justinsb Date: Tue, 13 Feb 2024 12:14:04 -0500 Subject: [PATCH 3/5] Activate direct reconciliation (only) with a feature flag This enables us to merge the code without activating it yet, and test it against the old code. Current syntax is: `export KCC_USE_DIRECT_RECONCILERS=APIKeysKey,` However I think we should consider this syntax provisional and subject to change. --- .../registration/registration_controller.go | 27 +++++++++---- pkg/kccfeatureflags/flags.go | 38 +++++++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 pkg/kccfeatureflags/flags.go diff --git a/pkg/controller/registration/registration_controller.go b/pkg/controller/registration/registration_controller.go index 7b82d2d676..f8b06e6fbb 100644 --- a/pkg/controller/registration/registration_controller.go +++ b/pkg/controller/registration/registration_controller.go @@ -34,6 +34,7 @@ import ( "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/crdgeneration" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/conversion" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/kccfeatureflags" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader" "github.com/GoogleCloudPlatform/declarative-resource-client-library/dcl" @@ -41,10 +42,11 @@ import ( apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" crcontroller "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" - klog "sigs.k8s.io/controller-runtime/pkg/log" + crlog "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" @@ -54,7 +56,7 @@ const controllerName = "registration-controller" const serviceAccountKeyAPIGroup = "iam.cnrm.cloud.google.com" const serviceAccountKeyKind = "IAMServiceAccountKey" -var logger = klog.Log.WithName(controllerName) +var logger = crlog.Log.WithName(controllerName) // Add creates a new registration Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller // and Start it when the Manager is Started. @@ -170,8 +172,22 @@ func registerDefaultController(r *ReconcileRegistration, config *controller.Conf if _, ok := k8s.IgnoredKindList[crd.Spec.Names.Kind]; ok { return nil, nil } - // Depending on which resource it is, we need to register a different controller. + var schemaUpdater k8s.SchemaReferenceUpdater + + if kccfeatureflags.UseDirectReconciler(gvk.GroupKind()) { + switch gvk.GroupKind() { + case schema.GroupKind{Group: "apikeys.cnrm.cloud.google.com", Kind: "APIKeysKey"}: + if err := apikeys.AddKeyReconciler(r.mgr, config); err != nil { + return nil, err + } + return schemaUpdater, nil + default: + klog.Warningf("requested direct reconciler for %v, but it is not supported", gvk.GroupKind()) + } + } + + // Depending on which resource it is, we need to register a different controller. switch gvk.Kind { case "IAMPolicy": if err := policy.Add(r.mgr, r.provider, r.smLoader, r.dclConverter, r.dclConfig, r.defaulters); err != nil { @@ -189,10 +205,7 @@ func registerDefaultController(r *ReconcileRegistration, config *controller.Conf if err := auditconfig.Add(r.mgr, r.provider, r.smLoader, r.dclConverter, r.dclConfig, r.defaulters); err != nil { return nil, err } - case "APIKeysKey": - if err := apikeys.AddKeyReconciler(r.mgr, config); err != nil { - return nil, err - } + default: // register controllers for dcl-based CRDs if val, ok := crd.Labels[k8s.DCL2CRDLabel]; ok && val == "true" { diff --git a/pkg/kccfeatureflags/flags.go b/pkg/kccfeatureflags/flags.go new file mode 100644 index 0000000000..5a3e9db615 --- /dev/null +++ b/pkg/kccfeatureflags/flags.go @@ -0,0 +1,38 @@ +// 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 kccfeatureflags + +import ( + "os" + "strings" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// UseDirectReconciler is true if we should use the direct reconciler to actuate the specified resource. +func UseDirectReconciler(gk schema.GroupKind) bool { + directReconcilers := os.Getenv("KCC_USE_DIRECT_RECONCILERS") + if directReconcilers == "" { + return false + } + + for _, directReconciler := range strings.Split(directReconcilers, ",") { + if directReconciler == gk.Kind { + return true + } + } + + return false +} From 17f273f99066a6afdb7b7dbc8c3fd3ab32bd48e6 Mon Sep 17 00:00:00 2001 From: justinsb Date: Wed, 31 Jan 2024 12:40:40 -0500 Subject: [PATCH 4/5] go.mod / go.sum updates --- go.mod | 59 ++++++++++++++++------------- go.sum | 117 +++++++++++++++++++++++++++++++++------------------------ 2 files changed, 101 insertions(+), 75 deletions(-) diff --git a/go.mod b/go.mod index 2ec9440186..67ff1dee77 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21.5 replace github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp => ./mockgcp require ( + cloud.google.com/go/apikeys v1.1.5 cloud.google.com/go/profiler v0.1.0 contrib.go.opencensus.io/exporter/prometheus v0.1.0 github.com/GoogleCloudPlatform/declarative-resource-client-library v1.58.0 @@ -17,6 +18,7 @@ require ( github.com/go-logr/logr v1.4.1 github.com/go-logr/zapr v1.3.0 github.com/google/go-cmp v0.6.0 + github.com/googleapis/gax-go/v2 v2.12.0 github.com/gosimple/slug v1.13.1 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-multierror v1.1.1 @@ -38,11 +40,11 @@ require ( github.com/zclconf/go-cty v1.13.0 go.opencensus.io v0.24.0 go.uber.org/zap v1.26.0 - golang.org/x/oauth2 v0.12.0 - golang.org/x/sync v0.5.0 - golang.org/x/time v0.3.0 - google.golang.org/api v0.139.0 - google.golang.org/protobuf v1.31.0 + golang.org/x/oauth2 v0.16.0 + golang.org/x/sync v0.6.0 + golang.org/x/time v0.5.0 + google.golang.org/api v0.160.0 + google.golang.org/protobuf v1.32.0 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.27.11 k8s.io/apiextensions-apiserver v0.27.9 @@ -61,12 +63,12 @@ require ( require ( bitbucket.org/creachadair/stringset v0.0.8 // indirect - cloud.google.com/go v0.110.8 // indirect + cloud.google.com/go v0.111.0 // indirect cloud.google.com/go/bigtable v1.19.0 // indirect - cloud.google.com/go/compute v1.23.0 // indirect + cloud.google.com/go/compute v1.23.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v1.1.2 // indirect - cloud.google.com/go/longrunning v0.5.1 // indirect + cloud.google.com/go/iam v1.1.5 // indirect + cloud.google.com/go/longrunning v0.5.4 // indirect dario.cat/mergo v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect @@ -83,17 +85,18 @@ require ( github.com/chai2010/gettext-go v1.0.2 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe // indirect - github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect + github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.10.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f // indirect - github.com/envoyproxy/protoc-gen-validate v0.10.1 // indirect + github.com/envoyproxy/go-control-plane v0.11.1 // indirect + github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/fatih/camelcase v1.0.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fvbommel/sortorder v1.0.1 // indirect github.com/gammazero/deque v0.0.0-20190521012701-46e4ffb7a622 // indirect @@ -102,12 +105,13 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-git/v5 v5.11.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/gobuffalo/flect v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v1.1.0 // indirect + github.com/golang/glog v1.1.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.1.2 // indirect @@ -117,9 +121,8 @@ require ( github.com/google/pprof v0.0.0-20210804190019-f964ff605595 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.3.1 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect - github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/gosimple/unidecode v1.0.1 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect @@ -183,22 +186,26 @@ require ( github.com/vmihailenco/tagparser v0.1.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xlab/treeprint v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect + go.opentelemetry.io/otel v1.22.0 // indirect + go.opentelemetry.io/otel/metric v1.22.0 // indirect + go.opentelemetry.io/otel/trace v1.22.0 // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/crypto v0.17.0 // indirect + golang.org/x/crypto v0.18.0 // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.16.1 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230815205213-6bfd019c3878 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230815205213-6bfd019c3878 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect - google.golang.org/grpc v1.57.1 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect + google.golang.org/grpc v1.61.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 242dad68fe..7be39ab6cd 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,10 @@ cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSU cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.92.2/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.110.8 h1:tyNdfIxjzaWctIiLYOTalaLKZ17SI44SKFW26QbOhME= -cloud.google.com/go v0.110.8/go.mod h1:Iz8AkXJf1qmxC3Oxoep8R1T36w8B92yU29PcBhHO5fk= +cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM= +cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU= +cloud.google.com/go/apikeys v1.1.5 h1:gwN72kErrFfRbDO2PvsJxXzmjULfVQ3RmdB1HI1y3dc= +cloud.google.com/go/apikeys v1.1.5/go.mod h1:ZeR5Gq5Te9fSZOYQ7AwBP91IWf5i4mQTt4xFayiy+wI= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -35,17 +37,17 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigtable v1.19.0 h1:wiq9LT0kukfInzvy1joMDijCw/OD1UChpSbORXYn0LI= cloud.google.com/go/bigtable v1.19.0/go.mod h1:xl5kPa8PTkJjdBxg6qdGH88464nNqmbISHSRU+D2yFE= -cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= -cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/iam v1.1.2 h1:gacbrBdWcoVmGLozRuStX45YKvJtzIjJdAolzUs1sm4= -cloud.google.com/go/iam v1.1.2/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= -cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI= -cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= +cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= cloud.google.com/go/profiler v0.1.0 h1:MG/rxKC1MztRfEWMGYKFISxyZak5hNh29f0A/z2tvWk= cloud.google.com/go/profiler v0.1.0/go.mod h1:D7S7LV/zKbRWkOzYL1b5xytpqt8Ikd/v/yvf1/Tx2pQ= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= @@ -175,8 +177,8 @@ github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nC github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY= +github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -236,11 +238,11 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f h1:7T++XKzy4xg7PKy+bM+Sa9/oe1OC88yz2hXQUISoXfA= -github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= +github.com/envoyproxy/go-control-plane v0.11.1 h1:wSUXTlLfiAQRWs2F+p+EKOY9rUyis1MyGqJ2DIk5HpM= +github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8= -github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= +github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/evanphx/json-patch v4.0.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -258,6 +260,8 @@ github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGE github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -305,8 +309,11 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v0.1.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= @@ -372,8 +379,8 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -472,10 +479,10 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM= -github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= @@ -924,6 +931,18 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= +go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= @@ -968,8 +987,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1073,8 +1092,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1090,8 +1109,8 @@ golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= -golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1105,8 +1124,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180117170059-2c42eef0765b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1194,8 +1213,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1203,8 +1222,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1216,6 +1235,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= @@ -1226,8 +1246,8 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1299,8 +1319,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gomodules.xyz/jsonpatch/v2 v2.0.1/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc= gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= @@ -1334,16 +1352,17 @@ google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtuk google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.139.0 h1:A1TrCPgMmOiYu0AiNkvQIpIx+D8blHTDcJ5EogkP7LI= -google.golang.org/api v0.139.0/go.mod h1:CVagp6Eekz9CjGZ718Z+sloknzkDJE7Vc1Ckj9+viBk= +google.golang.org/api v0.160.0 h1:SEspjXHVqE1m5a1fRy8JFB+5jSu+V0GEDKDghF3ttO4= +google.golang.org/api v0.160.0/go.mod h1:0mu0TpK33qnydLvWqbImq2b1eQ5FHRSDCBzAxX9ZHyw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1397,12 +1416,12 @@ google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKr google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20230815205213-6bfd019c3878 h1:Iveh6tGCJkHAjJgEqUQYGDGgbwmhjoAOz8kO/ajxefY= -google.golang.org/genproto v0.0.0-20230815205213-6bfd019c3878/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= -google.golang.org/genproto/googleapis/api v0.0.0-20230815205213-6bfd019c3878 h1:WGq4lvB/mlicysM/dUT3SBvijH4D3sm/Ny1A4wmt2CI= -google.golang.org/genproto/googleapis/api v0.0.0-20230815205213-6bfd019c3878/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= +google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8= +google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1428,8 +1447,8 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.57.1 h1:upNTNqv0ES+2ZOOqACwVtS3Il8M12/+Hz41RCPzAjQg= -google.golang.org/grpc v1.57.1/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= +google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1444,8 +1463,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 76adc4d41071fcd01b69bbe6cbbe08dc0f5e425b Mon Sep 17 00:00:00 2001 From: justinsb Date: Wed, 21 Feb 2024 21:07:10 -0500 Subject: [PATCH 5/5] mockgcp: Add comments on OperationsHandler This should help implementors choose the right prefix value. --- mockgcp/common/operations/http.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mockgcp/common/operations/http.go b/mockgcp/common/operations/http.go index f3e52e67f0..c131fd741e 100644 --- a/mockgcp/common/operations/http.go +++ b/mockgcp/common/operations/http.go @@ -27,6 +27,13 @@ import ( "k8s.io/klog/v2" ) +// RegisterOperationsPath will serve the operations REST API at `path`. +// This is currently a GET handler; `path` should include a parameter {name} +// which is the operation's name, and an optional parameter {prefix} which +// includes whatever else the service defines as the per-operation prefix. +// The value of path / prefix should match whatever the API defines as its +// operations endpoint, which is often most conveniently determined by looking +// at the API documentation, or by seeing what paths clients request. func (s *Operations) RegisterOperationsPath(path string) func(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { return func(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { forwardResponseOptions := mux.GetForwardResponseOptions()