Ctrl K
ComunidadGuías…

api/controller/cluster_upgrade_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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 })
}