Files
open-cluster-management/test/e2e/common.go
2024-01-17 14:28:54 +00:00

1258 lines
42 KiB
Go

package e2e
import (
"bytes"
"context"
"embed"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/onsi/gomega"
"github.com/valyala/fasttemplate"
authv1 "k8s.io/api/authentication/v1"
certificatesv1 "k8s.io/api/certificates/v1"
coordv1 "k8s.io/api/coordination/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/klog/v2"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
addonv1alpha1 "open-cluster-management.io/api/addon/v1alpha1"
addonclient "open-cluster-management.io/api/client/addon/clientset/versioned"
clusterclient "open-cluster-management.io/api/client/cluster/clientset/versioned"
operatorclient "open-cluster-management.io/api/client/operator/clientset/versioned"
workv1client "open-cluster-management.io/api/client/work/clientset/versioned"
clusterv1 "open-cluster-management.io/api/cluster/v1"
ocmfeature "open-cluster-management.io/api/feature"
operatorapiv1 "open-cluster-management.io/api/operator/v1"
workapiv1 "open-cluster-management.io/api/work/v1"
"open-cluster-management.io/ocm/pkg/operator/helpers"
"open-cluster-management.io/ocm/test/integration/util"
)
type Tester struct {
hubKubeConfigPath string
spokeKubeConfigPath string
HubKubeClient kubernetes.Interface
SpokeKubeClient kubernetes.Interface
HubAPIExtensionClient apiextensionsclient.Interface
HubClusterCfg *rest.Config
SpokeClusterCfg *rest.Config
OperatorClient operatorclient.Interface
ClusterClient clusterclient.Interface
HubWorkClient workv1client.Interface
SpokeWorkClient workv1client.Interface
AddOnClinet addonclient.Interface
hubRestMapper meta.RESTMapper
HubDynamicClient dynamic.Interface
SpokeDynamicClient dynamic.Interface
bootstrapHubSecret *corev1.Secret
EventuallyTimeout time.Duration
EventuallyInterval time.Duration
clusterManagerName string
clusterManagerNamespace string
operatorNamespace string
klusterletOperator string
registrationImage string
workImage string
singletonImage string
}
// kubeconfigPath is the path of kubeconfig file, will be get from env "KUBECONFIG" by default.
// bootstrapHubSecret is the bootstrap hub kubeconfig secret, and the format is "namespace/secretName".
// Default of bootstrapHubSecret is helpers.KlusterletDefaultNamespace/helpers.BootstrapHubKubeConfig.
func NewTester(hubKubeConfigPath, spokeKubeConfigPath, registrationImage, workImage, singletonImage string, timeout time.Duration) *Tester {
var tester = Tester{
hubKubeConfigPath: hubKubeConfigPath,
spokeKubeConfigPath: spokeKubeConfigPath,
EventuallyTimeout: timeout, // seconds
EventuallyInterval: 1 * time.Second, // seconds
clusterManagerName: "cluster-manager", // same name as deploy/cluster-manager/config/samples
clusterManagerNamespace: helpers.ClusterManagerDefaultNamespace,
operatorNamespace: "open-cluster-management",
klusterletOperator: "klusterlet",
registrationImage: registrationImage,
workImage: workImage,
singletonImage: singletonImage,
}
return &tester
}
func (t *Tester) Init() error {
var err error
if t.hubKubeConfigPath == "" {
t.hubKubeConfigPath = os.Getenv("KUBECONFIG")
}
if t.spokeKubeConfigPath == "" {
t.spokeKubeConfigPath = os.Getenv("KUBECONFIG")
}
if t.HubClusterCfg, err = clientcmd.BuildConfigFromFlags("", t.hubKubeConfigPath); err != nil {
klog.Errorf("failed to get HubClusterCfg from path %v . %v", t.hubKubeConfigPath, err)
return err
}
if t.SpokeClusterCfg, err = clientcmd.BuildConfigFromFlags("", t.spokeKubeConfigPath); err != nil {
klog.Errorf("failed to get SpokeClusterCfg from path %v . %v", t.spokeKubeConfigPath, err)
return err
}
if t.HubKubeClient, err = kubernetes.NewForConfig(t.HubClusterCfg); err != nil {
klog.Errorf("failed to get KubeClient. %v", err)
return err
}
if t.SpokeKubeClient, err = kubernetes.NewForConfig(t.SpokeClusterCfg); err != nil {
klog.Errorf("failed to get KubeClient. %v", err)
return err
}
hubHttpClient, err := rest.HTTPClientFor(t.HubClusterCfg)
if err != nil {
return err
}
t.hubRestMapper, err = apiutil.NewDynamicRESTMapper(t.HubClusterCfg, hubHttpClient)
if err != nil {
return err
}
if t.HubDynamicClient, err = dynamic.NewForConfig(t.HubClusterCfg); err != nil {
klog.Errorf("failed to get DynamicClient. %v", err)
return err
}
if t.SpokeDynamicClient, err = dynamic.NewForConfig(t.SpokeClusterCfg); err != nil {
klog.Errorf("failed to get DynamicClient. %v", err)
return err
}
if t.HubAPIExtensionClient, err = apiextensionsclient.NewForConfig(t.HubClusterCfg); err != nil {
klog.Errorf("failed to get HubApiExtensionClient. %v", err)
return err
}
if t.OperatorClient, err = operatorclient.NewForConfig(t.HubClusterCfg); err != nil {
klog.Errorf("failed to get OperatorClient. %v", err)
return err
}
if t.ClusterClient, err = clusterclient.NewForConfig(t.HubClusterCfg); err != nil {
klog.Errorf("failed to get ClusterClient. %v", err)
return err
}
if t.HubWorkClient, err = workv1client.NewForConfig(t.HubClusterCfg); err != nil {
klog.Errorf("failed to get WorkClient. %v", err)
return err
}
if t.SpokeWorkClient, err = workv1client.NewForConfig(t.SpokeClusterCfg); err != nil {
klog.Errorf("failed to get WorkClient. %v", err)
return err
}
if t.AddOnClinet, err = addonclient.NewForConfig(t.HubClusterCfg); err != nil {
klog.Errorf("failed to get AddOnClinet. %v", err)
return err
}
return nil
}
func (t *Tester) SetEventuallyTimeout(timeout time.Duration) *Tester {
t.EventuallyInterval = timeout
return t
}
func (t *Tester) SetEventuallyInterval(timeout time.Duration) *Tester {
t.EventuallyTimeout = timeout
return t
}
func (t *Tester) SetOperatorNamespace(ns string) *Tester {
t.operatorNamespace = ns
return t
}
func (t *Tester) SetBootstrapHubSecret(bootstrapHubSecret string) error {
var err error
var bootstrapHubSecretName = helpers.BootstrapHubKubeConfig
var bootstrapHubSecretNamespace = helpers.KlusterletDefaultNamespace
if bootstrapHubSecret != "" {
bootstrapHubSecretNamespace, bootstrapHubSecretName, err = cache.SplitMetaNamespaceKey(bootstrapHubSecret)
if err != nil {
klog.Errorf("the format of bootstrapHubSecret %v is invalid. %v", bootstrapHubSecret, err)
return err
}
}
if t.bootstrapHubSecret, err = t.SpokeKubeClient.CoreV1().Secrets(bootstrapHubSecretNamespace).
Get(context.TODO(), bootstrapHubSecretName, metav1.GetOptions{}); err != nil {
klog.Errorf("failed to get bootstrapHubSecret %v in ns %v. %v", bootstrapHubSecretName,
bootstrapHubSecretNamespace, err)
return err
}
t.bootstrapHubSecret.ObjectMeta.ResourceVersion = ""
t.bootstrapHubSecret.ObjectMeta.Namespace = ""
return nil
}
func (t *Tester) CreateKlusterlet(name, clusterName, klusterletNamespace string, mode operatorapiv1.InstallMode) (*operatorapiv1.Klusterlet, error) {
if name == "" {
return nil, fmt.Errorf("the name should not be null")
}
if klusterletNamespace == "" {
klusterletNamespace = helpers.KlusterletDefaultNamespace
}
var klusterlet = &operatorapiv1.Klusterlet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: operatorapiv1.KlusterletSpec{
RegistrationImagePullSpec: t.registrationImage,
WorkImagePullSpec: t.workImage,
ImagePullSpec: t.singletonImage,
ExternalServerURLs: []operatorapiv1.ServerURL{
{
URL: "https://localhost",
},
},
ClusterName: clusterName,
Namespace: klusterletNamespace,
DeployOption: operatorapiv1.KlusterletDeployOption{
Mode: mode,
},
},
}
agentNamespace := helpers.AgentNamespace(klusterlet)
klog.Infof("klusterlet: %s/%s, \t mode: %v, \t agent namespace: %s", klusterlet.Name, klusterlet.Namespace, mode, agentNamespace)
// create agentNamespace
namespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: agentNamespace,
Annotations: map[string]string{
"workload.openshift.io/allowed": "management",
},
},
}
if _, err := t.SpokeKubeClient.CoreV1().Namespaces().Get(context.TODO(), agentNamespace, metav1.GetOptions{}); err != nil {
if !errors.IsNotFound(err) {
klog.Errorf("failed to get ns %v. %v", agentNamespace, err)
return nil, err
}
if _, err := t.SpokeKubeClient.CoreV1().Namespaces().Create(context.TODO(),
namespace, metav1.CreateOptions{}); err != nil {
klog.Errorf("failed to create ns %v. %v", namespace, err)
return nil, err
}
}
// create bootstrap-hub-kubeconfig secret
secret := t.bootstrapHubSecret.DeepCopy()
if _, err := t.SpokeKubeClient.CoreV1().Secrets(agentNamespace).Get(context.TODO(), secret.Name, metav1.GetOptions{}); err != nil {
if !errors.IsNotFound(err) {
klog.Errorf("failed to get secret %v in ns %v. %v", secret.Name, agentNamespace, err)
return nil, err
}
if _, err = t.SpokeKubeClient.CoreV1().Secrets(agentNamespace).Create(context.TODO(), secret, metav1.CreateOptions{}); err != nil {
klog.Errorf("failed to create secret %v in ns %v. %v", secret, agentNamespace, err)
return nil, err
}
}
if helpers.IsHosted(mode) {
// create external-managed-kubeconfig, will use the same cluster to simulate the Hosted mode.
secret.Namespace = agentNamespace
secret.Name = helpers.ExternalManagedKubeConfig
if _, err := t.HubKubeClient.CoreV1().Secrets(agentNamespace).Get(context.TODO(), secret.Name, metav1.GetOptions{}); err != nil {
if !errors.IsNotFound(err) {
klog.Errorf("failed to get secret %v in ns %v. %v", secret.Name, agentNamespace, err)
return nil, err
}
if _, err = t.HubKubeClient.CoreV1().Secrets(agentNamespace).Create(context.TODO(), secret, metav1.CreateOptions{}); err != nil {
klog.Errorf("failed to create secret %v in ns %v. %v", secret, agentNamespace, err)
return nil, err
}
}
}
// create klusterlet CR
realKlusterlet, err := t.OperatorClient.OperatorV1().Klusterlets().Create(context.TODO(),
klusterlet, metav1.CreateOptions{})
if err != nil && !errors.IsAlreadyExists(err) {
klog.Errorf("failed to create klusterlet %v . %v", klusterlet.Name, err)
return nil, err
}
return realKlusterlet, nil
}
func (t *Tester) CreateApprovedKlusterlet(name, clusterName, klusterletNamespace string, mode operatorapiv1.InstallMode) (*operatorapiv1.Klusterlet, error) {
klusterlet, err := t.CreateKlusterlet(name, clusterName, klusterletNamespace, mode)
if err != nil {
return nil, err
}
gomega.Eventually(func() error {
_, err = t.GetCreatedManagedCluster(clusterName)
return err
}, t.EventuallyTimeout*5, t.EventuallyInterval*5).Should(gomega.Succeed())
gomega.Eventually(func() error {
return t.ApproveCSR(clusterName)
}, t.EventuallyTimeout, t.EventuallyInterval).Should(gomega.Succeed())
gomega.Eventually(func() error {
return t.AcceptsClient(clusterName)
}, t.EventuallyTimeout, t.EventuallyInterval).Should(gomega.Succeed())
gomega.Eventually(func() error {
return t.CheckManagedClusterStatus(clusterName)
}, t.EventuallyTimeout*5, t.EventuallyInterval*5).Should(gomega.Succeed())
return klusterlet, nil
}
func (t *Tester) CreatePureHostedKlusterlet(name, clusterName string) (*operatorapiv1.Klusterlet, error) {
if name == "" {
return nil, fmt.Errorf("the name should not be null")
}
var klusterlet = &operatorapiv1.Klusterlet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: operatorapiv1.KlusterletSpec{
RegistrationImagePullSpec: "quay.io/open-cluster-management/registration:latest",
WorkImagePullSpec: "quay.io/open-cluster-management/work:latest",
ExternalServerURLs: []operatorapiv1.ServerURL{
{
URL: "https://localhost",
},
},
ClusterName: clusterName,
DeployOption: operatorapiv1.KlusterletDeployOption{
Mode: operatorapiv1.InstallModeHosted,
},
},
}
// create klusterlet CR
realKlusterlet, err := t.OperatorClient.OperatorV1().Klusterlets().Create(context.TODO(),
klusterlet, metav1.CreateOptions{})
if err != nil {
klog.Errorf("failed to create klusterlet %v . %v", klusterlet.Name, err)
return nil, err
}
return realKlusterlet, nil
}
func (t *Tester) GetCreatedManagedCluster(clusterName string) (*clusterv1.ManagedCluster, error) {
if clusterName == "" {
return nil, fmt.Errorf("the name of managedcluster should not be null")
}
cluster, err := t.ClusterClient.ClusterV1().ManagedClusters().Get(context.TODO(), clusterName, metav1.GetOptions{})
if err != nil {
return nil, err
}
return cluster, nil
}
func (t *Tester) ApproveCSR(clusterName string) error {
var csrs *certificatesv1.CertificateSigningRequestList
var csrClient = t.HubKubeClient.CertificatesV1().CertificateSigningRequests()
var err error
if csrs, err = csrClient.List(context.TODO(), metav1.ListOptions{
LabelSelector: fmt.Sprintf("open-cluster-management.io/cluster-name = %v", clusterName)}); err != nil {
return err
}
if len(csrs.Items) == 0 {
return fmt.Errorf("there is no csr related cluster %v", clusterName)
}
for i := range csrs.Items {
csr := &csrs.Items[i]
if csr, err = csrClient.Get(context.TODO(), csr.Name, metav1.GetOptions{}); err != nil {
return err
}
if isCSRInTerminalState(&csr.Status) {
continue
}
csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "Approved by E2E",
Message: "Approved as part of e2e",
})
_, err = csrClient.UpdateApproval(context.TODO(), csr.Name, csr, metav1.UpdateOptions{})
if err != nil {
return err
}
}
return nil
}
func isCSRInTerminalState(status *certificatesv1.CertificateSigningRequestStatus) bool {
for _, c := range status.Conditions {
if c.Type == certificatesv1.CertificateApproved {
return true
}
if c.Type == certificatesv1.CertificateDenied {
return true
}
}
return false
}
func (t *Tester) AcceptsClient(clusterName string) error {
managedCluster, err := t.ClusterClient.ClusterV1().ManagedClusters().Get(context.TODO(),
clusterName, metav1.GetOptions{})
if err != nil {
return err
}
managedCluster.Spec.HubAcceptsClient = true
managedCluster.Spec.LeaseDurationSeconds = 5
_, err = t.ClusterClient.ClusterV1().ManagedClusters().Update(context.TODO(),
managedCluster, metav1.UpdateOptions{})
return err
}
func (t *Tester) CheckManagedClusterStatus(clusterName string) error {
managedCluster, err := t.ClusterClient.ClusterV1().ManagedClusters().Get(context.TODO(),
clusterName, metav1.GetOptions{})
if err != nil {
return err
}
var okCount = 0
for _, condition := range managedCluster.Status.Conditions {
if (condition.Type == clusterv1.ManagedClusterConditionHubAccepted ||
condition.Type == clusterv1.ManagedClusterConditionJoined ||
condition.Type == clusterv1.ManagedClusterConditionAvailable) &&
condition.Status == v1beta1.ConditionTrue {
okCount++
}
}
if okCount == 3 {
return nil
}
return fmt.Errorf("cluster %s condtions are not ready: %v", clusterName, managedCluster.Status.Conditions)
}
func (t *Tester) CreateWorkOfConfigMap(name, clusterName, configMapName, configMapNamespace string) (*workapiv1.ManifestWork, error) {
manifest := workapiv1.Manifest{}
manifest.Object = util.NewConfigmap(configMapNamespace, configMapName, map[string]string{"a": "b"}, []string{})
manifestWork := &workapiv1.ManifestWork{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: workapiv1.ManifestWorkSpec{
Workload: workapiv1.ManifestsTemplate{
Manifests: []workapiv1.Manifest{
manifest,
},
},
},
}
return t.HubWorkClient.WorkV1().ManifestWorks(clusterName).
Create(context.TODO(), manifestWork, metav1.CreateOptions{})
}
func (t *Tester) checkKlusterletStatus(klusterletName, condType, reason string, status metav1.ConditionStatus) error {
klusterlet, err := t.OperatorClient.OperatorV1().Klusterlets().Get(context.TODO(), klusterletName, metav1.GetOptions{})
if err != nil {
return err
}
cond := meta.FindStatusCondition(klusterlet.Status.Conditions, condType)
if cond == nil {
return fmt.Errorf("cannot find condition type %s", condType)
}
if cond.Reason != reason {
return fmt.Errorf("condition reason is not matched, expect %s, got %s", reason, cond.Reason)
}
if cond.Status != status {
return fmt.Errorf("condition status is not matched, expect %s, got %s", status, cond.Status)
}
return nil
}
func (t *Tester) cleanManifestWorks(clusterName, workName string) error {
err := t.HubWorkClient.WorkV1().ManifestWorks(clusterName).Delete(context.Background(), workName, metav1.DeleteOptions{})
if errors.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
gomega.Eventually(func() bool {
_, err := t.HubWorkClient.WorkV1().ManifestWorks(clusterName).Get(context.Background(), workName, metav1.GetOptions{})
return errors.IsNotFound(err)
}, t.EventuallyTimeout*5, t.EventuallyInterval*5).Should(gomega.BeTrue())
return nil
}
func (t *Tester) cleanKlusterletResources(klusterletName, clusterName string) error {
if klusterletName == "" {
return fmt.Errorf("the klusterlet name should not be null")
}
// clean the klusterlets
err := t.OperatorClient.OperatorV1().Klusterlets().Delete(context.TODO(), klusterletName, metav1.DeleteOptions{})
if err != nil {
if errors.IsNotFound(err) {
return nil
}
return err
}
gomega.Eventually(func() bool {
_, err := t.OperatorClient.OperatorV1().Klusterlets().Get(context.TODO(), klusterletName, metav1.GetOptions{})
if errors.IsNotFound(err) {
klog.Infof("klusterlet %s deleted successfully", klusterletName)
return true
}
if err != nil {
klog.Infof("get klusterlet %s error: %v", klusterletName, err)
}
return false
}, t.EventuallyTimeout*5, t.EventuallyInterval*5).Should(gomega.BeTrue())
// clean the managed clusters
err = t.ClusterClient.ClusterV1().ManagedClusters().Delete(context.TODO(), clusterName, metav1.DeleteOptions{})
if err != nil {
if errors.IsNotFound(err) {
return nil
}
return err
}
gomega.Eventually(func() bool {
_, err := t.ClusterClient.ClusterV1().ManagedClusters().Get(context.TODO(), clusterName, metav1.GetOptions{})
if errors.IsNotFound(err) {
klog.Infof("managed cluster %s deleted successfully", clusterName)
return true
}
if err != nil {
klog.Infof("get managed cluster %s error: %v", klusterletName, err)
}
return false
}, t.EventuallyTimeout*5, t.EventuallyInterval*5).Should(gomega.BeTrue())
return nil
}
func (t *Tester) CheckHubReady() error {
cm, err := t.checkClusterManagerStatus()
if err != nil {
return err
}
// make sure open-cluster-management-hub namespace is created
if _, err := t.HubKubeClient.CoreV1().Namespaces().
Get(context.TODO(), t.clusterManagerNamespace, metav1.GetOptions{}); err != nil {
return err
}
// make sure hub deployments are created
hubRegistrationDeployment := fmt.Sprintf("%s-registration-controller", t.clusterManagerName)
hubRegistrationWebhookDeployment := fmt.Sprintf("%s-registration-webhook", t.clusterManagerName)
hubWorkWebhookDeployment := fmt.Sprintf("%s-work-webhook", t.clusterManagerName)
hubWorkControllerDeployment := fmt.Sprintf("%s-work-controller", t.clusterManagerName)
hubPlacementDeployment := fmt.Sprintf("%s-placement-controller", t.clusterManagerName)
addonManagerDeployment := fmt.Sprintf("%s-addon-manager-controller", t.clusterManagerName)
if _, err := t.HubKubeClient.AppsV1().Deployments(t.clusterManagerNamespace).
Get(context.TODO(), hubRegistrationDeployment, metav1.GetOptions{}); err != nil {
return err
}
gomega.Eventually(func() error {
registrationWebhookDeployment, err := t.HubKubeClient.AppsV1().Deployments(t.clusterManagerNamespace).
Get(context.TODO(), hubRegistrationWebhookDeployment, metav1.GetOptions{})
if err != nil {
return err
}
replicas := *registrationWebhookDeployment.Spec.Replicas
readyReplicas := registrationWebhookDeployment.Status.ReadyReplicas
if readyReplicas != replicas {
return fmt.Errorf("deployment %s should have %d but got %d ready replicas", hubRegistrationWebhookDeployment, replicas, readyReplicas)
}
return nil
}, t.EventuallyTimeout*5, t.EventuallyInterval*5).Should(gomega.BeNil())
gomega.Eventually(func() error {
workWebhookDeployment, err := t.HubKubeClient.AppsV1().Deployments(t.clusterManagerNamespace).
Get(context.TODO(), hubWorkWebhookDeployment, metav1.GetOptions{})
if err != nil {
return err
}
replicas := *workWebhookDeployment.Spec.Replicas
readyReplicas := workWebhookDeployment.Status.ReadyReplicas
if readyReplicas != replicas {
return fmt.Errorf("deployment %s should have %d but got %d ready replicas", hubWorkWebhookDeployment, replicas, readyReplicas)
}
return nil
}, t.EventuallyTimeout*5, t.EventuallyInterval*5).Should(gomega.BeNil())
var hubWorkControllerEnabled, addonManagerControllerEnabled bool
if cm.Spec.WorkConfiguration != nil {
hubWorkControllerEnabled = helpers.FeatureGateEnabled(cm.Spec.WorkConfiguration.FeatureGates,
ocmfeature.DefaultHubWorkFeatureGates, ocmfeature.ManifestWorkReplicaSet)
}
if cm.Spec.AddOnManagerConfiguration != nil {
addonManagerControllerEnabled = helpers.FeatureGateEnabled(cm.Spec.AddOnManagerConfiguration.FeatureGates,
ocmfeature.DefaultHubAddonManagerFeatureGates, ocmfeature.AddonManagement)
}
if hubWorkControllerEnabled {
gomega.Eventually(func() error {
workHubControllerDeployment, err := t.HubKubeClient.AppsV1().Deployments(t.clusterManagerNamespace).
Get(context.TODO(), hubWorkControllerDeployment, metav1.GetOptions{})
if err != nil {
return err
}
replicas := *workHubControllerDeployment.Spec.Replicas
readyReplicas := workHubControllerDeployment.Status.ReadyReplicas
if readyReplicas != replicas {
return fmt.Errorf("deployment %s should have %d but got %d ready replicas", hubWorkControllerDeployment, replicas, readyReplicas)
}
return nil
}, t.EventuallyTimeout*5, t.EventuallyInterval*5).Should(gomega.BeNil())
}
if _, err := t.HubKubeClient.AppsV1().Deployments(t.clusterManagerNamespace).
Get(context.TODO(), hubPlacementDeployment, metav1.GetOptions{}); err != nil {
return err
}
if addonManagerControllerEnabled {
gomega.Eventually(func() error {
addonManagerControllerDeployment, err := t.HubKubeClient.AppsV1().Deployments(t.clusterManagerNamespace).
Get(context.TODO(), addonManagerDeployment, metav1.GetOptions{})
if err != nil {
return err
}
replicas := *addonManagerControllerDeployment.Spec.Replicas
readyReplicas := addonManagerControllerDeployment.Status.ReadyReplicas
if readyReplicas != replicas {
return fmt.Errorf("deployment %s should have %d but got %d ready replicas", addonManagerDeployment, replicas, readyReplicas)
}
return nil
}, t.EventuallyTimeout*5, t.EventuallyInterval*5).Should(gomega.BeNil())
}
return nil
}
func (t *Tester) EnableWorkFeature(feature string) error {
cm, err := t.OperatorClient.OperatorV1().ClusterManagers().Get(context.TODO(), t.clusterManagerName, metav1.GetOptions{})
if err != nil {
return err
}
if cm.Spec.WorkConfiguration == nil {
cm.Spec.WorkConfiguration = &operatorapiv1.WorkConfiguration{}
}
if len(cm.Spec.WorkConfiguration.FeatureGates) == 0 {
cm.Spec.WorkConfiguration.FeatureGates = make([]operatorapiv1.FeatureGate, 0)
}
for idx, f := range cm.Spec.WorkConfiguration.FeatureGates {
if f.Feature == feature {
if f.Mode == operatorapiv1.FeatureGateModeTypeEnable {
return nil
}
cm.Spec.WorkConfiguration.FeatureGates[idx].Mode = operatorapiv1.FeatureGateModeTypeEnable
_, err = t.OperatorClient.OperatorV1().ClusterManagers().Update(context.TODO(), cm, metav1.UpdateOptions{})
return err
}
}
featureGate := operatorapiv1.FeatureGate{
Feature: feature,
Mode: operatorapiv1.FeatureGateModeTypeEnable,
}
cm.Spec.WorkConfiguration.FeatureGates = append(cm.Spec.WorkConfiguration.FeatureGates, featureGate)
_, err = t.OperatorClient.OperatorV1().ClusterManagers().Update(context.TODO(), cm, metav1.UpdateOptions{})
return err
}
func (t *Tester) RemoveWorkFeature(feature string) error {
clusterManager, err := t.OperatorClient.OperatorV1().ClusterManagers().Get(context.TODO(), t.clusterManagerName, metav1.GetOptions{})
if err != nil {
return err
}
for indx, fg := range clusterManager.Spec.WorkConfiguration.FeatureGates {
if fg.Feature == feature {
clusterManager.Spec.WorkConfiguration.FeatureGates[indx].Mode = operatorapiv1.FeatureGateModeTypeDisable
break
}
}
_, err = t.OperatorClient.OperatorV1().ClusterManagers().Update(context.TODO(), clusterManager, metav1.UpdateOptions{})
return err
}
func (t *Tester) checkClusterManagerStatus() (*operatorapiv1.ClusterManager, error) {
cm, err := t.OperatorClient.OperatorV1().ClusterManagers().Get(context.TODO(), t.clusterManagerName, metav1.GetOptions{})
if err != nil {
return nil, err
}
if meta.IsStatusConditionFalse(cm.Status.Conditions, "Applied") {
return nil, fmt.Errorf("components of cluster manager are not all applied")
}
if meta.IsStatusConditionFalse(cm.Status.Conditions, "ValidFeatureGates") {
return nil, fmt.Errorf("feature gates are not all valid")
}
if !meta.IsStatusConditionFalse(cm.Status.Conditions, "HubRegistrationDegraded") {
return nil, fmt.Errorf("HubRegistration is degraded")
}
if !meta.IsStatusConditionFalse(cm.Status.Conditions, "HubPlacementDegraded") {
return nil, fmt.Errorf("HubPlacement is degraded")
}
if !meta.IsStatusConditionFalse(cm.Status.Conditions, "Progressing") {
return nil, fmt.Errorf("ClusterManager is still progressing")
}
return cm, nil
}
func (t *Tester) CheckKlusterletOperatorReady() error {
// make sure klusterlet operator deployment is created
_, err := t.SpokeKubeClient.AppsV1().Deployments(t.operatorNamespace).
Get(context.TODO(), t.klusterletOperator, metav1.GetOptions{})
return err
}
// GetRandomClusterName gets the clusterName generated by registration randomly.
// the cluster name is the random name if it has not prefix "e2e-".
// TODO: get random cluster name from event
func (t *Tester) GetRandomClusterName() (string, error) {
managedClusterList, err := t.ClusterClient.ClusterV1().ManagedClusters().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return "", err
}
for _, managedCluster := range managedClusterList.Items {
clusterName := managedCluster.Name
if !strings.HasPrefix(clusterName, "e2e-") {
return clusterName, nil
}
}
return "", fmt.Errorf("there is no managedCluster with the random name")
}
// TODO: only output the details of created resources during e2e
func (t *Tester) OutputDebugLogs() {
klusterletes, err := t.OperatorClient.OperatorV1().Klusterlets().List(context.TODO(), metav1.ListOptions{})
if err != nil {
klog.Errorf("failed to list klusterlets. error: %v", err)
}
for _, klusterlet := range klusterletes.Items {
klog.Infof("klusterlet %v : %#v \n", klusterlet.Name, klusterlet)
}
managedClusters, err := t.ClusterClient.ClusterV1().ManagedClusters().List(context.TODO(), metav1.ListOptions{})
if err != nil {
klog.Errorf("failed to list managedClusters. error: %v", err)
}
for _, managedCluster := range managedClusters.Items {
klog.Infof("managedCluster %v : %#v \n", managedCluster.Name, managedCluster)
}
registrationPods, err := t.SpokeKubeClient.CoreV1().Pods("").List(context.Background(),
metav1.ListOptions{LabelSelector: "app=klusterlet-registration-agent"})
if err != nil {
klog.Errorf("failed to list registration pods. error: %v", err)
}
manifestWorkPods, err := t.SpokeKubeClient.CoreV1().Pods("").List(context.Background(),
metav1.ListOptions{LabelSelector: "app=klusterlet-manifestwork-agent"})
if err != nil {
klog.Errorf("failed to get manifestwork pods. error: %v", err)
}
agentPods := append(registrationPods.Items, manifestWorkPods.Items...)
for _, pod := range agentPods {
klog.Infof("klusterlet agent pod %v/%v\n", pod.Namespace, pod.Name)
logs, err := t.SpokePodLog(pod.Name, pod.Namespace, int64(10))
if err != nil {
klog.Errorf("failed to get pod %v/%v log. error: %v", pod.Namespace, pod.Name, err)
continue
}
klog.Infof("pod %v/%v logs:\n %v \n", pod.Namespace, pod.Name, logs)
}
manifestWorks, err := t.HubWorkClient.WorkV1().ManifestWorks("").List(context.TODO(), metav1.ListOptions{})
if err != nil {
klog.Errorf("failed to list manifestWorks. error: %v", err)
}
for _, manifestWork := range manifestWorks.Items {
klog.Infof("manifestWork %v/%v : %#v \n", manifestWork.Namespace, manifestWork.Name, manifestWork)
}
}
func (t *Tester) SpokePodLog(podName, nameSpace string, lines int64) (string, error) {
podLogs, err := t.SpokeKubeClient.CoreV1().Pods(nameSpace).
GetLogs(podName, &corev1.PodLogOptions{TailLines: &lines}).Stream(context.TODO())
if err != nil {
return "", err
}
defer podLogs.Close()
buf := new(bytes.Buffer)
_, err = io.Copy(buf, podLogs)
if err != nil {
return "", err
}
return buf.String(), nil
}
func (t *Tester) CreateManagedClusterAddOn(managedClusterNamespace, addOnName, installNamespace string) error {
_, err := t.AddOnClinet.AddonV1alpha1().ManagedClusterAddOns(managedClusterNamespace).Create(
context.TODO(),
&addonv1alpha1.ManagedClusterAddOn{
ObjectMeta: metav1.ObjectMeta{
Namespace: managedClusterNamespace,
Name: addOnName,
},
Spec: addonv1alpha1.ManagedClusterAddOnSpec{
InstallNamespace: installNamespace,
},
},
metav1.CreateOptions{},
)
return err
}
func (t *Tester) CreateManagedClusterAddOnLease(addOnInstallNamespace, addOnName string) error {
if _, err := t.HubKubeClient.CoreV1().Namespaces().Create(
context.TODO(),
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: addOnInstallNamespace,
},
},
metav1.CreateOptions{},
); err != nil {
return err
}
_, err := t.HubKubeClient.CoordinationV1().Leases(addOnInstallNamespace).Create(
context.TODO(),
&coordv1.Lease{
ObjectMeta: metav1.ObjectMeta{
Name: addOnName,
Namespace: addOnInstallNamespace,
},
Spec: coordv1.LeaseSpec{
RenewTime: &metav1.MicroTime{Time: time.Now()},
},
},
metav1.CreateOptions{},
)
return err
}
func (t *Tester) CheckManagedClusterAddOnStatus(managedClusterNamespace, addOnName string) error {
addOn, err := t.AddOnClinet.AddonV1alpha1().ManagedClusterAddOns(managedClusterNamespace).Get(context.TODO(), addOnName, metav1.GetOptions{})
if err != nil {
return err
}
if addOn.Status.Conditions == nil {
return fmt.Errorf("there is no conditions in addon %v/%v", managedClusterNamespace, addOnName)
}
if !meta.IsStatusConditionTrue(addOn.Status.Conditions, "Available") {
return fmt.Errorf("the addon %v/%v available condition is not true", managedClusterNamespace, addOnName)
}
return nil
}
func (t *Tester) DeleteExternalKubeconfigSecret(klusterlet *operatorapiv1.Klusterlet) error {
agentNamespace := helpers.AgentNamespace(klusterlet)
err := t.HubKubeClient.CoreV1().Secrets(agentNamespace).Delete(context.TODO(),
helpers.ExternalManagedKubeConfig, metav1.DeleteOptions{})
if err != nil {
klog.Errorf("failed to delete external managed secret in ns %v. %v", agentNamespace, err)
return err
}
return nil
}
func (t *Tester) CreateFakeExternalKubeconfigSecret(klusterlet *operatorapiv1.Klusterlet) error {
agentNamespace := helpers.AgentNamespace(klusterlet)
klog.Infof("klusterlet: %s/%s, \t, \t agent namespace: %s",
klusterlet.Name, klusterlet.Namespace, agentNamespace)
bsSecret, err := t.HubKubeClient.CoreV1().Secrets(agentNamespace).Get(context.TODO(),
t.bootstrapHubSecret.Name, metav1.GetOptions{})
if err != nil {
klog.Errorf("failed to get bootstrap secret %v in ns %v. %v", bsSecret, agentNamespace, err)
return err
}
// create external-managed-kubeconfig, will use the same cluster to simulate the Hosted mode.
secret, err := changeHostOfKubeconfigSecret(*bsSecret, "https://kube-apiserver.i-am-a-fake-server:6443")
if err != nil {
klog.Errorf("failed to change host of the kubeconfig secret in. %v", err)
return err
}
secret.Namespace = agentNamespace
secret.Name = helpers.ExternalManagedKubeConfig
secret.ResourceVersion = ""
_, err = t.HubKubeClient.CoreV1().Secrets(agentNamespace).Create(context.TODO(), secret, metav1.CreateOptions{})
if err != nil {
klog.Errorf("failed to create external managed secret %v in ns %v. %v", bsSecret, agentNamespace, err)
return err
}
return nil
}
func (t *Tester) BuildClusterClient(saNamespace, saName string, clusterPolicyRules, policyRules []rbacv1.PolicyRule) (clusterclient.Interface, error) {
var err error
sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Namespace: saNamespace,
Name: saName,
},
}
_, err = t.HubKubeClient.CoreV1().ServiceAccounts(saNamespace).Create(context.TODO(), sa, metav1.CreateOptions{})
if err != nil {
return nil, err
}
// create cluster role/rolebinding
if len(clusterPolicyRules) > 0 {
clusterRoleName := fmt.Sprintf("%s-clusterrole", saName)
clusterRole := &rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: clusterRoleName,
},
Rules: clusterPolicyRules,
}
_, err = t.HubKubeClient.RbacV1().ClusterRoles().Create(context.TODO(), clusterRole, metav1.CreateOptions{})
if err != nil {
return nil, err
}
clusterRoleBinding := &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-clusterrolebinding", saName),
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Namespace: saNamespace,
Name: saName,
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: clusterRoleName,
},
}
_, err = t.HubKubeClient.RbacV1().ClusterRoleBindings().Create(context.TODO(), clusterRoleBinding, metav1.CreateOptions{})
if err != nil {
return nil, err
}
}
// create cluster role/rolebinding
if len(policyRules) > 0 {
roleName := fmt.Sprintf("%s-role", saName)
role := &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Namespace: saNamespace,
Name: roleName,
},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{"cluster.open-cluster-management.io"},
Resources: []string{"managedclustersetbindings"},
Verbs: []string{"create", "get", "update"},
},
},
}
_, err = t.HubKubeClient.RbacV1().Roles(saNamespace).Create(context.TODO(), role, metav1.CreateOptions{})
if err != nil {
return nil, err
}
roleBinding := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Namespace: saNamespace,
Name: fmt.Sprintf("%s-rolebinding", saName),
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Namespace: saNamespace,
Name: saName,
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: roleName,
},
}
_, err = t.HubKubeClient.RbacV1().RoleBindings(saNamespace).Create(context.TODO(), roleBinding, metav1.CreateOptions{})
if err != nil {
return nil, err
}
}
tokenRequest, err := t.HubKubeClient.CoreV1().ServiceAccounts(saNamespace).CreateToken(
context.TODO(),
saName,
&authv1.TokenRequest{
Spec: authv1.TokenRequestSpec{
ExpirationSeconds: pointer.Int64(8640 * 3600),
},
},
metav1.CreateOptions{},
)
if err != nil {
return nil, err
}
unauthorizedClusterClient, err := clusterclient.NewForConfig(&rest.Config{
Host: t.HubClusterCfg.Host,
TLSClientConfig: rest.TLSClientConfig{
CAData: t.HubClusterCfg.CAData,
},
BearerToken: tokenRequest.Status.Token,
})
return unauthorizedClusterClient, err
}
// cleanupClusterClient delete cluster-scope resource created by func "buildClusterClient",
// the namespace-scope resources should be deleted by an additional namespace deleting func.
// It is recommended be invoked as a pair with the func "buildClusterClient"
func (t *Tester) CleanupClusterClient(saNamespace, saName string) error {
err := t.HubKubeClient.CoreV1().ServiceAccounts(saNamespace).Delete(context.TODO(), saName, metav1.DeleteOptions{})
if err != nil {
return fmt.Errorf("delete sa %q/%q failed: %v", saNamespace, saName, err)
}
// delete cluster role and cluster role binding if exists
clusterRoleName := fmt.Sprintf("%s-clusterrole", saName)
err = t.HubKubeClient.RbacV1().ClusterRoles().Delete(context.TODO(), clusterRoleName, metav1.DeleteOptions{})
if err != nil && !errors.IsNotFound(err) {
return fmt.Errorf("delete cluster role %q failed: %v", clusterRoleName, err)
}
clusterRoleBindingName := fmt.Sprintf("%s-clusterrolebinding", saName)
err = t.HubKubeClient.RbacV1().ClusterRoleBindings().Delete(context.TODO(), clusterRoleBindingName, metav1.DeleteOptions{})
if err != nil && !errors.IsNotFound(err) {
return fmt.Errorf("delete cluster role binding %q failed: %v", clusterRoleBindingName, err)
}
return nil
}
func (t *Tester) DeleteManageClusterAndRelatedNamespace(clusterName string) error {
if err := wait.Poll(1*time.Second, 90*time.Second, func() (bool, error) {
err := t.ClusterClient.ClusterV1().ManagedClusters().Delete(context.TODO(), clusterName, metav1.DeleteOptions{})
if err != nil && !errors.IsNotFound(err) {
return false, err
}
return true, nil
}); err != nil {
return fmt.Errorf("delete managed cluster %q failed: %v", clusterName, err)
}
// delete namespace created by hub automatically
if err := wait.Poll(1*time.Second, 5*time.Second, func() (bool, error) {
err := t.HubKubeClient.CoreV1().Namespaces().Delete(context.TODO(), clusterName, metav1.DeleteOptions{})
// some managed cluster just created, but the csr is not approved,
// so there is not a related namespace
if err != nil && !errors.IsNotFound(err) {
return false, err
}
return true, nil
}); err != nil {
return fmt.Errorf("delete related namespace %q failed: %v", clusterName, err)
}
return nil
}
func changeHostOfKubeconfigSecret(secret corev1.Secret, apiServerURL string) (*corev1.Secret, error) {
kubeconfigData, ok := secret.Data["kubeconfig"]
if !ok {
return nil, fmt.Errorf("kubeconfig not found")
}
if kubeconfigData == nil {
return nil, fmt.Errorf("failed to get kubeconfig from secret: %s", secret.GetName())
}
kubeconfig, err := clientcmd.Load(kubeconfigData)
if err != nil {
return nil, fmt.Errorf("failed to load kubeconfig from secret: %s", secret.GetName())
}
if len(kubeconfig.Clusters) == 0 {
return nil, fmt.Errorf("there is no cluster in kubeconfig from secret: %s", secret.GetName())
}
for k := range kubeconfig.Clusters {
kubeconfig.Clusters[k].Server = apiServerURL
}
newKubeconfig, err := clientcmd.Write(*kubeconfig)
if err != nil {
return nil, fmt.Errorf("failed to write new kubeconfig to secret: %s", secret.GetName())
}
secret.Data = map[string][]byte{
"kubeconfig": newKubeconfig,
}
klog.Infof("Set the cluster server URL in %s secret with apiServerURL %s", secret.Name, apiServerURL)
return &secret, nil
}
func createResourcesFromYamlFiles(
ctx context.Context,
dynamicClient dynamic.Interface,
restMapper meta.RESTMapper,
scheme *runtime.Scheme,
manifests func(name string) ([]byte, error),
resourceFiles []string) error {
var appliedErrs []error
decoder := serializer.NewCodecFactory(scheme).UniversalDeserializer()
for _, fileName := range resourceFiles {
objData, err := manifests(fileName)
if err != nil {
return err
}
required := unstructured.Unstructured{}
_, gvk, err := decoder.Decode(objData, nil, &required)
if err != nil {
return err
}
mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return err
}
_, err = dynamicClient.Resource(mapping.Resource).Namespace(required.GetNamespace()).Create(
ctx, &required, metav1.CreateOptions{})
if errors.IsAlreadyExists(err) {
continue
}
if err != nil {
fmt.Printf("Error creating %q (%T): %v\n", fileName, mapping.Resource, err)
appliedErrs = append(appliedErrs, fmt.Errorf("%q (%T): %v", fileName, mapping.Resource, err))
}
}
return utilerrors.NewAggregate(appliedErrs)
}
func deleteResourcesFromYamlFiles(
ctx context.Context,
dynamicClient dynamic.Interface,
restMapper meta.RESTMapper,
scheme *runtime.Scheme,
manifests func(name string) ([]byte, error),
resourceFiles []string) error {
var appliedErrs []error
decoder := serializer.NewCodecFactory(scheme).UniversalDeserializer()
for _, fileName := range resourceFiles {
objData, err := manifests(fileName)
if err != nil {
return err
}
required := unstructured.Unstructured{}
_, gvk, err := decoder.Decode(objData, nil, &required)
if err != nil {
return err
}
mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return err
}
err = dynamicClient.Resource(mapping.Resource).Namespace(required.GetNamespace()).Delete(
ctx, required.GetName(), metav1.DeleteOptions{})
if errors.IsNotFound(err) {
continue
}
if err != nil {
fmt.Printf("Error deleting %q (%T): %v\n", fileName, mapping.Resource, err)
appliedErrs = append(appliedErrs, fmt.Errorf("%q (%T): %v", fileName, mapping.Resource, err))
}
}
return utilerrors.NewAggregate(appliedErrs)
}
// defaultAddonTemplateReaderManifestsFunc returns a function that reads the addon template from the embed.FS,
// and replaces the placeholder in format of "<< placeholder >>" with the value in configValues.
func defaultAddonTemplateReaderManifestsFunc(
fs embed.FS,
configValues map[string]interface{},
) func(string) ([]byte, error) {
return func(fileName string) ([]byte, error) {
template, err := fs.ReadFile(fileName)
if err != nil {
return nil, err
}
t := fasttemplate.New(string(template), "<< ", " >>")
objData := t.ExecuteString(configValues)
return []byte(objData), nil
}
}