From 8a43c3117a5d326a47e5b5fa0c486654e27ec309 Mon Sep 17 00:00:00 2001 From: Gemma Hou Date: Mon, 22 Jul 2024 23:22:49 +0000 Subject: [PATCH] resolve resourceRef --- apis/refs/v1beta1/computenetworkref.go | 51 ++------- apis/refs/v1beta1/resourceid.go | 32 ------ apis/refs/v1beta1/resourceref.go | 126 ++++++++++++++++++++++ apis/refs/v1beta1/util.go | 127 +++++++++++++++++++++++ pkg/controller/direct/monitoring/refs.go | 2 +- 5 files changed, 264 insertions(+), 74 deletions(-) delete mode 100644 apis/refs/v1beta1/resourceid.go create mode 100644 apis/refs/v1beta1/resourceref.go create mode 100644 apis/refs/v1beta1/util.go diff --git a/apis/refs/v1beta1/computenetworkref.go b/apis/refs/v1beta1/computenetworkref.go index 6a789a97b8..cd4129cebc 100644 --- a/apis/refs/v1beta1/computenetworkref.go +++ b/apis/refs/v1beta1/computenetworkref.go @@ -26,6 +26,15 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +type ComputeNetworkRef struct { + /* The compute network selflink of form "projects//global/networks/", when not managed by KCC. */ + External string `json:"external,omitempty"` + /* The `name` field of a `ComputeNetwork` resource. */ + Name string `json:"name,omitempty"` + /* The `namespace` field of a `ComputeNetwork` resource. */ + Namespace string `json:"namespace,omitempty"` +} + type ComputeNetwork struct { Project string ComputeNetworkID string @@ -87,7 +96,7 @@ func ResolveComputeNetwork(ctx context.Context, reader client.Reader, src client computenetworkID = computenetwork.GetName() } - computeNetworkProjectID, err := ResolveProjectIDForObject(ctx, reader, computenetwork) + computeNetworkProjectID, err := GetProjectID(ctx, reader, computenetwork) if err != nil { return nil, err } @@ -96,43 +105,3 @@ func ResolveComputeNetwork(ctx context.Context, reader client.Reader, src client ComputeNetworkID: computenetworkID, }, nil } - -func ResolveProjectIDForObject(ctx context.Context, reader client.Reader, obj *unstructured.Unstructured) (string, error) { - projectRefExternal, _, _ := unstructured.NestedString(obj.Object, "spec", "projectRef", "external") - if projectRefExternal != "" { - projectRef := ProjectRef{ - External: projectRefExternal, - } - - project, err := ResolveProject(ctx, reader, obj, &projectRef) - if err != nil { - return "", fmt.Errorf("cannot parse projectRef.external %q in %v %v/%v: %w", projectRefExternal, obj.GetKind(), obj.GetNamespace(), obj.GetName(), err) - } - return project.ProjectID, nil - } - - projectRefName, _, _ := unstructured.NestedString(obj.Object, "spec", "projectRef", "name") - if projectRefName != "" { - projectRefNamespace, _, _ := unstructured.NestedString(obj.Object, "spec", "projectRef", "namespace") - - projectRef := ProjectRef{ - Name: projectRefName, - Namespace: projectRefNamespace, - } - if projectRef.Namespace == "" { - projectRef.Namespace = obj.GetNamespace() - } - - project, err := ResolveProject(ctx, reader, obj, &projectRef) - if err != nil { - return "", fmt.Errorf("cannot parse projectRef in %v %v/%v: %w", obj.GetKind(), obj.GetNamespace(), obj.GetName(), err) - } - return project.ProjectID, nil - } - - if projectID := obj.GetAnnotations()["cnrm.cloud.google.com/project-id"]; projectID != "" { - return projectID, nil - } - - return "", fmt.Errorf("cannot find project id for %v %v/%v", obj.GetKind(), obj.GetNamespace(), obj.GetName()) -} diff --git a/apis/refs/v1beta1/resourceid.go b/apis/refs/v1beta1/resourceid.go deleted file mode 100644 index 2513a1353f..0000000000 --- a/apis/refs/v1beta1/resourceid.go +++ /dev/null @@ -1,32 +0,0 @@ -// 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 ( - "fmt" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -func GetResourceID(u *unstructured.Unstructured) (string, error) { - resourceID, _, err := unstructured.NestedString(u.Object, "spec", "resourceID") - if err != nil { - return "", fmt.Errorf("reading spec.resourceID from %v %v/%v: %w", u.GroupVersionKind().Kind, u.GetNamespace(), u.GetName(), err) - } - if resourceID == "" { - resourceID = u.GetName() - } - return resourceID, nil -} diff --git a/apis/refs/v1beta1/resourceref.go b/apis/refs/v1beta1/resourceref.go new file mode 100644 index 0000000000..7556744aaf --- /dev/null +++ b/apis/refs/v1beta1/resourceref.go @@ -0,0 +1,126 @@ +// 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/pkg/clients/generated/apis/k8s/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func ComputeNetworkRef_ConvertToExternal(ctx context.Context, reader client.Reader, src client.Object, ref *v1alpha1.ResourceRef) (*v1alpha1.ResourceRef, error) { + // Validate the ResourceRef + if err := ValidateResourceRef(ref); err != nil { + return nil, err + } + + // Validate and get external references if exist + if ref.External != "" { + if err := validateExternalComputeNetwork(ref); err != nil { + return nil, err + } + return ref, nil + } + + // Convert references to external references + // Parse the ComputeNetwork resource + computenetwork, err := ParseResourceRef(ctx, reader, src, ref, "compute.cnrm.cloud.google.com", "v1beta1", "ComputeNetwork") + if err != nil { + return nil, err + } + + // Get the resourceID + computenetworkID, err := GetResourceID(computenetwork) + if err != nil { + return nil, err + } + + // Get the project ID + computeNetworkProjectID, err := GetProjectID(ctx, reader, computenetwork) + if err != nil { + return nil, err + } + + return &v1alpha1.ResourceRef{ + External: fmt.Sprintf("projects/%s/global/networks/%s", computeNetworkProjectID, computenetworkID), + }, nil +} + +func ComputeTargetHTTPProxyRef_ConvertToExternal(ctx context.Context, reader client.Reader, src client.Object, ref *v1alpha1.ResourceRef) (*v1alpha1.ResourceRef, error) { + if err := ValidateResourceRef(ref); err != nil { + return nil, err + } + + if ref.External != "" { + if err := validateExternalComputeTargetHTTPProxy(ref); err != nil { + return nil, err + } + return ref, nil + } + + computeTargetHTTPProxy, err := ParseResourceRef(ctx, reader, src, ref, "compute.cnrm.cloud.google.com", "v1beta1", "computeTargetHTTPProxy") + if err != nil { + return nil, err + } + computeTargetHTTPProxyID, err := GetResourceID(computeTargetHTTPProxy) + if err != nil { + return nil, err + } + computeTargetHTTPProxyProjectID, err := GetProjectID(ctx, reader, computeTargetHTTPProxy) + if err != nil { + return nil, err + } + location, err := GetLocation(computeTargetHTTPProxy) + if err != nil { + return nil, err + } + + var external string + if location == "global" { + external = fmt.Sprintf("projects/%s/global/targetHttpProxies/%s", computeTargetHTTPProxyProjectID, computeTargetHTTPProxyID) + } else { + external = fmt.Sprintf("projects/%s/location/%s/targetHttpProxies/%s", computeTargetHTTPProxyProjectID, location, computeTargetHTTPProxyID) + } + + return &v1alpha1.ResourceRef{ + External: external, + }, nil +} + +func validateExternalComputeNetwork(ref *v1alpha1.ResourceRef) error { + tokens := strings.Split(ref.External, "/") + if len(tokens) == 5 && tokens[0] == "projects" && tokens[2] == "global" && tokens[3] == "networks" { + return nil + } + return fmt.Errorf( + "format of ComputeNetwork external=%q was not known (use projects//global/networks/)", + ref.External) +} + +func validateExternalComputeTargetHTTPProxy(ref *v1alpha1.ResourceRef) error { + tokens := strings.Split(ref.External, "/") + if len(tokens) == 5 && tokens[0] == "projects" && tokens[2] == "global" && tokens[3] == "targetHttpProxies" { + return nil + } else if len(tokens) == 6 && tokens[0] == "projects" && tokens[2] == "location" && tokens[4] == "targetHttpProxies" { + return nil + } + return fmt.Errorf( + "format of ComputeTargetHTTPProxy external=%q was not known "+ + "(use projects//global/targetHttpProxies/ or projects//location//targetHttpProxies/)", + ref.External) +} diff --git a/apis/refs/v1beta1/util.go b/apis/refs/v1beta1/util.go new file mode 100644 index 0000000000..64cdde671c --- /dev/null +++ b/apis/refs/v1beta1/util.go @@ -0,0 +1,127 @@ +// 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" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/k8s/v1alpha1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func ValidateResourceRef(ref *v1alpha1.ResourceRef) error { + if ref == nil { + return nil + } + if ref.Name == "" && ref.External == "" { + return fmt.Errorf("must specify either name or external on reference") + } + if ref.Name != "" && ref.External != "" { + return fmt.Errorf("cannot specify both name and external on reference") + } + return nil +} + +func ParseResourceRef(ctx context.Context, reader client.Reader, src client.Object, ref *v1alpha1.ResourceRef, group, version, kind string) (*unstructured.Unstructured, error) { + key := types.NamespacedName{ + Namespace: ref.Namespace, + Name: ref.Name, + } + if key.Namespace == "" { + key.Namespace = src.GetNamespace() + } + + resource := &unstructured.Unstructured{} + resource.SetGroupVersionKind(schema.GroupVersionKind{ + Group: group, + Version: version, + Kind: kind, + }) + if err := reader.Get(ctx, key, resource); err != nil { + if apierrors.IsNotFound(err) { + return nil, fmt.Errorf("referenced %s %v not found", kind, key) + } + return nil, fmt.Errorf("error reading referenced %s %v: %w", kind, key, err) + } + return resource, nil +} + +func GetResourceID(u *unstructured.Unstructured) (string, error) { + resourceID, _, err := unstructured.NestedString(u.Object, "spec", "resourceID") + if err != nil { + return "", fmt.Errorf("reading spec.resourceID from %v %v/%v: %w", u.GroupVersionKind().Kind, u.GetNamespace(), u.GetName(), err) + } + if resourceID == "" { + resourceID = u.GetName() + } + return resourceID, nil +} + +func GetProjectID(ctx context.Context, reader client.Reader, obj *unstructured.Unstructured) (string, error) { + projectRefExternal, _, _ := unstructured.NestedString(obj.Object, "spec", "projectRef", "external") + if projectRefExternal != "" { + projectRef := ProjectRef{ + External: projectRefExternal, + } + + project, err := ResolveProject(ctx, reader, obj, &projectRef) + if err != nil { + return "", fmt.Errorf("cannot parse projectRef.external %q in %v %v/%v: %w", projectRefExternal, obj.GetKind(), obj.GetNamespace(), obj.GetName(), err) + } + return project.ProjectID, nil + } + + projectRefName, _, _ := unstructured.NestedString(obj.Object, "spec", "projectRef", "name") + if projectRefName != "" { + projectRefNamespace, _, _ := unstructured.NestedString(obj.Object, "spec", "projectRef", "namespace") + + projectRef := ProjectRef{ + Name: projectRefName, + Namespace: projectRefNamespace, + } + if projectRef.Namespace == "" { + projectRef.Namespace = obj.GetNamespace() + } + + project, err := ResolveProject(ctx, reader, obj, &projectRef) + if err != nil { + return "", fmt.Errorf("cannot parse projectRef in %v %v/%v: %w", obj.GetKind(), obj.GetNamespace(), obj.GetName(), err) + } + return project.ProjectID, nil + } + + if projectID := obj.GetAnnotations()["cnrm.cloud.google.com/project-id"]; projectID != "" { + return projectID, nil + } + + return "", fmt.Errorf("cannot find project id for %v %v/%v", obj.GetKind(), obj.GetNamespace(), obj.GetName()) +} + +// todo(yuhou): Location can be optional. Use default location when it's unset. +func GetLocation(obj *unstructured.Unstructured) (string, error) { + location, _, err := unstructured.NestedString(obj.Object, "spec", "location") + if err != nil { + return "", fmt.Errorf("cannot get location for referenced %s %v: %w", obj.GetKind(), obj.GetNamespace(), err) + } + if location == "" { + return "", fmt.Errorf("cannot get location for referenced %s %v (spec.location not set)", obj.GetKind(), obj.GetNamespace()) + } + return location, nil +} diff --git a/pkg/controller/direct/monitoring/refs.go b/pkg/controller/direct/monitoring/refs.go index a39ca07c6c..f602e9a557 100644 --- a/pkg/controller/direct/monitoring/refs.go +++ b/pkg/controller/direct/monitoring/refs.go @@ -143,7 +143,7 @@ func normalizeMonitoringAlertPolicyRef(ctx context.Context, reader client.Reader return nil, err } - alertPolicyProjectID, err := refs.ResolveProjectIDForObject(ctx, reader, alertPolicy) + alertPolicyProjectID, err := refs.GetProjectID(ctx, reader, alertPolicy) if err != nil { return nil, err }