github.com/IBM-Cloud/bluemix-go@v0.0.0-20240423071914-9e96525baef4/api/container/containerv2/openshift.go (about)

     1  package containerv2
     2  
     3  /*******************************************************************************
     4   * IBM Confidential
     5   * OCO Source Materials
     6   * IBM Cloud Schematics
     7   * (C) Copyright IBM Corp. 2017 All Rights Reserved.
     8   * The source code for this program is not  published or otherwise divested of
     9   * its trade secrets, irrespective of what has been deposited with
    10   * the U.S. Copyright Office.
    11   ******************************************************************************/
    12  
    13  /*******************************************************************************
    14   * A file for openshift related utility functions, like getting kube
    15   * config
    16   ******************************************************************************/
    17  
    18  import (
    19  	"encoding/base64"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"net/http"
    25  	"net/url"
    26  	"regexp"
    27  	"runtime/debug"
    28  	"strings"
    29  	"time"
    30  
    31  	yaml "github.com/ghodss/yaml"
    32  
    33  	"github.com/IBM-Cloud/bluemix-go/client"
    34  	bxhttp "github.com/IBM-Cloud/bluemix-go/http"
    35  	"github.com/IBM-Cloud/bluemix-go/rest"
    36  	"github.com/IBM-Cloud/bluemix-go/trace"
    37  )
    38  
    39  const (
    40  	// IAMHTTPtimeout -
    41  	IAMHTTPtimeout            = 10 * time.Second
    42  	VirtualPrivateEndpoint    = "vpe"
    43  	PrivateServiceEndpoint    = "private"
    44  	VirtualPrivateEndpointDNS = ".vpe.private"
    45  	PrivateEndpointDNS        = ".private"
    46  )
    47  
    48  // Frame -
    49  type Frame uintptr
    50  
    51  // StackTrace -
    52  type StackTrace []Frame
    53  type stackTracer interface {
    54  	StackTrace() StackTrace
    55  }
    56  
    57  type openShiftUser struct {
    58  	Kind       string `json:"kind"`
    59  	APIVersion string `json:"apiVersion"`
    60  	Metadata   struct {
    61  		Name              string    `json:"name"`
    62  		SelfLink          string    `json:"selfLink"`
    63  		UID               string    `json:"uid"`
    64  		ResourceVersion   string    `json:"resourceVersion"`
    65  		CreationTimestamp time.Time `json:"creationTimestamp"`
    66  	} `json:"metadata"`
    67  	Identities []string `json:"identities"`
    68  	Groups     []string `json:"groups"`
    69  }
    70  
    71  type authEndpoints struct {
    72  	Issuer                string `json:"issuer"`
    73  	AuthorizationEndpoint string `json:"authorization_endpoint"`
    74  	TokenEndpoint         string `json:"token_endpoint"`
    75  	ServerURL             string `json:"server_endpoint,omitempty"`
    76  }
    77  
    78  // PanicCatch - Catch panic and give error
    79  func PanicCatch(r interface{}) error {
    80  	if r != nil {
    81  		var e error
    82  		switch x := r.(type) {
    83  		case string:
    84  			e = errors.New(x)
    85  		case error:
    86  			e = x
    87  		default:
    88  			e = errors.New("Unknown panic")
    89  		}
    90  		fmt.Printf("Panic error %v", e)
    91  		if err, ok := e.(stackTracer); ok {
    92  			fmt.Printf("Panic stack trace %v", err.StackTrace())
    93  		} else {
    94  			debug.PrintStack()
    95  		}
    96  		return e
    97  	}
    98  	return nil
    99  }
   100  
   101  // NormalizeName -
   102  func NormalizeName(name string) (string, error) {
   103  	name = strings.ToLower(name)
   104  	reg, err := regexp.Compile("[^A-Za-z0-9:]+")
   105  	if err != nil {
   106  		return "", err
   107  	}
   108  	return reg.ReplaceAllString(name, "-"), nil
   109  }
   110  
   111  // logInAndFillOCToken will update kubeConfig with an Openshift token, if one is not there
   112  func (r *clusters) FetchOCTokenForKubeConfig(kubecfg []byte, cMeta *ClusterInfo, skipSSLVerification bool, endpointType string) (kubecfgEdited []byte, host string, rerr error) {
   113  	// TODO: this is not a a standard manner to login ... using propriatary OC cli reverse engineering
   114  	defer func() {
   115  		err := PanicCatch(recover())
   116  		if err != nil {
   117  			rerr = fmt.Errorf("could not login to openshift account %s", err)
   118  		}
   119  	}()
   120  
   121  	var cfg map[string]interface{}
   122  	err := yaml.Unmarshal(kubecfg, &cfg)
   123  	if err != nil {
   124  		return kubecfg, "", err
   125  	}
   126  	var token, passcode string
   127  	if r.client.Config.BluemixAPIKey == "" {
   128  		trace.Logger.Println("Creating user passcode to login for getting oc token")
   129  
   130  		// Retry to cover rate limiting on passcode endpoint in particular
   131  		for try := 1; try <= 3; try++ {
   132  			passcode, err = r.client.TokenRefresher.GetPasscode()
   133  
   134  			if err == nil {
   135  				break
   136  			}
   137  
   138  			if err != nil && try == 3 {
   139  				return kubecfg, "", err
   140  			}
   141  
   142  			time.Sleep(1 * time.Second)
   143  		}
   144  	}
   145  
   146  	// honor the endpointType parameter if the current parameter is different
   147  	switch endpointType {
   148  	case PrivateServiceEndpoint:
   149  		if !strings.Contains(cMeta.ServerURL, PrivateEndpointDNS) || strings.Contains(cMeta.ServerURL, VirtualPrivateEndpointDNS) {
   150  			// Could be changed to private only if the cluster's private service endpoint is enabled and public is enabled
   151  			if cMeta.ServiceEndpoints.PrivateServiceEndpointEnabled && cMeta.ServiceEndpoints.PrivateServiceEndpointURL != "" && !cMeta.ServiceEndpoints.PublicServiceEndpointEnabled {
   152  				// As this is Openshift, we need to use the URL with the signed certificate (-e) (the right URL is not available in getCluster response)
   153  				urlParts := strings.Split(cMeta.ServiceEndpoints.PrivateServiceEndpointURL, ".")
   154  				cMeta.ServerURL = urlParts[0] + "-e." + strings.Join(urlParts[1:], ".")
   155  			} else {
   156  				trace.Logger.Println("Ignore endpoint parameter and use default ServerURL - currently unsupported scenario")
   157  			}
   158  		}
   159  	case VirtualPrivateEndpoint:
   160  		if !strings.Contains(cMeta.ServerURL, VirtualPrivateEndpointDNS) && !cMeta.ServiceEndpoints.PublicServiceEndpointEnabled {
   161  			if cMeta.VirtualPrivateEndpointURL != "" {
   162  				cMeta.ServerURL = cMeta.VirtualPrivateEndpointURL
   163  			} else {
   164  				return kubecfg, "", fmt.Errorf("virtual private endpoint is not supported by the cluster")
   165  			}
   166  		}
   167  	}
   168  
   169  	authEP, err := func(meta *ClusterInfo) (*authEndpoints, error) {
   170  		request := rest.GetRequest(meta.ServerURL + "/.well-known/oauth-authorization-server")
   171  		var auth authEndpoints
   172  
   173  		// Create new REST client - reusing modified existing client instances could lead to race conditions
   174  		restClient := &rest.Client{}
   175  		resp, err := restClient.Do(request, &auth, nil)
   176  
   177  		if err != nil {
   178  			return &auth, err
   179  		}
   180  		defer resp.Body.Close()
   181  		if resp.StatusCode > 299 {
   182  			msg, _ := ioutil.ReadAll(resp.Body)
   183  			return nil, fmt.Errorf("bad status code [%d] returned when fetching Cluster authentication endpoints: %s", resp.StatusCode, msg)
   184  		}
   185  		if endpointType != "" {
   186  			auth.AuthorizationEndpoint, err = reconfigureAuthorizationEndpoint(auth.AuthorizationEndpoint, endpointType, meta)
   187  			if err != nil {
   188  				return &auth, err
   189  			}
   190  		}
   191  		auth.ServerURL = meta.ServerURL
   192  		return &auth, nil
   193  	}(cMeta)
   194  
   195  	if err != nil {
   196  		return kubecfg, "", err
   197  	}
   198  
   199  	trace.Logger.Println("Got authentication endpoints for getting oc token")
   200  	token, uname, err := r.openShiftAuthorizePasscode(authEP, passcode, cMeta.IsStagingSatelliteCluster())
   201  
   202  	if err != nil {
   203  		return kubecfg, "", err
   204  	}
   205  
   206  	trace.Logger.Println("Got the token and user ", uname)
   207  	clusterName, _ := NormalizeName(authEP.ServerURL[len("https://"):len(authEP.ServerURL)]) //TODO deal with http
   208  	ccontext := "default/" + clusterName + "/" + uname
   209  	uname = uname + "/" + clusterName
   210  	clusters := cfg["clusters"].([]interface{})
   211  	newCluster := map[string]interface{}{"name": clusterName, "cluster": map[string]interface{}{"server": authEP.ServerURL}}
   212  	if skipSSLVerification {
   213  		newCluster["cluster"].(map[string]interface{})["insecure-skip-tls-verify"] = true
   214  	}
   215  	clusters = append(clusters, newCluster)
   216  	cfg["clusters"] = clusters
   217  
   218  	contexts := cfg["contexts"].([]interface{})
   219  	newContext := map[string]interface{}{"name": ccontext, "context": map[string]interface{}{"cluster": clusterName, "namespace": "default", "user": uname}}
   220  	contexts = append(contexts, newContext)
   221  	cfg["contexts"] = contexts
   222  
   223  	users := cfg["users"].([]interface{})
   224  	newUser := map[string]interface{}{"name": uname, "user": map[string]interface{}{"token": token}}
   225  	users = append(users, newUser)
   226  	cfg["users"] = users
   227  
   228  	cfg["current-context"] = ccontext
   229  
   230  	bytes, err := yaml.Marshal(cfg)
   231  	if err != nil {
   232  		return kubecfg, "", err
   233  	}
   234  	kubecfg = bytes
   235  	return kubecfg, cMeta.ServerURL, nil
   236  }
   237  
   238  // Never redirect. Let caller handle. This is an http.Client callback method (CheckRedirect)
   239  func neverRedirect(req *http.Request, via []*http.Request) error {
   240  	return http.ErrUseLastResponse
   241  }
   242  
   243  func (r *clusters) openShiftAuthorizePasscode(authEP *authEndpoints, passcode string, skipSSLVerification bool) (string, string, error) {
   244  	var request *rest.Request
   245  	authString := "passcode:" + passcode
   246  	if r.client.Config.BluemixAPIKey != "" {
   247  		apikey := r.client.Config.BluemixAPIKey
   248  		authString = "apikey:" + apikey
   249  	}
   250  	request = rest.GetRequest(authEP.AuthorizationEndpoint+"?response_type=token&client_id=openshift-challenging-client").
   251  		Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(authString)))
   252  	// Creating a new client instance (instead of tempering with existing one) to avoid race conditions
   253  	copyConfig := r.client.Config.Copy()
   254  	copyConfig.SSLDisable = skipSSLVerification
   255  	copyConfig.HTTPClient = bxhttp.NewHTTPClient(copyConfig)
   256  	copyConfig.HTTPClient.CheckRedirect = neverRedirect
   257  
   258  	client := client.New(copyConfig, r.client.ServiceName, r.client.TokenRefresher)
   259  
   260  	var respInterface interface{}
   261  	var resp *http.Response
   262  	var err error
   263  	for try := 1; try <= 3; try++ {
   264  		// bmxerror.NewRequestFailure("ServerErrorResponse", string(raw), resp.StatusCode)
   265  		resp, err = client.SendRequest(request, respInterface)
   266  		if err != nil {
   267  			if resp.StatusCode != 302 {
   268  				return "", "", err
   269  			}
   270  		}
   271  		defer resp.Body.Close()
   272  		if resp.StatusCode > 399 {
   273  			if try >= 3 {
   274  				msg, _ := io.ReadAll(resp.Body)
   275  				return "", "", fmt.Errorf("bad status code [%d] returned when openshift login: %s", resp.StatusCode, string(msg))
   276  			}
   277  			time.Sleep(200 * time.Millisecond)
   278  		} else {
   279  			break
   280  		}
   281  	}
   282  
   283  	loc, err := resp.Location()
   284  	if err != nil {
   285  		return "", "", err
   286  	}
   287  	val, err := url.ParseQuery(loc.Fragment)
   288  	if err != nil {
   289  		return "", "", err
   290  	}
   291  	token := val.Get("access_token")
   292  	trace.Logger.Println("Getting username after getting the token")
   293  	name, err := r.getOpenShiftUser(authEP, token)
   294  	if err != nil {
   295  		return "", "", err
   296  	}
   297  	return token, name, nil
   298  }
   299  
   300  func (r *clusters) getOpenShiftUser(authEP *authEndpoints, token string) (string, error) {
   301  	request := rest.GetRequest(authEP.ServerURL+"/apis/user.openshift.io/v1/users/~").
   302  		Set("Authorization", "Bearer "+token)
   303  
   304  	var user openShiftUser
   305  	resp, err := r.client.SendRequest(request, &user)
   306  	if err != nil {
   307  		return "", err
   308  	}
   309  	defer resp.Body.Close()
   310  	if resp.StatusCode > 299 {
   311  		msg, _ := io.ReadAll(resp.Body)
   312  		return "", fmt.Errorf("bad status code [%d] returned when fetching OpenShift user Details: %s", resp.StatusCode, string(msg))
   313  	}
   314  
   315  	return user.Metadata.Name, nil
   316  }
   317  
   318  // honor endpointType for OauthServer if the current parameter is different
   319  func reconfigureAuthorizationEndpoint(originalAuthEndpoint string, endpointType string, clusterInfo *ClusterInfo) (string, error) {
   320  	urlDefault, err := url.ParseRequestURI(originalAuthEndpoint)
   321  	if err != nil || urlDefault.Host == "" {
   322  		return "", fmt.Errorf("could not parse original auth endpoint raw url: %s, error: %v", originalAuthEndpoint, err)
   323  	}
   324  	switch endpointType {
   325  	case PrivateServiceEndpoint:
   326  		if (!strings.Contains(originalAuthEndpoint, PrivateEndpointDNS) || strings.Contains(originalAuthEndpoint, VirtualPrivateEndpointDNS)) &&
   327  			!clusterInfo.ServiceEndpoints.PublicServiceEndpointEnabled &&
   328  			clusterInfo.ServiceEndpoints.PrivateServiceEndpointEnabled {
   329  			urlPrivate, err := url.ParseRequestURI(clusterInfo.ServiceEndpoints.PrivateServiceEndpointURL)
   330  			if err != nil || urlPrivate.Host == "" {
   331  				return "", fmt.Errorf("could not parse private service endpoint raw url, cluster may not support it: %s, error: %v", clusterInfo.ServiceEndpoints.PrivateServiceEndpointURL, err)
   332  			}
   333  			// As this is Openshift, we need to use the URL with the signed certificate (the right URL is not available in getCluster response)
   334  			hostNameParts := strings.Split(urlPrivate.Hostname(), ".")
   335  			hostName := hostNameParts[0] + "-e." + strings.Join(hostNameParts[1:], ".")
   336  
   337  			u := url.URL{
   338  				Scheme: urlDefault.Scheme,
   339  				Host:   hostName + ":" + urlDefault.Port(),
   340  				Path:   urlDefault.Path,
   341  			}
   342  			return u.String(), nil
   343  		} else {
   344  			trace.Logger.Println("Ignore endpoint parameter and use default OauthServerURL - currently unsupported scenario")
   345  		}
   346  	case VirtualPrivateEndpoint:
   347  		if !strings.Contains(originalAuthEndpoint, VirtualPrivateEndpointDNS) && !clusterInfo.ServiceEndpoints.PublicServiceEndpointEnabled {
   348  			urlVPE, err := url.ParseRequestURI(clusterInfo.VirtualPrivateEndpointURL)
   349  			if err != nil || urlVPE.Host == "" {
   350  				return "", fmt.Errorf("could not parse virtual private endpoint raw url, cluster may not support it: %s, error: %v", clusterInfo.VirtualPrivateEndpointURL, err)
   351  			}
   352  			u := url.URL{
   353  				Scheme: urlDefault.Scheme,
   354  				Host:   urlVPE.Hostname() + ":" + urlDefault.Port(),
   355  				Path:   urlDefault.Path,
   356  			}
   357  			return u.String(), nil
   358  		}
   359  	}
   360  	return originalAuthEndpoint, nil
   361  }