pkg/controller/direct/cloudquota/quotapreference_controller.go (197 lines of code) (raw):

// 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. // +tool:controller // proto.service: google.api.cloudquotas.v1beta.CloudQuotas // proto.message: google.api.cloudquotas.v1beta.QuotaPreference // crd.type: APIQuotaPreference // crd.version: v1alpha1 package cloudquota import ( "context" "fmt" "reflect" gcp "cloud.google.com/go/cloudquotas/apiv1beta" pb "cloud.google.com/go/cloudquotas/apiv1beta/cloudquotaspb" krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/cloudquota/v1alpha1" refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry" "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/client" ) func init() { registry.RegisterModel(krm.APIQuotaPreferenceGVK, NewQuotaPreferenceModel) } func NewQuotaPreferenceModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) { return &apiQuotaPreferenceModel{config: *config}, nil } var _ directbase.Model = &apiQuotaPreferenceModel{} type apiQuotaPreferenceModel struct { config config.ControllerConfig } func (m *apiQuotaPreferenceModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) { obj := &krm.APIQuotaPreference{} if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil { return nil, fmt.Errorf("error converting to %T: %w", obj, err) } id, err := krm.NewQuotaPreferenceIdentity(ctx, reader, obj) if err != nil { return nil, err } config := m.config // the service requires that a quota project be set if !config.UserProjectOverride || config.BillingProject == "" { config.UserProjectOverride = true if id.Parent().ProjectID != "" { config.BillingProject = id.Parent().ProjectID } // Folder and Organization parents are not billing projects } gcpClient, err := newGCPClient(ctx, &config) if err != nil { return nil, err } client, err := gcpClient.newQuotaClient(ctx) if err != nil { return nil, err } return &apiQuotaPreferenceAdapter{ gcpClient: client, id: id, desired: obj, reader: reader, }, nil } func (m *apiQuotaPreferenceModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) { // TODO: Support URLs return nil, nil } type apiQuotaPreferenceAdapter struct { gcpClient *gcp.Client id *krm.QuotaPreferenceIdentity desired *krm.APIQuotaPreference actual *pb.QuotaPreference reader client.Reader } var _ directbase.Adapter = &apiQuotaPreferenceAdapter{} // Find retrieves the GCP resource. // Return true means the object is found. This triggers Adapter `Update` call. // Return false means the object is not found. This triggers Adapter `Create` call. func (a *apiQuotaPreferenceAdapter) Find(ctx context.Context) (bool, error) { log := klog.FromContext(ctx) log.V(2).Info("getting cloudquotas quotapreference", "name", a.id) req := &pb.GetQuotaPreferenceRequest{Name: a.id.String()} actual, err := a.gcpClient.GetQuotaPreference(ctx, req) if err != nil { if direct.IsNotFound(err) { return false, nil } return false, fmt.Errorf("getting cloudquotas quotapreference %q: %w", a.id, err) } log.V(2).Info("found cloudquotas quotapreference", "name", a.id) a.actual = actual return true, nil } // Create creates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response. func (a *apiQuotaPreferenceAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error { log := klog.FromContext(ctx) log.V(2).Info("creating cloudquotas quotapreference", "name", a.id) mapCtx := &direct.MapContext{} desired := a.desired.DeepCopy() resource := APIQuotaPreferenceSpec_ToProto(mapCtx, &desired.Spec) if mapCtx.Err() != nil { return mapCtx.Err() } // if not set it is generated by the server resource.Name = a.id.String() req := &pb.CreateQuotaPreferenceRequest{ Parent: a.id.Parent().String(), QuotaPreferenceId: a.id.ID(), QuotaPreference: resource, } created, err := a.gcpClient.CreateQuotaPreference(ctx, req) if err != nil { return fmt.Errorf("creating cloudquotas quotapreference %s: %w", a.id, err) } log.V(2).Info("successfully created cloudquotas quotapreference", "name", a.id) status := &krm.APIQuotaPreferenceStatus{} status.ObservedState = APIQuotaPreferenceObservedState_FromProto(mapCtx, created) if mapCtx.Err() != nil { return mapCtx.Err() } status.ExternalRef = direct.LazyPtr(created.Name) // Use the server-generated name return createOp.UpdateStatus(ctx, status, nil) } // Update updates the resource in GCP based on `spec` and update the Config Connector object `status` based on the GCP response. func (a *apiQuotaPreferenceAdapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error { log := klog.FromContext(ctx) log.V(2).Info("updating cloudquotas quotapreference", "name", a.id) mapCtx := &direct.MapContext{} desired := a.desired.DeepCopy() resource := APIQuotaPreferenceSpec_ToProto(mapCtx, &desired.Spec) if mapCtx.Err() != nil { return mapCtx.Err() } resource.Name = a.id.String() // Set name for update request paths := []string{} // Only quota_config, justification, contact_email are updatable. if desired.Spec.QuotaConfig != nil && !reflect.DeepEqual(resource.QuotaConfig, a.actual.QuotaConfig) { paths = append(paths, "quota_config") } if desired.Spec.Justification != nil && !reflect.DeepEqual(resource.Justification, a.actual.Justification) { paths = append(paths, "justification") } if desired.Spec.ContactEmail != nil && !reflect.DeepEqual(resource.ContactEmail, a.actual.ContactEmail) { paths = append(paths, "contact_email") } var updated *pb.QuotaPreference if len(paths) == 0 { log.V(2).Info("no field needs update", "name", a.id) updated = a.actual // Use the current actual state for status update } else { // Use the etag from the last read if not specified in spec resource.Etag = a.actual.Etag req := &pb.UpdateQuotaPreferenceRequest{ QuotaPreference: resource, UpdateMask: &fieldmaskpb.FieldMask{Paths: paths}, // Allow missing field behavior is default false. Not supported for now. // Ignore safety checks behavior is default false. Not supported for now. } var err error updated, err = a.gcpClient.UpdateQuotaPreference(ctx, req) if err != nil { return fmt.Errorf("updating cloudquotas quotapreference %s: %w", a.id.String(), err) } log.V(2).Info("successfully updated cloudquotas quotapreference", "name", a.id) } status := &krm.APIQuotaPreferenceStatus{} status.ObservedState = APIQuotaPreferenceObservedState_FromProto(mapCtx, updated) if mapCtx.Err() != nil { return mapCtx.Err() } status.ExternalRef = direct.LazyPtr(a.id.String()) return updateOp.UpdateStatus(ctx, status, nil) } // Export maps the GCP object to a Config Connector resource `spec`. func (a *apiQuotaPreferenceAdapter) Export(ctx context.Context) (*unstructured.Unstructured, error) { if a.actual == nil { return nil, fmt.Errorf("Find() not called") } u := &unstructured.Unstructured{} obj := &krm.APIQuotaPreference{} mapCtx := &direct.MapContext{} obj.Spec = direct.ValueOf(APIQuotaPreferenceSpec_FromProto(mapCtx, a.actual)) if mapCtx.Err() != nil { return nil, mapCtx.Err() } parentIdentity, resourceID, err := krm.ParseQuotaPreferenceExternal(a.actual.Name) if err != nil { return nil, fmt.Errorf("parsing parent from name %q: %w", a.actual.Name, err) } if parentIdentity.ProjectID != "" { obj.Spec.Parent.ProjectRef = &refs.ProjectRef{External: parentIdentity.ProjectID} } else if parentIdentity.FolderID != "" { obj.Spec.Parent.FolderRef = &refs.FolderRef{External: parentIdentity.FolderID} } else if parentIdentity.OrganizationID != "" { obj.Spec.Parent.OrganizationRef = &refs.OrganizationRef{External: parentIdentity.OrganizationID} } else { return nil, fmt.Errorf("unknown parent type in name %q", a.actual.Name) } uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) if err != nil { return nil, err } u.SetName(resourceID) // Use the server-generated ID as the K8s name u.SetGroupVersionKind(krm.APIQuotaPreferenceGVK) u.Object = uObj return u, nil } // Delete is not supported for QuotaPreference. func (a *apiQuotaPreferenceAdapter) Delete(ctx context.Context, deleteOp *directbase.DeleteOperation) (bool, error) { log := klog.FromContext(ctx) log.Info("delete operation is not supported for QuotaPreference, treating as no-op", "name", a.id.String()) return false, nil // Indicate that deletion is not supported and should not be retried. }