Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add types, mappers and fuzzer for GKEBackupRestore #4259

Merged
3 changes: 2 additions & 1 deletion apis/gkebackup/v1alpha1/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ go run . generate-types \
--api-version gkebackup.cnrm.cloud.google.com/v1alpha1 \
--resource GKEBackupBackupPlan:BackupPlan \
--resource GKEBackupRestorePlan:RestorePlan \
--resource GKEBackupBackup:Backup
--resource GKEBackupBackup:Backup \
--resource GKEBackupRestore:Restore

go run . generate-mapper \
--service google.cloud.gkebackup.v1 \
Expand Down
106 changes: 106 additions & 0 deletions apis/gkebackup/v1alpha1/restore_identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2025 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"
"fmt"
"strings"

"github.com/GoogleCloudPlatform/k8s-config-connector/apis/common"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// RestoreIdentity defines the resource reference to GKEBackupRestore, which "External" field
// holds the GCP identifier for the KRM object.
type RestoreIdentity struct {
parent *RestoreParent
id string
}

func (i *RestoreIdentity) String() string {
return i.parent.String() + "/restores/" + i.id
}

func (i *RestoreIdentity) ID() string {
return i.id
}

func (i *RestoreIdentity) Parent() *RestoreParent {
return i.parent
}

type RestoreParent struct {
RestorePlan string
}

func (p *RestoreParent) String() string {
return p.RestorePlan
}

// New builds a RestoreIdentity from the Config Connector Restore object.
func NewRestoreIdentity(ctx context.Context, reader client.Reader, obj *GKEBackupRestore) (*RestoreIdentity, error) {
// Get Parent
restorePlanRef := obj.Spec.RestorePlanRef
restorePlan, err := restorePlanRef.NormalizedExternal(ctx, reader, obj.GetNamespace())
if err != nil {
return nil, err
}

// Get desired ID
resourceID := common.ValueOf(obj.Spec.ResourceID)
if resourceID == "" {
resourceID = obj.GetName()
}
if resourceID == "" {
return nil, fmt.Errorf("cannot resolve resource ID")
}

// Use approved External
externalRef := common.ValueOf(obj.Status.ExternalRef)
if externalRef != "" {
// Validate desired with actual
actualParent, actualResourceID, err := ParseRestoreExternal(externalRef)
if err != nil {
return nil, err
}
if actualParent.RestorePlan != restorePlan {
return nil, fmt.Errorf("spec.restorePlanRef changed, expect %s, got %s", actualParent.RestorePlan, restorePlan)
}
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)
}
}
return &RestoreIdentity{
parent: &RestoreParent{
RestorePlan: restorePlan,
},
id: resourceID,
}, nil
}

func ParseRestoreExternal(external string) (parent *RestoreParent, resourceID string, err error) {
tokens := strings.Split(external, "/")
if len(tokens) != 8 || tokens[0] != "projects" || tokens[2] != "locations" || tokens[4] != "restorePlans" || tokens[6] != "restores" {
return nil, "", fmt.Errorf("format of GKEBackupRestore external=%q was not known (use projects/{{projectID}}/locations/{{location}}/restorePlans/{{restoreplanID}}/restores/{{restoreID}})", external)
}
restorePlan := strings.Join(tokens[:len(tokens)-2], "/")
parent = &RestoreParent{
RestorePlan: restorePlan,
}
resourceID = tokens[7]
return parent, resourceID, nil
}
83 changes: 83 additions & 0 deletions apis/gkebackup/v1alpha1/restore_reference.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2025 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"
"fmt"

refsv1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var _ refsv1beta1.ExternalNormalizer = &RestoreRef{}

// RestoreRef defines the resource reference to GKEBackupRestore, which "External" field
// holds the GCP identifier for the KRM object.
type RestoreRef struct {
// A reference to an externally managed GKEBackupRestore resource.
// Should be in the format "projects/{{projectID}}/locations/{{location}}/restorePlans/{{restoreplanID}}/restores/{{restoreID}}".
External string `json:"external,omitempty"`

// The name of a GKEBackupRestore resource.
Name string `json:"name,omitempty"`

// The namespace of a GKEBackupRestore resource.
Namespace string `json:"namespace,omitempty"`
}

