github.com/verrazzano/verrazzano@v1.7.1/tests/e2e/pkg/api.go (about)

     1  // Copyright (c) 2020, 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  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"net/http"
    13  	"strings"
    14  
    15  	"github.com/verrazzano/verrazzano/pkg/k8sutil"
    16  	"github.com/verrazzano/verrazzano/platform-operator/constants"
    17  
    18  	"github.com/hashicorp/go-retryablehttp"
    19  	"github.com/onsi/gomega"
    20  	networkingv1 "k8s.io/api/networking/v1"
    21  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    22  )
    23  
    24  const (
    25  	// Username - the username of the Verrazzano admin user
    26  	Username               = "verrazzano"
    27  	realm                  = "verrazzano-system"
    28  	verrazzanoAPIURLPrefix = "20210501"
    29  )
    30  
    31  // APIEndpoint contains information needed to access an API
    32  type APIEndpoint struct {
    33  	AccessToken string `json:"access_token"`
    34  	APIURL      string
    35  	HTTPClient  *retryablehttp.Client
    36  }
    37  
    38  func EventuallyGetAPIEndpoint(kubeconfigPath string) *APIEndpoint {
    39  	var api *APIEndpoint
    40  	gomega.Eventually(func() (*APIEndpoint, error) {
    41  		var err error
    42  		api, err = GetAPIEndpoint(kubeconfigPath)
    43  		return api, err
    44  	}, waitTimeout, pollingInterval).ShouldNot(gomega.BeNil())
    45  	return api
    46  }
    47  
    48  // GetAPIEndpoint returns the APIEndpoint stub with AccessToken, from the given cluster
    49  func GetAPIEndpoint(kubeconfigPath string) (*APIEndpoint, error) {
    50  	var err error
    51  	api := APIEndpoint{}
    52  	token, err := getAccessToken(kubeconfigPath, IsDexEnabled(kubeconfigPath))
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  
    57  	api.AccessToken = *token
    58  	api.APIURL, err = getAPIURL(kubeconfigPath)
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  	api.HTTPClient, err = GetVerrazzanoHTTPClient(kubeconfigPath)
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  
    67  	return &api, nil
    68  }
    69  
    70  // getAPIURL returns the Verrazzano REST API URL for the cluster whose kubeconfig is given as argument
    71  func getAPIURL(kubeconfigPath string) (string, error) {
    72  	clientset, err := GetKubernetesClientsetForCluster(kubeconfigPath)
    73  	if err != nil {
    74  		return "", err
    75  	}
    76  
    77  	ingress, err := clientset.NetworkingV1().Ingresses("verrazzano-system").Get(context.TODO(), "verrazzano-ingress", v1.GetOptions{})
    78  	if err != nil {
    79  		return "", err
    80  	}
    81  	var ingressRules = ingress.Spec.Rules
    82  	return fmt.Sprintf("https://%s/%s", ingressRules[0].Host, verrazzanoAPIURLPrefix), nil
    83  }
    84  
    85  // Get Invoke GET API Request
    86  func (api *APIEndpoint) Get(path string) (*HTTPResponse, error) {
    87  	return api.Request(http.MethodGet, path, nil)
    88  }
    89  
    90  // Post Invoke POST API Request
    91  func (api *APIEndpoint) Post(path string, body io.Reader) (*HTTPResponse, error) {
    92  	return api.Request(http.MethodPost, path, body)
    93  }
    94  
    95  // Patch Invoke POST API Request
    96  func (api *APIEndpoint) Patch(path string, body io.Reader) (*HTTPResponse, error) {
    97  	return api.Request(http.MethodPut, path, body)
    98  }
    99  
   100  // Delete Invoke DELETE API Request
   101  func (api *APIEndpoint) Delete(path string) (*HTTPResponse, error) {
   102  	return api.Request(http.MethodDelete, path, nil)
   103  }
   104  
   105  // Request Invoke API
   106  func (api *APIEndpoint) Request(method, path string, body io.Reader) (*HTTPResponse, error) {
   107  	url := fmt.Sprintf("%s/%s", api.APIURL, path)
   108  	req, _ := retryablehttp.NewRequest(method, url, body)
   109  	if api.AccessToken != "" {
   110  		value := fmt.Sprintf("Bearer %v", api.AccessToken)
   111  		req.Header.Set("Authorization", value)
   112  	}
   113  	resp, err := api.HTTPClient.Do(req)
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	return ProcessHTTPResponse(resp)
   118  }
   119  
   120  // ProcessHTTPResponse processes the HTTP response by reading and closing the body, then returning
   121  // the HTTPResponse object.  This function is used to prevent file descriptor leaks
   122  // and other problems.
   123  // See https://github.com/golang/go/blob/master/src/net/http/response.go
   124  //
   125  // Params
   126  //
   127  //	resp: Http response returned by http call
   128  //	httpErr: Http error returned by the http call
   129  //
   130  // Returns
   131  //
   132  //	HttpReponse which has the body and status code.
   133  func ProcessHTTPResponse(resp *http.Response) (*HTTPResponse, error) {
   134  	// Must read entire body and close it.  See http.Response.Body doc
   135  	defer resp.Body.Close()
   136  	body, err := io.ReadAll(resp.Body)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	processedResponse := &HTTPResponse{
   141  		StatusCode: resp.StatusCode,
   142  		Header:     resp.Header,
   143  		Body:       body,
   144  	}
   145  	return processedResponse, nil
   146  }
   147  
   148  // GetIngress fetches ingress from api
   149  func (api *APIEndpoint) GetIngress(namespace, name string) (*networkingv1.Ingress, error) {
   150  	response, err := api.Get(fmt.Sprintf("apis/networking.k8s.io/v1/namespaces/%s/ingresses/%s", namespace, name))
   151  	if err != nil {
   152  		Log(Error, fmt.Sprintf("Error fetching ingress %s/%s from api, error: %v", namespace, name, err))
   153  		return nil, err
   154  	}
   155  	if response.StatusCode != http.StatusOK {
   156  		Log(Error, fmt.Sprintf("Error fetching ingress %s/%s from api, response: %v", namespace, name, response))
   157  		return nil, fmt.Errorf("unexpected HTTP status code: %d", response.StatusCode)
   158  	}
   159  
   160  	ingress := networkingv1.Ingress{}
   161  	err = json.Unmarshal(response.Body, &ingress)
   162  	if err != nil {
   163  		Log(Error, fmt.Sprintf("Invalid response for ingress %s/%s from api, error: %v", namespace, name, err))
   164  		return nil, err
   165  	}
   166  
   167  	return &ingress, nil
   168  }
   169  
   170  // GetOpensearchURL fetches OpenSearch endpoint URL
   171  func (api *APIEndpoint) GetOpensearchURL() (string, error) {
   172  	kubeconfigPath, err := k8sutil.GetKubeConfigLocation()
   173  	if err != nil {
   174  		return "", err
   175  	}
   176  	ok, _ := IsVerrazzanoMinVersion("1.7.0", kubeconfigPath)
   177  
   178  	var ingress *networkingv1.Ingress
   179  	if ok {
   180  		ingress, err = api.GetIngress("verrazzano-system", "opensearch")
   181  	} else {
   182  		ingress, err = api.GetIngress("verrazzano-system", "vmi-system-os-ingest")
   183  	}
   184  	if err != nil {
   185  		return "", err
   186  	}
   187  	return fmt.Sprintf("https://%s", ingress.Spec.Rules[0].Host), nil
   188  }
   189  
   190  // GetVerrazzanoIngressURL fetches Verrazzano-Ingress endpoint URL
   191  func (api *APIEndpoint) GetVerrazzanoIngressURL() (string, error) {
   192  	ingress, err := api.GetIngress("verrazzano-system", "verrazzano-ingress")
   193  	if err != nil {
   194  		return "", err
   195  	}
   196  	return fmt.Sprintf("https://%s", ingress.Spec.Rules[0].Host), nil
   197  }
   198  
   199  // EventuallyGetAccessToken eventually returns the AccessToken from the OIDC provider in the given cluster
   200  func EventuallyGetAccessToken(kubeconfigPath string, isOIDCProviderDex bool) *string {
   201  	var token *string
   202  	gomega.Eventually(func() (*string, error) {
   203  		var err error
   204  		token, err = getAccessToken(kubeconfigPath, isOIDCProviderDex)
   205  		return token, err
   206  	}, waitTimeout, pollingInterval).ShouldNot(gomega.BeNil())
   207  	return token
   208  }
   209  
   210  // getAccessToken returns the AccessToken from the OIDC provider in the given cluster
   211  func getAccessToken(kubeconfigPath string, isOIDCProviderDex bool) (*string, error) {
   212  	var err error
   213  	clientset, err := GetKubernetesClientsetForCluster(kubeconfigPath)
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  
   218  	var ingress *networkingv1.Ingress
   219  
   220  	if isOIDCProviderDex {
   221  		ingress, err = clientset.NetworkingV1().Ingresses(constants.DexNamespace).Get(context.TODO(), constants.DexIngress, v1.GetOptions{})
   222  	} else {
   223  		ingress, err = clientset.NetworkingV1().Ingresses(constants.KeycloakNamespace).Get(context.TODO(), constants.KeycloakIngress, v1.GetOptions{})
   224  	}
   225  
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  	oidcProviderHTTPClient, err := GetVerrazzanoHTTPClient(kubeconfigPath)
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  	var ingressRules = ingress.Spec.Rules
   234  	var oidcProviderTokenURL string
   235  	if isOIDCProviderDex {
   236  		oidcProviderTokenURL = fmt.Sprintf("https://%s/token", ingressRules[0].Host)
   237  	} else {
   238  		oidcProviderTokenURL = fmt.Sprintf("https://%s/auth/realms/%s/protocol/openid-connect/token", ingressRules[0].Host, realm)
   239  	}
   240  
   241  	password, err := GetVerrazzanoPassword()
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  
   246  	body := fmt.Sprintf("username=%s&password=%s&grant_type=password&client_id=%s", Username, password, verrazzanoPgClientID)
   247  	if isOIDCProviderDex {
   248  		body = fmt.Sprintf("%s&scope=openid+profile", body)
   249  	}
   250  
   251  	resp, err := doReq(oidcProviderTokenURL, "POST", "application/x-www-form-urlencoded", "", "", "", strings.NewReader(body), oidcProviderHTTPClient)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  	var api APIEndpoint
   256  	if resp.StatusCode == http.StatusOK {
   257  		json.Unmarshal([]byte(resp.Body), &api)
   258  	} else {
   259  		msg := fmt.Sprintf("error getting API access token from %s: %d", oidcProviderTokenURL, resp.StatusCode)
   260  		Log(Error, msg)
   261  		return nil, errors.New(msg)
   262  	}
   263  
   264  	return &api.AccessToken, nil
   265  }