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 }