diff --git a/apis/bigtable/v1alpha1/generate.sh b/apis/bigtable/v1alpha1/generate.sh index 9407bdeb7de..0ab51667067 100755 --- a/apis/bigtable/v1alpha1/generate.sh +++ b/apis/bigtable/v1alpha1/generate.sh @@ -30,9 +30,11 @@ go run . generate-types \ --resource BigtableBackup:Backup \ --resource BigtableCluster:Cluster \ --resource BigtableLogicalView:LogicalView \ - --resource BigtableMaterializedView:MaterializedView + --resource BigtableMaterializedView:MaterializedView \ + --resource BigtableSchemaBundle:SchemaBundle go run . generate-mapper \ + --multiversion \ --service google.bigtable.admin.v2 \ --api-version bigtable.cnrm.cloud.google.com/v1alpha1 diff --git a/apis/bigtable/v1alpha1/schemabundle_identity.go b/apis/bigtable/v1alpha1/schemabundle_identity.go new file mode 100644 index 00000000000..10301e3d4e4 --- /dev/null +++ b/apis/bigtable/v1alpha1/schemabundle_identity.go @@ -0,0 +1,112 @@ +// 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" + "fmt" + "strings" + + bigtablev1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/apis/bigtable/v1beta1" + "github.com/GoogleCloudPlatform/k8s-config-connector/apis/common" + "github.com/GoogleCloudPlatform/k8s-config-connector/apis/common/parent" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// SchemaBundleIdentity defines the resource reference to BigtableSchemaBundle, which "External" field +// holds the GCP identifier for the KRM object. +type SchemaBundleIdentity struct { + parent *bigtablev1beta1.TableIdentity + id string +} + +func (i *SchemaBundleIdentity) String() string { + return i.parent.String() + "/schemaBundles/" + i.id +} + +func (i *SchemaBundleIdentity) ID() string { + return i.id +} + +func (i *SchemaBundleIdentity) Parent() *bigtablev1beta1.TableIdentity { + return i.parent +} + +// New builds a SchemaBundleIdentity from the Config Connector SchemaBundle object. +func NewSchemaBundleIdentity(ctx context.Context, reader client.Reader, obj *BigtableSchemaBundle) (*SchemaBundleIdentity, error) { + + // Get Parent + tableRef, err := obj.Spec.TableRef.NormalizedExternal(ctx, reader, obj.GetNamespace()) + if err != nil { + return nil, err + } + tableParent, tableID, err := bigtablev1beta1.ParseTableExternal(tableRef) + 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 schema bundle name") + } + + // Use approved External + externalRef := common.ValueOf(obj.Status.ExternalRef) + if externalRef != "" { + // Validate desired with actual + actualParent, actualResourceID, err := ParseSchemaBundleExternal(externalRef) + if err != nil { + return nil, err + } + if actualParent.Parent.Parent.ProjectID != tableParent.Parent.ProjectID { + return nil, fmt.Errorf("ProjectID changed, expect %s, got %s", actualParent.Parent.Parent.ProjectID, tableParent.Parent.ProjectID) + } + if actualParent.Parent.Id != tableParent.Id { + return nil, fmt.Errorf("InstanceID changed, expect %s, got %s", actualParent.Parent.Id, tableParent.Id) + } + if actualParent.Id != tableID { + return nil, fmt.Errorf("TableID changed, expect %s, got %s", actualParent.Id, tableID) + } + 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 &SchemaBundleIdentity{ + parent: &bigtablev1beta1.TableIdentity{ + Parent: tableParent, + Id: tableID, + }, + id: resourceID, + }, nil +} + +func ParseSchemaBundleExternal(external string) (*bigtablev1beta1.TableIdentity, string, error) { + tokens := strings.Split(external, "/") + if len(tokens) != 8 || tokens[0] != "projects" || tokens[2] != "instances" || tokens[4] != "tables" || tokens[6] != "schemaBundles" { + return nil, "", fmt.Errorf("format of BigtableSchemaBundle external=%q was not known (use projects/{{projectID}}/instances/{{instanceID}}/tables/{{tableID}}/schemaBundles/{{schemaBundleID}})", external) + } + return &bigtablev1beta1.TableIdentity{ + Parent: &bigtablev1beta1.InstanceIdentity{ + Parent: &parent.ProjectParent{ProjectID: tokens[1]}, + Id: tokens[3], + }, + Id: tokens[5], + }, tokens[7], nil +} diff --git a/apis/bigtable/v1alpha1/schemabundle_reference.go b/apis/bigtable/v1alpha1/schemabundle_reference.go new file mode 100644 index 00000000000..2a596a25abe --- /dev/null +++ b/apis/bigtable/v1alpha1/schemabundle_reference.go @@ -0,0 +1,83 @@ +// 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" + "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 = &SchemaBundleRef{} + +// SchemaBundleRef defines the resource reference to BigtableSchemaBundle, which "External" field +// holds the GCP identifier for the KRM object. +type SchemaBundleRef struct { + // A reference to an externally managed BigtableSchemaBundle resource. + // Should be in the format "projects/{{projectID}}/instances/{{instanceID}}/tables/{{tableID}}/schemaBundles/{{schemaBundleID}}". + External string `json:"external,omitempty"` + + // The name of a BigtableSchemaBundle resource. + Name string `json:"name,omitempty"` + + // The namespace of a BigtableSchemaBundle resource. + Namespace string `json:"namespace,omitempty"` +} + +// NormalizedExternal provision the "External" value for other resource that depends on BigtableSchemaBundle. +// If the "External" is given in the other resource's spec.BigtableSchemaBundleRef, the given value will be used. +// Otherwise, the "Name" and "Namespace" will be used to query the actual BigtableSchemaBundle object from the cluster. +func (r *SchemaBundleRef) 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", BigtableSchemaBundleGVK.Kind) + } + // From given External + if r.External != "" { + if _, _, err := ParseSchemaBundleExternal(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(BigtableSchemaBundleGVK) + 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", BigtableSchemaBundleGVK, 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 +} diff --git a/apis/bigtable/v1alpha1/schemabundle_types.go b/apis/bigtable/v1alpha1/schemabundle_types.go new file mode 100644 index 00000000000..c1603d42465 --- /dev/null +++ b/apis/bigtable/v1alpha1/schemabundle_types.go @@ -0,0 +1,105 @@ +// 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 ( + bigtablev1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/apis/bigtable/v1beta1" + k8sv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/k8s/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var BigtableSchemaBundleGVK = GroupVersion.WithKind("BigtableSchemaBundle") + +// BigtableSchemaBundleSpec defines the desired state of BigtableSchemaBundle +// +kcc:spec:proto=google.bigtable.admin.v2.SchemaBundle +type BigtableSchemaBundleSpec struct { + // The table to create the schema bundle in. + // +required + TableRef bigtablev1beta1.TableRef `json:"tableRef"` + + // The BigtableSchemaBundle name. If not given, the metadata.name will be used. + ResourceID *string `json:"resourceID,omitempty"` + + // Optional. The protobuf schema for the table. + // +optional + ProtoSchema *ProtoSchema `json:"protoSchema,omitempty"` +} + +// ProtoSchema contains a protobuf-serialized FileDescriptorSet. +type ProtoSchema struct { + // Optional. The protobuf schema descriptor. + // The schema descriptor must be a file descriptor set, which can be generated using `protoc --descriptor_set_out=myschema.fd myschema.proto`. + // The file descriptor set must be base64 encoded. + // +optional + ProtoDescriptors []byte `json:"protoDescriptors,omitempty"` +} + +// BigtableSchemaBundleStatus defines the config connector machine state of BigtableSchemaBundle +type BigtableSchemaBundleStatus struct { + /* Conditions represent the latest available observations of the + object's current state. */ + Conditions []k8sv1alpha1.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 BigtableSchemaBundle resource in GCP. + ExternalRef *string `json:"externalRef,omitempty"` + + // ObservedState is the state of the resource as most recently observed in GCP. + ObservedState *BigtableSchemaBundleObservedState `json:"observedState,omitempty"` +} + +// BigtableSchemaBundleObservedState is the state of the BigtableSchemaBundle resource as most recently observed in GCP. +// +kcc:observedstate:proto=google.bigtable.admin.v2.SchemaBundle +type BigtableSchemaBundleObservedState struct { + // Optional. The etag for this schema bundle. + Etag *string `json:"etag,omitempty"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=gcp,shortName=gcpbigtableschemabundle;gcpbigtableschemabundles +// +kubebuilder:subresource:status +// +kubebuilder:metadata:labels="cnrm.cloud.google.com/managed-by-kcc=true" +// +kubebuilder:metadata:labels="cnrm.cloud.google.com/system=true" +// +kubebuilder:metadata:annotations="cnrm.cloud.google.com/owner=fkc1e100/Frank Currie/fcurrie" +// +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'" + +// BigtableSchemaBundle is the Schema for the BigtableSchemaBundle API +// +k8s:openapi-gen=true +type BigtableSchemaBundle struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // +required + Spec BigtableSchemaBundleSpec `json:"spec,omitempty"` + Status BigtableSchemaBundleStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// BigtableSchemaBundleList contains a list of BigtableSchemaBundle +type BigtableSchemaBundleList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []BigtableSchemaBundle `json:"items"` +} + +func init() { + SchemeBuilder.Register(&BigtableSchemaBundle{}, &BigtableSchemaBundleList{}) +} diff --git a/apis/bigtable/v1alpha1/types.generated.go b/apis/bigtable/v1alpha1/types.generated.go index 9175be135e1..00925dbf870 100644 --- a/apis/bigtable/v1alpha1/types.generated.go +++ b/apis/bigtable/v1alpha1/types.generated.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Code generated by dev/tasks/generate-all. DO NOT EDIT. // +generated:types // krm.group: bigtable.cnrm.cloud.google.com // krm.version: v1alpha1 @@ -21,6 +22,7 @@ // resource: BigtableCluster:Cluster // resource: BigtableLogicalView:LogicalView // resource: BigtableMaterializedView:MaterializedView +// resource: BigtableSchemaBundle:SchemaBundle package v1alpha1 diff --git a/apis/bigtable/v1alpha1/zz_generated.deepcopy.go b/apis/bigtable/v1alpha1/zz_generated.deepcopy.go index e45e78a9a56..52f01756e9d 100644 --- a/apis/bigtable/v1alpha1/zz_generated.deepcopy.go +++ b/apis/bigtable/v1alpha1/zz_generated.deepcopy.go @@ -1096,6 +1096,146 @@ func (in *BigtableMaterializedViewStatus) DeepCopy() *BigtableMaterializedViewSt return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BigtableSchemaBundle) DeepCopyInto(out *BigtableSchemaBundle) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BigtableSchemaBundle. +func (in *BigtableSchemaBundle) DeepCopy() *BigtableSchemaBundle { + if in == nil { + return nil + } + out := new(BigtableSchemaBundle) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BigtableSchemaBundle) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BigtableSchemaBundleList) DeepCopyInto(out *BigtableSchemaBundleList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]BigtableSchemaBundle, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BigtableSchemaBundleList. +func (in *BigtableSchemaBundleList) DeepCopy() *BigtableSchemaBundleList { + if in == nil { + return nil + } + out := new(BigtableSchemaBundleList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BigtableSchemaBundleList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BigtableSchemaBundleObservedState) DeepCopyInto(out *BigtableSchemaBundleObservedState) { + *out = *in + if in.Etag != nil { + in, out := &in.Etag, &out.Etag + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BigtableSchemaBundleObservedState. +func (in *BigtableSchemaBundleObservedState) DeepCopy() *BigtableSchemaBundleObservedState { + if in == nil { + return nil + } + out := new(BigtableSchemaBundleObservedState) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BigtableSchemaBundleSpec) DeepCopyInto(out *BigtableSchemaBundleSpec) { + *out = *in + out.TableRef = in.TableRef + if in.ResourceID != nil { + in, out := &in.ResourceID, &out.ResourceID + *out = new(string) + **out = **in + } + if in.ProtoSchema != nil { + in, out := &in.ProtoSchema, &out.ProtoSchema + *out = new(ProtoSchema) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BigtableSchemaBundleSpec. +func (in *BigtableSchemaBundleSpec) DeepCopy() *BigtableSchemaBundleSpec { + if in == nil { + return nil + } + out := new(BigtableSchemaBundleSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BigtableSchemaBundleStatus) DeepCopyInto(out *BigtableSchemaBundleStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]k8sv1alpha1.Condition, len(*in)) + copy(*out, *in) + } + if in.ObservedGeneration != nil { + in, out := &in.ObservedGeneration, &out.ObservedGeneration + *out = new(int64) + **out = **in + } + if in.ExternalRef != nil { + in, out := &in.ExternalRef, &out.ExternalRef + *out = new(string) + **out = **in + } + if in.ObservedState != nil { + in, out := &in.ObservedState, &out.ObservedState + *out = new(BigtableSchemaBundleObservedState) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BigtableSchemaBundleStatus. +func (in *BigtableSchemaBundleStatus) DeepCopy() *BigtableSchemaBundleStatus { + if in == nil { + return nil + } + out := new(BigtableSchemaBundleStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterIdentity) DeepCopyInto(out *ClusterIdentity) { *out = *in @@ -1311,6 +1451,61 @@ func (in *MaterializedViewRef) DeepCopy() *MaterializedViewRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProtoSchema) DeepCopyInto(out *ProtoSchema) { + *out = *in + if in.ProtoDescriptors != nil { + in, out := &in.ProtoDescriptors, &out.ProtoDescriptors + *out = make([]byte, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProtoSchema. +func (in *ProtoSchema) DeepCopy() *ProtoSchema { + if in == nil { + return nil + } + out := new(ProtoSchema) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SchemaBundleIdentity) DeepCopyInto(out *SchemaBundleIdentity) { + *out = *in + if in.parent != nil { + in, out := &in.parent, &out.parent + *out = new(v1beta1.TableIdentity) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SchemaBundleIdentity. +func (in *SchemaBundleIdentity) DeepCopy() *SchemaBundleIdentity { + if in == nil { + return nil + } + out := new(SchemaBundleIdentity) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SchemaBundleRef) DeepCopyInto(out *SchemaBundleRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SchemaBundleRef. +func (in *SchemaBundleRef) DeepCopy() *SchemaBundleRef { + if in == nil { + return nil + } + out := new(SchemaBundleRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Status) DeepCopyInto(out *Status) { *out = *in diff --git a/apis/bigtable/v1beta1/table_reference.go b/apis/bigtable/v1beta1/table_reference.go index d76c417180c..93608450150 100644 --- a/apis/bigtable/v1beta1/table_reference.go +++ b/apis/bigtable/v1beta1/table_reference.go @@ -34,10 +34,10 @@ type TableRef struct { // A reference to an externally managed BigtableTable resource. External string `json:"external,omitempty"` - // The name of a BigtableInstance resource. + // The name of a BigtableTable resource. Name string `json:"name,omitempty"` - // The namespace of a BigtableInstance resource. + // The namespace of a BigtableTable resource. Namespace string `json:"namespace,omitempty"` } diff --git a/apis/bigtable/v1beta1/types.generated.go b/apis/bigtable/v1beta1/types.generated.go index be2eb15c3fe..1081f52e33b 100644 --- a/apis/bigtable/v1beta1/types.generated.go +++ b/apis/bigtable/v1beta1/types.generated.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Code generated by dev/tasks/generate-all. DO NOT EDIT. // +generated:types // krm.group: bigtable.cnrm.cloud.google.com // krm.version: v1beta1 diff --git a/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_bigtableschemabundles.bigtable.cnrm.cloud.google.com.yaml b/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_bigtableschemabundles.bigtable.cnrm.cloud.google.com.yaml new file mode 100644 index 00000000000..06d635e1f27 --- /dev/null +++ b/config/crds/resources/apiextensions.k8s.io_v1_customresourcedefinition_bigtableschemabundles.bigtable.cnrm.cloud.google.com.yaml @@ -0,0 +1,169 @@ +# Generated by dev/tasks/generate-crds. DO NOT EDIT. + +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cnrm.cloud.google.com/owner: fkc1e100/Frank Currie/fcurrie + cnrm.cloud.google.com/version: 0.0.0-dev + creationTimestamp: null + labels: + cnrm.cloud.google.com/managed-by-kcc: "true" + cnrm.cloud.google.com/system: "true" + name: bigtableschemabundles.bigtable.cnrm.cloud.google.com +spec: + group: bigtable.cnrm.cloud.google.com + names: + categories: + - gcp + kind: BigtableSchemaBundle + listKind: BigtableSchemaBundleList + plural: bigtableschemabundles + shortNames: + - gcpbigtableschemabundle + - gcpbigtableschemabundles + singular: bigtableschemabundle + preserveUnknownFields: false + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: When 'True', the most recent reconcile of the resource succeeded + jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - description: The reason for the value in 'Ready' + jsonPath: .status.conditions[?(@.type=='Ready')].reason + name: Status + type: string + - description: The last transition time for the value in 'Status' + jsonPath: .status.conditions[?(@.type=='Ready')].lastTransitionTime + name: Status Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: BigtableSchemaBundle is the Schema for the BigtableSchemaBundle + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BigtableSchemaBundleSpec defines the desired state of BigtableSchemaBundle + properties: + protoSchema: + description: Optional. The protobuf schema for the table. + properties: + protoDescriptors: + description: Optional. The protobuf schema descriptor. The schema + descriptor must be a file descriptor set, which can be generated + using `protoc --descriptor_set_out=myschema.fd myschema.proto`. + The file descriptor set must be base64 encoded. + format: byte + type: string + type: object + resourceID: + description: The BigtableSchemaBundle name. If not given, the metadata.name + will be used. + type: string + tableRef: + description: The table to create the schema bundle in. + oneOf: + - not: + required: + - external + required: + - name + - not: + anyOf: + - required: + - name + - required: + - namespace + required: + - external + properties: + external: + description: A reference to an externally managed BigtableTable + resource. + type: string + name: + description: The name of a BigtableInstance resource. + type: string + namespace: + description: The namespace of a BigtableInstance resource. + type: string + type: object + required: + - tableRef + type: object + status: + description: BigtableSchemaBundleStatus defines the config connector machine + state of BigtableSchemaBundle + properties: + conditions: + description: Conditions represent the latest available observations + of the object's current state. + items: + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + type: string + message: + description: Human-readable message indicating details about + last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's + last transition. + type: string + status: + description: Status is the status of the condition. Can be True, + False, Unknown. + type: string + type: + description: Type is the type of the condition. + type: string + type: object + type: array + externalRef: + description: A unique specifier for the BigtableSchemaBundle resource + in GCP. + type: string + observedGeneration: + description: 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. + format: int64 + type: integer + observedState: + description: ObservedState is the state of the resource as most recently + observed in GCP. + properties: + etag: + description: Optional. The etag for this schema bundle. + type: string + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/docs/reports/crd_report.csv b/docs/reports/crd_report.csv index b597db90e92..c76d6be20fe 100644 --- a/docs/reports/crd_report.csv +++ b/docs/reports/crd_report.csv @@ -73,6 +73,7 @@ bigtable.cnrm.cloud.google.com,BigtableGCPolicy,False,True,False,Terraform,False bigtable.cnrm.cloud.google.com,BigtableInstance,False,True,False,Terraform,True bigtable.cnrm.cloud.google.com,BigtableLogicalView,True,False,False,Direct,True bigtable.cnrm.cloud.google.com,BigtableMaterializedView,True,False,False,Direct,True +bigtable.cnrm.cloud.google.com,BigtableSchemaBundle,True,False,False,Direct,True bigtable.cnrm.cloud.google.com,BigtableTable,False,True,False,Terraform,True billing.cnrm.cloud.google.com,BillingAccount,True,False,False,Direct,True billingbudgets.cnrm.cloud.google.com,BillingBudgetsBudget,False,True,False,DCL,False diff --git a/docs/reports/crd_report.md b/docs/reports/crd_report.md index d8aa6c8faed..5e9769ea654 100644 --- a/docs/reports/crd_report.md +++ b/docs/reports/crd_report.md @@ -76,6 +76,7 @@ | bigtable.cnrm.cloud.google.com | BigtableInstance | False | True | False | Terraform | True | | bigtable.cnrm.cloud.google.com | BigtableLogicalView | True | False | False | Direct | True | | bigtable.cnrm.cloud.google.com | BigtableMaterializedView | True | False | False | Direct | True | +| bigtable.cnrm.cloud.google.com | BigtableSchemaBundle | True | False | False | Direct | True | | bigtable.cnrm.cloud.google.com | BigtableTable | False | True | False | Terraform | True | | billing.cnrm.cloud.google.com | BillingAccount | True | False | False | Direct | True | | billingbudgets.cnrm.cloud.google.com | BillingBudgetsBudget | False | True | False | DCL | False | diff --git a/mockgcp/mockbigtable/table.go b/mockgcp/mockbigtable/table.go index 4cef9ec8cd9..ef2f2649f41 100644 --- a/mockgcp/mockbigtable/table.go +++ b/mockgcp/mockbigtable/table.go @@ -25,6 +25,7 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp/pkg/storage" + longrunningpb "google.golang.org/genproto/googleapis/longrunning" // Note: we use the "real" proto (not mockgcp), because the client uses GRPC. pb "cloud.google.com/go/bigtable/admin/apiv2/adminpb" @@ -192,6 +193,108 @@ func (s *tableAdminServer) DeleteTable(ctx context.Context, req *pb.DeleteTableR return &emptypb.Empty{}, nil } +func (s *tableAdminServer) CreateSchemaBundle(ctx context.Context, req *pb.CreateSchemaBundleRequest) (*longrunningpb.Operation, error) { + reqName := req.GetParent() + "/schemaBundles/" + req.GetSchemaBundleId() + name, err := s.parseSchemaBundleName(reqName) + if err != nil { + return nil, err + } + + fqn := name.String() + + obj := proto.Clone(req.SchemaBundle).(*pb.SchemaBundle) + obj.Name = fqn + + if err := s.storage.Create(ctx, fqn, obj); err != nil { + return nil, err + } + + return s.operations.DoneLRO(ctx, "", nil, obj) +} + +func (s *tableAdminServer) GetSchemaBundle(ctx context.Context, req *pb.GetSchemaBundleRequest) (*pb.SchemaBundle, error) { + name, err := s.parseSchemaBundleName(req.Name) + if err != nil { + return nil, err + } + + fqn := name.String() + + obj := &pb.SchemaBundle{} + if err := s.storage.Get(ctx, fqn, obj); err != nil { + return nil, err + } + + return obj, nil +} + +func (s *tableAdminServer) UpdateSchemaBundle(ctx context.Context, req *pb.UpdateSchemaBundleRequest) (*longrunningpb.Operation, error) { + name, err := s.parseSchemaBundleName(req.GetSchemaBundle().GetName()) + if err != nil { + return nil, err + } + + fqn := name.String() + + obj := &pb.SchemaBundle{} + if err := s.storage.Get(ctx, fqn, obj); err != nil { + return nil, err + } + + // TODO: Use updateMask + + if err := s.storage.Update(ctx, fqn, req.GetSchemaBundle()); err != nil { + return nil, err + } + + return s.operations.DoneLRO(ctx, "", nil, req.GetSchemaBundle()) +} + +func (s *tableAdminServer) DeleteSchemaBundle(ctx context.Context, req *pb.DeleteSchemaBundleRequest) (*emptypb.Empty, error) { + name, err := s.parseSchemaBundleName(req.Name) + if err != nil { + return nil, err + } + + fqn := name.String() + + deleted := &pb.SchemaBundle{} + if err := s.storage.Delete(ctx, fqn, deleted); err != nil { + return nil, err + } + + return &emptypb.Empty{}, nil +} + +type schemaBundleName struct { + tableName + SchemaBundleID string +} + +func (n *schemaBundleName) String() string { + return n.tableName.String() + "/schemaBundles/" + n.SchemaBundleID +} + +func (s *MockService) parseSchemaBundleName(name string) (*schemaBundleName, error) { + tokens := strings.Split(name, "/") + + if len(tokens) == 8 && tokens[6] == "schemaBundles" { + tableName, err := s.parseTableName(strings.Join(tokens[0:6], "/")) + if err != nil { + return nil, err + } + + name := &schemaBundleName{ + tableName: *tableName, + SchemaBundleID: tokens[7], + } + + return name, nil + } else { + return nil, status.Errorf(codes.InvalidArgument, "name %q is not valid", name) + } +} + type tableName struct { instanceName TableName string diff --git a/pkg/controller/direct/bigtable/bigtableschemabundle_controller.go b/pkg/controller/direct/bigtable/bigtableschemabundle_controller.go new file mode 100644 index 00000000000..a04fd5303a7 --- /dev/null +++ b/pkg/controller/direct/bigtable/bigtableschemabundle_controller.go @@ -0,0 +1,289 @@ +// 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 bigtable + +import ( + "context" + "fmt" + + krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/bigtable/v1alpha1" + bigtablev1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/apis/bigtable/v1beta1" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry" + + gcp "cloud.google.com/go/bigtable" + bigtablepb "cloud.google.com/go/bigtable/admin/apiv2/adminpb" + "google.golang.org/api/option" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/klog/v2" +) + +func init() { + registry.RegisterModel(krm.BigtableSchemaBundleGVK, NewBigtableSchemaBundleModel) +} + +func NewBigtableSchemaBundleModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) { + return &modelBigtableSchemaBundle{config: *config}, nil +} + +var _ directbase.Model = &modelBigtableSchemaBundle{} + +type modelBigtableSchemaBundle struct { + config config.ControllerConfig +} + +func (m *modelBigtableSchemaBundle) client(ctx context.Context, projectID, instanceID string) (*gcp.AdminClient, error) { + var opts []option.ClientOption + opts, err := m.config.GRPCClientOptions() + if err != nil { + return nil, fmt.Errorf("building BigtableSchemaBundle client options: %w", err) + } + gcpClient, err := gcp.NewAdminClient(ctx, projectID, instanceID, opts...) + if err != nil { + return nil, fmt.Errorf("building BigtableSchemaBundle client: %w", err) + } + return gcpClient, err +} + +func (m *modelBigtableSchemaBundle) AdapterForObject(ctx context.Context, op *directbase.AdapterForObjectOperation) (directbase.Adapter, error) { + u := op.GetUnstructured() + reader := op.Reader + obj := &krm.BigtableSchemaBundle{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil { + return nil, fmt.Errorf("error converting to %T: %w", obj, err) + } + + id, err := krm.NewSchemaBundleIdentity(ctx, reader, obj) + if err != nil { + return nil, err + } + + gcpClient, err := m.client(ctx, id.Parent().Parent.Parent.ProjectID, id.Parent().Parent.Id) + if err != nil { + return nil, err + } + return &BigtableSchemaBundleAdapter{ + id: id, + gcpClient: gcpClient, + desired: obj, + }, nil +} + +func (m *modelBigtableSchemaBundle) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) { + // TODO: Support URLs + return nil, nil +} + +type BigtableSchemaBundleAdapter struct { + id *krm.SchemaBundleIdentity + gcpClient *gcp.AdminClient + desired *krm.BigtableSchemaBundle + actual *gcp.SchemaBundleInfo +} + +var _ directbase.Adapter = &BigtableSchemaBundleAdapter{} + +func (a *BigtableSchemaBundleAdapter) Find(ctx context.Context) (bool, error) { + log := klog.FromContext(ctx) + log.V(2).Info("getting BigtableSchemaBundle", "name", a.id) + + bigtableschemabundleinfo, err := a.gcpClient.GetSchemaBundle(ctx, a.id.Parent().Id, a.id.ID()) + if err != nil { + if direct.IsNotFound(err) { + return false, nil + } + return false, fmt.Errorf("getting BigtableSchemaBundle %q: %w", a.id, err) + } + + a.actual = bigtableschemabundleinfo + return true, nil +} + +func (a *BigtableSchemaBundleAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error { + log := klog.FromContext(ctx) + log.V(2).Info("creating BigtableSchemaBundle", "name", a.id) + mapCtx := &direct.MapContext{} + + desired := a.desired.DeepCopy() + resource := BigtableSchemaBundleSpec_v1alpha1_ToProto(mapCtx, &desired.Spec) + if mapCtx.Err() != nil { + return mapCtx.Err() + } + + conf := &gcp.SchemaBundleConf{ + TableID: a.id.Parent().Id, + SchemaBundleID: a.id.ID(), + ProtoSchema: &gcp.ProtoSchemaInfo{ + ProtoDescriptors: resource.GetProtoSchema().GetProtoDescriptors(), + }, + } + + err := a.gcpClient.CreateSchemaBundle(ctx, conf) + if err != nil { + return fmt.Errorf("creating BigtableSchemaBundle %s: %w", a.id, err) + } + log.V(2).Info("successfully created BigtableSchemaBundle", "name", a.id) + + // Get the created resource to get the etag/status + created, err := a.gcpClient.GetSchemaBundle(ctx, a.id.Parent().Id, a.id.ID()) + if err != nil { + return fmt.Errorf("getting created BigtableSchemaBundle %s: %w", a.id, err) + } + + status := &krm.BigtableSchemaBundleStatus{} + // Map SchemaBundleInfo back to pb.SchemaBundle for the generated mapper + pbCreated := &bigtablepb.SchemaBundle{ + Name: a.id.String(), + Etag: created.Etag, + Type: &bigtablepb.SchemaBundle_ProtoSchema{ + ProtoSchema: &bigtablepb.ProtoSchema{ + ProtoDescriptors: created.SchemaBundle, + }, + }, + } + status.ObservedState = BigtableSchemaBundleObservedState_v1alpha1_FromProto(mapCtx, pbCreated) + if mapCtx.Err() != nil { + return mapCtx.Err() + } + status.ExternalRef = direct.LazyPtr(a.id.String()) + if err := createOp.UpdateStatus(ctx, status, nil); err != nil { + return err + } + + // Write resourceID into spec. + if err := unstructured.SetNestedField(createOp.GetUnstructured().Object, a.id.ID(), "spec", "resourceID"); err != nil { + return fmt.Errorf("error setting spec.resourceID: %w", err) + } + + return nil +} + +func (a *BigtableSchemaBundleAdapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error { + log := klog.FromContext(ctx) + log.V(2).Info("updating BigtableSchemaBundle", "name", a.id) + mapCtx := &direct.MapContext{} + + desired := a.desired.DeepCopy() + resource := BigtableSchemaBundleSpec_v1alpha1_ToProto(mapCtx, &desired.Spec) + if mapCtx.Err() != nil { + return mapCtx.Err() + } + + conf := gcp.UpdateSchemaBundleConf{ + SchemaBundleConf: gcp.SchemaBundleConf{ + TableID: a.id.Parent().Id, + SchemaBundleID: a.id.ID(), + ProtoSchema: &gcp.ProtoSchemaInfo{ + ProtoDescriptors: resource.GetProtoSchema().GetProtoDescriptors(), + }, + Etag: a.actual.Etag, + }, + } + + err := a.gcpClient.UpdateSchemaBundle(ctx, conf) + if err != nil { + return fmt.Errorf("updating BigtableSchemaBundle %s: %w", a.id, err) + } + log.V(2).Info("successfully updated BigtableSchemaBundle", "name", a.id) + + // Get the updated resource + updated, err := a.gcpClient.GetSchemaBundle(ctx, a.id.Parent().Id, a.id.ID()) + if err != nil { + return fmt.Errorf("getting updated BigtableSchemaBundle %s: %w", a.id, err) + } + + status := &krm.BigtableSchemaBundleStatus{} + pbUpdated := &bigtablepb.SchemaBundle{ + Name: a.id.String(), + Etag: updated.Etag, + Type: &bigtablepb.SchemaBundle_ProtoSchema{ + ProtoSchema: &bigtablepb.ProtoSchema{ + ProtoDescriptors: updated.SchemaBundle, + }, + }, + } + status.ObservedState = BigtableSchemaBundleObservedState_v1alpha1_FromProto(mapCtx, pbUpdated) + if mapCtx.Err() != nil { + return mapCtx.Err() + } + status.ExternalRef = direct.LazyPtr(a.id.String()) + return updateOp.UpdateStatus(ctx, status, nil) +} + +func (a *BigtableSchemaBundleAdapter) Export(ctx context.Context) (*unstructured.Unstructured, error) { + if a.actual == nil { + return nil, fmt.Errorf("Find() not called") + } + u := &unstructured.Unstructured{} + + obj := &krm.BigtableSchemaBundle{} + mapCtx := &direct.MapContext{} + + // Map SchemaBundleInfo back to pb.SchemaBundle for the generated mapper + pbActual := &bigtablepb.SchemaBundle{ + Name: a.id.String(), + Etag: a.actual.Etag, + Type: &bigtablepb.SchemaBundle_ProtoSchema{ + ProtoSchema: &bigtablepb.ProtoSchema{ + ProtoDescriptors: a.actual.SchemaBundle, + }, + }, + } + + obj.Spec = direct.ValueOf(BigtableSchemaBundleSpec_v1alpha1_FromProto(mapCtx, pbActual)) + if mapCtx.Err() != nil { + return nil, mapCtx.Err() + } + + // parent tableRef + tableID, resourceID, err := krm.ParseSchemaBundleExternal(a.id.String()) + if err != nil { + return nil, err + } + obj.Spec.TableRef = bigtablev1beta1.TableRef{External: tableID.String()} + obj.Spec.ResourceID = direct.LazyPtr(resourceID) + + uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, err + } + + u.SetName(a.id.ID()) + u.SetGroupVersionKind(krm.BigtableSchemaBundleGVK) + + u.Object = uObj + return u, nil +} + +func (a *BigtableSchemaBundleAdapter) Delete(ctx context.Context, deleteOp *directbase.DeleteOperation) (bool, error) { + log := klog.FromContext(ctx) + log.V(2).Info("deleting BigtableSchemaBundle", "name", a.id) + + err := a.gcpClient.DeleteSchemaBundle(ctx, a.id.Parent().Id, a.id.ID()) + if err != nil { + if direct.IsNotFound(err) { + // Return success if not found (assume it was already deleted). + log.V(2).Info("skipping delete for non-existent BigtableSchemaBundle, assuming it was already deleted", "name", a.id) + return true, nil + } + return false, fmt.Errorf("deleting BigtableSchemaBundle %s: %w", a.id, err) + } + log.V(2).Info("successfully deleted BigtableSchemaBundle", "name", a.id) + + return true, nil +} diff --git a/pkg/controller/direct/bigtable/bigtableschemabundle_mapper_test.go b/pkg/controller/direct/bigtable/bigtableschemabundle_mapper_test.go new file mode 100644 index 00000000000..dd6473bed60 --- /dev/null +++ b/pkg/controller/direct/bigtable/bigtableschemabundle_mapper_test.go @@ -0,0 +1,52 @@ +// 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 bigtable + +import ( + "reflect" + "testing" + + krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/bigtable/v1alpha1" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct" +) + +func TestBigtableSchemaBundleMapper(t *testing.T) { + mapCtx := &direct.MapContext{} + + protoDescriptors := []byte("test-descriptors") + krmSpec := &krm.BigtableSchemaBundleSpec{ + ProtoSchema: &krm.ProtoSchema{ + ProtoDescriptors: protoDescriptors, + }, + } + + protoObj := BigtableSchemaBundleSpec_v1alpha1_ToProto(mapCtx, krmSpec) + if mapCtx.Err() != nil { + t.Fatalf("Spec to Proto failed: %v", mapCtx.Err()) + } + + if !reflect.DeepEqual(protoObj.GetProtoSchema().ProtoDescriptors, protoDescriptors) { + t.Errorf("Proto descriptors mismatch, got %v, want %v", protoObj.GetProtoSchema().ProtoDescriptors, protoDescriptors) + } + + krmSpec2 := BigtableSchemaBundleSpec_v1alpha1_FromProto(mapCtx, protoObj) + if mapCtx.Err() != nil { + t.Fatalf("Proto to Spec failed: %v", mapCtx.Err()) + } + + if !reflect.DeepEqual(krmSpec2.ProtoSchema.ProtoDescriptors, protoDescriptors) { + t.Errorf("KRM descriptors mismatch, got %v, want %v", krmSpec2.ProtoSchema.ProtoDescriptors, protoDescriptors) + } +} diff --git a/pkg/controller/direct/bigtable/mapper.generated.go b/pkg/controller/direct/bigtable/mapper.generated.go index 24ef4d9793d..4f2164a201b 100644 --- a/pkg/controller/direct/bigtable/mapper.generated.go +++ b/pkg/controller/direct/bigtable/mapper.generated.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Code generated by dev/tasks/generate-all. DO NOT EDIT. // +generated:mapper // krm.group: bigtable.cnrm.cloud.google.com // krm.version: v1beta1 @@ -513,6 +514,44 @@ func BigtableMaterializedViewSpec_v1alpha1_ToProto(mapCtx *direct.MapContext, in out.DeletionProtection = direct.ValueOf(in.DeletionProtection) return out } +func BigtableSchemaBundleObservedState_v1alpha1_FromProto(mapCtx *direct.MapContext, in *pb.SchemaBundle) *krmbigtablev1alpha1.BigtableSchemaBundleObservedState { + if in == nil { + return nil + } + out := &krmbigtablev1alpha1.BigtableSchemaBundleObservedState{} + // MISSING: Name + out.Etag = direct.LazyPtr(in.GetEtag()) + return out +} +func BigtableSchemaBundleObservedState_v1alpha1_ToProto(mapCtx *direct.MapContext, in *krmbigtablev1alpha1.BigtableSchemaBundleObservedState) *pb.SchemaBundle { + if in == nil { + return nil + } + out := &pb.SchemaBundle{} + // MISSING: Name + out.Etag = direct.ValueOf(in.Etag) + return out +} +func BigtableSchemaBundleSpec_v1alpha1_FromProto(mapCtx *direct.MapContext, in *pb.SchemaBundle) *krmbigtablev1alpha1.BigtableSchemaBundleSpec { + if in == nil { + return nil + } + out := &krmbigtablev1alpha1.BigtableSchemaBundleSpec{} + // MISSING: Name + out.ProtoSchema = ProtoSchema_v1alpha1_FromProto(mapCtx, in.GetProtoSchema()) + return out +} +func BigtableSchemaBundleSpec_v1alpha1_ToProto(mapCtx *direct.MapContext, in *krmbigtablev1alpha1.BigtableSchemaBundleSpec) *pb.SchemaBundle { + if in == nil { + return nil + } + out := &pb.SchemaBundle{} + // MISSING: Name + if oneof := ProtoSchema_v1alpha1_ToProto(mapCtx, in.ProtoSchema); oneof != nil { + out.Type = &pb.SchemaBundle_ProtoSchema{ProtoSchema: oneof} + } + return out +} func ChangeStreamConfig_v1beta1_FromProto(mapCtx *direct.MapContext, in *pb.ChangeStreamConfig) *krm.ChangeStreamConfig { if in == nil { return nil diff --git a/pkg/controller/direct/bigtable/mapper_test.go b/pkg/controller/direct/bigtable/mapper_test.go index 2194995ea9b..69468f36d8d 100644 --- a/pkg/controller/direct/bigtable/mapper_test.go +++ b/pkg/controller/direct/bigtable/mapper_test.go @@ -15,10 +15,12 @@ package bigtable import ( + "reflect" "testing" gcp "cloud.google.com/go/bigtable" pb "cloud.google.com/go/bigtable/admin/apiv2/adminpb" + krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/bigtable/v1alpha1" ) func TestBigtableMaterializedViewInfo_ToBigtableMaterializedView(t *testing.T) { @@ -121,3 +123,33 @@ func TestBigtableMaterializedView_ToBigtableMaterializedViewInfo(t *testing.T) { } }) } + +func TestProtoSchema_v1alpha1_Mappers(t *testing.T) { + testBytes := []byte{0x00, 0x01, 0x02, 0x03, 0xff} + + t.Run("ToProto", func(t *testing.T) { + in := &krm.ProtoSchema{ + ProtoDescriptors: testBytes, + } + got := ProtoSchema_v1alpha1_ToProto(nil, in) + if got == nil { + t.Fatalf("unexpected nil result") + } + if !reflect.DeepEqual(got.ProtoDescriptors, testBytes) { + t.Errorf("ProtoDescriptors: got %v, want %v", got.ProtoDescriptors, testBytes) + } + }) + + t.Run("FromProto", func(t *testing.T) { + in := &pb.ProtoSchema{ + ProtoDescriptors: testBytes, + } + got := ProtoSchema_v1alpha1_FromProto(nil, in) + if got == nil { + t.Fatalf("unexpected nil result") + } + if !reflect.DeepEqual(got.ProtoDescriptors, testBytes) { + t.Errorf("ProtoDescriptors: got %v, want %v", got.ProtoDescriptors, testBytes) + } + }) +} diff --git a/pkg/controller/direct/bigtable/schemabundle_fuzzer.go b/pkg/controller/direct/bigtable/schemabundle_fuzzer.go new file mode 100644 index 00000000000..a83b91cabd5 --- /dev/null +++ b/pkg/controller/direct/bigtable/schemabundle_fuzzer.go @@ -0,0 +1,41 @@ +// 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. + +// +tool:fuzz-gen +// proto.message: google.bigtable.admin.v2.SchemaBundle +// api.group: bigtable.cnrm.cloud.google.com + +package bigtable + +import ( + pb "cloud.google.com/go/bigtable/admin/apiv2/adminpb" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/fuzztesting" +) + +func init() { + fuzztesting.RegisterKRMSpecFuzzer(bigtableSchemaBundleFuzzer()) +} + +func bigtableSchemaBundleFuzzer() fuzztesting.KRMFuzzer { + f := fuzztesting.NewKRMTypedSpecFuzzer(&pb.SchemaBundle{}, + BigtableSchemaBundleSpec_v1alpha1_FromProto, BigtableSchemaBundleSpec_v1alpha1_ToProto, + ) + + f.SpecFields.Insert(".proto_schema") + + f.UnimplementedFields.Insert(".name") // special field + f.Unimplemented_Etag() + + return f +} diff --git a/pkg/controller/direct/bigtable/schemabundle_mapper.go b/pkg/controller/direct/bigtable/schemabundle_mapper.go new file mode 100644 index 00000000000..aee2fe4e907 --- /dev/null +++ b/pkg/controller/direct/bigtable/schemabundle_mapper.go @@ -0,0 +1,39 @@ +// 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 bigtable + +import ( + pb "cloud.google.com/go/bigtable/admin/apiv2/adminpb" + krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/bigtable/v1alpha1" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct" +) + +func ProtoSchema_v1alpha1_FromProto(mapCtx *direct.MapContext, in *pb.ProtoSchema) *krm.ProtoSchema { + if in == nil { + return nil + } + out := &krm.ProtoSchema{} + out.ProtoDescriptors = in.ProtoDescriptors + return out +} + +func ProtoSchema_v1alpha1_ToProto(mapCtx *direct.MapContext, in *krm.ProtoSchema) *pb.ProtoSchema { + if in == nil { + return nil + } + out := &pb.ProtoSchema{} + out.ProtoDescriptors = in.ProtoDescriptors + return out +} diff --git a/pkg/controller/resourceconfig/static_config.go b/pkg/controller/resourceconfig/static_config.go index 2ab9a95ebb7..67b0ac15439 100644 --- a/pkg/controller/resourceconfig/static_config.go +++ b/pkg/controller/resourceconfig/static_config.go @@ -82,6 +82,7 @@ var ControllerConfigStatic = ResourcesControllerMap{ {Group: "bigtable.cnrm.cloud.google.com", Kind: "BigtableInstance"}: {DefaultController: k8s.ReconcilerTypeTerraform, SupportedControllers: []k8s.ReconcilerType{k8s.ReconcilerTypeTerraform}}, {Group: "bigtable.cnrm.cloud.google.com", Kind: "BigtableLogicalView"}: {DefaultController: k8s.ReconcilerTypeDirect, SupportedControllers: []k8s.ReconcilerType{k8s.ReconcilerTypeDirect}}, {Group: "bigtable.cnrm.cloud.google.com", Kind: "BigtableMaterializedView"}: {DefaultController: k8s.ReconcilerTypeDirect, SupportedControllers: []k8s.ReconcilerType{k8s.ReconcilerTypeDirect}}, + {Group: "bigtable.cnrm.cloud.google.com", Kind: "BigtableSchemaBundle"}: {DefaultController: k8s.ReconcilerTypeDirect, SupportedControllers: []k8s.ReconcilerType{k8s.ReconcilerTypeDirect}}, {Group: "bigtable.cnrm.cloud.google.com", Kind: "BigtableTable"}: {DefaultController: k8s.ReconcilerTypeTerraform, SupportedControllers: []k8s.ReconcilerType{k8s.ReconcilerTypeTerraform}}, {Group: "billingbudgets.cnrm.cloud.google.com", Kind: "BillingBudgetsBudget"}: {DefaultController: k8s.ReconcilerTypeDCL, SupportedControllers: []k8s.ReconcilerType{k8s.ReconcilerTypeDCL}}, {Group: "binaryauthorization.cnrm.cloud.google.com", Kind: "BinaryAuthorizationAttestor"}: {DefaultController: k8s.ReconcilerTypeDCL, SupportedControllers: []k8s.ReconcilerType{k8s.ReconcilerTypeDCL}}, diff --git a/pkg/test/resourcefixture/testdata/basic/bigtable/v1alpha1/bigtableschemabundle/basic/create.yaml b/pkg/test/resourcefixture/testdata/basic/bigtable/v1alpha1/bigtableschemabundle/basic/create.yaml new file mode 100644 index 00000000000..1ab8b075931 --- /dev/null +++ b/pkg/test/resourcefixture/testdata/basic/bigtable/v1alpha1/bigtableschemabundle/basic/create.yaml @@ -0,0 +1,24 @@ +# 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. + +apiVersion: bigtable.cnrm.cloud.google.com/v1alpha1 +kind: BigtableSchemaBundle +metadata: + name: bigtableschemabundle-${uniqueId} +spec: + tableRef: + name: bigtabletable-dep-${uniqueId} + protoSchema: + # Dummy base64 encoded descriptors + protoDescriptors: dGVzdCBkZXNjcmlwdG9ycw== diff --git a/pkg/test/resourcefixture/testdata/basic/bigtable/v1alpha1/bigtableschemabundle/basic/dependencies.yaml b/pkg/test/resourcefixture/testdata/basic/bigtable/v1alpha1/bigtableschemabundle/basic/dependencies.yaml new file mode 100644 index 00000000000..7aaf6e5cefd --- /dev/null +++ b/pkg/test/resourcefixture/testdata/basic/bigtable/v1alpha1/bigtableschemabundle/basic/dependencies.yaml @@ -0,0 +1,35 @@ +# 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. + +apiVersion: bigtable.cnrm.cloud.google.com/v1beta1 +kind: BigtableInstance +metadata: + name: bigtable-dep-${uniqueId} +spec: + displayName: Bigtable Instance Dep + cluster: + - clusterId: cluster-1 + zone: us-central1-b + numNodes: 1 + storageType: SSD +--- +apiVersion: bigtable.cnrm.cloud.google.com/v1beta1 +kind: BigtableTable +metadata: + name: bigtabletable-dep-${uniqueId} +spec: + instanceRef: + name: bigtable-dep-${uniqueId} + columnFamily: + - family: family1