diff --git a/apis/gkeclusters/definition.yaml b/apis/gkeclusters/definition.yaml index c36bd0bdc..5dba7a1eb 100644 --- a/apis/gkeclusters/definition.yaml +++ b/apis/gkeclusters/definition.yaml @@ -206,12 +206,12 @@ spec: description: >- The type of credential this secret contains. Kubeconfig contains a kubeconfig file with the cluster - endpoint and CA certificate. GCPServiceAccountKey - contains a GCP service account JSON key that can - authenticate to the cluster via GKE IAM. + endpoint and CA certificate. GoogleApplicationCredentials + contains a GCP service account JSON key that + authenticates to the cluster via GKE IAM. enum: - Kubeconfig - - GCPServiceAccountKey + - GoogleApplicationCredentials name: type: string description: Name of the Secret. diff --git a/apis/inferenceclusters/definition.yaml b/apis/inferenceclusters/definition.yaml index 16c74d366..dc0a13609 100644 --- a/apis/inferenceclusters/definition.yaml +++ b/apis/inferenceclusters/definition.yaml @@ -77,6 +77,9 @@ spec: description: >- Optional reference to a Secret containing cloud provider credentials for IAM-based authentication. + The type selects which cloud identity the + ProviderConfigs authenticate as, and must match the + cloud the existing cluster runs on. required: [name] properties: name: @@ -88,6 +91,17 @@ spec: default: private_key minLength: 1 maxLength: 253 + type: + type: string + description: >- + Cloud identity type the credential authenticates + as. Must match the cloud the existing cluster runs + on. Defaults to GoogleApplicationCredentials. + default: GoogleApplicationCredentials + enum: + - GoogleApplicationCredentials + - AWSWebIdentityCredentials + - NebiusServiceAccountCredentials cache: type: object description: >- diff --git a/apis/servingstacks/definition.yaml b/apis/servingstacks/definition.yaml index 26c475857..04c9ef444 100644 --- a/apis/servingstacks/definition.yaml +++ b/apis/servingstacks/definition.yaml @@ -46,9 +46,8 @@ spec: Secrets used to authenticate to the target cluster. Typically sourced from a GKECluster's status.secrets. All secrets must be in the same namespace as this ServingStack. A Kubeconfig - secret is required. If a cloud-specific credential secret is - present (e.g. GCPServiceAccountKey), the ProviderConfigs - will use it for identity-based authentication instead of + secret is required. If a cloud identity secret is present, the + serving stack authenticates as that identity instead of relying on the kubeconfig's embedded credentials. minItems: 1 maxItems: 8 @@ -63,11 +62,15 @@ spec: type: string description: >- The type of credential this secret contains. Kubeconfig - is required. Cloud-specific types are optional and - determine how the ProviderConfigs authenticate. + is required. Any other value is a cloud identity type; + when present, the serving stack authenticates to the + cluster as that identity instead of using the + kubeconfig's embedded credentials. enum: - Kubeconfig - - GCPServiceAccountKey + - GoogleApplicationCredentials + - AWSWebIdentityCredentials + - NebiusServiceAccountCredentials name: type: string description: Name of the Secret. diff --git a/crossplane-project.yaml b/crossplane-project.yaml index 18114bba7..1084dfd52 100644 --- a/crossplane-project.yaml +++ b/crossplane-project.yaml @@ -115,11 +115,13 @@ spec: xpkg: apiVersion: pkg.crossplane.io/v1 kind: Provider - package: xpkg.upbound.io/upbound/provider-helm - version: v1.2.0 + # ToDo(haarchri): switch to official provider + package: xpkg.upbound.io/modelplane/provider-helm + version: v1.3.0-9a6fb4b - type: xpkg xpkg: apiVersion: pkg.crossplane.io/v1 kind: Provider - package: xpkg.upbound.io/upbound/provider-kubernetes - version: v1.2.1 + # ToDo(haarchri): switch to official provider + package: xpkg.upbound.io/modelplane/provider-kubernetes + version: v1.2.1-070dae7 diff --git a/docs/manifests/concepts/inference-cluster-existing.yaml b/docs/manifests/concepts/inference-cluster-existing.yaml index fadf74227..93a6f227f 100644 --- a/docs/manifests/concepts/inference-cluster-existing.yaml +++ b/docs/manifests/concepts/inference-cluster-existing.yaml @@ -21,11 +21,15 @@ spec: name: byo-cluster-kubeconfig key: kubeconfig - # Optional: a cloud identity Secret for pulling images from private - # registries or accessing cloud APIs from the remote cluster. + # Optional: a cloud identity Secret so Modelplane authenticates to the + # cluster via the cloud's IAM instead of a token baked into the + # kubeconfig. type selects the cloud identity and must match the cluster's + # cloud; it defaults to GoogleApplicationCredentials. For example, a + # Nebius managed cluster: # identitySecretRef: # name: byo-cluster-sa-key - # key: private_key + # key: credentials.json + # type: NebiusServiceAccountCredentials # Each pool's nodes must be labeled modelplane.ai/pool= (here # modelplane.ai/pool=gpu-h100). The scheduler pins a worker to its pool by diff --git a/docs/manifests/reference/servingstacks.yaml b/docs/manifests/reference/servingstacks.yaml index b02eee569..8e5695d67 100644 --- a/docs/manifests/reference/servingstacks.yaml +++ b/docs/manifests/reference/servingstacks.yaml @@ -8,7 +8,7 @@ spec: - type: Kubeconfig name: west-gke-kubeconfig key: kubeconfig - - type: GCPServiceAccountKey + - type: GoogleApplicationCredentials name: west-gke-sa-key key: private_key versions: diff --git a/functions/compose-gke-cluster/function/fn.py b/functions/compose-gke-cluster/function/fn.py index b8b5d7c76..6e8621c08 100644 --- a/functions/compose-gke-cluster/function/fn.py +++ b/functions/compose-gke-cluster/function/fn.py @@ -64,9 +64,9 @@ _LABEL_POOL = "modelplane.ai/pool" # Secret types written to XR status. compose-inference-cluster reads -# these to wire the kubeconfig and SA key into ProviderConfigs. +# these to wire the kubeconfig and SA key into ProviderConfigs. The SA key's +# type is the provider identity it authenticates as (see _IDENTITY_TYPE_GCP). _SECRET_TYPE_KUBECONFIG = "Kubeconfig" -_SECRET_TYPE_GCP_SA_KEY = "GCPServiceAccountKey" # Secret keys within the Kubernetes Secrets created by GCP providers. _SECRET_KEY_KUBECONFIG = "kubeconfig" @@ -503,7 +503,7 @@ def write_status(self) -> None: key=_SECRET_KEY_KUBECONFIG, ), v1alpha1.Secret( - type=_SECRET_TYPE_GCP_SA_KEY, + type=_IDENTITY_TYPE_GCP, name=_sa_key_secret_name(self.xr), key=_SECRET_KEY_GCP_SA, ), diff --git a/functions/compose-gke-cluster/tests/test_fn.py b/functions/compose-gke-cluster/tests/test_fn.py index 405ff4e5c..7283328bb 100644 --- a/functions/compose-gke-cluster/tests/test_fn.py +++ b/functions/compose-gke-cluster/tests/test_fn.py @@ -95,7 +95,7 @@ async def test_compose(self) -> None: "key": "kubeconfig", }, { - "type": "GCPServiceAccountKey", + "type": "GoogleApplicationCredentials", "name": "test-cluster-sa-key-3295c", "key": "private_key", }, @@ -446,7 +446,7 @@ async def test_compose(self) -> None: "key": "kubeconfig", }, { - "type": "GCPServiceAccountKey", + "type": "GoogleApplicationCredentials", "name": "test-cluster-sa-key-3295c", "key": "private_key", }, diff --git a/functions/compose-inference-cluster/function/fn.py b/functions/compose-inference-cluster/function/fn.py index ada357388..b0a60ec36 100644 --- a/functions/compose-inference-cluster/function/fn.py +++ b/functions/compose-inference-cluster/function/fn.py @@ -78,10 +78,10 @@ # function's _LABEL_CLUSTER. _LABEL_CLUSTER = "modelplane.ai/cluster" -# Secret types that couple compose-gke-cluster (writer) to this function -# (reader) and compose-serving-stack (reader). +# Secret type that couples compose-gke-cluster (writer) to this function +# (reader). Every other secret type is a provider identity type, passed through +# to the ProviderConfigs unchanged. _SECRET_TYPE_KUBECONFIG = "Kubeconfig" -_SECRET_TYPE_GCP_SA_KEY = "GCPServiceAccountKey" # The modelplane-system namespace. Used for the ServingStack XR, # ClusterProviderConfig secretRefs, and status.namespace. @@ -255,11 +255,16 @@ def compose_gke(self, gke: v1alpha1.Gke | None) -> None: gke_ready = resource.get_condition(self.req.observed.resources.get("gke-cluster"), "Ready").status == "True" kubeconfig_secret = self.observed_gke_secret(_SECRET_TYPE_KUBECONFIG) - sa_key = self.observed_gke_secret(_SECRET_TYPE_GCP_SA_KEY) + sa_key = self.observed_gke_secret(_IDENTITY_TYPE_GCP) backend_exists = BACKEND_RESOURCE_KEY in self.req.observed.resources if gke_ready and kubeconfig_secret: - self.compose_cluster_provider_config(kubeconfig_secret.name, kubeconfig_secret.key, sa_key) + self.compose_cluster_provider_config( + kubeconfig_secret.name, + kubeconfig_secret.key, + identity_ref=sa_key, + identity_type=_IDENTITY_TYPE_GCP, + ) backend_secrets = self.resolve_gke_backend_secrets(gke_ready=gke_ready, backend_exists=backend_exists) if backend_secrets or backend_exists: @@ -322,14 +327,20 @@ def compose_existing(self, existing: v1alpha1.Existing | None) -> None: identity = existing.identitySecretRef - self.compose_cluster_provider_config(existing.secretRef.name, existing.secretRef.key, sa_key=identity) + self.compose_cluster_provider_config( + existing.secretRef.name, + existing.secretRef.key, + identity_ref=identity, + identity_type=(identity.type or _IDENTITY_TYPE_GCP) if identity else None, + ) backend_secrets = [ ssv1alpha1.Secret(type=_SECRET_TYPE_KUBECONFIG, name=existing.secretRef.name, key=existing.secretRef.key), ] if identity: backend_secrets.append( - ssv1alpha1.Secret(type=_SECRET_TYPE_GCP_SA_KEY, name=identity.name, key=identity.key), + # type defaults to GCP in the XRD; coalesce so it's never None. + ssv1alpha1.Secret(type=identity.type or _IDENTITY_TYPE_GCP, name=identity.name, key=identity.key), ) self.compose_serving_stack(backend_secrets) @@ -366,10 +377,16 @@ def compose_cluster_provider_config( self, kubeconfig_name: str, kubeconfig_key: str | None, - sa_key: gkev1alpha1.Secret | v1alpha1.IdentitySecretRef | None = None, + identity_ref: gkev1alpha1.Secret | v1alpha1.IdentitySecretRef | None = None, + identity_type: str | None = None, ) -> None: """Compose a ClusterProviderConfig for provider-kubernetes so that - ModelReplicas can create Objects on the remote cluster.""" + ModelReplicas can create Objects on the remote cluster. + + When identity_ref is set, the ProviderConfig authenticates as the given + identity_type (the cloud IAM identity) on top of the kubeconfig instead + of relying on the kubeconfig's embedded credentials. + """ cpc = k8scpcv1alpha1.ClusterProviderConfig( metadata=metav1.ObjectMeta(name=resource.child_name(_name(self.xr.metadata), "cluster-kubeconfig")), spec=k8scpcv1alpha1.Spec( @@ -383,14 +400,14 @@ def compose_cluster_provider_config( ), ), ) - if sa_key: + if identity_ref: cpc.spec.identity = k8scpcv1alpha1.Identity( - type=_IDENTITY_TYPE_GCP, + type=identity_type, # ty: ignore[invalid-argument-type] # value comes from the XRD/GKE identity-type enums source="Secret", secretRef=k8scpcv1alpha1.SecretRef( namespace=_NAMESPACE_SYSTEM, - name=sa_key.name, - key=sa_key.key, # ty: ignore[invalid-argument-type] # XRD defaults the secret key + name=identity_ref.name, + key=identity_ref.key, # ty: ignore[invalid-argument-type] # XRD defaults the secret key ), ) resource.update( diff --git a/functions/compose-inference-cluster/tests/test_fn.py b/functions/compose-inference-cluster/tests/test_fn.py index 610902c31..1d6d84c07 100644 --- a/functions/compose-inference-cluster/tests/test_fn.py +++ b/functions/compose-inference-cluster/tests/test_fn.py @@ -360,6 +360,77 @@ async def test_compose(self) -> None: # noqa: PLR0915 ) want1.requirements.resources["class-gpu-l4"].CopyFrom(class_selector) + # --- Case 1b: Existing cluster with a non-GCP identity threads the + # declared identity type into the CPC and the ServingStack. --- + req1b = fnv1.RunFunctionRequest( + observed=fnv1.State( + composite=fnv1.Resource( + resource=resource.dict_to_struct( + v1alpha1.InferenceCluster( + metadata=metav1.ObjectMeta( + name="test-cluster", + namespace="modelplane-system", + ), + spec=v1alpha1.Spec( + cluster=v1alpha1.Cluster( + source="Existing", + existing=v1alpha1.Existing( + secretRef=v1alpha1.SecretRef(name="my-kubeconfig"), + identitySecretRef=v1alpha1.IdentitySecretRef( + name="nebius-creds", + key="credentials.json", + type="NebiusServiceAccountCredentials", + ), + ), + ), + nodePools=[ + v1alpha1.NodePool( + name="l4-pool", + className="gpu-l4", + nodeCount=2, + maxNodeCount=4, + ), + ], + ), + ).model_dump(exclude_none=True, mode="json") + ), + ), + ), + ) + req1b.required_resources["class-gpu-l4"].items.append( + fnv1.Resource(resource=resource.dict_to_struct(inference_class_l4)) + ) + + # want1b mirrors want1 but with the Nebius identity on the CPC and an + # extra ServingStack identity secret of the same type. + want1b = fnv1.RunFunctionResponse() + want1b.CopyFrom(want1) + cpc1b = want1b.desired.resources["cluster-provider-config-kubernetes"] + cpc1b_dict = resource.struct_to_dict(cpc1b.resource) + cpc1b_dict["spec"]["identity"] = { + "type": "NebiusServiceAccountCredentials", + "source": "Secret", + "secretRef": { + "namespace": "modelplane-system", + "name": "nebius-creds", + "key": "credentials.json", + }, + } + cpc1b.resource.CopyFrom(resource.dict_to_struct(cpc1b_dict)) + backend1b = want1b.desired.resources["serving-stack"] + backend1b_dict = resource.struct_to_dict(backend1b.resource) + backend1b_dict["spec"]["secrets"].append( + { + "type": "NebiusServiceAccountCredentials", + "name": "nebius-creds", + "key": "credentials.json", + } + ) + backend1b.resource.CopyFrom(resource.dict_to_struct(backend1b_dict)) + # want1 gains the replica-guard requirement in place from the guard cases + # below, after this snapshot; add it here so want1b matches on its own. + want1b.requirements.resources["model-replicas"].CopyFrom(_replicas_selector("test-cluster")) + # --- Case 2: GKE cluster first pass - no observed GKE, classes resolved. --- req2 = fnv1.RunFunctionRequest( observed=fnv1.State( @@ -1068,7 +1139,11 @@ async def test_compose(self) -> None: # noqa: PLR0915 "cache": {"storageClassName": "modelplane-rwx"}, "secrets": [ {"type": "Kubeconfig", "name": "test-cluster-kubeconfig-abcde", "key": "kubeconfig"}, - {"type": "GCPServiceAccountKey", "name": "test-cluster-sa-key-fghij", "key": "credentials.json"}, + { + "type": "GoogleApplicationCredentials", + "name": "test-cluster-sa-key-fghij", + "key": "credentials.json", + }, ], }, } @@ -1225,7 +1300,7 @@ async def test_compose(self) -> None: # noqa: PLR0915 "key": "kubeconfig", }, { - "type": "GCPServiceAccountKey", + "type": "GoogleApplicationCredentials", "name": "test-cluster-sa-key-fghij", "key": "credentials.json", }, @@ -1442,6 +1517,7 @@ async def test_compose(self) -> None: # noqa: PLR0915 cases = [ Case(name="existing cluster with secrets composes backend and CPC", req=req1, want=want1), + Case(name="existing cluster with a non-GCP identity threads the identity type", req=req1b, want=want1b), Case(name="GKE cluster first pass composes GKECluster XR only", req=req2, want=want2), Case(name="existing cluster second pass with backend ready", req=req3, want=want3), Case(name="EKS cluster first pass composes EKSCluster XR only", req=req4, want=want4), diff --git a/functions/compose-serving-stack/function/fn.py b/functions/compose-serving-stack/function/fn.py index 25944e0e3..5c8e04337 100644 --- a/functions/compose-serving-stack/function/fn.py +++ b/functions/compose-serving-stack/function/fn.py @@ -69,13 +69,10 @@ # writes status.addresses. _GATEWAY_READY_CEL = "has(object.status.addresses) && object.status.addresses.size() > 0" -# Secret types that couple compose-gke-cluster (writer) to this function -# (reader) via the InferenceCluster status. +# Secret type that names the kubeconfig entry in the XR's secrets. Every other +# entry's type is a provider identity type, which both ProviderConfigs stamp +# verbatim as their identity.type. _SECRET_TYPE_KUBECONFIG = "Kubeconfig" -_SECRET_TYPE_GCP_SA_KEY = "GCPServiceAccountKey" - -# Identity type for GCP service account credentials. -_IDENTITY_TYPE_GCP = "GoogleApplicationCredentials" # Prometheus constants. _PROMETHEUS_NAMESPACE = "monitoring" @@ -337,10 +334,10 @@ def compose_provider_configs(self) -> None: kubeconfig_secret = next(s for s in xr_secrets if s.type == _SECRET_TYPE_KUBECONFIG) - # The kubeconfig provides the cluster endpoint and CA cert. If a - # cloud-specific credential secret is present, it's layered on as an - # identity block so the provider authenticates via the cloud's IAM - # instead of relying on whatever auth is baked into the kubeconfig. + # The kubeconfig provides the cluster endpoint and CA cert. If an + # identity secret is present, it's layered on as an identity block so the + # provider authenticates via the cloud's IAM instead of relying on + # whatever auth is baked into the kubeconfig. k8s_pc_spec = k8spcv1alpha1.Spec( credentials=k8spcv1alpha1.Credentials( source="Secret", @@ -362,27 +359,27 @@ def compose_provider_configs(self) -> None: ), ) - gcp_secret = next( - (s for s in xr_secrets if s.type == _SECRET_TYPE_GCP_SA_KEY), + identity_secret = next( + (s for s in xr_secrets if s.type != _SECRET_TYPE_KUBECONFIG), None, ) - if gcp_secret: + if identity_secret: k8s_pc_spec.identity = k8spcv1alpha1.Identity( - type=_IDENTITY_TYPE_GCP, + type=identity_secret.type, # ty: ignore[invalid-argument-type] # non-Kubeconfig types are exactly the provider identity types source="Secret", secretRef=k8spcv1alpha1.SecretRef( - name=gcp_secret.name, + name=identity_secret.name, namespace=_namespace(self.xr.metadata), - key=gcp_secret.key, + key=identity_secret.key, ), ) helm_pc_spec.identity = helmpcv1beta1.Identity( - type=_IDENTITY_TYPE_GCP, + type=identity_secret.type, # ty: ignore[invalid-argument-type] # non-Kubeconfig types are exactly the provider identity types source="Secret", secretRef=helmpcv1beta1.SecretRef( - name=gcp_secret.name, + name=identity_secret.name, namespace=_namespace(self.xr.metadata), - key=gcp_secret.key, + key=identity_secret.key, ), ) diff --git a/functions/compose-serving-stack/tests/test_fn.py b/functions/compose-serving-stack/tests/test_fn.py index eb35a1eae..faf3342b0 100644 --- a/functions/compose-serving-stack/tests/test_fn.py +++ b/functions/compose-serving-stack/tests/test_fn.py @@ -589,7 +589,9 @@ def _base_request( spec=v1alpha1.Spec( secrets=[ v1alpha1.Secret(type="Kubeconfig", name="kube-secret", key="kubeconfig"), - v1alpha1.Secret(type="GCPServiceAccountKey", name="sa-secret", key="private_key"), + v1alpha1.Secret( + type="GoogleApplicationCredentials", name="sa-secret", key="private_key" + ), ], nvidiaDriverRoot=nvidia_driver_root, ), @@ -646,6 +648,49 @@ async def test_first_pass(self) -> None: "-want, +got", ) + async def test_non_gcp_identity(self) -> None: + """A non-GCP identity secret stamps its own type on both ProviderConfigs. + + The identity secret's type is the provider identity type verbatim, so a + Nebius (or any other cloud's) credential authenticates as that cloud + rather than being forced to GoogleApplicationCredentials. + """ + req = fnv1.RunFunctionRequest( + observed=fnv1.State( + composite=fnv1.Resource( + resource=resource.dict_to_struct( + v1alpha1.ServingStack( + metadata=metav1.ObjectMeta(name="test-backend", namespace="test-ns"), + spec=v1alpha1.Spec( + secrets=[ + v1alpha1.Secret(type="Kubeconfig", name="kube-secret", key="kubeconfig"), + v1alpha1.Secret( + type="NebiusServiceAccountCredentials", + name="nebius-secret", + key="credentials.json", + ), + ], + ), + ).model_dump(exclude_none=True, mode="json") + ), + ), + ), + ) + + got = await self.runner.RunFunction(req, None) + got_resources = json_format.MessageToDict(got).get("desired", {}).get("resources", {}) + want_identity = { + "secretRef": {"key": "credentials.json", "name": "nebius-secret", "namespace": "test-ns"}, + "source": "Secret", + "type": "NebiusServiceAccountCredentials", + } + for pc in ("provider-config-kubernetes", "provider-config-helm"): + self.assertEqual( + want_identity, + got_resources[pc]["resource"]["spec"]["identity"], + f"{pc} identity", + ) + async def test_second_pass(self) -> None: """Observed PCs ungate Helm releases, CRD objects, and gateway objects.""" req = _base_request() diff --git a/schemas/.lock.json b/schemas/.lock.json index 5511e202d..fd8627874 100644 --- a/schemas/.lock.json +++ b/schemas/.lock.json @@ -1 +1 @@ -{"packages":{"fs://apis":"2626e776e4d5762951d5c19f58879c187d771942c3b6a29f5c008bd26f3b2d16","git://https://github.com/crossplane/crossplane/cluster/crds":"90d8b72ad8b829f0bcd7d7d5a98eaa0d579f244a","xpkg://xpkg.upbound.io/upbound/provider-aws-ec2:v2.6.0":"sha256:acc26c8d2710e0306185b6c626a2f8c8fe0fdf89874e85efe7944a3668322865","xpkg://xpkg.upbound.io/upbound/provider-aws-efs:v2.6.0":"sha256:00f1bbbb3c0f1948b6dd45c841a083d63e41850f5a559a15580915311f404911","xpkg://xpkg.upbound.io/upbound/provider-aws-eks:v2.6.0":"sha256:5d144b19e188cb96c918aa7e4ccbc6759b8733ff09bd8ce412723c669aa3f763","xpkg://xpkg.upbound.io/upbound/provider-aws-iam:v2.6.0":"sha256:dbc5288589ccb302d527565680477f08477c280fc5c616dda95dfd558108a038","xpkg://xpkg.upbound.io/upbound/provider-gcp-cloudplatform:v2.6.0":"sha256:f1fe8bc55c474464642303e6fa8608c83e369b42ff12bb8a60a3e2d77339a52b","xpkg://xpkg.upbound.io/upbound/provider-gcp-compute:v2.6.0":"sha256:c7417c461d403f0d59a2dd83f242cebd5d738f9ad328e03ffe0c7b99ea251635","xpkg://xpkg.upbound.io/upbound/provider-gcp-container:v2.6.0":"sha256:b3f68d01ab2529026f1a5dd6d6215a2f499710fad4edd4850546bf6c756befb6","xpkg://xpkg.upbound.io/upbound/provider-helm:v1.2.0":"sha256:4e2bada791af5550195ce0a7b3031273e005c55ff74807d0955b5f38061f7dc9","xpkg://xpkg.upbound.io/upbound/provider-kubernetes:v1.2.1":"sha256:87e2ce59b20bc7f516ad5b53f3214862f42352aabc713e342d33659fb1135564"}} \ No newline at end of file +{"packages":{"fs://apis":"a944c621e7433ac5cc6ead61170bc7b261c943d736c8a9ff7a3006f6341820f1","git://https://github.com/crossplane/crossplane/cluster/crds":"90d8b72ad8b829f0bcd7d7d5a98eaa0d579f244a","xpkg://xpkg.upbound.io/modelplane/provider-helm:v1.3.0-9a6fb4b":"sha256:af1858ee7dcabc9149bf2f8005bffb298961519c066b8e7c21a244039ccd066b","xpkg://xpkg.upbound.io/modelplane/provider-kubernetes:v1.2.1-070dae7":"sha256:718b481f5b760f5436e162e997fedf74c3b5e950c4f9d824a990973df071676b","xpkg://xpkg.upbound.io/upbound/provider-aws-ec2:v2.6.0":"sha256:acc26c8d2710e0306185b6c626a2f8c8fe0fdf89874e85efe7944a3668322865","xpkg://xpkg.upbound.io/upbound/provider-aws-efs:v2.6.0":"sha256:00f1bbbb3c0f1948b6dd45c841a083d63e41850f5a559a15580915311f404911","xpkg://xpkg.upbound.io/upbound/provider-aws-eks:v2.6.0":"sha256:5d144b19e188cb96c918aa7e4ccbc6759b8733ff09bd8ce412723c669aa3f763","xpkg://xpkg.upbound.io/upbound/provider-aws-iam:v2.6.0":"sha256:dbc5288589ccb302d527565680477f08477c280fc5c616dda95dfd558108a038","xpkg://xpkg.upbound.io/upbound/provider-gcp-cloudplatform:v2.6.0":"sha256:f1fe8bc55c474464642303e6fa8608c83e369b42ff12bb8a60a3e2d77339a52b","xpkg://xpkg.upbound.io/upbound/provider-gcp-compute:v2.6.0":"sha256:c7417c461d403f0d59a2dd83f242cebd5d738f9ad328e03ffe0c7b99ea251635","xpkg://xpkg.upbound.io/upbound/provider-gcp-container:v2.6.0":"sha256:b3f68d01ab2529026f1a5dd6d6215a2f499710fad4edd4850546bf6c756befb6"}} \ No newline at end of file diff --git a/schemas/python/models/ai/modelplane/inferencecluster/v1alpha1.py b/schemas/python/models/ai/modelplane/inferencecluster/v1alpha1.py index 855d872d4..339a8345e 100644 --- a/schemas/python/models/ai/modelplane/inferencecluster/v1alpha1.py +++ b/schemas/python/models/ai/modelplane/inferencecluster/v1alpha1.py @@ -31,6 +31,17 @@ class Cache(BaseModel): class IdentitySecretRef(BaseModel): key: constr(min_length=1, max_length=253) | None = 'private_key' name: constr(min_length=1, max_length=253) + type: ( + Literal[ + 'GoogleApplicationCredentials', + 'AWSWebIdentityCredentials', + 'NebiusServiceAccountCredentials', + ] + | None + ) = 'GoogleApplicationCredentials' + """ + Cloud identity type the credential authenticates as. Must match the cloud the existing cluster runs on. Defaults to GoogleApplicationCredentials. + """ class SecretRef(BaseModel): @@ -45,7 +56,7 @@ class Existing(BaseModel): """ identitySecretRef: IdentitySecretRef | None = None """ - Optional reference to a Secret containing cloud provider credentials for IAM-based authentication. + Optional reference to a Secret containing cloud provider credentials for IAM-based authentication. The type selects which cloud identity the ProviderConfigs authenticate as, and must match the cloud the existing cluster runs on. """ secretRef: SecretRef """ diff --git a/schemas/python/models/ai/modelplane/infrastructure/gkecluster/v1alpha1.py b/schemas/python/models/ai/modelplane/infrastructure/gkecluster/v1alpha1.py index edf0885dd..e611e38be 100644 --- a/schemas/python/models/ai/modelplane/infrastructure/gkecluster/v1alpha1.py +++ b/schemas/python/models/ai/modelplane/infrastructure/gkecluster/v1alpha1.py @@ -162,9 +162,9 @@ class Secret(BaseModel): """ Name of the Secret. """ - type: Literal['Kubeconfig', 'GCPServiceAccountKey'] + type: Literal['Kubeconfig', 'GoogleApplicationCredentials'] """ - The type of credential this secret contains. Kubeconfig contains a kubeconfig file with the cluster endpoint and CA certificate. GCPServiceAccountKey contains a GCP service account JSON key that can authenticate to the cluster via GKE IAM. + The type of credential this secret contains. Kubeconfig contains a kubeconfig file with the cluster endpoint and CA certificate. GoogleApplicationCredentials contains a GCP service account JSON key that authenticates to the cluster via GKE IAM. """ diff --git a/schemas/python/models/ai/modelplane/infrastructure/servingstack/v1alpha1.py b/schemas/python/models/ai/modelplane/infrastructure/servingstack/v1alpha1.py index f90be4d52..57a03ba46 100644 --- a/schemas/python/models/ai/modelplane/infrastructure/servingstack/v1alpha1.py +++ b/schemas/python/models/ai/modelplane/infrastructure/servingstack/v1alpha1.py @@ -76,9 +76,14 @@ class Secret(BaseModel): """ Name of the Secret. """ - type: Literal['Kubeconfig', 'GCPServiceAccountKey'] + type: Literal[ + 'Kubeconfig', + 'GoogleApplicationCredentials', + 'AWSWebIdentityCredentials', + 'NebiusServiceAccountCredentials', + ] """ - The type of credential this secret contains. Kubeconfig is required. Cloud-specific types are optional and determine how the ProviderConfigs authenticate. + The type of credential this secret contains. Kubeconfig is required. Any other value is a cloud identity type; when present, the serving stack authenticates to the cluster as that identity instead of using the kubeconfig's embedded credentials. """ @@ -128,7 +133,7 @@ class Spec(BaseModel): """ secrets: list[Secret] = Field(..., max_length=8, min_length=1) """ - Secrets used to authenticate to the target cluster. Typically sourced from a GKECluster's status.secrets. All secrets must be in the same namespace as this ServingStack. A Kubeconfig secret is required. If a cloud-specific credential secret is present (e.g. GCPServiceAccountKey), the ProviderConfigs will use it for identity-based authentication instead of relying on the kubeconfig's embedded credentials. + Secrets used to authenticate to the target cluster. Typically sourced from a GKECluster's status.secrets. All secrets must be in the same namespace as this ServingStack. A Kubeconfig secret is required. If a cloud identity secret is present, the serving stack authenticates as that identity instead of relying on the kubeconfig's embedded credentials. """ versions: Versions | None = None """ diff --git a/schemas/python/models/io/crossplane/helm/providerconfig/v1beta1.py b/schemas/python/models/io/crossplane/helm/providerconfig/v1beta1.py index 204a2c389..efaddebe4 100644 --- a/schemas/python/models/io/crossplane/helm/providerconfig/v1beta1.py +++ b/schemas/python/models/io/crossplane/helm/providerconfig/v1beta1.py @@ -87,6 +87,7 @@ class Identity(BaseModel): 'AzureWorkloadIdentityCredentials', 'UpboundTokens', 'AWSWebIdentityCredentials', + 'NebiusServiceAccountCredentials', ] """ Type of identity. diff --git a/schemas/python/models/io/crossplane/helm/release/v1beta1.py b/schemas/python/models/io/crossplane/helm/release/v1beta1.py index 09bad7a7c..10711ccda 100644 --- a/schemas/python/models/io/crossplane/helm/release/v1beta1.py +++ b/schemas/python/models/io/crossplane/helm/release/v1beta1.py @@ -5,7 +5,7 @@ from typing import Any, Literal -from pydantic import AwareDatetime, BaseModel, Field +from pydantic import AwareDatetime, BaseModel, Field, constr from ....k8s.apimachinery.pkg.apis.meta import v1 @@ -69,6 +69,12 @@ class PullSecretRef(BaseModel): class Chart(BaseModel): + digest: constr(pattern=r'^sha256:[a-f0-9]{64}$') | None = None + """ + Digest is the OCI image digest in the format "sha256:abc123..." + Only supported for OCI registries. When specified, the chart will be pulled by digest. + Can be used alone or in combination with Version. Optional. + """ name: str | None = None """ Name of Helm chart, required if ChartSpec.URL not set @@ -89,7 +95,10 @@ class Chart(BaseModel): """ version: str | None = None """ - Version of Helm chart, late initialized with latest version if not set + Version of Helm chart. Optional when Digest is specified. + If not set and Digest is not specified, gets late initialized with the latest available version. + If not set and Digest is specified, version is NOT late initialized to avoid spec drift. + The actual deployed version is always available in status.atProvider.version for observability. """ @@ -158,6 +167,10 @@ class ForProvider(BaseModel): """ InsecureSkipTLSVerify skips tls certificate checks for the chart download """ + maxHistory: int | None = 20 + """ + MaxHistory limits the maximum number of revisions saved per release. Use 0 for no limit. + """ namespace: str """ Namespace to install the release into. @@ -179,6 +192,14 @@ class ForProvider(BaseModel): """ SkipCreateNamespace won't create the namespace for the release. This requires the namespace to already exist. """ + takeOwnership: bool | None = None + """ + TakeOwnership ignores Helm ownership validation and adopts pre-existing releases. + This is a ONE-TIME operation: after the first successful deployment, the flag is recorded + in status.atProvider.ownershipTaken and subsequent reconciles use normal Helm validation. + This prevents silent adoption of unrelated resources during chart upgrades. + Use this field to migrate manually-deployed Helm releases into Crossplane management. + """ values: dict[str, Any] | None = None valuesFrom: list[ValuesFromItem] | None = None wait: bool | None = None @@ -285,12 +306,26 @@ class Spec(BaseModel): class AtProvider(BaseModel): + digest: str | None = None + """ + Digest is the last successfully deployed chart digest (for OCI charts only). + """ + ownershipTaken: bool | None = None + """ + OwnershipTaken indicates that spec.forProvider.takeOwnership was used for initial adoption. + Once set to true, subsequent reconciles use normal Helm validation instead of takeOwnership, + preventing silent adoption of unrelated resources during upgrades. + """ releaseDescription: str | None = None revision: int | None = None state: str | None = None """ Status is the status of a release """ + version: str | None = None + """ + Version is the actual deployed chart version. + """ class Condition(BaseModel): diff --git a/schemas/python/models/io/crossplane/kubernetes/object/v1alpha2.py b/schemas/python/models/io/crossplane/kubernetes/object/v1alpha2.py index 843a34ec7..49882c583 100644 --- a/schemas/python/models/io/crossplane/kubernetes/object/v1alpha2.py +++ b/schemas/python/models/io/crossplane/kubernetes/object/v1alpha2.py @@ -54,6 +54,12 @@ class ConnectionDetail(BaseModel): class ForProvider(BaseModel): + deletionPropagationPolicy: Literal['Orphan', 'Background', 'Foreground'] | None = ( + 'Background' + ) + """ + Deletion policy for created kubernetes object, defaults to Background + """ manifest: dict[str, Any] """ Raw JSON representation of the kubernetes object to be created. diff --git a/schemas/python/models/io/crossplane/kubernetes/providerconfig/v1alpha1.py b/schemas/python/models/io/crossplane/kubernetes/providerconfig/v1alpha1.py index 32fc12dc6..724de4630 100644 --- a/schemas/python/models/io/crossplane/kubernetes/providerconfig/v1alpha1.py +++ b/schemas/python/models/io/crossplane/kubernetes/providerconfig/v1alpha1.py @@ -87,6 +87,7 @@ class Identity(BaseModel): 'AzureWorkloadIdentityCredentials', 'UpboundTokens', 'AWSWebIdentityCredentials', + 'NebiusServiceAccountCredentials', ] """ Type of identity. diff --git a/schemas/python/models/io/crossplane/m/helm/clusterproviderconfig/v1beta1.py b/schemas/python/models/io/crossplane/m/helm/clusterproviderconfig/v1beta1.py index afe76db5b..d17c3b66d 100644 --- a/schemas/python/models/io/crossplane/m/helm/clusterproviderconfig/v1beta1.py +++ b/schemas/python/models/io/crossplane/m/helm/clusterproviderconfig/v1beta1.py @@ -87,6 +87,7 @@ class Identity(BaseModel): 'AzureWorkloadIdentityCredentials', 'UpboundTokens', 'AWSWebIdentityCredentials', + 'NebiusServiceAccountCredentials', ] """ Type of identity. diff --git a/schemas/python/models/io/crossplane/m/helm/providerconfig/v1beta1.py b/schemas/python/models/io/crossplane/m/helm/providerconfig/v1beta1.py index a2f78f35b..f71160c81 100644 --- a/schemas/python/models/io/crossplane/m/helm/providerconfig/v1beta1.py +++ b/schemas/python/models/io/crossplane/m/helm/providerconfig/v1beta1.py @@ -87,6 +87,7 @@ class Identity(BaseModel): 'AzureWorkloadIdentityCredentials', 'UpboundTokens', 'AWSWebIdentityCredentials', + 'NebiusServiceAccountCredentials', ] """ Type of identity. diff --git a/schemas/python/models/io/crossplane/m/helm/release/v1beta1.py b/schemas/python/models/io/crossplane/m/helm/release/v1beta1.py index 096456c0a..bf12429e9 100644 --- a/schemas/python/models/io/crossplane/m/helm/release/v1beta1.py +++ b/schemas/python/models/io/crossplane/m/helm/release/v1beta1.py @@ -5,7 +5,7 @@ from typing import Any, Literal -from pydantic import AwareDatetime, BaseModel, Field +from pydantic import AwareDatetime, BaseModel, Field, constr from .....k8s.apimachinery.pkg.apis.meta import v1 @@ -65,6 +65,12 @@ class PullSecretRef(BaseModel): class Chart(BaseModel): + digest: constr(pattern=r'^sha256:[a-f0-9]{64}$') | None = None + """ + Digest is the OCI image digest in the format "sha256:abc123..." + Only supported for OCI registries. When specified, the chart will be pulled by digest. + Can be used alone or in combination with Version. Optional. + """ name: str | None = None """ Name of Helm chart, required if ChartSpec.URL not set @@ -85,7 +91,10 @@ class Chart(BaseModel): """ version: str | None = None """ - Version of Helm chart, late initialized with latest version if not set + Version of Helm chart. Optional when Digest is specified. + If not set and Digest is not specified, gets late initialized with the latest available version. + If not set and Digest is specified, version is NOT late initialized to avoid spec drift. + The actual deployed version is always available in status.atProvider.version for observability. """ @@ -152,6 +161,10 @@ class ForProvider(BaseModel): """ InsecureSkipTLSVerify skips tls certificate checks for the chart download """ + maxHistory: int | None = 20 + """ + MaxHistory limits the maximum number of revisions saved per release. Use 0 for no limit. + """ namespace: str | None = None """ Namespace to install the release into. @@ -174,6 +187,14 @@ class ForProvider(BaseModel): """ SkipCreateNamespace won't create the namespace for the release. This requires the namespace to already exist. """ + takeOwnership: bool | None = None + """ + TakeOwnership ignores Helm ownership validation and adopts pre-existing releases. + This is a ONE-TIME operation: after the first successful deployment, the flag is recorded + in status.atProvider.ownershipTaken and subsequent reconciles use normal Helm validation. + This prevents silent adoption of unrelated resources during chart upgrades. + Use this field to migrate manually-deployed Helm releases into Crossplane management. + """ values: dict[str, Any] | None = None valuesFrom: list[ValuesFromItem] | None = None wait: bool | None = None @@ -245,12 +266,26 @@ class Spec(BaseModel): class AtProvider(BaseModel): + digest: str | None = None + """ + Digest is the last successfully deployed chart digest (for OCI charts only). + """ + ownershipTaken: bool | None = None + """ + OwnershipTaken indicates that spec.forProvider.takeOwnership was used for initial adoption. + Once set to true, subsequent reconciles use normal Helm validation instead of takeOwnership, + preventing silent adoption of unrelated resources during upgrades. + """ releaseDescription: str | None = None revision: int | None = None state: str | None = None """ Status is the status of a release """ + version: str | None = None + """ + Version is the actual deployed chart version. + """ class Condition(BaseModel): diff --git a/schemas/python/models/io/crossplane/m/kubernetes/clusterproviderconfig/v1alpha1.py b/schemas/python/models/io/crossplane/m/kubernetes/clusterproviderconfig/v1alpha1.py index 255bb1796..a99da58dc 100644 --- a/schemas/python/models/io/crossplane/m/kubernetes/clusterproviderconfig/v1alpha1.py +++ b/schemas/python/models/io/crossplane/m/kubernetes/clusterproviderconfig/v1alpha1.py @@ -87,6 +87,7 @@ class Identity(BaseModel): 'AzureWorkloadIdentityCredentials', 'UpboundTokens', 'AWSWebIdentityCredentials', + 'NebiusServiceAccountCredentials', ] """ Type of identity. diff --git a/schemas/python/models/io/crossplane/m/kubernetes/object/v1alpha1.py b/schemas/python/models/io/crossplane/m/kubernetes/object/v1alpha1.py index b3aa2c9ad..1528677d5 100644 --- a/schemas/python/models/io/crossplane/m/kubernetes/object/v1alpha1.py +++ b/schemas/python/models/io/crossplane/m/kubernetes/object/v1alpha1.py @@ -54,6 +54,12 @@ class ConnectionDetail(BaseModel): class ForProvider(BaseModel): + deletionPropagationPolicy: Literal['Orphan', 'Background', 'Foreground'] | None = ( + 'Background' + ) + """ + Deletion policy for created kubernetes object, defaults to Background + """ manifest: dict[str, Any] """ Raw JSON representation of the kubernetes object to be created. diff --git a/schemas/python/models/io/crossplane/m/kubernetes/providerconfig/v1alpha1.py b/schemas/python/models/io/crossplane/m/kubernetes/providerconfig/v1alpha1.py index f25f361e0..e74d37fb5 100644 --- a/schemas/python/models/io/crossplane/m/kubernetes/providerconfig/v1alpha1.py +++ b/schemas/python/models/io/crossplane/m/kubernetes/providerconfig/v1alpha1.py @@ -87,6 +87,7 @@ class Identity(BaseModel): 'AzureWorkloadIdentityCredentials', 'UpboundTokens', 'AWSWebIdentityCredentials', + 'NebiusServiceAccountCredentials', ] """ Type of identity.