apis/iap/v1beta1/iapsettings_identity.go (264 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. package v1beta1 import ( "context" "fmt" "strings" "github.com/GoogleCloudPlatform/k8s-config-connector/apis/common" computev1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/apis/compute/v1beta1" refsv1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" ) // IAPSettingsIdentity defines the resource reference to IAPSettings. // The id could have the following format: // // organizations/{organization_id} // folders/{folder_id} // projects/{projects_id} // projects/{projects_id}/iap_web // projects/{projects_id}/iap_web/compute // projects/{projects_id}/iap_web/compute-{region} // projects/{projects_id}/iap_web/compute/services/{service_id} // projects/{projects_id}/iap_web/compute-{region}/services/{service_id} // projects/{projects_id}/iap_web/appengine-{app_id} // projects/{projects_id}/iap_web/appengine-{app_id}/services/{service_id} // projects/{projects_id}/iap_web/appengine-{app_id}/services/{service_id}/versions/{version_id} type IAPSettingsIdentity struct { id string } func (i *IAPSettingsIdentity) String() string { return i.id } func (i *IAPSettingsIdentity) ID() string { return i.id } // NewIAPSettingsIdentity builds a IAPSettingsIdentity from the Config Connector IAPSettings object. func NewIAPSettingsIdentity(ctx context.Context, reader client.Reader, obj *IAPSettings) (*IAPSettingsIdentity, error) { // Get desired ID resourceID := common.ValueOf(obj.Spec.ResourceID) if resourceID == "" { // Get resource ID from parent var err error resourceID, err = buildIAPSettingsIDFromParent(ctx, reader, obj) if err != nil { return nil, err } } if err := ValidateIAPSettingsID(resourceID); err != nil { return nil, err } // Use approved External externalRef := common.ValueOf(obj.Status.ExternalRef) if externalRef != "" { // Validate desired with actual actualResourceID := externalRef if err := ValidateIAPSettingsID(actualResourceID); err != nil { return nil, err } if actualResourceID != resourceID { return nil, fmt.Errorf("cannot reset `spec.resourceID` to %s, since it has already assigned to %s", resourceID, actualResourceID) } } return &IAPSettingsIdentity{ id: resourceID, }, nil } // buildIAPSettingsIDFromParent constructs the resource ID based on the parent reference type func buildIAPSettingsIDFromParent(ctx context.Context, reader client.Reader, obj *IAPSettings) (string, error) { parent, err := getParentReference(obj) if err != nil { return "", err } return parent.buildIAPSettingsID(ctx, reader, obj.GetNamespace()) } // parentReference is an interface that all parent reference types must implement type parentReference interface { buildIAPSettingsID(ctx context.Context, reader client.Reader, namespace string) (string, error) } // OrganizationParent represents organization-level settings type OrganizationParent struct { Ref *refsv1beta1.OrganizationRef } func (p OrganizationParent) buildIAPSettingsID(ctx context.Context, reader client.Reader, namespace string) (string, error) { organization, err := refsv1beta1.ResolveOrganization(ctx, reader, nil, p.Ref) if err != nil { return "", err } return fmt.Sprintf("organizations/%s", organization.OrganizationID), nil } // FolderParent represents folder-level settings type FolderParent struct { Ref *refsv1beta1.FolderRef } func (p FolderParent) buildIAPSettingsID(ctx context.Context, reader client.Reader, namespace string) (string, error) { folder, err := refsv1beta1.ResolveFolder(ctx, reader, nil, p.Ref) if err != nil { return "", err } return fmt.Sprintf("folders/%s", folder.FolderID), nil } // ProjectParent represents project-level settings type ProjectParent struct { Ref *refsv1beta1.ProjectRef } func (p ProjectParent) buildIAPSettingsID(ctx context.Context, reader client.Reader, namespace string) (string, error) { project, err := refsv1beta1.ResolveProject(ctx, reader, namespace, p.Ref) if err != nil { return "", err } return fmt.Sprintf("projects/%s", project.ProjectID), nil } // ProjectWebParent represents project-wide web service settings type ProjectWebParent struct { ProjectRef *refsv1beta1.ProjectRef } func (p ProjectWebParent) buildIAPSettingsID(ctx context.Context, reader client.Reader, namespace string) (string, error) { project, err := refsv1beta1.ResolveProject(ctx, reader, namespace, p.ProjectRef) if err != nil { return "", err } return fmt.Sprintf("projects/%s/iap_web", project.ProjectID), nil } // ComputeServiceParent represents project-wide Compute service settings type ComputeServiceParent struct { ProjectRef *refsv1beta1.ProjectRef Region *string ServiceRef *computev1beta1.ComputeBackendServiceRef } func (p ComputeServiceParent) buildIAPSettingsID(ctx context.Context, reader client.Reader, namespace string) (string, error) { project, err := refsv1beta1.ResolveProject(ctx, reader, namespace, p.ProjectRef) if err != nil { return "", err } if p.Region != nil { if p.ServiceRef != nil { external, err := p.ServiceRef.NormalizedExternal(ctx, reader, namespace) if err != nil { return "", err } serviceID, err := parseComputeBackendServiceID(external) if err != nil { return "", err } return fmt.Sprintf("projects/%s/iap_web/compute-%s/services/%s", project.ProjectID, *p.Region, serviceID), nil } return fmt.Sprintf("projects/%s/iap_web/compute-%s", project.ProjectID, *p.Region), nil } if p.ServiceRef != nil { external, err := p.ServiceRef.NormalizedExternal(ctx, reader, namespace) if err != nil { return "", err } serviceID, err := parseComputeBackendServiceID(external) if err != nil { return "", err } return fmt.Sprintf("projects/%s/iap_web/compute/services/%s", project.ProjectID, serviceID), nil } return fmt.Sprintf("projects/%s/iap_web/compute", project.ProjectID), nil } // AppEngineParent represents project-wide App Engine service settings type AppEngineParent struct { ProjectRef *refsv1beta1.ProjectRef ApplicationRef *refsv1beta1.AppEngineApplicationRef ServiceRef *refsv1beta1.AppEngineServiceRef VersionRef *refsv1beta1.AppEngineVersionRef } func (p AppEngineParent) buildIAPSettingsID(ctx context.Context, reader client.Reader, namespace string) (string, error) { project, err := refsv1beta1.ResolveProject(ctx, reader, namespace, p.ProjectRef) if err != nil { return "", err } appID, err := refsv1beta1.ResolveAppEngineApplicationID(ctx, reader, namespace, p.ApplicationRef) if err != nil { return "", err } if p.ServiceRef != nil { serviceID, err := refsv1beta1.ResolveAppEngineServiceID(ctx, reader, namespace, p.ServiceRef) if err != nil { return "", err } if p.VersionRef != nil { versionID, err := refsv1beta1.ResolveAppEngineVersionID(ctx, reader, namespace, p.VersionRef) if err != nil { return "", err } return fmt.Sprintf("projects/%s/iap_web/appengine-%s/services/%s/versions/%s", project.ProjectID, appID, serviceID, versionID), nil } return fmt.Sprintf("projects/%s/iap_web/appengine-%s/services/%s", project.ProjectID, appID, serviceID), nil } return fmt.Sprintf("projects/%s/iap_web/appengine-%s", project.ProjectID, appID), nil } // getParentReference extracts the appropriate parent reference from an IAPSettings object func getParentReference(obj *IAPSettings) (parentReference, error) { switch { case obj.Spec.OrganizationRef != nil: return OrganizationParent{Ref: obj.Spec.OrganizationRef}, nil case obj.Spec.FolderRef != nil: return FolderParent{Ref: obj.Spec.FolderRef}, nil case obj.Spec.ProjectRef != nil: return ProjectParent{Ref: obj.Spec.ProjectRef}, nil case obj.Spec.ProjectWebRef != nil: return ProjectWebParent{ProjectRef: obj.Spec.ProjectWebRef.ProjectRef}, nil case obj.Spec.ComputeServiceRef != nil: return ComputeServiceParent{ ProjectRef: obj.Spec.ComputeServiceRef.ProjectRef, Region: obj.Spec.ComputeServiceRef.Region, ServiceRef: obj.Spec.ComputeServiceRef.ServiceRef, }, nil case obj.Spec.AppEngineRef != nil: return AppEngineParent{ ProjectRef: obj.Spec.AppEngineRef.ProjectRef, ApplicationRef: obj.Spec.AppEngineRef.ApplicationRef, ServiceRef: obj.Spec.AppEngineRef.ServiceRef, VersionRef: obj.Spec.AppEngineRef.VersionRef, }, nil default: return nil, fmt.Errorf("no parent reference specified") } } // ValidateIAPSettingsID validates the IAPSettings resource ID. func ValidateIAPSettingsID(id string) error { if id == "" { return fmt.Errorf("id cannot be empty") } parts := strings.Split(id, "/") if len(parts) < 2 { return fmt.Errorf("invalid IAP settings ID format %q: must have at least 2 segments (e.g., 'projects/my-project')", id) } // Validate root resource type switch parts[0] { case "organizations", "folders", "projects": // Valid root types default: return fmt.Errorf("invalid root resource type %q: must be one of: organizations, folders, projects", parts[0]) } // For organization and folder paths, only expect 2 parts if parts[0] == "organizations" || parts[0] == "folders" { if len(parts) != 2 { return fmt.Errorf("invalid %s IAP settings path %q: must have exactly 2 segments (e.g., '%s/my-id')", parts[0], id, parts[0]) } return nil } // For project paths, validate the structure if len(parts) > 2 { if parts[2] != "iap_web" { return fmt.Errorf("invalid project IAP settings path %q: third segment must be 'iap_web', got %q", id, parts[2]) } } switch len(parts) { case 2: // projects/{project_id} return nil case 3: // projects/{project_id}/iap_web return nil case 4: // projects/{project_id}/iap_web/compute or compute-{region} if !strings.HasPrefix(parts[3], "compute") && !strings.HasPrefix(parts[3], "appengine-") { return fmt.Errorf("invalid IAP web resource type %q: must start with 'compute' or 'appengine-'", parts[3]) } case 6: // projects/{project_id}/iap_web/(compute|compute-{region}|appengine-{app_id})/services/{service_id} if parts[4] != "services" { return fmt.Errorf("invalid service path %q: fifth segment must be 'services', got %q", id, parts[4]) } case 8: // projects/{project_id}/iap_web/appengine-{app_id}/services/{service_id}/versions/{version_id} if !strings.HasPrefix(parts[3], "appengine-") { return fmt.Errorf("invalid path %q: version paths are only valid for App Engine resources (must start with 'appengine-')", id) } if parts[4] != "services" || parts[6] != "versions" { return fmt.Errorf("invalid App Engine version path %q: must follow pattern 'appengine-{app_id}/services/{service_id}/versions/{version_id}'", id) } default: return fmt.Errorf("invalid number of path segments in IAP settings ID %q: got %d segments, expected 2, 3, 4, 6, or 8", id, len(parts)) } return nil } func parseComputeBackendServiceID(selfLink string) (string, error) { // example global: https://www.googleapis.com/compute/v1/projects/${projectId}/global/backendServices/computebackendservice-${uniqueId} // example regional: https://www.googleapis.com/compute/v1/projects/${projectId}/regions/${location}/backendServices/computebackendservice-${uniqueId} if !strings.HasPrefix(selfLink, "https://www.googleapis.com/compute/v1/") { return "", fmt.Errorf("invalid selfLink %q: must start with 'https://www.googleapis.com/compute/v1/'", selfLink) } selfLink = strings.TrimPrefix(selfLink, "https://www.googleapis.com/compute/v1/") parts := strings.Split(selfLink, "/") switch len(parts) { case 5: // global if parts[0] != "projects" || parts[2] != "global" || parts[3] != "backendServices" { return "", fmt.Errorf("invalid selfLink %q: must have the format 'projects/{project_id}/global/backendServices/{backend_service_id}'", selfLink) } serviceID := parts[len(parts)-1] return serviceID, nil case 6: // regional if parts[0] != "projects" || parts[2] != "regions" || parts[4] != "backendServices" { return "", fmt.Errorf("invalid selfLink %q: must have the format 'projects/{project_id}/regions/{region}/backendServices/{backend_service_id}'", selfLink) } serviceID := parts[len(parts)-1] return serviceID, nil } return "", fmt.Errorf("invalid selfLink %q: must have at least 3 segments (e.g., 'projects/{project_id}/global/backendServices/{backend_service_id}')", selfLink) }