github.com/jaylevin/jenkins-library@v1.230.4/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 }