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