github.com/verrazzano/verrazzano@v1.7.1/pkg/rancherutil/rancher_config.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 rancherutil 5 6 import ( 7 "bytes" 8 "context" 9 "crypto/tls" 10 "encoding/json" 11 "fmt" 12 "io" 13 "net" 14 "net/http" 15 "net/url" 16 "os" 17 "strconv" 18 "strings" 19 "sync" 20 "time" 21 22 "github.com/verrazzano/verrazzano/pkg/nginxutil" 23 24 cons "github.com/verrazzano/verrazzano/pkg/constants" 25 "github.com/verrazzano/verrazzano/pkg/httputil" 26 "github.com/verrazzano/verrazzano/pkg/log/vzlog" 27 "github.com/verrazzano/verrazzano/platform-operator/constants" 28 "github.com/verrazzano/verrazzano/platform-operator/controllers/verrazzano/component/common" 29 corev1 "k8s.io/api/core/v1" 30 k8net "k8s.io/api/networking/v1" 31 "k8s.io/apimachinery/pkg/types" 32 "k8s.io/apimachinery/pkg/util/wait" 33 "sigs.k8s.io/controller-runtime/pkg/client" 34 ) 35 36 const ( 37 rancherNamespace = "cattle-system" 38 rancherIngressName = "rancher" 39 40 rancherAdminSecret = "rancher-admin-secret" //nolint:gosec //#gosec G101 41 rancherAdminUsername = "admin" 42 43 loginPath = "/v3-public/localProviders/local?action=login" 44 tokensPath = "/v3/tokens" //nolint:gosec 45 ) 46 47 // DefaultRancherIngressHostPrefix is the default internal Ingress host prefix used for Rancher API requests 48 const DefaultRancherIngressHostPrefix = "ingress-controller-ingress-nginx-controller." 49 50 type RancherConfig struct { 51 Host string 52 BaseURL string 53 APIAccessToken string 54 CertificateAuthorityData []byte 55 AdditionalCA []byte 56 User string 57 } 58 59 var DefaultRetry = wait.Backoff{ 60 Steps: 10, 61 Duration: 1 * time.Second, 62 Factor: 2.0, 63 Jitter: 0.1, 64 } 65 66 // The userTokenCache stores rancher auth tokens for a given user if it exists 67 // This reuses tokens when possible instead of creating a new one every reconcile loop 68 var userTokenCache = make(map[string]string) 69 var userLock = &sync.RWMutex{} 70 71 // requestSender is an interface for sending requests to Rancher that allows us to mock during unit testing 72 type requestSender interface { 73 Do(httpClient *http.Client, req *http.Request) (*http.Response, error) 74 } 75 76 // HTTPRequestSender is an implementation of requestSender that uses http.Client to send requests 77 type HTTPRequestSender struct{} 78 79 // RancherHTTPClient will be replaced with a mock in unit tests 80 var RancherHTTPClient requestSender = &HTTPRequestSender{} 81 82 // Do is a function that simply delegates sending the request to the http.Client 83 func (*HTTPRequestSender) Do(httpClient *http.Client, req *http.Request) (*http.Response, error) { 84 return httpClient.Do(req) 85 } 86 87 // NewAdminRancherConfig creates A rancher config that authenticates with the admin user 88 func NewAdminRancherConfig(rdr client.Reader, host string, log vzlog.VerrazzanoLogger) (*RancherConfig, error) { 89 secret, err := GetAdminSecret(rdr) 90 if err != nil { 91 return nil, log.ErrorfNewErr("Failed to get the admin secret from the cluster: %v", err) 92 } 93 return NewRancherConfigForUser(rdr, rancherAdminUsername, secret, host, log) 94 } 95 96 // NewVerrazzanoClusterRancherConfig creates A rancher config that authenticates with the Verrazzano cluster user 97 func NewVerrazzanoClusterRancherConfig(rdr client.Reader, host string, log vzlog.VerrazzanoLogger) (*RancherConfig, error) { 98 secret, err := GetVerrazzanoClusterUserSecret(rdr) 99 if err != nil { 100 return nil, log.ErrorfNewErr("Failed to get the Verrazzano cluster secret from the cluster: %v", err) 101 } 102 return NewRancherConfigForUser(rdr, cons.VerrazzanoClusterRancherUsername, secret, host, log) 103 } 104 105 // NewRancherConfigForUser returns a populated RancherConfig struct that can be used to make calls to the Rancher API 106 func NewRancherConfigForUser(rdr client.Reader, username, password, host string, log vzlog.VerrazzanoLogger) (*RancherConfig, error) { 107 rc := &RancherConfig{BaseURL: "https://" + host} 108 // Needed to populate userToken[] map 109 rc.User = username 110 111 // Rancher host name is needed for TLS 112 log.Debug("Getting Rancher ingress host name") 113 hostname, err := getRancherIngressHostname(rdr) 114 if err != nil { 115 log.Errorf("Failed to get Rancher ingress host name: %v", err) 116 return nil, err 117 } 118 rc.Host = hostname 119 120 log.Debug("Getting Rancher TLS root CA") 121 caCert, err := common.GetRootCA(rdr) 122 if err != nil { 123 log.Errorf("Failed to get Rancher TLS root CA: %v", err) 124 return nil, err 125 } 126 rc.CertificateAuthorityData = caCert 127 128 log.Debugf("Checking for Rancher additional CA in secret %s", cons.RancherTLSCA) 129 rc.AdditionalCA = common.GetAdditionalCA(rdr) 130 131 token, exists := getStoredToken(username) 132 if !exists { 133 token, err = getUserToken(rc, log, password, username) 134 if err != nil { 135 return nil, err 136 } 137 newStoredToken(username, token) 138 } 139 140 rc.APIAccessToken = token 141 return rc, nil 142 } 143 144 // newStoredToken creates a new user:token key-pair in memory 145 func newStoredToken(username string, token string) { 146 userLock.Lock() 147 defer userLock.Unlock() 148 userTokenCache[username] = token 149 } 150 151 // getStoredToken gets the token for the given user from memory 152 func getStoredToken(username string) (string, bool) { 153 userLock.RLock() 154 defer userLock.RUnlock() 155 token, exists := userTokenCache[username] 156 return token, exists 157 } 158 159 // deleteStoredToken deletes the token for the given user from memory 160 func deleteStoredToken(username string) { 161 userLock.Lock() 162 defer userLock.Unlock() 163 delete(userTokenCache, username) 164 } 165 166 // DeleteStoredTokens clears the map of stored tokens. 167 func DeleteStoredTokens() { 168 userLock.Lock() 169 defer userLock.Unlock() 170 userTokenCache = make(map[string]string) 171 } 172 173 // getRancherIngressHostname gets the Rancher ingress host name. This is used to set the host for TLS. 174 func getRancherIngressHostname(rdr client.Reader) (string, error) { 175 ingress := &k8net.Ingress{} 176 nsName := types.NamespacedName{ 177 Namespace: rancherNamespace, 178 Name: rancherIngressName} 179 if err := rdr.Get(context.TODO(), nsName, ingress); err != nil { 180 return "", fmt.Errorf("Failed to get Rancher ingress %v: %v", nsName, err) 181 } 182 183 if len(ingress.Spec.Rules) > 0 { 184 // the first host will do 185 return ingress.Spec.Rules[0].Host, nil 186 } 187 188 return "", fmt.Errorf("Failed, Rancher ingress %v is missing host names", nsName) 189 } 190 191 // GetVerrazzanoClusterUserSecret fetches the Rancher Verrazzano user secret 192 func GetVerrazzanoClusterUserSecret(rdr client.Reader) (string, error) { 193 secret := &corev1.Secret{} 194 nsName := types.NamespacedName{ 195 Namespace: constants.VerrazzanoMultiClusterNamespace, 196 Name: cons.VerrazzanoClusterRancherName} 197 198 if err := rdr.Get(context.TODO(), nsName, secret); err != nil { 199 return "", err 200 } 201 return string(secret.Data["password"]), nil 202 } 203 204 // GetAdminSecret fetches the Rancher admin secret 205 func GetAdminSecret(rdr client.Reader) (string, error) { 206 secret := &corev1.Secret{} 207 nsName := types.NamespacedName{ 208 Namespace: rancherNamespace, 209 Name: rancherAdminSecret} 210 211 if err := rdr.Get(context.TODO(), nsName, secret); err != nil { 212 return "", err 213 } 214 return string(secret.Data["password"]), nil 215 } 216 217 // getUserToken gets a user token from a secret 218 func getUserToken(rc *RancherConfig, log vzlog.VerrazzanoLogger, secret, username string) (string, error) { 219 action := http.MethodPost 220 payload := `{"Username": "` + username + `", "Password": "` + secret + `"}` 221 reqURL := rc.BaseURL + loginPath 222 headers := map[string]string{"Content-Type": "application/json"} 223 224 response, responseBody, err := SendRequest(action, reqURL, headers, payload, rc, log) 225 if err != nil { 226 return "", err 227 } 228 229 err = httputil.ValidateResponseCode(response, http.StatusCreated) 230 if err != nil { 231 return "", err 232 } 233 234 return httputil.ExtractFieldFromResponseBodyOrReturnError(responseBody, "token", "unable to find token in Rancher response") 235 } 236 237 type Payload struct { 238 ClusterID string `json:"clusterID"` 239 TTL int `json:"ttl"` 240 } 241 242 type TokenPostResponse struct { 243 Token string `json:"token"` 244 Created string `json:"created"` 245 } 246 247 // CreateTokenWithTTL creates a user token with ttl (in minutes) 248 func CreateTokenWithTTL(rc *RancherConfig, log vzlog.VerrazzanoLogger, ttl, clusterID string) (string, string, error) { 249 val, _ := strconv.Atoi(ttl) 250 payload := &Payload{ 251 ClusterID: clusterID, 252 TTL: val * 60000, 253 } 254 data, err := json.Marshal(payload) 255 if err != nil { 256 return "", "", err 257 } 258 action := http.MethodPost 259 reqURL := rc.BaseURL + tokensPath 260 headers := map[string]string{"Authorization": "Bearer " + rc.APIAccessToken, "Content-Type": "application/json"} 261 262 response, responseBody, err := SendRequest(action, reqURL, headers, string(data), rc, log) 263 if err != nil { 264 return "", "", err 265 } 266 err = httputil.ValidateResponseCode(response, http.StatusCreated) 267 if err != nil { 268 return "", "", log.ErrorfNewErr("Failed to validate response: %v", err) 269 } 270 271 var tokenPostResponse TokenPostResponse 272 err = json.Unmarshal([]byte(responseBody), &tokenPostResponse) 273 if err != nil { 274 return "", "", log.ErrorfNewErr("Failed to parse response: %v", err) 275 } 276 277 return tokenPostResponse.Token, tokenPostResponse.Created, nil 278 } 279 280 type TokenGetResponse struct { 281 Created string `json:"created"` 282 ClusterID string `json:"clusterId"` 283 ExpiresAt string `json:"expiresAt"` 284 UserID string `json:"userID"` 285 } 286 287 // GetTokenWithFilter get created expiresAt attribute of a user token with filter 288 func GetTokenWithFilter(rc *RancherConfig, log vzlog.VerrazzanoLogger, userID, clusterID string) (string, string, error) { 289 action := http.MethodGet 290 reqURL := rc.BaseURL + tokensPath + "?userId=" + url.PathEscape(userID) + "&clusterId=" + url.PathEscape(clusterID) 291 headers := map[string]string{"Authorization": "Bearer " + rc.APIAccessToken} 292 293 response, responseBody, err := SendRequest(action, reqURL, headers, "", rc, log) 294 if err != nil { 295 return "", "", err 296 } 297 err = httputil.ValidateResponseCode(response, http.StatusOK) 298 if err != nil { 299 return "", "", log.ErrorfNewErr("Failed to validate response: %v", err) 300 } 301 302 data, err := httputil.ExtractFieldFromResponseBodyOrReturnError(responseBody, "data", "failed to locate the data field of the response body") 303 if err != nil { 304 return "", "", log.ErrorfNewErr("Failed to find data in Rancher response: %v", err) 305 } 306 307 var items []TokenGetResponse 308 json.Unmarshal([]byte(data), &items) 309 if err != nil { 310 return "", "", log.ErrorfNewErr("Failed to parse response: %v", err) 311 } 312 var tokenToReturn *TokenGetResponse 313 for i, item := range items { 314 if item.ClusterID != clusterID || item.UserID != userID { 315 continue 316 } 317 318 if tokenToReturn == nil && len(item.Created) > 0 { 319 tokenToReturn = &(items[i]) 320 continue 321 } 322 // Find the latest token based on creation timestamp 323 timeOfCurrentTokenToReturn, _ := time.Parse(time.RFC3339, tokenToReturn.Created) 324 timeOfTokenBeingExamined, _ := time.Parse(time.RFC3339, item.Created) 325 326 if timeOfTokenBeingExamined.After(timeOfCurrentTokenToReturn) { 327 tokenToReturn = &(items[i]) 328 } 329 330 } 331 if tokenToReturn == nil { 332 return "", "", nil 333 } 334 return tokenToReturn.Created, tokenToReturn.ExpiresAt, nil 335 } 336 337 // getProxyURL returns an HTTP proxy from the environment if one is set, otherwise an empty string 338 func getProxyURL() string { 339 if proxyURL := os.Getenv("https_proxy"); proxyURL != "" { 340 return proxyURL 341 } 342 if proxyURL := os.Getenv("HTTPS_PROXY"); proxyURL != "" { 343 return proxyURL 344 } 345 if proxyURL := os.Getenv("http_proxy"); proxyURL != "" { 346 return proxyURL 347 } 348 if proxyURL := os.Getenv("HTTP_PROXY"); proxyURL != "" { 349 return proxyURL 350 } 351 return "" 352 } 353 354 // SendRequest builds an HTTP request, sends it, and returns the response 355 func SendRequest(action string, reqURL string, headers map[string]string, payload string, 356 rc *RancherConfig, log vzlog.VerrazzanoLogger) (*http.Response, string, error) { 357 358 req, err := http.NewRequest(action, reqURL, strings.NewReader(payload)) 359 if err != nil { 360 return nil, "", err 361 } 362 363 req.Header.Add("Accept", "*/*") 364 365 for k := range headers { 366 req.Header.Add(k, headers[k]) 367 } 368 req.Header.Add("Host", rc.Host) 369 req.Host = rc.Host 370 371 response, body, err := doRequest(req, rc, log) 372 // If we get an unauthorized response, remove the token from the cache 373 if response != nil && response.StatusCode == http.StatusUnauthorized { 374 deleteStoredToken(rc.User) 375 } 376 return response, body, err 377 } 378 379 // doRequest configures an HTTP transport (including TLS), sends an HTTP request with retries, and returns the response 380 func doRequest(req *http.Request, rc *RancherConfig, log vzlog.VerrazzanoLogger) (*http.Response, string, error) { 381 log.Debugf("Attempting HTTP request: %v", req) 382 383 proxyURL := getProxyURL() 384 385 var tlsConfig *tls.Config 386 if len(rc.CertificateAuthorityData) < 1 && len(rc.AdditionalCA) < 1 { 387 tlsConfig = &tls.Config{ 388 ServerName: rc.Host, 389 MinVersion: tls.VersionTLS12, 390 } 391 } else { 392 tlsConfig = &tls.Config{ 393 RootCAs: common.CertPool(rc.CertificateAuthorityData, rc.AdditionalCA), 394 ServerName: rc.Host, 395 MinVersion: tls.VersionTLS12, 396 } 397 } 398 tr := &http.Transport{ 399 TLSClientConfig: tlsConfig, 400 TLSHandshakeTimeout: 10 * time.Second, 401 ResponseHeaderTimeout: 10 * time.Second, 402 ExpectContinueTimeout: 1 * time.Second, 403 } 404 405 // if we have a proxy, then set it in the transport 406 if proxyURL != "" { 407 u := url.URL{} 408 proxy, err := u.Parse(proxyURL) 409 if err != nil { 410 return nil, "", err 411 } 412 tr.Proxy = http.ProxyURL(proxy) 413 } 414 415 client := &http.Client{Transport: tr, Timeout: 30 * time.Second} 416 var resp *http.Response 417 var err error 418 419 // resp.Body is consumed by the first try, and then no longer available (empty) 420 // so we need to read the body and save it so we can use it in each retry 421 buffer, _ := io.ReadAll(req.Body) 422 423 common.Retry(DefaultRetry, log, true, func() (bool, error) { 424 // update the body with the saved data to prevent the "zero length body" error 425 req.Body = io.NopCloser(bytes.NewBuffer(buffer)) 426 resp, err = RancherHTTPClient.Do(client, req) 427 428 // check for a network error and retry 429 if nerr, ok := err.(net.Error); ok && nerr.Timeout() { 430 log.Infof("Temporary error executing HTTP request %v : %v, retrying", req, nerr) 431 return false, err 432 } 433 434 // if err is another kind of network error that is not considered "temporary", then retry 435 if err, ok := err.(*url.Error); ok { 436 if err, ok := err.Err.(*net.OpError); ok { 437 if derr, ok := err.Err.(*net.DNSError); ok { 438 log.Infof("DNS error: %v, retrying", derr) 439 return false, err 440 } 441 } 442 } 443 444 // retry any HTTP 500 errors 445 if resp != nil && resp.StatusCode >= 500 && resp.StatusCode <= 599 { 446 log.ErrorfThrottled("HTTP status %v executing HTTP request %v, retrying", resp.StatusCode, req) 447 return false, err 448 } 449 450 // if err is some other kind of unexpected error, retry 451 if err != nil { 452 return false, err 453 } 454 return true, err 455 }) 456 457 if err != nil { 458 return resp, "", err 459 } 460 defer resp.Body.Close() 461 462 // extract the response body 463 body, err := io.ReadAll(resp.Body) 464 if err != nil { 465 return nil, "", err 466 } 467 468 return resp, string(body), err 469 } 470 471 // RancherIngressServiceHost returns the internal service host name of the Rancher ingress 472 func RancherIngressServiceHost() string { 473 return DefaultRancherIngressHostPrefix + nginxutil.IngressNGINXNamespace() 474 }