fix(capacity): 允许显式禁用无节点站点
当 Worker Deployment 为零副本且站点没有 eligible Worker 节点时返回零容量状态,避免 min/max=0 的禁用站点令容量控制器持续 NotReady;非零副本仍保持失败关闭。\n\n验证:容量控制器单测、Go 全量测试、go vet、gofmt、迁移安全检查和人工发布脚本测试。
This commit is contained in:
@@ -179,6 +179,13 @@ func (client *KubernetesClient) SiteState(ctx context.Context, site string) (Kub
|
||||
return KubernetesSiteState{}, err
|
||||
}
|
||||
if len(nodes.Items) == 0 {
|
||||
// A zero-replica Deployment with no eligible nodes represents an
|
||||
// explicitly disabled site. It must remain visible to the planner, but it
|
||||
// has no node or metrics budget to collect. A non-zero Deployment still
|
||||
// fails closed so replicas cannot disappear from capacity accounting.
|
||||
if state.CurrentReplicas == 0 {
|
||||
return state, nil
|
||||
}
|
||||
return KubernetesSiteState{}, fmt.Errorf("site %s has no matching nodes", site)
|
||||
}
|
||||
var pods struct {
|
||||
|
||||
@@ -88,6 +88,71 @@ func TestKubernetesClientReadsSiteAndMutatesOnlyWorkerScale(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestKubernetesClientAllowsDisabledSiteWithoutWorkerNodes(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||||
switch {
|
||||
case request.Method == http.MethodGet && strings.Contains(request.URL.Path, "/deployments/easyai-worker-ningbo"):
|
||||
_, _ = w.Write([]byte(`{
|
||||
"spec":{"replicas":0,"template":{"spec":{"containers":[{
|
||||
"name":"worker","image":"registry.invalid/gateway@sha256:abc",
|
||||
"env":[{"name":"AI_GATEWAY_REVISION","value":"release-sha"}],
|
||||
"resources":{"requests":{"memory":"1536Mi","cpu":"500m"}}
|
||||
}]}}}
|
||||
}`))
|
||||
case request.Method == http.MethodGet && request.URL.Path == "/api/v1/nodes":
|
||||
_, _ = w.Write([]byte(`{"items":[]}`))
|
||||
default:
|
||||
http.NotFound(w, request)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
tokenFile := filepath.Join(t.TempDir(), "token")
|
||||
if err := os.WriteFile(tokenFile, []byte("test-token"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client, err := NewKubernetesClient(KubernetesConfig{
|
||||
Namespace: "easyai", APIServer: server.URL, TokenFile: tokenFile, HTTPClient: server.Client(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state, err := client.SiteState(context.Background(), "ningbo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if state.Site != "ningbo" || state.CurrentReplicas != 0 || len(state.Nodes) != 0 {
|
||||
t.Fatalf("site state=%+v", state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKubernetesClientRejectsActiveSiteWithoutWorkerNodes(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||||
switch {
|
||||
case request.Method == http.MethodGet && strings.Contains(request.URL.Path, "/deployments/easyai-worker-ningbo"):
|
||||
_, _ = w.Write([]byte(`{"spec":{"replicas":1,"template":{"spec":{"containers":[]}}}}`))
|
||||
case request.Method == http.MethodGet && request.URL.Path == "/api/v1/nodes":
|
||||
_, _ = w.Write([]byte(`{"items":[]}`))
|
||||
default:
|
||||
http.NotFound(w, request)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
tokenFile := filepath.Join(t.TempDir(), "token")
|
||||
if err := os.WriteFile(tokenFile, []byte("test-token"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client, err := NewKubernetesClient(KubernetesConfig{
|
||||
Namespace: "easyai", APIServer: server.URL, TokenFile: tokenFile, HTTPClient: server.Client(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := client.SiteState(context.Background(), "ningbo"); err == nil ||
|
||||
!strings.Contains(err.Error(), "site ningbo has no matching nodes") {
|
||||
t.Fatalf("expected missing Worker node error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuantityParsing(t *testing.T) {
|
||||
if value, err := parseBinaryQuantity("1536Mi"); err != nil || value != 1536<<20 {
|
||||
t.Fatalf("memory=%d err=%v", value, err)
|
||||
|
||||
Reference in New Issue
Block a user