github.com/xgoffin/jenkins-library@v1.154.0/cmd/xsDeploy.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"github.com/SAP/jenkins-library/pkg/command"
     8  	"github.com/SAP/jenkins-library/pkg/log"
     9  	"github.com/SAP/jenkins-library/pkg/piperutils"
    10  	"github.com/SAP/jenkins-library/pkg/telemetry"
    11  	"github.com/pkg/errors"
    12  	"io"
    13  	"io/ioutil"
    14  	"os"
    15  	"regexp"
    16  	"strings"
    17  	"sync"
    18  	"text/template"
    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  		io.Copy(buf, r)
   181  		o = buf.String()
   182  		wg.Done()
   183  	}()
   184  
   185  	go func() {
   186  		buf := new(bytes.Buffer)
   187  		r := io.TeeReader(prErr, os.Stderr)
   188  		io.Copy(buf, r)
   189  		e = buf.String()
   190  		wg.Done()
   191  	}()
   192  
   193  	var loginErr error
   194  
   195  	xsSessionFile := ".xsconfig"
   196  	if len(XsDeployOptions.XsSessionFile) > 0 {
   197  		xsSessionFile = XsDeployOptions.XsSessionFile
   198  	}
   199  
   200  	if performLogin {
   201  		loginErr = xsLogin(XsDeployOptions, s)
   202  		if loginErr == nil {
   203  			err = copyFileFromHomeToPwd(xsSessionFile, fileUtils)
   204  		}
   205  	}
   206  
   207  	if loginErr == nil && err == nil {
   208  
   209  		{
   210  			exists, e := fileUtils.FileExists(xsSessionFile)
   211  			if e != nil {
   212  				return e
   213  			}
   214  			if !exists {
   215  				return fmt.Errorf("xs session file does not exist (%s)", xsSessionFile)
   216  			}
   217  		}
   218  
   219  		copyFileFromPwdToHome(xsSessionFile, fileUtils)
   220  
   221  		switch action {
   222  		case Resume, Abort, Retry:
   223  			err = complete(mode, action, XsDeployOptions.OperationID, s)
   224  		default:
   225  			err = deploy(mode, XsDeployOptions, s)
   226  		}
   227  	}
   228  
   229  	if loginErr == nil && (performLogout || err != nil) {
   230  		if logoutErr := xsLogout(XsDeployOptions, s); logoutErr != nil {
   231  			if err == nil {
   232  				err = logoutErr
   233  			}
   234  		} else {
   235  
   236  			// we delete the xs session file from workspace. From home directory it is deleted by the
   237  			// xs command itself.
   238  			if e := fRemove(xsSessionFile); e != nil {
   239  				err = e
   240  			}
   241  			log.Entry().Debugf("xs session file '%s' has been deleted from workspace", xsSessionFile)
   242  		}
   243  	} else {
   244  		if loginErr != nil {
   245  			log.Entry().Info("Logout skipped since login did not succeed.")
   246  		} else if !performLogout {
   247  			log.Entry().Info("Logout skipped in order to be able to resume or abort later")
   248  		}
   249  	}
   250  
   251  	if err == nil {
   252  		err = loginErr
   253  	}
   254  
   255  	if err != nil {
   256  		if e := handleLog(fmt.Sprintf("%s/%s", os.Getenv("HOME"), ".xs_logs")); e != nil {
   257  			log.Entry().Warningf("Cannot provide the logs: %s", e.Error())
   258  		}
   259  	}
   260  
   261  	pwOut.Close()
   262  	pwErr.Close()
   263  
   264  	wg.Wait()
   265  
   266  	if err == nil && (mode == BGDeploy && action == None) {
   267  		piperEnvironment.operationID = retrieveOperationID(o, XsDeployOptions.OperationIDLogPattern)
   268  		if len(piperEnvironment.operationID) == 0 && err == nil {
   269  			err = errors.New("No operationID found")
   270  		}
   271  		XsDeployOptions.OperationID = piperEnvironment.operationID // for backward compatibility as long as we render that struc to stdout (printStatus)
   272  	}
   273  
   274  	if err != nil {
   275  		log.Entry().Errorf("An error occurred. Stdout from underlying process: >>%s<<. Stderr from underlying process: >>%s<<", o, e)
   276  	}
   277  
   278  	if e := printStatus(XsDeployOptions, stdout); e != nil {
   279  		if err == nil {
   280  			err = e
   281  		}
   282  	}
   283  
   284  	return err
   285  }
   286  
   287  func printStatus(XsDeployOptions xsDeployOptions, stdout io.Writer) error {
   288  	XsDeployOptionsCopy := XsDeployOptions
   289  	XsDeployOptionsCopy.Password = ""
   290  
   291  	var e error
   292  	if b, e := json.Marshal(XsDeployOptionsCopy); e == nil {
   293  		fmt.Fprintln(stdout, string(b))
   294  	}
   295  	return e
   296  }
   297  
   298  func handleLog(logDir string) error {
   299  
   300  	if _, e := os.Stat(logDir); !os.IsNotExist(e) {
   301  		log.Entry().Warningf(fmt.Sprintf("Here are the logs (%s):", logDir))
   302  
   303  		logFiles, e := ioutil.ReadDir(logDir)
   304  
   305  		if e != nil {
   306  			return e
   307  		}
   308  
   309  		if len(logFiles) == 0 {
   310  			log.Entry().Warningf("Cannot provide xs logs. No log files found inside '%s'.", logDir)
   311  		}
   312  
   313  		for _, logFile := range logFiles {
   314  			buf := make([]byte, 32*1024)
   315  			log.Entry().Infof("File: '%s'", logFile.Name())
   316  			if f, e := os.Open(fmt.Sprintf("%s/%s", logDir, logFile.Name())); e == nil {
   317  				for {
   318  					if n, e := f.Read(buf); e != nil {
   319  						if e == io.EOF {
   320  							break
   321  						}
   322  						return e
   323  					} else {
   324  						os.Stderr.WriteString(string(buf[:n]))
   325  					}
   326  				}
   327  			} else {
   328  				return e
   329  			}
   330  		}
   331  	} else {
   332  		log.Entry().Warningf("Cannot provide xs logs. Log directory '%s' does not exist.", logDir)
   333  	}
   334  	return nil
   335  }
   336  
   337  func retrieveOperationID(deployLog, pattern string) string {
   338  	re := regexp.MustCompile(pattern)
   339  	lines := strings.Split(deployLog, "\n")
   340  	var operationID string
   341  	for _, line := range lines {
   342  		matched := re.FindStringSubmatch(line)
   343  		if len(matched) >= 1 {
   344  			operationID = matched[1]
   345  			break
   346  		}
   347  	}
   348  
   349  	if len(operationID) > 0 {
   350  		log.Entry().Infof("Operation identifier: '%s'", operationID)
   351  	} else {
   352  		log.Entry().Infof("No operation identifier found in >>>>%s<<<<.", deployLog)
   353  	}
   354  
   355  	return operationID
   356  }
   357  
   358  func xsLogin(XsDeployOptions xsDeployOptions, s command.ShellRunner) error {
   359  
   360  	log.Entry().Debugf("Performing xs login. api-url: '%s', org: '%s', space: '%s'",
   361  		XsDeployOptions.APIURL, XsDeployOptions.Org, XsDeployOptions.Space)
   362  
   363  	if e := executeCmd("login", loginScript, XsDeployOptions, s); e != nil {
   364  		log.Entry().Errorf("xs login failed: %s", e.Error())
   365  		return e
   366  	}
   367  
   368  	log.Entry().Infof("xs login has been performed. api-url: '%s', org: '%s', space: '%s'",
   369  		XsDeployOptions.APIURL, XsDeployOptions.Org, XsDeployOptions.Space)
   370  
   371  	return nil
   372  }
   373  
   374  func xsLogout(XsDeployOptions xsDeployOptions, s command.ShellRunner) error {
   375  
   376  	log.Entry().Debug("Performing xs logout.")
   377  
   378  	if e := executeCmd("logout", logoutScript, XsDeployOptions, s); e != nil {
   379  		return e
   380  	}
   381  	log.Entry().Info("xs logout has been performed")
   382  
   383  	return nil
   384  }
   385  
   386  func deploy(mode DeployMode, XsDeployOptions xsDeployOptions, s command.ShellRunner) error {
   387  
   388  	deployCommand, err := mode.GetDeployCommand()
   389  	if err != nil {
   390  		return err
   391  	}
   392  
   393  	type deployProperties struct {
   394  		xsDeployOptions
   395  		Mode string
   396  	}
   397  
   398  	log.Entry().Infof("Performing xs %s.", deployCommand)
   399  	if e := executeCmd("deploy", deployScript, deployProperties{xsDeployOptions: XsDeployOptions, Mode: deployCommand}, s); e != nil {
   400  		return e
   401  	}
   402  	log.Entry().Infof("xs %s performed.", deployCommand)
   403  
   404  	return nil
   405  }
   406  
   407  func complete(mode DeployMode, action Action, operationID string, s command.ShellRunner) error {
   408  	log.Entry().Debugf("Performing xs %s", action)
   409  
   410  	type completeProperties struct {
   411  		xsDeployOptions
   412  		Mode        DeployMode
   413  		Action      Action
   414  		OperationID string
   415  	}
   416  
   417  	CompleteProperties := completeProperties{Mode: mode, Action: action, OperationID: operationID}
   418  
   419  	if e := executeCmd("complete", completeScript, CompleteProperties, s); e != nil {
   420  		return e
   421  	}
   422  
   423  	return nil
   424  }
   425  
   426  func executeCmd(templateID string, commandPattern string, properties interface{}, s command.ShellRunner) error {
   427  
   428  	tmpl, e := template.New(templateID).Parse(commandPattern)
   429  	if e != nil {
   430  		return e
   431  	}
   432  
   433  	var script bytes.Buffer
   434  	tmpl.Execute(&script, properties)
   435  	if e := s.RunShell("/bin/bash", script.String()); e != nil {
   436  		return e
   437  	}
   438  
   439  	return nil
   440  }
   441  
   442  func copyFileFromHomeToPwd(xsSessionFile string, fileUtils piperutils.FileUtils) error {
   443  	src, dest := fmt.Sprintf("%s/%s", os.Getenv("HOME"), xsSessionFile), fmt.Sprintf("%s", xsSessionFile)
   444  	log.Entry().Debugf("Copying xs session file from home directory ('%s') to workspace ('%s')", src, dest)
   445  	if _, err := fileUtils.Copy(src, dest); err != nil {
   446  		return errors.Wrapf(err, "Cannot copy xssession file from home directory ('%s') to workspace ('%s')", src, dest)
   447  	}
   448  	log.Entry().Debugf("xs session file copied from home directory ('%s') to workspace ('%s')", src, dest)
   449  	return nil
   450  }
   451  
   452  func copyFileFromPwdToHome(xsSessionFile string, fileUtils piperutils.FileUtils) error {
   453  
   454  	//
   455  	// We rely on running inside a docker container which is discarded after a single use.
   456  	// In general it is not a good idea to update files in the build users home directory in case
   457  	// we are on an infrastructure which is used not only for single builds since updates at that level
   458  	// affects also other builds.
   459  	//
   460  
   461  	src, dest := fmt.Sprintf("%s", xsSessionFile), fmt.Sprintf("%s/%s", os.Getenv("HOME"), xsSessionFile)
   462  	log.Entry().Debugf("Copying xs session file from workspace ('%s') to home directory ('%s')", src, dest)
   463  	if _, err := fileUtils.Copy(src, dest); err != nil {
   464  		return errors.Wrapf(err, "Cannot copy xssession file from workspace ('%s') to home directory ('%s')", src, dest)
   465  	}
   466  	log.Entry().Debugf("xs session file copied from workspace ('%s') to home directory ('%s')", src, dest)
   467  	return nil
   468  }
   469  
   470  //GetAction ...
   471  func (a Action) GetAction() (string, error) {
   472  	switch a {
   473  	case Resume, Abort, Retry:
   474  		return strings.ToLower(a.String()), nil
   475  	}
   476  	return "", errors.New(fmt.Sprintf("Invalid deploy mode: '%s'.", a))
   477  
   478  }
   479  
   480  //GetDeployCommand ...
   481  func (m DeployMode) GetDeployCommand() (string, error) {
   482  
   483  	switch m {
   484  	case Deploy:
   485  		return "deploy", nil
   486  	case BGDeploy:
   487  		return "bg-deploy", nil
   488  	}
   489  	return "", errors.New(fmt.Sprintf("Invalid deploy mode: '%s'.", m))
   490  }