// NormalizedExternal provision the "External" value for other resource that depends on GKEBackupRestore.
// If the "External" is given in the other resource's spec.GKEBackupRestoreRef, the given value will be used.
// Otherwise, the "Name" and "Namespace" will be used to query the actual GKEBackupRestore object from the cluster.
func (r *RestoreRef) NormalizedExternal(ctx context.Context, reader client.Reader, otherNamespace string) (string, error) {
if r.External != "" && r.Name != "" {
return "", fmt.Errorf("cannot specify both name and external on %s reference", GKEBackupRestoreGVK.Kind)
}
// From given External
if r.External != "" {
if _, _, err := ParseRestoreExternal(r.External); err != nil {
return "", err
}
return r.External, nil
}

// From the Config Connector object
if r.Namespace == "" {
r.Namespace = otherNamespace
}
key := types.NamespacedName{Name: r.Name, Namespace: r.Namespace}
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(GKEBackupRestoreGVK)
if err := reader.Get(ctx, key, u); err != nil {
if apierrors.IsNotFound(err) {
return "", k8s.NewReferenceNotFoundError(u.GroupVersionKind(), key)
}
return "", fmt.Errorf("reading referenced %s %s: %w", GKEBackupRestoreGVK, key, err)
}
// Get external from status.externalRef. This is the most trustworthy place.
actualExternalRef, _, err := unstructured.NestedString(u.Object, "status", "externalRef")
if err != nil {
return "", fmt.Errorf("reading status.externalRef: %w", err)
}
if actualExternalRef == "" {
return "", k8s.NewReferenceNotReadyError(u.GroupVersionKind(), key)
}
r.External = actualExternalRef
return r.External, nil
}
196 changes: 196 additions & 0 deletions apis/gkebackup/v1alpha1/restore_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Copyright 2025 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 (
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/k8s/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var GKEBackupRestoreGVK = GroupVersion.WithKind("GKEBackupRestore")

// GKEBackupRestoreSpec defines the desired state of GKEBackupRestore
// +kcc:proto=google.cloud.gkebackup.v1.Restore
type GKEBackupRestoreSpec struct {
// The GKEBackupRestore name. If not given, the metadata.name will be used.
ResourceID *string `json:"resourceID,omitempty"`

// Required. The RestorePlan from which this Restore is created.
// +required
RestorePlanRef *RestorePlanRef `json:"restorePlanRef,omitempty"`

// User specified descriptive string for this Restore.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.description
Description *string `json:"description,omitempty"`

// Required. Immutable. A reference to the
// [Backup][google.cloud.gkebackup.v1.Backup] used as the source from which
// this Restore will restore. Note that this Backup must be a sub-resource of
// the RestorePlan's
// [backup_plan][google.cloud.gkebackup.v1.RestorePlan.backup_plan].
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.backup
BackupRef *BackupRef `json:"backupRef,omitempty"`

// A set of custom labels supplied by user.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.labels
Labels map[string]string `json:"labels,omitempty"`

// Optional. Immutable. Filters resources for `Restore`. If not specified, the
// scope of the restore will remain the same as defined in the `RestorePlan`.
// If this is specified, and no resources are matched by the
// `inclusion_filters` or everything is excluded by the `exclusion_filters`,
// nothing will be restored. This filter can only be specified if the value of
// [namespaced_resource_restore_mode][google.cloud.gkebackup.v1.RestoreConfig.namespaced_resource_restore_mode]
// is set to `MERGE_SKIP_ON_CONFLICT`, `MERGE_REPLACE_VOLUME_ON_CONFLICT` or
// `MERGE_REPLACE_ON_CONFLICT`.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.filter
Filter *Restore_Filter `json:"filter,omitempty"`

// Optional. Immutable. Overrides the volume data restore policies selected in
// the Restore Config for override-scoped resources.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.volume_data_restore_policy_overrides
VolumeDataRestorePolicyOverrides []VolumeDataRestorePolicyOverride `json:"volumeDataRestorePolicyOverrides,omitempty"`
}

// GKEBackupRestoreStatus defines the config connector machine state of GKEBackupRestore
type GKEBackupRestoreStatus struct {
/* Conditions represent the latest available observations of the
object's current state. */
Conditions []v1alpha1.Condition `json:"conditions,omitempty"`

// ObservedGeneration is the generation of the resource that was most recently observed by the Config Connector controller. If this is equal to metadata.generation, then that means that the current reported status reflects the most recent desired state of the resource.
ObservedGeneration *int64 `json:"observedGeneration,omitempty"`

// A unique specifier for the GKEBackupRestore resource in GCP.
ExternalRef *string `json:"externalRef,omitempty"`

// ObservedState is the state of the resource as most recently observed in GCP.
ObservedState *GKEBackupRestoreObservedState `json:"observedState,omitempty"`
}

// GKEBackupRestoreObservedState is the state of the GKEBackupRestore resource as most recently observed in GCP.
// +kcc:proto=google.cloud.gkebackup.v1.Restore
type GKEBackupRestoreObservedState struct {
// Output only. The full name of the Restore resource.
// Format: `projects/*/locations/*/restorePlans/*/restores/*`
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.name
// NOTYET: this field serves the same purpose as externalRef
// Name *string `json:"name,omitempty"`

// Output only. Server generated global unique identifier of
// [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) format.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.uid
UID *string `json:"uid,omitempty"`

// Output only. The timestamp when this Restore resource was created.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.create_time
CreateTime *string `json:"createTime,omitempty"`

// Output only. The timestamp when this Restore resource was last
// updated.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.update_time
UpdateTime *string `json:"updateTime,omitempty"`

// Output only. The target cluster into which this Restore will restore data.
// Valid formats:
//
// - `projects/*/locations/*/clusters/*`
// - `projects/*/zones/*/clusters/*`
//
// Inherited from parent RestorePlan's
// [cluster][google.cloud.gkebackup.v1.RestorePlan.cluster] value.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.cluster
Cluster *string `json:"cluster,omitempty"`

// Output only. Configuration of the Restore. Inherited from parent
// RestorePlan's
// [restore_config][google.cloud.gkebackup.v1.RestorePlan.restore_config].
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.restore_config
RestoreConfig *RestoreConfig `json:"restoreConfig,omitempty"`

// Output only. The current state of the Restore.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.state
State *string `json:"state,omitempty"`

// Output only. Human-readable description of why the Restore is in its
// current state.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.state_reason
StateReason *string `json:"stateReason,omitempty"`

// Output only. Timestamp of when the restore operation completed.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.complete_time
CompleteTime *string `json:"completeTime,omitempty"`

// Output only. Number of resources restored during the restore execution.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.resources_restored_count
ResourcesRestoredCount *int32 `json:"resourcesRestoredCount,omitempty"`

// Output only. Number of resources excluded during the restore execution.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.resources_excluded_count
ResourcesExcludedCount *int32 `json:"resourcesExcludedCount,omitempty"`

// Output only. Number of resources that failed to be restored during the
// restore execution.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.resources_failed_count
ResourcesFailedCount *int32 `json:"resourcesFailedCount,omitempty"`

// Output only. Number of volumes restored during the restore execution.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.volumes_restored_count
VolumesRestoredCount *int32 `json:"volumesRestoredCount,omitempty"`

// Output only. `etag` is used for optimistic concurrency control as a way to
// help prevent simultaneous updates of a restore from overwriting each other.
// It is strongly suggested that systems make use of the `etag` in the
// read-modify-write cycle to perform restore updates in order to avoid
// race conditions: An `etag` is returned in the response to `GetRestore`,
// and systems are expected to put that etag in the request to
// `UpdateRestore` or `DeleteRestore` to ensure that their change will be
// applied to the same version of the resource.
// +kcc:proto:field=google.cloud.gkebackup.v1.Restore.etag
Etag *string `json:"etag,omitempty"`
}

// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:resource:categories=gcp,shortName=gcpgkebackuprestore;gcpgkebackuprestores
// +kubebuilder:subresource:status
// +kubebuilder:metadata:labels="cnrm.cloud.google.com/managed-by-kcc=true";"cnrm.cloud.google.com/system=true"
// +kubebuilder:printcolumn:name="Age",JSONPath=".metadata.creationTimestamp",type="date"
// +kubebuilder:printcolumn:name="Ready",JSONPath=".status.conditions[?(@.type=='Ready')].status",type="string",description="When 'True', the most recent reconcile of the resource succeeded"
// +kubebuilder:printcolumn:name="Status",JSONPath=".status.conditions[?(@.type=='Ready')].reason",type="string",description="The reason for the value in 'Ready'"
// +kubebuilder:printcolumn:name="Status Age",JSONPath=".status.conditions[?(@.type=='Ready')].lastTransitionTime",type="date",description="The last transition time for the value in 'Status'"

// GKEBackupRestore is the Schema for the GKEBackupRestore API
// +k8s:openapi-gen=true
type GKEBackupRestore struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

// +required
Spec GKEBackupRestoreSpec `json:"spec,omitempty"`
Status GKEBackupRestoreStatus `json:"status,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// GKEBackupRestoreList contains a list of GKEBackupRestore
type GKEBackupRestoreList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []GKEBackupRestore `json:"items"`
}

func init() {
SchemeBuilder.Register(&GKEBackupRestore{}, &GKEBackupRestoreList{})
}
Loading
Loading