github.com/redhat-appstudio/e2e-tests@v0.0.0-20230619105049-9a422b2094d7/pkg/sandbox/sandbox.go (about)

     1  package sandbox
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"errors"
     7  	"fmt"
     8  	"net/http"
     9  	"os"
    10  	"strings"
    11  	"time"
    12  
    13  	toolchainApi "github.com/codeready-toolchain/api/api/v1alpha1"
    14  	"github.com/codeready-toolchain/toolchain-e2e/testsupport/md5"
    15  	. "github.com/onsi/ginkgo/v2"
    16  	routev1 "github.com/openshift/api/route/v1"
    17  	"github.com/redhat-appstudio/e2e-tests/pkg/constants"
    18  	"github.com/redhat-appstudio/e2e-tests/pkg/utils"
    19  	corev1 "k8s.io/api/core/v1"
    20  	k8sErrors "k8s.io/apimachinery/pkg/api/errors"
    21  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    22  	"k8s.io/apimachinery/pkg/types"
    23  	"k8s.io/client-go/kubernetes"
    24  	"k8s.io/client-go/tools/clientcmd"
    25  	"k8s.io/client-go/tools/clientcmd/api"
    26  	crclient "sigs.k8s.io/controller-runtime/pkg/client"
    27  )
    28  
    29  const (
    30  	DEFAULT_KEYCLOAK_MASTER_REALM = "master"
    31  
    32  	DEFAULT_KEYCLOAK_ADMIN_CLIENT_ID = "admin-cli"
    33  
    34  	DEFAULT_KEYCLOAK_ADMIN_USERNAME = "admin"
    35  
    36  	DEFAULT_KEYCLOAK_ADMIN_SECRET = "credential-dev-sso"
    37  
    38  	SECRET_KEY = "ADMIN_PASSWORD"
    39  
    40  	DEFAULT_TOOLCHAIN_INSTANCE_NAME = "api"
    41  
    42  	DEFAULT_TOOLCHAIN_NAMESPACE = "toolchain-host-operator"
    43  
    44  	DEFAULT_KEYCLOAK_TESTING_REALM = "redhat-external"
    45  
    46  	DEFAULT_KEYCLOAK_TEST_CLIENT_ID = "cloud-services"
    47  )
    48  
    49  type SandboxController struct {
    50  	// A Client is an HTTP client. Its zero value (DefaultClient) is a
    51  	// usable client that uses DefaultTransport.
    52  	HttpClient *http.Client
    53  
    54  	// A valid keycloak url where to point all API REST calls
    55  	KeycloakUrl string
    56  
    57  	// Wrapper of valid kubernetes with admin access to the cluster
    58  	KubeClient kubernetes.Interface
    59  
    60  	// Wrapper of valid kubernetes with admin access to the cluster
    61  	KubeRest crclient.Client
    62  }
    63  
    64  // Return specs to authenticate with toolchain proxy
    65  type SandboxUserAuthInfo struct {
    66  	// Add a description about user
    67  	UserName string
    68  
    69  	// Returns the username namespace provisioned by toolchain
    70  	UserNamespace string
    71  
    72  	// Add a description about kubeconfigpath
    73  	KubeconfigPath string
    74  
    75  	// Url of user api to access kubernetes host
    76  	ProxyUrl string
    77  
    78  	// User token used as bearer to authenticate against kubernetes host
    79  	UserToken string
    80  }
    81  
    82  // Values to create a valid user for testing purposes
    83  type KeycloakUser struct {
    84  	FirstName   string                    `json:"firstName"`
    85  	LastName    string                    `json:"lastName"`
    86  	Username    string                    `json:"username"`
    87  	Enabled     string                    `json:"enabled"`
    88  	Email       string                    `json:"email"`
    89  	Credentials []KeycloakUserCredentials `json:"credentials"`
    90  }
    91  
    92  type KeycloakUserCredentials struct {
    93  	Type      string `json:"type"`
    94  	Value     string `json:"value"`
    95  	Temporary string `json:"temporary"`
    96  }
    97  
    98  type HttpClient struct {
    99  	*http.Client
   100  }
   101  
   102  // NewHttpClient creates http client wrapper with helper functions for rest models call
   103  func NewHttpClient() (*http.Client, error) {
   104  	client := &http.Client{Transport: LoggingRoundTripper{&http.Transport{
   105  		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
   106  	},
   107  	},
   108  	}
   109  	return client, nil
   110  }
   111  
   112  // NewKeyCloakApiController creates http client wrapper with helper functions for keycloak calls
   113  func NewDevSandboxController(kube kubernetes.Interface, kubeRest crclient.Client) (*SandboxController, error) {
   114  	newHttpClient, err := NewHttpClient()
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	return &SandboxController{
   120  		HttpClient: newHttpClient,
   121  		KubeClient: kube,
   122  		KubeRest:   kubeRest,
   123  	}, nil
   124  }
   125  
   126  // This type implements the http.RoundTripper interface
   127  type LoggingRoundTripper struct {
   128  	Proxied http.RoundTripper
   129  }
   130  
   131  func (lrt LoggingRoundTripper) RoundTrip(req *http.Request) (res *http.Response, e error) {
   132  	// Do "before sending requests" actions here.
   133  	GinkgoWriter.Printf("Sandbox proxy sending request to %v:%v %v\n", req.URL, req.Header, req.Body)
   134  
   135  	// Send the request, get the response (or the error)
   136  	res, e = lrt.Proxied.RoundTrip(req)
   137  
   138  	// Handle the result.
   139  	if e != nil {
   140  		GinkgoWriter.Printf("Sandbox proxy error: %v", e)
   141  	} else {
   142  		GinkgoWriter.Printf("Sandbox proxy received %v response\n", res.Status)
   143  	}
   144  
   145  	return res, e
   146  }
   147  
   148  // ReconcileUserCreation create a user in sandbox and return a valid kubeconfig for user to be used for the tests
   149  func (s *SandboxController) ReconcileUserCreation(userName string) (*SandboxUserAuthInfo, error) {
   150  	var compliantUsername string
   151  	wd, err := os.Getwd()
   152  	if err != nil {
   153  		return nil, err
   154  	}
   155  	kubeconfigPath := utils.GetEnv(constants.USER_KUBE_CONFIG_PATH_ENV, fmt.Sprintf("%s/tmp/%s.kubeconfig", wd, userName))
   156  
   157  	toolchainApiUrl, err := s.GetOpenshiftRouteHost(DEFAULT_TOOLCHAIN_NAMESPACE, DEFAULT_TOOLCHAIN_INSTANCE_NAME)
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	if s.KeycloakUrl, err = s.GetOpenshiftRouteHost(DEFAULT_KEYCLOAK_NAMESPACE, DEFAULT_KEYCLOAK_INSTANCE_NAME); err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	if err := s.IsKeycloakRunning(); err != nil {
   167  		return nil, err
   168  	}
   169  
   170  	adminSecret, err := s.GetKeycloakAdminSecret()
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	adminToken, err := s.GetKeycloakToken(DEFAULT_KEYCLOAK_ADMIN_CLIENT_ID, DEFAULT_KEYCLOAK_ADMIN_USERNAME, adminSecret, DEFAULT_KEYCLOAK_MASTER_REALM)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	if compliantUsername, err = s.RegisterSandboxUser(userName); err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	if !s.KeycloakUserExists(DEFAULT_KEYCLOAK_TESTING_REALM, adminToken.AccessToken, userName) {
   185  		registerUser, err := s.RegisterKeycloakUser(userName, adminToken.AccessToken, DEFAULT_KEYCLOAK_TESTING_REALM)
   186  		if err != nil && registerUser.Username == "" {
   187  			return nil, errors.New("failed to register user in keycloak: " + err.Error())
   188  		}
   189  	}
   190  
   191  	userToken, err := s.GetKeycloakToken(DEFAULT_KEYCLOAK_TEST_CLIENT_ID, userName, userName, DEFAULT_KEYCLOAK_TESTING_REALM)
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  
   196  	return s.GetKubeconfigPathForSpecificUser(toolchainApiUrl, compliantUsername, kubeconfigPath, userToken)
   197  }
   198  
   199  func (s *SandboxController) GetKubeconfigPathForSpecificUser(toolchainApiUrl string, userName string, kubeconfigPath string, keycloakAuth *KeycloakAuth) (*SandboxUserAuthInfo, error) {
   200  	kubeconfig := api.NewConfig()
   201  	kubeconfig.Clusters[toolchainApiUrl] = &api.Cluster{
   202  		Server:                toolchainApiUrl,
   203  		InsecureSkipTLSVerify: true,
   204  	}
   205  	kubeconfig.Contexts[fmt.Sprintf("%s/%s/%s", userName, toolchainApiUrl, userName)] = &api.Context{
   206  		Cluster:   toolchainApiUrl,
   207  		Namespace: fmt.Sprintf("%s-tenant", userName),
   208  		AuthInfo:  fmt.Sprintf("%s/%s", userName, toolchainApiUrl),
   209  	}
   210  	kubeconfig.AuthInfos[fmt.Sprintf("%s/%s", userName, toolchainApiUrl)] = &api.AuthInfo{
   211  		Token: keycloakAuth.AccessToken,
   212  	}
   213  	kubeconfig.CurrentContext = fmt.Sprintf("%s/%s/%s", userName, toolchainApiUrl, userName)
   214  
   215  	err := clientcmd.WriteToFile(*kubeconfig, kubeconfigPath)
   216  	if err != nil {
   217  		return nil, fmt.Errorf("error writing sandbox user kubeconfig to %s path: %v", kubeconfigPath, err)
   218  	}
   219  
   220  	ns, err := s.GetUserProvisionedNamespace(userName)
   221  	if err != nil {
   222  		return nil, fmt.Errorf("error getting provisioned usernamespace: %v", err)
   223  	}
   224  
   225  	return &SandboxUserAuthInfo{
   226  		UserName:       userName,
   227  		UserNamespace:  ns,
   228  		KubeconfigPath: kubeconfigPath,
   229  		ProxyUrl:       toolchainApiUrl,
   230  		UserToken:      keycloakAuth.AccessToken,
   231  	}, nil
   232  }
   233  
   234  func (s *SandboxController) RegisterSandboxUser(userName string) (compliantUsername string, err error) {
   235  	userSignup := getUserSignupSpecs(userName)
   236  
   237  	if err := s.KubeRest.Create(context.TODO(), userSignup); err != nil {
   238  		if k8sErrors.IsAlreadyExists(err) {
   239  			GinkgoWriter.Printf("User %s already exists\n", userName)
   240  		} else {
   241  			return "", err
   242  		}
   243  	}
   244  
   245  	err = utils.WaitUntil(func() (done bool, err error) {
   246  		err = s.KubeRest.Get(context.TODO(), types.NamespacedName{
   247  			Namespace: DEFAULT_TOOLCHAIN_NAMESPACE,
   248  			Name:      userName,
   249  		}, userSignup)
   250  
   251  		if err != nil {
   252  			return false, err
   253  		}
   254  
   255  		for _, condition := range userSignup.Status.Conditions {
   256  			if condition.Type == toolchainApi.UserSignupComplete && condition.Status == corev1.ConditionTrue {
   257  				compliantUsername = userSignup.Status.CompliantUsername
   258  				if len(compliantUsername) < 1 {
   259  					GinkgoWriter.Printf("Status.CompliantUsername field in UserSignup CR %s in %s namespace is empty\n", userSignup.GetName(), userSignup.GetNamespace())
   260  					return false, nil
   261  				}
   262  				return true, nil
   263  			}
   264  		}
   265  		GinkgoWriter.Printf("Waiting for UserSignup %s to have condition Complete:True\n", userSignup.GetName())
   266  		return false, nil
   267  	}, 4*time.Minute)
   268  
   269  	if err != nil {
   270  		return "", err
   271  	}
   272  	return compliantUsername, nil
   273  
   274  }
   275  
   276  func getUserSignupSpecs(username string) *toolchainApi.UserSignup {
   277  	return &toolchainApi.UserSignup{
   278  		ObjectMeta: metav1.ObjectMeta{
   279  			Name:      username,
   280  			Namespace: DEFAULT_TOOLCHAIN_NAMESPACE,
   281  			Annotations: map[string]string{
   282  				"toolchain.dev.openshift.com/user-email": fmt.Sprintf("%s@user.us", username),
   283  			},
   284  			Labels: map[string]string{
   285  				"toolchain.dev.openshift.com/email-hash": md5.CalcMd5(fmt.Sprintf("%s@user.us", username)),
   286  			},
   287  		},
   288  		Spec: toolchainApi.UserSignupSpec{
   289  			Userid:   username,
   290  			Username: username,
   291  			States: []toolchainApi.UserSignupState{
   292  				toolchainApi.UserSignupStateApproved,
   293  			},
   294  		},
   295  	}
   296  }
   297  
   298  func (s *SandboxController) GetUserProvisionedNamespace(userName string) (namespace string, err error) {
   299  	ns, err := s.waitForNamespaceToBeProvisioned(userName)
   300  	if err != nil {
   301  		return "", err
   302  	}
   303  
   304  	return ns, err
   305  }
   306  
   307  func (s *SandboxController) waitForNamespaceToBeProvisioned(userName string) (provisionedNamespace string, err error) {
   308  	err = utils.WaitUntil(func() (done bool, err error) {
   309  		var namespaceProvisioned bool
   310  		userSpace := &toolchainApi.Space{}
   311  		err = s.KubeRest.Get(context.TODO(), types.NamespacedName{
   312  			Namespace: DEFAULT_TOOLCHAIN_NAMESPACE,
   313  			Name:      userName,
   314  		}, userSpace)
   315  
   316  		if err != nil {
   317  			return false, err
   318  		}
   319  
   320  		// check if a namespace with the username prefix was provisioned
   321  		for _, pns := range userSpace.Status.ProvisionedNamespaces {
   322  			if strings.Contains(pns.Name, userName) {
   323  				namespaceProvisioned = true
   324  				provisionedNamespace = pns.Name
   325  			}
   326  		}
   327  
   328  		for _, condition := range userSpace.Status.Conditions {
   329  			if condition.Reason == toolchainApi.SpaceProvisionedReason && condition.Status == corev1.ConditionTrue && namespaceProvisioned {
   330  				return true, nil
   331  			}
   332  		}
   333  
   334  		return false, nil
   335  	}, 4*time.Minute)
   336  
   337  	return provisionedNamespace, err
   338  }
   339  
   340  func (s *SandboxController) GetOpenshiftRouteHost(namespace string, name string) (string, error) {
   341  	route := &routev1.Route{}
   342  	err := s.KubeRest.Get(context.TODO(), types.NamespacedName{
   343  		Namespace: namespace,
   344  		Name:      name,
   345  	}, route)
   346  	if err != nil {
   347  		return "", err
   348  	}
   349  	return fmt.Sprintf("https://%s", route.Spec.Host), nil
   350  }
   351  
   352  func (s *SandboxController) DeleteUserSignup(userName string) (bool, error) {
   353  	userSignup := &toolchainApi.UserSignup{
   354  		ObjectMeta: metav1.ObjectMeta{
   355  			Name:      userName,
   356  			Namespace: DEFAULT_TOOLCHAIN_NAMESPACE,
   357  		},
   358  	}
   359  	if err := s.KubeRest.Delete(context.TODO(), userSignup); err != nil {
   360  		return false, err
   361  	}
   362  	err := utils.WaitUntil(func() (done bool, err error) {
   363  		err = s.KubeRest.Get(context.TODO(), types.NamespacedName{
   364  			Namespace: DEFAULT_TOOLCHAIN_NAMESPACE,
   365  			Name:      userName,
   366  		}, userSignup)
   367  
   368  		if err != nil {
   369  			if k8sErrors.IsNotFound(err) {
   370  				return true, nil
   371  			}
   372  			return false, err
   373  		}
   374  		return false, nil
   375  	}, 5*time.Minute)
   376  
   377  	if err != nil {
   378  		return false, err
   379  	}
   380  
   381  	return true, nil
   382  }