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 }