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  }