github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/cd-service/pkg/repository/transformer.go (about) 1 /*This file is part of kuberpult. 2 3 Kuberpult is free software: you can redistribute it and/or modify 4 it under the terms of the Expat(MIT) License as published by 5 the Free Software Foundation. 6 7 Kuberpult is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 MIT License for more details. 11 12 You should have received a copy of the MIT License 13 along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>. 14 15 Copyright 2023 freiheit.com*/ 16 17 package repository 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io" 25 "io/fs" 26 "os" 27 "path" 28 "sort" 29 "strconv" 30 "strings" 31 "time" 32 33 "github.com/DataDog/datadog-go/v5/statsd" 34 "github.com/freiheit-com/kuberpult/pkg/metrics" 35 "github.com/freiheit-com/kuberpult/pkg/ptr" 36 37 "github.com/freiheit-com/kuberpult/pkg/uuid" 38 "github.com/freiheit-com/kuberpult/services/cd-service/pkg/event" 39 git "github.com/libgit2/git2go/v34" 40 41 "github.com/freiheit-com/kuberpult/pkg/grpc" 42 "github.com/freiheit-com/kuberpult/pkg/valid" 43 44 "github.com/freiheit-com/kuberpult/pkg/logger" 45 46 yaml3 "gopkg.in/yaml.v3" 47 48 api "github.com/freiheit-com/kuberpult/pkg/api/v1" 49 "github.com/freiheit-com/kuberpult/pkg/auth" 50 "github.com/freiheit-com/kuberpult/services/cd-service/pkg/config" 51 "github.com/freiheit-com/kuberpult/services/cd-service/pkg/mapper" 52 billy "github.com/go-git/go-billy/v5" 53 "github.com/go-git/go-billy/v5/util" 54 "github.com/hexops/gotextdiff" 55 "github.com/hexops/gotextdiff/myers" 56 "github.com/hexops/gotextdiff/span" 57 ) 58 59 const ( 60 queueFileName = "queued_version" 61 yamlParsingError = "# yaml parsing error" 62 fieldSourceAuthor = "source_author" 63 fieldSourceMessage = "source_message" 64 fieldSourceCommitId = "source_commit_id" 65 fieldDisplayVersion = "display_version" 66 fieldSourceRepoUrl = "sourceRepoUrl" // urgh, inconsistent 67 fieldCreatedAt = "created_at" 68 fieldTeam = "team" 69 fieldNextCommidId = "nextCommit" 70 fieldPreviousCommitId = "previousCommit" 71 // number of old releases that will ALWAYS be kept in addition to the ones that are deployed: 72 keptVersionsOnCleanup = 20 73 ) 74 75 func versionToString(Version uint64) string { 76 return strconv.FormatUint(Version, 10) 77 } 78 79 func releasesDirectory(fs billy.Filesystem, application string) string { 80 return fs.Join("applications", application, "releases") 81 } 82 83 func applicationDirectory(fs billy.Filesystem, application string) string { 84 return fs.Join("applications", application) 85 } 86 87 func environmentDirectory(fs billy.Filesystem, environment string) string { 88 return fs.Join("environments", environment) 89 } 90 91 func environmentApplicationDirectory(fs billy.Filesystem, environment, application string) string { 92 return fs.Join("environments", environment, "applications", application) 93 } 94 95 func releasesDirectoryWithVersion(fs billy.Filesystem, application string, version uint64) string { 96 return fs.Join(releasesDirectory(fs, application), versionToString(version)) 97 } 98 99 func manifestDirectoryWithReleasesVersion(fs billy.Filesystem, application string, version uint64) string { 100 return fs.Join(releasesDirectoryWithVersion(fs, application, version), "environments") 101 } 102 103 func commitDirectory(fs billy.Filesystem, commit string) string { 104 return fs.Join("commits", commit[:2], commit[2:]) 105 } 106 107 func commitApplicationDirectory(fs billy.Filesystem, commit, application string) string { 108 return fs.Join(commitDirectory(fs, commit), "applications", application) 109 } 110 111 func commitEventDir(fs billy.Filesystem, commit, eventId string) string { 112 return fs.Join(commitDirectory(fs, commit), "events", eventId) 113 } 114 115 func GetEnvironmentLocksCount(fs billy.Filesystem, env string) float64 { 116 envLocksCount := 0 117 envDir := environmentDirectory(fs, env) 118 locksDir := fs.Join(envDir, "locks") 119 if entries, _ := fs.ReadDir(locksDir); entries != nil { 120 envLocksCount += len(entries) 121 } 122 return float64(envLocksCount) 123 } 124 125 func GetEnvironmentApplicationLocksCount(fs billy.Filesystem, environment, application string) float64 { 126 envAppLocksCount := 0 127 appDir := environmentApplicationDirectory(fs, environment, application) 128 locksDir := fs.Join(appDir, "locks") 129 if entries, _ := fs.ReadDir(locksDir); entries != nil { 130 envAppLocksCount += len(entries) 131 } 132 return float64(envAppLocksCount) 133 } 134 135 func GaugeEnvLockMetric(fs billy.Filesystem, env string) { 136 if ddMetrics != nil { 137 ddMetrics.Gauge("env_lock_count", GetEnvironmentLocksCount(fs, env), []string{"env:" + env}, 1) //nolint: errcheck 138 } 139 } 140 141 func GaugeEnvAppLockMetric(fs billy.Filesystem, env, app string) { 142 if ddMetrics != nil { 143 ddMetrics.Gauge("app_lock_count", GetEnvironmentApplicationLocksCount(fs, env, app), []string{"app:" + app, "env:" + env}, 1) //nolint: errcheck 144 } 145 } 146 147 func GaugeDeploymentMetric(_ context.Context, env, app string, timeInMinutes float64) error { 148 if ddMetrics != nil { 149 // store the time since the last deployment in minutes: 150 err := ddMetrics.Gauge( 151 "lastDeployed", 152 timeInMinutes, 153 []string{metrics.EventTagApplication + ":" + app, metrics.EventTagEnvironment + ":" + env}, 154 1) 155 return err 156 } 157 return nil 158 } 159 160 func sortFiles(gs []os.FileInfo) func(i int, j int) bool { 161 return func(i, j int) bool { 162 iIndex := gs[i].Name() 163 jIndex := gs[j].Name() 164 return iIndex < jIndex 165 } 166 } 167 168 func UpdateDatadogMetrics(ctx context.Context, state *State, changes *TransformerResult, now time.Time) error { 169 filesystem := state.Filesystem 170 if ddMetrics == nil { 171 return nil 172 } 173 configs, err := state.GetEnvironmentConfigs() 174 if err != nil { 175 return err 176 } 177 // sorting the environments to get a deterministic order of events: 178 var configKeys []string = nil 179 for k := range configs { 180 configKeys = append(configKeys, k) 181 } 182 sort.Strings(configKeys) 183 for i := range configKeys { 184 env := configKeys[i] 185 GaugeEnvLockMetric(filesystem, env) 186 appsDir := filesystem.Join(environmentDirectory(filesystem, env), "applications") 187 if entries, _ := filesystem.ReadDir(appsDir); entries != nil { 188 // according to the docs, entries should already be sorted, but turns out it is not, so we sort it: 189 sort.Slice(entries, sortFiles(entries)) 190 for _, app := range entries { 191 GaugeEnvAppLockMetric(filesystem, env, app.Name()) 192 193 _, deployedAtTimeUtc, err := state.GetDeploymentMetaData(env, app.Name()) 194 if err != nil { 195 return err 196 } 197 timeDiff := now.Sub(deployedAtTimeUtc) 198 err = GaugeDeploymentMetric(ctx, env, app.Name(), timeDiff.Minutes()) 199 if err != nil { 200 return err 201 } 202 } 203 } 204 } 205 if changes != nil && ddMetrics != nil { 206 for i := range changes.ChangedApps { 207 oneChange := changes.ChangedApps[i] 208 teamMessage := func() string { 209 if oneChange.Team != "" { 210 return fmt.Sprintf(" for team %s", oneChange.Team) 211 } 212 return "" 213 }() 214 evt := statsd.Event{ 215 Hostname: "", 216 AggregationKey: "", 217 Priority: "", 218 SourceTypeName: "", 219 AlertType: "", 220 Title: "Kuberpult app deployed", 221 Text: fmt.Sprintf("Kuberpult has deployed %s to %s%s", oneChange.App, oneChange.Env, teamMessage), 222 Timestamp: now, 223 Tags: []string{ 224 "kuberpult.application:" + oneChange.App, 225 "kuberpult.environment:" + oneChange.Env, 226 "kuberpult.team:" + oneChange.Team, 227 }, 228 } 229 err := ddMetrics.Event(&evt) 230 if err != nil { 231 return err 232 } 233 } 234 } 235 return nil 236 } 237 238 func RegularlySendDatadogMetrics(repo Repository, interval time.Duration, callBack func(repository Repository)) { 239 metricEventTimer := time.NewTicker(interval * time.Second) 240 for range metricEventTimer.C { 241 callBack(repo) 242 } 243 } 244 245 func GetRepositoryStateAndUpdateMetrics(ctx context.Context, repo Repository) { 246 repoState := repo.State() 247 if err := UpdateDatadogMetrics(ctx, repoState, nil, time.Now()); err != nil { 248 panic(err.Error()) 249 } 250 } 251 252 // A Transformer updates the files in the worktree 253 type Transformer interface { 254 Transform(context.Context, *State, TransformerContext) (commitMsg string, e error) 255 } 256 257 type TransformerContext interface { 258 Execute(Transformer) error 259 AddAppEnv(app string, env string, team string) 260 DeleteEnvFromApp(app string, env string) 261 } 262 263 func RunTransformer(ctx context.Context, t Transformer, s *State) (string, *TransformerResult, error) { 264 runner := transformerRunner{ 265 ChangedApps: nil, 266 DeletedRootApps: nil, 267 Commits: nil, 268 Context: ctx, 269 State: s, 270 Stack: [][]string{nil}, 271 } 272 if err := runner.Execute(t); err != nil { 273 return "", nil, err 274 } 275 commitMsg := "" 276 if len(runner.Stack[0]) > 0 { 277 commitMsg = runner.Stack[0][0] 278 } 279 return commitMsg, &TransformerResult{ 280 ChangedApps: runner.ChangedApps, 281 DeletedRootApps: runner.DeletedRootApps, 282 Commits: runner.Commits, 283 }, nil 284 } 285 286 type transformerRunner struct { 287 Context context.Context 288 State *State 289 // Stores the current stack of commit messages. Each entry of 290 // the outer slice corresponds to a step being executed. Each 291 // entry of the inner slices correspond to a message generated 292 // by that step. 293 Stack [][]string 294 ChangedApps []AppEnv 295 DeletedRootApps []RootApp 296 Commits *CommitIds 297 } 298 299 func (r *transformerRunner) Execute(t Transformer) error { 300 r.Stack = append(r.Stack, nil) 301 msg, err := t.Transform(r.Context, r.State, r) 302 if err != nil { 303 return err 304 } 305 idx := len(r.Stack) - 1 306 if len(r.Stack[idx]) != 0 { 307 if msg != "" { 308 msg = msg + "\n" + strings.Join(r.Stack[idx], "\n") 309 } else { 310 msg = strings.Join(r.Stack[idx], "\n") 311 } 312 } 313 if msg != "" { 314 r.Stack[idx-1] = append(r.Stack[idx-1], msg) 315 } 316 r.Stack = r.Stack[:idx] 317 return nil 318 } 319 320 func (r *transformerRunner) AddAppEnv(app string, env string, team string) { 321 r.ChangedApps = append(r.ChangedApps, AppEnv{ 322 App: app, 323 Env: env, 324 Team: team, 325 }) 326 } 327 328 func (r *transformerRunner) DeleteEnvFromApp(app string, env string) { 329 r.ChangedApps = append(r.ChangedApps, AppEnv{ 330 Team: "", 331 App: app, 332 Env: env, 333 }) 334 r.DeletedRootApps = append(r.DeletedRootApps, RootApp{ 335 Env: env, 336 }) 337 } 338 339 type CreateApplicationVersion struct { 340 Authentication 341 Version uint64 342 Application string 343 Manifests map[string]string 344 SourceCommitId string 345 SourceAuthor string 346 SourceMessage string 347 SourceRepoUrl string 348 Team string 349 DisplayVersion string 350 WriteCommitData bool 351 PreviousCommit string 352 NextCommit string 353 } 354 355 type ctxMarkerGenerateUuid struct{} 356 357 var ( 358 ctxMarkerGenerateUuidKey = &ctxMarkerGenerateUuid{} 359 ) 360 361 func GetLastRelease(fs billy.Filesystem, application string) (uint64, error) { 362 var err error 363 releasesDir := releasesDirectory(fs, application) 364 err = fs.MkdirAll(releasesDir, 0777) 365 if err != nil { 366 return 0, err 367 } 368 if entries, err := fs.ReadDir(releasesDir); err != nil { 369 return 0, err 370 } else { 371 var lastRelease uint64 = 0 372 for _, e := range entries { 373 if i, err := strconv.ParseUint(e.Name(), 10, 64); err != nil { 374 //TODO(HVG): decide what to do with bad named releases 375 } else { 376 if i > lastRelease { 377 lastRelease = i 378 } 379 } 380 } 381 return lastRelease, nil 382 } 383 } 384 385 func (c *CreateApplicationVersion) Transform( 386 ctx context.Context, 387 state *State, 388 t TransformerContext, 389 ) (string, error) { 390 version, err := c.calculateVersion(state) 391 if err != nil { 392 return "", err 393 } 394 fs := state.Filesystem 395 if !valid.ApplicationName(c.Application) { 396 return "", GetCreateReleaseAppNameTooLong(c.Application, valid.AppNameRegExp, valid.MaxAppNameLen) 397 } 398 releaseDir := releasesDirectoryWithVersion(fs, c.Application, version) 399 appDir := applicationDirectory(fs, c.Application) 400 if err = fs.MkdirAll(releaseDir, 0777); err != nil { 401 return "", GetCreateReleaseGeneralFailure(err) 402 } 403 404 var checkForInvalidCommitId = func(commitId, helperText string) { 405 if !valid.SHA1CommitID(commitId) { 406 logger.FromContext(ctx). 407 Sugar(). 408 Warnf("%s commit ID is not a valid SHA1 hash, should be exactly 40 characters [0-9a-fA-F] %s\n", commitId, helperText) 409 } 410 } 411 412 checkForInvalidCommitId(c.SourceCommitId, "Source") 413 checkForInvalidCommitId(c.PreviousCommit, "Previous") 414 checkForInvalidCommitId(c.NextCommit, "Next") 415 416 configs, err := state.GetEnvironmentConfigs() 417 if err != nil { 418 if errors.Is(err, InvalidJson) { 419 return "", err 420 } 421 return "", GetCreateReleaseGeneralFailure(err) 422 } 423 424 if c.SourceCommitId != "" { 425 c.SourceCommitId = strings.ToLower(c.SourceCommitId) 426 if err := util.WriteFile(fs, fs.Join(releaseDir, fieldSourceCommitId), []byte(c.SourceCommitId), 0666); err != nil { 427 return "", GetCreateReleaseGeneralFailure(err) 428 } 429 } 430 431 if c.SourceAuthor != "" { 432 if err := util.WriteFile(fs, fs.Join(releaseDir, fieldSourceAuthor), []byte(c.SourceAuthor), 0666); err != nil { 433 return "", GetCreateReleaseGeneralFailure(err) 434 } 435 } 436 if c.SourceMessage != "" { 437 if err := util.WriteFile(fs, fs.Join(releaseDir, fieldSourceMessage), []byte(c.SourceMessage), 0666); err != nil { 438 return "", GetCreateReleaseGeneralFailure(err) 439 } 440 } 441 if c.DisplayVersion != "" { 442 if err := util.WriteFile(fs, fs.Join(releaseDir, fieldDisplayVersion), []byte(c.DisplayVersion), 0666); err != nil { 443 return "", GetCreateReleaseGeneralFailure(err) 444 } 445 } 446 if err := util.WriteFile(fs, fs.Join(releaseDir, fieldCreatedAt), []byte(getTimeNow(ctx).Format(time.RFC3339)), 0666); err != nil { 447 return "", GetCreateReleaseGeneralFailure(err) 448 } 449 if c.Team != "" { 450 if err := util.WriteFile(fs, fs.Join(appDir, fieldTeam), []byte(c.Team), 0666); err != nil { 451 return "", GetCreateReleaseGeneralFailure(err) 452 } 453 } 454 if c.SourceRepoUrl != "" { 455 if err := util.WriteFile(fs, fs.Join(appDir, fieldSourceRepoUrl), []byte(c.SourceRepoUrl), 0666); err != nil { 456 return "", GetCreateReleaseGeneralFailure(err) 457 } 458 } 459 isLatest, err := isLatestsVersion(state, c.Application, version) 460 if err != nil { 461 return "", GetCreateReleaseGeneralFailure(err) 462 } 463 if !isLatest { 464 // check that we can actually backfill this version 465 oldVersions, err := findOldApplicationVersions(state, c.Application) 466 if err != nil { 467 return "", GetCreateReleaseGeneralFailure(err) 468 } 469 for _, oldVersion := range oldVersions { 470 if version == oldVersion { 471 return "", GetCreateReleaseTooOld() 472 } 473 } 474 } 475 476 var allEnvsOfThisApp []string = nil 477 478 for env := range c.Manifests { 479 allEnvsOfThisApp = append(allEnvsOfThisApp, env) 480 } 481 gen := getGenerator(ctx) 482 eventUuid := gen.Generate() 483 if c.WriteCommitData { 484 err = writeCommitData(ctx, c.SourceCommitId, c.SourceMessage, c.Application, eventUuid, allEnvsOfThisApp, c.PreviousCommit, c.NextCommit, fs) 485 if err != nil { 486 return "", GetCreateReleaseGeneralFailure(err) 487 } 488 } 489 490 for env, man := range c.Manifests { 491 err := state.checkUserPermissions(ctx, env, c.Application, auth.PermissionCreateRelease, c.Team, c.RBACConfig) 492 if err != nil { 493 return "", GetCreateReleaseGeneralFailure(err) 494 } 495 envDir := fs.Join(releaseDir, "environments", env) 496 497 config, found := configs[env] 498 hasUpstream := false 499 if found { 500 hasUpstream = config.Upstream != nil 501 } 502 503 if err = fs.MkdirAll(envDir, 0777); err != nil { 504 return "", GetCreateReleaseGeneralFailure(err) 505 } 506 if err := util.WriteFile(fs, fs.Join(envDir, "manifests.yaml"), []byte(man), 0666); err != nil { 507 return "", GetCreateReleaseGeneralFailure(err) 508 } 509 teamOwner, err := state.GetApplicationTeamOwner(c.Application) 510 if err != nil { 511 return "", err 512 } 513 t.AddAppEnv(c.Application, env, teamOwner) 514 if hasUpstream && config.Upstream.Latest && isLatest { 515 d := &DeployApplicationVersion{ 516 SourceTrain: nil, 517 Environment: env, 518 Application: c.Application, 519 Version: version, // the train should queue deployments, instead of giving up: 520 LockBehaviour: api.LockBehavior_RECORD, 521 Authentication: c.Authentication, 522 WriteCommitData: c.WriteCommitData, 523 } 524 err := t.Execute(d) 525 if err != nil { 526 _, ok := err.(*LockedError) 527 if ok { 528 continue // LockedErrors are expected 529 } else { 530 return "", GetCreateReleaseGeneralFailure(err) 531 } 532 } 533 } 534 } 535 return fmt.Sprintf("created version %d of %q", version, c.Application), nil 536 } 537 538 func getGenerator(ctx context.Context) uuid.GenerateUUIDs { 539 gen, ok := ctx.Value(ctxMarkerGenerateUuidKey).(uuid.GenerateUUIDs) 540 if !ok || gen == nil { 541 return uuid.RealUUIDGenerator{} 542 } 543 return gen 544 } 545 546 func AddGeneratorToContext(ctx context.Context, gen uuid.GenerateUUIDs) context.Context { 547 return context.WithValue(ctx, ctxMarkerGenerateUuidKey, gen) 548 } 549 550 func writeCommitData(ctx context.Context, sourceCommitId string, sourceMessage string, app string, eventId string, environments []string, previousCommitId string, nextCommitId string, fs billy.Filesystem) error { 551 if !valid.SHA1CommitID(sourceCommitId) { 552 return nil 553 } 554 commitDir := commitDirectory(fs, sourceCommitId) 555 if err := fs.MkdirAll(commitDir, 0777); err != nil { 556 return GetCreateReleaseGeneralFailure(err) 557 } 558 if err := util.WriteFile(fs, fs.Join(commitDir, ".empty"), make([]byte, 0), 0666); err != nil { 559 return GetCreateReleaseGeneralFailure(err) 560 } 561 562 if previousCommitId != "" && valid.SHA1CommitID(previousCommitId) { 563 if err := writeNextPrevInfo(ctx, sourceCommitId, strings.ToLower(previousCommitId), fieldPreviousCommitId, app, fs); err != nil { 564 return GetCreateReleaseGeneralFailure(err) 565 } 566 } 567 if nextCommitId != "" && valid.SHA1CommitID(nextCommitId) { 568 if err := writeNextPrevInfo(ctx, sourceCommitId, strings.ToLower(nextCommitId), fieldNextCommidId, app, fs); err != nil { 569 return GetCreateReleaseGeneralFailure(err) 570 } 571 } 572 573 commitAppDir := commitApplicationDirectory(fs, sourceCommitId, app) 574 if err := fs.MkdirAll(commitAppDir, 0777); err != nil { 575 return GetCreateReleaseGeneralFailure(err) 576 } 577 if err := util.WriteFile(fs, fs.Join(commitDir, ".gitkeep"), make([]byte, 0), 0666); err != nil { 578 return err 579 } 580 if err := util.WriteFile(fs, fs.Join(commitDir, "source_message"), []byte(sourceMessage), 0666); err != nil { 581 return GetCreateReleaseGeneralFailure(err) 582 } 583 584 if err := util.WriteFile(fs, fs.Join(commitAppDir, ".gitkeep"), make([]byte, 0), 0666); err != nil { 585 return GetCreateReleaseGeneralFailure(err) 586 } 587 envMap := make(map[string]struct{}, len(environments)) 588 for _, env := range environments { 589 envMap[env] = struct{}{} 590 } 591 err := writeEvent(eventId, sourceCommitId, fs, &event.NewRelease{ 592 Environments: envMap, 593 }) 594 if err != nil { 595 return fmt.Errorf("error while writing event: %v", err) 596 } 597 return nil 598 } 599 600 func writeNextPrevInfo(ctx context.Context, sourceCommitId string, otherCommitId string, fieldSource string, application string, fs billy.Filesystem) error { 601 602 otherCommitId = strings.ToLower(otherCommitId) 603 sourceCommitDir := commitDirectory(fs, sourceCommitId) 604 605 otherCommitDir := commitDirectory(fs, otherCommitId) 606 607 if _, err := fs.Stat(otherCommitDir); err != nil { 608 logger.FromContext(ctx).Sugar().Warnf( 609 "Could not find the previous commit while trying to create a new release for commit %s and application %s. This is expected when `git.enableWritingCommitData` was just turned on, however it should not happen multiple times.", otherCommitId, application, otherCommitDir) 610 return nil 611 } 612 613 if err := util.WriteFile(fs, fs.Join(sourceCommitDir, fieldSource), []byte(otherCommitId), 0666); err != nil { 614 return err 615 } 616 fieldOther := "" 617 if otherCommitId != "" { 618 619 if fieldSource == fieldPreviousCommitId { 620 fieldOther = fieldNextCommidId 621 } else { 622 fieldOther = fieldPreviousCommitId 623 } 624 625 //This is a workaround. util.WriteFile does NOT truncate file contents, so we simply delete the file before writing. 626 if err := fs.Remove(fs.Join(otherCommitDir, fieldOther)); err != nil && !errors.Is(err, os.ErrNotExist) { 627 return err 628 } 629 630 if err := util.WriteFile(fs, fs.Join(otherCommitDir, fieldOther), []byte(sourceCommitId), 0666); err != nil { 631 return err 632 } 633 } 634 return nil 635 } 636 637 func writeEvent( 638 eventId string, 639 sourceCommitId string, 640 filesystem billy.Filesystem, 641 ev event.Event, 642 ) error { 643 eventDir := commitEventDir(filesystem, sourceCommitId, eventId) 644 if err := event.Write(filesystem, eventDir, ev); err != nil { 645 return fmt.Errorf( 646 "could not write an event for commit %s for uuid %s, error: %w", 647 sourceCommitId, eventId, err) 648 } 649 return nil 650 651 } 652 653 func (c *CreateApplicationVersion) calculateVersion(state *State) (uint64, error) { 654 bfs := state.Filesystem 655 if c.Version == 0 { 656 lastRelease, err := GetLastRelease(bfs, c.Application) 657 if err != nil { 658 return 0, err 659 } 660 return lastRelease + 1, nil 661 } else { 662 // check that the version doesn't already exist 663 dir := releasesDirectoryWithVersion(bfs, c.Application, c.Version) 664 _, err := bfs.Stat(dir) 665 if err != nil { 666 if !errors.Is(err, fs.ErrNotExist) { 667 return 0, err 668 } 669 } else { 670 // check if version differs 671 return 0, c.sameAsExisting(state, c.Version) 672 } 673 // TODO: check GC here 674 return c.Version, nil 675 } 676 } 677 678 func (c *CreateApplicationVersion) sameAsExisting(state *State, version uint64) error { 679 fs := state.Filesystem 680 releaseDir := releasesDirectoryWithVersion(fs, c.Application, version) 681 appDir := applicationDirectory(fs, c.Application) 682 if c.SourceCommitId != "" { 683 existingSourceCommitId, err := util.ReadFile(fs, fs.Join(releaseDir, fieldSourceCommitId)) 684 if err != nil { 685 return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_COMMIT_ID, "") 686 } 687 existingSourceCommitIdStr := string(existingSourceCommitId) 688 if existingSourceCommitIdStr != c.SourceCommitId { 689 return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_COMMIT_ID, createUnifiedDiff(existingSourceCommitIdStr, c.SourceCommitId, "")) 690 } 691 } 692 if c.SourceAuthor != "" { 693 existingSourceAuthor, err := util.ReadFile(fs, fs.Join(releaseDir, fieldSourceAuthor)) 694 if err != nil { 695 return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_AUTHOR, "") 696 } 697 existingSourceAuthorStr := string(existingSourceAuthor) 698 if existingSourceAuthorStr != c.SourceAuthor { 699 return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_AUTHOR, createUnifiedDiff(existingSourceAuthorStr, c.SourceAuthor, "")) 700 } 701 } 702 if c.SourceMessage != "" { 703 existingSourceMessage, err := util.ReadFile(fs, fs.Join(releaseDir, fieldSourceMessage)) 704 if err != nil { 705 return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_MESSAGE, "") 706 } 707 existingSourceMessageStr := string(existingSourceMessage) 708 if existingSourceMessageStr != c.SourceMessage { 709 return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_MESSAGE, createUnifiedDiff(existingSourceMessageStr, c.SourceMessage, "")) 710 } 711 } 712 if c.DisplayVersion != "" { 713 existingDisplayVersion, err := util.ReadFile(fs, fs.Join(releaseDir, fieldDisplayVersion)) 714 if err != nil { 715 return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_DISPLAY_VERSION, "") 716 } 717 existingDisplayVersionStr := string(existingDisplayVersion) 718 if existingDisplayVersionStr != c.DisplayVersion { 719 return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_DISPLAY_VERSION, createUnifiedDiff(existingDisplayVersionStr, c.DisplayVersion, "")) 720 } 721 } 722 if c.Team != "" { 723 existingTeam, err := util.ReadFile(fs, fs.Join(appDir, fieldTeam)) 724 if err != nil { 725 return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_TEAM, "") 726 } 727 existingTeamStr := string(existingTeam) 728 if existingTeamStr != c.Team { 729 return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_TEAM, createUnifiedDiff(existingTeamStr, c.Team, "")) 730 } 731 } 732 if c.SourceRepoUrl != "" { 733 existingSourceRepoUrl, err := util.ReadFile(fs, fs.Join(releaseDir, fieldSourceCommitId)) 734 if err != nil { 735 return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_REPO_URL, "") 736 } 737 existingSourceRepoUrlStr := string(existingSourceRepoUrl) 738 if existingSourceRepoUrlStr != c.SourceRepoUrl { 739 return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_SOURCE_REPO_URL, createUnifiedDiff(existingSourceRepoUrlStr, c.SourceRepoUrl, "")) 740 } 741 } 742 for env, man := range c.Manifests { 743 envDir := fs.Join(releaseDir, "environments", env) 744 existingMan, err := util.ReadFile(fs, fs.Join(envDir, "manifests.yaml")) 745 if err != nil { 746 return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_MANIFESTS, fmt.Sprintf("manifest missing for env %s", env)) 747 } 748 existingManStr := string(existingMan) 749 if canonicalizeYaml(existingManStr) != canonicalizeYaml(man) { 750 return GetCreateReleaseAlreadyExistsDifferent(api.DifferingField_MANIFESTS, createUnifiedDiff(existingManStr, man, fmt.Sprintf("%s-", env))) 751 } 752 } 753 return GetCreateReleaseAlreadyExistsSame() 754 } 755 756 type RawNode struct{ *yaml3.Node } 757 758 func (n *RawNode) UnmarshalYAML(node *yaml3.Node) error { 759 n.Node = node 760 return nil 761 } 762 763 func canonicalizeYaml(unformatted string) string { 764 var target RawNode 765 if errDeserial := yaml3.Unmarshal([]byte(unformatted), &target); errDeserial != nil { 766 return yamlParsingError // we only use this for comparisons 767 } 768 if canonicalData, errSerial := yaml3.Marshal(target.Node); errSerial == nil { 769 return string(canonicalData) 770 } else { 771 return yamlParsingError // only for comparisons 772 } 773 } 774 775 func createUnifiedDiff(existingValue string, requestValue string, prefix string) string { 776 existingValueStr := string(existingValue) 777 existingFilename := fmt.Sprintf("%sexisting", prefix) 778 requestFilename := fmt.Sprintf("%srequest", prefix) 779 edits := myers.ComputeEdits(span.URIFromPath(existingFilename), existingValueStr, string(requestValue)) 780 return fmt.Sprint(gotextdiff.ToUnified(existingFilename, requestFilename, existingValueStr, edits)) 781 } 782 783 func isLatestsVersion(state *State, application string, version uint64) (bool, error) { 784 rels, err := state.GetApplicationReleases(application) 785 if err != nil { 786 return false, err 787 } 788 for _, r := range rels { 789 if r > version { 790 return false, nil 791 } 792 } 793 return true, nil 794 } 795 796 type CreateUndeployApplicationVersion struct { 797 Authentication 798 Application string 799 WriteCommitData bool 800 } 801 802 func (c *CreateUndeployApplicationVersion) Transform( 803 ctx context.Context, 804 state *State, 805 t TransformerContext, 806 ) (string, error) { 807 fs := state.Filesystem 808 lastRelease, err := GetLastRelease(fs, c.Application) 809 if err != nil { 810 return "", err 811 } 812 if lastRelease == 0 { 813 return "", fmt.Errorf("cannot undeploy non-existing application '%v'", c.Application) 814 } 815 816 releaseDir := releasesDirectoryWithVersion(fs, c.Application, lastRelease+1) 817 if err = fs.MkdirAll(releaseDir, 0777); err != nil { 818 return "", err 819 } 820 821 configs, err := state.GetEnvironmentConfigs() 822 if err != nil { 823 return "", err 824 } 825 // this is a flag to indicate that this is the special "undeploy" version 826 if err := util.WriteFile(fs, fs.Join(releaseDir, "undeploy"), []byte(""), 0666); err != nil { 827 return "", err 828 } 829 if err := util.WriteFile(fs, fs.Join(releaseDir, fieldCreatedAt), []byte(getTimeNow(ctx).Format(time.RFC3339)), 0666); err != nil { 830 return "", err 831 } 832 for env := range configs { 833 err := state.checkUserPermissions(ctx, env, c.Application, auth.PermissionCreateUndeploy, "", c.RBACConfig) 834 if err != nil { 835 return "", err 836 } 837 envDir := fs.Join(releaseDir, "environments", env) 838 839 config, found := configs[env] 840 hasUpstream := false 841 if found { 842 hasUpstream = config.Upstream != nil 843 } 844 845 if err = fs.MkdirAll(envDir, 0777); err != nil { 846 return "", err 847 } 848 // note that the manifest is empty here! 849 // but actually it's not quite empty! 850 // The function we are using in DeployApplication version is `util.WriteFile`. And that does not allow overwriting files with empty content. 851 // We work around this unusual behavior by writing a space into the file 852 if err := util.WriteFile(fs, fs.Join(envDir, "manifests.yaml"), []byte(" "), 0666); err != nil { 853 return "", err 854 } 855 teamOwner, err := state.GetApplicationTeamOwner(c.Application) 856 if err != nil { 857 return "", err 858 } 859 t.AddAppEnv(c.Application, env, teamOwner) 860 if hasUpstream && config.Upstream.Latest { 861 d := &DeployApplicationVersion{ 862 SourceTrain: nil, 863 Environment: env, 864 Application: c.Application, 865 Version: lastRelease + 1, 866 // the train should queue deployments, instead of giving up: 867 LockBehaviour: api.LockBehavior_RECORD, 868 Authentication: c.Authentication, 869 WriteCommitData: c.WriteCommitData, 870 } 871 err := t.Execute(d) 872 if err != nil { 873 _, ok := err.(*LockedError) 874 if ok { 875 continue // locked error are expected 876 } else { 877 return "", err 878 } 879 } 880 } 881 } 882 return fmt.Sprintf("created undeploy-version %d of '%v'", lastRelease+1, c.Application), nil 883 } 884 885 func removeCommit(fs billy.Filesystem, commitID, application string) error { 886 errorTemplate := func(message string, err error) error { 887 return fmt.Errorf("while removing applicaton %s from commit %s and error was encountered, message: %s, error %w", application, commitID, message, err) 888 } 889 890 commitApplicationDir := commitApplicationDirectory(fs, commitID, application) 891 if err := fs.Remove(commitApplicationDir); err != nil { 892 if os.IsNotExist(err) { 893 // could not read the directory commitApplicationDir - but that's ok, because we don't know 894 // if the kuberpult version that accepted this commit in the release endpoint, did already have commit writing enabled. 895 // So there's no guarantee that this file ever existed 896 return nil 897 } 898 return errorTemplate(fmt.Sprintf("could not remove the application directory %s", commitApplicationDir), err) 899 } 900 // check if there are no other services updated by this commit 901 // if there are none, start removing the entire branch of the commit 902 903 deleteDirIfEmpty := func(dir string) error { 904 files, err := fs.ReadDir(dir) 905 if err != nil { 906 return errorTemplate(fmt.Sprintf("could not read the directory %s", dir), err) 907 } 908 if len(files) == 0 { 909 if err = fs.Remove(dir); err != nil { 910 return errorTemplate(fmt.Sprintf("could not remove the directory %s", dir), err) 911 } 912 } 913 return nil 914 } 915 916 commitApplicationsDir := path.Dir(commitApplicationDir) 917 if err := deleteDirIfEmpty(commitApplicationsDir); err != nil { 918 return errorTemplate(fmt.Sprintf("could not remove directory %s", commitApplicationsDir), err) 919 } 920 commitDir2 := path.Dir(commitApplicationsDir) 921 922 // if there are no more apps in the "applications" dir, then remove the commit message file and continue cleaning going up 923 if _, err := fs.Stat(commitApplicationsDir); err != nil { 924 if os.IsNotExist(err) { 925 if err := fs.Remove(fs.Join(commitDir2)); err != nil { 926 return errorTemplate(fmt.Sprintf("could not remove commit dir %s file", commitDir2), err) 927 } 928 } else { 929 return errorTemplate(fmt.Sprintf("could not stat directory %s with an unexpected error", commitApplicationsDir), err) 930 } 931 } 932 933 commitDir1 := path.Dir(commitDir2) 934 if err := deleteDirIfEmpty(commitDir1); err != nil { 935 return errorTemplate(fmt.Sprintf("could not remove directory %s", commitDir2), err) 936 } 937 938 return nil 939 } 940 941 type UndeployApplication struct { 942 Authentication 943 Application string 944 } 945 946 func (u *UndeployApplication) Transform( 947 ctx context.Context, 948 state *State, 949 t TransformerContext, 950 ) (string, error) { 951 fs := state.Filesystem 952 lastRelease, err := GetLastRelease(fs, u.Application) 953 if err != nil { 954 return "", err 955 } 956 if lastRelease == 0 { 957 return "", fmt.Errorf("UndeployApplication: error cannot undeploy non-existing application '%v'", u.Application) 958 } 959 isUndeploy, err := state.IsUndeployVersion(u.Application, lastRelease) 960 if err != nil { 961 return "", err 962 } 963 if !isUndeploy { 964 return "", fmt.Errorf("UndeployApplication: error last release is not un-deployed application version of '%v'", u.Application) 965 } 966 appDir := applicationDirectory(fs, u.Application) 967 configs, err := state.GetEnvironmentConfigs() 968 if err != nil { 969 return "", err 970 } 971 for env := range configs { 972 err := state.checkUserPermissions(ctx, env, u.Application, auth.PermissionDeployUndeploy, "", u.RBACConfig) 973 if err != nil { 974 return "", err 975 } 976 envAppDir := environmentApplicationDirectory(fs, env, u.Application) 977 entries, err := fs.ReadDir(envAppDir) 978 if err != nil { 979 return "", wrapFileError(err, envAppDir, "UndeployApplication: Could not open application directory. Does the app exist?") 980 } 981 if entries == nil { 982 // app was never deployed on this env, so we must ignore it! 983 continue 984 } 985 986 appLocksDir := fs.Join(envAppDir, "locks") 987 err = fs.Remove(appLocksDir) 988 if err != nil { 989 return "", fmt.Errorf("UndeployApplication: cannot delete app locks '%v'", appLocksDir) 990 } 991 992 versionDir := fs.Join(envAppDir, "version") 993 undeployFile := fs.Join(versionDir, "undeploy") 994 995 _, err = fs.Stat(versionDir) 996 if err != nil && errors.Is(err, os.ErrNotExist) { 997 // if the app was never deployed here, that's not a reason to stop 998 continue 999 } 1000 1001 _, err = fs.Stat(undeployFile) 1002 if err != nil && errors.Is(err, os.ErrNotExist) { 1003 return "", fmt.Errorf("UndeployApplication: error cannot un-deploy application '%v' the release '%v' is not un-deployed: '%v'", u.Application, env, undeployFile) 1004 } 1005 1006 } 1007 // remove application 1008 releasesDir := fs.Join(appDir, "releases") 1009 files, err := fs.ReadDir(releasesDir) 1010 if err != nil { 1011 return "", fmt.Errorf("could not read the releases directory %s %w", releasesDir, err) 1012 } 1013 for _, file := range files { 1014 if file.IsDir() { 1015 releaseDir := fs.Join(releasesDir, file.Name()) 1016 commitIDFile := fs.Join(releaseDir, "source_commit_id") 1017 var commitID string 1018 dat, err := util.ReadFile(fs, commitIDFile) 1019 if err != nil { 1020 // release does not have a corresponding commit, which might be the case if it's an undeploy release, no prob 1021 continue 1022 } 1023 commitID = string(dat) 1024 if valid.SHA1CommitID(commitID) { 1025 if err := removeCommit(fs, commitID, u.Application); err != nil { 1026 return "", fmt.Errorf("could not remove the commit: %w", err) 1027 } 1028 } 1029 } 1030 } 1031 if err = fs.Remove(appDir); err != nil { 1032 return "", err 1033 } 1034 for env := range configs { 1035 appDir := environmentApplicationDirectory(fs, env, u.Application) 1036 teamOwner, err := state.GetApplicationTeamOwner(u.Application) 1037 if err != nil { 1038 return "", err 1039 } 1040 t.AddAppEnv(u.Application, env, teamOwner) 1041 // remove environment application 1042 if err := fs.Remove(appDir); err != nil && !errors.Is(err, os.ErrNotExist) { 1043 return "", fmt.Errorf("UndeployApplication: unexpected error application '%v' environment '%v': '%w'", u.Application, env, err) 1044 } 1045 } 1046 return fmt.Sprintf("application '%v' was deleted successfully", u.Application), nil 1047 } 1048 1049 type DeleteEnvFromApp struct { 1050 Authentication 1051 Application string 1052 Environment string 1053 } 1054 1055 func (u *DeleteEnvFromApp) Transform( 1056 ctx context.Context, 1057 state *State, 1058 t TransformerContext, 1059 ) (string, error) { 1060 err := state.checkUserPermissions(ctx, u.Environment, u.Application, auth.PermissionDeleteEnvironmentApplication, "", u.RBACConfig) 1061 if err != nil { 1062 return "", err 1063 } 1064 fs := state.Filesystem 1065 thisSprintf := func(format string, a ...any) string { 1066 return fmt.Sprintf("DeleteEnvFromApp app '%s' on env '%s': %s", u.Application, u.Environment, fmt.Sprintf(format, a...)) 1067 } 1068 1069 if u.Application == "" { 1070 return "", fmt.Errorf(thisSprintf("Need to provide the application")) 1071 } 1072 1073 if u.Environment == "" { 1074 return "", fmt.Errorf(thisSprintf("Need to provide the environment")) 1075 } 1076 1077 envAppDir := environmentApplicationDirectory(fs, u.Environment, u.Application) 1078 entries, err := fs.ReadDir(envAppDir) 1079 if err != nil { 1080 return "", wrapFileError(err, envAppDir, thisSprintf("Could not open application directory. Does the app exist?")) 1081 } 1082 1083 if entries == nil { 1084 // app was never deployed on this env, so that's unusual - but for idempotency we treat it just like a success case: 1085 return fmt.Sprintf("Attempted to remove environment '%v' from application '%v' but it did not exist.", u.Environment, u.Application), nil 1086 } 1087 1088 err = fs.Remove(envAppDir) 1089 if err != nil { 1090 return "", wrapFileError(err, envAppDir, thisSprintf("Cannot delete app.'")) 1091 } 1092 1093 t.DeleteEnvFromApp(u.Application, u.Environment) 1094 return fmt.Sprintf("Environment '%v' was removed from application '%v' successfully.", u.Environment, u.Application), nil 1095 } 1096 1097 type CleanupOldApplicationVersions struct { 1098 Application string 1099 } 1100 1101 // Finds old releases for an application 1102 func findOldApplicationVersions(state *State, name string) ([]uint64, error) { 1103 // 1) get release in each env: 1104 envConfigs, err := state.GetEnvironmentConfigs() 1105 if err != nil { 1106 return nil, err 1107 } 1108 versions, err := state.GetApplicationReleases(name) 1109 if err != nil { 1110 return nil, err 1111 } 1112 if len(versions) == 0 { 1113 return nil, err 1114 } 1115 sort.Slice(versions, func(i, j int) bool { 1116 return versions[i] < versions[j] 1117 }) 1118 // Use the latest version as oldest deployed version 1119 oldestDeployedVersion := versions[len(versions)-1] 1120 for env := range envConfigs { 1121 version, err := state.GetEnvironmentApplicationVersion(env, name) 1122 if err != nil { 1123 return nil, err 1124 } 1125 if version != nil { 1126 if *version < oldestDeployedVersion { 1127 oldestDeployedVersion = *version 1128 } 1129 } 1130 } 1131 positionOfOldestVersion := sort.Search(len(versions), func(i int) bool { 1132 return versions[i] >= oldestDeployedVersion 1133 }) 1134 1135 if positionOfOldestVersion < (keptVersionsOnCleanup - 1) { 1136 return nil, nil 1137 } 1138 return versions[0 : positionOfOldestVersion-(keptVersionsOnCleanup-1)], err 1139 } 1140 1141 func (c *CleanupOldApplicationVersions) Transform( 1142 ctx context.Context, 1143 state *State, 1144 t TransformerContext, 1145 ) (string, error) { 1146 fs := state.Filesystem 1147 oldVersions, err := findOldApplicationVersions(state, c.Application) 1148 if err != nil { 1149 return "", fmt.Errorf("cleanup: could not get application releases for app '%s': %w", c.Application, err) 1150 } 1151 1152 msg := "" 1153 for _, oldRelease := range oldVersions { 1154 // delete oldRelease: 1155 releasesDir := releasesDirectoryWithVersion(fs, c.Application, oldRelease) 1156 _, err := fs.Stat(releasesDir) 1157 if err != nil { 1158 return "", wrapFileError(err, releasesDir, "CleanupOldApplicationVersions: could not stat") 1159 } 1160 1161 { 1162 commitIDFile := fs.Join(releasesDir, fieldSourceCommitId) 1163 dat, err := util.ReadFile(fs, commitIDFile) 1164 if err != nil { 1165 // not a problem, might be the undeploy commit or the commit has was not specified in CreateApplicationVersion 1166 } else { 1167 commitID := string(dat) 1168 if valid.SHA1CommitID(commitID) { 1169 if err := removeCommit(fs, commitID, c.Application); err != nil { 1170 return "", wrapFileError(err, releasesDir, "CleanupOldApplicationVersions: could not remove commit path") 1171 } 1172 } 1173 } 1174 } 1175 1176 err = fs.Remove(releasesDir) 1177 if err != nil { 1178 return "", fmt.Errorf("CleanupOldApplicationVersions: Unexpected error app %s: %w", 1179 c.Application, err) 1180 } 1181 msg = fmt.Sprintf("%sremoved version %d of app %v as cleanup\n", msg, oldRelease, c.Application) 1182 } 1183 // we only cleanup non-deployed versions, so there are not changes for argoCd here 1184 return msg, nil 1185 } 1186 1187 func wrapFileError(e error, filename string, message string) error { 1188 return fmt.Errorf("%s '%s': %w", message, filename, e) 1189 } 1190 1191 type Authentication struct { 1192 RBACConfig auth.RBACConfig 1193 } 1194 1195 type CreateEnvironmentLock struct { 1196 Authentication 1197 Environment string 1198 LockId string 1199 Message string 1200 } 1201 1202 func (s *State) checkUserPermissions(ctx context.Context, env, application, action, team string, RBACConfig auth.RBACConfig) error { 1203 if !RBACConfig.DexEnabled { 1204 return nil 1205 } 1206 user, err := auth.ReadUserFromContext(ctx) 1207 if err != nil { 1208 return fmt.Errorf(fmt.Sprintf("checkUserPermissions: user not found: %v", err)) 1209 } 1210 1211 envs, err := s.GetEnvironmentConfigs() 1212 if err != nil { 1213 return err 1214 } 1215 var group string 1216 for envName, config := range envs { 1217 if envName == env { 1218 group = mapper.DeriveGroupName(config, env) 1219 break 1220 } 1221 } 1222 if group == "" { 1223 return fmt.Errorf("group not found for environment: %s", env) 1224 } 1225 return auth.CheckUserPermissions(RBACConfig, user, env, team, group, application, action) 1226 } 1227 1228 // checkUserPermissionsCreateEnvironment check the permission for the environment creation action. 1229 // This is a "special" case because the environment group is already provided on the request. 1230 func (s *State) checkUserPermissionsCreateEnvironment(ctx context.Context, RBACConfig auth.RBACConfig, envConfig config.EnvironmentConfig) error { 1231 if !RBACConfig.DexEnabled { 1232 return nil 1233 } 1234 user, err := auth.ReadUserFromContext(ctx) 1235 if err != nil { 1236 return fmt.Errorf(fmt.Sprintf("checkUserPermissions: user not found: %v", err)) 1237 } 1238 envGroup := "*" 1239 // If an env group is provided on the request, use it on the permission. 1240 if envConfig.EnvironmentGroup != nil { 1241 envGroup = *(envConfig.EnvironmentGroup) 1242 } 1243 return auth.CheckUserPermissions(RBACConfig, user, "*", "", envGroup, "*", auth.PermissionCreateEnvironment) 1244 } 1245 1246 func (c *CreateEnvironmentLock) Transform( 1247 ctx context.Context, 1248 state *State, 1249 t TransformerContext, 1250 ) (string, error) { 1251 err := state.checkUserPermissions(ctx, c.Environment, "*", auth.PermissionCreateLock, "", c.RBACConfig) 1252 if err != nil { 1253 return "", err 1254 } 1255 fs := state.Filesystem 1256 envDir := fs.Join("environments", c.Environment) 1257 if _, err := fs.Stat(envDir); err != nil { 1258 return "", fmt.Errorf("error accessing dir %q: %w", envDir, err) 1259 } 1260 chroot, err := fs.Chroot(envDir) 1261 if err != nil { 1262 return "", err 1263 } 1264 if err := createLock(ctx, chroot, c.LockId, c.Message); err != nil { 1265 return "", err 1266 } 1267 GaugeEnvLockMetric(fs, c.Environment) 1268 return fmt.Sprintf("Created lock %q on environment %q", c.LockId, c.Environment), nil 1269 } 1270 1271 func createLock(ctx context.Context, fs billy.Filesystem, lockId, message string) error { 1272 locksDir := "locks" 1273 if err := fs.MkdirAll(locksDir, 0777); err != nil { 1274 return err 1275 } 1276 1277 user, err := auth.ReadUserFromContext(ctx) 1278 if err != nil { 1279 return err 1280 } 1281 1282 // create lock dir 1283 newLockDir := fs.Join(locksDir, lockId) 1284 if err := fs.MkdirAll(newLockDir, 0777); err != nil { 1285 return err 1286 } 1287 1288 // write message 1289 if err := util.WriteFile(fs, fs.Join(newLockDir, "message"), []byte(message), 0666); err != nil { 1290 return err 1291 } 1292 1293 // write email 1294 if err := util.WriteFile(fs, fs.Join(newLockDir, "created_by_email"), []byte(user.Email), 0666); err != nil { 1295 return err 1296 } 1297 1298 // write name 1299 if err := util.WriteFile(fs, fs.Join(newLockDir, "created_by_name"), []byte(user.Name), 0666); err != nil { 1300 return err 1301 } 1302 1303 // write date in iso format 1304 if err := util.WriteFile(fs, fs.Join(newLockDir, fieldCreatedAt), []byte(getTimeNow(ctx).Format(time.RFC3339)), 0666); err != nil { 1305 return err 1306 } 1307 return nil 1308 } 1309 1310 type DeleteEnvironmentLock struct { 1311 Authentication 1312 Environment string 1313 LockId string 1314 } 1315 1316 func (c *DeleteEnvironmentLock) Transform( 1317 ctx context.Context, 1318 state *State, 1319 t TransformerContext, 1320 ) (string, error) { 1321 err := state.checkUserPermissions(ctx, c.Environment, "*", auth.PermissionDeleteLock, "", c.RBACConfig) 1322 if err != nil { 1323 return "", err 1324 } 1325 fs := state.Filesystem 1326 s := State{ 1327 Commit: nil, 1328 BootstrapMode: false, 1329 EnvironmentConfigsPath: "", 1330 Filesystem: fs, 1331 } 1332 lockDir := s.GetEnvLockDir(c.Environment, c.LockId) 1333 _, err = fs.Stat(lockDir) 1334 if err != nil { 1335 if errors.Is(err, os.ErrNotExist) { 1336 return "", grpc.FailedPrecondition(ctx, fmt.Errorf("directory %s for env lock does not exist", lockDir)) 1337 } 1338 return "", err 1339 } 1340 1341 if err := fs.Remove(lockDir); err != nil && !errors.Is(err, os.ErrNotExist) { 1342 return "", fmt.Errorf("failed to delete directory %q: %w", lockDir, err) 1343 } 1344 if err := s.DeleteEnvLockIfEmpty(ctx, c.Environment); err != nil { 1345 return "", err 1346 } 1347 1348 apps, err := s.GetEnvironmentApplications(c.Environment) 1349 if err != nil { 1350 return "", fmt.Errorf("environment applications for %q not found: %v", c.Environment, err.Error()) 1351 } 1352 1353 additionalMessageFromDeployment := "" 1354 for _, appName := range apps { 1355 queueMessage, err := s.ProcessQueue(ctx, fs, c.Environment, appName) 1356 if err != nil { 1357 return "", err 1358 } 1359 if queueMessage != "" { 1360 additionalMessageFromDeployment = additionalMessageFromDeployment + "\n" + queueMessage 1361 } 1362 } 1363 GaugeEnvLockMetric(fs, c.Environment) 1364 return fmt.Sprintf("Deleted lock %q on environment %q%s", c.LockId, c.Environment, additionalMessageFromDeployment), nil 1365 } 1366 1367 type CreateEnvironmentGroupLock struct { 1368 Authentication 1369 EnvironmentGroup string 1370 LockId string 1371 Message string 1372 } 1373 1374 func (c *CreateEnvironmentGroupLock) Transform( 1375 ctx context.Context, 1376 state *State, 1377 t TransformerContext, 1378 ) (string, error) { 1379 err := state.checkUserPermissions(ctx, c.EnvironmentGroup, "*", auth.PermissionCreateLock, "", c.RBACConfig) 1380 if err != nil { 1381 return "", err 1382 } 1383 envNamesSorted, err := state.GetEnvironmentConfigsForGroup(c.EnvironmentGroup) 1384 if err != nil { 1385 return "", grpc.PublicError(ctx, err) 1386 } 1387 for index := range envNamesSorted { 1388 envName := envNamesSorted[index] 1389 x := CreateEnvironmentLock{ 1390 Authentication: c.Authentication, 1391 Environment: envName, 1392 LockId: c.LockId, // the IDs should be the same for all. See `useLocksSimilarTo` in store.tsx 1393 Message: c.Message, 1394 } 1395 if err := t.Execute(&x); err != nil { 1396 return "", err 1397 } 1398 } 1399 return fmt.Sprintf("Creating locks '%s' for environment group '%s':", c.LockId, c.EnvironmentGroup), nil 1400 } 1401 1402 type DeleteEnvironmentGroupLock struct { 1403 Authentication 1404 EnvironmentGroup string 1405 LockId string 1406 } 1407 1408 func (c *DeleteEnvironmentGroupLock) Transform( 1409 ctx context.Context, 1410 state *State, 1411 t TransformerContext, 1412 ) (string, error) { 1413 err := state.checkUserPermissions(ctx, c.EnvironmentGroup, "*", auth.PermissionDeleteLock, "", c.RBACConfig) 1414 if err != nil { 1415 return "", err 1416 } 1417 envNamesSorted, err := state.GetEnvironmentConfigsForGroup(c.EnvironmentGroup) 1418 if err != nil { 1419 return "", grpc.PublicError(ctx, err) 1420 } 1421 for index := range envNamesSorted { 1422 envName := envNamesSorted[index] 1423 x := DeleteEnvironmentLock{ 1424 Authentication: c.Authentication, 1425 Environment: envName, 1426 LockId: c.LockId, 1427 } 1428 if err := t.Execute(&x); err != nil { 1429 return "", err 1430 } 1431 } 1432 return fmt.Sprintf("Deleting locks '%s' for environment group '%s':", c.LockId, c.EnvironmentGroup), nil 1433 } 1434 1435 type CreateEnvironmentApplicationLock struct { 1436 Authentication 1437 Environment string 1438 Application string 1439 LockId string 1440 Message string 1441 } 1442 1443 func (c *CreateEnvironmentApplicationLock) Transform( 1444 ctx context.Context, 1445 state *State, 1446 t TransformerContext, 1447 ) (string, error) { 1448 // Note: it's possible to lock an application BEFORE it's even deployed to the environment. 1449 err := state.checkUserPermissions(ctx, c.Environment, c.Application, auth.PermissionCreateLock, "", c.RBACConfig) 1450 if err != nil { 1451 return "", err 1452 } 1453 fs := state.Filesystem 1454 envDir := fs.Join("environments", c.Environment) 1455 if _, err := fs.Stat(envDir); err != nil { 1456 return "", fmt.Errorf("error accessing dir %q: %w", envDir, err) 1457 } 1458 1459 appDir := fs.Join(envDir, "applications", c.Application) 1460 if err := fs.MkdirAll(appDir, 0777); err != nil { 1461 return "", err 1462 } 1463 chroot, err := fs.Chroot(appDir) 1464 if err != nil { 1465 return "", err 1466 } 1467 if err := createLock(ctx, chroot, c.LockId, c.Message); err != nil { 1468 return "", err 1469 } 1470 GaugeEnvAppLockMetric(fs, c.Environment, c.Application) 1471 // locks are invisible to argoCd, so no changes here 1472 return fmt.Sprintf("Created lock %q on environment %q for application %q", c.LockId, c.Environment, c.Application), nil 1473 } 1474 1475 type DeleteEnvironmentApplicationLock struct { 1476 Authentication 1477 Environment string 1478 Application string 1479 LockId string 1480 } 1481 1482 func (c *DeleteEnvironmentApplicationLock) Transform( 1483 ctx context.Context, 1484 state *State, 1485 t TransformerContext, 1486 ) (string, error) { 1487 err := state.checkUserPermissions(ctx, c.Environment, c.Application, auth.PermissionDeleteLock, "", c.RBACConfig) 1488 if err != nil { 1489 return "", err 1490 } 1491 fs := state.Filesystem 1492 lockDir := fs.Join("environments", c.Environment, "applications", c.Application, "locks", c.LockId) 1493 _, err = fs.Stat(lockDir) 1494 if err != nil { 1495 if errors.Is(err, os.ErrNotExist) { 1496 return "", grpc.FailedPrecondition(ctx, fmt.Errorf("directory %s for app lock does not exist", lockDir)) 1497 } 1498 return "", err 1499 } 1500 if err := fs.Remove(lockDir); err != nil && !errors.Is(err, os.ErrNotExist) { 1501 return "", fmt.Errorf("failed to delete directory %q: %w", lockDir, err) 1502 } 1503 s := State{ 1504 Commit: nil, 1505 BootstrapMode: false, 1506 EnvironmentConfigsPath: "", 1507 Filesystem: fs, 1508 } 1509 if err := s.DeleteAppLockIfEmpty(ctx, c.Environment, c.Application); err != nil { 1510 return "", err 1511 } 1512 queueMessage, err := s.ProcessQueue(ctx, fs, c.Environment, c.Application) 1513 if err != nil { 1514 return "", err 1515 } 1516 GaugeEnvAppLockMetric(fs, c.Environment, c.Application) 1517 return fmt.Sprintf("Deleted lock %q on environment %q for application %q%s", c.LockId, c.Environment, c.Application, queueMessage), nil 1518 } 1519 1520 type CreateEnvironment struct { 1521 Authentication 1522 Environment string 1523 Config config.EnvironmentConfig 1524 } 1525 1526 func (c *CreateEnvironment) Transform( 1527 ctx context.Context, 1528 state *State, 1529 t TransformerContext, 1530 ) (string, error) { 1531 err := state.checkUserPermissionsCreateEnvironment(ctx, c.RBACConfig, c.Config) 1532 if err != nil { 1533 return "", err 1534 } 1535 fs := state.Filesystem 1536 envDir := fs.Join("environments", c.Environment) 1537 // Creation of environment is possible, but configuring it is not if running in bootstrap mode. 1538 // Configuration needs to be done by modifying config map in source repo 1539 //exhaustruct:ignore 1540 defaultConfig := config.EnvironmentConfig{} 1541 if state.BootstrapMode && c.Config != defaultConfig { 1542 return "", fmt.Errorf("Cannot create or update configuration in bootstrap mode. Please update configuration in config map instead.") 1543 } 1544 if err := fs.MkdirAll(envDir, 0777); err != nil { 1545 return "", err 1546 } 1547 configFile := fs.Join(envDir, "config.json") 1548 file, err := fs.OpenFile(configFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) 1549 if err != nil { 1550 return "", fmt.Errorf("error creating config: %w", err) 1551 } 1552 enc := json.NewEncoder(file) 1553 enc.SetIndent("", " ") 1554 if err := enc.Encode(c.Config); err != nil { 1555 return "", fmt.Errorf("error writing json: %w", err) 1556 } 1557 // we do not need to inform argoCd when creating an environment, as there are no apps yet 1558 return fmt.Sprintf("create environment %q", c.Environment), file.Close() 1559 } 1560 1561 type QueueApplicationVersion struct { 1562 Environment string 1563 Application string 1564 Version uint64 1565 } 1566 1567 func (c *QueueApplicationVersion) Transform( 1568 ctx context.Context, 1569 state *State, 1570 t TransformerContext, 1571 ) (string, error) { 1572 fs := state.Filesystem 1573 // Create a symlink to the release 1574 applicationDir := fs.Join("environments", c.Environment, "applications", c.Application) 1575 if err := fs.MkdirAll(applicationDir, 0777); err != nil { 1576 return "", err 1577 } 1578 queuedVersionFile := fs.Join(applicationDir, queueFileName) 1579 if err := fs.Remove(queuedVersionFile); err != nil && !errors.Is(err, os.ErrNotExist) { 1580 return "", err 1581 } 1582 releaseDir := releasesDirectoryWithVersion(fs, c.Application, c.Version) 1583 if err := fs.Symlink(fs.Join("..", "..", "..", "..", releaseDir), queuedVersionFile); err != nil { 1584 return "", err 1585 } 1586 1587 // TODO SU: maybe check here if that version is already deployed? or somewhere else ... or not at all... 1588 return fmt.Sprintf("Queued version %d of app %q in env %q", c.Version, c.Application, c.Environment), nil 1589 } 1590 1591 type DeployApplicationVersion struct { 1592 Authentication 1593 Environment string 1594 Application string 1595 Version uint64 1596 LockBehaviour api.LockBehavior 1597 WriteCommitData bool 1598 SourceTrain *DeployApplicationVersionSource 1599 } 1600 1601 type DeployApplicationVersionSource struct { 1602 TargetGroup *string 1603 Upstream string 1604 } 1605 1606 func (c *DeployApplicationVersion) Transform( 1607 ctx context.Context, 1608 state *State, 1609 t TransformerContext, 1610 ) (string, error) { 1611 err := state.checkUserPermissions(ctx, c.Environment, c.Application, auth.PermissionDeployRelease, "", c.RBACConfig) 1612 if err != nil { 1613 return "", err 1614 } 1615 fs := state.Filesystem 1616 // Check that the release exist and fetch manifest 1617 releaseDir := releasesDirectoryWithVersion(fs, c.Application, c.Version) 1618 manifest := fs.Join(releaseDir, "environments", c.Environment, "manifests.yaml") 1619 var manifestContent []byte 1620 if file, err := fs.Open(manifest); err != nil { 1621 return "", wrapFileError(err, manifest, fmt.Sprintf("deployment failed: could not open manifest for app %s with release %d on env %s", c.Application, c.Version, c.Environment)) 1622 } else { 1623 if content, err := io.ReadAll(file); err != nil { 1624 return "", err 1625 } else { 1626 manifestContent = content 1627 } 1628 file.Close() 1629 } 1630 lockPreventedDeployment := false 1631 if c.LockBehaviour != api.LockBehavior_IGNORE { 1632 // Check that the environment is not locked 1633 var ( 1634 envLocks, appLocks map[string]Lock 1635 err error 1636 ) 1637 envLocks, err = state.GetEnvironmentLocks(c.Environment) 1638 if err != nil { 1639 return "", err 1640 } 1641 appLocks, err = state.GetEnvironmentApplicationLocks(c.Environment, c.Application) 1642 if err != nil { 1643 return "", err 1644 } 1645 if len(envLocks) > 0 || len(appLocks) > 0 { 1646 if c.WriteCommitData { 1647 var lockType, lockMsg string 1648 if len(envLocks) > 0 { 1649 lockType = "environment" 1650 for _, lock := range envLocks { 1651 lockMsg = lock.Message 1652 break 1653 } 1654 } else { 1655 lockType = "application" 1656 for _, lock := range appLocks { 1657 lockMsg = lock.Message 1658 break 1659 } 1660 } 1661 if err := addEventForRelease(ctx, fs, releaseDir, &event.LockPreventedDeployment{ 1662 Application: c.Application, 1663 Environment: c.Environment, 1664 LockMessage: lockMsg, 1665 LockType: lockType, 1666 }); err != nil { 1667 return "", err 1668 } 1669 lockPreventedDeployment = true 1670 } 1671 switch c.LockBehaviour { 1672 case api.LockBehavior_RECORD: 1673 q := QueueApplicationVersion{ 1674 Environment: c.Environment, 1675 Application: c.Application, 1676 Version: c.Version, 1677 } 1678 return q.Transform(ctx, state, t) 1679 case api.LockBehavior_FAIL: 1680 return "", &LockedError{ 1681 EnvironmentApplicationLocks: appLocks, 1682 EnvironmentLocks: envLocks, 1683 } 1684 } 1685 } 1686 } 1687 1688 applicationDir := fs.Join("environments", c.Environment, "applications", c.Application) 1689 firstDeployment := false 1690 versionFile := fs.Join(applicationDir, "version") 1691 oldReleaseDir := "" 1692 1693 //Check if there is a version of target app already deployed on target environment 1694 if _, err := fs.Lstat(versionFile); err == nil { 1695 //File Exists 1696 evaledPath, _ := fs.Readlink(versionFile) //Version is stored as symlink, eval it 1697 oldReleaseDir = evaledPath 1698 } else { 1699 //File does not exist 1700 firstDeployment = true 1701 } 1702 1703 // Create a symlink to the release 1704 if err := fs.MkdirAll(applicationDir, 0777); err != nil { 1705 return "", err 1706 } 1707 if err := fs.Remove(versionFile); err != nil && !errors.Is(err, os.ErrNotExist) { 1708 return "", err 1709 } 1710 if err := fs.Symlink(fs.Join("..", "..", "..", "..", releaseDir), versionFile); err != nil { 1711 return "", err 1712 } 1713 // Copy the manifest for argocd 1714 manifestsDir := fs.Join(applicationDir, "manifests") 1715 if err := fs.MkdirAll(manifestsDir, 0777); err != nil { 1716 return "", err 1717 } 1718 manifestFilename := fs.Join(manifestsDir, "manifests.yaml") 1719 // note that the manifest is empty here! 1720 // but actually it's not quite empty! 1721 // The function we are using here is `util.WriteFile`. And that does not allow overwriting files with empty content. 1722 // We work around this unusual behavior by writing a space into the file 1723 if len(manifestContent) == 0 { 1724 manifestContent = []byte(" ") 1725 } 1726 if err := util.WriteFile(fs, manifestFilename, manifestContent, 0666); err != nil { 1727 return "", err 1728 } 1729 teamOwner, err := state.GetApplicationTeamOwner(c.Application) 1730 if err != nil { 1731 return "", err 1732 } 1733 t.AddAppEnv(c.Application, c.Environment, teamOwner) 1734 1735 user, err := auth.ReadUserFromContext(ctx) 1736 if err != nil { 1737 return "", err 1738 } 1739 1740 if err := util.WriteFile(fs, fs.Join(applicationDir, "deployed_by"), []byte(user.Name), 0666); err != nil { 1741 return "", err 1742 } 1743 if err := util.WriteFile(fs, fs.Join(applicationDir, "deployed_by_email"), []byte(user.Email), 0666); err != nil { 1744 return "", err 1745 } 1746 1747 if err := util.WriteFile(fs, fs.Join(applicationDir, "deployed_at_utc"), []byte(getTimeNow(ctx).UTC().String()), 0666); err != nil { 1748 return "", err 1749 } 1750 1751 s := State{ 1752 Commit: nil, 1753 BootstrapMode: false, 1754 EnvironmentConfigsPath: "", 1755 Filesystem: fs, 1756 } 1757 err = s.DeleteQueuedVersionIfExists(c.Environment, c.Application) 1758 if err != nil { 1759 return "", err 1760 } 1761 d := &CleanupOldApplicationVersions{ 1762 Application: c.Application, 1763 } 1764 if err := t.Execute(d); err != nil { 1765 return "", err 1766 } 1767 1768 if c.WriteCommitData { // write the corresponding event 1769 if err := addEventForRelease(ctx, fs, releaseDir, createDeploymentEvent(c.Application, c.Environment, c.SourceTrain)); err != nil { 1770 return "", err 1771 } 1772 1773 if !firstDeployment && !lockPreventedDeployment { 1774 //If not first deployment and current deployment is successful, signal a new replaced by event 1775 if newReleaseCommitId, err := getCommitIDFromReleaseDir(ctx, fs, releaseDir); err == nil { 1776 if !valid.SHA1CommitID(newReleaseCommitId) { 1777 logger.FromContext(ctx).Sugar().Infof( 1778 "The source commit ID %s is not a valid/complete SHA1 hash, event cannot be stored.", 1779 newReleaseCommitId) 1780 } else { 1781 if err := addEventForRelease(ctx, fs, oldReleaseDir, createReplacedByEvent(c.Application, c.Environment, newReleaseCommitId)); err != nil { 1782 return "", err 1783 } 1784 } 1785 } 1786 } else { 1787 logger.FromContext(ctx).Sugar().Infof( 1788 "Release to replace decteted, but could not retrieve new commit information. Replaced-by event not stored.") 1789 } 1790 } 1791 1792 return fmt.Sprintf("deployed version %d of %q to %q", c.Version, c.Application, c.Environment), nil 1793 } 1794 1795 func getCommitIDFromReleaseDir(ctx context.Context, fs billy.Filesystem, releaseDir string) (string, error) { 1796 commitIdPath := fs.Join(releaseDir, "source_commit_id") 1797 1798 commitIDBytes, err := util.ReadFile(fs, commitIdPath) 1799 if err != nil { 1800 logger.FromContext(ctx).Sugar().Infof( 1801 "Error while reading source commit ID file at %s, error %w. Deployment event not stored.", 1802 commitIdPath, err) 1803 return "", err 1804 } 1805 commitID := string(commitIDBytes) 1806 // if the stored source commit ID is invalid then we will not be able to store the event (simply) 1807 return commitID, nil 1808 } 1809 1810 func addEventForRelease(ctx context.Context, fs billy.Filesystem, releaseDir string, ev event.Event) error { 1811 if commitID, err := getCommitIDFromReleaseDir(ctx, fs, releaseDir); err == nil { 1812 gen := getGenerator(ctx) 1813 eventUuid := gen.Generate() 1814 1815 if !valid.SHA1CommitID(commitID) { 1816 logger.FromContext(ctx).Sugar().Infof( 1817 "The source commit ID %s is not a valid/complete SHA1 hash, event cannot be stored.", 1818 commitID) 1819 return nil 1820 } 1821 1822 if err := writeEvent(eventUuid, commitID, fs, ev); err != nil { 1823 return fmt.Errorf( 1824 "could not write an event for commit %s, error: %w", 1825 commitID, err) 1826 //return fmt.Errorf( 1827 // "could not write an event for commit %s with uuid %s, error: %w", 1828 // commitID, eventUuid, err) 1829 } 1830 } 1831 return nil 1832 } 1833 1834 func createDeploymentEvent(application, environment string, sourceTrain *DeployApplicationVersionSource) *event.Deployment { 1835 ev := event.Deployment{ 1836 SourceTrainEnvironmentGroup: nil, 1837 SourceTrainUpstream: nil, 1838 Application: application, 1839 Environment: environment, 1840 } 1841 if sourceTrain != nil { 1842 if sourceTrain.TargetGroup != nil { 1843 ev.SourceTrainEnvironmentGroup = sourceTrain.TargetGroup 1844 } 1845 ev.SourceTrainUpstream = &sourceTrain.Upstream 1846 } 1847 return &ev 1848 } 1849 1850 func createReplacedByEvent(application, environment, commitId string) *event.ReplacedBy { 1851 ev := event.ReplacedBy{ 1852 Application: application, 1853 Environment: environment, 1854 CommitIDtoReplace: commitId, 1855 } 1856 return &ev 1857 } 1858 1859 type ReleaseTrain struct { 1860 Authentication 1861 Target string 1862 Team string 1863 CommitHash string 1864 WriteCommitData bool 1865 Repo Repository 1866 } 1867 type Overview struct { 1868 App string 1869 Version uint64 1870 } 1871 1872 func getEnvironmentInGroup(groups []*api.EnvironmentGroup, groupNameToReturn string, envNameToReturn string) *api.Environment { 1873 for _, currentGroup := range groups { 1874 if currentGroup.EnvironmentGroupName == groupNameToReturn { 1875 for _, currentEnv := range currentGroup.Environments { 1876 if currentEnv.Name == envNameToReturn { 1877 return currentEnv 1878 } 1879 } 1880 } 1881 } 1882 return nil 1883 } 1884 1885 func getOverrideVersions(commitHash, upstreamEnvName string, repo Repository) (resp []Overview, err error) { 1886 oid, err := git.NewOid(commitHash) 1887 if err != nil { 1888 return nil, fmt.Errorf("Error creating new oid for commitHash %s: %w", commitHash, err) 1889 } 1890 s, err := repo.StateAt(oid) 1891 if err != nil { 1892 var gerr *git.GitError 1893 if errors.As(err, &gerr) { 1894 if gerr.Code == git.ErrorCodeNotFound { 1895 return nil, fmt.Errorf("ErrNotFound: %w", err) 1896 } 1897 } 1898 return nil, fmt.Errorf("unable to get oid: %w", err) 1899 } 1900 envs, err := s.GetEnvironmentConfigs() 1901 if err != nil { 1902 return nil, fmt.Errorf("unable to get EnvironmentConfigs for %s: %w", commitHash, err) 1903 } 1904 result := mapper.MapEnvironmentsToGroups(envs) 1905 for envName, config := range envs { 1906 var groupName = mapper.DeriveGroupName(config, envName) 1907 var envInGroup = getEnvironmentInGroup(result, groupName, envName) 1908 if upstreamEnvName != envInGroup.Name || upstreamEnvName != groupName { 1909 continue 1910 } 1911 apps, err := s.GetEnvironmentApplications(envName) 1912 if err != nil { 1913 return nil, fmt.Errorf("unable to get EnvironmentApplication for env %s: %w", envName, err) 1914 } 1915 for _, appName := range apps { 1916 app := api.Environment_Application{ 1917 Version: 0, 1918 Locks: nil, 1919 QueuedVersion: 0, 1920 UndeployVersion: false, 1921 ArgoCd: nil, 1922 DeploymentMetaData: nil, 1923 Name: appName, 1924 } 1925 version, err := s.GetEnvironmentApplicationVersion(envName, appName) 1926 if err != nil && !errors.Is(err, os.ErrNotExist) { 1927 return nil, fmt.Errorf("unable to get EnvironmentApplicationVersion for %s: %w", appName, err) 1928 } 1929 if version == nil { 1930 continue 1931 } 1932 app.Version = *version 1933 resp = append(resp, Overview{App: app.Name, Version: app.Version}) 1934 } 1935 } 1936 return resp, nil 1937 } 1938 1939 func (c *ReleaseTrain) getUpstreamLatestApp(upstreamLatest bool, state *State, ctx context.Context, upstreamEnvName, source, commitHash string) (apps []string, appVersions []Overview, err error) { 1940 if commitHash != "" { 1941 appVersions, err := getOverrideVersions(c.CommitHash, upstreamEnvName, c.Repo) 1942 if err != nil { 1943 return nil, nil, grpc.PublicError(ctx, fmt.Errorf("could not get app version for commitHash %s for %s: %w", c.CommitHash, c.Target, err)) 1944 } 1945 // check that commit hash is not older than 20 commits in the past 1946 for _, app := range appVersions { 1947 apps = append(apps, app.App) 1948 versions, err := findOldApplicationVersions(state, app.App) 1949 if err != nil { 1950 return nil, nil, grpc.PublicError(ctx, fmt.Errorf("unable to find findOldApplicationVersions for app %s: %w", app.App, err)) 1951 } 1952 if len(versions) > 0 && versions[0] > app.Version { 1953 return nil, nil, grpc.PublicError(ctx, fmt.Errorf("Version for app %s is older than 20 commits when running release train to commitHash %s: %w", app.App, c.CommitHash, err)) 1954 } 1955 1956 } 1957 return apps, appVersions, nil 1958 } 1959 if upstreamLatest { 1960 apps, err = state.GetApplications() 1961 if err != nil { 1962 return nil, nil, grpc.PublicError(ctx, fmt.Errorf("could not get all applications for %q: %w", source, err)) 1963 } 1964 return apps, nil, nil 1965 } 1966 apps, err = state.GetEnvironmentApplications(upstreamEnvName) 1967 if err != nil { 1968 return nil, nil, grpc.PublicError(ctx, fmt.Errorf("upstream environment (%q) does not have applications: %w", upstreamEnvName, err)) 1969 } 1970 return apps, nil, nil 1971 } 1972 1973 func getEnvironmentGroupsEnvironmentsOrEnvironment(configs map[string]config.EnvironmentConfig, targetGroupName string) (map[string]config.EnvironmentConfig, bool) { 1974 envGroupConfigs := make(map[string]config.EnvironmentConfig) 1975 isEnvGroup := false 1976 1977 for env, config := range configs { 1978 if config.EnvironmentGroup != nil && *config.EnvironmentGroup == targetGroupName { 1979 isEnvGroup = true 1980 envGroupConfigs[env] = config 1981 } 1982 } 1983 if len(envGroupConfigs) == 0 { 1984 envConfig, ok := configs[targetGroupName] 1985 if ok { 1986 envGroupConfigs[targetGroupName] = envConfig 1987 } 1988 } 1989 return envGroupConfigs, isEnvGroup 1990 } 1991 1992 type ReleaseTrainApplicationPrognosis struct { 1993 SkipCause *api.ReleaseTrainAppPrognosis_SkipCause 1994 Version uint64 1995 } 1996 1997 type ReleaseTrainEnvironmentPrognosis struct { 1998 SkipCause *api.ReleaseTrainEnvPrognosis_SkipCause 1999 Error error 2000 // map key is the name of the app 2001 AppsPrognoses map[string]ReleaseTrainApplicationPrognosis 2002 } 2003 2004 type ReleaseTrainPrognosisOutcome = uint64 2005 2006 type ReleaseTrainPrognosis struct { 2007 Error error 2008 EnvironmentPrognoses map[string]ReleaseTrainEnvironmentPrognosis 2009 } 2010 2011 func (c *ReleaseTrain) Prognosis( 2012 ctx context.Context, 2013 state *State, 2014 ) ReleaseTrainPrognosis { 2015 configs, err := state.GetEnvironmentConfigs() 2016 if err != nil { 2017 return ReleaseTrainPrognosis{ 2018 Error: grpc.InternalError(ctx, err), 2019 EnvironmentPrognoses: nil, 2020 } 2021 } 2022 2023 var targetGroupName = c.Target 2024 var envGroupConfigs, isEnvGroup = getEnvironmentGroupsEnvironmentsOrEnvironment(configs, targetGroupName) 2025 if len(envGroupConfigs) == 0 { 2026 return ReleaseTrainPrognosis{ 2027 Error: grpc.PublicError(ctx, fmt.Errorf("could not find environment group or environment configs for '%v'", targetGroupName)), 2028 EnvironmentPrognoses: nil, 2029 } 2030 } 2031 2032 // this to sort the env, to make sure that for the same input we always got the same output 2033 envGroups := make([]string, 0, len(envGroupConfigs)) 2034 for env := range envGroupConfigs { 2035 envGroups = append(envGroups, env) 2036 } 2037 sort.Strings(envGroups) 2038 2039 envPrognoses := make(map[string]ReleaseTrainEnvironmentPrognosis) 2040 2041 for _, envName := range envGroups { 2042 var trainGroup *string 2043 if isEnvGroup { 2044 trainGroup = ptr.FromString(targetGroupName) 2045 } 2046 2047 envReleaseTrain := &envReleaseTrain{ 2048 Parent: c, 2049 Env: envName, 2050 EnvConfigs: configs, 2051 EnvGroupConfigs: envGroupConfigs, 2052 WriteCommitData: c.WriteCommitData, 2053 TrainGroup: trainGroup, 2054 } 2055 2056 envPrognosis := envReleaseTrain.prognosis(ctx, state) 2057 2058 if envPrognosis.Error != nil { 2059 return ReleaseTrainPrognosis{ 2060 Error: envPrognosis.Error, 2061 EnvironmentPrognoses: nil, 2062 } 2063 } 2064 2065 envPrognoses[envName] = envPrognosis 2066 } 2067 2068 return ReleaseTrainPrognosis{ 2069 Error: nil, 2070 EnvironmentPrognoses: envPrognoses, 2071 } 2072 } 2073 2074 func (c *ReleaseTrain) Transform( 2075 ctx context.Context, 2076 state *State, 2077 t TransformerContext, 2078 ) (string, error) { 2079 prognosis := c.Prognosis(ctx, state) 2080 2081 if prognosis.Error != nil { 2082 return "", prognosis.Error 2083 } 2084 2085 var targetGroupName = c.Target 2086 configs, _ := state.GetEnvironmentConfigs() 2087 var envGroupConfigs, isEnvGroup = getEnvironmentGroupsEnvironmentsOrEnvironment(configs, targetGroupName) 2088 2089 // sorting for determinism 2090 envNames := make([]string, 0, len(prognosis.EnvironmentPrognoses)) 2091 for envName := range prognosis.EnvironmentPrognoses { 2092 envNames = append(envNames, envName) 2093 } 2094 sort.Strings(envNames) 2095 2096 for _, envName := range envNames { 2097 var trainGroup *string 2098 if isEnvGroup { 2099 trainGroup = ptr.FromString(targetGroupName) 2100 } 2101 2102 if err := t.Execute(&envReleaseTrain{ 2103 Parent: c, 2104 Env: envName, 2105 EnvConfigs: configs, 2106 EnvGroupConfigs: envGroupConfigs, 2107 WriteCommitData: c.WriteCommitData, 2108 TrainGroup: trainGroup, 2109 }); err != nil { 2110 return "", err 2111 } 2112 } 2113 2114 return fmt.Sprintf( 2115 "Release Train to environment/environment group '%s':\n", 2116 targetGroupName), nil 2117 } 2118 2119 type envReleaseTrain struct { 2120 Parent *ReleaseTrain 2121 Env string 2122 EnvConfigs map[string]config.EnvironmentConfig 2123 EnvGroupConfigs map[string]config.EnvironmentConfig 2124 WriteCommitData bool 2125 TrainGroup *string 2126 } 2127 2128 func (c *envReleaseTrain) prognosis( 2129 ctx context.Context, 2130 state *State, 2131 ) ReleaseTrainEnvironmentPrognosis { 2132 envConfig := c.EnvGroupConfigs[c.Env] 2133 if envConfig.Upstream == nil { 2134 return ReleaseTrainEnvironmentPrognosis{ 2135 SkipCause: &api.ReleaseTrainEnvPrognosis_SkipCause{ 2136 SkipCause: api.ReleaseTrainEnvSkipCause_ENV_HAS_NO_UPSTREAM, 2137 }, 2138 Error: nil, 2139 AppsPrognoses: nil, 2140 } 2141 } 2142 2143 err := state.checkUserPermissions( 2144 ctx, 2145 c.Env, 2146 "*", 2147 auth.PermissionDeployReleaseTrain, 2148 c.Parent.Team, 2149 c.Parent.RBACConfig, 2150 ) 2151 2152 if err != nil { 2153 return ReleaseTrainEnvironmentPrognosis{ 2154 SkipCause: nil, 2155 Error: err, 2156 AppsPrognoses: nil, 2157 } 2158 } 2159 2160 upstreamLatest := envConfig.Upstream.Latest 2161 upstreamEnvName := envConfig.Upstream.Environment 2162 if !upstreamLatest && upstreamEnvName == "" { 2163 return ReleaseTrainEnvironmentPrognosis{ 2164 SkipCause: &api.ReleaseTrainEnvPrognosis_SkipCause{ 2165 SkipCause: api.ReleaseTrainEnvSkipCause_ENV_HAS_NO_UPSTREAM_LATEST_OR_UPSTREAM_ENV, 2166 }, 2167 Error: nil, 2168 AppsPrognoses: nil, 2169 } 2170 } 2171 2172 if upstreamLatest && upstreamEnvName != "" { 2173 return ReleaseTrainEnvironmentPrognosis{ 2174 SkipCause: &api.ReleaseTrainEnvPrognosis_SkipCause{ 2175 SkipCause: api.ReleaseTrainEnvSkipCause_ENV_HAS_BOTH_UPSTREAM_LATEST_AND_UPSTREAM_ENV, 2176 }, 2177 Error: nil, 2178 AppsPrognoses: nil, 2179 } 2180 } 2181 2182 if !upstreamLatest { 2183 _, ok := c.EnvConfigs[upstreamEnvName] 2184 if !ok { 2185 return ReleaseTrainEnvironmentPrognosis{ 2186 SkipCause: &api.ReleaseTrainEnvPrognosis_SkipCause{ 2187 SkipCause: api.ReleaseTrainEnvSkipCause_UPSTREAM_ENV_CONFIG_NOT_FOUND, 2188 }, 2189 Error: nil, 2190 AppsPrognoses: nil, 2191 } 2192 } 2193 } 2194 2195 envLocks, err := state.GetEnvironmentLocks(c.Env) 2196 if err != nil { 2197 return ReleaseTrainEnvironmentPrognosis{ 2198 SkipCause: nil, 2199 Error: grpc.InternalError(ctx, fmt.Errorf("could not get lock for environment %q: %w", c.Env, err)), 2200 AppsPrognoses: nil, 2201 } 2202 } 2203 2204 if len(envLocks) > 0 { 2205 return ReleaseTrainEnvironmentPrognosis{ 2206 SkipCause: &api.ReleaseTrainEnvPrognosis_SkipCause{ 2207 SkipCause: api.ReleaseTrainEnvSkipCause_ENV_IS_LOCKED, 2208 }, 2209 Error: nil, 2210 AppsPrognoses: nil, 2211 } 2212 } 2213 2214 source := upstreamEnvName 2215 if upstreamLatest { 2216 source = "latest" 2217 } 2218 2219 apps, overrideVersions, err := c.Parent.getUpstreamLatestApp(upstreamLatest, state, ctx, upstreamEnvName, source, c.Parent.CommitHash) 2220 if err != nil { 2221 return ReleaseTrainEnvironmentPrognosis{ 2222 SkipCause: nil, 2223 Error: err, 2224 AppsPrognoses: nil, 2225 } 2226 } 2227 sort.Strings(apps) 2228 2229 appsPrognoses := make(map[string]ReleaseTrainApplicationPrognosis) 2230 2231 for _, appName := range apps { 2232 if c.Parent.Team != "" { 2233 if team, err := state.GetApplicationTeamOwner(appName); err != nil { 2234 return ReleaseTrainEnvironmentPrognosis{ 2235 SkipCause: nil, 2236 Error: err, 2237 AppsPrognoses: nil, 2238 } 2239 } else if c.Parent.Team != team { 2240 continue 2241 } 2242 } 2243 2244 currentlyDeployedVersion, err := state.GetEnvironmentApplicationVersion(c.Env, appName) 2245 if err != nil { 2246 return ReleaseTrainEnvironmentPrognosis{ 2247 SkipCause: nil, 2248 Error: grpc.PublicError(ctx, fmt.Errorf("application %q in env %q does not have a version deployed: %w", appName, c.Env, err)), 2249 AppsPrognoses: nil, 2250 } 2251 } 2252 2253 var versionToDeploy uint64 2254 if overrideVersions != nil { 2255 for _, override := range overrideVersions { 2256 if override.App == appName { 2257 versionToDeploy = override.Version 2258 } 2259 } 2260 } else if upstreamLatest { 2261 versionToDeploy, err = GetLastRelease(state.Filesystem, appName) 2262 if err != nil { 2263 return ReleaseTrainEnvironmentPrognosis{ 2264 SkipCause: nil, 2265 Error: grpc.PublicError(ctx, fmt.Errorf("application %q does not have a latest deployed: %w", appName, err)), 2266 AppsPrognoses: nil, 2267 } 2268 } 2269 } else { 2270 upstreamVersion, err := state.GetEnvironmentApplicationVersion(upstreamEnvName, appName) 2271 if err != nil { 2272 return ReleaseTrainEnvironmentPrognosis{ 2273 SkipCause: nil, 2274 Error: grpc.PublicError(ctx, fmt.Errorf("application %q does not have a version deployed in env %q: %w", appName, upstreamEnvName, err)), 2275 AppsPrognoses: nil, 2276 } 2277 } 2278 if upstreamVersion == nil { 2279 appsPrognoses[appName] = ReleaseTrainApplicationPrognosis{ 2280 SkipCause: &api.ReleaseTrainAppPrognosis_SkipCause{ 2281 SkipCause: api.ReleaseTrainAppSkipCause_APP_HAS_NO_VERSION_IN_UPSTREAM_ENV, 2282 }, 2283 Version: 0, 2284 } 2285 continue 2286 } 2287 versionToDeploy = *upstreamVersion 2288 } 2289 if currentlyDeployedVersion != nil && *currentlyDeployedVersion == versionToDeploy { 2290 appsPrognoses[appName] = ReleaseTrainApplicationPrognosis{ 2291 SkipCause: &api.ReleaseTrainAppPrognosis_SkipCause{ 2292 SkipCause: api.ReleaseTrainAppSkipCause_APP_ALREADY_IN_UPSTREAM_VERSION, 2293 }, 2294 Version: 0, 2295 } 2296 continue 2297 } 2298 2299 appLocks, err := state.GetEnvironmentApplicationLocks(c.Env, appName) 2300 2301 if err != nil { 2302 return ReleaseTrainEnvironmentPrognosis{ 2303 SkipCause: nil, 2304 Error: err, 2305 AppsPrognoses: nil, 2306 } 2307 } 2308 2309 if len(appLocks) > 0 { 2310 appsPrognoses[appName] = ReleaseTrainApplicationPrognosis{ 2311 SkipCause: &api.ReleaseTrainAppPrognosis_SkipCause{ 2312 SkipCause: api.ReleaseTrainAppSkipCause_APP_IS_LOCKED, 2313 }, 2314 Version: 0, 2315 } 2316 continue 2317 } 2318 2319 fs := state.Filesystem 2320 2321 releaseDir := releasesDirectoryWithVersion(fs, appName, versionToDeploy) 2322 manifest := fs.Join(releaseDir, "environments", c.Env, "manifests.yaml") 2323 2324 if _, err := fs.Stat(manifest); err != nil { 2325 appsPrognoses[appName] = ReleaseTrainApplicationPrognosis{ 2326 SkipCause: &api.ReleaseTrainAppPrognosis_SkipCause{ 2327 SkipCause: api.ReleaseTrainAppSkipCause_APP_DOES_NOT_EXIST_IN_ENV, 2328 }, 2329 Version: 0, 2330 } 2331 continue 2332 } 2333 2334 appsPrognoses[appName] = ReleaseTrainApplicationPrognosis{ 2335 SkipCause: nil, 2336 Version: versionToDeploy, 2337 } 2338 } 2339 2340 return ReleaseTrainEnvironmentPrognosis{ 2341 SkipCause: nil, 2342 Error: nil, 2343 AppsPrognoses: appsPrognoses, 2344 } 2345 } 2346 2347 func (c *envReleaseTrain) Transform( 2348 ctx context.Context, 2349 state *State, 2350 t TransformerContext, 2351 ) (string, error) { 2352 renderEnvironmentSkipCause := func(SkipCause *api.ReleaseTrainEnvPrognosis_SkipCause) string { 2353 envConfig := c.EnvGroupConfigs[c.Env] 2354 upstreamEnvName := envConfig.Upstream.Environment 2355 switch SkipCause.SkipCause { 2356 case api.ReleaseTrainEnvSkipCause_ENV_HAS_NO_UPSTREAM: 2357 return fmt.Sprintf("Environment '%q' does not have upstream configured - skipping.", c.Env) 2358 case api.ReleaseTrainEnvSkipCause_ENV_HAS_NO_UPSTREAM_LATEST_OR_UPSTREAM_ENV: 2359 return fmt.Sprintf("Environment %q does not have upstream.latest or upstream.environment configured - skipping.", c.Env) 2360 case api.ReleaseTrainEnvSkipCause_ENV_HAS_BOTH_UPSTREAM_LATEST_AND_UPSTREAM_ENV: 2361 return fmt.Sprintf("Environment %q has both upstream.latest and upstream.environment configured - skipping.", c.Env) 2362 case api.ReleaseTrainEnvSkipCause_UPSTREAM_ENV_CONFIG_NOT_FOUND: 2363 return fmt.Sprintf("Could not find environment config for upstream env %q. Target env was %q", upstreamEnvName, c.Env) 2364 case api.ReleaseTrainEnvSkipCause_ENV_IS_LOCKED: 2365 return fmt.Sprintf("Target Environment '%s' is locked - skipping.", c.Env) 2366 default: 2367 return fmt.Sprintf("Environment '%s' is skipped for an unrecognized reason", c.Env) 2368 } 2369 } 2370 2371 renderApplicationSkipCause := func(SkipCause *api.ReleaseTrainAppPrognosis_SkipCause, appName string) string { 2372 envConfig := c.EnvGroupConfigs[c.Env] 2373 upstreamEnvName := envConfig.Upstream.Environment 2374 currentlyDeployedVersion, _ := state.GetEnvironmentApplicationVersion(c.Env, appName) 2375 switch SkipCause.SkipCause { 2376 case api.ReleaseTrainAppSkipCause_APP_HAS_NO_VERSION_IN_UPSTREAM_ENV: 2377 return fmt.Sprintf("skipping because there is no version for application %q in env %q \n", appName, upstreamEnvName) 2378 case api.ReleaseTrainAppSkipCause_APP_ALREADY_IN_UPSTREAM_VERSION: 2379 return fmt.Sprintf("skipping %q because it is already in the version %d\n", appName, currentlyDeployedVersion) 2380 case api.ReleaseTrainAppSkipCause_APP_IS_LOCKED: 2381 return fmt.Sprintf("skipping application %q in environment %q due to application lock", appName, c.Env) 2382 case api.ReleaseTrainAppSkipCause_APP_DOES_NOT_EXIST_IN_ENV: 2383 return fmt.Sprintf("skipping application %q in environment %q because it doesn't exist there", appName, c.Env) 2384 default: 2385 return fmt.Sprintf("skipping application %q in environment %q for an unrecognized reason", appName, c.Env) 2386 } 2387 } 2388 2389 prognosis := c.prognosis(ctx, state) 2390 2391 if prognosis.Error != nil { 2392 return "", prognosis.Error 2393 } 2394 if prognosis.SkipCause != nil { 2395 return renderEnvironmentSkipCause(prognosis.SkipCause), nil 2396 } 2397 2398 envConfig := c.EnvGroupConfigs[c.Env] 2399 upstreamLatest := envConfig.Upstream.Latest 2400 upstreamEnvName := envConfig.Upstream.Environment 2401 2402 source := upstreamEnvName 2403 if upstreamLatest { 2404 source = "latest" 2405 } 2406 2407 // now iterate over all apps, deploying all that are not locked 2408 var skipped []string 2409 2410 // sorting for determinism 2411 appNames := make([]string, 0, len(prognosis.AppsPrognoses)) 2412 for appName := range prognosis.AppsPrognoses { 2413 appNames = append(appNames, appName) 2414 } 2415 sort.Strings(appNames) 2416 2417 for _, appName := range appNames { 2418 appPrognosis := prognosis.AppsPrognoses[appName] 2419 if appPrognosis.SkipCause != nil { 2420 skipped = append(skipped, renderApplicationSkipCause(appPrognosis.SkipCause, appName)) 2421 continue 2422 } 2423 d := &DeployApplicationVersion{ 2424 Environment: c.Env, // here we deploy to the next env 2425 Application: appName, 2426 Version: appPrognosis.Version, 2427 LockBehaviour: api.LockBehavior_RECORD, 2428 Authentication: c.Parent.Authentication, 2429 WriteCommitData: c.WriteCommitData, 2430 SourceTrain: &DeployApplicationVersionSource{ 2431 Upstream: upstreamEnvName, 2432 TargetGroup: c.TrainGroup, 2433 }, 2434 } 2435 if err := t.Execute(d); err != nil { 2436 return "", grpc.InternalError(ctx, fmt.Errorf("unexpected error while deploying app %q to env %q: %w", appName, c.Env, err)) 2437 } 2438 } 2439 teamInfo := "" 2440 if c.Parent.Team != "" { 2441 teamInfo = " for team '" + c.Parent.Team + "'" 2442 } 2443 if err := t.Execute(&skippedServices{ 2444 Messages: skipped, 2445 }); err != nil { 2446 return "", err 2447 } 2448 return fmt.Sprintf("Release Train to '%s' environment:\n\n"+ 2449 "The release train deployed %d services from '%s' to '%s'%s", 2450 c.Env, len(prognosis.AppsPrognoses), source, c.Env, teamInfo, 2451 ), nil 2452 } 2453 2454 // skippedServices is a helper Transformer to generate the "skipped 2455 // services" commit log. 2456 type skippedServices struct { 2457 Messages []string 2458 } 2459 2460 func (c *skippedServices) Transform( 2461 ctx context.Context, 2462 state *State, 2463 t TransformerContext, 2464 ) (string, error) { 2465 if len(c.Messages) == 0 { 2466 return "", nil 2467 } 2468 for _, msg := range c.Messages { 2469 if err := t.Execute(&skippedService{Message: msg}); err != nil { 2470 return "", err 2471 } 2472 } 2473 return "Skipped services", nil 2474 } 2475 2476 type skippedService struct { 2477 Message string 2478 } 2479 2480 func (c *skippedService) Transform( 2481 ctx context.Context, 2482 state *State, 2483 t TransformerContext, 2484 ) (string, error) { 2485 return c.Message, nil 2486 }