Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 60 additions & 64 deletions apis/compute/v1alpha1/networkattachment_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,100 +17,96 @@ package v1alpha1
import (
"context"
"fmt"
"strings"

"github.com/GoogleCloudPlatform/k8s-config-connector/apis/common"
refsv1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
"github.com/GoogleCloudPlatform/k8s-config-connector/apis/common/identity"
refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gcpurls"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// NetworkAttachmentIdentity is the identity of a ComputeNetworkAttachment.
type NetworkAttachmentIdentity struct {
parent *NetworkAttachmentParent
id string
}
var (
_ identity.IdentityV2 = &ComputeNetworkAttachmentIdentity{}
_ identity.Resource = &ComputeNetworkAttachment{}
)

func (i *NetworkAttachmentIdentity) String() string {
return i.parent.String() + "/networkAttachments/" + i.id
}
var ComputeNetworkAttachmentIdentityFormat = gcpurls.Template[ComputeNetworkAttachmentIdentity](
"compute.googleapis.com",
"projects/{project}/regions/{region}/networkAttachments/{networkattachment}",
)

func (i *NetworkAttachmentIdentity) ID() string {
return i.id
// ComputeNetworkAttachmentIdentity is the identity of a GCP ComputeNetworkAttachment resource.
// +k8s:deepcopy-gen=false
type ComputeNetworkAttachmentIdentity struct {
Project string
Region string
NetworkAttachment string
}

func (i *NetworkAttachmentIdentity) Parent() *NetworkAttachmentParent {
return i.parent
func (i *ComputeNetworkAttachmentIdentity) String() string {
return ComputeNetworkAttachmentIdentityFormat.ToString(*i)
}

type NetworkAttachmentParent struct {
ProjectID string
Location string
func (i *ComputeNetworkAttachmentIdentity) FromExternal(ref string) error {
parsed, match, err := ComputeNetworkAttachmentIdentityFormat.Parse(ref)
if err != nil {
return fmt.Errorf("format of ComputeNetworkAttachment external=%q was not known (use %s): %w", ref, ComputeNetworkAttachmentIdentityFormat.CanonicalForm(), err)
}
if !match {
return fmt.Errorf("format of ComputeNetworkAttachment external=%q was not known (use %s)", ref, ComputeNetworkAttachmentIdentityFormat.CanonicalForm())
}

*i = *parsed
return nil
}

func (p *NetworkAttachmentParent) String() string {
return "projects/" + p.ProjectID + "/regions/" + p.Location
func (i *ComputeNetworkAttachmentIdentity) Host() string {
return ComputeNetworkAttachmentIdentityFormat.Host()
}

// New builds a NetworkAttachmentIdentity from the Config Connector NetworkAttachment object.
func NewNetworkAttachmentIdentity(ctx context.Context, reader client.Reader, obj *ComputeNetworkAttachment) (*NetworkAttachmentIdentity, error) {
func getIdentityFromComputeNetworkAttachmentSpec(ctx context.Context, reader client.Reader, obj *ComputeNetworkAttachment) (*ComputeNetworkAttachmentIdentity, error) {
resourceID, err := refs.GetResourceID(obj)
if err != nil {
return nil, fmt.Errorf("cannot resolve resource ID")
}

// Get Parent
projectRef, err := refsv1beta1.ResolveProject(ctx, reader, obj.GetNamespace(), obj.Spec.ProjectRef)
location, err := refs.GetLocation(obj)
if err != nil {
return nil, err
return nil, fmt.Errorf("cannot resolve location")
}
projectID := projectRef.ProjectID
if projectID == "" {

projectID, err := refs.ResolveProjectID(ctx, reader, obj)
if err != nil {
return nil, fmt.Errorf("cannot resolve project")
}
location := obj.Spec.Location

// Get desired ID
resourceID := common.ValueOf(obj.Spec.ResourceID)
if resourceID == "" {
resourceID = obj.GetName()
identity := &ComputeNetworkAttachmentIdentity{
Project: projectID,
Region: location,
NetworkAttachment: resourceID,
}
if resourceID == "" {
return nil, fmt.Errorf("cannot resolve resource ID")
return identity, nil
}

func (obj *ComputeNetworkAttachment) GetIdentity(ctx context.Context, reader client.Reader) (identity.Identity, error) {
specIdentity, err := getIdentityFromComputeNetworkAttachmentSpec(ctx, reader, obj)
if err != nil {
return nil, err
}

// Use approved External
// Cross-check the identity against the status value, if present.
externalRef := common.ValueOf(obj.Status.ExternalRef)
if externalRef != "" {
// Validate desired with actual
actualParent, actualResourceID, err := ParseNetworkAttachmentExternal(externalRef)
if err != nil {
statusIdentity := &ComputeNetworkAttachmentIdentity{}
if err := statusIdentity.FromExternal(externalRef); err != nil {
return nil, err
}
if actualParent.ProjectID != projectID {
return nil, fmt.Errorf("spec.projectRef changed, expect %s, got %s", actualParent.ProjectID, projectID)
}
if actualParent.Location != location {
return nil, fmt.Errorf("spec.location changed, expect %s, got %s", actualParent.Location, location)
}
if actualResourceID != resourceID {
return nil, fmt.Errorf("cannot reset `metadata.name` or `spec.resourceID` to %s, since it has already assigned to %s",
resourceID, actualResourceID)

if statusIdentity.String() != specIdentity.String() {
return nil, fmt.Errorf("cannot change ComputeNetworkAttachment identity (old=%q, new=%q)", statusIdentity.String(), specIdentity.String())
}
}
return &NetworkAttachmentIdentity{
parent: &NetworkAttachmentParent{
ProjectID: projectID,
Location: location,
},
id: resourceID,
}, nil
}

func ParseNetworkAttachmentExternal(external string) (parent *NetworkAttachmentParent, resourceID string, err error) {
tokens := strings.Split(external, "/")
if len(tokens) != 6 || tokens[0] != "projects" || tokens[2] != "regions" || tokens[4] != "networkAttachments" {
return nil, "", fmt.Errorf("format of ComputeNetworkAttachment external=%q was not known (use projects/{{projectID}}/regions/{{location}}/networkAttachments/{{networkattachmentID}})", external)
}
parent = &NetworkAttachmentParent{
ProjectID: tokens[1],
Location: tokens[3],
}
resourceID = tokens[5]
return parent, resourceID, nil
return specIdentity, nil
}
220 changes: 220 additions & 0 deletions apis/compute/v1alpha1/networkattachment_identity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// Copyright 2026 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 v1alpha1

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestComputeNetworkAttachmentIdentity_FromExternal(t *testing.T) {
tests := []struct {
name string
ref string
wantErr bool
want *ComputeNetworkAttachmentIdentity
}{
{
name: "valid reference",
ref: "projects/my-project/regions/us-central1/networkAttachments/my-na",
want: &ComputeNetworkAttachmentIdentity{
Project: "my-project",
Region: "us-central1",
NetworkAttachment: "my-na",
},
},
{
name: "invalid reference format",
ref: "invalid/format",
wantErr: true,
},
{
name: "full url",
ref: "https://compute.googleapis.com/projects/my-project/regions/us-central1/networkAttachments/my-na",
want: &ComputeNetworkAttachmentIdentity{
Project: "my-project",
Region: "us-central1",
NetworkAttachment: "my-na",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
i := &ComputeNetworkAttachmentIdentity{}
err := i.FromExternal(tt.ref)
if (err != nil) != tt.wantErr {
t.Fatalf("FromExternal() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr {
if diff := cmp.Diff(tt.want, i); diff != "" {
t.Errorf("FromExternal() mismatch (-want +got):\n%s", diff)
}
}
})
}
}

func TestComputeNetworkAttachmentRef_ValidateExternal(t *testing.T) {
tests := []struct {
name string
ref string
wantErr bool
}{
{
name: "valid reference",
ref: "projects/my-project/regions/us-central1/networkAttachments/my-na",
wantErr: false,
},
{
name: "invalid prefix",
ref: "invalid/my-project/regions/us-central1/networkAttachments/my-na",
wantErr: true,
},
{
name: "missing region",
ref: "projects/my-project/networkAttachments/my-na",
wantErr: true,
},
{
name: "missing networkAttachment",
ref: "projects/my-project/regions/us-central1",
wantErr: true,
},
{
name: "empty string",
ref: "",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &ComputeNetworkAttachmentRef{}
if err := r.ValidateExternal(tt.ref); (err != nil) != tt.wantErr {
t.Errorf("ComputeNetworkAttachmentRef.ValidateExternal() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestComputeNetworkAttachmentRef_Normalize(t *testing.T) {
testCases := []struct {
name string
ref *ComputeNetworkAttachmentRef
otherNamespace string
objects []runtime.Object
wantExternal string
wantErr string
}{
{
name: "external with valid format",
ref: &ComputeNetworkAttachmentRef{
External: "projects/test-project/regions/us-central1/networkAttachments/test-na",
},
wantExternal: "projects/test-project/regions/us-central1/networkAttachments/test-na",
},
{
name: "external with invalid format",
ref: &ComputeNetworkAttachmentRef{
External: "invalid-format",
},
wantErr: `format of ComputeNetworkAttachment external="invalid-format" was not known (use projects/{project}/regions/{region}/networkAttachments/{networkattachment})`,
},
{
name: "name specified, with status.externalRef",
ref: &ComputeNetworkAttachmentRef{
Name: "test-na",
Namespace: "my-namespace",
},
objects: []runtime.Object{
&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "compute.cnrm.cloud.google.com/v1alpha1",
"kind": "ComputeNetworkAttachment",
"metadata": map[string]interface{}{
"name": "test-na",
"namespace": "my-namespace",
},
"status": map[string]interface{}{
"externalRef": "projects/test-project/regions/us-central1/networkAttachments/test-na",
},
},
},
},
wantExternal: "projects/test-project/regions/us-central1/networkAttachments/test-na",
},
{
name: "name specified, without status.externalRef",
ref: &ComputeNetworkAttachmentRef{
Name: "test-na",
Namespace: "my-namespace",
},
objects: []runtime.Object{
&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "compute.cnrm.cloud.google.com/v1alpha1",
"kind": "ComputeNetworkAttachment",
"metadata": map[string]interface{}{
"name": "test-na",
"namespace": "my-namespace",
},
"status": map[string]interface{}{},
},
},
},
wantErr: `reference ComputeNetworkAttachment my-namespace/test-na is not ready`,
},
{
name: "name specified, resource not found",
ref: &ComputeNetworkAttachmentRef{
Name: "test-na",
Namespace: "my-namespace",
},
wantErr: `reference ComputeNetworkAttachment my-namespace/test-na is not found`,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := runtime.NewScheme()
s.AddKnownTypes(GroupVersion, &unstructured.Unstructured{})
s.AddKnownTypes(GroupVersion, &ComputeNetworkAttachment{})
cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(tc.objects...).Build()

err := tc.ref.Normalize(context.TODO(), cl, "default")
if tc.wantErr != "" {
if err == nil {
t.Fatalf("Normalize() expected error %q, got nil", tc.wantErr)
}
if err.Error() != tc.wantErr {
t.Errorf("Normalize() error = %q, want %q", err.Error(), tc.wantErr)
}
return
}
if err != nil {
t.Fatalf("Normalize() unexpected error: %v", err)
}
if tc.ref.External != tc.wantExternal {
t.Errorf("Normalize() external = %q, want %q", tc.ref.External, tc.wantExternal)
}
})
}
}
Loading
Loading