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  }