api/controller/cluster_upgrade_test.go
Rama main · réplica de lectura del repositorio original.
package controller
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/goodrain/rainbond-operator/api/v1alpha1"
"github.com/goodrain/rainbond/pkg/component/k8s"
httputil "github.com/goodrain/rainbond/util/http"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/rest"
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
)
// capability_id: rainbond.cluster.upgrade-preserves-component-config
func TestUpgradePreservesComponentConfig(t *testing.T) {
for _, tc := range []struct {
name string
namespace string
image string
}{
{name: "new image", image: "example.invalid/rainbond:new"},
{name: "same image", image: "example.invalid/rainbond:old"},
{name: "custom namespace", namespace: "custom-system", image: "example.invalid/rainbond:new"},
} {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("RBD_NAMESPACE", tc.namespace)
namespace := tc.namespace
if namespace == "" {
namespace = "rbd-system"
}
// affinity and tolerations are valid CRD fields missing from the vendored Go type.
stored := []byte(`{
"apiVersion":"rainbond.io/v1alpha1","kind":"RbdComponent",
"metadata":{"name":"rbd-app-ui","namespace":"` + namespace + `","resourceVersion":"42",
"labels":{"custom":"keep"},"annotations":{"custom":"keep"}},
"spec":{"image":"example.invalid/rainbond:old","replicas":3,"priorityComponent":false,
"imagePullPolicy":"IfNotPresent","args":["--custom"],
"env":[{"name":"DB_TYPE","value":"mysql"}],
"resources":{"requests":{"cpu":"100m"}},
"volumes":[{"name":"custom","emptyDir":{}}],
"volumeMounts":[{"name":"custom","mountPath":"/custom"}],
"affinity":{"nodeAffinity":{"requiredDuringSchedulingIgnoredDuringExecution":{
"nodeSelectorTerms":[{"matchFields":[{"key":"metadata.name","operator":"In","values":["custom-node"]}]}]}}},
"tolerations":[{"key":"node.kubernetes.io/unschedulable","operator":"Exists","effect":"NoSchedule"}]},
"status":{"conditions":[{"type":"ClusterConfigCompeleted","status":"True","reason":"ConfigCompleted"}]}
}`)
var expected map[string]interface{}
if err := json.Unmarshal(stored, &expected); err != nil {
t.Fatal(err)
}
expected["spec"].(map[string]interface{})["image"] = tc.image
writes := 0
setUpgradeTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if want := "/apis/rainbond.io/v1alpha1/namespaces/" + namespace + "/rbdcomponents/rbd-app-ui"; r.URL.Path != want {
t.Errorf("unexpected request path: %s", r.URL.Path)
http.NotFound(w, r)
return
}
switch r.Method {
case http.MethodGet:
case http.MethodPut, http.MethodPatch:
writes++
body, err := io.ReadAll(r.Body)
if err != nil {
t.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if r.Method == http.MethodPatch {
if got := r.Header.Get("Content-Type"); got != "application/merge-patch+json" {
t.Errorf("unexpected patch type: %s", got)
}
stored, err = jsonpatch.MergePatch(stored, body)
if err != nil {
t.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
} else {
stored = body
}
default:
t.Errorf("unexpected method: %s", r.Method)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
_, _ = w.Write(stored)
})
response := runUpgradeRequest(t, `{"rbd-app-ui":"`+tc.image+`"}`)
if len(response.List.([]interface{})) != 0 {
t.Fatalf("unexpected component errors: %v", response.List)
}
if writes != 1 {
t.Fatalf("expected one component write, got %d", writes)
}
var actual map[string]interface{}
if err := json.Unmarshal(stored, &actual); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("upgrade must change only spec.image; got %s", stored)
}
})
}
}
func TestUpgradeReportsComponentErrorsAndContinues(t *testing.T) {
for _, tc := range []struct {
name string
failMethod string
message string
}{
{name: "get failure", failMethod: http.MethodGet, message: "rbd-failed获取异常"},
{name: "patch failure", failMethod: http.MethodPatch, message: "rbd-failed更新异常"},
} {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("RBD_NAMESPACE", "rbd-system")
upgraded := false
setUpgradeTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
failed := strings.HasSuffix(r.URL.Path, "/rbd-failed")
if failed && r.Method == tc.failMethod {
w.WriteHeader(http.StatusForbidden)
_, _ = io.WriteString(w, `{"apiVersion":"v1","kind":"Status","status":"Failure","reason":"Forbidden","message":"access denied","code":403}`)
return
}
name := "rbd-success"
if failed {
name = "rbd-failed"
}
if r.Method == http.MethodPatch && !failed {
upgraded = true
}
_, _ = io.WriteString(w, `{"apiVersion":"rainbond.io/v1alpha1","kind":"RbdComponent","metadata":{"name":"`+name+`","namespace":"rbd-system"},"spec":{"image":"example.invalid/rainbond:old"}}`)
})
response := runUpgradeRequest(t, `{"rbd-failed":"example.invalid/rainbond:new","rbd-success":"example.invalid/rainbond:new"}`)
failures, ok := response.List.([]interface{})
if !ok || len(failures) != 1 {
t.Fatalf("expected one component error, got %v", response.List)
}
if message, ok := failures[0].(string); !ok || !strings.Contains(message, tc.message) {
t.Fatalf("expected error containing %q, got %v", tc.message, failures[0])
}
if !upgraded {
t.Fatal("failure of one component must not prevent another component from upgrading")
}
})
}
}
func runUpgradeRequest(t *testing.T, body string) httputil.ResponseBody {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/v2/cluster/rbd-upgrade", strings.NewReader(body))
recorder := httptest.NewRecorder()
(&ClusterController{}).Upgrade(recorder, req)
if recorder.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d: %s", recorder.Code, recorder.Body.String())
}
var response httputil.ResponseBody
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatal(err)
}
return response
}
func setUpgradeTestClient(t *testing.T, handler http.HandlerFunc) {
t.Helper()
server := httptest.NewServer(handler)
t.Cleanup(server.Close)
scheme := runtime.NewScheme()
if err := v1alpha1.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{v1alpha1.GroupVersion})
mapper.Add(v1alpha1.GroupVersion.WithKind("RbdComponent"), meta.RESTScopeNamespace)
client, err := k8sclient.New(&rest.Config{Host: server.URL}, k8sclient.Options{Scheme: scheme, Mapper: mapper})
if err != nil {
t.Fatal(err)
}
component := k8s.Default()
if component == nil {
component = k8s.New()
}
previous := component.K8sClient
component.K8sClient = client
t.Cleanup(func() { component.K8sClient = previous })
}