github.com/redhat-appstudio/e2e-tests@v0.0.0-20240520140907-9709f6f59323/magefiles/installation/install.go (about)

     1  package installation
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"os"
     9  	"time"
    10  
    11  	"strings"
    12  
    13  	appsv1 "k8s.io/api/apps/v1"
    14  
    15  	"github.com/devfile/library/v2/pkg/util"
    16  	"github.com/go-git/go-git/v5"
    17  	"github.com/go-git/go-git/v5/config"
    18  	"github.com/go-git/go-git/v5/plumbing"
    19  	configv1client "github.com/openshift/client-go/config/clientset/versioned"
    20  
    21  	appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
    22  	kubeCl "github.com/redhat-appstudio/e2e-tests/pkg/clients/kubernetes"
    23  	"github.com/redhat-appstudio/e2e-tests/pkg/constants"
    24  	"github.com/redhat-appstudio/e2e-tests/pkg/utils"
    25  	corev1 "k8s.io/api/core/v1"
    26  	k8sErrors "k8s.io/apimachinery/pkg/api/errors"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/types"
    29  
    30  	"k8s.io/client-go/tools/clientcmd"
    31  	"k8s.io/klog/v2"
    32  
    33  	sigsConfig "sigs.k8s.io/controller-runtime/pkg/client/config"
    34  )
    35  
    36  const (
    37  	DEFAULT_TMP_DIR                  = "tmp"
    38  	DEFAULT_INFRA_DEPLOYMENTS_BRANCH = "main"
    39  	DEFAULT_INFRA_DEPLOYMENTS_GH_ORG = "redhat-appstudio"
    40  	DEFAULT_LOCAL_FORK_NAME          = "qe"
    41  	DEFAULT_LOCAL_FORK_ORGANIZATION  = "redhat-appstudio-qe"
    42  	DEFAULT_E2E_QUAY_ORG             = "redhat-appstudio-qe"
    43  
    44  	enableSchedulingOnMasterNodes = "true"
    45  )
    46  
    47  var (
    48  	previewInstallArgs = []string{"preview", "--keycloak", "--toolchain"}
    49  )
    50  
    51  type patchStringValue struct {
    52  	Op    string `json:"op"`
    53  	Path  string `json:"path"`
    54  	Value string `json:"value"`
    55  }
    56  
    57  type InstallAppStudio struct {
    58  	// Kubernetes Client to interact with Openshift Cluster
    59  	KubernetesClient *kubeCl.CustomClient
    60  
    61  	// TmpDirectory to store temporary files like git repos or some metadata
    62  	TmpDirectory string
    63  
    64  	// Directory where to clone https://github.com/redhat-appstudio/infra-deployments repo
    65  	InfraDeploymentsCloneDir string
    66  
    67  	// Branch to clone from https://github.com/redhat-appstudio/infra-deployments. By default will be main
    68  	InfraDeploymentsBranch string
    69  
    70  	// Github organization from where will be cloned
    71  	InfraDeploymentsOrganizationName string
    72  
    73  	// Desired fork name for testing
    74  	LocalForkName string
    75  
    76  	// Github organization to use for testing purposes in preview mode
    77  	LocalGithubForkOrganization string
    78  
    79  	// Namespace where build applications will be placed
    80  	E2EApplicationsNamespace string
    81  
    82  	// base64-encoded content of a docker/config.json file which contains a valid login credentials for quay.io
    83  	QuayToken string
    84  
    85  	// Default quay organization for repositories generated by Image-controller
    86  	DefaultImageQuayOrg string
    87  
    88  	// Oauth2 token for default quay organization
    89  	DefaultImageQuayOrgOAuth2Token string
    90  
    91  	// Default expiration for image tags
    92  	DefaultImageTagExpiration string
    93  
    94  	// If set to "true", e2e-tests installer will mark master/control plane nodes as schedulable
    95  	EnableSchedulingOnMasterNodes string
    96  }
    97  
    98  func NewAppStudioInstallController() (*InstallAppStudio, error) {
    99  	cwd, _ := os.Getwd()
   100  	k8sClient, err := kubeCl.NewAdminKubernetesClient()
   101  
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	return &InstallAppStudio{
   107  		KubernetesClient:                 k8sClient,
   108  		TmpDirectory:                     DEFAULT_TMP_DIR,
   109  		InfraDeploymentsCloneDir:         fmt.Sprintf("%s/%s/infra-deployments", cwd, DEFAULT_TMP_DIR),
   110  		InfraDeploymentsBranch:           utils.GetEnv("INFRA_DEPLOYMENTS_BRANCH", DEFAULT_INFRA_DEPLOYMENTS_BRANCH),
   111  		InfraDeploymentsOrganizationName: utils.GetEnv("INFRA_DEPLOYMENTS_ORG", DEFAULT_INFRA_DEPLOYMENTS_GH_ORG),
   112  		LocalForkName:                    DEFAULT_LOCAL_FORK_NAME,
   113  		LocalGithubForkOrganization:      utils.GetEnv("MY_GITHUB_ORG", DEFAULT_LOCAL_FORK_ORGANIZATION),
   114  		QuayToken:                        utils.GetEnv("QUAY_TOKEN", ""),
   115  		DefaultImageQuayOrg:              utils.GetEnv("DEFAULT_QUAY_ORG", DEFAULT_E2E_QUAY_ORG),
   116  		DefaultImageQuayOrgOAuth2Token:   utils.GetEnv("DEFAULT_QUAY_ORG_TOKEN", ""),
   117  		DefaultImageTagExpiration:        utils.GetEnv(constants.IMAGE_TAG_EXPIRATION_ENV, constants.DefaultImageTagExpiration),
   118  		EnableSchedulingOnMasterNodes:    utils.GetEnv(constants.ENABLE_SCHEDULING_ON_MASTER_NODES_ENV, enableSchedulingOnMasterNodes),
   119  	}, nil
   120  }
   121  
   122  func NewAppStudioInstallControllerUpgrade(infraFork string, infraBranch string) (*InstallAppStudio, error) {
   123  	cwd, _ := os.Getwd()
   124  	k8sClient, err := kubeCl.NewAdminKubernetesClient()
   125  
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  
   130  	return &InstallAppStudio{
   131  		KubernetesClient:                 k8sClient,
   132  		TmpDirectory:                     DEFAULT_TMP_DIR,
   133  		InfraDeploymentsCloneDir:         fmt.Sprintf("%s/%s/infra-deployments", cwd, DEFAULT_TMP_DIR),
   134  		InfraDeploymentsBranch:           infraBranch,
   135  		InfraDeploymentsOrganizationName: infraFork,
   136  		LocalForkName:                    DEFAULT_LOCAL_FORK_NAME,
   137  		LocalGithubForkOrganization:      utils.GetEnv("MY_GITHUB_ORG", DEFAULT_LOCAL_FORK_ORGANIZATION),
   138  		QuayToken:                        utils.GetEnv("QUAY_TOKEN", ""),
   139  		DefaultImageQuayOrg:              utils.GetEnv("DEFAULT_QUAY_ORG", ""),
   140  		DefaultImageQuayOrgOAuth2Token:   utils.GetEnv("DEFAULT_QUAY_ORG_TOKEN", ""),
   141  		DefaultImageTagExpiration:        utils.GetEnv(constants.IMAGE_TAG_EXPIRATION_ENV, constants.DefaultImageTagExpiration),
   142  		EnableSchedulingOnMasterNodes:    utils.GetEnv(constants.ENABLE_SCHEDULING_ON_MASTER_NODES_ENV, enableSchedulingOnMasterNodes),
   143  	}, nil
   144  }
   145  
   146  // Start the appstudio installation in preview mode.
   147  func (i *InstallAppStudio) InstallAppStudioPreviewMode() error {
   148  	if err := i.cloneInfraDeployments(); err != nil {
   149  		return fmt.Errorf("failed to clone infra-deployments repository: %+v", err)
   150  	}
   151  	i.setInstallationEnvironments()
   152  
   153  	if i.EnableSchedulingOnMasterNodes == "true" {
   154  		if err := i.MarkMasterNodesAsSchedulable(); err != nil {
   155  			return err
   156  		}
   157  	}
   158  
   159  	if err := utils.ExecuteCommandInASpecificDirectory("hack/bootstrap-cluster.sh", previewInstallArgs, i.InfraDeploymentsCloneDir); err != nil {
   160  		return err
   161  	}
   162  
   163  	i.addSPIOauthRedirectProxyUrl()
   164  
   165  	return i.createE2EQuaySecret()
   166  }
   167  
   168  // MarkMasterNodesAsSchedulable uses configv1client for updating scheduler/cluster with "spec.mastersSchedulable:true"
   169  func (i *InstallAppStudio) MarkMasterNodesAsSchedulable() error {
   170  	klog.Infof("Configuring master/control plane nodes as schedulable")
   171  
   172  	kubeconfig, err := sigsConfig.GetConfig()
   173  	if err != nil {
   174  		return fmt.Errorf("error when getting config: %+v", err)
   175  	}
   176  
   177  	configv1client, err := configv1client.NewForConfig(kubeconfig)
   178  	if err != nil {
   179  		return fmt.Errorf("error when creating configv1client: %+v", err)
   180  	}
   181  	_, err = configv1client.ConfigV1().Schedulers().Patch(context.Background(), "cluster", types.MergePatchType, []byte("{\"spec\":{\"mastersSchedulable\":true}}"), metav1.PatchOptions{})
   182  	if err != nil {
   183  		return fmt.Errorf("failed to mark master nodes as schedulable: %+v", err)
   184  	}
   185  	return nil
   186  }
   187  
   188  func (i *InstallAppStudio) setInstallationEnvironments() {
   189  	os.Setenv("MY_GITHUB_ORG", i.LocalGithubForkOrganization)
   190  	os.Setenv("MY_GITHUB_TOKEN", utils.GetEnv("GITHUB_TOKEN", ""))
   191  	os.Setenv("MY_GIT_FORK_REMOTE", i.LocalForkName)
   192  	os.Setenv("TEST_BRANCH_ID", util.GenerateRandomString(4))
   193  	os.Setenv("QUAY_TOKEN", i.QuayToken)
   194  	os.Setenv("IMAGE_CONTROLLER_QUAY_ORG", i.DefaultImageQuayOrg)
   195  	os.Setenv("IMAGE_CONTROLLER_QUAY_TOKEN", i.DefaultImageQuayOrgOAuth2Token)
   196  	os.Setenv("BUILD_SERVICE_IMAGE_TAG_EXPIRATION", i.DefaultImageTagExpiration)
   197  	os.Setenv("PAC_GITHUB_APP_ID", utils.GetEnv("E2E_PAC_GITHUB_APP_ID", ""))                   // #nosec G104
   198  	os.Setenv("PAC_GITHUB_APP_PRIVATE_KEY", utils.GetEnv("E2E_PAC_GITHUB_APP_PRIVATE_KEY", "")) // #nosec G104
   199  	os.Setenv(constants.ENABLE_SCHEDULING_ON_MASTER_NODES_ENV, i.EnableSchedulingOnMasterNodes)
   200  }
   201  
   202  func (i *InstallAppStudio) cloneInfraDeployments() error {
   203  	dirInfo, err := os.Stat(i.InfraDeploymentsCloneDir)
   204  
   205  	if !os.IsNotExist(err) && dirInfo.IsDir() {
   206  		klog.Warningf("folder %s already exists... removing", i.InfraDeploymentsCloneDir)
   207  
   208  		err := os.RemoveAll(i.InfraDeploymentsCloneDir)
   209  		if err != nil {
   210  			return fmt.Errorf("error removing %s folder", i.InfraDeploymentsCloneDir)
   211  		}
   212  	}
   213  
   214  	url := fmt.Sprintf("https://github.com/%s/infra-deployments", i.InfraDeploymentsOrganizationName)
   215  	refName := fmt.Sprintf("refs/heads/%s", i.InfraDeploymentsBranch)
   216  	klog.Infof("cloning '%s' with git ref '%s'", url, refName)
   217  	repo, _ := git.PlainClone(i.InfraDeploymentsCloneDir, false, &git.CloneOptions{
   218  		URL:           url,
   219  		ReferenceName: plumbing.ReferenceName(refName),
   220  		Progress:      os.Stdout,
   221  	})
   222  
   223  	if _, err := repo.CreateRemote(&config.RemoteConfig{Name: "upstream", URLs: []string{"https://github.com/redhat-appstudio/infra-deployments.git"}}); err != nil {
   224  		return err
   225  	}
   226  	if _, err := repo.CreateRemote(&config.RemoteConfig{Name: i.LocalForkName, URLs: []string{fmt.Sprintf("https://github.com/%s/infra-deployments.git", i.LocalGithubForkOrganization)}}); err != nil {
   227  		return err
   228  	}
   229  	if err := utils.ExecuteCommandInASpecificDirectory("git", []string{"pull", "--rebase", "upstream", "main"}, i.InfraDeploymentsCloneDir); err != nil {
   230  		return err
   231  	}
   232  
   233  	return nil
   234  }
   235  
   236  func (i *InstallAppStudio) CheckOperatorsReady() (err error) {
   237  	apiConfig, err := clientcmd.NewDefaultClientConfigLoadingRules().Load()
   238  	if err != nil {
   239  		klog.Fatal(err)
   240  	}
   241  	config, err := clientcmd.NewDefaultClientConfig(*apiConfig, &clientcmd.ConfigOverrides{}).ClientConfig()
   242  	if err != nil {
   243  		klog.Fatal(err)
   244  	}
   245  	appClientset := appclientset.NewForConfigOrDie(config)
   246  
   247  	patchPayload := []patchStringValue{{
   248  		Op:    "replace",
   249  		Path:  "/metadata/annotations/argocd.argoproj.io~1refresh",
   250  		Value: "hard",
   251  	}}
   252  	patchPayloadBytes, err := json.Marshal(patchPayload)
   253  	if err != nil {
   254  		klog.Fatal(err)
   255  	}
   256  	_, err = appClientset.ArgoprojV1alpha1().Applications("openshift-gitops").Patch(context.Background(), "all-application-sets", types.JSONPatchType, patchPayloadBytes, metav1.PatchOptions{})
   257  	if err != nil {
   258  		klog.Fatal(err)
   259  	}
   260  
   261  	for {
   262  		var count = 0
   263  		appsListFor, err := appClientset.ArgoprojV1alpha1().Applications("openshift-gitops").List(context.Background(), metav1.ListOptions{})
   264  		for _, app := range appsListFor.Items {
   265  			fmt.Printf("Check application: %s\n", app.Name)
   266  			application, err := appClientset.ArgoprojV1alpha1().Applications("openshift-gitops").Get(context.Background(), app.Name, metav1.GetOptions{})
   267  			if err != nil {
   268  				klog.Fatal(err)
   269  			}
   270  
   271  			if !(application.Status.Sync.Status == "Synced" && application.Status.Health.Status == "Healthy") {
   272  				klog.Infof("Application %s not ready", app.Name)
   273  				count++
   274  			} else if strings.Contains(application.String(), ("context deadline exceeded")) {
   275  				fmt.Printf("Refreshing Application %s\n", app.Name)
   276  				patchPayload := []patchStringValue{{
   277  					Op:    "replace",
   278  					Path:  "/metadata/annotations/argocd.argoproj.io~1refresh",
   279  					Value: "soft",
   280  				}}
   281  
   282  				patchPayloadBytes, err := json.Marshal(patchPayload)
   283  				if err != nil {
   284  					klog.Fatal(err)
   285  				}
   286  				for _, app := range appsListFor.Items {
   287  					_, err = i.KubernetesClient.KubeInterface().AppsV1().Deployments("openshift-gitops").Patch(context.Background(), app.Name, types.JSONPatchType, patchPayloadBytes, metav1.PatchOptions{})
   288  					if err != nil {
   289  						klog.Fatal(err)
   290  					}
   291  				}
   292  			}
   293  		}
   294  		if err != nil {
   295  			klog.Fatal(err)
   296  		}
   297  
   298  		if count == 0 {
   299  			klog.Info("All Application are ready\n")
   300  			break
   301  		}
   302  		time.Sleep(10 * time.Second)
   303  	}
   304  	return err
   305  }
   306  
   307  // Create secret in e2e-secrets which can be copied to testing namespaces
   308  func (i *InstallAppStudio) createE2EQuaySecret() error {
   309  	quayToken := os.Getenv("QUAY_TOKEN")
   310  	if quayToken == "" {
   311  		return fmt.Errorf("failed to obtain quay token from 'QUAY_TOKEN' env; make sure the env exists")
   312  	}
   313  
   314  	decodedToken, err := base64.StdEncoding.DecodeString(quayToken)
   315  	if err != nil {
   316  		return fmt.Errorf("failed to decode quay token. Make sure that QUAY_TOKEN env contain a base64 token")
   317  	}
   318  
   319  	namespace := constants.QuayRepositorySecretNamespace
   320  	_, err = i.KubernetesClient.KubeInterface().CoreV1().Namespaces().Get(context.Background(), namespace, metav1.GetOptions{})
   321  	if err != nil {
   322  		if k8sErrors.IsNotFound(err) {
   323  			_, err := i.KubernetesClient.KubeInterface().CoreV1().Namespaces().Create(context.Background(), &corev1.Namespace{
   324  				ObjectMeta: metav1.ObjectMeta{
   325  					Name: namespace,
   326  				},
   327  			}, metav1.CreateOptions{})
   328  			if err != nil {
   329  				return fmt.Errorf("error when creating namespace %s : %v", namespace, err)
   330  			}
   331  		} else {
   332  			return fmt.Errorf("error when getting namespace %s : %v", namespace, err)
   333  		}
   334  	}
   335  
   336  	secretName := constants.QuayRepositorySecretName
   337  	secret, err := i.KubernetesClient.KubeInterface().CoreV1().Secrets(namespace).Get(context.Background(), secretName, metav1.GetOptions{})
   338  
   339  	if err != nil {
   340  		if k8sErrors.IsNotFound(err) {
   341  			_, err := i.KubernetesClient.KubeInterface().CoreV1().Secrets(namespace).Create(context.Background(), &corev1.Secret{
   342  				ObjectMeta: metav1.ObjectMeta{
   343  					Name:      secretName,
   344  					Namespace: namespace,
   345  				},
   346  				Type: corev1.SecretTypeDockerConfigJson,
   347  				Data: map[string][]byte{
   348  					corev1.DockerConfigJsonKey: decodedToken,
   349  				},
   350  			}, metav1.CreateOptions{})
   351  
   352  			if err != nil {
   353  				return fmt.Errorf("error when creating secret %s : %v", secretName, err)
   354  			}
   355  		} else {
   356  			secret.Data = map[string][]byte{
   357  				corev1.DockerConfigJsonKey: decodedToken,
   358  			}
   359  			_, err = i.KubernetesClient.KubeInterface().CoreV1().Secrets(namespace).Update(context.Background(), secret, metav1.UpdateOptions{})
   360  			if err != nil {
   361  				return fmt.Errorf("error when updating secret '%s' namespace: %v", secretName, err)
   362  			}
   363  		}
   364  	}
   365  
   366  	return nil
   367  }
   368  
   369  // Update spi-oauth-service-environment-config to add OAUTH_REDIRECT_PROXY_URL property for oauth tests
   370  func (i *InstallAppStudio) addSPIOauthRedirectProxyUrl() {
   371  	OauthRedirectProxyUrl := os.Getenv("OAUTH_REDIRECT_PROXY_URL")
   372  	if OauthRedirectProxyUrl == "" {
   373  		klog.Error("OAUTH_REDIRECT_PROXY_URL not set: not updating spi configuration")
   374  		return
   375  	}
   376  
   377  	namespace := "spi-system"
   378  	configMapName := "spi-oauth-service-environment-config"
   379  	deploymentName := "spi-oauth-service"
   380  
   381  	patchData := []byte(fmt.Sprintf(`{"data": {"OAUTH_REDIRECT_PROXY_URL": "%s"}}`, OauthRedirectProxyUrl))
   382  	_, err := i.KubernetesClient.KubeInterface().CoreV1().ConfigMaps(namespace).Patch(context.Background(), configMapName, types.MergePatchType, patchData, metav1.PatchOptions{})
   383  	if err != nil {
   384  		klog.Error(err)
   385  		return
   386  	}
   387  
   388  	namespacedName := types.NamespacedName{
   389  		Name:      deploymentName,
   390  		Namespace: namespace,
   391  	}
   392  
   393  	deployment := &appsv1.Deployment{}
   394  	err = i.KubernetesClient.KubeRest().Get(context.Background(), namespacedName, deployment)
   395  	if err != nil {
   396  		klog.Error(err)
   397  		return
   398  	}
   399  
   400  	newDeployment := deployment.DeepCopy()
   401  	ann := newDeployment.ObjectMeta.Annotations
   402  	if ann == nil {
   403  		ann = make(map[string]string)
   404  	}
   405  	ann["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339)
   406  	var replicas int32 = 0
   407  	newDeployment.Spec.Replicas = &replicas
   408  	newDeployment.SetAnnotations(ann)
   409  
   410  	_, err = i.KubernetesClient.KubeInterface().AppsV1().Deployments(namespace).Update(context.Background(), newDeployment, metav1.UpdateOptions{})
   411  	if err != nil {
   412  		klog.Error(err)
   413  		return
   414  	}
   415  
   416  }