github.com/verrazzano/verrazzano@v1.7.1/tests/e2e/pkg/rancher.go (about) 1 // Copyright (c) 2022, 2023, Oracle and/or its affiliates. 2 // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. 3 4 package pkg 5 6 import ( 7 "context" 8 "encoding/json" 9 "fmt" 10 "io" 11 "net/http" 12 urlpkg "net/url" 13 "strconv" 14 "strings" 15 16 "github.com/hashicorp/go-retryablehttp" 17 "github.com/onsi/gomega" 18 "github.com/verrazzano/verrazzano/pkg/constants" 19 "github.com/verrazzano/verrazzano/pkg/httputil" 20 "github.com/verrazzano/verrazzano/pkg/k8sutil" 21 "github.com/verrazzano/verrazzano/pkg/rancherutil" 22 "github.com/verrazzano/verrazzano/platform-operator/controllers/verrazzano/component/common" 23 "github.com/verrazzano/verrazzano/platform-operator/controllers/verrazzano/component/rancher" 24 "go.uber.org/zap" 25 corev1 "k8s.io/api/core/v1" 26 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/runtime/schema" 28 ) 29 30 type Payload struct { 31 ClusterID string `json:"clusterID"` 32 TTL int `json:"ttl"` 33 } 34 35 type TokenPostResponse struct { 36 Token string `json:"token"` 37 Created string `json:"created"` 38 } 39 40 type ListOfTokenOutputFromRancher struct { 41 Data []struct { 42 ClusterID string `json:"clusterId"` 43 Name string `json:"name"` 44 } `json:"data"` 45 } 46 47 func EventuallyGetURLForIngress(log *zap.SugaredLogger, api *APIEndpoint, namespace string, name string, scheme string) string { 48 ingressHost := EventuallyGetIngressHost(log, api, namespace, name) 49 gomega.Expect(ingressHost).ToNot(gomega.BeEmpty()) 50 return fmt.Sprintf("%s://%s", scheme, ingressHost) 51 } 52 53 func EventuallyGetIngressHost(log *zap.SugaredLogger, api *APIEndpoint, namespace string, name string) string { 54 var ingressHost string 55 gomega.Eventually(func() error { 56 ingress, err := api.GetIngress(namespace, name) 57 if err != nil { 58 return err 59 } 60 if len(ingress.Spec.Rules) == 0 { 61 return fmt.Errorf("no rules found in ingress %s/%s", namespace, name) 62 } 63 ingressHost = ingress.Spec.Rules[0].Host 64 log.Info(fmt.Sprintf("Found ingress host: %s", ingressHost)) 65 return nil 66 }, waitTimeout, pollingInterval).Should(gomega.BeNil()) 67 return ingressHost 68 } 69 70 func GetURLForIngress(log *zap.SugaredLogger, api *APIEndpoint, namespace string, name string, scheme string) (string, error) { 71 ingress, err := api.GetIngress(namespace, name) 72 if err != nil { 73 return "", err 74 } 75 ingressURL := fmt.Sprintf("%s://%s", scheme, ingress.Spec.Rules[0].Host) 76 log.Info(fmt.Sprintf("Found ingress URL: %s", ingressURL)) 77 return ingressURL, err 78 } 79 80 func eventuallyGetRancherAdminPassword(log *zap.SugaredLogger) (string, error) { 81 var err error 82 var secret *corev1.Secret 83 gomega.Eventually(func() error { 84 secret, err = GetSecret("cattle-system", "rancher-admin-secret") 85 if err != nil { 86 log.Error(fmt.Sprintf("Error getting rancher-admin-secret, retrying: %v", err)) 87 } 88 return err 89 }, waitTimeout, pollingInterval).Should(gomega.BeNil()) 90 91 if secret == nil { 92 return "", fmt.Errorf("Unable to get rancher admin secret") 93 } 94 95 var rancherAdminPassword []byte 96 var ok bool 97 if rancherAdminPassword, ok = secret.Data["password"]; !ok { 98 return "", fmt.Errorf("Error getting rancher admin credentials") 99 } 100 101 return string(rancherAdminPassword), nil 102 } 103 104 func GetRancherAdminToken(log *zap.SugaredLogger, httpClient *retryablehttp.Client, rancherURL string) string { 105 rancherAdminPassword, err := eventuallyGetRancherAdminPassword(log) 106 if err != nil { 107 log.Error(fmt.Sprintf("Error getting rancher admin password: %v", err)) 108 return "" 109 } 110 111 token, err := getRancherUserToken(log, httpClient, rancherURL, "admin", string(rancherAdminPassword)) 112 if err != nil { 113 log.Error(fmt.Sprintf("Error getting user token from rancher: %v", err)) 114 return "" 115 } 116 117 return token 118 } 119 120 func getRancherUserToken(log *zap.SugaredLogger, httpClient *retryablehttp.Client, rancherURL string, username string, password string) (string, error) { 121 rancherLoginURL := fmt.Sprintf("%s/%s", rancherURL, "v3-public/localProviders/local?action=login") 122 payload := `{"Username": "` + username + `", "Password": "` + password + `"}` 123 response, err := httpClient.Post(rancherLoginURL, "application/json", strings.NewReader(payload)) 124 if err != nil { 125 log.Error(fmt.Sprintf("Error getting rancher admin token: %v", err)) 126 return "", err 127 } 128 129 err = httputil.ValidateResponseCode(response, http.StatusCreated) 130 if err != nil { 131 log.Errorf("Invalid response code when fetching Rancher token: %v", err) 132 return "", err 133 } 134 135 defer response.Body.Close() 136 137 // extract the response body 138 body, err := io.ReadAll(response.Body) 139 if err != nil { 140 log.Errorf("Failed to read Rancher token response: %v", err) 141 return "", err 142 } 143 144 token, err := httputil.ExtractFieldFromResponseBodyOrReturnError(string(body), "token", "unable to find token in Rancher response") 145 if err != nil { 146 log.Errorf("Failed to extract token from Rancher response: %v", err) 147 return "", err 148 } 149 150 return token, nil 151 } 152 153 // This function adds an access token to Rancher gven that a ttl and clusterID string is provided 154 func AddAccessTokenToRancherForLoggedInUser(log *zap.SugaredLogger, adminKubeConfig, managedClusterName, usernameForRancher, ttl string) (string, error) { 155 responseBody, err := ExecutePostRequestToAddAToken(log, adminKubeConfig, managedClusterName, usernameForRancher, ttl) 156 if err != nil { 157 return "", err 158 } 159 var tokenPostResponse TokenPostResponse 160 err = json.Unmarshal(responseBody, &tokenPostResponse) 161 if err != nil { 162 return "", err 163 } 164 165 return tokenPostResponse.Created, nil 166 } 167 168 // This function returns the list of token names that correspond to the cluster ID and this user before when this is called 169 // If no error occurs, this means that these tokens were found and deleted in Rancher 170 func GetAndDeleteTokenNamesForLoggedInUserBasedOnClusterID(log *zap.SugaredLogger, adminKubeConfig, managedClusterName, usernameForRancher string) error { 171 responseBody, err := ExecuteGetRequestToReturnAllTokens(log, adminKubeConfig, managedClusterName, usernameForRancher) 172 if err != nil { 173 return err 174 } 175 var listOfTokenOutputFromRancher = ListOfTokenOutputFromRancher{} 176 err = json.Unmarshal(responseBody, &listOfTokenOutputFromRancher) 177 if err != nil { 178 return err 179 } 180 listOfTokens := listOfTokenOutputFromRancher.Data 181 182 clusterID, err := getClusterIDForManagedCluster(adminKubeConfig, managedClusterName) 183 184 if err != nil { 185 return err 186 } 187 188 for _, token := range listOfTokens { 189 //Check that it is not the same name as the user access token and it has the same cluster ID 190 if token.ClusterID != clusterID { 191 continue 192 } 193 err = ExecuteDeleteRequestForToken(log, adminKubeConfig, managedClusterName, usernameForRancher, token.Name) 194 if err != nil { 195 return err 196 } 197 } 198 199 return nil 200 201 } 202 203 // VerifyRancherAccess verifies that Rancher is accessible. 204 func VerifyRancherAccess(log *zap.SugaredLogger) error { 205 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 206 if err != nil { 207 log.Error(fmt.Sprintf("Error getting kubeconfig: %v", err)) 208 return err 209 } 210 211 api := EventuallyGetAPIEndpoint(kubeconfigPath) 212 rancherURL := EventuallyGetURLForIngress(log, api, "cattle-system", "rancher", "https") 213 httpClient := EventuallyVerrazzanoRetryableHTTPClient() 214 var httpResponse *HTTPResponse 215 216 gomega.Eventually(func() (*HTTPResponse, error) { 217 httpResponse, err = GetWebPageWithClient(httpClient, rancherURL, "") 218 return httpResponse, err 219 }, waitTimeout, pollingInterval).Should(HasStatus(http.StatusOK)) 220 221 gomega.Expect(CheckNoServerHeader(httpResponse)).To(gomega.BeTrue(), "Found unexpected server header in response") 222 return nil 223 } 224 225 // VerifyRancherKeycloakAuthConfig verifies that Rancher/Keycloak AuthConfig is correctly populated 226 func VerifyRancherKeycloakAuthConfig(log *zap.SugaredLogger) error { 227 kubeconfigPath, err := k8sutil.GetKubeConfigLocation() 228 if err != nil { 229 log.Error(fmt.Sprintf("Error getting kubeconfig: %v", err)) 230 return err 231 } 232 233 log.Info("Verify Keycloak AuthConfig") 234 235 gomega.Eventually(func() (bool, error) { 236 api, err := GetAPIEndpoint(kubeconfigPath) 237 if err != nil { 238 log.Error(fmt.Sprintf("Error getting API endpoint: %v", err)) 239 return false, err 240 } 241 keycloakURL, err := GetURLForIngress(log, api, "keycloak", "keycloak", "https") 242 if err != nil { 243 log.Error(fmt.Sprintf("Error getting API endpoint: %v", err)) 244 return false, err 245 } 246 rancherURL, err := GetURLForIngress(log, api, "cattle-system", "rancher", "https") 247 if err != nil { 248 return false, err 249 } 250 k8sClient, err := GetDynamicClientInCluster(kubeconfigPath) 251 if err != nil { 252 log.Error(fmt.Sprintf("Error getting dynamic client: %v", err)) 253 return false, err 254 } 255 256 authConfigData, err := k8sClient.Resource(GvkToGvr(common.GVKAuthConfig)).Get(context.Background(), common.AuthConfigKeycloak, v1.GetOptions{}) 257 if err != nil { 258 log.Error(fmt.Sprintf("error getting keycloak oidc authConfig: %v", err)) 259 return false, err 260 } 261 262 authConfigAttributes := authConfigData.UnstructuredContent() 263 if err = verifyAuthConfigAttribute(rancher.AuthConfigKeycloakAttributeAccessMode, authConfigAttributes[rancher.AuthConfigKeycloakAttributeAccessMode].(string), rancher.AuthConfigKeycloakAccessMode); err != nil { 264 log.Error(err) 265 return false, err 266 } 267 268 if err = verifyAuthConfigAttribute(rancher.AuthConfigKeycloakAttributeClientID, authConfigAttributes[rancher.AuthConfigKeycloakAttributeClientID].(string), rancher.AuthConfigKeycloakClientIDRancher); err != nil { 269 log.Error(err) 270 return false, err 271 } 272 273 if err = verifyAuthConfigAttribute(rancher.AuthConfigKeycloakAttributeGroupSearchEnabled, authConfigAttributes[rancher.AuthConfigKeycloakAttributeGroupSearchEnabled].(bool), true); err != nil { 274 return false, err 275 } 276 277 if err = verifyAuthConfigAttribute(rancher.AuthConfigKeycloakAttributeAuthEndpoint, authConfigAttributes[rancher.AuthConfigKeycloakAttributeAuthEndpoint].(string), keycloakURL+rancher.AuthConfigKeycloakURLPathAuthEndPoint); err != nil { 278 log.Error(err) 279 return false, err 280 } 281 282 if err = verifyAuthConfigAttribute(rancher.AuthConfigKeycloakAttributeRancherURL, authConfigAttributes[rancher.AuthConfigKeycloakAttributeRancherURL].(string), rancherURL+rancher.AuthConfigKeycloakURLPathVerifyAuth); err != nil { 283 log.Error(err) 284 return false, err 285 } 286 287 authConfigClientSecret := authConfigAttributes[common.AuthConfigKeycloakAttributeClientSecret].(string) 288 if authConfigClientSecret == "" { 289 err = fmt.Errorf("keycloak auth config attribute %s not correctly configured, value is empty", common.AuthConfigKeycloakAttributeClientSecret) 290 log.Error(err) 291 return false, err 292 } 293 294 return true, nil 295 }, waitTimeout, pollingInterval).Should(gomega.Equal(true), "keycloak oidc authconfig not configured correctly") 296 return nil 297 } 298 299 // GvkToGvr converts a GroupVersionKind to corresponding GroupVersionResource 300 func GvkToGvr(gvk schema.GroupVersionKind) schema.GroupVersionResource { 301 resource := strings.ToLower(gvk.Kind) 302 if strings.HasSuffix(resource, "s") { 303 resource = resource + "es" 304 } else { 305 resource = resource + "s" 306 } 307 308 return schema.GroupVersionResource{Group: gvk.Group, 309 Version: gvk.Version, 310 Resource: resource, 311 } 312 } 313 314 func verifyAuthConfigAttribute(name string, actual interface{}, expected interface{}) error { 315 if expected != actual { 316 return fmt.Errorf("keycloak auth config attribute %s not correctly configured, expected %v, actual %v", name, expected, actual) 317 } 318 return nil 319 } 320 321 func EventuallyGetRancherHost(log *zap.SugaredLogger, api *APIEndpoint) (string, error) { 322 rancherHost := EventuallyGetIngressHost(log, api, rancher.ComponentNamespace, common.RancherName) 323 if rancherHost == "" { 324 return "", fmt.Errorf("got empty Rancher ingress host") 325 } 326 return rancherHost, nil 327 } 328 329 func CreateNewRancherConfig(log *zap.SugaredLogger, kubeconfigPath string) (*rancherutil.RancherConfig, error) { 330 rancherAdminPassword, err := eventuallyGetRancherAdminPassword(log) 331 if err != nil { 332 return nil, err 333 } 334 return CreateNewRancherConfigForUser(log, kubeconfigPath, "admin", rancherAdminPassword) 335 } 336 337 func CreateNewRancherConfigForUser(log *zap.SugaredLogger, kubeconfigPath string, username string, password string) (*rancherutil.RancherConfig, error) { 338 apiEndpoint := EventuallyGetAPIEndpoint(kubeconfigPath) 339 rancherHost, err := EventuallyGetRancherHost(log, apiEndpoint) 340 if err != nil { 341 return nil, err 342 } 343 rancherURL := fmt.Sprintf("https://%s", rancherHost) 344 caCert, err := GetCACertFromSecret(common.RancherIngressCAName, constants.RancherSystemNamespace, "ca.crt", kubeconfigPath) 345 if err != nil { 346 return nil, fmt.Errorf("failed to get caCert: %v", err) 347 } 348 349 // the tls-ca secret is optional, and contains the private CA bundle configured for Rancher 350 additionalCA, _ := GetCACertFromSecret(constants.RancherTLSCA, constants.RancherSystemNamespace, constants.RancherTLSCAKey, kubeconfigPath) 351 352 httpClient, err := GetVerrazzanoHTTPClient(kubeconfigPath) 353 if err != nil { 354 return nil, err 355 } 356 token, err := getRancherUserToken(log, httpClient, rancherURL, username, password) 357 if err != nil { 358 return nil, fmt.Errorf("failed to get user token from Rancher: %v", err) 359 } 360 361 rc := rancherutil.RancherConfig{ 362 // populate Rancher config from the functions available in this file,adding as necessary 363 BaseURL: rancherURL, 364 Host: rancherHost, 365 APIAccessToken: token, 366 CertificateAuthorityData: caCert, 367 AdditionalCA: additionalCA, 368 } 369 return &rc, nil 370 } 371 372 func GetClusterKubeconfig(log *zap.SugaredLogger, httpClient *retryablehttp.Client, rc *rancherutil.RancherConfig, clusterID string) (string, error) { 373 reqURL := rc.BaseURL + "/v3/clusters/" + clusterID + "?action=generateKubeconfig" 374 req, err := retryablehttp.NewRequest("POST", reqURL, nil) 375 if err != nil { 376 return "", err 377 } 378 req.Header.Set("Authorization", "Bearer "+rc.APIAccessToken) 379 380 response, err := httpClient.Do(req) 381 if err != nil { 382 log.Error(fmt.Sprintf("Error getting managed cluster kubeconfig: %v", err)) 383 return "", err 384 } 385 386 err = httputil.ValidateResponseCode(response, http.StatusOK) 387 if err != nil { 388 log.Errorf("Invalid response code when fetching cluster kubeconfig: %v", err) 389 return "", err 390 } 391 392 defer response.Body.Close() 393 394 // extract the response body 395 responseBody, err := io.ReadAll(response.Body) 396 if err != nil { 397 log.Errorf("Failed to read Rancher kubeconfig response: %v", err) 398 return "", err 399 } 400 401 return httputil.ExtractFieldFromResponseBodyOrReturnError(string(responseBody), "config", "") 402 } 403 404 // This function is a wrapper function that executes a GET Request to return all tokens in Rancher for a given user 405 func ExecuteGetRequestToReturnAllTokens(log *zap.SugaredLogger, adminKubeconfig, managedClusterName, usernameForRancher string) ([]byte, error) { 406 getReq, err := retryablehttp.NewRequest("GET", "", nil) 407 if err != nil { 408 return nil, err 409 } 410 getReq.Header = map[string][]string{"Content-Type": {"application/json"}, "Accept": {"application/json"}} 411 return sendTokenRequestToRancher(log, adminKubeconfig, managedClusterName, usernameForRancher, getReq, http.StatusOK, "/v3/tokens") 412 } 413 414 // This function is a wrapper function that executes a POST Request to add a token in Rancher for a given user 415 func ExecutePostRequestToAddAToken(log *zap.SugaredLogger, adminKubeconfig, managedClusterName, usernameForRancher, ttl string) ([]byte, error) { 416 clusterID, err := getClusterIDForManagedCluster(adminKubeconfig, managedClusterName) 417 if err != nil { 418 return nil, err 419 } 420 val, _ := strconv.Atoi(ttl) 421 payload := &Payload{ 422 ClusterID: clusterID, 423 TTL: val * 60000, 424 } 425 data, err := json.Marshal(payload) 426 if err != nil { 427 return nil, err 428 } 429 postReq, err := retryablehttp.NewRequest("POST", "", data) 430 if err != nil { 431 return nil, err 432 } 433 postReq.Header = map[string][]string{"Content-Type": {"application/json"}} 434 return sendTokenRequestToRancher(log, adminKubeconfig, managedClusterName, usernameForRancher, postReq, http.StatusCreated, "/v3/tokens") 435 } 436 437 // This function is a wrapper function to delete a given token for a specified user in Rancher 438 func ExecuteDeleteRequestForToken(log *zap.SugaredLogger, adminKubeconfig, managedClusterName, usernameForRancher, tokenName string) error { 439 deleteReq, err := retryablehttp.NewRequest("DELETE", "", nil) 440 if err != nil { 441 return err 442 } 443 deleteReq.Header = map[string][]string{"Accept": {"application/json"}} 444 _, err = sendTokenRequestToRancher(log, adminKubeconfig, managedClusterName, usernameForRancher, deleteReq, http.StatusNoContent, "/v3/tokens/"+tokenName) 445 return err 446 } 447 448 // This function is a helper function that sends a Token Request to Rancher for a specified user 449 // This function expects a retryable HTTP Request object 450 func sendTokenRequestToRancher(log *zap.SugaredLogger, adminKubeconfig, managedClusterName, usernameForRancher string, requestObject *retryablehttp.Request, expectedReturnCode int, requestPath string) ([]byte, error) { 451 httpClient, APIAccessToken, err := getRequiredInfoToPreformTokenOperationsInRancherForArgoCD(log, adminKubeconfig, managedClusterName, usernameForRancher) 452 if err != nil { 453 return nil, err 454 } 455 api, err := GetAPIEndpoint(adminKubeconfig) 456 if err != nil { 457 log.Errorf("API Endpoint not successfully received based on KubeConfig Path") 458 return nil, err 459 } 460 rancherURL, err := GetURLForIngress(log, api, "cattle-system", "rancher", "https") 461 if err != nil { 462 log.Errorf("URL For Rancher not successfully found") 463 return nil, err 464 } 465 reqURL := rancherURL + requestPath 466 URLForRequest, err := urlpkg.Parse(reqURL) 467 if err != nil { 468 return nil, err 469 } 470 requestObject.URL = URLForRequest 471 requestObject.Header["Authorization"] = []string{"Bearer " + APIAccessToken} 472 response, err := httpClient.Do(requestObject) 473 if err != nil { 474 return nil, err 475 } 476 responseBody, err := io.ReadAll(response.Body) 477 if err != nil { 478 return nil, err 479 } 480 err = httputil.ValidateResponseCode(response, expectedReturnCode) 481 if err != nil { 482 return nil, err 483 } 484 return responseBody, err 485 486 } 487 488 // This function gets the necessary information required to access the token API resources in Rancher for ArgoCD 489 func getRequiredInfoToPreformTokenOperationsInRancherForArgoCD(log *zap.SugaredLogger, adminKubeconfig, managedClusterName, argoCDUsernameForRancher string) (httpClient *retryablehttp.Client, APIAccessToken string, err error) { 490 argoCDPasswordForRancher, err := RetrieveArgoCDPassword("verrazzano-mc", "verrazzano-argocd-secret") 491 if err != nil { 492 return nil, "", err 493 } 494 rancherConfigForArgoCD, err := CreateNewRancherConfigForUser(log, adminKubeconfig, argoCDUsernameForRancher, argoCDPasswordForRancher) 495 if err != nil { 496 Log(Error, "Error occurred when created a Rancher Config for ArgoCD") 497 return nil, "", err 498 } 499 httpClientForRancher, err := GetVerrazzanoHTTPClient(adminKubeconfig) 500 if err != nil { 501 Log(Error, "Error getting the Verrazzano http client") 502 return nil, "", err 503 } 504 return httpClientForRancher, rancherConfigForArgoCD.APIAccessToken, nil 505 } 506 507 func getClusterIDForManagedCluster(adminKubeConfig, managedClusterName string) (string, error) { 508 client, err := GetClusterOperatorClientset(adminKubeConfig) 509 if err != nil { 510 Log(Error, "Error creating the client set used by the cluster operator") 511 return "", err 512 } 513 managedCluster, err := client.ClustersV1alpha1().VerrazzanoManagedClusters(constants.VerrazzanoMultiClusterNamespace).Get(context.TODO(), managedClusterName, v1.GetOptions{}) 514 if err != nil { 515 Log(Error, "Error getting the current managed cluster resource") 516 return "", err 517 } 518 clusterID := managedCluster.Status.RancherRegistration.ClusterID 519 if clusterID == "" { 520 Log(Error, "The managed cluster does not have a clusterID value") 521 err := fmt.Errorf("ClusterID value is not yet populated for the managed cluster") 522 return "", err 523 } 524 return clusterID, nil 525 526 }