github.com/SAP/jenkins-library@v1.362.0/cmd/xsDeploy.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"regexp"
    10  	"strings"
    11  	"sync"
    12  	"text/template"
    13  
    14  	"github.com/SAP/jenkins-library/pkg/command"
    15  	"github.com/SAP/jenkins-library/pkg/log"
    16  	"github.com/SAP/jenkins-library/pkg/piperutils"
    17  	"github.com/SAP/jenkins-library/pkg/telemetry"
    18  	"github.com/pkg/errors"
    19  )
    20  
    21  // DeployMode ...
    22  type DeployMode int
    23  
    24  const (
    25  	// NoDeploy ...
    26  	NoDeploy DeployMode = iota
    27  	//Deploy ...
    28  	Deploy DeployMode = iota
    29  	//BGDeploy ...
    30  	BGDeploy DeployMode = iota
    31  )
    32  
    33  // ValueOfMode ...
    34  func ValueOfMode(str string) (DeployMode, error) {
    35  	switch str {
    36  	case "NONE":
    37  		return NoDeploy, nil
    38  	case "DEPLOY":
    39  		return Deploy, nil
    40  	case "BG_DEPLOY":
    41  		return BGDeploy, nil
    42  	default:
    43  		return NoDeploy, errors.New(fmt.Sprintf("Unknown DeployMode: '%s'", str))
    44  	}
    45  }
    46  
    47  // String ...
    48  func (m DeployMode) String() string {
    49  	return [...]string{
    50  		"NONE",
    51  		"DEPLOY",
    52  		"BG_DEPLOY",
    53  	}[m]
    54  }
    55  
    56  // Action ...
    57  type Action int
    58  
    59  const (
    60  	//None ...
    61  	None Action = iota
    62  	//Resume ...
    63  	Resume Action = iota
    64  	//Abort ...
    65  	Abort Action = iota
    66  	//Retry ...
    67  	Retry Action = iota
    68  )
    69  
    70  // ValueOfAction ...
    71  func ValueOfAction(str string) (Action, error) {
    72  	switch str {
    73  	case "NONE":
    74  		return None, nil
    75  	case "RESUME":
    76  		return Resume, nil
    77  	case "ABORT":
    78  		return Abort, nil
    79  	case "RETRY":
    80  		return Retry, nil
    81  
    82  	default:
    83  		return None, errors.New(fmt.Sprintf("Unknown Action: '%s'", str))
    84  	}
    85  }
    86  
    87  // String ...
    88  func (a Action) String() string {
    89  	return [...]string{
    90  		"NONE",
    91  		"RESUME",
    92  		"ABORT",
    93  		"RETRY",
    94  	}[a]
    95  }
    96  
    97  const loginScript = `#!/bin/bash
    98  xs login -a {{.APIURL}} -u {{.Username}} -p '{{.Password}}' -o {{.Org}} -s {{.Space}} {{.LoginOpts}}
    99  `
   100  
   101  const logoutScript = `#!/bin/bash
   102  xs logout`
   103  
   104  const deployScript = `#!/bin/bash
   105  xs {{.Mode}} {{.MtaPath}} {{.DeployOpts}}`
   106  
   107  const completeScript = `#!/bin/bash
   108  xs {{.Mode.GetDeployCommand}} -i {{.OperationID}} -a {{.Action.GetAction}}
   109  `
   110  
   111  func xsDeploy(config xsDeployOptions, telemetryData *telemetry.CustomData, piperEnvironment *xsDeployCommonPipelineEnvironment) {
   112  	c := command.Command{}
   113  	fileUtils := piperutils.Files{}
   114  	err := runXsDeploy(config, piperEnvironment, &c, fileUtils, os.Remove, os.Stdout)
   115  	if err != nil {
   116  		log.Entry().
   117  			WithError(err).
   118  			Fatal("failed to execute xs deployment")
   119  	}
   120  }
   121  
   122  func runXsDeploy(XsDeployOptions xsDeployOptions, piperEnvironment *xsDeployCommonPipelineEnvironment, s command.ShellRunner,
   123  	fileUtils piperutils.FileUtils,
   124  	fRemove func(string) error,
   125  	stdout io.Writer) error {
   126  
   127  	mode, err := ValueOfMode(XsDeployOptions.Mode)
   128  	if err != nil {
   129  		return errors.Wrapf(err, "Extracting mode failed: '%s'", XsDeployOptions.Mode)
   130  	}
   131  
   132  	if mode == NoDeploy {
   133  		log.Entry().Infof("Deployment skipped intentionally. Deploy mode '%s'", mode.String())
   134  		return nil
   135  	}
   136  
   137  	action, err := ValueOfAction(XsDeployOptions.Action)
   138  	if err != nil {
   139  		return errors.Wrapf(err, "Extracting action failed: '%s'", XsDeployOptions.Action)
   140  	}
   141  
   142  	if mode == Deploy && action != None {
   143  		return errors.New(fmt.Sprintf("Cannot perform action '%s' in mode '%s'. Only action '%s' is allowed.", action, mode, None))
   144  	}
   145  
   146  	log.Entry().Debugf("Mode: '%s', Action: '%s'", mode, action)
   147  
   148  	performLogin := mode == Deploy || (mode == BGDeploy && !(action == Resume || action == Abort))
   149  	performLogout := mode == Deploy || (mode == BGDeploy && action != None)
   150  	log.Entry().Debugf("performLogin: %t, performLogout: %t", performLogin, performLogout)
   151  
   152  	{
   153  		exists, e := fileUtils.FileExists(XsDeployOptions.MtaPath)
   154  		if e != nil {
   155  			return e
   156  		}
   157  		if action == None && !exists {
   158  			return errors.New(fmt.Sprintf("Deployable '%s' does not exist", XsDeployOptions.MtaPath))
   159  		}
   160  	}
   161  
   162  	if action != None && len(XsDeployOptions.OperationID) == 0 {
   163  		return errors.New(fmt.Sprintf("OperationID was not provided. This is required for action '%s'.", action))
   164  	}
   165  
   166  	prOut, pwOut := io.Pipe()
   167  	prErr, pwErr := io.Pipe()
   168  
   169  	s.Stdout(pwOut)
   170  	s.Stderr(pwErr)
   171  
   172  	var e, o string
   173  
   174  	var wg sync.WaitGroup
   175  	wg.Add(2)
   176  
   177  	go func() {
   178  		buf := new(bytes.Buffer)
   179  		r := io.TeeReader(prOut, os.Stderr)
   180  		if _, err := io.Copy(buf, r); err != nil {
   181  			log.Entry().Warningf("failed to copy buffer")
   182  		}
   183  		o = buf.String()
   184  		wg.Done()
   185  	}()
   186  
   187  	go func() {
   188  		buf := new(bytes.Buffer)
   189  		r := io.TeeReader(prErr, os.Stderr)
   190  		if _, err := io.Copy(buf, r); err != nil {
   191  			log.Entry().Warningf("failed to copy buffer")
   192  		}
   193  		e = buf.String()
   194  		wg.Done()
   195  	}()
   196  
   197  	var loginErr error
   198  
   199  	xsSessionFile := ".xsconfig"
   200  	if len(XsDeployOptions.XsSessionFile) > 0 {
   201  		xsSessionFile = XsDeployOptions.XsSessionFile
   202  	}
   203  
   204  	if performLogin {
   205  		loginErr = xsLogin(XsDeployOptions, s)
   206  		if loginErr == nil {
   207  			err = copyFileFromHomeToPwd(xsSessionFile, fileUtils)
   208  		}
   209  	}
   210  
   211  	if loginErr == nil && err == nil {
   212  
   213  		{
   214  			exists, e := fileUtils.FileExists(xsSessionFile)
   215  			if e != nil {
   216  				return e
   217  			}
   218  			if !exists {
   219  				return fmt.Errorf("xs session file does not exist (%s)", xsSessionFile)
   220  			}
   221  		}
   222  
   223  		copyFileFromPwdToHome(xsSessionFile, fileUtils)
   224  
   225  		switch action {
   226  		case Resume, Abort, Retry:
   227  			err = complete(mode, action, XsDeployOptions.OperationID, s)
   228  		default:
   229  			err = deploy(mode, XsDeployOptions, s)
   230  		}
   231  	}
   232  
   233  	if loginErr == nil && (performLogout || err != nil) {
   234  		if logoutErr := xsLogout(XsDeployOptions, s); logoutErr != nil {
   235  			if err == nil {
   236  				err = logoutErr
   237  			}
   238  		} else {
   239  
   240  			// we delete the xs session file from workspace. From home directory it is deleted by the
   241  			// xs command itself.
   242  			if e := fRemove(xsSessionFile); e != nil {
   243  				err = e
   244  			}
   245  			log.Entry().Debugf("xs session file '%s' has been deleted from workspace", xsSessionFile)
   246  		}
   247  	} else {
   248  		if loginErr != nil {
   249  			log.Entry().Info("Logout skipped since login did not succeed.")
   250  		} else if !performLogout {
   251  			log.Entry().Info("Logout skipped in order to be able to resume or abort later")
   252  		}
   253  	}
   254  
   255  	if err == nil {
   256  		err = loginErr
   257  	}
   258  
   259  	if err != nil {
   260  		if e := handleLog(fmt.Sprintf("%s/%s", os.Getenv("HOME"), ".xs_logs")); e != nil {
   261  			log.Entry().Warningf("Cannot provide the logs: %s", e.Error())
   262  		}
   263  	}
   264  
   265  	pwOut.Close()
   266  	pwErr.Close()
   267  
   268  	wg.Wait()
   269  
   270  	if err == nil && (mode == BGDeploy && action == None) {
   271  		piperEnvironment.operationID = retrieveOperationID(o, XsDeployOptions.OperationIDLogPattern)
   272  		if len(piperEnvironment.operationID) == 0 && err == nil {
   273  			err = errors.New("No operationID found")
   274  		}
   275  		XsDeployOptions.OperationID = piperEnvironment.operationID // for backward compatibility as long as we render that struc to stdout (printStatus)
   276  	}
   277  
   278  	if err != nil {
   279  		log.Entry().Errorf("An error occurred. Stdout from underlying process: >>%s<<. Stderr from underlying process: >>%s<<", o, e)
   280  	}
   281  
   282  	if e := printStatus(XsDeployOptions, stdout); e != nil {
   283  		if err == nil {
   284  			err = e
   285  		}
   286  	}
   287  
   288  	return err
   289  }
   290  
   291  func printStatus(XsDeployOptions xsDeployOptions, stdout io.Writer) error {
   292  	XsDeployOptionsCopy := XsDeployOptions
   293  	XsDeployOptionsCopy.Password = ""
   294  
   295  	var e error
   296  	if b, e := json.Marshal(XsDeployOptionsCopy); e == nil {
   297  		fmt.Fprintln(stdout, string(b))
   298  	}
   299  	return e
   300  }
   301  
   302  func handleLog(logDir string) error {
   303  
   304  	if _, e := os.Stat(logDir); !os.IsNotExist(e) {
   305  		log.Entry().Warningf(fmt.Sprintf("Here are the logs (%s):", logDir))
   306  
   307  		logFiles, e := os.ReadDir(logDir)
   308  
   309  		if e != nil {
   310  			return e
   311  		}
   312  
   313  		if len(logFiles) == 0 {
   314  			log.Entry().Warningf("Cannot provide xs logs. No log files found inside '%s'.", logDir)
   315  		}
   316  
   317  		for _, logFile := range logFiles {
   318  			buf := make([]byte, 32*1024)
   319  			log.Entry().Infof("File: '%s'", logFile.Name())
   320  			if f, e := os.Open(fmt.Sprintf("%s/%s", logDir, logFile.Name())); e == nil {
   321  				for {
   322  					if n, e := f.Read(buf); e != nil {
   323  						if e == io.EOF {
   324  							break
   325  						}
   326  						return e
   327  					} else {
   328  						os.Stderr.WriteString(string(buf[:n]))
   329  					}
   330  				}
   331  			} else {
   332  				return e
   333  			}
   334  		}
   335  	} else {
   336  		log.Entry().Warningf("Cannot provide xs logs. Log directory '%s' does not exist.", logDir)
   337  	}
   338  	return nil
   339  }
   340  
   341  func retrieveOperationID(deployLog, pattern string) string {
   342  	re := regexp.MustCompile(pattern)
   343  	lines := strings.Split(deployLog, "\n")
   344  	var operationID string
   345  	for _, line := range lines {
   346  		matched := re.FindStringSubmatch(line)
   347  		if len(matched) >= 1 {
   348  			operationID = matched[1]
   349  			break
   350  		}
   351  	}
   352  
   353  	if len(operationID) > 0 {
   354  		log.Entry().Infof("Operation identifier: '%s'", operationID)
   355  	} else {
   356  		log.Entry().Infof("No operation identifier found in >>>>%s<<<<.", deployLog)
   357  	}
   358  
   359  	return operationID
   360  }
   361  
   362  func xsLogin(XsDeployOptions xsDeployOptions, s command.ShellRunner) error {
   363  
   364  	log.Entry().Debugf("Performing xs login. api-url: '%s', org: '%s', space: '%s'",
   365  		XsDeployOptions.APIURL, XsDeployOptions.Org, XsDeployOptions.Space)
   366  
   367  	if e := executeCmd("login", loginScript, XsDeployOptions, s); e != nil {
   368  		log.Entry().Errorf("xs login failed: %s", e.Error())
   369  		return e
   370  	}
   371  
   372  	log.Entry().Infof("xs login has been performed. api-url: '%s', org: '%s', space: '%s'",
   373  		XsDeployOptions.APIURL, XsDeployOptions.Org, XsDeployOptions.Space)
   374  
   375  	return nil
   376  }
   377  
   378  func xsLogout(XsDeployOptions xsDeployOptions, s command.ShellRunner) error {
   379  
   380  	log.Entry().Debug("Performing xs logout.")
   381  
   382  	if e := executeCmd("logout", logoutScript, XsDeployOptions, s); e != nil {
   383  		return e
   384  	}
   385  	log.Entry().Info("xs logout has been performed")
   386  
   387  	return nil
   388  }
   389  
   390  func deploy(mode DeployMode, XsDeployOptions xsDeployOptions, s command.ShellRunner) error {
   391  
   392  	deployCommand, err := mode.GetDeployCommand()
   393  	if err != nil {
   394  		return err
   395  	}
   396  
   397  	type deployProperties struct {
   398  		xsDeployOptions
   399  		Mode string
   400  	}
   401  
   402  	log.Entry().Infof("Performing xs %s.", deployCommand)
   403  	if e := executeCmd("deploy", deployScript, deployProperties{xsDeployOptions: XsDeployOptions, Mode: deployCommand}, s); e != nil {
   404  		return e
   405  	}
   406  	log.Entry().Infof("xs %s performed.", deployCommand)
   407  
   408  	return nil
   409  }
   410  
   411  func complete(mode DeployMode, action Action, operationID string, s command.ShellRunner) error {
   412  	log.Entry().Debugf("Performing xs %s", action)
   413  
   414  	type completeProperties struct {
   415  		xsDeployOptions
   416  		Mode        DeployMode
   417  		Action      Action
   418  		OperationID string
   419  	}
   420  
   421  	CompleteProperties := completeProperties{Mode: mode, Action: action, OperationID: operationID}
   422  
   423  	if e := executeCmd("complete", completeScript, CompleteProperties, s); e != nil {
   424  		return e
   425  	}
   426  
   427  	return nil
   428  }
   429  
   430  func executeCmd(templateID string, commandPattern string, properties interface{}, s command.ShellRunner) error {
   431  
   432  	tmpl, e := template.New(templateID).Parse(commandPattern)
   433  	if e != nil {
   434  		return e
   435  	}
   436  
   437  	var script bytes.Buffer
   438  	if err := tmpl.Execute(&script, properties); err != nil {
   439  		return err
   440  	}
   441  	if e := s.RunShell("/bin/bash", script.String()); e != nil {
   442  		return e
   443  	}
   444  
   445  	return nil
   446  }
   447  
   448  func copyFileFromHomeToPwd(xsSessionFile string, fileUtils piperutils.FileUtils) error {
   449  	src, dest := fmt.Sprintf("%s/%s", os.Getenv("HOME"), xsSessionFile), xsSessionFile
   450  	log.Entry().Debugf("Copying xs session file from home directory ('%s') to workspace ('%s')", src, dest)
   451  	if _, err := fileUtils.Copy(src, dest); err != nil {
   452  		return errors.Wrapf(err, "Cannot copy xssession file from home directory ('%s') to workspace ('%s')", src, dest)
   453  	}
   454  	log.Entry().Debugf("xs session file copied from home directory ('%s') to workspace ('%s')", src, dest)
   455  	return nil
   456  }
   457  
   458  func copyFileFromPwdToHome(xsSessionFile string, fileUtils piperutils.FileUtils) error {
   459  
   460  	//
   461  	// We rely on running inside a docker container which is discarded after a single use.
   462  	// In general it is not a good idea to update files in the build users home directory in case
   463  	// we are on an infrastructure which is used not only for single builds since updates at that level
   464  	// affects also other builds.
   465  	//
   466  
   467  	src, dest := xsSessionFile, fmt.Sprintf("%s/%s", os.Getenv("HOME"), xsSessionFile)
   468  	log.Entry().Debugf("Copying xs session file from workspace ('%s') to home directory ('%s')", src, dest)
   469  	if _, err := fileUtils.Copy(src, dest); err != nil {
   470  		return errors.Wrapf(err, "Cannot copy xssession file from workspace ('%s') to home directory ('%s')", src, dest)
   471  	}
   472  	log.Entry().Debugf("xs session file copied from workspace ('%s') to home directory ('%s')", src, dest)
   473  	return nil
   474  }
   475  
   476  // GetAction ...
   477  func (a Action) GetAction() (string, error) {
   478  	switch a {
   479  	case Resume, Abort, Retry:
   480  		return strings.ToLower(a.String()), nil
   481  	}
   482  	return "", errors.New(fmt.Sprintf("Invalid deploy mode: '%s'.", a))
   483  
   484  }
   485  
   486  // GetDeployCommand ...
   487  func (m DeployMode) GetDeployCommand() (string, error) {
   488  
   489  	switch m {
   490  	case Deploy:
   491  		return "deploy", nil
   492  	case BGDeploy:
   493  		return "bg-deploy", nil
   494  	}
   495  	return "", errors.New(fmt.Sprintf("Invalid deploy mode: '%s'.", m))
   496  }