github.com/SAP/jenkins-library@v1.362.0/cmd/artifactPrepareVersion.go (about) 1 package cmd 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 netHttp "net/http" 8 "os" 9 "strings" 10 "text/template" 11 "time" 12 13 "github.com/SAP/jenkins-library/pkg/certutils" 14 piperhttp "github.com/SAP/jenkins-library/pkg/http" 15 "github.com/SAP/jenkins-library/pkg/piperutils" 16 17 "github.com/SAP/jenkins-library/pkg/command" 18 gitUtils "github.com/SAP/jenkins-library/pkg/git" 19 "github.com/SAP/jenkins-library/pkg/log" 20 "github.com/SAP/jenkins-library/pkg/orchestrator" 21 "github.com/SAP/jenkins-library/pkg/telemetry" 22 "github.com/SAP/jenkins-library/pkg/versioning" 23 "github.com/pkg/errors" 24 25 "github.com/go-git/go-git/v5" 26 gitConfig "github.com/go-git/go-git/v5/config" 27 "github.com/go-git/go-git/v5/plumbing" 28 "github.com/go-git/go-git/v5/plumbing/object" 29 "github.com/go-git/go-git/v5/plumbing/transport/http" 30 "github.com/go-git/go-git/v5/plumbing/transport/ssh" 31 ) 32 33 type gitRepository interface { 34 CommitObject(plumbing.Hash) (*object.Commit, error) 35 CreateTag(string, plumbing.Hash, *git.CreateTagOptions) (*plumbing.Reference, error) 36 CreateRemote(*gitConfig.RemoteConfig) (*git.Remote, error) 37 DeleteRemote(string) error 38 Push(*git.PushOptions) error 39 Remote(string) (*git.Remote, error) 40 ResolveRevision(plumbing.Revision) (*plumbing.Hash, error) 41 Worktree() (*git.Worktree, error) 42 } 43 44 type gitWorktree interface { 45 Checkout(*git.CheckoutOptions) error 46 Commit(string, *git.CommitOptions) (plumbing.Hash, error) 47 } 48 49 func getGitWorktree(repository gitRepository) (gitWorktree, error) { 50 return repository.Worktree() 51 } 52 53 type artifactPrepareVersionUtils interface { 54 Stdout(out io.Writer) 55 Stderr(err io.Writer) 56 RunExecutable(e string, p ...string) error 57 58 DownloadFile(url, filename string, header netHttp.Header, cookies []*netHttp.Cookie) error 59 piperhttp.Sender 60 61 Glob(pattern string) (matches []string, err error) 62 FileExists(filename string) (bool, error) 63 Copy(src, dest string) (int64, error) 64 MkdirAll(path string, perm os.FileMode) error 65 FileWrite(path string, content []byte, perm os.FileMode) error 66 FileRead(path string) ([]byte, error) 67 FileRemove(path string) error 68 69 GetConfigProvider() (orchestrator.ConfigProvider, error) 70 } 71 72 type artifactPrepareVersionUtilsBundle struct { 73 *command.Command 74 *piperutils.Files 75 *piperhttp.Client 76 } 77 78 func (a *artifactPrepareVersionUtilsBundle) GetConfigProvider() (orchestrator.ConfigProvider, error) { 79 return orchestrator.GetOrchestratorConfigProvider(nil) 80 } 81 82 func newArtifactPrepareVersionUtilsBundle() artifactPrepareVersionUtils { 83 utils := artifactPrepareVersionUtilsBundle{ 84 Command: &command.Command{}, 85 Files: &piperutils.Files{}, 86 Client: &piperhttp.Client{}, 87 } 88 utils.Stdout(log.Writer()) 89 utils.Stderr(log.Writer()) 90 return &utils 91 } 92 93 func artifactPrepareVersion(config artifactPrepareVersionOptions, telemetryData *telemetry.CustomData, commonPipelineEnvironment *artifactPrepareVersionCommonPipelineEnvironment) { 94 utils := newArtifactPrepareVersionUtilsBundle() 95 96 // open local .git repository 97 repository, err := openGit() 98 if err != nil { 99 log.Entry().WithError(err).Fatal("git repository required - none available") 100 } 101 102 err = runArtifactPrepareVersion(&config, telemetryData, commonPipelineEnvironment, nil, utils, repository, getGitWorktree) 103 if err != nil { 104 log.Entry().WithError(err).Fatal("artifactPrepareVersion failed") 105 } 106 } 107 108 var sshAgentAuth = ssh.NewSSHAgentAuth 109 110 func runArtifactPrepareVersion(config *artifactPrepareVersionOptions, telemetryData *telemetry.CustomData, commonPipelineEnvironment *artifactPrepareVersionCommonPipelineEnvironment, artifact versioning.Artifact, utils artifactPrepareVersionUtils, repository gitRepository, getWorktree func(gitRepository) (gitWorktree, error)) error { 111 112 telemetryData.BuildTool = config.BuildTool 113 telemetryData.FilePath = config.FilePath 114 115 // Options for artifact 116 artifactOpts := versioning.Options{ 117 GlobalSettingsFile: config.GlobalSettingsFile, 118 M2Path: config.M2Path, 119 ProjectSettingsFile: config.ProjectSettingsFile, 120 VersionField: config.CustomVersionField, 121 VersionSection: config.CustomVersionSection, 122 VersioningScheme: config.CustomVersioningScheme, 123 VersionSource: config.DockerVersionSource, 124 CAPVersioningPreference: config.CAPVersioningPreference, 125 } 126 127 var err error 128 if artifact == nil { 129 artifact, err = versioning.GetArtifact(config.BuildTool, config.FilePath, &artifactOpts, utils) 130 if err != nil { 131 log.SetErrorCategory(log.ErrorConfiguration) 132 return errors.Wrap(err, "failed to retrieve artifact") 133 } 134 } 135 136 // support former groovy versioning template and translate into new options 137 if len(config.VersioningTemplate) > 0 { 138 config.VersioningType, _, config.IncludeCommitID = templateCompatibility(config.VersioningTemplate) 139 } 140 141 version, err := artifact.GetVersion() 142 if err != nil { 143 log.SetErrorCategory(log.ErrorConfiguration) 144 return errors.Wrap(err, "failed to retrieve version") 145 } else if len(version) == 0 { 146 log.SetErrorCategory(log.ErrorConfiguration) 147 return fmt.Errorf("version is empty - please check versioning configuration") 148 } 149 log.Entry().Infof("Version before automatic versioning: %v", version) 150 151 gitCommit, gitCommitMessage, err := getGitCommitID(repository) 152 if err != nil { 153 log.SetErrorCategory(log.ErrorConfiguration) 154 return err 155 } 156 gitCommitID := gitCommit.String() 157 158 commonPipelineEnvironment.git.headCommitID = gitCommitID 159 newVersion := version 160 now := time.Now() 161 162 if config.VersioningType == "cloud" || config.VersioningType == "cloud_noTag" { 163 // make sure that versioning does not create tags (when set to "cloud") 164 // for PR pipelines, optimized pipelines (= no build) 165 provider, err := utils.GetConfigProvider() 166 if err != nil { 167 log.Entry().WithError(err).Warning("Cannot infer config from CI environment") 168 } 169 if provider.IsPullRequest() || config.IsOptimizedAndScheduled { 170 config.VersioningType = "cloud_noTag" 171 } 172 173 newVersion, err = calculateCloudVersion(artifact, config, version, gitCommitID, now) 174 if err != nil { 175 return err 176 } 177 178 worktree, err := getWorktree(repository) 179 if err != nil { 180 log.SetErrorCategory(log.ErrorConfiguration) 181 return errors.Wrap(err, "failed to retrieve git worktree") 182 } 183 184 // opening repository does not seem to consider already existing files properly 185 // behavior in case we do not run initializeWorktree: 186 // git.Add(".") will add the complete workspace instead of only changed files 187 err = initializeWorktree(gitCommit, worktree) 188 if err != nil { 189 return err 190 } 191 192 // only update version in build descriptor if required in order to save prossing time (e.g. maven case) 193 if newVersion != version { 194 err = artifact.SetVersion(newVersion) 195 if err != nil { 196 log.SetErrorCategory(log.ErrorConfiguration) 197 return errors.Wrap(err, "failed to write version") 198 } 199 } 200 201 // propagate version information to additional descriptors 202 if len(config.AdditionalTargetTools) > 0 { 203 err = propagateVersion(config, utils, &artifactOpts, version, gitCommitID, now) 204 if err != nil { 205 return err 206 } 207 } 208 209 if config.VersioningType == "cloud" { 210 certs, err := certutils.CertificateDownload(config.CustomTLSCertificateLinks, utils) 211 // commit changes and push to repository (including new version tag) 212 gitCommitID, err = pushChanges(config, newVersion, repository, worktree, now, certs) 213 if err != nil { 214 if strings.Contains(fmt.Sprint(err), "reference already exists") { 215 log.SetErrorCategory(log.ErrorCustom) 216 } 217 return errors.Wrapf(err, "failed to push changes for version '%v'", newVersion) 218 } 219 } 220 } else { 221 // propagate version information to additional descriptors 222 if len(config.AdditionalTargetTools) > 0 { 223 err = propagateVersion(config, utils, &artifactOpts, version, gitCommitID, now) 224 if err != nil { 225 return err 226 } 227 } 228 } 229 230 log.Entry().Infof("New version: '%v'", newVersion) 231 232 commonPipelineEnvironment.git.commitID = gitCommitID // this commitID changes and is not necessarily the HEAD commitID 233 commonPipelineEnvironment.artifactVersion = newVersion 234 commonPipelineEnvironment.originalArtifactVersion = version 235 commonPipelineEnvironment.git.commitMessage = gitCommitMessage 236 237 // we may replace GetVersion() above with GetCoordinates() at some point ... 238 coordinates, err := artifact.GetCoordinates() 239 if err != nil && !config.FetchCoordinates { 240 log.Entry().Warnf("fetchCoordinates is false and failed get artifact Coordinates") 241 } else if err != nil && config.FetchCoordinates { 242 return fmt.Errorf("failed to get coordinates: %w", err) 243 } else { 244 commonPipelineEnvironment.artifactID = coordinates.ArtifactID 245 commonPipelineEnvironment.groupID = coordinates.GroupID 246 commonPipelineEnvironment.packaging = coordinates.Packaging 247 } 248 249 return nil 250 } 251 252 func openGit() (gitRepository, error) { 253 workdir, _ := os.Getwd() 254 return gitUtils.PlainOpen(workdir) 255 } 256 257 func getGitCommitID(repository gitRepository) (plumbing.Hash, string, error) { 258 commitID, err := repository.ResolveRevision(plumbing.Revision("HEAD")) 259 if err != nil { 260 return plumbing.Hash{}, "", errors.Wrap(err, "failed to retrieve git commit ID") 261 } 262 // ToDo not too elegant to retrieve the commit message here, must be refactored sooner than later 263 // but to quickly address https://github.com/SAP/jenkins-library/pull/1515 let's revive this 264 commitObject, err := repository.CommitObject(*commitID) 265 if err != nil { 266 return *commitID, "", errors.Wrap(err, "failed to retrieve git commit message") 267 } 268 return *commitID, commitObject.Message, nil 269 } 270 271 func versioningTemplate(scheme string) (string, error) { 272 // generally: timestamp acts as build number providing a proper order 273 switch scheme { 274 case "docker": 275 // from Docker documentation: 276 // A tag name must be valid ASCII and may contain lowercase and uppercase letters, digits, underscores, periods and dashes. 277 // A tag name may not start with a period or a dash and may contain a maximum of 128 characters. 278 return "{{.Version}}{{if .Timestamp}}-{{.Timestamp}}{{if .CommitID}}-{{.CommitID}}{{end}}{{end}}", nil 279 case "maven": 280 // according to https://www.mojohaus.org/versions-maven-plugin/version-rules.html 281 return "{{.Version}}{{if .Timestamp}}-{{.Timestamp}}{{if .CommitID}}_{{.CommitID}}{{end}}{{end}}", nil 282 case "pep440": 283 // according to https://www.python.org/dev/peps/pep-0440/ 284 return "{{.Version}}{{if .Timestamp}}.{{.Timestamp}}{{if .CommitID}}+{{.CommitID}}{{end}}{{end}}", nil 285 case "semver2": 286 // according to https://semver.org/spec/v2.0.0.html 287 return "{{.Version}}{{if .Timestamp}}-{{.Timestamp}}{{if .CommitID}}+{{.CommitID}}{{end}}{{end}}", nil 288 } 289 return "", fmt.Errorf("versioning scheme '%v' not supported", scheme) 290 } 291 292 func calculateNewVersion(versioningTemplate, currentVersion, commitID string, includeCommitID, shortCommitID, unixTimestamp bool, t time.Time) (string, error) { 293 tmpl, err := template.New("version").Parse(versioningTemplate) 294 if err != nil { 295 return "", errors.Wrapf(err, "failed to create version template: %v", versioningTemplate) 296 } 297 298 timestamp := t.Format("20060102150405") 299 if unixTimestamp { 300 timestamp = fmt.Sprint(t.Unix()) 301 } 302 303 buf := new(bytes.Buffer) 304 versionParts := struct { 305 Version string 306 Timestamp string 307 CommitID string 308 }{ 309 Version: currentVersion, 310 Timestamp: timestamp, 311 } 312 313 if includeCommitID { 314 versionParts.CommitID = commitID 315 if shortCommitID { 316 versionParts.CommitID = commitID[0:7] 317 } 318 } 319 320 err = tmpl.Execute(buf, versionParts) 321 if err != nil { 322 return "", errors.Wrapf(err, "failed to execute versioning template: %v", versioningTemplate) 323 } 324 325 newVersion := buf.String() 326 if len(newVersion) == 0 { 327 return "", fmt.Errorf("failed calculate version, new version is '%v'", newVersion) 328 } 329 return buf.String(), nil 330 } 331 332 func initializeWorktree(gitCommit plumbing.Hash, worktree gitWorktree) error { 333 // checkout current revision in order to work on that 334 err := worktree.Checkout(&git.CheckoutOptions{Hash: gitCommit, Keep: true}) 335 if err != nil { 336 return errors.Wrap(err, "failed to initialize worktree") 337 } 338 339 return nil 340 } 341 342 func pushChanges(config *artifactPrepareVersionOptions, newVersion string, repository gitRepository, worktree gitWorktree, t time.Time, certs []byte) (string, error) { 343 344 var commitID string 345 346 commit, err := addAndCommit(config, worktree, newVersion, t) 347 if err != nil { 348 return commit.String(), err 349 } 350 351 commitID = commit.String() 352 353 tag := fmt.Sprintf("%v%v", config.TagPrefix, newVersion) 354 _, err = repository.CreateTag(tag, commit, nil) 355 if err != nil { 356 return commitID, err 357 } 358 359 ref := gitConfig.RefSpec(fmt.Sprintf("refs/tags/%v:refs/tags/%v", tag, tag)) 360 361 pushOptions := git.PushOptions{ 362 RefSpecs: []gitConfig.RefSpec{gitConfig.RefSpec(ref)}, 363 CABundle: certs, 364 } 365 366 currentRemoteOrigin, err := repository.Remote("origin") 367 if err != nil { 368 return commitID, errors.Wrap(err, "failed to retrieve current remote origin") 369 } 370 var updatedRemoteOrigin *git.Remote 371 372 urls := originUrls(repository) 373 if len(urls) == 0 { 374 log.SetErrorCategory(log.ErrorConfiguration) 375 return commitID, fmt.Errorf("no remote url maintained") 376 } 377 if strings.HasPrefix(urls[0], "http") { 378 if len(config.Username) == 0 || len(config.Password) == 0 { 379 // handling compatibility: try to use ssh in case no credentials are available 380 log.Entry().Info("git username/password missing - switching to ssh") 381 382 remoteURL := convertHTTPToSSHURL(urls[0]) 383 384 // update remote origin url to point to ssh url instead of http(s) url 385 err = repository.DeleteRemote("origin") 386 if err != nil { 387 return commitID, errors.Wrap(err, "failed to update remote origin - remove") 388 } 389 updatedRemoteOrigin, err = repository.CreateRemote(&gitConfig.RemoteConfig{Name: "origin", URLs: []string{remoteURL}}) 390 if err != nil { 391 return commitID, errors.Wrap(err, "failed to update remote origin - create") 392 } 393 394 pushOptions.Auth, err = sshAgentAuth("git") 395 if err != nil { 396 log.SetErrorCategory(log.ErrorConfiguration) 397 return commitID, errors.Wrap(err, "failed to retrieve ssh authentication") 398 } 399 log.Entry().Infof("using remote '%v'", remoteURL) 400 } else { 401 pushOptions.Auth = &http.BasicAuth{Username: config.Username, Password: config.Password} 402 } 403 } else { 404 pushOptions.Auth, err = sshAgentAuth("git") 405 if err != nil { 406 log.SetErrorCategory(log.ErrorConfiguration) 407 return commitID, errors.Wrap(err, "failed to retrieve ssh authentication") 408 } 409 } 410 411 err = repository.Push(&pushOptions) 412 if err != nil { 413 errText := fmt.Sprint(err) 414 switch { 415 case strings.Contains(errText, "ssh: handshake failed"): 416 log.SetErrorCategory(log.ErrorConfiguration) 417 case strings.Contains(errText, "Permission"): 418 log.SetErrorCategory(log.ErrorConfiguration) 419 case strings.Contains(errText, "authorization failed"): 420 log.SetErrorCategory(log.ErrorConfiguration) 421 case strings.Contains(errText, "authentication required"): 422 log.SetErrorCategory(log.ErrorConfiguration) 423 case strings.Contains(errText, "knownhosts:"): 424 err = errors.Wrap(err, "known_hosts file seems invalid") 425 log.SetErrorCategory(log.ErrorConfiguration) 426 case strings.Contains(errText, "unable to find any valid known_hosts file"): 427 log.SetErrorCategory(log.ErrorConfiguration) 428 case strings.Contains(errText, "connection timed out"): 429 log.SetErrorCategory(log.ErrorInfrastructure) 430 } 431 return commitID, err 432 } 433 434 if updatedRemoteOrigin != currentRemoteOrigin { 435 err = repository.DeleteRemote("origin") 436 if err != nil { 437 return commitID, errors.Wrap(err, "failed to restore remote origin - remove") 438 } 439 _, err := repository.CreateRemote(currentRemoteOrigin.Config()) 440 if err != nil { 441 return commitID, errors.Wrap(err, "failed to restore remote origin - create") 442 } 443 } 444 445 return commitID, nil 446 } 447 448 func addAndCommit(config *artifactPrepareVersionOptions, worktree gitWorktree, newVersion string, t time.Time) (plumbing.Hash, error) { 449 //maybe more options are required: https://github.com/go-git/go-git/blob/master/_examples/commit/main.go 450 commit, err := worktree.Commit(fmt.Sprintf("update version %v", newVersion), &git.CommitOptions{All: true, Author: &object.Signature{Name: config.CommitUserName, When: t}}) 451 if err != nil { 452 return commit, errors.Wrap(err, "failed to commit new version") 453 } 454 return commit, nil 455 } 456 457 func originUrls(repository gitRepository) []string { 458 remote, err := repository.Remote("origin") 459 if err != nil || remote == nil { 460 return []string{} 461 } 462 return remote.Config().URLs 463 } 464 465 func convertHTTPToSSHURL(url string) string { 466 sshURL := strings.Replace(url, "https://", "git@", 1) 467 return strings.Replace(sshURL, "/", ":", 1) 468 } 469 470 func templateCompatibility(groovyTemplate string) (versioningType string, useTimestamp bool, useCommitID bool) { 471 useTimestamp = strings.Contains(groovyTemplate, "${timestamp}") 472 useCommitID = strings.Contains(groovyTemplate, "${commitId") 473 474 versioningType = "library" 475 476 if useTimestamp { 477 versioningType = "cloud" 478 } 479 480 return 481 } 482 483 func calculateCloudVersion(artifact versioning.Artifact, config *artifactPrepareVersionOptions, version, gitCommitID string, timestamp time.Time) (string, error) { 484 versioningTempl, err := versioningTemplate(artifact.VersioningScheme()) 485 if err != nil { 486 log.SetErrorCategory(log.ErrorConfiguration) 487 return "", errors.Wrapf(err, "failed to get versioning template for scheme '%v'", artifact.VersioningScheme()) 488 } 489 490 newVersion, err := calculateNewVersion(versioningTempl, version, gitCommitID, config.IncludeCommitID, config.ShortCommitID, config.UnixTimestamp, timestamp) 491 if err != nil { 492 return "", errors.Wrap(err, "failed to calculate new version") 493 } 494 return newVersion, nil 495 } 496 497 func propagateVersion(config *artifactPrepareVersionOptions, utils artifactPrepareVersionUtils, artifactOpts *versioning.Options, version, gitCommitID string, now time.Time) error { 498 var err error 499 500 if len(config.AdditionalTargetDescriptors) > 0 && len(config.AdditionalTargetTools) != len(config.AdditionalTargetDescriptors) { 501 log.SetErrorCategory(log.ErrorConfiguration) 502 return fmt.Errorf("additionalTargetDescriptors cannot have a different number of entries than additionalTargetTools") 503 } 504 505 for i, targetTool := range config.AdditionalTargetTools { 506 507 var buildDescriptors []string 508 if len(config.AdditionalTargetDescriptors) > 0 { 509 buildDescriptors, err = utils.Glob(config.AdditionalTargetDescriptors[i]) 510 if err != nil { 511 log.SetErrorCategory(log.ErrorConfiguration) 512 return fmt.Errorf("failed to retrieve build descriptors: %w", err) 513 } 514 } 515 516 if len(buildDescriptors) == 0 { 517 buildDescriptors = append(buildDescriptors, "") 518 } 519 520 // in case of helm, make sure that app version is adapted as well 521 artifactOpts.HelmUpdateAppVersion = true 522 523 for _, buildDescriptor := range buildDescriptors { 524 targetArtifact, err := versioning.GetArtifact(targetTool, buildDescriptor, artifactOpts, utils) 525 if err != nil { 526 log.SetErrorCategory(log.ErrorConfiguration) 527 return fmt.Errorf("failed to retrieve artifact: %w", err) 528 } 529 530 // Make sure that version type fits to target artifact 531 descriptorVersion := version 532 if config.VersioningType == "cloud" || config.VersioningType == "cloud_noTag" { 533 descriptorVersion, err = calculateCloudVersion(targetArtifact, config, version, gitCommitID, now) 534 if err != nil { 535 return err 536 } 537 } 538 err = targetArtifact.SetVersion(descriptorVersion) 539 if err != nil { 540 return fmt.Errorf("failed to set additional target version for '%v': %w", targetTool, err) 541 } 542 } 543 } 544 return nil 545 }