Skip to content

Commit 12ed2ec

Browse files
Merge pull request #3702 from xiaoweim/vertexai-metadatastore-api-mapper
Add API and mapper and controller for Vertex AI Metadatastore
2 parents c1dc258 + 8942c16 commit 12ed2ec

File tree

26 files changed

+1595
-129
lines changed

26 files changed

+1595
-129
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package v1alpha1
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"strings"
21+
22+
"github.com/GoogleCloudPlatform/k8s-config-connector/apis/common"
23+
refsv1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
24+
"sigs.k8s.io/controller-runtime/pkg/client"
25+
)
26+
27+
// MetadataStoreIdentity defines the resource reference to VertexAIMetadataStore, which "External" field
28+
// holds the GCP identifier for the KRM object.
29+
type MetadataStoreIdentity struct {
30+
parent *MetadataStoreParent
31+
id string
32+
}
33+
34+
func (i *MetadataStoreIdentity) String() string {
35+
return i.parent.String() + "/metadataStores/" + i.id
36+
}
37+
38+
func (i *MetadataStoreIdentity) ID() string {
39+
return i.id
40+
}
41+
42+
func (i *MetadataStoreIdentity) Parent() *MetadataStoreParent {
43+
return i.parent
44+
}
45+
46+
type MetadataStoreParent struct {
47+
ProjectID string
48+
Location string
49+
}
50+
51+
func (p *MetadataStoreParent) String() string {
52+
return "projects/" + p.ProjectID + "/locations/" + p.Location
53+
}
54+
55+
// New builds a MetadataStoreIdentity from the Config Connector MetadataStore object.
56+
func NewMetadataStoreIdentity(ctx context.Context, reader client.Reader, obj *VertexAIMetadataStore) (*MetadataStoreIdentity, error) {
57+
58+
// Get Parent
59+
projectRef, err := refsv1beta1.ResolveProject(ctx, reader, obj.GetNamespace(), obj.Spec.ProjectRef)
60+
if err != nil {
61+
return nil, err
62+
}
63+
projectID := projectRef.ProjectID
64+
if projectID == "" {
65+
return nil, fmt.Errorf("cannot resolve project")
66+
}
67+
location := obj.Spec.Region
68+
69+
// Get desired ID
70+
resourceID := common.ValueOf(obj.Spec.ResourceID)
71+
if resourceID == "" {
72+
resourceID = obj.GetName()
73+
}
74+
if resourceID == "" {
75+
return nil, fmt.Errorf("cannot resolve resource ID")
76+
}
77+
78+
// Use approved External
79+
externalRef := common.ValueOf(obj.Status.ExternalRef)
80+
if externalRef != "" {
81+
// Validate desired with actual
82+
actualParent, actualResourceID, err := ParseMetadataStoreExternal(externalRef)
83+
if err != nil {
84+
return nil, err
85+
}
86+
if actualParent.ProjectID != projectID {
87+
return nil, fmt.Errorf("spec.projectRef changed, expect %s, got %s", actualParent.ProjectID, projectID)
88+
}
89+
if actualParent.Location != location {
90+
return nil, fmt.Errorf("spec.location changed, expect %s, got %s", actualParent.Location, location)
91+
}
92+
if actualResourceID != resourceID {
93+
return nil, fmt.Errorf("cannot reset `metadata.name` or `spec.resourceID` to %s, since it has already assigned to %s",
94+
resourceID, actualResourceID)
95+
}
96+
}
97+
return &MetadataStoreIdentity{
98+
parent: &MetadataStoreParent{
99+
ProjectID: projectID,
100+
Location: location,
101+
},
102+
id: resourceID,
103+
}, nil
104+
}
105+
106+
func ParseMetadataStoreExternal(external string) (parent *MetadataStoreParent, resourceID string, err error) {
107+
tokens := strings.Split(external, "/")
108+
if len(tokens) != 6 || tokens[0] != "projects" || tokens[2] != "locations" || tokens[4] != "metadataStores" {
109+
return nil, "", fmt.Errorf("format of VertexAIMetadataStore external=%q was not known (use projects/{{projectID}}/locations/{{location}}/metadataStores/{{metadatastoreID}})", external)
110+
}
111+
parent = &MetadataStoreParent{
112+
ProjectID: tokens[1],
113+
Location: tokens[3],
114+
}
115+
resourceID = tokens[5]
116+
return parent, resourceID, nil
117+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package v1alpha1
16+
17+
import (
18+
"context"
19+
"fmt"
20+
21+
refsv1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
22+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
23+
apierrors "k8s.io/apimachinery/pkg/api/errors"
24+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
25+
"k8s.io/apimachinery/pkg/types"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
)
28+
29+
var _ refsv1beta1.ExternalNormalizer = &MetadataStoreRef{}
30+
31+
// MetadataStoreRef defines the resource reference to VertexAIMetadataStore, which "External" field
32+
// holds the GCP identifier for the KRM object.
33+
type MetadataStoreRef struct {
34+
// A reference to an externally managed VertexAIMetadataStore resource.
35+
// Should be in the format "projects/{{projectID}}/locations/{{location}}/metadatastores/{{metadatastoreID}}".
36+
External string `json:"external,omitempty"`
37+
38+
// The name of a VertexAIMetadataStore resource.
39+
Name string `json:"name,omitempty"`
40+
41+
// The namespace of a VertexAIMetadataStore resource.
42+
Namespace string `json:"namespace,omitempty"`
43+
}
44+
45+
// NormalizedExternal provision the "External" value for other resource that depends on VertexAIMetadataStore.
46+
// If the "External" is given in the other resource's spec.VertexAIMetadataStoreRef, the given value will be used.
47+
// Otherwise, the "Name" and "Namespace" will be used to query the actual VertexAIMetadataStore object from the cluster.
48+
func (r *MetadataStoreRef) NormalizedExternal(ctx context.Context, reader client.Reader, otherNamespace string) (string, error) {
49+
if r.External != "" && r.Name != "" {
50+
return "", fmt.Errorf("cannot specify both name and external on %s reference", VertexAIMetadataStoreGVK.Kind)
51+
}
52+
// From given External
53+
if r.External != "" {
54+
if _, _, err := ParseMetadataStoreExternal(r.External); err != nil {
55+
return "", err
56+
}
57+
return r.External, nil
58+
}
59+
60+
// From the Config Connector object
61+
if r.Namespace == "" {
62+
r.Namespace = otherNamespace
63+
}
64+
key := types.NamespacedName{Name: r.Name, Namespace: r.Namespace}
65+
u := &unstructured.Unstructured{}
66+
u.SetGroupVersionKind(VertexAIMetadataStoreGVK)
67+
if err := reader.Get(ctx, key, u); err != nil {
68+
if apierrors.IsNotFound(err) {
69+
return "", k8s.NewReferenceNotFoundError(u.GroupVersionKind(), key)
70+
}
71+
return "", fmt.Errorf("reading referenced %s %s: %w", VertexAIMetadataStoreGVK, key, err)
72+
}
73+
// Get external from status.externalRef. This is the most trustworthy place.
74+
actualExternalRef, _, err := unstructured.NestedString(u.Object, "status", "externalRef")
75+
if err != nil {
76+
return "", fmt.Errorf("reading status.externalRef: %w", err)
77+
}
78+
if actualExternalRef == "" {
79+
return "", k8s.NewReferenceNotReadyError(u.GroupVersionKind(), key)
80+
}
81+
r.External = actualExternalRef
82+
return r.External, nil
83+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package v1alpha1
16+
17+
import (
18+
refsv1beta1 "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
19+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/k8s/v1alpha1"
20+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
)
22+
23+
var VertexAIMetadataStoreGVK = GroupVersion.WithKind("VertexAIMetadataStore")
24+
25+
// VertexAIMetadataStoreSpec defines the desired state of VertexAIMetadataStore
26+
// +kcc:proto=google.cloud.aiplatform.v1beta1.MetadataStore
27+
type VertexAIMetadataStoreSpec struct {
28+
// Customer-managed encryption key spec for a Metadata Store. If set, this
29+
// Metadata Store and all sub-resources of this Metadata Store are secured
30+
// using this key.
31+
// +kcc:proto:field=google.cloud.aiplatform.v1beta1.MetadataStore.encryption_spec
32+
EncryptionSpec *EncryptionSpec `json:"encryptionSpec,omitempty"`
33+
34+
// Optional. Dataplex integration settings.
35+
// +kcc:proto:field=google.cloud.aiplatform.v1beta1.MetadataStore.dataplex_config
36+
DataplexConfig *MetadataStore_DataplexConfig `json:"dataplexConfig,omitempty"`
37+
38+
// Description of the MetadataStore.
39+
// +kcc:proto:field=google.cloud.aiplatform.v1beta1.MetadataStore.description
40+
Description *string `json:"description,omitempty"`
41+
42+
// The region of the Metadata Store. eg us-central1.
43+
Region string `json:"region,omitempty"`
44+
45+
// The project that this resource belongs to.
46+
ProjectRef *refsv1beta1.ProjectRef `json:"projectRef,omitempty"`
47+
48+
// The VertexAIMetadataStore name. If not given, the metadata.name will be used.
49+
ResourceID *string `json:"resourceID,omitempty"`
50+
}
51+
52+
// VertexAIMetadataStoreStatus defines the config connector machine state of VertexAIMetadataStore
53+
type VertexAIMetadataStoreStatus struct {
54+
/* Conditions represent the latest available observations of the
55+
object's current state. */
56+
Conditions []v1alpha1.Condition `json:"conditions,omitempty"`
57+
58+
// 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.
59+
ObservedGeneration *int64 `json:"observedGeneration,omitempty"`
60+
61+
// A unique specifier for the VertexAIMetadataStore resource in GCP.
62+
ExternalRef *string `json:"externalRef,omitempty"`
63+
64+
// ObservedState is the state of the resource as most recently observed in GCP.
65+
ObservedState *VertexAIMetadataStoreObservedState `json:"observedState,omitempty"`
66+
}
67+
68+
// VertexAIMetadataStoreObservedState is the state of the VertexAIMetadataStore resource as most recently observed in GCP.
69+
// +kcc:proto=google.cloud.aiplatform.v1beta1.MetadataStore
70+
type VertexAIMetadataStoreObservedState struct {
71+
// Output only. The resource name of the MetadataStore instance.
72+
// +kcc:proto:field=google.cloud.aiplatform.v1beta1.MetadataStore.name
73+
Name *string `json:"name,omitempty"`
74+
75+
// Output only. Timestamp when this MetadataStore was created.
76+
// +kcc:proto:field=google.cloud.aiplatform.v1beta1.MetadataStore.create_time
77+
CreateTime *string `json:"createTime,omitempty"`
78+
79+
// Output only. Timestamp when this MetadataStore was last updated.
80+
// +kcc:proto:field=google.cloud.aiplatform.v1beta1.MetadataStore.update_time
81+
UpdateTime *string `json:"updateTime,omitempty"`
82+
83+
// Output only. State information of the MetadataStore.
84+
// +kcc:proto:field=google.cloud.aiplatform.v1beta1.MetadataStore.state
85+
State *MetadataStore_MetadataStoreState `json:"state,omitempty"`
86+
}
87+
88+
// +genclient
89+
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
90+
// TODO(user): make sure the pluralizaiton below is correct"
91+
// +kubebuilder:resource:categories=gcp,shortName=gcpvertexaimetadatastore;gcpvertexaimetadatastores
92+
// +kubebuilder:subresource:status
93+
// +kubebuilder:metadata:labels="cnrm.cloud.google.com/managed-by-kcc=true";"cnrm.cloud.google.com/system=true";"cnrm.cloud.google.com/stability-level=alpha"
94+
// +kubebuilder:printcolumn:name="Age",JSONPath=".metadata.creationTimestamp",type="date"
95+
// +kubebuilder:printcolumn:name="Ready",JSONPath=".status.conditions[?(@.type=='Ready')].status",type="string",description="When 'True', the most recent reconcile of the resource succeeded"
96+
// +kubebuilder:printcolumn:name="Status",JSONPath=".status.conditions[?(@.type=='Ready')].reason",type="string",description="The reason for the value in 'Ready'"
97+
// +kubebuilder:printcolumn:name="Status Age",JSONPath=".status.conditions[?(@.type=='Ready')].lastTransitionTime",type="date",description="The last transition time for the value in 'Status'"
98+
99+
// VertexAIMetadataStore is the Schema for the VertexAIMetadataStore API
100+
// +k8s:openapi-gen=true
101+
type VertexAIMetadataStore struct {
102+
metav1.TypeMeta `json:",inline"`
103+
metav1.ObjectMeta `json:"metadata,omitempty"`
104+
105+
// +required
106+
Spec VertexAIMetadataStoreSpec `json:"spec,omitempty"`
107+
Status VertexAIMetadataStoreStatus `json:"status,omitempty"`
108+
}
109+
110+
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
111+
// VertexAIMetadataStoreList contains a list of VertexAIMetadataStore
112+
type VertexAIMetadataStoreList struct {
113+
metav1.TypeMeta `json:",inline"`
114+
metav1.ListMeta `json:"metadata,omitempty"`
115+
Items []VertexAIMetadataStore `json:"items"`
116+
}
117+
118+
func init() {
119+
SchemeBuilder.Register(&VertexAIMetadataStore{}, &VertexAIMetadataStoreList{})
120+
}

apis/vertexai/v1alpha1/types.generated.go

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)