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 }