From 62c3dca4df0f2e4607b6b186592a4c504553b18a Mon Sep 17 00:00:00 2001 From: r3loac Date: Tue, 25 Aug 2026 15:03:08 +0300 Subject: [PATCH 1/2] feat: show NodePool status in kubectl output - Add a Status printer column derived from the NodePool Ready condition - Regenerate the NodePool CRD - Add regression coverage for the printer column - Document the design, behavior, and deployment workflow --- api/v1alpha1/nodepool_printer_columns_test.go | 76 +++++++++++++++++++ api/v1alpha1/nodepool_types.go | 1 + .../bases/nebula.inftyai.com_nodepools.yaml | 11 ++- docs/architecture.md | 7 ++ docs/deploy.md | 7 +- docs/design/nodepool-status-column.md | 47 ++++++++++++ 6 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 api/v1alpha1/nodepool_printer_columns_test.go create mode 100644 docs/design/nodepool-status-column.md diff --git a/api/v1alpha1/nodepool_printer_columns_test.go b/api/v1alpha1/nodepool_printer_columns_test.go new file mode 100644 index 0000000..d6a7f00 --- /dev/null +++ b/api/v1alpha1/nodepool_printer_columns_test.go @@ -0,0 +1,76 @@ +/* +Copyright 2026. + +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 ( + "os" + "path/filepath" + "runtime" + "testing" + + "sigs.k8s.io/yaml" +) + +func TestNodePoolCRDHasReadyStatusPrinterColumn(t *testing.T) { + t.Parallel() + + _, thisFile, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("resolve test file path") + } + manifestPath := filepath.Join(filepath.Dir(thisFile), "..", "..", "config", "crd", "bases", "nebula.inftyai.com_nodepools.yaml") + raw, err := os.ReadFile(manifestPath) + if err != nil { + t.Fatalf("read NodePool CRD: %v", err) + } + + var manifest struct { + Spec struct { + Versions []struct { + Name string `yaml:"name"` + AdditionalPrinterColumns []struct { + Name string `yaml:"name"` + Type string `yaml:"type"` + JSONPath string `yaml:"jsonPath"` + } `yaml:"additionalPrinterColumns"` + } `yaml:"versions"` + } `yaml:"spec"` + } + if err := yaml.Unmarshal(raw, &manifest); err != nil { + t.Fatalf("parse NodePool CRD: %v", err) + } + + const wantJSONPath = `.status.conditions[?(@.type=="Ready")].status` + found := 0 + for _, version := range manifest.Spec.Versions { + if version.Name != "v1alpha1" { + continue + } + for _, column := range version.AdditionalPrinterColumns { + if column.Name != "Status" { + continue + } + found++ + if column.Type != "string" || column.JSONPath != wantJSONPath { + t.Fatalf("Status column = type %q, JSONPath %q; want string, %q", column.Type, column.JSONPath, wantJSONPath) + } + } + } + if found != 1 { + t.Fatalf("found %d Status columns in v1alpha1; want 1", found) + } +} diff --git a/api/v1alpha1/nodepool_types.go b/api/v1alpha1/nodepool_types.go index 647167f..cf64934 100644 --- a/api/v1alpha1/nodepool_types.go +++ b/api/v1alpha1/nodepool_types.go @@ -266,6 +266,7 @@ type NodePoolStatus struct { // +kubebuilder:object:root=true // +kubebuilder:resource:scope=Cluster,shortName=np // +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` // +kubebuilder:printcolumn:name="Strategy",type=string,JSONPath=`.spec.strategy` // +kubebuilder:printcolumn:name="Providers",type=string,JSONPath=`.status.providers` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` diff --git a/config/crd/bases/nebula.inftyai.com_nodepools.yaml b/config/crd/bases/nebula.inftyai.com_nodepools.yaml index b084570..9fb4152 100644 --- a/config/crd/bases/nebula.inftyai.com_nodepools.yaml +++ b/config/crd/bases/nebula.inftyai.com_nodepools.yaml @@ -17,10 +17,13 @@ spec: scope: Cluster versions: - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Status + type: string - jsonPath: .spec.strategy name: Strategy type: string - - jsonPath: .spec.providers[*].name + - jsonPath: .status.providers name: Providers type: string - jsonPath: .metadata.creationTimestamp @@ -296,6 +299,12 @@ spec: Placed counts existing instances per provider (booting included), for at-a-glance balance. type: object + providers: + description: |- + Providers is a comma-separated list of provider names from the pool + spec. kubectl printcolumns cannot join array fields via JSONPath, so + the controller materializes this summary for `kubectl get nodepool`. + type: string type: object type: object served: true diff --git a/docs/architecture.md b/docs/architecture.md index 49d9adf..c4030e0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -406,6 +406,12 @@ Responsibilities: - compute `status.placed` from Bound NodeClaims per provider; - watch NodeClaims so placement counts update as instances come and go. +The default `kubectl get nodepools` table exposes the `Ready` condition's value +as `STATUS`, followed by strategy, providers, and age. The CRD printer column +reads the condition directly, so `status.conditions` remains the source of truth. +See the [printer-column design](design/nodepool-status-column.md) for the empty +condition and compatibility behavior. + Static spec rules are admission-time CEL validations. Examples: `Weighted` requires a weight on every provider entry, and AWS provider entries require at least one region. @@ -502,6 +508,7 @@ spec: failover: blocklistTTL: 30s status: + providers: modal,aws placed: modal: 2 aws: 1 diff --git a/docs/deploy.md b/docs/deploy.md index 663618f..8c4c08d 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -197,7 +197,12 @@ diff <(kubectl get secret nebula-webhook-server-cert -n nebula-system -o jsonpat A pool referencing an unregistered provider shows it plainly: ```bash -kubectl get nodepool -o jsonpath='{.status.conditions}' +kubectl get nodepools +# NAME STATUS STRATEGY PROVIDERS AGE +# gpu-pool False Ordered modal,aws 2m + +# Inspect the condition reason and message when STATUS is False. +kubectl get nodepool -o jsonpath='{.status.conditions[?(@.type=="Ready")]}' # Ready=False / UnknownProvider means that provider's creds are missing or wrong. ``` diff --git a/docs/design/nodepool-status-column.md b/docs/design/nodepool-status-column.md new file mode 100644 index 0000000..be58592 --- /dev/null +++ b/docs/design/nodepool-status-column.md @@ -0,0 +1,47 @@ +# NodePool status printer column + +## Context + +`NodePool.status.conditions` already reports whether a pool can be used. The +controller owns a standard `Ready` condition and sets it to `True` for a valid +pool or `False` when an environment-dependent validation, such as provider +registration, fails. However, the default `kubectl get nodepools` table does not +show that signal, so operators must request the full object or write a JSONPath. + +## Decision + +Add a `Status` CRD printer column whose JSONPath selects the status of the +`Ready` condition: + +```text +.status.conditions[?(@.type=="Ready")].status +``` + +The column is derived directly by the Kubernetes API server when it renders the +table. No duplicate status field or controller change is introduced. This keeps +the condition as the single source of truth and uses the standard condition +values `True`, `False`, and `Unknown`. + +The column appears before policy details so pool health is visible immediately: + +```text +NAME STATUS STRATEGY PROVIDERS AGE +gpu-pool True Ordered modal,runpod 2m +``` + +Before the controller has written the `Ready` condition, the table cell has no +value. This is preferable to manufacturing a fourth status value because absence +already means the controller has not observed the object. + +## Compatibility and rollout + +This is an additive change to `additionalPrinterColumns`; the stored and served +resource schema is unchanged. Existing clients that read `NodePool` objects are +unaffected. Installing the regenerated CRD is sufficient to enable the column +for existing pools, and the next `kubectl get` uses their existing conditions. + +## Verification + +Generation is checked into `config/crd/bases`. A unit test parses that manifest +and requires exactly one `Status` string column with the `Ready`-condition +JSONPath, preventing source markers and generated API artifacts from drifting. From a03b79c78a85bf9788a1d6a518289ddf1cc8f5e1 Mon Sep 17 00:00:00 2001 From: r3loac Date: Tue, 25 Aug 2026 20:37:28 +0300 Subject: [PATCH 2/2] part 2# Add a Status printer column derived from the NodePool Ready condition Regenerate the NodePool CRD Add regression coverage for the printer column Document the design, behavior, and deployment workflow --- api/v1alpha1/nodepool_printer_columns_test.go | 76 ------------------- docs/design/nodepool-status-column.md | 5 +- 2 files changed, 2 insertions(+), 79 deletions(-) delete mode 100644 api/v1alpha1/nodepool_printer_columns_test.go diff --git a/api/v1alpha1/nodepool_printer_columns_test.go b/api/v1alpha1/nodepool_printer_columns_test.go deleted file mode 100644 index d6a7f00..0000000 --- a/api/v1alpha1/nodepool_printer_columns_test.go +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright 2026. - -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 ( - "os" - "path/filepath" - "runtime" - "testing" - - "sigs.k8s.io/yaml" -) - -func TestNodePoolCRDHasReadyStatusPrinterColumn(t *testing.T) { - t.Parallel() - - _, thisFile, _, ok := runtime.Caller(0) - if !ok { - t.Fatal("resolve test file path") - } - manifestPath := filepath.Join(filepath.Dir(thisFile), "..", "..", "config", "crd", "bases", "nebula.inftyai.com_nodepools.yaml") - raw, err := os.ReadFile(manifestPath) - if err != nil { - t.Fatalf("read NodePool CRD: %v", err) - } - - var manifest struct { - Spec struct { - Versions []struct { - Name string `yaml:"name"` - AdditionalPrinterColumns []struct { - Name string `yaml:"name"` - Type string `yaml:"type"` - JSONPath string `yaml:"jsonPath"` - } `yaml:"additionalPrinterColumns"` - } `yaml:"versions"` - } `yaml:"spec"` - } - if err := yaml.Unmarshal(raw, &manifest); err != nil { - t.Fatalf("parse NodePool CRD: %v", err) - } - - const wantJSONPath = `.status.conditions[?(@.type=="Ready")].status` - found := 0 - for _, version := range manifest.Spec.Versions { - if version.Name != "v1alpha1" { - continue - } - for _, column := range version.AdditionalPrinterColumns { - if column.Name != "Status" { - continue - } - found++ - if column.Type != "string" || column.JSONPath != wantJSONPath { - t.Fatalf("Status column = type %q, JSONPath %q; want string, %q", column.Type, column.JSONPath, wantJSONPath) - } - } - } - if found != 1 { - t.Fatalf("found %d Status columns in v1alpha1; want 1", found) - } -} diff --git a/docs/design/nodepool-status-column.md b/docs/design/nodepool-status-column.md index be58592..85501de 100644 --- a/docs/design/nodepool-status-column.md +++ b/docs/design/nodepool-status-column.md @@ -42,6 +42,5 @@ for existing pools, and the next `kubectl get` uses their existing conditions. ## Verification -Generation is checked into `config/crd/bases`. A unit test parses that manifest -and requires exactly one `Status` string column with the `Ready`-condition -JSONPath, preventing source markers and generated API artifacts from drifting. +Generation is checked into `config/crd/bases`. Regenerating the manifests keeps +the CRD printer column aligned with the marker in `nodepool_types.go`.