mirror of
https://github.com/hanzoai/compute.git
synced 2026-10-30 14:50:41 +00:00
The fleet showed standalone droplets but never the WORKER NODES of a DOKS cluster: ListNodePools already fetched each pool's Nodes (with real DropletID) but SyncNodePoolsCloud discarded the per-node list, and visor never called godo's Kubernetes.List/Get. So a cluster's nodes were invisible on world.hanzo.ai even though each is a real droplet. Source the nodes and surface them as fleet Machines: - service/doks.go: ListClusters(ctx) (Kubernetes.List + per-cluster Get for authoritative region/status/pools), nodePoolMachines (the ONE node->Machine mapper: Id=DropletID so it dedups against the droplet list, Name=node.Name, Provider=DigitalOcean, Size=pool.Size, Region=cluster.RegionSlug, State=node.Status, Tag=doks-cluster:<name>, IPs honestly empty; a node still provisioning with no DropletID is skipped), and the two org associations that feed it. - service/compute.go ListOrgKubernetesNodes: HOUSE account, cluster hanzo-org tag association (DOKS node droplets carry k8s tags, not a hanzo-org DROPLET tag, so they never surfaced via ListOrgMachines). - object/machine_cloud.go GetKubernetesNodesCloud: BYOC providers, the Provider.ClusterID association (mirrors SyncNodePoolsCloud's DO selection). - controllers/compute.go ListComputeKubernetesNodes: org-scoped, unions BOTH associations deduped by id-or-name (a cluster found via tag AND provider, or a node whose droplet is also listed, appears once) in the casibase envelope. - routers/router.go: GET /v1/kubernetes-nodes. Both associations implemented and unioned rather than guessing which is live — strictly more correct. Tests: service TestNodePoolMachines (mapping + skip unprovisioned + empty IPs) passes; controllers TestUnionMachinesDedup compiles and pins the dedup contract. (The object/controllers test BINARIES currently panic at init on origin/main — modernc.org/sqlite vs hanzoai/sqlite both register driver "sqlite" via object -> hanzoai/vfs/replica -> modernc, a pre-existing PR #10 migration artifact unrelated to this change; go build ./... and go vet are green.)
78 lines
3.0 KiB
Go
78 lines
3.0 KiB
Go
// Copyright 2025 Hanzo Industries Inc. All Rights Reserved.
|
|
//
|
|
// 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 service
|
|
|
|
import "testing"
|
|
|
|
// TestNodePoolMachines pins the DOKS node -> fleet Machine mapping: one Machine
|
|
// per WORKER NODE (not per pool), keyed by the node's DropletID so it dedups
|
|
// against the live droplet list, sized by its pool, regioned + cluster-tagged by
|
|
// its cluster, and carrying the node's DO status. A node still provisioning (no
|
|
// DropletID) has no fleet identity and is skipped. IPs are honestly empty.
|
|
func TestNodePoolMachines(t *testing.T) {
|
|
cluster := &KubernetesCluster{ID: "cl-1", Name: "prod", RegionSlug: "sfo3"}
|
|
pools := []*NodePool{
|
|
{
|
|
Name: "default", Size: "s-4vcpu-8gb",
|
|
Nodes: []NodeInfo{
|
|
{ID: "n-1", Name: "prod-default-aaa", DropletID: "111", Status: "running"},
|
|
{ID: "n-2", Name: "prod-default-bbb", DropletID: "222", Status: "running"},
|
|
{ID: "n-3", Name: "prod-default-ccc", DropletID: "", Status: "provisioning"}, // skipped
|
|
},
|
|
},
|
|
{
|
|
Name: "gpu", Size: "gpu-h100x1-80gb",
|
|
Nodes: []NodeInfo{
|
|
{ID: "n-4", Name: "prod-gpu-ddd", DropletID: "333", Status: "running"},
|
|
},
|
|
},
|
|
}
|
|
|
|
got := nodePoolMachines(cluster, pools)
|
|
if len(got) != 3 {
|
|
t.Fatalf("want 3 machines (2 default + 1 gpu, provisioning node skipped), got %d: %+v", len(got), got)
|
|
}
|
|
|
|
byID := map[string]*Machine{}
|
|
for _, m := range got {
|
|
byID[m.Id] = m
|
|
}
|
|
for _, tc := range []struct{ id, name, size string }{
|
|
{"111", "prod-default-aaa", "s-4vcpu-8gb"},
|
|
{"222", "prod-default-bbb", "s-4vcpu-8gb"},
|
|
{"333", "prod-gpu-ddd", "gpu-h100x1-80gb"},
|
|
} {
|
|
m, ok := byID[tc.id]
|
|
if !ok {
|
|
t.Fatalf("node droplet %q missing from mapped machines: %+v", tc.id, got)
|
|
}
|
|
if m.Name != tc.name || m.Size != tc.size {
|
|
t.Errorf("droplet %q: got name=%q size=%q, want name=%q size=%q", tc.id, m.Name, m.Size, tc.name, tc.size)
|
|
}
|
|
if m.Provider != "DigitalOcean" || m.Region != "sfo3" || m.State != "running" || m.Tag != "doks-cluster:prod" {
|
|
t.Errorf("droplet %q: got provider=%q region=%q state=%q tag=%q, want DigitalOcean/sfo3/running/doks-cluster:prod",
|
|
tc.id, m.Provider, m.Region, m.State, m.Tag)
|
|
}
|
|
if m.PublicIp != "" || m.PrivateIp != "" {
|
|
t.Errorf("droplet %q: IPs must be empty (DOKS node API exposes none), got public=%q private=%q", tc.id, m.PublicIp, m.PrivateIp)
|
|
}
|
|
}
|
|
|
|
// A cluster with no pools maps to no machines (no panic, honest empty).
|
|
if got := nodePoolMachines(&KubernetesCluster{Name: "empty"}, nil); len(got) != 0 {
|
|
t.Fatalf("empty cluster want 0 machines, got %d", len(got))
|
|
}
|
|
}
|