sg/msp: warn user if sg-msp lockfile differs from current version (#62134)

In https://github.com/sourcegraph/managed-services/pull/1288 I'm introducing granular, per-category version locks for https://github.com/sourcegraph/managed-services/issues/599. This change adds warnings that are shown to the user on version mismatches.

This may cause some friction with users, but for now, let's just deal with this on a case-by-case basis - our primary goal right now is to build a process for https://github.com/sourcegraph/managed-services/issues/599 by introducing more granular version locks.

Requires https://github.com/sourcegraph/sourcegraph/pull/62176 so that we can access the same version format as the one used in our lockfiles.

Details on our locking strategy is here: https://sourcegraph.notion.site/Deploying-new-versions-of-MSP-1808e7e45bd54f419dd93af542d99238#58dabe4992754ca18ed39bc212ccbbba

## Test plan

```
sg msp generate -all
```

![image](https://github.com/sourcegraph/sourcegraph/assets/23356519/d5c19821-7a26-4a91-a55e-1de03de2912b)
This commit is contained in:
Robert Lin 2024-04-29 17:50:24 -07:00 committed by GitHub
parent 3b5fb78377
commit 0fb6806f3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 96 additions and 1 deletions

View File

@ -7,6 +7,7 @@ import (
"path/filepath"
"slices"
"strings"
"sync"
"github.com/urfave/cli/v2"
"golang.org/x/exp/maps"
@ -152,7 +153,44 @@ func syncEnvironmentWorkspaces(c *cli.Context, tfc *terraformcloud.Client, servi
return std.Out.WriteMarkdown(summary.String())
}
type toolingLockfileChecker struct {
version string
categories map[spec.EnvironmentCategory]*sync.Once
}
// checkCategoryVersion performs warning checks for the given environment category's
// tooling version.
//
// Requires UseManagedServicesRepo.
func (c *toolingLockfileChecker) checkCategoryVersion(out *std.Output, category spec.EnvironmentCategory) {
var categoryOnce *sync.Once
if o, ok := c.categories[category]; ok {
categoryOnce = o
} else {
categoryOnce = &sync.Once{}
c.categories[category] = categoryOnce
}
categoryOnce.Do(func() {
lockedSgVersion, err := msprepo.ToolingLockfileVersion(category)
if err != nil {
out.WriteWarningf("Unable to determine locked 'sg' version for category %q: %s",
category, err.Error())
} else if lockedSgVersion != c.version {
out.WriteWarningf(`Lockfile for category %q declares 'sg' version %q, you are using %q - generated outputs may differ from what is expected.
If there is a diff in the generated output, try running the following:`,
category, lockedSgVersion, c.version)
_ = out.WriteCode("bash", fmt.Sprintf(
"sg update -release %q &&\n SG_SKIP_AUTO_UPDATE=true sg msp generate -all -category %q",
lockedSgVersion, string(category)))
}
})
}
type generateTerraformOptions struct {
// tooling is used to validate the current tooling version matches what
// is expected, and warn the user if there is a mismatch.
tooling *toolingLockfileChecker
// targetEnv generates the specified env only, otherwise generates all
targetEnv string
// targetCategory generates the specified category only
@ -188,6 +226,10 @@ func generateTerraform(service *spec.Spec, opts generateTerraformOptions) error
continue
}
// Check tooling version and emit warnings
opts.tooling.checkCategoryVersion(std.Out, env.Category)
// Then, start our actual work
pending := std.Out.Pending(output.StylePending.Linef(
"[%s] Preparing Terraform for %q environment %q", serviceID, env.Category, env.ID))
renderer := managedservicesplatform.Renderer{
@ -262,7 +304,8 @@ func generateTerraform(service *spec.Spec, opts generateTerraformOptions) error
}
pending.Complete(output.Styledf(output.StyleSuccess,
"[%s] Infrastructure assets generated in %q!", serviceID, renderer.OutputDir))
"[%s] Category %q environment %q infrastructure assets generated!",
serviceID, env.Category, env.ID))
}
return nil

View File

