github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/cmd/integrationArtifactDeploy.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"net/http"
     7  	"time"
     8  
     9  	"github.com/Jeffail/gabs/v2"
    10  	"github.com/SAP/jenkins-library/pkg/command"
    11  	"github.com/SAP/jenkins-library/pkg/cpi"
    12  	piperhttp "github.com/SAP/jenkins-library/pkg/http"
    13  	"github.com/SAP/jenkins-library/pkg/log"
    14  	"github.com/SAP/jenkins-library/pkg/telemetry"
    15  	"github.com/pkg/errors"
    16  )
    17  
    18  const retryCount = 14
    19  
    20  type integrationArtifactDeployUtils interface {
    21  	command.ExecRunner
    22  
    23  	// Add more methods here, or embed additional interfaces, or remove/replace as required.
    24  	// The integrationArtifactDeployUtils interface should be descriptive of your runtime dependencies,
    25  	// i.e. include everything you need to be able to mock in tests.
    26  	// Unit tests shall be executable in parallel (not depend on global state), and don't (re-)test dependencies.
    27  }
    28  
    29  type integrationArtifactDeployUtilsBundle struct {
    30  	*command.Command
    31  
    32  	// Embed more structs as necessary to implement methods or interfaces you add to integrationArtifactDeployUtils.
    33  	// Structs embedded in this way must each have a unique set of methods attached.
    34  	// If there is no struct which implements the method you need, attach the method to
    35  	// integrationArtifactDeployUtilsBundle and forward to the implementation of the dependency.
    36  }
    37  
    38  func newIntegrationArtifactDeployUtils() integrationArtifactDeployUtils {
    39  	utils := integrationArtifactDeployUtilsBundle{
    40  		Command: &command.Command{},
    41  	}
    42  	// Reroute command output to logging framework
    43  	utils.Stdout(log.Writer())
    44  	utils.Stderr(log.Writer())
    45  	return &utils
    46  }
    47  
    48  func integrationArtifactDeploy(config integrationArtifactDeployOptions, telemetryData *telemetry.CustomData) {
    49  	// Utils can be used wherever the command.ExecRunner interface is expected.
    50  	// It can also be used for example as a mavenExecRunner.
    51  	utils := newIntegrationArtifactDeployUtils()
    52  	utils.Stdout(log.Writer())
    53  	httpClient := &piperhttp.Client{}
    54  
    55  	// For HTTP calls import  piperhttp "github.com/SAP/jenkins-library/pkg/http"
    56  	// and use a  &piperhttp.Client{} in a custom system
    57  	// Example: step checkmarxExecuteScan.go
    58  
    59  	// Error situations should be bubbled up until they reach the line below which will then stop execution
    60  	// through the log.Entry().Fatal() call leading to an os.Exit(1) in the end.
    61  	err := runIntegrationArtifactDeploy(&config, telemetryData, httpClient)
    62  	if err != nil {
    63  		log.Entry().WithError(err).Fatal("step execution failed")
    64  	}
    65  }
    66  
    67  func runIntegrationArtifactDeploy(config *integrationArtifactDeployOptions, telemetryData *telemetry.CustomData, httpClient piperhttp.Sender) error {
    68  	clientOptions := piperhttp.ClientOptions{}
    69  	header := make(http.Header)
    70  	header.Add("Accept", "application/json")
    71  	serviceKey, err := cpi.ReadCpiServiceKey(config.APIServiceKey)
    72  	if err != nil {
    73  		return err
    74  	}
    75  	deployURL := fmt.Sprintf("%s/api/v1/DeployIntegrationDesigntimeArtifact?Id='%s'&Version='%s'", serviceKey.OAuth.Host, config.IntegrationFlowID, "Active")
    76  
    77  	tokenParameters := cpi.TokenParameters{TokenURL: serviceKey.OAuth.OAuthTokenProviderURL, Username: serviceKey.OAuth.ClientID, Password: serviceKey.OAuth.ClientSecret, Client: httpClient}
    78  	token, err := cpi.CommonUtils.GetBearerToken(tokenParameters)
    79  	if err != nil {
    80  		return errors.Wrap(err, "failed to fetch Bearer Token")
    81  	}
    82  	clientOptions.Token = fmt.Sprintf("Bearer %s", token)
    83  	httpClient.SetOptions(clientOptions)
    84  	httpMethod := "POST"
    85  	deployResp, httpErr := httpClient.SendRequest(httpMethod, deployURL, nil, header, nil)
    86  	if httpErr != nil {
    87  		return errors.Wrapf(httpErr, "HTTP %v request to %v failed with error", httpMethod, deployURL)
    88  	}
    89  
    90  	if deployResp != nil && deployResp.Body != nil {
    91  		defer deployResp.Body.Close()
    92  	}
    93  
    94  	if deployResp == nil {
    95  		return errors.Errorf("did not retrieve a HTTP response")
    96  	}
    97  
    98  	if deployResp.StatusCode == http.StatusAccepted {
    99  		log.Entry().
   100  			WithField("IntegrationFlowID", config.IntegrationFlowID).
   101  			Info("successfully deployed into CPI runtime")
   102  		taskId, readErr := io.ReadAll(deployResp.Body)
   103  		if readErr != nil {
   104  			return errors.Wrap(readErr, "Task Id not found. HTTP response body could not be read.")
   105  		}
   106  		deploymentError := pollIFlowDeploymentStatus(string(taskId), retryCount, config, httpClient, serviceKey.OAuth.Host)
   107  		return deploymentError
   108  	}
   109  	responseBody, readErr := io.ReadAll(deployResp.Body)
   110  
   111  	if readErr != nil {
   112  		return errors.Wrapf(readErr, "HTTP response body could not be read, response status code: %v", deployResp.StatusCode)
   113  	}
   114  	log.Entry().Errorf("a HTTP error occurred! Response body: %v, Response status code : %v", string(responseBody), deployResp.StatusCode)
   115  	return errors.Errorf("integration flow deployment failed, response Status code: %v", deployResp.StatusCode)
   116  }
   117  
   118  // pollIFlowDeploymentStatus - Poll the integration flow deployment status, return status or error details
   119  func pollIFlowDeploymentStatus(taskId string, retryCount int, config *integrationArtifactDeployOptions, httpClient piperhttp.Sender, apiHost string) error {
   120  
   121  	if retryCount <= 0 {
   122  		return errors.New("failed to start integration artifact after retrying several times")
   123  	}
   124  	deployStatus, err := getIntegrationArtifactDeployStatus(config, httpClient, apiHost, taskId)
   125  	if err != nil {
   126  		return err
   127  	}
   128  
   129  	//if artifact starting, then retry based on provided retry count
   130  	//with specific delay between each retry
   131  	if deployStatus == "DEPLOYING" {
   132  		// Calling Sleep method
   133  		sleepTime := int(retryCount * 3)
   134  		time.Sleep(time.Duration(sleepTime) * time.Second)
   135  		retryCount--
   136  		return pollIFlowDeploymentStatus(taskId, retryCount, config, httpClient, apiHost)
   137  	}
   138  
   139  	//if artifact started, then just return
   140  	if deployStatus == "SUCCESS" {
   141  		return nil
   142  	}
   143  
   144  	//if error return immediately with error details
   145  	if deployStatus == "FAIL" || deployStatus == "FAIL_ON_LICENSE_ERROR" {
   146  		resp, err := getIntegrationArtifactDeployError(config, httpClient, apiHost)
   147  		if err != nil {
   148  			return err
   149  		}
   150  		return errors.New(resp)
   151  	}
   152  	return nil
   153  }
   154  
   155  // GetHTTPErrorMessage - Return HTTP failure message
   156  func getHTTPErrorMessage(httpErr error, response *http.Response, httpMethod, statusURL string) (string, error) {
   157  	responseBody, readErr := io.ReadAll(response.Body)
   158  	if readErr != nil {
   159  		return "", errors.Wrapf(readErr, "HTTP response body could not be read, response status code: %v", response.StatusCode)
   160  	}
   161  	log.Entry().Errorf("a HTTP error occurred! Response body: %v, response status code: %v", string(responseBody), response.StatusCode)
   162  	return "", errors.Wrapf(httpErr, "HTTP %v request to %v failed with error: %v", httpMethod, statusURL, responseBody)
   163  }
   164  
   165  // getIntegrationArtifactDeployStatus - Get integration artifact Deploy Status
   166  func getIntegrationArtifactDeployStatus(config *integrationArtifactDeployOptions, httpClient piperhttp.Sender, apiHost string, taskId string) (string, error) {
   167  	httpMethod := "GET"
   168  	header := make(http.Header)
   169  	header.Add("content-type", "application/json")
   170  	header.Add("Accept", "application/json")
   171  	deployStatusURL := fmt.Sprintf("%s/api/v1/BuildAndDeployStatus(TaskId='%s')", apiHost, taskId)
   172  	deployStatusResp, httpErr := httpClient.SendRequest(httpMethod, deployStatusURL, nil, header, nil)
   173  
   174  	if deployStatusResp != nil && deployStatusResp.Body != nil {
   175  		defer deployStatusResp.Body.Close()
   176  	}
   177  
   178  	if deployStatusResp == nil {
   179  		return "", errors.Errorf("did not retrieve a HTTP response: %v", httpErr)
   180  	}
   181  
   182  	if deployStatusResp.StatusCode == http.StatusOK {
   183  		log.Entry().
   184  			WithField("IntegrationFlowID", config.IntegrationFlowID).
   185  			Info("Successfully started integration flow artefact in CPI runtime")
   186  
   187  		bodyText, readErr := io.ReadAll(deployStatusResp.Body)
   188  		if readErr != nil {
   189  			return "", errors.Wrapf(readErr, "HTTP response body could not be read, response status code: %v", deployStatusResp.StatusCode)
   190  		}
   191  		jsonResponse, parsingErr := gabs.ParseJSON([]byte(bodyText))
   192  		if parsingErr != nil {
   193  			return "", errors.Wrapf(parsingErr, "HTTP response body could not be parsed as JSON: %v", string(bodyText))
   194  		}
   195  		deployStatus := jsonResponse.Path("d.Status").Data().(string)
   196  		return deployStatus, nil
   197  	}
   198  	if httpErr != nil {
   199  		return getHTTPErrorMessage(httpErr, deployStatusResp, httpMethod, deployStatusURL)
   200  	}
   201  	return "", errors.Errorf("failed to get Integration Flow artefact runtime status, response Status code: %v", deployStatusResp.StatusCode)
   202  }
   203  
   204  // getIntegrationArtifactDeployError - Get integration artifact deploy error details
   205  func getIntegrationArtifactDeployError(config *integrationArtifactDeployOptions, httpClient piperhttp.Sender, apiHost string) (string, error) {
   206  	httpMethod := "GET"
   207  	header := make(http.Header)
   208  	header.Add("content-type", "application/json")
   209  	errorStatusURL := fmt.Sprintf("%s/api/v1/IntegrationRuntimeArtifacts('%s')/ErrorInformation/$value", apiHost, config.IntegrationFlowID)
   210  	errorStatusResp, httpErr := httpClient.SendRequest(httpMethod, errorStatusURL, nil, header, nil)
   211  
   212  	if errorStatusResp != nil && errorStatusResp.Body != nil {
   213  		defer errorStatusResp.Body.Close()
   214  	}
   215  
   216  	if errorStatusResp == nil {
   217  		return "", errors.Errorf("did not retrieve a HTTP response: %v", httpErr)
   218  	}
   219  
   220  	if errorStatusResp.StatusCode == http.StatusOK {
   221  		log.Entry().
   222  			WithField("IntegrationFlowID", config.IntegrationFlowID).
   223  			Info("Successfully retrieved Integration Flow artefact deploy error details")
   224  		responseBody, readErr := io.ReadAll(errorStatusResp.Body)
   225  		if readErr != nil {
   226  			return "", errors.Wrapf(readErr, "HTTP response body could not be read, response status code: %v", errorStatusResp.StatusCode)
   227  		}
   228  		log.Entry().Errorf("a HTTP error occurred! Response body: %v, Response status code: %v", string(responseBody), errorStatusResp.StatusCode)
   229  		errorDetails := string(responseBody)
   230  		return errorDetails, nil
   231  	}
   232  	if httpErr != nil {
   233  		return getHTTPErrorMessage(httpErr, errorStatusResp, httpMethod, errorStatusURL)
   234  	}
   235  	return "", errors.Errorf("failed to get Integration Flow artefact deploy error details, response Status code: %v", errorStatusResp.StatusCode)
   236  }