github.com/kyma-project/kyma-environment-broker@v0.0.1/internal/broker/client.go (about) 1 package broker 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "time" 11 12 "github.com/kyma-project/kyma-environment-broker/internal" 13 "golang.org/x/oauth2/clientcredentials" 14 15 log "github.com/sirupsen/logrus" 16 ) 17 18 const ( 19 kymaClassID = "47c9dcbf-ff30-448e-ab36-d3bad66ba281" 20 AccountCleanupJob = "accountcleanup-job" 21 22 instancesURL = "/oauth/v2/service_instances" 23 deprovisionTmpl = "%s%s/%s?service_id=%s&plan_id=%s" 24 updateInstanceTmpl = "%s%s/%s" 25 getInstanceTmpl = "%s%s/%s" 26 ) 27 28 type ( 29 contextDTO struct { 30 GlobalAccountID string `json:"globalaccount_id"` 31 SubAccountID string `json:"subaccount_id"` 32 Active *bool `json:"active"` 33 } 34 35 parametersDTO struct { 36 Expired *bool `json:"expired"` 37 } 38 39 serviceUpdatePatchDTO struct { 40 ServiceID string `json:"service_id"` 41 PlanID string `json:"plan_id"` 42 Context contextDTO `json:"context"` 43 Parameters parametersDTO `json:"parameters"` 44 } 45 46 serviceInstancesResponseDTO struct { 47 Operation string `json:"operation"` 48 } 49 50 errorResponse struct { 51 Error string `json:"error"` 52 Description string `json:"description"` 53 } 54 ) 55 56 type ClientConfig struct { 57 URL string 58 TokenURL string `envconfig:"optional"` 59 ClientID string `envconfig:"optional"` 60 ClientSecret string `envconfig:"optional"` 61 Scope string `envconfig:"optional"` 62 } 63 64 type Client struct { 65 brokerConfig ClientConfig 66 httpClient *http.Client 67 poller Poller 68 UserAgent string 69 } 70 71 func NewClientConfig(URL string) *ClientConfig { 72 return &ClientConfig{ 73 URL: URL, 74 } 75 } 76 77 func NewClient(ctx context.Context, config ClientConfig) *Client { 78 return NewClientWithPoller(ctx, config, NewDefaultPoller()) 79 } 80 81 func NewClientWithPoller(ctx context.Context, config ClientConfig, poller Poller) *Client { 82 if config.TokenURL == "" { 83 return &Client{ 84 brokerConfig: config, 85 httpClient: http.DefaultClient, 86 poller: poller, 87 } 88 } 89 cfg := clientcredentials.Config{ 90 ClientID: config.ClientID, 91 ClientSecret: config.ClientSecret, 92 TokenURL: config.TokenURL, 93 Scopes: []string{config.Scope}, 94 } 95 httpClientOAuth := cfg.Client(ctx) 96 httpClientOAuth.Timeout = 30 * time.Second 97 98 return &Client{ 99 brokerConfig: config, 100 httpClient: httpClientOAuth, 101 poller: poller, 102 } 103 } 104 105 // Deprovision requests Runtime deprovisioning in KEB with given details 106 func (c *Client) Deprovision(instance internal.Instance) (string, error) { 107 deprovisionURL, err := c.formatDeprovisionUrl(instance) 108 if err != nil { 109 return "", err 110 } 111 112 response := serviceInstancesResponseDTO{} 113 log.Infof("Requesting deprovisioning of the environment with instance id: %q", instance.InstanceID) 114 err = c.poller.Invoke(func() (bool, error) { 115 err := c.executeRequestWithPoll(http.MethodDelete, deprovisionURL, http.StatusAccepted, nil, &response) 116 if err != nil { 117 log.Warn(fmt.Sprintf("while executing request: %s", err.Error())) 118 return false, nil 119 } 120 return true, nil 121 }) 122 123 if err != nil { 124 return "", fmt.Errorf("while waiting for successful deprovision call: %w", err) 125 } 126 127 return response.Operation, nil 128 } 129 130 // SendExpirationRequest requests Runtime suspension due to expiration 131 func (c *Client) SendExpirationRequest(instance internal.Instance) (suspensionUnderWay bool, err error) { 132 request, err := preparePatchRequest(instance, c.brokerConfig.URL) 133 if err != nil { 134 return false, err 135 } 136 137 resp, err := c.httpClient.Do(request) 138 if err != nil { 139 return false, fmt.Errorf("while executing request URL: %s for instanceID: %s: %w", request.URL, 140 instance.InstanceID, err) 141 } 142 defer c.warnOnError(resp.Body.Close) 143 144 return processResponse(instance.InstanceID, resp.StatusCode, resp) 145 } 146 147 func (c *Client) GetInstanceRequest(instanceID string) (response *http.Response, err error) { 148 request, err := prepareGetRequest(instanceID, c.brokerConfig.URL) 149 if err != nil { 150 return nil, err 151 } 152 153 resp, err := c.httpClient.Do(request) 154 if err != nil { 155 return nil, fmt.Errorf("while executing request URL: %s for instanceID: %s: %w", request.URL, 156 instanceID, err) 157 } 158 defer c.warnOnError(resp.Body.Close) 159 160 return resp, nil 161 } 162 163 func processResponse(instanceID string, statusCode int, resp *http.Response) (suspensionUnderWay bool, err error) { 164 switch statusCode { 165 case http.StatusAccepted, http.StatusOK: 166 { 167 log.Infof("Request for instanceID: %s accepted with status: %+v", instanceID, statusCode) 168 operation, err := decodeOperation(resp) 169 if err != nil { 170 return false, err 171 } 172 log.Infof("For instanceID: %s we received operation: %s", instanceID, operation) 173 return true, nil 174 } 175 case http.StatusUnprocessableEntity: 176 { 177 log.Warnf("For instanceID: %s we received entity unprocessable - status: %+v", instanceID, statusCode) 178 description, errorString, err := decodeErrorResponse(resp) 179 if err != nil { 180 return false, fmt.Errorf("for instanceID: %s: %w", instanceID, err) 181 } 182 log.Warnf("error: %+v description: %+v instanceID: %s", errorString, description, instanceID) 183 return false, nil 184 } 185 default: 186 { 187 if statusCode >= 200 && statusCode <= 299 { 188 return false, fmt.Errorf("for instanceID: %s we received unexpected status: %+v", instanceID, statusCode) 189 } 190 description, errorString, err := decodeErrorResponse(resp) 191 if err != nil { 192 return false, fmt.Errorf("for instanceID: %s: %w", instanceID, err) 193 } 194 return false, fmt.Errorf("error: %+v description: %+v instanceID: %s", errorString, description, instanceID) 195 } 196 } 197 } 198 199 func decodeOperation(resp *http.Response) (string, error) { 200 response := serviceInstancesResponseDTO{} 201 err := json.NewDecoder(resp.Body).Decode(&response) 202 if err != nil { 203 return "", fmt.Errorf("while decoding response body: %w", err) 204 } 205 return response.Operation, nil 206 } 207 208 func decodeErrorResponse(resp *http.Response) (string, string, error) { 209 response := errorResponse{} 210 err := json.NewDecoder(resp.Body).Decode(&response) 211 if err != nil { 212 return "", "", fmt.Errorf("while decoding error response body: %w", err) 213 } 214 return response.Description, response.Error, nil 215 } 216 217 func preparePatchRequest(instance internal.Instance, brokerConfigURL string) (*http.Request, error) { 218 updateInstanceUrl := fmt.Sprintf(updateInstanceTmpl, brokerConfigURL, instancesURL, instance.InstanceID) 219 220 jsonPayload, err := preparePayload(instance) 221 if err != nil { 222 return nil, fmt.Errorf("while marshaling payload for instanceID: %s: %w", instance.InstanceID, err) 223 } 224 225 log.Infof("Requesting expiration of the environment with instanceID: %q", instance.InstanceID) 226 227 request, err := http.NewRequest(http.MethodPatch, updateInstanceUrl, bytes.NewBuffer(jsonPayload)) 228 if err != nil { 229 return nil, fmt.Errorf("while creating request for instanceID: %s: %w", instance.InstanceID, err) 230 } 231 request.Header.Set("X-Broker-API-Version", "2.14") 232 return request, nil 233 } 234 235 func prepareGetRequest(instanceID string, brokerConfigURL string) (*http.Request, error) { 236 getInstanceUrl := fmt.Sprintf(getInstanceTmpl, brokerConfigURL, instancesURL, instanceID) 237 238 request, err := http.NewRequest(http.MethodGet, getInstanceUrl, nil) 239 if err != nil { 240 return nil, fmt.Errorf("while creating GET request for instanceID: %s: %w", instanceID, err) 241 } 242 request.Header.Set("X-Broker-API-Version", "2.14") 243 return request, nil 244 } 245 246 func preparePayload(instance internal.Instance) ([]byte, error) { 247 expired := true 248 active := false 249 payload := serviceUpdatePatchDTO{ 250 ServiceID: KymaServiceID, 251 PlanID: instance.ServicePlanID, 252 Context: contextDTO{ 253 GlobalAccountID: instance.SubscriptionGlobalAccountID, 254 SubAccountID: instance.SubAccountID, 255 Active: &active}, 256 Parameters: parametersDTO{Expired: &expired}} 257 jsonPayload, err := json.Marshal(payload) 258 return jsonPayload, err 259 } 260 261 func (c *Client) formatDeprovisionUrl(instance internal.Instance) (string, error) { 262 if len(instance.ServicePlanID) == 0 { 263 return "", fmt.Errorf("empty ServicePlanID") 264 } 265 266 return fmt.Sprintf(deprovisionTmpl, c.brokerConfig.URL, instancesURL, instance.InstanceID, kymaClassID, instance.ServicePlanID), nil 267 } 268 269 func (c *Client) executeRequestWithPoll(method, url string, expectedStatus int, body io.Reader, responseBody interface{}) error { 270 request, err := http.NewRequest(method, url, body) 271 if err != nil { 272 return fmt.Errorf("while creating request for provisioning: %w", err) 273 } 274 request.Header.Set("X-Broker-API-Version", "2.14") 275 if len(c.UserAgent) != 0 { 276 request.Header.Set("User-Agent", c.UserAgent) 277 } 278 279 resp, err := c.httpClient.Do(request) 280 if err != nil { 281 return fmt.Errorf("while executing request URL: %s: %w", url, err) 282 } 283 defer c.warnOnError(resp.Body.Close) 284 if resp.StatusCode != expectedStatus { 285 return fmt.Errorf("got unexpected status code while calling Kyma Environment Broker: want: %d, got: %d", 286 expectedStatus, resp.StatusCode) 287 } 288 289 err = json.NewDecoder(resp.Body).Decode(responseBody) 290 if err != nil { 291 return fmt.Errorf("while decoding body: %w", err) 292 } 293 294 return nil 295 } 296 297 func (c *Client) warnOnError(do func() error) { 298 if err := do(); err != nil { 299 log.Warn(err.Error()) 300 } 301 } 302 303 // setHttpClient auxiliary method of testing to get rid of oAuth client wrapper 304 func (c *Client) setHttpClient(httpClient *http.Client) { 305 c.httpClient = httpClient 306 }