Skip to content

Commit

Permalink
Merge pull request #3234 from yuwenma/code-talks-parent
Browse files Browse the repository at this point in the history
feat: A new parent interface
  • Loading branch information
google-oss-prow[bot] authored Nov 27, 2024
2 parents 45eab90 + 56046b9 commit d192546
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 1 deletion.
37 changes: 37 additions & 0 deletions apis/common/parent/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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 parent

import (
"context"

"sigs.k8s.io/controller-runtime/pkg/client"
)

type Parent interface {
// The external format of the Parent.
String() string
// Verify that the desired parent (from .spec) matches the actual parent in .status.externalRef.
// This ensures the parent remains unchanged.
// We currently don't enforce parent immutability using a webhook or CRD CEL due to legacy reasons.
MatchActual(Parent) error
}

// ParentBuilder builds a Parent object from a ParentRef.
// - ParentRef is the Config Connector API reference for identifying a resource's logical parent.
// - The Parent object provides helper functions for parent-related logic in direct reconciliation.
type ParentBuilder interface {
// Parent API reference builds its corresponding Parent object.
Build(ctx context.Context, reader client.Reader, othernamespace string, parent Parent) error
}
132 changes: 132 additions & 0 deletions apis/common/parent/project.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// 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 parent

import (
"context"
"fmt"
"strings"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var _ Parent = &ProjectParent{}

type ProjectParent struct {
ProjectID string
}

var _ ParentBuilder = &ProjectRef{}

// Project specifies the resource's GCP hierarchy (Project/Folder/Organization).
// +kubebuilder:object:generate:=true
type ProjectRef struct {
/* The `projectID` field of a project, when not managed by Config Connector. */
External string `json:"external,omitempty"`
/* The `name` field of a `Project` resource. */
Name string `json:"name,omitempty"`
/* The `namespace` field of a `Project` resource. */
Namespace string `json:"namespace,omitempty"`
}

// Builds a the ProjectParent from ProjectRef.
// If `projectRef.external` is given, parse projectID from External, otherwise find the ConfigConnector project
// according to `projectRef.name` and `projectRef.namespace`.
func (p *ProjectRef) Build(ctx context.Context, reader client.Reader, othernamespace string, parent Parent) error {
projectParent, ok := parent.(*ProjectParent)
if !ok {
return fmt.Errorf("build invalid parent, except %T", &ProjectParent{})
}
if p.External != "" {
if p.Name != "" {
return fmt.Errorf("cannot specify both name and external on project reference")
}

tokens := strings.Split(p.External, "/")
if len(tokens) == 1 {
projectParent.ProjectID = tokens[0]
return nil
}
if len(tokens) == 2 && tokens[0] == "projects" {
projectParent.ProjectID = tokens[1]
return nil
}
return fmt.Errorf("format of project external=%q was not known (use projects/<projectId> or <projectId>)", p.External)
}

if p.Name == "" {
return fmt.Errorf("must specify either name or external on project reference")
}

key := types.NamespacedName{
Namespace: p.Namespace,
Name: p.Name,
}
if key.Namespace == "" {
key.Namespace = othernamespace
}

project := &unstructured.Unstructured{}
project.SetGroupVersionKind(schema.GroupVersionKind{
Group: "resourcemanager.cnrm.cloud.google.com",
Version: "v1beta1",
Kind: "Project",
})
if err := reader.Get(ctx, key, project); err != nil {
if apierrors.IsNotFound(err) {
return fmt.Errorf("referenced Project %v not found", key)
}
return fmt.Errorf("error reading referenced Project %v: %w", key, err)
}

projectID, _, err := unstructured.NestedString(project.Object, "spec", "resourceID")
if err != nil {
return fmt.Errorf("reading spec.resourceID from %v %v/%v: %w", project.GroupVersionKind().Kind, p.Namespace, p.Name, err)
}
if projectID == "" {
projectID = project.GetName()
}
projectParent.ProjectID = projectID
return nil
}

func ParseProjectParent(external string) (*ProjectParent, error) {
tokens := strings.Split(external, "/")
if len(tokens) != 2 || tokens[0] != "projects" {
return nil, fmt.Errorf("format of Project external=%q was not known (use projects/<projectId>)", external)
}

return &ProjectParent{
ProjectID: tokens[1],
}, nil
}

func (p *ProjectParent) String() string {
return "projects/" + p.ProjectID
}

func (p *ProjectParent) MatchActual(actualI Parent) error {
actual, ok := actualI.(*ProjectParent)
if !ok {
return fmt.Errorf("parent format changed, desired %T", p)
}
if p.ProjectID != actual.ProjectID {
return fmt.Errorf("spec.projectRef changed, desired %s, actual %s", p.ProjectID, actual.ProjectID)
}
return nil
}
88 changes: 88 additions & 0 deletions apis/common/parent/projectlocation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// 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 parent

import (
"context"
"fmt"
"strings"

"sigs.k8s.io/controller-runtime/pkg/client"
)

var _ Parent = &ProjectAndLocationParent{}

type ProjectAndLocationParent struct {
ProjectID string
Location string
}

func (p *ProjectAndLocationParent) String() string {
return "projects/" + p.ProjectID + "/locations/" + p.Location
}

func (p *ProjectAndLocationParent) MatchActual(actualI Parent) error {
actual, ok := actualI.(*ProjectAndLocationParent)
if !ok {
return fmt.Errorf("parent format changed, desired %T", p)
}
if p.ProjectID != actual.ProjectID {
return fmt.Errorf("spec.projectRef changed, desired %s, actual %s", p.ProjectID, actual.ProjectID)
}
if p.Location != actual.Location {
return fmt.Errorf("spec.location changed, desired %s, actual %s", p.Location, actual.Location)
}
return nil
}

var _ ParentBuilder = &ProjectAndLocationRef{}

// ProjectAndLocationParent specifies the resource's GCP hierarchy (Project/Folder/Organization) and its geographical location.
// +kubebuilder:object:generate:=true
type ProjectAndLocationRef struct {
// +required
ProjectRef *ProjectRef `json:"projectRef"`

// +required
Location string `json:"location"`
}

func (p *ProjectAndLocationRef) Build(ctx context.Context, reader client.Reader, othernamespace string, parent Parent) error {
projectAndLocation, ok := parent.(*ProjectAndLocationParent)
if !ok {
return fmt.Errorf("build invalid parent, except %T", &ProjectAndLocationParent{})
}
project := new(ProjectParent)
if err := p.ProjectRef.Build(ctx, reader, othernamespace, project); err != nil {
return err
}
if project.ProjectID == "" {
return fmt.Errorf("cannot resolve project")
}
projectAndLocation.ProjectID = project.ProjectID
projectAndLocation.Location = p.Location
return nil
}

func ParseProjectAndLocationParent(external string) (*ProjectAndLocationParent, error) {
tokens := strings.Split(external, "/")
if len(tokens) != 4 || tokens[0] != "projects" || tokens[2] != "locations" {
return nil, fmt.Errorf("format of ProjectAndLocation external=%q was not known (use projects/<projectId>/locations/<location>)", external)
}

return &ProjectAndLocationParent{
ProjectID: tokens[1],
Location: tokens[3],
}, nil
}
54 changes: 54 additions & 0 deletions apis/common/parent/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion dev/tasks/generate-crds
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,7 @@ done

# clean up apis/config/crd for now
cd ${REPO_ROOT}
rm -rf apis/config/crd
rm -rf apis/config/crd

# controller-gen sometimes leaves empty imports in the api files.
go run -mod=readonly golang.org/x/tools/cmd/goimports@latest -w apis

0 comments on commit d192546

Please sign in to comment.