github.com/kyma-project/kyma-environment-broker@v0.0.1/common/orchestration/client.go (about) 1 package orchestration 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "net/http" 11 "net/url" 12 "path" 13 "strconv" 14 "strings" 15 16 "github.com/kyma-project/kyma-environment-broker/common/pagination" 17 "golang.org/x/oauth2" 18 ) 19 20 const defaultPageSize = 100 21 22 // Client is the interface to interact with the KEB /orchestrations and /upgrade API 23 // as an HTTP client using OIDC ID token in JWT format. 24 type Client interface { 25 ListOrchestrations(params ListParameters) (StatusResponseList, error) 26 GetOrchestration(orchestrationID string) (StatusResponse, error) 27 ListOperations(orchestrationID string, params ListParameters) (OperationResponseList, error) 28 GetOperation(orchestrationID, operationID string) (OperationDetailResponse, error) 29 UpgradeKyma(params Parameters) (UpgradeResponse, error) 30 UpgradeCluster(params Parameters) (UpgradeResponse, error) 31 CancelOrchestration(orchestrationID string) error 32 RetryOrchestration(orchestrationID string, operationIDs []string, now bool) (RetryResponse, error) 33 } 34 35 type client struct { 36 url string 37 httpClient *http.Client 38 } 39 40 // NewClient constructs and returns new Client for KEB /runtimes API 41 // It takes the following arguments: 42 // - ctx : context in which the http request will be executed 43 // - url : base url of all KEB APIs, e.g. https://kyma-env-broker.kyma.local 44 // - auth : TokenSource object which provides the ID token for the HTTP request 45 func NewClient(ctx context.Context, url string, auth oauth2.TokenSource) Client { 46 return &client{ 47 url: url, 48 httpClient: oauth2.NewClient(ctx, auth), 49 } 50 } 51 52 // ListOrchestrations fetches the orchestrations from KEB according to the given params. 53 // If params.Page or params.PageSize is not set (zero), the client will fetch and return all orchestrations. 54 func (c client) ListOrchestrations(params ListParameters) (StatusResponseList, error) { 55 orchestrations := StatusResponseList{} 56 getAll := false 57 fetchedAll := false 58 if params.Page == 0 || params.PageSize == 0 { 59 getAll = true 60 params.Page = 1 61 if params.PageSize == 0 { 62 params.PageSize = defaultPageSize 63 } 64 } 65 66 for !fetchedAll { 67 req, err := http.NewRequest("GET", fmt.Sprintf("%s/orchestrations", c.url), nil) 68 if err != nil { 69 return orchestrations, fmt.Errorf("while creating request: %w", err) 70 } 71 setQuery(req.URL, params) 72 73 resp, err := c.httpClient.Do(req) 74 if err != nil { 75 return orchestrations, fmt.Errorf("while calling %s: %w", req.URL.String(), err) 76 } 77 78 // Drain response body and close, return error to context if there isn't any. 79 defer func() { 80 derr := drainResponseBody(resp.Body) 81 if err == nil { 82 err = derr 83 } 84 cerr := resp.Body.Close() 85 if err == nil { 86 err = cerr 87 } 88 }() 89 90 if resp.StatusCode != http.StatusOK { 91 return orchestrations, fmt.Errorf("calling %s returned %s status", req.URL.String(), resp.Status) 92 } 93 94 var srl StatusResponseList 95 decoder := json.NewDecoder(resp.Body) 96 err = decoder.Decode(&srl) 97 if err != nil { 98 return orchestrations, fmt.Errorf("while decoding response body: %w", err) 99 } 100 101 orchestrations.TotalCount = srl.TotalCount 102 orchestrations.Count += srl.Count 103 orchestrations.Data = append(orchestrations.Data, srl.Data...) 104 if getAll { 105 params.Page++ 106 fetchedAll = orchestrations.Count >= orchestrations.TotalCount 107 } else { 108 fetchedAll = true 109 } 110 } 111 112 return orchestrations, nil 113 } 114 115 // GetOrchestration fetches one orchestration by the given ID. 116 func (c client) GetOrchestration(orchestrationID string) (StatusResponse, error) { 117 orchestration := StatusResponse{} 118 url := fmt.Sprintf("%s/orchestrations/%s", c.url, orchestrationID) 119 resp, err := c.httpClient.Get(url) 120 if err != nil { 121 return orchestration, fmt.Errorf("while calling %s: %w", url, err) 122 } 123 124 // Drain response body and close, return error to context if there isn't any. 125 defer func() { 126 derr := drainResponseBody(resp.Body) 127 if err == nil { 128 err = derr 129 } 130 cerr := resp.Body.Close() 131 if err == nil { 132 err = cerr 133 } 134 }() 135 136 if resp.StatusCode != http.StatusOK { 137 return orchestration, fmt.Errorf("calling %s returned %s status", url, resp.Status) 138 } 139 140 decoder := json.NewDecoder(resp.Body) 141 err = decoder.Decode(&orchestration) 142 if err != nil { 143 return orchestration, fmt.Errorf("while decoding response body: %w", err) 144 } 145 146 return orchestration, nil 147 } 148 149 // ListOperations fetches the Runtime operations of a given orchestration from KEB according to the given params. 150 // If params.Page or params.PageSize is not set (zero), the client will fetch and return all operations. 151 func (c client) ListOperations(orchestrationID string, params ListParameters) (OperationResponseList, error) { 152 operations := OperationResponseList{} 153 url := fmt.Sprintf("%s/orchestrations/%s/operations", c.url, orchestrationID) 154 getAll := false 155 fetchedAll := false 156 if params.Page == 0 || params.PageSize == 0 { 157 getAll = true 158 params.Page = 1 159 if params.PageSize == 0 { 160 params.PageSize = defaultPageSize 161 } 162 } 163 164 for !fetchedAll { 165 if params.Page > 1 { 166 failedFound, failedIndex := c.searchFilter(params.States, "failed") 167 if failedFound { 168 params.States = c.removeIndex(params.States, failedIndex) 169 } 170 } 171 172 req, err := http.NewRequest("GET", url, nil) 173 if err != nil { 174 return operations, fmt.Errorf("while creating request: %w", err) 175 } 176 setQuery(req.URL, params) 177 178 resp, err := c.httpClient.Do(req) 179 if err != nil { 180 return operations, fmt.Errorf("while calling %s: %w", url, err) 181 } 182 183 // Drain response body and close, return error to context if there isn't any. 184 defer func() { 185 derr := drainResponseBody(resp.Body) 186 if err == nil { 187 err = derr 188 } 189 cerr := resp.Body.Close() 190 if err == nil { 191 err = cerr 192 } 193 }() 194 195 if resp.StatusCode != http.StatusOK { 196 return operations, fmt.Errorf("calling %s returned %s status", url, resp.Status) 197 } 198 199 var orl OperationResponseList 200 decoder := json.NewDecoder(resp.Body) 201 err = decoder.Decode(&orl) 202 if err != nil { 203 return operations, fmt.Errorf("while decoding response body: %w", err) 204 } 205 206 operations.TotalCount = orl.TotalCount 207 operations.Count += orl.Count 208 209 operations.Data = append(operations.Data, orl.Data...) 210 if getAll { 211 params.Page++ 212 fetchedAll = operations.Count >= operations.TotalCount 213 } else { 214 fetchedAll = true 215 } 216 } 217 218 return operations, nil 219 } 220 221 func (c client) searchFilter(states []string, inputState string) (bool, int) { 222 var failedFound bool 223 var failedIndex int 224 for index, state := range states { 225 if strings.Contains(state, inputState) { 226 failedFound = true 227 failedIndex = index 228 break 229 } 230 } 231 return failedFound, failedIndex 232 } 233 234 func (c client) removeIndex(arr []string, index int) []string { 235 var temp = make([]string, len(arr)-1) 236 j := 0 237 for i := range arr { 238 if i != index { 239 temp[j] = arr[i] 240 j = j + 1 241 } 242 } 243 return temp 244 } 245 246 // GetOperation fetches detailed Runtime operation corresponding to the given orchestration and operation ID. 247 func (c client) GetOperation(orchestrationID, operationID string) (OperationDetailResponse, error) { 248 operation := OperationDetailResponse{} 249 url := fmt.Sprintf("%s/orchestrations/%s/operations/%s", c.url, orchestrationID, operationID) 250 resp, err := c.httpClient.Get(url) 251 if err != nil { 252 return operation, fmt.Errorf("while calling %s: %w", url, err) 253 } 254 255 // Drain response body and close, return error to context if there isn't any. 256 defer func() { 257 derr := drainResponseBody(resp.Body) 258 if err == nil { 259 err = derr 260 } 261 cerr := resp.Body.Close() 262 if err == nil { 263 err = cerr 264 } 265 }() 266 267 if resp.StatusCode != http.StatusOK { 268 return operation, fmt.Errorf("calling %s returned %s status", url, resp.Status) 269 } 270 271 decoder := json.NewDecoder(resp.Body) 272 err = decoder.Decode(&operation) 273 if err != nil { 274 return operation, fmt.Errorf("while decoding response body: %w", err) 275 } 276 277 return operation, nil 278 } 279 280 // UpgradeKyma creates a new Kyma upgrade orchestration according to the given orchestration parameters. 281 // If successful, the UpgradeResponse returned contains the ID of the newly created orchestration. 282 func (c client) UpgradeKyma(params Parameters) (UpgradeResponse, error) { 283 uri := "/upgrade/kyma" 284 285 ur, err := c.upgradeOperation(uri, params) 286 if err != nil { 287 return ur, fmt.Errorf("while calling kyma upgrade operation: %w", err) 288 } 289 290 return ur, nil 291 } 292 293 // UpgradeCluster creates a new Cluster upgrade orchestration according to the given orchestration parameters. 294 // If successful, the UpgradeResponse returned contains the ID of the newly created orchestration. 295 func (c client) UpgradeCluster(params Parameters) (UpgradeResponse, error) { 296 uri := "/upgrade/cluster" 297 298 ur, err := c.upgradeOperation(uri, params) 299 if err != nil { 300 return ur, fmt.Errorf("while calling cluster upgrade operation: %w", err) 301 } 302 303 return ur, nil 304 } 305 306 // common func trigger kyma or cluster upgrade 307 func (c client) upgradeOperation(uri string, params Parameters) (UpgradeResponse, error) { 308 ur := UpgradeResponse{} 309 blob, err := json.Marshal(params) 310 if err != nil { 311 return ur, fmt.Errorf("while converting upgrade parameters to JSON: %w", err) 312 } 313 314 u, err := url.Parse(c.url) 315 if err != nil { 316 return ur, fmt.Errorf("while parsing %s: %w", c.url, err) 317 } 318 u.Path = path.Join(u.Path, uri) 319 320 resp, err := c.httpClient.Post(u.String(), "application/json", bytes.NewBuffer(blob)) 321 if err != nil { 322 return ur, fmt.Errorf("while calling %s: %w", u, err) 323 } 324 325 // Drain response body and close, return error to context if there isn't any. 326 defer func() { 327 derr := drainResponseBody(resp.Body) 328 if err == nil { 329 err = derr 330 } 331 cerr := resp.Body.Close() 332 if err == nil { 333 err = cerr 334 } 335 }() 336 337 if resp.StatusCode != http.StatusAccepted { 338 return ur, fmt.Errorf("calling %s returned %s status", u, resp.Status) 339 } 340 341 decoder := json.NewDecoder(resp.Body) 342 err = decoder.Decode(&ur) 343 if err != nil { 344 return ur, fmt.Errorf("while decoding response body: %w", err) 345 } 346 347 return ur, nil 348 } 349 350 func (c client) RetryOrchestration(orchestrationID string, operationIDs []string, now bool) (RetryResponse, error) { 351 rr := RetryResponse{} 352 uri := fmt.Sprintf("%s/orchestrations/%s/retry", c.url, orchestrationID) 353 354 for i, id := range operationIDs { 355 operationIDs[i] = "operation-id=" + id 356 } 357 358 str := strings.Join(operationIDs, "&") 359 if now { 360 str = str + "&immediate=true" 361 } 362 body := strings.NewReader(str) 363 364 req, err := http.NewRequest(http.MethodPost, uri, body) 365 if err != nil { 366 return rr, fmt.Errorf("while creating retry request: %w", err) 367 } 368 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 369 370 resp, err := c.httpClient.Do(req) 371 if err != nil { 372 return rr, fmt.Errorf("while calling %s: %w", uri, err) 373 } 374 375 // Drain response body and close, return error to context if there isn't any. 376 defer func() { 377 derr := drainResponseBody(resp.Body) 378 if err == nil { 379 err = derr 380 } 381 cerr := resp.Body.Close() 382 if err == nil { 383 err = cerr 384 } 385 }() 386 387 if resp.StatusCode != http.StatusAccepted { 388 return rr, fmt.Errorf("calling %s returned %s status", uri, resp.Status) 389 } 390 391 decoder := json.NewDecoder(resp.Body) 392 err = decoder.Decode(&rr) 393 if err != nil { 394 return rr, fmt.Errorf("while decoding response body: %w", err) 395 } 396 397 return rr, nil 398 } 399 400 func (c client) CancelOrchestration(orchestrationID string) error { 401 url := fmt.Sprintf("%s/orchestrations/%s/cancel", c.url, orchestrationID) 402 403 req, err := http.NewRequest(http.MethodPut, url, nil) 404 if err != nil { 405 return fmt.Errorf("while creating cancel request: %w", err) 406 } 407 408 resp, err := c.httpClient.Do(req) 409 if err != nil { 410 return fmt.Errorf("while calling %s: %w", url, err) 411 } 412 413 // Drain response body and close, return error to context if there isn't any. 414 defer func() { 415 derr := drainResponseBody(resp.Body) 416 if err == nil { 417 err = derr 418 } 419 cerr := resp.Body.Close() 420 if err == nil { 421 err = cerr 422 } 423 }() 424 425 if resp.StatusCode != http.StatusOK { 426 return fmt.Errorf("calling %s returned %s status", url, resp.Status) 427 } 428 429 return nil 430 } 431 432 func setQuery(url *url.URL, params ListParameters) { 433 query := url.Query() 434 query.Add(pagination.PageParam, strconv.Itoa(params.Page)) 435 query.Add(pagination.PageSizeParam, strconv.Itoa(params.PageSize)) 436 setParamList(query, StateParam, params.States) 437 url.RawQuery = query.Encode() 438 } 439 440 func setParamList(query url.Values, key string, values []string) { 441 for _, value := range values { 442 query.Add(key, value) 443 } 444 } 445 446 func drainResponseBody(body io.Reader) error { 447 if body == nil { 448 return nil 449 } 450 _, err := io.Copy(ioutil.Discard, io.LimitReader(body, 4096)) 451 return err 452 }