@ -2,9 +2,11 @@ package repo
import (
"context"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/urfave/cli/v2"
@ -13,6 +15,7 @@ import (
"github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/spec"
"github.com/sourcegraph/sourcegraph/dev/sg/internal/std"
"github.com/sourcegraph/sourcegraph/lib/cliutil/completions"
"github.com/sourcegraph/sourcegraph/lib/errors"
)
// UseManagedServicesRepo is a cli.BeforeFunc that enforces that we are in the
@ -94,19 +97,59 @@ func ServicesAndEnvironmentsCompletion(additionalArgs ...func(args cli.Args) (op
return completions.CompletePositionalArgs(append(args, additionalArgs...)...)
}
// ServiceYAMLPath returns the relative path to the service.yaml file for the
// given service.
//
// Requires UseManagedServicesRepo to be relevant.
func ServiceYAMLPath(serviceID string) string {
return filepath.Join("services", serviceID, "service.yaml")
}
// ServiceEnvironmentYAMLPath returns the relative path to the Terraform Stacks
// directory for the given service environment's stack.
//
// Requires UseManagedServicesRepo to be relevant.
func ServiceStackPath(serviceID, envID, stackID string) string {
return filepath.Join("services", serviceID, "terraform", envID, "stacks", stackID)
}
// ServiceStackTerraformPath returns the relative path to the Terraform CDKTF
// configuration file for the given service environment's stack.
//
// Requires UseManagedServicesRepo to be relevant.
func ServiceStackCDKTFPath(serviceID, envID, stackID string) string {
return filepath.Join(ServiceStackPath(serviceID, envID, stackID), "cdk.tf.json")
}
// ToolingLockfileVersion retrieves the contents of the sg-msp lockfile for the
// given category (./sg-msp-$CATEGORY.lock).
//
// Requires UseManagedServicesRepo.
func ToolingLockfileVersion(category spec.EnvironmentCategory) (string, error) {
lockfile := fmt.Sprintf("sg-msp-%s.lock", category)
if category == "" {
lockfile = "sg-msp.lock" // fallback to the old format (no category)
}
contents, err := os.ReadFile(lockfile)
if err != nil {
// Try to fall back to category-less-lockfile
if v, fallbackErr := ToolingLockfileVersion(""); fallbackErr == nil {
return v, nil
}
// Otherwise, return the error we got.
return "", errors.Wrapf(err, "read %q", lockfile)
}
version := strings.TrimSpace(string(contents))
if len(version) == 0 {
return "", errors.Newf("empty %q", lockfile)
}
return version, nil
}
// GitRevision gets the revision of the managed-services repository.
//
// Requires UseManagedServicesRepo.
func GitRevision(ctx context.Context) (string, error) {
return run.Cmd(ctx, "git rev-parse HEAD").

View File

@ -8,6 +8,7 @@ import (
"slices"
"sort"
"strings"
"sync"
"github.com/urfave/cli/v2"
"golang.org/x/exp/maps"
@ -320,6 +321,11 @@ sg msp generate -all -category=test
}
}
toolingChecker := &toolingLockfileChecker{
version: c.App.Version,
categories: make(map[spec.EnvironmentCategory]*sync.Once),
}
// Generate a specific service environment if '-all' is not provided
if !generateAll {
std.Out.WriteNoticef("Generating a specific service environment...")
@ -328,6 +334,7 @@ sg msp generate -all -category=test
return err
}
return generateTerraform(svc, generateTerraformOptions{
tooling: toolingChecker,
targetEnv: env.ID,
stableGenerate: stableGenerate,
})
@ -341,6 +348,7 @@ sg msp generate -all -category=test
return err
}
return generateTerraform(svc, generateTerraformOptions{
tooling: toolingChecker,
stableGenerate: stableGenerate,
targetCategory: generateCategory,
})
@ -360,6 +368,7 @@ sg msp generate -all -category=test
return err
}
if err := generateTerraform(s, generateTerraformOptions{
tooling: toolingChecker,
stableGenerate: stableGenerate,
targetCategory: generateCategory,
}); err != nil {