diff --git a/.semaphore/release/hashrelease.yml b/.semaphore/release/hashrelease.yml index d8de6bedae4..1112a4030b8 100644 --- a/.semaphore/release/hashrelease.yml +++ b/.semaphore/release/hashrelease.yml @@ -60,9 +60,10 @@ blocks: jobs: - name: Build and publish hashrelease commands: - - if [[ ${SEMAPHORE_WORKFLOW_TRIGGERED_BY_SCHEDULE} == "true" ]]; then export BUILD_CONTAINER_IMAGES=true; export SKIP_PUBLISH_IMAGES=false; fi - make hashrelease + - make hashrelease-publish prologue: commands: + - if [[ ${SEMAPHORE_WORKFLOW_TRIGGERED_BY_SCHEDULE} == "true" ]]; then export BUILD_CONTAINER_IMAGES=true; export SKIP_PUBLISH_IMAGES=false; fi - export GITHUB_TOKEN=${MARVIN_GITHUB_TOKEN} - cd release diff --git a/Makefile b/Makefile index 4ab4d04a937..af1502dbe15 100644 --- a/Makefile +++ b/Makefile @@ -113,7 +113,7 @@ e2e-test: ############################################################################### # Build the release tool. release/bin/release: $(shell find ./release -type f -name '*.go') - $(call build_binary, ./release/build, $@) + $(MAKE) -C release build # Install ghr for publishing to github. bin/ghr: diff --git a/go.mod b/go.mod index 7debbd18bc5..32b6e3e9264 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,7 @@ require ( github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 github.com/mipearson/rfw v0.0.0-20170619235010-6f0a6f3266ba github.com/mitchellh/go-homedir v1.1.0 + github.com/mitchellh/mapstructure v1.5.0 github.com/natefinch/atomic v1.0.1 github.com/nmrshll/go-cp v0.0.0-20180115193924-61436d3b7cfa github.com/olekukonko/tablewriter v0.0.5 @@ -210,7 +211,6 @@ require ( github.com/mdlayher/genetlink v1.0.0 // indirect github.com/mdlayher/netlink v1.1.0 // indirect github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/moby/sys/mountinfo v0.6.2 // indirect diff --git a/release/Makefile b/release/Makefile index 6d3ac20b44e..64734fa1ffe 100644 --- a/release/Makefile +++ b/release/Makefile @@ -13,7 +13,7 @@ clean: bin/release: $(shell find . -name "*.go") @mkdir -p bin && \ - $(call build_binary, ./build, bin/release) + $(call build_binary, ./cmd, bin/release) ############################################################################### # CI/CD @@ -27,6 +27,8 @@ ci: static-checks .PHONY: hashrelease hashrelease: bin/release var-require-all-GITHUB_TOKEN @bin/release hashrelease build + +hashrelease-publish: bin/release @bin/release hashrelease publish ############################################################################### diff --git a/release/build/main.go b/release/build/main.go deleted file mode 100644 index 1fa544de32d..00000000000 --- a/release/build/main.go +++ /dev/null @@ -1,574 +0,0 @@ -// Copyright (c) 2024 Tigera, Inc. All rights reserved. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package main - -import ( - "fmt" - "io" - "os" - "path/filepath" - - "github.com/sirupsen/logrus" - cli "github.com/urfave/cli/v2" - "gopkg.in/natefinch/lumberjack.v2" - - "github.com/projectcalico/calico/release/internal/config" - "github.com/projectcalico/calico/release/internal/hashreleaseserver" - "github.com/projectcalico/calico/release/internal/outputs" - "github.com/projectcalico/calico/release/internal/pinnedversion" - "github.com/projectcalico/calico/release/internal/registry" - "github.com/projectcalico/calico/release/internal/utils" - "github.com/projectcalico/calico/release/internal/version" - "github.com/projectcalico/calico/release/pkg/manager/branch" - "github.com/projectcalico/calico/release/pkg/manager/calico" - "github.com/projectcalico/calico/release/pkg/manager/operator" - "github.com/projectcalico/calico/release/pkg/tasks" -) - -const ( - latestFlag = "latest" - skipValidationFlag = "skip-validation" - skipImageScanFlag = "skip-image-scan" - skipBranchCheckFlag = "skip-branch-check" - publishBranchFlag = "git-publish" - buildImagesFlag = "build-images" - - orgFlag = "org" - repoFlag = "repo" - - imageRegistryFlag = "registry" - - operatorOrgFlag = "operator-org" - operatorRepoFlag = "operator-repo" - operatorImageFlag = "operator-image" - operatorRegistryFlag = "operator-registry" - - sourceBranchFlag = "source-branch" - newBranchFlag = "new-branch-version" - - // Configuration flags for the release publish command. - skipPublishImagesFlag = "skip-publish-images" - skipPublishGitTagFlag = "skip-publish-git-tag" - skipPublishGithubReleaseFlag = "skip-publish-github-release" - skipPublishHashreleaseFlag = "skip-publish-hashrelease-server" -) - -var ( - // debug controls whether or not to emit debug level logging. - debug bool - - // hashreleaseDir is the directory where hashreleases are built relative to the repo root. - hashreleaseDir = []string{"release", "_output", "hashrelease"} - - // releaseNotesDir is the directory where release notes are stored - releaseNotesDir = "release-notes" -) - -func configureLogging(filename string) { - if debug { - logrus.SetLevel(logrus.DebugLevel) - } else { - logrus.SetLevel(logrus.InfoLevel) - } - - // Set up logging to both stdout as well as a file. - writers := []io.Writer{os.Stdout, &lumberjack.Logger{ - Filename: filename, - MaxSize: 100, - MaxAge: 30, - MaxBackups: 10, - }} - logrus.SetOutput(io.MultiWriter(writers...)) -} - -// globalFlags are flags that are available to all sub-commands. -var globalFlags = []cli.Flag{ - &cli.BoolFlag{ - Name: "debug", - Aliases: []string{"d"}, - Usage: "Enable verbose log output", - Value: false, - Destination: &debug, - }, -} - -func main() { - cfg := config.LoadConfig() - - app := &cli.App{ - Name: "release", - Usage: fmt.Sprintf("a tool for building %s releases", utils.DisplayProductName()), - Flags: globalFlags, - Commands: []*cli.Command{}, - } - - // Add sub-commands below. - - // The hashrelease command suite is used to build and publish hashreleases, as well as - // to interact with the hashrelease server. - app.Commands = append(app.Commands, &cli.Command{ - Name: "hashrelease", - Aliases: []string{"hr"}, - Usage: "Build and publish hashreleases.", - Subcommands: hashreleaseSubCommands(cfg), - }) - - // The release command suite is used to build and publish official Calico releases. - app.Commands = append(app.Commands, &cli.Command{ - Name: "release", - Aliases: []string{"rel"}, - Usage: "Build and publish official Calico releases.", - Subcommands: releaseSubCommands(cfg), - }) - - // The branch command suite manages branches. - app.Commands = append(app.Commands, &cli.Command{ - Name: "branch", - Aliases: []string{"br"}, - Usage: "Manage branches.", - Subcommands: branchSubCommands(cfg), - }) - - // Run the app. - if err := app.Run(os.Args); err != nil { - logrus.WithError(err).Fatal("Error running task") - } -} - -func hashreleaseSubCommands(cfg *config.Config) []*cli.Command { - // dir is the directory where hashreleases are built. - dir := filepath.Join(append([]string{cfg.RepoRootDir}, hashreleaseDir...)...) - - return []*cli.Command{ - // The build command is used to produce a new local hashrelease in the output directory. - { - Name: "build", - Usage: "Build a hashrelease locally in _output/", - Flags: []cli.Flag{ - &cli.StringFlag{Name: orgFlag, Usage: "Git organization", EnvVars: []string{"ORGANIZATION"}, Value: config.DefaultOrg}, - &cli.StringFlag{Name: repoFlag, Usage: "Git repository", EnvVars: []string{"GIT_REPO"}, Value: config.DefaultRepo}, - &cli.BoolFlag{Name: skipValidationFlag, Usage: "Skip all pre-build validation", Value: false}, - &cli.BoolFlag{Name: skipBranchCheckFlag, Usage: "Skip check that this is a valid release branch.", Value: false}, - &cli.BoolFlag{Name: buildImagesFlag, Usage: "Build images from local codebase. If false, will use images from CI instead.", EnvVars: []string{"BUILD_CONTAINER_IMAGES"}, Value: false}, - &cli.StringSliceFlag{Name: imageRegistryFlag, Usage: "Specify image registry or registries to use", EnvVars: []string{"REGISTRIES"}, Value: &cli.StringSlice{}}, - &cli.StringFlag{Name: operatorOrgFlag, Usage: "Operator git organization", EnvVars: []string{"OPERATOR_GIT_ORGANIZATION"}, Value: config.OperatorDefaultOrg}, - &cli.StringFlag{Name: operatorRepoFlag, Usage: "Operator git repository", EnvVars: []string{"OPERATOR_GIT_REPO"}, Value: config.OperatorDefaultRepo}, - &cli.StringFlag{Name: operatorImageFlag, Usage: "Specify the operator image to use", EnvVars: []string{"OPERATOR_IMAGE"}, Value: config.OperatorDefaultImage}, - &cli.StringFlag{Name: operatorRegistryFlag, Usage: "Specify the operator registry to use", EnvVars: []string{"OPERATOR_REGISTRY"}, Value: registry.QuayRegistry}, - }, - Action: func(c *cli.Context) error { - configureLogging("hashrelease-build.log") - if c.Bool(skipValidationFlag) && !c.Bool(skipBranchCheckFlag) { - return fmt.Errorf("%s must be set if %s is set", skipBranchCheckFlag, skipValidationFlag) - } - if len(c.StringSlice(imageRegistryFlag)) > 0 && c.String(operatorRegistryFlag) == "" { - return fmt.Errorf("%s must be set if %s is set", operatorRegistryFlag, imageRegistryFlag) - } - if c.String(operatorImageFlag) != "" && c.String(operatorRegistryFlag) == "" { - return fmt.Errorf("%s must be set if %s is set", operatorRegistryFlag, operatorImageFlag) - } else if c.String(operatorRegistryFlag) != "" && c.String(operatorImageFlag) == "" { - return fmt.Errorf("%s must be set if %s is set", operatorImageFlag, operatorRegistryFlag) - } - if !cfg.CI.IsCI { - if len(c.StringSlice(imageRegistryFlag)) == 0 && c.Bool(buildImagesFlag) { - logrus.Warn("Local builds should specify an image registry using the --dev-registry flag") - } - if c.String(operatorRegistryFlag) == registry.QuayRegistry && c.String(operatorImageFlag) == config.OperatorDefaultImage { - logrus.Warn("Local builds should specify an operator image and registry using the --operator-image and --operator-registry flags") - } - } - - // Clone the operator repository - if err := utils.Clone(fmt.Sprintf("git@github.com:%s/%s.git", c.String(operatorOrgFlag), c.String(operatorRepoFlag)), cfg.Operator.Branch, cfg.Operator.Dir); err != nil { - return err - } - - // Create the pinned-version.yaml file and extract the versions and hash. - pinnedCfg := pinnedversion.Config{ - RootDir: cfg.RepoRootDir, - ReleaseBranchPrefix: cfg.RepoReleaseBranchPrefix, - Operator: cfg.Operator, - } - if c.String(operatorImageFlag) != "" { - pinnedCfg.Operator.Image = c.String(operatorImageFlag) - } - if c.String(operatorRegistryFlag) != "" { - pinnedCfg.Operator.Registry = c.String(operatorRegistryFlag) - } - _, data, err := pinnedversion.GeneratePinnedVersionFile(pinnedCfg, cfg.TmpFolderPath()) - if err != nil { - return err - } - - versions := &version.Data{ - ProductVersion: version.New(data.ProductVersion), - OperatorVersion: version.New(data.Operator.Version), - } - - // Check if the hashrelease has already been published. - if published, err := tasks.HashreleasePublished(cfg, data.Hash); err != nil { - return err - } else if published { - // On CI, we want it to fail if the hashrelease has already been published. - // However, on local builds, we just log a warning and continue. - if cfg.CI.IsCI { - return fmt.Errorf("hashrelease %s has already been published", data.Hash) - } else { - logrus.Warnf("hashrelease %s has already been published", data.Hash) - } - } - - // Build the operator - operatorOpts := []operator.Option{ - operator.WithOperatorDirectory(cfg.Operator.Dir), - operator.WithReleaseBranchPrefix(cfg.RepoReleaseBranchPrefix), - operator.IsHashRelease(), - operator.WithArchitectures(cfg.Arches), - operator.WithValidate(!c.Bool(skipValidationFlag)), - operator.WithReleaseBranchValidation(!c.Bool(skipBranchCheckFlag)), - operator.WithVersion(versions.OperatorVersion.FormattedString()), - operator.WithCalicoDirectory(cfg.RepoRootDir), - } - o := operator.NewManager(operatorOpts...) - if err := o.Build(cfg.TmpFolderPath()); err != nil { - return err - } - - // Configure a release builder using the generated versions, and use it - // to build a Calico release. - opts := []calico.Option{ - calico.WithRepoRoot(cfg.RepoRootDir), - calico.WithReleaseBranchPrefix(cfg.RepoReleaseBranchPrefix), - calico.IsHashRelease(), - calico.WithVersions(versions), - calico.WithOutputDir(dir), - calico.WithBuildImages(c.Bool(buildImagesFlag)), - calico.WithValidate(!c.Bool(skipValidationFlag)), - calico.WithReleaseBranchValidation(!c.Bool(skipBranchCheckFlag)), - calico.WithGithubOrg(c.String(orgFlag)), - calico.WithRepoName(c.String(repoFlag)), - calico.WithRepoRemote(cfg.GitRemote), - calico.WithArchitectures(cfg.Arches), - } - if reg := c.StringSlice(imageRegistryFlag); len(reg) > 0 { - opts = append(opts, calico.WithImageRegistries(reg)) - } - - r := calico.NewManager(opts...) - if err := r.Build(); err != nil { - return err - } - - // For real releases, release notes are generated prior to building the release. - // For hash releases, generate a set of release notes and add them to the hashrelease directory. - releaseVersion, err := version.DetermineReleaseVersion(versions.ProductVersion, cfg.DevTagSuffix) - if err != nil { - return fmt.Errorf("failed to determine release version: %v", err) - } - if _, err := outputs.ReleaseNotes(config.DefaultOrg, cfg.GithubToken, cfg.RepoRootDir, filepath.Join(dir, releaseNotesDir), releaseVersion); err != nil { - return err - } - - // Adjsut the formatting of the generated outputs to match the legacy hashrelease format. - return tasks.ReformatHashrelease(cfg, dir) - }, - }, - - // The publish command is used to publish a locally built hashrelease to the hashrelease server. - { - Name: "publish", - Usage: "Publish hashrelease from _output/ to hashrelease server", - Flags: []cli.Flag{ - &cli.StringFlag{Name: orgFlag, Usage: "Git organization", EnvVars: []string{"ORGANIZATION"}, Value: config.DefaultOrg}, - &cli.StringFlag{Name: repoFlag, Usage: "Git repository", EnvVars: []string{"GIT_REPO"}, Value: config.DefaultRepo}, - &cli.StringSliceFlag{Name: imageRegistryFlag, Usage: "Specify image registry or registries to use", EnvVars: []string{"REGISTRIES"}, Value: &cli.StringSlice{}}, - &cli.BoolFlag{Name: skipPublishImagesFlag, Usage: "Skip publishing of container images to registry/registries", EnvVars: []string{"SKIP_PUBLISH_IMAGES"}, Value: true}, - &cli.BoolFlag{Name: skipPublishHashreleaseFlag, Usage: "Skip publishing to hashrelease server", Value: false}, - &cli.BoolFlag{Name: latestFlag, Usage: "Promote this release as the latest for this stream", Value: true}, - &cli.BoolFlag{Name: skipValidationFlag, Usage: "Skip pre-build validation", Value: false}, - &cli.BoolFlag{Name: skipImageScanFlag, Usage: "Skip sending images to image scan service.", Value: false}, - }, - Action: func(c *cli.Context) error { - configureLogging("hashrelease-publish.log") - - // If using a custom registry, do not set the hashrelease as latest - if len(c.StringSlice(imageRegistryFlag)) > 0 && c.Bool(latestFlag) { - return fmt.Errorf("cannot set hashrelease as latest when using a custom registry") - } - - // If skipValidationFlag is set, then we will also skip the image scan. Ensure the user - // has set the correct flags. - if c.Bool(skipValidationFlag) && !c.Bool(skipImageScanFlag) { - return fmt.Errorf("%s must be set if %s is set", skipImageScanFlag, skipValidationFlag) - } - - // Extract the pinned version as a hashrelease. - hashrel, err := pinnedversion.LoadHashrelease(cfg.RepoRootDir, cfg.TmpFolderPath(), dir) - if err != nil { - return err - } - if c.Bool(latestFlag) { - hashrel.Latest = true - } - - // Check if the hashrelease has already been published. - if published, err := tasks.HashreleasePublished(cfg, hashrel.Hash); err != nil { - return err - } else if published { - return fmt.Errorf("%s hashrelease (%s) has already been published", hashrel.Name, hashrel.Hash) - } - - // Push the operator hashrelease first before validaion - // This is because validation checks all images exists and sends to Image Scan Service - o := operator.NewManager( - operator.WithOperatorDirectory(cfg.Operator.Dir), - operator.IsHashRelease(), - operator.WithArchitectures(cfg.Arches), - operator.WithValidate(!c.Bool(skipValidationFlag)), - ) - if err := o.Publish(cfg.TmpFolderPath()); err != nil { - return err - } - - opts := []calico.Option{ - calico.WithRepoRoot(cfg.RepoRootDir), - calico.IsHashRelease(), - calico.WithVersions(&version.Data{ - ProductVersion: version.New(hashrel.ProductVersion), - OperatorVersion: version.New(hashrel.OperatorVersion), - }), - calico.WithGithubOrg(c.String(orgFlag)), - calico.WithRepoName(c.String(repoFlag)), - calico.WithRepoRemote(cfg.GitRemote), - calico.WithValidate(!c.Bool(skipValidationFlag)), - calico.WithTmpDir(cfg.TmpFolderPath()), - calico.WithHashrelease(*hashrel, cfg.HashreleaseServerConfig), - calico.WithPublishImages(!c.Bool(skipPublishImagesFlag)), - calico.WithPublishHashrelease(!c.Bool(skipPublishHashreleaseFlag)), - calico.WithImageScanning(!c.Bool(skipImageScanFlag), cfg.ImageScannerConfig), - } - if reg := c.StringSlice(imageRegistryFlag); len(reg) > 0 { - opts = append(opts, calico.WithImageRegistries(reg)) - } - r := calico.NewManager(opts...) - if err := r.PublishRelease(); err != nil { - return err - } - - // Send a slack message to notify that the hashrelease has been published. - if !c.Bool(skipPublishHashreleaseFlag) { - if err := tasks.HashreleaseSlackMessage(cfg, hashrel); err != nil { - return err - } - } - return nil - }, - }, - - // The garbage-collect command is used to clean up older hashreleases from the hashrelease server. - { - Name: "garbage-collect", - Usage: "Clean up older hashreleases", - Aliases: []string{"gc"}, - Action: func(c *cli.Context) error { - configureLogging("hashrelease-garbage-collect.log") - return hashreleaseserver.CleanOldHashreleases(&cfg.HashreleaseServerConfig) - }, - }, - } -} - -func releaseSubCommands(cfg *config.Config) []*cli.Command { - // Base location for release uploads. Each release will get a directory - // within this location. - baseUploadDir := filepath.Join(cfg.RepoRootDir, "release", "_output", "upload") - - return []*cli.Command{ - // Build release notes prior to a release. - { - Name: "generate-release-notes", - Usage: "Generate release notes for the next release", - Flags: []cli.Flag{ - &cli.StringFlag{Name: orgFlag, Usage: "Git organization", EnvVars: []string{"ORGANIZATION"}, Value: config.DefaultOrg}, - }, - Action: func(c *cli.Context) error { - configureLogging("release-notes.log") - ver, err := version.DetermineReleaseVersion(version.GitVersion(), cfg.DevTagSuffix) - if err != nil { - return err - } - filePath, err := outputs.ReleaseNotes(c.String(orgFlag), cfg.GithubToken, cfg.RepoRootDir, filepath.Join(cfg.RepoRootDir, releaseNotesDir), ver) - if err != nil { - logrus.WithError(err).Fatal("Failed to generate release notes") - } - logrus.WithField("file", filePath).Info("Generated release notes") - logrus.Info("Please review for accuracy, and format appropriately before releasing.") - return nil - }, - }, - - // Build a release. - { - Name: "build", - Usage: "Build an official Calico release", - Flags: []cli.Flag{ - &cli.StringFlag{Name: orgFlag, Usage: "Git organization", EnvVars: []string{"ORGANIZATION"}, Value: config.DefaultOrg}, - &cli.StringFlag{Name: repoFlag, Usage: "Git repository", EnvVars: []string{"GIT_REPO"}, Value: config.DefaultRepo}, - &cli.BoolFlag{Name: buildImagesFlag, Usage: "Build images from local codebase. If false, will use images from CI instead.", EnvVars: []string{"BUILD_CONTAINER_IMAGES"}, Value: true}, - &cli.BoolFlag{Name: skipValidationFlag, Usage: "Skip pre-build validation", Value: false}, - &cli.StringSliceFlag{Name: imageRegistryFlag, Usage: "Specify image registry or registries to use", EnvVars: []string{"REGISTRIES"}, Value: &cli.StringSlice{}}, - }, - Action: func(c *cli.Context) error { - configureLogging("release-build.log") - - // Determine the versions to use for the release. - ver, err := version.DetermineReleaseVersion(version.GitVersion(), cfg.DevTagSuffix) - if err != nil { - return err - } - operatorVer, err := version.DetermineOperatorVersion(cfg.RepoRootDir) - if err != nil { - return err - } - - // Configure the builder. - opts := []calico.Option{ - calico.WithRepoRoot(cfg.RepoRootDir), - calico.WithReleaseBranchPrefix(cfg.RepoReleaseBranchPrefix), - calico.WithVersions(&version.Data{ - ProductVersion: ver, - OperatorVersion: operatorVer, - }), - calico.WithOutputDir(filepath.Join(baseUploadDir, ver.FormattedString())), - calico.WithArchitectures(cfg.Arches), - calico.WithGithubOrg(c.String(orgFlag)), - calico.WithRepoName(c.String(repoFlag)), - calico.WithRepoRemote(cfg.GitRemote), - calico.WithBuildImages(c.Bool(buildImagesFlag)), - } - if c.Bool(skipValidationFlag) { - opts = append(opts, calico.WithValidate(false)) - } - if reg := c.StringSlice(imageRegistryFlag); len(reg) > 0 { - opts = append(opts, calico.WithImageRegistries(reg)) - } - r := calico.NewManager(opts...) - return r.Build() - }, - }, - - // Publish a release. - { - Name: "publish", - Usage: "Publish a pre-built Calico release", - Flags: []cli.Flag{ - &cli.StringFlag{Name: orgFlag, Usage: "Git organization", EnvVars: []string{"ORGANIZATION"}, Value: config.DefaultOrg}, - &cli.StringFlag{Name: repoFlag, Usage: "Git repository", EnvVars: []string{"GIT_REPO"}, Value: config.DefaultRepo}, - &cli.BoolFlag{Name: skipPublishImagesFlag, Usage: "Skip publishing of container images to registry", EnvVars: []string{"SKIP_PUBLISH_IMAGES"}, Value: false}, - &cli.BoolFlag{Name: skipPublishGitTagFlag, Usage: "Skip publishing of tag to git repository", Value: false}, - &cli.BoolFlag{Name: skipPublishGithubReleaseFlag, Usage: "Skip publishing of release to Github", Value: false}, - &cli.StringSliceFlag{Name: imageRegistryFlag, Usage: "Specify image registry or registries to use", EnvVars: []string{"REGISTRIES"}, Value: &cli.StringSlice{}}, - }, - Action: func(c *cli.Context) error { - configureLogging("release-publish.log") - - ver, operatorVer, err := version.VersionsFromManifests(cfg.RepoRootDir) - if err != nil { - return err - } - opts := []calico.Option{ - calico.WithRepoRoot(cfg.RepoRootDir), - calico.WithVersions(&version.Data{ - ProductVersion: ver, - OperatorVersion: operatorVer, - }), - calico.WithOutputDir(filepath.Join(baseUploadDir, ver.FormattedString())), - calico.WithGithubOrg(c.String(orgFlag)), - calico.WithRepoName(c.String(repoFlag)), - calico.WithRepoRemote(cfg.GitRemote), - calico.WithPublishImages(!c.Bool(skipPublishImagesFlag)), - calico.WithPublishGitTag(!c.Bool(skipPublishGitTagFlag)), - calico.WithPublishGithubRelease(!c.Bool(skipPublishGithubReleaseFlag)), - } - if reg := c.StringSlice(imageRegistryFlag); len(reg) > 0 { - opts = append(opts, calico.WithImageRegistries(reg)) - } - r := calico.NewManager(opts...) - return r.PublishRelease() - }, - }, - } -} - -func branchSubCommands(cfg *config.Config) []*cli.Command { - return []*cli.Command{ - // Cut a new release branch - { - Name: "cut", - Usage: fmt.Sprintf("Cut a new release branch from %s", utils.DefaultBranch), - Flags: []cli.Flag{ - &cli.BoolFlag{Name: skipValidationFlag, Usage: "Skip release branch cut validations", Value: false}, - &cli.BoolFlag{Name: publishBranchFlag, Usage: "Push branch and tag to git. If false, all changes are local.", Value: false}, - }, - Action: func(c *cli.Context) error { - configureLogging("cut-branch.log") - m := branch.NewManager(branch.WithRepoRoot(cfg.RepoRootDir), - branch.WithRepoRemote(cfg.GitRemote), - branch.WithMainBranch(utils.DefaultBranch), - branch.WithDevTagIdentifier(cfg.DevTagSuffix), - branch.WithReleaseBranchPrefix(cfg.RepoReleaseBranchPrefix), - branch.WithValidate(!c.Bool(skipValidationFlag)), - branch.WithPublish(c.Bool(publishBranchFlag))) - return m.CutReleaseBranch() - }, - }, - // Cut a new operator release branch - { - Name: "cut-operator", - Usage: fmt.Sprintf("Cut a new operator release branch from %s", utils.DefaultBranch), - Flags: []cli.Flag{ - &cli.StringFlag{Name: operatorOrgFlag, Usage: "Operator git organization", EnvVars: []string{"OPERATOR_GIT_ORGANIZATION"}, Value: config.OperatorDefaultOrg}, - &cli.StringFlag{Name: operatorRepoFlag, Usage: "Operator git repository", EnvVars: []string{"OPERATOR_GIT_REPO"}, Value: config.OperatorDefaultRepo}, - &cli.BoolFlag{Name: skipValidationFlag, Usage: "Skip release branch cut validations", Value: false}, - &cli.BoolFlag{Name: publishBranchFlag, Usage: "Push branch and tag to git. If false, all changes are local.", Value: false}, - &cli.StringFlag{Name: sourceBranchFlag, Usage: "The branch to cut the operator release from", Value: utils.DefaultBranch}, - &cli.StringFlag{Name: newBranchFlag, Usage: fmt.Sprintf("The new version for the branch to create i.e. vX.Y to create a %s-vX.Y branch", cfg.Operator.RepoReleaseBranchPrefix), Value: ""}, - }, - Action: func(c *cli.Context) error { - configureLogging("cut-operator-branch.log") - if c.String(newBranchFlag) == "" { - logrus.Warn("No branch version specified, will cut branch based on latest dev tag") - } - // Clone the operator repository - if err := utils.Clone(fmt.Sprintf("git@github.com:%s/%s.git", c.String(operatorOrgFlag), c.String(operatorRepoFlag)), cfg.Operator.Branch, cfg.Operator.Dir); err != nil { - return err - } - // Create operator manager - m := operator.NewManager( - operator.WithOperatorDirectory(cfg.Operator.Dir), - operator.WithRepoRemote(cfg.Operator.GitRemote), - operator.WithGithubOrg(c.String(operatorOrgFlag)), - operator.WithRepoName(c.String(operatorRepoFlag)), - operator.WithBranch(utils.DefaultBranch), - operator.WithDevTagIdentifier(cfg.Operator.DevTagSuffix), - operator.WithReleaseBranchPrefix(cfg.Operator.RepoReleaseBranchPrefix), - operator.WithValidate(!c.Bool(skipValidationFlag)), - operator.WithPublish(c.Bool(publishBranchFlag)), - ) - return m.CutBranch(c.String(newBranchFlag)) - }, - }, - } -} diff --git a/release/cmd/branch/branch.go b/release/cmd/branch/branch.go new file mode 100644 index 00000000000..8f7403ed43e --- /dev/null +++ b/release/cmd/branch/branch.go @@ -0,0 +1,161 @@ +// Copyright (c) 2024 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance branch.With the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// branch.WithOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package branch + +import ( + "fmt" + "path/filepath" + + "github.com/urfave/cli/v2" + + "github.com/projectcalico/calico/release/cmd/flags" + cmd "github.com/projectcalico/calico/release/cmd/utils" + "github.com/projectcalico/calico/release/internal/config" + "github.com/projectcalico/calico/release/internal/logger" + "github.com/projectcalico/calico/release/internal/utils" + "github.com/projectcalico/calico/release/pkg/manager/branch" + "github.com/projectcalico/calico/release/pkg/manager/operator" +) + +var ( + publishBranchFlagName = "git-publish" + publishBranchFlag = &cli.BoolFlag{ + Name: publishBranchFlagName, + Usage: "Publish the branch to the remote repository", + Value: true, + } + + baseBranchFlagName = "main-branch" + baseBranchFlag = &cli.StringFlag{ + Name: baseBranchFlagName, + Usage: "Branch to cut the release branch off of", + Value: utils.DefaultBranch, + } + + streamFlagName = "release-stream" + streamFlag = &cli.StringFlag{ + Name: streamFlagName, + Usage: "Release stream to use for the release", + } +) + +func Command(cfg *config.Config) *cli.Command { + b := NewBranchCommand(cfg) + return b.Command() +} + +func NewBranchCommand(cfg *config.Config) cmd.Command { + return &Branch{ + RepoRootDir: cfg.RepoRootDir, + TmpDir: cfg.TmpFolderPath(), + } +} + +type Branch struct { + RepoRootDir string + TmpDir string +} + +func (b *Branch) Command() *cli.Command { + return &cli.Command{ + Name: "branch", + Aliases: []string{"br"}, + Usage: "Manage branches", + Subcommands: b.Subcommands(), + } +} + +func (b *Branch) Subcommands() []*cli.Command { + return []*cli.Command{ + b.CutCmd(), + } +} + +func (b *Branch) CutCmd() *cli.Command { + return &cli.Command{ + Name: "cut", + Usage: fmt.Sprintf("Cut a new release branch from %s", utils.DefaultBranch), + Flags: []cli.Flag{ + flags.RepoRemoteFlag, + flags.ReleaseBranchPrefixFlag, + flags.DevTagSuffixFlag, + baseBranchFlag, + publishBranchFlag, + flags.SkipValidationFlag, + }, + Action: func(ctx *cli.Context) error { + logger.Configure("cut-branch.log", ctx.Bool(flags.DebugFlagName)) + + m := branch.NewManager( + branch.WithRepoRoot(b.RepoRootDir), + branch.WithRepoRemote(ctx.String(flags.RepoRemoteFlagName)), + branch.WithMainBranch(ctx.String(baseBranchFlagName)), + branch.WithDevTagIdentifier(ctx.String(flags.DevTagSuffixFlagName)), + branch.WithReleaseBranchPrefix(ctx.String(flags.ReleaseBranchPrefixFlagName)), + branch.WithValidate(!ctx.Bool(flags.SkipValidationFlagName)), + branch.WithPublish(ctx.Bool(publishBranchFlagName)), + ) + return m.CutReleaseBranch() + }, + } +} + +func (b *Branch) OperatorCutCmd() *cli.Command { + return &cli.Command{ + Name: "cut-operator", + Usage: "Cut a new operator release branch from the main branch", + Flags: []cli.Flag{ + flags.OperatorGitRemoteFlag, + flags.OperatorOrgFlag, + flags.OperatorRepoFlag, + flags.OperatorReleaseBranchPrefixFlag, + flags.OperatorDevTagSuffixFlag, + baseBranchFlag, + streamFlag, + publishBranchFlag, + flags.SkipValidationFlag, + }, + Action: func(ctx *cli.Context) error { + logger.Configure("cut-operator-branch.log", ctx.Bool(flags.DebugFlagName)) + + operatorDir := filepath.Join(b.TmpDir, "operator") + + // Clone the operator repository + if err := utils.Clone( + fmt.Sprintf("git@github.com:%s/%s.git", ctx.String(flags.OperatorOrgFlagName), ctx.String(flags.OperatorRepoFlagName)), + ctx.String(flags.OperatorBranchFlagName), operatorDir); err != nil { + return fmt.Errorf("failed to clone operator repository: %s", err) + } + + opts := []operator.Option{ + operator.WithOperatorDirectory(operatorDir), + operator.WithRepoRemote(ctx.String(flags.OperatorGitRemoteFlagName)), + operator.WithGithubOrg(ctx.String(flags.OperatorOrgFlagName)), + operator.WithRepoName(ctx.String(flags.OperatorRepoFlagName)), + operator.WithBranch(ctx.String(baseBranchFlagName)), + operator.WithDevTagIdentifier(ctx.String(flags.OperatorDevTagSuffixFlagName)), + operator.WithReleaseBranchPrefix(ctx.String(flags.OperatorReleaseBranchPrefixFlagName)), + operator.WithValidate(!ctx.Bool(flags.SkipValidationFlagName)), + operator.WithPublish(ctx.Bool(publishBranchFlagName)), + } + if ctx.String(streamFlagName) == "" { + opts = append(opts, operator.WithReleaseStream(ctx.String(streamFlagName))) + } + + m := operator.NewManager(opts...) + return m.CutBranch() + }, + } +} diff --git a/release/cmd/flags/common.go b/release/cmd/flags/common.go new file mode 100644 index 00000000000..64babf0ec83 --- /dev/null +++ b/release/cmd/flags/common.go @@ -0,0 +1,133 @@ +// Copyright (c) 2024 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flags + +import ( + "fmt" + + "github.com/urfave/cli/v2" +) + +// Git flags +var ( + OrgFlagName = "org" + OrgFlag = &cli.StringFlag{ + Name: "org", + Usage: "The GitHub organization to use for the release", + EnvVars: []string{"ORGANIZATION"}, + Value: "projectcalico", + } + + RepoFlagName = "repo" + RepoFlag = &cli.StringFlag{ + Name: "repo", + Usage: "The GitHub repository to use for the release", + EnvVars: []string{"GIT_REPO"}, + Value: "calico", + } + + RepoRemoteFlagName = "remote" + RepoRemoteFlag = &cli.StringFlag{ + Name: "remote", + Usage: "The remote for the git repository", + EnvVars: []string{"GIT_REMOTE"}, + Value: "origin", + } + + GitFlags = []cli.Flag{OrgFlag, RepoFlag, RepoRemoteFlag} +) + +var ProductFlags = append(GitFlags, []cli.Flag{ + ReleaseBranchPrefixFlag, + DevTagSuffixFlag, +}...) + +var ( + ReleaseBranchPrefixFlagName = "release-branch-prefix" + ReleaseBranchPrefixFlag = &cli.StringFlag{ + Name: "release-branch-prefix", + Usage: "The stardard prefix used to denote release branches", + Value: "release", + } + + DevTagSuffixFlagName = "dev-tag-suffix" + DevTagSuffixFlag = &cli.StringFlag{ + Name: "dev-tag-suffix", + Usage: "The suffix used to denote development tags", + EnvVars: []string{"DEV_TAG_SUFFIX"}, + Value: "0.dev", + } +) + +var ( + SkipValidationFlagName = "skip-validation" + SkipValidationFlag = &cli.BoolFlag{ + Name: "skip-validation", + Usage: "Skip all validation while performing the action", + Value: false, + } +) + +var ( + RegistryFlagName = "registry" + RegistryFlag = &cli.StringSliceFlag{ + Name: RegistryFlagName, + Usage: "Override default registries for the release. Repeat for multiple registries.", + EnvVars: []string{"REGISTRIES"}, + Value: cli.NewStringSlice(), + } +) + +var ( + ArchFlagName = "arch" + ArchFlag = &cli.StringSliceFlag{ + Name: ArchFlagName, + Usage: "The architecture to use for the release. Repeat for multiple architectures.", + EnvVars: []string{"ARCHES"}, + Value: cli.NewStringSlice("amd64", "arm64", "ppc64le", "s390x"), + } +) + +var BuildImagesFlagName = "build-images" + +func BuildImagesFlag(defaultValue bool, product string) *cli.BoolFlag { + return &cli.BoolFlag{ + Name: BuildImagesFlagName, + Usage: fmt.Sprintf("Build container images for %s from local codebase", product), + EnvVars: []string{"BUILD_CONTAINER_IMAGES"}, + Value: defaultValue, + } +} + +var PublishImagesFlagName = "publish-images" + +func PublishImagesFlag(defaultValue bool) *cli.BoolFlag { + return &cli.BoolFlag{ + Name: PublishImagesFlagName, + Usage: "Publish images to the registry", + EnvVars: []string{"PUBLISH_IMAGES"}, + Value: defaultValue, + } +} + +var ( + GitHubTokenFlagName = "github-token" + GitHubTokenFlag = &cli.StringFlag{ + Name: "github-token", + Usage: "The GitHub token to use when interacting with the GitHub API", + EnvVars: []string{"GITHUB_TOKEN"}, + Required: true, + } +) diff --git a/release/cmd/flags/global.go b/release/cmd/flags/global.go new file mode 100644 index 00000000000..1501de3ae2e --- /dev/null +++ b/release/cmd/flags/global.go @@ -0,0 +1,88 @@ +// Copyright (c) 2024 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flags + +import ( + "fmt" + + "github.com/urfave/cli/v2" +) + +var ( + DebugFlagName = "debug" + + DebugFlag = &cli.BoolFlag{ + Name: "debug", + Aliases: []string{"d"}, + Usage: "Enable verbose log output", + Value: false, + } +) + +// Slack flags +var ( + SlackTokenFlagName = "slack-token" + SlackTokenFlag = &cli.StringFlag{ + Name: "slack-token", + Usage: "The Slack token to use for posting messages", + EnvVars: []string{"SLACK_TOKEN"}, + } + + SlackChannelFlagName = "slack-channel" + SlackChannelFlag = &cli.StringFlag{ + Name: "slack-channel", + Usage: "The Slack channel to post messages", + EnvVars: []string{"SLACK_CHANNEL"}, + } + + SlackFlags = []cli.Flag{SlackTokenFlag, SlackChannelFlag} +) + +// CI flags +var ( + semaphoreCI = "semaphore" + + CIFlagName = "ci" + CIFlag = &cli.BoolFlag{ + Name: "ci", + Usage: "Enable CI mode", + EnvVars: []string{"CI"}, + Value: false, + } + + CIURLFlagName = "ci-url" + CIURLFlag = &cli.StringFlag{ + Name: "ci-url", + Usage: fmt.Sprintf("The URL for accesing %s CI", semaphoreCI), + EnvVars: []string{"SEMAPHORE_ORGANIZATION_URL"}, + } + + CIJobIDFlagName = "ci-job-id" + CIJobIDFlag = &cli.StringFlag{ + Name: "ci-job-id", + Usage: fmt.Sprintf("The job ID for the %s CI job", semaphoreCI), + EnvVars: []string{"SEMAPHORE_JOB_ID"}, + } + + CIFlags = []cli.Flag{CIFlag, CIURLFlag, CIJobIDFlag} +) + +// GlobalFlags are flags that are available to all commands +func GlobalFlags() []cli.Flag { + f := []cli.Flag{DebugFlag} + f = append(f, SlackFlags...) + f = append(f, CIFlags...) + return f +} diff --git a/release/cmd/flags/operator.go b/release/cmd/flags/operator.go new file mode 100644 index 00000000000..d7b371f1efc --- /dev/null +++ b/release/cmd/flags/operator.go @@ -0,0 +1,93 @@ +// Copyright (c) 2024 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package flags + +import ( + "github.com/urfave/cli/v2" + + "github.com/projectcalico/calico/release/pkg/manager/operator" +) + +var OperatorFlags = []cli.Flag{ + OperatorOrgFlag, OperatorRepoFlag, OperatorBranchFlag, OperatorGitRemoteFlag, + OperatorReleaseBranchPrefixFlag, OperatorDevTagSuffixFlag, + OperatorRegistryFlag, OperatorImageFlag, +} + +// Git Flags +var ( + OperatorOrgFlagName = "operator-org" + OperatorOrgFlag = &cli.StringFlag{ + Name: OperatorOrgFlagName, + Usage: "The GitHub organization to use for Tigera operator release", + Value: operator.DefaultOrg, + } + + OperatorRepoFlagName = "operator-repo" + OperatorRepoFlag = &cli.StringFlag{ + Name: OperatorRepoFlagName, + Usage: "The GitHub repository to use for Tigera operator release", + Value: operator.DefaultRepoName, + } + + OperatorGitRemoteFlagName = "operator-git-remote" + OperatorGitRemoteFlag = &cli.StringFlag{ + Name: OperatorGitRemoteFlagName, + Usage: "The remote for Tigera operator git repository", + Value: operator.DefaultRemote, + } + + OperatorBranchFlagName = "operator-branch" + OperatorBranchFlag = &cli.StringFlag{ + Name: OperatorBranchFlagName, + Usage: "The branch to use for Tigera operator release", + Value: operator.DefaultBranchName, + } +) + +var ( + OperatorReleaseBranchPrefixFlagName = "operator-release-branch-prefix" + OperatorReleaseBranchPrefixFlag = &cli.StringFlag{ + Name: OperatorReleaseBranchPrefixFlagName, + Usage: "The stardard prefix used to denote Tigera operator release branches", + Value: operator.DefaultReleaseBranchPrefix, + } + + OperatorDevTagSuffixFlagName = "operator-dev-tag-suffix" + OperatorDevTagSuffixFlag = &cli.StringFlag{ + Name: OperatorDevTagSuffixFlagName, + Usage: "The suffix used to denote development tags for Tigera operator", + Value: operator.DefaultDevTagSuffix, + } +) + +// Image flags +var ( + OperatorRegistryFlagName = "operator-registry" + OperatorRegistryFlag = &cli.StringFlag{ + Name: OperatorRegistryFlagName, + Usage: "The registry to use for Tigera operator release", + EnvVars: []string{"OPERATOR_REGISTRY"}, + Value: operator.DefaultRegistry, + } + + OperatorImageFlagName = "operator-image" + OperatorImageFlag = &cli.StringFlag{ + Name: OperatorImageFlagName, + Usage: "The image name to use for Tigera operator release", + EnvVars: []string{"OPERATOR_IMAGE"}, + Value: operator.DefaultImage, + } +) diff --git a/release/cmd/hashrelease/flags.go b/release/cmd/hashrelease/flags.go new file mode 100644 index 00000000000..3432a2413de --- /dev/null +++ b/release/cmd/hashrelease/flags.go @@ -0,0 +1,118 @@ +// Copyright (c) 2024 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hashrelease + +import "github.com/urfave/cli/v2" + +// Validation flags +var ( + skipBranchCheckFlagName = "skip-branch-check" + skipBranchCheckFlag = &cli.BoolFlag{ + Name: "skip-branch-check", + Usage: "Skip checking if current branch is a valid branch for release", + Value: false, + } + + skipImageScanFlagName = "skip-image-scan" + skipImageScanFlag = &cli.BoolFlag{ + Name: "skip-image-scan", + Usage: "Skip sending the image to the image scan service", + } +) + +// Image Scanner flags +var ( + imageScannerAPIFlagName = "image-scanner-api" + imageScannerAPIFlag = &cli.StringFlag{ + Name: "image-scanner-api", + Usage: "The URL for the Image Scan Service API", + EnvVars: []string{"IMAGE_SCANNER_API"}, + } + + imageScannerTokenFlagName = "image-scanning-token" + imageScannerTokenFlag = &cli.StringFlag{ + Name: "image-scanner-token", + Usage: "The token for the Image Scan Service API", + EnvVars: []string{"IMAGE_SCANNING_TOKEN"}, + } + + imageScannerSelectFlagName = "image-scanner-select" + imageScannerSelectFlag = &cli.StringFlag{ + Name: "image-scanner-select", + Usage: "The name of the scanner to use", + EnvVars: []string{"IMAGE_SCANNER_SELECT"}, + Value: "all", + } + + imageScannerFlags = []cli.Flag{skipImageScanFlag, imageScannerAPIFlag, imageScannerTokenFlag, imageScannerSelectFlag} +) + +// Publishing flags +var ( + sshHostFlagName = "host" + sshHostFlag = &cli.StringFlag{ + Name: "host", + Aliases: []string{"H"}, + Usage: "The SSH host for the connection to the hashrelease server", + EnvVars: []string{"DOCS_HOST"}, + } + + sshUserFlagName = "user" + sshUserFlag = &cli.StringFlag{ + Name: "user", + Aliases: []string{"U"}, + Usage: "The SSH user for the connection to the hashrelease server", + EnvVars: []string{"DOCS_USER"}, + } + + sshKeyFlagName = "key" + sshKeyFlag = &cli.StringFlag{ + Name: "key", + Aliases: []string{"K"}, + Usage: "The SSH key for the connection to the hashrelease server", + EnvVars: []string{"DOCS_KEY"}, + } + + sshPortFlagName = "port" + sshPortFlag = &cli.StringFlag{ + Name: "port", + Aliases: []string{"P"}, + Usage: "The SSH port for the connection to the hashrelease server", + EnvVars: []string{"DOCS_PORT"}, + } + + sshKnownHostsFlagName = "known-hosts" + sshKnownHostsFlag = &cli.StringFlag{ + Name: "known-hosts", + Aliases: []string{"KH"}, + Usage: "The known_hosts file to use for the connection to the hashrelease server", + EnvVars: []string{"DOCS_KNOWN_HOSTS"}, + } + + publishHashreleaseFlagName = "publish-to-hashrelease-server" + publishHashreleaseFlag = &cli.BoolFlag{ + Name: "publish-to-hashrelease-server", + Aliases: []string{"phr"}, + Usage: "Publish the hashrelease to the hashrelease server", + Value: true, + } + + latestFlagName = "latest" + latestFlag = &cli.BoolFlag{ + Name: "latest", + Usage: "Publish the hashrelease as the latest hashrelease", + Value: true, + } +) diff --git a/release/cmd/hashrelease/hashrelease.go b/release/cmd/hashrelease/hashrelease.go new file mode 100644 index 00000000000..10ad749da03 --- /dev/null +++ b/release/cmd/hashrelease/hashrelease.go @@ -0,0 +1,418 @@ +// Copyright (c) 2024 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hashrelease + +import ( + "fmt" + "path/filepath" + + "github.com/mitchellh/mapstructure" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + + "github.com/projectcalico/calico/release/cmd/flags" + "github.com/projectcalico/calico/release/cmd/release" + cmd "github.com/projectcalico/calico/release/cmd/utils" + "github.com/projectcalico/calico/release/internal/config" + "github.com/projectcalico/calico/release/internal/hashreleaseserver" + "github.com/projectcalico/calico/release/internal/imagescanner" + "github.com/projectcalico/calico/release/internal/logger" + "github.com/projectcalico/calico/release/internal/outputs" + "github.com/projectcalico/calico/release/internal/pinnedversion" + "github.com/projectcalico/calico/release/internal/slack" + "github.com/projectcalico/calico/release/internal/utils" + "github.com/projectcalico/calico/release/internal/version" + "github.com/projectcalico/calico/release/pkg/manager/calico" + "github.com/projectcalico/calico/release/pkg/manager/operator" + "github.com/projectcalico/calico/release/pkg/tasks" +) + +var hashreleaseDir = []string{"release", "_output", "hashrelease"} + +func Command(cfg *config.Config) *cli.Command { + hr := NewCalicoHashreleaseCommand(cfg) + return hr.Command() +} + +func NewCalicoHashreleaseCommand(cfg *config.Config) cmd.ReleaseCommand { + c := release.NewCalicoReleaseComand(cfg).(*release.CalicoRelease) + return &CalicoHashrelease{ + CalicoRelease: *c, + } +} + +type CalicoHashrelease struct { + release.CalicoRelease +} + +func (c *CalicoHashrelease) Command() *cli.Command { + return &cli.Command{ + Name: "hashrelease", + Aliases: []string{"hr"}, + Usage: "Build and publish hashreleases.", + Subcommands: c.Subcommands(), + Flags: []cli.Flag{ + sshHostFlag, sshUserFlag, sshKeyFlag, sshPortFlag, sshKnownHostsFlag, + }, + } +} + +func (c *CalicoHashrelease) baseOutputDir() string { + return filepath.Join(append([]string{c.RepoRootDir}, hashreleaseDir...)...) +} + +func (c *CalicoHashrelease) OutputDir(ver string) string { + return filepath.Join(c.baseOutputDir(), ver) +} + +func (c *CalicoHashrelease) Subcommands() []*cli.Command { + return []*cli.Command{ + c.BuildCmd(), + c.PublishCmd(), + c.GarbageCollectCmd(), + } +} + +func operatorDir(tmpDir string) string { + return filepath.Join(tmpDir, operator.DefaultRepoName) +} + +func (c *CalicoHashrelease) hashreleaseServerConfig(ctx *cli.Context) hashreleaseserver.Config { + return hashreleaseserver.Config{ + Host: ctx.String(sshHostFlagName), + User: ctx.String(sshUserFlagName), + Key: ctx.String(sshKeyFlagName), + Port: ctx.String(sshPortFlagName), + } +} + +func (c *CalicoHashrelease) BuildFlags() []cli.Flag { + f := flags.ProductFlags + f = append(f, flags.RegistryFlag, + flags.BuildImagesFlag(false, c.ProductName), + flags.ArchFlag) + f = append(f, flags.OperatorFlags...) + f = append(f, + flags.SkipValidationFlag, + skipBranchCheckFlag, + flags.GitHubTokenFlag, + ) + return f +} + +func (c *CalicoHashrelease) BuildCmd() *cli.Command { + return &cli.Command{ + Name: "build", + Usage: "Build a hashrelease locally", + Flags: c.BuildFlags(), + Action: func(ctx *cli.Context) error { + logger.Configure("hashrelease-build.log", ctx.Bool(flags.DebugFlagName)) + if err := c.ValidateBuildFlags(ctx); err != nil { + return err + } + if err := c.CloneRepos(ctx); err != nil { + return err + } + pinned := pinnedversion.New(map[string]any{ + "repoRootDir": c.RepoRootDir, + "releaseBranchPrefix": ctx.String(flags.ReleaseBranchPrefixFlagName), + "operator": pinnedversion.OperatorConfig{ + Branch: ctx.String(flags.OperatorBranchFlagName), + Image: ctx.String(flags.OperatorImageFlagName), + Registry: ctx.String(flags.OperatorRegistryFlagName), + Dir: filepath.Join(c.TmpDir, operator.DefaultRepoName), + }, + }, c.TmpDir) + _, data, err := pinned.Generate() + if err != nil { + return fmt.Errorf("failed to generate pinned version file: %s", err) + } + var versions version.Data + if err := mapstructure.Decode(data["versions"], &versions); err != nil { + return fmt.Errorf("failed to decode versions: %s", err) + } + hash := data["hash"].(string) + outputDir := c.OutputDir(versions.ProductVersion.FormattedString()) + + // Check if the hashrelease already exists + hrCfg := c.hashreleaseServerConfig(ctx) + if hrCfg.Valid() { + if published, err := tasks.HashreleasePublished(&hrCfg, hash, ctx.Bool(flags.CIFlagName)); err != nil { + return err + } else if published { + // On CI, we want it to fail if the hashrelease has already been published. + // However, on local builds, we just log a warning and continue. + if ctx.Bool(flags.CIFlagName) { + return fmt.Errorf("hashrelease %s has already been published", hash) + } else { + logrus.Warnf("hashrelease %s has already been published", hash) + } + } + } + + // Build the operator + if err := c.BuildOperator(ctx, versions.OperatorVersion.FormattedString()); err != nil { + return fmt.Errorf("failed to build operator: %s", err) + } + + // Build the product + opts, err := c.BuildOptions(ctx, data["versions"].(map[string]any)) + if err != nil { + return fmt.Errorf("failed to compose build options: %s", err) + } + manager := calico.NewManager(opts...) + if err := manager.Build(); err != nil { + return fmt.Errorf("failed to build calico: %s", err) + } + + // For real releases, release notes are generated prior to building the release. + // For hash releases, generate a set of release notes and add them to the hashrelease directory. + releaseVersion, err := version.DetermineReleaseVersion(versions.ProductVersion, ctx.String(flags.DevTagSuffixFlagName)) + if err != nil { + return fmt.Errorf("failed to determine release version: %v", err) + } + if _, err := outputs.ReleaseNotes(utils.CalicoOrg, ctx.String(flags.GitHubTokenFlagName), c.RepoRootDir, outputDir, releaseVersion); err != nil { + return err + } + + // Adjust the formatting of the generated outputs to match the legacy hashrelease format. + return tasks.ReformatHashrelease(outputDir, c.TmpDir) + }, + } +} + +func (c *CalicoHashrelease) BuildOperator(ctx *cli.Context, operatorVersion string) error { + opts := []operator.Option{ + operator.WithOperatorDirectory(operatorDir(c.TmpDir)), + operator.WithReleaseBranchPrefix(ctx.String(flags.OperatorReleaseBranchPrefixFlagName)), + operator.IsHashRelease(), + operator.WithArchitectures(ctx.StringSlice(flags.ArchFlagName)), + operator.WithValidate(!ctx.Bool(flags.SkipValidationFlagName)), + operator.WithReleaseBranchValidation(!ctx.Bool(skipBranchCheckFlagName)), + operator.WithVersion(operatorVersion), + operator.WithCalicoDirectory(c.RepoRootDir), + operator.WithTmpDirectory(c.TmpDir), + } + manager := operator.NewManager(opts...) + return manager.Build() +} + +func (c *CalicoHashrelease) BuildOptions(ctx *cli.Context, versions map[string]any) ([]calico.Option, error) { + var d version.Data + if err := mapstructure.Decode(versions, &d); err != nil { + return nil, err + } + opts, err := c.CalicoRelease.BuildOptions(ctx, versions) + if err != nil { + return nil, err + } + opts = append(opts, + calico.IsHashRelease(), + calico.WithOutputDir(c.OutputDir(d.ProductVersion.FormattedString())), + calico.WithReleaseBranchValidation(!ctx.Bool(skipBranchCheckFlagName)), + ) + return opts, nil +} + +func (c *CalicoHashrelease) ValidateBuildFlags(ctx *cli.Context) error { + // If skipping validation, ensure that branch check is disabled + if ctx.Bool(flags.SkipValidationFlagName) && !ctx.Bool(skipBranchCheckFlagName) { + return fmt.Errorf("%s must be set if %s is set", skipBranchCheckFlagName, flags.SkipValidationFlagName) + } + + // If using custom registry for product, ensure that Tigera operator registry is set + if len(ctx.StringSlice(flags.RegistryFlagName)) > 0 && ctx.String(flags.OperatorRegistryFlagName) == "" { + return fmt.Errorf("%s must be set if %s is set", flags.OperatorRegistryFlagName, flags.RegistryFlagName) + } + + // If using custom image name for Tigera operator, ensure that registry is set + // and vice versa + if ctx.String(flags.OperatorImageFlagName) != "" && ctx.String(flags.OperatorRegistryFlagName) == "" { + return fmt.Errorf("%s must be set if %s is set", flags.OperatorRegistryFlag, flags.OperatorImageFlag) + } else if ctx.String(flags.OperatorRegistryFlagName) != "" && ctx.String(flags.OperatorImageFlagName) == "" { + return fmt.Errorf("%s must be set if %s is set", flags.OperatorImageFlagName, flags.OperatorRegistryFlagName) + } + + // CI conditional checks + if ctx.Bool(flags.CIFlagName) { + hrCfg := c.hashreleaseServerConfig(ctx) + if !hrCfg.Valid() { + return fmt.Errorf("missing hashrelease server configuration") + } + } else { + // Add warning to run non-CI builds with registry flag when building images + if ctx.Bool(flags.BuildImagesFlagName) && len(ctx.StringSlice(flags.RegistryFlagName)) == 0 { + logrus.Warnf("Local builds should specify an image registry using the %s flag", flags.RegistryFlagName) + } + } + + return nil +} + +func (c *CalicoHashrelease) CloneRepos(ctx *cli.Context) error { + if err := utils.Clone( + fmt.Sprintf("git@github.com:%s/%s.git", ctx.String(flags.OperatorOrgFlagName), ctx.String(flags.OperatorRepoFlagName)), + ctx.String(flags.OperatorBranchFlagName), operatorDir(c.TmpDir)); err != nil { + return fmt.Errorf("failed to clone operator repository: %s", err) + } + return nil +} + +func (c *CalicoHashrelease) PublishCmd() *cli.Command { + return &cli.Command{ + Name: "publish", + Usage: "Publish a hashrelease to the hashrelease server", + Flags: c.PublishFlags(), + Action: func(ctx *cli.Context) error { + logger.Configure("hashrelease-publish.log", ctx.Bool(flags.DebugFlagName)) + + if err := c.ValidatePublishFlags(ctx); err != nil { + return err + } + + // Extract the pinned version data as a hashrelease object + hashrel, err := pinnedversion.New(map[string]any{}, c.TmpDir).LoadHashrelease(c.baseOutputDir()) + if err != nil { + return fmt.Errorf("failed to load hashrelease: %s", err) + } + if ctx.Bool(latestFlagName) { + hashrel.Latest = true + } + + // Check if hashrelease already exists in the server + hrCfg := c.hashreleaseServerConfig(ctx) + if ctx.Bool(publishHashreleaseFlagName) && hrCfg.Valid() { + if published, err := tasks.HashreleasePublished(&hrCfg, hashrel.Hash, ctx.Bool(flags.CIFlagName)); err != nil { + return err + } else if published { + return fmt.Errorf("%s hashrelease (%s) has already been published", hashrel.Name, hashrel.Hash) + } + } + + // Publish Operator + if err := c.PublishOperator(ctx); err != nil { + return fmt.Errorf("failed to publish operator: %s", err) + } + + // Publish the hashrelease + opts, err := c.PublishOptions(ctx, hashrel) + if err != nil { + return fmt.Errorf("failed to compose publish options: %s", err) + } + manager := calico.NewManager(opts...) + if err := manager.PublishRelease(); err != nil { + return fmt.Errorf("failed to publish hashrelease: %s", err) + } + + if ctx.Bool(publishHashreleaseFlagName) { + if err := tasks.AnnouceHashrelease(slack.Config{ + Token: ctx.String(flags.SlackTokenFlagName), + Channel: ctx.String(flags.SlackChannelFlagName), + }, *hashrel, "", c.TmpDir); err != nil { + return fmt.Errorf("failed to announce hashrelease: %s", err) + } + } + + return nil + }, + } +} + +func (c *CalicoHashrelease) PublishOptions(ctx *cli.Context, hashrel *hashreleaseserver.Hashrelease) ([]calico.Option, error) { + var d version.Data + if err := mapstructure.Decode(hashrel.Versions, &d); err != nil { + return nil, err + } + opts, err := c.CalicoRelease.PublishOptions(ctx, hashrel.Versions) + if err != nil { + return nil, err + } + opts = append(opts, + calico.IsHashRelease(), + calico.WithArchitectures(ctx.StringSlice(flags.ArchFlagName)), + calico.WithOutputDir(c.OutputDir(d.ProductVersion.FormattedString())), + calico.WithTmpDir(c.TmpDir), + calico.WithHashrelease(*hashrel, c.hashreleaseServerConfig(ctx)), + calico.WithPublishHashrelease(ctx.Bool(publishHashreleaseFlagName)), + calico.WithImageScanning(!ctx.Bool(skipImageScanFlagName), imagescanner.Config{ + APIURL: ctx.String(imageScannerAPIFlagName), + Token: ctx.String(imageScannerTokenFlagName), + Scanner: ctx.String(imageScannerSelectFlagName), + }), + ) + return opts, nil +} + +func (c *CalicoHashrelease) PublishOperator(ctx *cli.Context) error { + opts := []operator.Option{ + operator.WithOperatorDirectory(operatorDir(c.TmpDir)), + operator.WithCalicoDirectory(c.RepoRootDir), + operator.WithTmpDirectory(c.TmpDir), + operator.IsHashRelease(), + operator.WithArchitectures(ctx.StringSlice(flags.ArchFlagName)), + operator.WithValidate(!ctx.Bool(flags.SkipValidationFlagName)), + } + manager := operator.NewManager(opts...) + return manager.Publish() +} + +func (c *CalicoHashrelease) PublishFlags() []cli.Flag { + f := flags.GitFlags + f = append(f, + flags.RegistryFlag, + flags.PublishImagesFlag(false), + publishHashreleaseFlag, + latestFlag, + flags.SkipValidationFlag, + ) + return append(f, imageScannerFlags...) +} + +func (c *CalicoHashrelease) ValidatePublishFlags(ctx *cli.Context) error { + // Do not allow setting the hashrelease as latest if using custom registries + if ctx.Bool(latestFlagName) && len(ctx.StringSlice(flags.RegistryFlagName)) > 0 { + return fmt.Errorf("cannot set hashrelease as latest when using custom registries") + } + + // If skipping validation, ensure that image scanning is disabled + if ctx.Bool(flags.SkipValidationFlagName) && !ctx.Bool(skipImageScanFlagName) { + return fmt.Errorf("%s must be set if %s is set", skipImageScanFlagName, flags.SkipValidationFlagName) + } + + hrCfg := c.hashreleaseServerConfig(ctx) + if !hrCfg.Valid() && ctx.Bool(publishHashreleaseFlagName) { + return fmt.Errorf("missing hashrelease server configuration") + } + + return nil +} + +func (c *CalicoHashrelease) GarbageCollectCmd() *cli.Command { + return &cli.Command{ + Name: "garbage-collect", + Usage: "Clean up older hashreleases in the hashrelease server", + Aliases: []string{"gc"}, + Action: func(context *cli.Context) error { + logger.Configure("hashrelease-garbage-collect.log", context.Bool(flags.DebugFlagName)) + return hashreleaseserver.CleanOldHashreleases(&hashreleaseserver.Config{ + Host: context.String(sshHostFlagName), + User: context.String(sshUserFlagName), + Key: context.String(sshKeyFlagName), + Port: context.String(sshPortFlagName), + KnownHosts: context.String(sshKnownHostsFlagName), + }) + }, + } +} diff --git a/release/cmd/main.go b/release/cmd/main.go new file mode 100644 index 00000000000..2836dc892eb --- /dev/null +++ b/release/cmd/main.go @@ -0,0 +1,53 @@ +// Copyright (c) 2024 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "os" + + "github.com/sirupsen/logrus" + cli "github.com/urfave/cli/v2" + + "github.com/projectcalico/calico/release/cmd/branch" + "github.com/projectcalico/calico/release/cmd/flags" + "github.com/projectcalico/calico/release/cmd/hashrelease" + "github.com/projectcalico/calico/release/cmd/release" + "github.com/projectcalico/calico/release/internal/config" + "github.com/projectcalico/calico/release/internal/utils" +) + +func main() { + cfg := config.LoadConfig() + + app := &cli.App{ + Name: "release", + Usage: fmt.Sprintf("a tool for building %s releases", utils.DisplayProductName()), + Flags: flags.GlobalFlags(), + Commands: []*cli.Command{}, + } + + // Add sub-commands below.' + app.Commands = append(app.Commands, + hashrelease.Command(cfg), + release.Command(cfg), + branch.Command(cfg), + ) + + // Run the app. + if err := app.Run(os.Args); err != nil { + logrus.WithError(err).Fatal("Error running task") + } +} diff --git a/release/cmd/release/flags.go b/release/cmd/release/flags.go new file mode 100644 index 00000000000..9d5502368ff --- /dev/null +++ b/release/cmd/release/flags.go @@ -0,0 +1,34 @@ +// Copyright (c) 2024 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package release + +import "github.com/urfave/cli/v2" + +// publish flags +var ( + publishGitTagFlagName = "publish-git-tag" + publishGitTagFlag = &cli.BoolFlag{ + Name: publishGitTagFlagName, + Usage: "Push the git tag to the remote", + Value: true, + } + + publishGitHubReleaseFlagName = "publish-github-release" + publishGitHubReleaseFlag = &cli.BoolFlag{ + Name: publishGitHubReleaseFlagName, + Usage: "Publish the release to GitHub", + Value: true, + } +) diff --git a/release/cmd/release/release.go b/release/cmd/release/release.go new file mode 100644 index 00000000000..3c37872810b --- /dev/null +++ b/release/cmd/release/release.go @@ -0,0 +1,242 @@ +// Copyright (c) 2024 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package release + +import ( + "fmt" + "path/filepath" + + "github.com/mitchellh/mapstructure" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + + "github.com/projectcalico/calico/release/cmd/flags" + cmd "github.com/projectcalico/calico/release/cmd/utils" + "github.com/projectcalico/calico/release/internal/config" + "github.com/projectcalico/calico/release/internal/logger" + "github.com/projectcalico/calico/release/internal/outputs" + "github.com/projectcalico/calico/release/internal/utils" + "github.com/projectcalico/calico/release/internal/version" + "github.com/projectcalico/calico/release/pkg/manager/calico" +) + +var relativeUploadDir = []string{"release", "_output", "upload"} + +func Command(cfg *config.Config) *cli.Command { + rel := NewCalicoReleaseComand(cfg) + return rel.Command() +} + +func NewCalicoReleaseComand(cfg *config.Config) cmd.ReleaseCommand { + return &CalicoRelease{ + ProductName: utils.Calico, + RepoRootDir: cfg.RepoRootDir, + TmpDir: cfg.TmpFolderPath(), + } +} + +type CalicoRelease struct { + ProductName string + RepoRootDir string + TmpDir string +} + +func (c *CalicoRelease) Command() *cli.Command { + return &cli.Command{ + Name: "release", + Aliases: []string{"rel"}, + Usage: "Build and publish public releases", + Subcommands: c.Subcommands(), + } +} + +func (c *CalicoRelease) OutputDir(ver string) string { + baseOutputDir := filepath.Join(append([]string{c.RepoRootDir}, relativeUploadDir...)...) + return filepath.Join(baseOutputDir, ver) +} + +func (c *CalicoRelease) Subcommands() []*cli.Command { + return []*cli.Command{ + c.ReleaseNotesCmd(), + c.BuildCmd(), + c.PublishCmd(), + } +} + +func (c *CalicoRelease) ReleaseNotesCmd() *cli.Command { + return &cli.Command{ + Name: "release-notes", + Aliases: []string{"rn"}, + Usage: "Generate release notes", + Flags: []cli.Flag{ + flags.OrgFlag, + flags.DevTagSuffixFlag, + flags.GitHubTokenFlag, + }, + Action: func(ctx *cli.Context) error { + logger.Configure("release-notes.log", ctx.Bool(flags.DebugFlagName)) + + ver, err := version.DetermineReleaseVersion(version.GitVersion(), ctx.String(flags.DevTagSuffixFlagName)) + if err != nil { + return fmt.Errorf("failed to determine release version: %w", err) + } + + f, err := outputs.ReleaseNotes(ctx.String(flags.OrgFlagName), ctx.String(flags.GitHubTokenFlagName), c.RepoRootDir, c.RepoRootDir, ver) + if err != nil { + return fmt.Errorf("failed to generate release notes: %w", err) + } + logrus.WithField("release-notes", f).Info("Release notes generated") + logrus.Info("Please review for accuracy, and format appropriately before releasing.") + + return nil + }, + } +} + +func (c *CalicoRelease) BuildFlags() []cli.Flag { + f := flags.ProductFlags + f = append(f, + flags.BuildImagesFlag(true, c.ProductName), + flags.ArchFlag, + flags.RegistryFlag, + flags.SkipValidationFlag, + ) + return f +} + +func (c *CalicoRelease) BuildCmd() *cli.Command { + return &cli.Command{ + Name: "build", + Usage: "Build an official release", + Flags: c.BuildFlags(), + Action: func(ctx *cli.Context) error { + logger.Configure("release-build.log", ctx.Bool(flags.DebugFlagName)) + + // Determine the versions to use for the release. + ver, err := version.DetermineReleaseVersion(version.GitVersion(), ctx.String(flags.DevTagSuffixFlagName)) + if err != nil { + return fmt.Errorf("failed to determine release version: %w", err) + } + operatorVer, err := version.DetermineOperatorVersion(c.RepoRootDir) + if err != nil { + return fmt.Errorf("failed to determine operator version: %w", err) + } + + var versions map[string]any + if err := mapstructure.Decode(version.Data{ + ProductVersion: ver, + OperatorVersion: operatorVer, + }, &versions); err != nil { + return fmt.Errorf("failed to decode versions: %w", err) + } + + opts, err := c.BuildOptions(ctx, versions) + if err != nil { + return fmt.Errorf("failed to compose build options: %w", err) + } + manager := calico.NewManager(opts...) + return manager.Build() + }, + } +} + +func (c *CalicoRelease) BuildOptions(ctx *cli.Context, versions map[string]any) ([]calico.Option, error) { + var d version.Data + if err := mapstructure.Decode(versions, &d); err != nil { + return nil, err + } + opts := []calico.Option{ + calico.WithRepoRoot(c.RepoRootDir), + calico.WithReleaseBranchPrefix(ctx.String(flags.ReleaseBranchPrefixFlagName)), + calico.WithVersions(d), + calico.WithGithubOrg(ctx.String(flags.OrgFlagName)), + calico.WithRepoName(ctx.String(flags.RepoFlagName)), + calico.WithRepoRemote(ctx.String(flags.RepoRemoteFlagName)), + calico.WithBuildImages(ctx.Bool(flags.BuildImagesFlagName)), + calico.WithArchitectures(ctx.StringSlice(flags.ArchFlagName)), + calico.WithOutputDir(c.OutputDir(d.ProductVersion.FormattedString())), + calico.WithValidate(!ctx.Bool(flags.SkipValidationFlagName)), + } + if reg := ctx.StringSlice(flags.RegistryFlagName); len(reg) > 0 { + opts = append(opts, calico.WithImageRegistries(reg)) + } + return opts, nil +} + +func (c *CalicoRelease) PublishFlags() []cli.Flag { + f := flags.ProductFlags + f = append(f, + flags.RegistryFlag, + flags.PublishImagesFlag(true), + publishGitTagFlag, + publishGitHubReleaseFlag, + flags.SkipValidationFlag, + ) + return f +} + +func (c *CalicoRelease) PublishCmd() *cli.Command { + return &cli.Command{ + Name: "publish", + Usage: "Publish an official release", + Flags: c.PublishFlags(), + Action: func(ctx *cli.Context) error { + logger.Configure("release-publish.log", ctx.Bool(flags.DebugFlagName)) + + // Determine the versions to use for the release. + ver, operatorVer, err := version.VersionsFromManifests(c.RepoRootDir) + if err != nil { + return fmt.Errorf("failed to determine release versions: %w", err) + } + var versions map[string]any + if err := mapstructure.Decode(version.Data{ + ProductVersion: ver, + OperatorVersion: operatorVer, + }, &versions); err != nil { + return fmt.Errorf("failed to decode versions: %w", err) + } + + opts, err := c.PublishOptions(ctx, versions) + if err != nil { + return fmt.Errorf("failed to compose publish options: %w", err) + } + manager := calico.NewManager(opts...) + return manager.PublishRelease() + }, + } +} + +func (c *CalicoRelease) PublishOptions(ctx *cli.Context, versions map[string]any) ([]calico.Option, error) { + var d version.Data + if err := mapstructure.Decode(versions, &d); err != nil { + return nil, err + } + opts := []calico.Option{ + calico.WithRepoRoot(c.RepoRootDir), + calico.WithVersions(d), + calico.WithGithubOrg(ctx.String(flags.OrgFlagName)), + calico.WithRepoName(ctx.String(flags.RepoFlagName)), + calico.WithRepoRemote(ctx.String(flags.RepoRemoteFlagName)), + calico.WithOutputDir(c.OutputDir(d.ProductVersion.FormattedString())), + calico.WithPublishImages(ctx.Bool(flags.PublishImagesFlagName)), + calico.WithPublishGitTag(ctx.Bool(publishGitTagFlagName)), + calico.WithPublishGithubRelease(ctx.Bool(publishGitHubReleaseFlagName)), + calico.WithValidate(!ctx.Bool(flags.SkipValidationFlagName)), + } + if reg := ctx.StringSlice(flags.RegistryFlagName); len(reg) > 0 { + opts = append(opts, calico.WithImageRegistries(reg)) + } + return opts, nil +} diff --git a/release/internal/config/ci.go b/release/cmd/utils/command.go similarity index 63% rename from release/internal/config/ci.go rename to release/cmd/utils/command.go index c6e02091323..0cfc5cb788e 100644 --- a/release/internal/config/ci.go +++ b/release/cmd/utils/command.go @@ -12,21 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package utils -import ( - "fmt" -) +import "github.com/urfave/cli/v2" -type CIConfig struct { - IsCI bool `envconfig:"CI" default:"false"` - OrgURL string `envconfig:"SEMAPHORE_ORGANIZATION_URL" default:""` - JobID string `envconfig:"SEMAPHORE_JOB_ID" default:""` +type Command interface { + Command() *cli.Command + Subcommands() []*cli.Command } -func (c *CIConfig) URL() string { - if c.IsCI && c.OrgURL != "" { - return fmt.Sprintf("%s/jobs/%s", c.OrgURL, c.JobID) - } - return "" +type ReleaseCommand interface { + Command + BuildCmd() *cli.Command + PublishCmd() *cli.Command } diff --git a/release/internal/config/config.go b/release/internal/config/config.go index 30d23d57525..ead46b89101 100644 --- a/release/internal/config/config.go +++ b/release/internal/config/config.go @@ -17,56 +17,15 @@ package config import ( "path/filepath" - "github.com/kelseyhightower/envconfig" "github.com/sirupsen/logrus" "github.com/projectcalico/calico/release/internal/command" - "github.com/projectcalico/calico/release/internal/hashreleaseserver" - "github.com/projectcalico/calico/release/internal/imagescanner" - "github.com/projectcalico/calico/release/internal/registry" - "github.com/projectcalico/calico/release/internal/slack" "github.com/projectcalico/calico/release/internal/utils" ) -const ( - DefaultOrg = "projectcalico" - DefaultRepo = "calico" -) - type Config struct { - // RepoRootDir is the root directory for this repository - RepoRootDir string `envconfig:"REPO_ROOT"` - - // DevTagSuffix is the suffix for the development tag - DevTagSuffix string `envconfig:"DEV_TAG_SUFFIX" default:"0.dev"` - - // RepoReleaseBranchPrefix is the suffix for the release tag - RepoReleaseBranchPrefix string `envconfig:"RELEASE_BRANCH_PREFIX" default:"release"` - - // GitRemote is the remote for the git repository - GitRemote string `envconfig:"GIT_REMOTE" default:"origin"` - - // Operator is the configuration for Tigera operator - Operator OperatorConfig - - // Arches are the OS architectures supported for multi-arch build - Arches []string `envconfig:"ARCHES" default:"amd64,arm64,ppc64le,s390x"` - - HashreleaseServerConfig hashreleaseserver.Config - - // GithubToken is the token for the GitHub API - GithubToken string `envconfig:"GITHUB_TOKEN"` - - // OutputDir is the directory for the output - OutputDir string `envconfig:"OUTPUT_DIR"` - - // SlackConfig is the configuration for Slack integration - SlackConfig slack.Config - - // ImageScannerConfig is the configuration for Image Scanning Service integration - ImageScannerConfig imagescanner.Config - - CI CIConfig + RepoRootDir string + OutputDir string } // TmpFolderPath returns the temporary folder path. @@ -84,20 +43,14 @@ func repoRootDir() string { return dir } -// LoadConfig loads the configuration from the environment +// LoadConfig loads the basic configuration for the release tool func LoadConfig() *Config { config := &Config{} - envconfig.MustProcess("", config) if config.RepoRootDir == "" { config.RepoRootDir = repoRootDir() } if config.OutputDir == "" { config.OutputDir = filepath.Join(config.RepoRootDir, utils.ReleaseFolderName, "_output") } - if config.Operator.Dir == "" { - config.Operator.Dir = filepath.Join(config.TmpFolderPath(), OperatorDefaultRepo) - } - config.Operator.Registry = registry.QuayRegistry - config.Operator.Image = OperatorDefaultImage return config } diff --git a/release/internal/config/operator.go b/release/internal/config/operator.go deleted file mode 100644 index 7503ded72c9..00000000000 --- a/release/internal/config/operator.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2024 Tigera, Inc. All rights reserved. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package config - -import ( - "github.com/sirupsen/logrus" - - "github.com/projectcalico/calico/release/internal/command" - "github.com/projectcalico/calico/release/internal/version" -) - -const ( - OperatorDefaultImage = "tigera/operator" - OperatorDefaultOrg = "tigera" - OperatorDefaultRepo = "operator" -) - -type OperatorConfig struct { - // GitRemote is the remote for the git repository - GitRemote string `envconfig:"OPERATOR_GIT_REMOTE" default:"origin"` - - // Branch is the repository for the operator - Branch string `envconfig:"OPERATOR_BRANCH" default:"master"` - - // RepoReleaseBranchPrefix is the prefix for the release branch - RepoReleaseBranchPrefix string `envconfig:"OPERATOR_RELEASE_BRANCH_PREFIX" default:"release"` - - // DevTagSuffix is the suffix for the development tag - DevTagSuffix string `envconfig:"OPERATOR_DEV_TAG_SUFFIX" default:"0.dev"` - - // Dir is the directory to clone the operator repository. - Dir string - - // Image is the image for Tigera operator - Image string - - // Registry is the registry for Tigera operator - Registry string -} - -func (c OperatorConfig) GitVersion() version.Version { - previousTag, err := command.GitVersion(c.Dir, true) - if err != nil { - logrus.WithError(err).Fatal("Failed to determine latest git version") - } - logrus.WithField("out", previousTag).Info("Current git describe") - return version.New(previousTag) -} - -func (c OperatorConfig) GitBranch() (string, error) { - return command.GitInDir(c.Dir, "rev-parse", "--abbrev-ref", "HEAD") -} diff --git a/release/internal/hashreleaseserver/hashrelease.go b/release/internal/hashreleaseserver/hashrelease.go new file mode 100644 index 00000000000..f194cf57776 --- /dev/null +++ b/release/internal/hashreleaseserver/hashrelease.go @@ -0,0 +1,65 @@ +// Copyright (c) 2024 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hashreleaseserver + +import ( + "fmt" + "time" + + "github.com/projectcalico/calico/release/internal/registry" + "github.com/projectcalico/calico/release/internal/version" +) + +type Hashrelease struct { + // Name is the name of the hashrelease. + // When publishing a hashrelease, this is the name of the folder in the server. + // When getting a hashrelease, this is the full path of the hashrelease folder. + Name string + + // Hash is the hash of the hashrelease + Hash string + + // Note is the info about the hashrelease + Note string + + // Branch is the branch the hashrelease is built from + Branch string + + Versions map[string]any + + // Source is the source of hashrelease content + Source string + + // Time is the modified time of the hashrelease + Time time.Time + + // Latest is if the hashrelease is the latest for the stream + Latest bool + + // Components is the components of the hashrelease + Components map[string]registry.Component +} + +func (h Hashrelease) Stream() (string, error) { + ver, err := version.ProductVersion(h.Versions) + if err != nil { + return "", err + } + return version.DeterminePublishStream(h.Branch, ver.FormattedString()), nil +} + +func (h Hashrelease) URL() string { + return fmt.Sprintf("https://%s.%s", h.Name, BaseDomain) +} diff --git a/release/internal/hashreleaseserver/server.go b/release/internal/hashreleaseserver/server.go index ece725d5dd4..76596b9970b 100644 --- a/release/internal/hashreleaseserver/server.go +++ b/release/internal/hashreleaseserver/server.go @@ -35,41 +35,6 @@ const ( BaseDomain = "docs.eng.tigera.net" ) -type Hashrelease struct { - // Name is the name of the hashrelease. - // When publishing a hashrelease, this is the name of the folder in the server. - // When getting a hashrelease, this is the full path of the hashrelease folder. - Name string - - // Hash is the hash of the hashrelease - Hash string - - // Note is the info about the hashrelease - Note string - - // Stream is the version the hashrelease is for (e.g master, v3.19) - Stream string - - // ProductVersion is the product version in the hashrelease - ProductVersion string - - // OperatorVersion is the operator version for the hashreleaseq - OperatorVersion string - - // Source is the source of hashrelease content - Source string - - // Time is the modified time of the hashrelease - Time time.Time - - // Latest is if the hashrelease is the latest for the stream - Latest bool -} - -func (h *Hashrelease) URL() string { - return fmt.Sprintf("https://%s.%s", h.Name, BaseDomain) -} - func RemoteDocsPath(user string) string { path := "files" if user != "root" { @@ -92,8 +57,12 @@ func HasHashrelease(hash string, cfg *Config) bool { // SetHashreleaseAsLatest sets the hashrelease as the latest for the stream func SetHashreleaseAsLatest(rel Hashrelease, cfg *Config) error { - logrus.Debugf("Updating latest hashrelease for %s stream to %s", rel.Stream, rel.Name) - if _, err := runSSHCommand(cfg, fmt.Sprintf(`echo "%s/" > %s/latest-os/%s.txt && echo %s >> %s`, rel.URL(), RemoteDocsPath(cfg.User), rel.Stream, rel.Name, remoteReleasesLibraryPath(cfg.User))); err != nil { + stream, err := rel.Stream() + if err != nil { + return err + } + logrus.Debugf("Updating latest hashrelease for %s stream to %s", stream, rel.Name) + if _, err := runSSHCommand(cfg, fmt.Sprintf(`echo "%s/" > %s/latest-os/%s.txt && echo %s >> %s`, rel.URL(), RemoteDocsPath(cfg.User), stream, rel.Name, remoteReleasesLibraryPath(cfg.User))); err != nil { logrus.WithError(err).Error("Failed to update latest hashrelease and hashrelease library") return err } diff --git a/release/internal/imagescanner/scanner.go b/release/internal/imagescanner/scanner.go index 087028341d6..05914e38f95 100644 --- a/release/internal/imagescanner/scanner.go +++ b/release/internal/imagescanner/scanner.go @@ -69,7 +69,7 @@ func (i *Scanner) Scan(images []string, stream string, release bool, outputDir s scanType = "image" bucketPath = fmt.Sprintf("hashrelease/%s", stream) } - payload := map[string]interface{}{ + payload := map[string]any{ "images": images, "bucket_path": bucketPath, } @@ -88,7 +88,7 @@ func (i *Scanner) Scan(images []string, stream string, release bool, outputDir s query := req.URL.Query() query.Add("scan_type", scanType) query.Add("scanner_select", i.config.Scanner) - query.Add("project_name", utils.ProductCode) + query.Add("project_name", utils.CalicoCode) query.Add("project_version", stream) req.URL.RawQuery = query.Encode() logrus.WithFields(logrus.Fields{ @@ -161,7 +161,7 @@ func RetrieveResultURL(outputDir string) string { logrus.WithError(err).Error("Image scan result file does not exist") return "" } - var result map[string]interface{} + var result map[string]any resultData, err := os.ReadFile(outputFilePath) if err != nil { logrus.WithError(err).Error("Failed to read image scan result file") diff --git a/release/internal/logger/logger.go b/release/internal/logger/logger.go new file mode 100644 index 00000000000..516f12dd63d --- /dev/null +++ b/release/internal/logger/logger.go @@ -0,0 +1,40 @@ +// Copyright (c) 2024 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logger + +import ( + "io" + "os" + + "github.com/sirupsen/logrus" + "gopkg.in/natefinch/lumberjack.v2" +) + +func Configure(filename string, debug bool) { + if debug { + logrus.SetLevel(logrus.DebugLevel) + } else { + logrus.SetLevel(logrus.InfoLevel) + } + + // Set up logging to both stdout as well as a file. + writers := []io.Writer{os.Stdout, &lumberjack.Logger{ + Filename: filename, + MaxSize: 100, + MaxAge: 30, + MaxBackups: 10, + }} + logrus.SetOutput(io.MultiWriter(writers...)) +} diff --git a/release/internal/outputs/releasenotes.go b/release/internal/outputs/releasenotes.go index 69e6cab26b5..5ab783aa2f1 100644 --- a/release/internal/outputs/releasenotes.go +++ b/release/internal/outputs/releasenotes.go @@ -42,7 +42,7 @@ const ( var ( //go:embed templates/release-note.md.gotmpl releaseNoteTemplate string - repos = []string{"calico", "bird"} + repos = []string{utils.CalicoRepo, "bird"} ) type issueState string @@ -196,6 +196,11 @@ func ReleaseNotes(owner, githubToken, repoRootDir, outputDir string, ver version logrus.Warn("No directory is set, using current directory") outputDir = "." } + outputDir = filepath.Join(outputDir, "release-notes") + if err := os.MkdirAll(outputDir, utils.DirPerms); err != nil { + logrus.WithError(err).Errorf("Failed to create release notes folder %s", outputDir) + return "", err + } logrus.Infof("Generating release notes for %s", ver.FormattedString()) milestone := ver.Milestone() githubClient := github.NewTokenClient(context.Background(), githubToken) diff --git a/release/internal/pinnedversion/pinnedversion.go b/release/internal/pinnedversion/pinnedversion.go index ae62fa42a83..c6a2347a8ea 100644 --- a/release/internal/pinnedversion/pinnedversion.go +++ b/release/internal/pinnedversion/pinnedversion.go @@ -23,10 +23,11 @@ import ( "strings" "time" + "github.com/mitchellh/mapstructure" "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" - "github.com/projectcalico/calico/release/internal/config" + "github.com/projectcalico/calico/release/internal/command" "github.com/projectcalico/calico/release/internal/hashreleaseserver" "github.com/projectcalico/calico/release/internal/registry" "github.com/projectcalico/calico/release/internal/utils" @@ -41,16 +42,25 @@ const ( operatorComponentsFileName = "components.yaml" ) -// Config represents the configuration needed to generate the pinned version file. -type Config struct { - // RootDir is the root directory of the repository. - RootDir string +type OperatorConfig struct { + Dir string + Branch string + Image string + Registry string +} - // ReleaseBranchPrefix is the prefix for the release branch. - ReleaseBranchPrefix string +func (c *OperatorConfig) GitVersion() (version.Version, error) { + previousTag, err := command.GitVersion(c.Dir, true) + if err != nil { + logrus.WithError(err).Error("failed to determine operator git version") + return version.Version(""), err + } + logrus.WithField("out", previousTag).Info("Current git describe") + return version.New(previousTag), nil +} - // Operator is the configuration for the operator. - Operator config.OperatorConfig +func (c *OperatorConfig) GitBranch() (string, error) { + return command.GitInDir(c.Dir, "rev-parse", "--abbrev-ref", "HEAD") } // PinnedVersionData represents the data needed to generate the pinned version file from the template. @@ -77,6 +87,10 @@ type PinnedVersionData struct { ReleaseBranch string } +func (d *PinnedVersionData) ReleaseURL() string { + return fmt.Sprintf("https://%s.%s", d.ReleaseName, d.BaseDomain) +} + // PinnedVersion represents an entry in pinned version file. type PinnedVersion struct { Title string `yaml:"title"` @@ -88,6 +102,20 @@ type PinnedVersion struct { Components map[string]registry.Component `yaml:"components"` } +func (p *PinnedVersion) ProductVersion() string { + return p.Components[utils.Calico].Version +} + +func (p *PinnedVersion) HelmChartVersion() string { + return p.ProductVersion() +} + +func (p *PinnedVersion) Operator() registry.OperatorComponent { + return registry.OperatorComponent{ + Component: p.TigeraOperator, + } +} + // PinnedVersionFile represents the pinned version file. type PinnedVersionFile []PinnedVersion @@ -99,11 +127,53 @@ func operatorComponentsFilePath(outputDir string) string { return filepath.Join(outputDir, operatorComponentsFileName) } -// GeneratePinnedVersionFile generates the pinned version file. -func GeneratePinnedVersionFile(cfg Config, outputDir string) (string, *PinnedVersionData, error) { - pinnedVersionPath := pinnedVersionFilePath(outputDir) +type PinnedVersions interface { + Get() (PinnedVersion, error) + Generate() (string, map[string]any, error) + OperatorComponent() (registry.OperatorComponent, string, error) + ComponentsToValidate() (map[string]registry.Component, error) + LoadHashrelease(hashreleaseBaseDir string) (*hashreleaseserver.Hashrelease, error) +} + +func New(cfg map[string]any, outputDir string) PinnedVersions { + return &CalicoPinnedVersions{ + Cfg: cfg, + OutputDir: outputDir, + } +} + +type CalicoPinnedVersions struct { + Cfg map[string]any + OutputDir string +} - productBranch, err := utils.GitBranch(cfg.RootDir) +func (p *CalicoPinnedVersions) RepoRootDir() string { + return p.Cfg["repoRootDir"].(string) +} + +func (p *CalicoPinnedVersions) ReleaseBranchPrefix() string { + return p.Cfg["releaseBranchPrefix"].(string) +} + +func (p *CalicoPinnedVersions) OperatorConfig() OperatorConfig { + return p.Cfg["operator"].(OperatorConfig) +} + +func (p *CalicoPinnedVersions) Get() (PinnedVersion, error) { + pinnedVersionPath := pinnedVersionFilePath(p.OutputDir) + var pinnedVersionFile PinnedVersionFile + if pinnedVersionData, err := os.ReadFile(pinnedVersionPath); err != nil { + return PinnedVersion{}, err + } else if err := yaml.Unmarshal([]byte(pinnedVersionData), &pinnedVersionFile); err != nil { + return PinnedVersion{}, err + } + return pinnedVersionFile[0], nil +} + +func (p *CalicoPinnedVersions) Generate() (string, map[string]any, error) { + pinnedVersionPath := pinnedVersionFilePath(p.OutputDir) + + productBranch, err := utils.GitBranch(p.RepoRootDir()) if err != nil { return "", nil, err } @@ -111,11 +181,16 @@ func GeneratePinnedVersionFile(cfg Config, outputDir string) (string, *PinnedVer productVersion := version.GitVersion() releaseName := fmt.Sprintf("%s-%s-%s", time.Now().Format("2006-01-02"), version.DeterminePublishStream(productBranch, string(productVersion)), RandomWord()) releaseName = strings.ReplaceAll(releaseName, ".", "-") - operatorBranch, err := cfg.Operator.GitBranch() + operatorCfg := p.OperatorConfig() + operatorBranch, err := operatorCfg.GitBranch() if err != nil { return "", nil, err } - operatorVersion := cfg.Operator.GitVersion() + operatorVersion, err := operatorCfg.GitVersion() + if err != nil { + return "", nil, err + } + pinnedOperatorVersion := operatorVersion.FormattedString() + "-" + releaseName tmpl, err := template.New("pinnedversion").Parse(calicoVersionTemplateData) if err != nil { return "", nil, err @@ -125,32 +200,46 @@ func GeneratePinnedVersionFile(cfg Config, outputDir string) (string, *PinnedVer BaseDomain: hashreleaseserver.BaseDomain, ProductVersion: productVersion.FormattedString(), Operator: registry.Component{ - Version: operatorVersion.FormattedString() + "-" + releaseName, - Image: cfg.Operator.Image, - Registry: cfg.Operator.Registry, + Version: pinnedOperatorVersion, + Image: operatorCfg.Image, + Registry: operatorCfg.Registry, }, Hash: productVersion.FormattedString() + "-" + operatorVersion.FormattedString(), Note: fmt.Sprintf("%s - generated at %s using %s release branch with %s operator branch", releaseName, time.Now().Format(time.RFC1123), productBranch, operatorBranch), - ReleaseBranch: productVersion.ReleaseBranch(cfg.ReleaseBranchPrefix), + ReleaseBranch: productVersion.ReleaseBranch(p.ReleaseBranchPrefix()), } logrus.WithField("file", pinnedVersionPath).Info("Generating pinned-version.yaml") pinnedVersionFile, err := os.Create(pinnedVersionPath) if err != nil { + logrus.WithError(err).Error("Failed to create pinned-version.yaml file") return "", nil, err } defer pinnedVersionFile.Close() if err := tmpl.Execute(pinnedVersionFile, data); err != nil { + logrus.WithError(err).Error("Failed to generate pinned-version.yaml from template") + return "", nil, err + } + + var versions map[string]any + if err := mapstructure.Decode(version.Data{ + ProductVersion: productVersion, + OperatorVersion: version.New(pinnedOperatorVersion), + }, &versions); err != nil { return "", nil, err } - return pinnedVersionPath, data, nil + releaseMetadata := map[string]any{ + "versions": versions, + "hash": data.Hash, + } + + return pinnedVersionPath, releaseMetadata, nil } -// GenerateOperatorComponents generates the components-version.yaml for operator. -func GenerateOperatorComponents(outputDir string) (registry.OperatorComponent, string, error) { +func (p *CalicoPinnedVersions) OperatorComponent() (registry.OperatorComponent, string, error) { op := registry.OperatorComponent{} - pinnedVersionPath := pinnedVersionFilePath(outputDir) + pinnedVersionPath := pinnedVersionFilePath(p.OutputDir) logrus.WithField("file", pinnedVersionPath).Info("Generating components-version.yaml for operator") var pinnedversion PinnedVersionFile if pinnedVersionData, err := os.ReadFile(pinnedVersionPath); err != nil { @@ -158,7 +247,7 @@ func GenerateOperatorComponents(outputDir string) (registry.OperatorComponent, s } else if err := yaml.Unmarshal([]byte(pinnedVersionData), &pinnedversion); err != nil { return op, "", err } - operatorComponentsFilePath := operatorComponentsFilePath(outputDir) + operatorComponentsFilePath := operatorComponentsFilePath(p.OutputDir) operatorComponentsFile, err := os.Create(operatorComponentsFilePath) if err != nil { return op, "", err @@ -171,35 +260,31 @@ func GenerateOperatorComponents(outputDir string) (registry.OperatorComponent, s return op, operatorComponentsFilePath, nil } -// RetrievePinnedVersion retrieves the pinned version from the pinned version file. -func RetrievePinnedVersion(outputDir string) (PinnedVersion, error) { - pinnedVersionPath := pinnedVersionFilePath(outputDir) - var pinnedVersionFile PinnedVersionFile - if pinnedVersionData, err := os.ReadFile(pinnedVersionPath); err != nil { - return PinnedVersion{}, err - } else if err := yaml.Unmarshal([]byte(pinnedVersionData), &pinnedVersionFile); err != nil { - return PinnedVersion{}, err +func (p *CalicoPinnedVersions) LoadHashrelease(hashreleaseBaseDir string) (*hashreleaseserver.Hashrelease, error) { + pinnedVersion, err := p.Get() + if err != nil { + logrus.WithError(err).Fatal("Failed to get pinned version") + return nil, err } - return pinnedVersionFile[0], nil -} - -// RetrievePinnedOperatorVersion retrieves the operator version from the pinned version file. -func RetrievePinnedOperator(outputDir string) (registry.OperatorComponent, error) { - pinnedVersionPath := pinnedVersionFilePath(outputDir) - var pinnedVersionFile PinnedVersionFile - if pinnedVersionData, err := os.ReadFile(pinnedVersionPath); err != nil { - return registry.OperatorComponent{}, err - } else if err := yaml.Unmarshal([]byte(pinnedVersionData), &pinnedVersionFile); err != nil { - return registry.OperatorComponent{}, err + var versions map[string]any + if err := mapstructure.Decode(version.Data{ + ProductVersion: version.New(pinnedVersion.ProductVersion()), + OperatorVersion: version.New(pinnedVersion.Operator().Version), + }, versions); err != nil { + return nil, err } - return registry.OperatorComponent{ - Component: pinnedVersionFile[0].TigeraOperator, + return &hashreleaseserver.Hashrelease{ + Name: pinnedVersion.ReleaseName, + Hash: pinnedVersion.Hash, + Note: pinnedVersion.Note, + Versions: versions, + Source: filepath.Join(hashreleaseBaseDir, pinnedVersion.Title), + Time: time.Now(), }, nil } -// RetrieveComponentsToValidate retrieves the components to validate from the pinned version file. -func RetrieveComponentsToValidate(outputDir string) (map[string]registry.Component, error) { - pinnedVersionPath := pinnedVersionFilePath(outputDir) +func (p *CalicoPinnedVersions) ComponentsToValidate() (map[string]registry.Component, error) { + pinnedVersionPath := pinnedVersionFilePath(p.OutputDir) var pinnedversion PinnedVersionFile if pinnedVersionData, err := os.ReadFile(pinnedVersionPath); err != nil { return nil, err @@ -228,24 +313,14 @@ func RetrieveComponentsToValidate(outputDir string) (map[string]registry.Compone return components, nil } -func LoadHashrelease(repoRootDir, tmpDir, srcDir string) (*hashreleaseserver.Hashrelease, error) { - productBranch, err := utils.GitBranch(repoRootDir) - if err != nil { - logrus.WithError(err).Errorf("Failed to get %s branch name", utils.ProductName) - return nil, err - } - pinnedVersion, err := RetrievePinnedVersion(tmpDir) - if err != nil { - logrus.WithError(err).Fatal("Failed to get pinned version") +// RetrievePinnedVersion retrieves the pinned version from the pinned version file. +func RetrievePinnedVersion(outputDir string) (PinnedVersion, error) { + pinnedVersionPath := pinnedVersionFilePath(outputDir) + var pinnedVersionFile PinnedVersionFile + if pinnedVersionData, err := os.ReadFile(pinnedVersionPath); err != nil { + return PinnedVersion{}, err + } else if err := yaml.Unmarshal([]byte(pinnedVersionData), &pinnedVersionFile); err != nil { + return PinnedVersion{}, err } - return &hashreleaseserver.Hashrelease{ - Name: pinnedVersion.ReleaseName, - Hash: pinnedVersion.Hash, - Note: pinnedVersion.Note, - Stream: version.DeterminePublishStream(productBranch, pinnedVersion.Title), - ProductVersion: pinnedVersion.Title, - OperatorVersion: pinnedVersion.TigeraOperator.Version, - Source: srcDir, - Time: time.Now(), - }, nil + return pinnedVersionFile[0], nil } diff --git a/release/internal/registry/auth.go b/release/internal/registry/auth.go index 06304597d8a..3e000f5abdf 100644 --- a/release/internal/registry/auth.go +++ b/release/internal/registry/auth.go @@ -112,7 +112,7 @@ func getBearerTokenWithAuth(auth string, registry Registry, scope string) (strin if res.StatusCode != http.StatusOK { return "", fmt.Errorf("failed to get bearer token: %s", res.Status) } - resp := map[string]interface{}{} + resp := map[string]any{} if err := json.NewDecoder(res.Body).Decode(&resp); err != nil { return "", err } diff --git a/release/internal/slack/slack.go b/release/internal/slack/slack.go index 362f527426d..daf1c182325 100644 --- a/release/internal/slack/slack.go +++ b/release/internal/slack/slack.go @@ -17,21 +17,17 @@ package slack import ( "bytes" _ "embed" - "fmt" "text/template" "github.com/sirupsen/logrus" "github.com/slack-go/slack" - "github.com/projectcalico/calico/release/internal/registry" + "github.com/projectcalico/calico/release/internal/utils" + "github.com/projectcalico/calico/release/internal/version" ) -var ( - //go:embed templates/success.json.gotmpl - successMessageTemplateData string - //go:embed templates/failure.json.gotmpl - failureMessageTemplateData string -) +//go:embed templates/success.json.gotmpl +var successMessageTemplateData string // Config is the configuration for the Slack client type Config struct { @@ -42,95 +38,42 @@ type Config struct { Channel string `envconfig:"SLACK_CHANNEL"` } -// MessageData is the data to be rendered in the message -type MessageData struct { - // ReleaseName is the name of the release - ReleaseName string - - // Product is the name of the product - Product string - - // Stream is the stream of the release - Stream string - - // Version is the version of the release - Version string - - // OperatorVersion is the version of the operator - OperatorVersion string - - // DocsURL is the URL for the release docs. - // This is only used for success messages - DocsURL string - - // CIURL is the URL for the CI job. - // This is required for failure messages - // and optional for success messages. - CIURL string - - // ImageScanResultURL is the URL for the results from the image scanner. - // This is only used for success messages - ImageScanResultURL string - - // FailedImages is the list of failed images. - // This is required for failure messages - FailedImages []registry.Component -} - // Message is a Slack message -type Message struct { - // Config is the configuration for the message - Config Config - - // Data is the data to be rendered in the message - Data MessageData +type Message interface { + Send(cfg Config) error + TemplateText() string } -// Create a new Slack client -func client(token string, debug bool) *slack.Client { - options := []slack.Option{} - if debug { - options = append(options, slack.OptionDebug(true)) - } - client := slack.New(token, options...) - return client +type BaseMessageData struct { + ReleaseName string + Versions version.Data + Product string + Stream string + ReleaseType utils.ReleaseType + CIURL string } -// SendFailure sends a failure message to Slack -func (m *Message) SendFailure(debug bool) error { - if len(m.Data.FailedImages) == 0 { - return fmt.Errorf("no failed images to report") - } - if m.Data.CIURL == "" { - return fmt.Errorf("CI URL is required for failure messages") - } - client := client(m.Config.Token, debug) - return m.send(client, failureMessageTemplateData) +type BaseMessage struct { + Data BaseMessageData } -// SendSuccess sends a success message to Slack -func (m *Message) SendSuccess(debug bool) error { - client := client(m.Config.Token, debug) - return m.send(client, successMessageTemplateData) +func (m BaseMessage) TemplateText() string { + logrus.Fatal("TemplateText not implemented") + return "" } -// send sends the message to Slack -func (m *Message) send(client *slack.Client, messageTemplateData string) error { - message, err := m.renderMessage(messageTemplateData) +func (m BaseMessage) Send(cfg Config) error { + message, err := m.renderMessage() if err != nil { return err } - logrus.WithFields(logrus.Fields{ - "channel": m.Config.Channel, - "message": message, - }).Debug("Sending message to Slack") - _, _, err = client.PostMessage(m.Config.Channel, slack.MsgOptionBlocks(message...)) + client := slack.New(cfg.Token, slack.OptionDebug(logrus.IsLevelEnabled(logrus.DebugLevel))) + _, _, err = client.PostMessage(cfg.Channel, slack.MsgOptionBlocks(message...)) return err } -// renderMessage renders the message from the template -func (m *Message) renderMessage(templateData string) ([]slack.Block, error) { - tmpl, err := template.New("message").Parse(templateData) +func (m BaseMessage) renderMessage() ([]slack.Block, error) { + tmpl, err := template.New("message").Parse(m.TemplateText()) if err != nil { return nil, err } @@ -144,3 +87,18 @@ func (m *Message) renderMessage(templateData string) ([]slack.Block, error) { } return blocks.BlockSet, nil } + +type SuccessMessageData struct { + BaseMessageData + DocsURL string + ImageScanResultURL string +} + +type SuccessMessage struct { + BaseMessage + Data SuccessMessageData +} + +func (m SuccessMessage) TemplateText() string { + return successMessageTemplateData +} diff --git a/release/internal/slack/templates/failure.json.gotmpl b/release/internal/slack/templates/failure.json.gotmpl deleted file mode 100644 index 316829f336c..00000000000 --- a/release/internal/slack/templates/failure.json.gotmpl +++ /dev/null @@ -1,85 +0,0 @@ -[ - { - "type": "header", - "text": { - "type": "plain_text", - "text": ":warning: Failed to create {{.Product}} {{.Stream}} release" - } - }, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "*{{.ReleaseName}}*" - } - {{- if .CIURL}}, - "accessory": { - "type": "button", - "text": { - "type": "plain_text", - "text": ":building_construction: Build Details", - "emoji": true - }, - "value": "ci_link", - "url": "{{.CIURL}}" - } - {{- end }} - }, - { - "type": "context", - "elements": [ - { - "type": "mrkdwn", - "text": "{{.Version}}\nOperator {{.OperatorVersion}}" - } - ] - }, - { - "type": "divider" - }, - { - "type": "section", - "text": { - "type": "plain_text", - "text": "See the list of unavailable images and versions below :arrow_heading_down:", - "emoji": true - } - }, - { - "type": "section", - "fields": [ - { - "type": "mrkdwn", - "text": "*Images*" - }, - { - "type": "mrkdwn", - "text": "*Version*" - } - {{- range $component := .FailedImages }}, - { - "type": "plain_text", - "text": "{{$component.Image}}" - }, - { - "type": "plain_text", - "text": "{{$component.Version}}" - } - {{- end }} - ] - } - {{- if not .CIURL }}, - { - "type": "divider" - }, - { - "type": "context", - "elements": [ - { - "type": "plain_text", - "text": "This release was not built by CI." - } - ] - } - {{- end }} -] diff --git a/release/internal/slack/templates/success.json.gotmpl b/release/internal/slack/templates/success.json.gotmpl index 18df96604fe..863fd0cc2fe 100644 --- a/release/internal/slack/templates/success.json.gotmpl +++ b/release/internal/slack/templates/success.json.gotmpl @@ -3,7 +3,7 @@ "type": "header", "text": { "type": "plain_text", - "text": ":loud_sound: New {{.Product}} {{.Stream}} release" + "text": ":loud_sound: New {{.Product}} {{.Stream}} {{.ReleaseType))" } }, { @@ -18,7 +18,7 @@ "elements": [ { "type": "mrkdwn", - "text": "Version:{{.Version}}\nOperator {{.OperatorVersion}}" + "text": "Version:{{.Version.ProductVersion}}\nOperator {{.Version.OperatorVersion}}" } ] }, diff --git a/release/internal/utils/config.go b/release/internal/utils/config.go index 7b86c2618a1..eeb187df12f 100644 --- a/release/internal/utils/config.go +++ b/release/internal/utils/config.go @@ -20,14 +20,26 @@ import ( // limitations under the License. const ( - // ProductName is the name of the product. - ProductName = "calico" + // Calico is the name of projectcalico.product. + Calico = "calico" - // ProductCode is the code of the product. - ProductCode = "os" + // CalicoCode is the code for Calico. + CalicoCode = "os" + + // CalicoOrg is the organization for Calico. + CalicoOrg = "projectcalico" + + // CalicoRepo is the repository for Calico. + CalicoRepo = "calico" + + DevTagSuffix = "0.dev" + + ReleaseBranchPrefix = "release" + + GitRemote = "origin" ) // DisplayProductName returns the product name in title case. func DisplayProductName() string { - return cases.Title(language.English).String(ProductName) + return cases.Title(language.English).String(Calico) } diff --git a/release/internal/utils/releasetype.go b/release/internal/utils/releasetype.go new file mode 100644 index 00000000000..2ac9c8aeab7 --- /dev/null +++ b/release/internal/utils/releasetype.go @@ -0,0 +1,29 @@ +// Copyright (c) 2024 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +type ReleaseType string + +func (r ReleaseType) String() string { + return string(r) +} + +const ( + // ReleaseTypeHashrelease is the release type for hashrelease. + ReleaseTypeHashrelease ReleaseType = "hashrelease" + + // ReleaseTypeRelease is the release type for release. + ReleaseTypeRelease ReleaseType = "release" +) diff --git a/release/internal/version/version.go b/release/internal/version/version.go index a4182d82437..7562d46a306 100644 --- a/release/internal/version/version.go +++ b/release/internal/version/version.go @@ -21,6 +21,7 @@ import ( "strings" "github.com/Masterminds/semver/v3" + "github.com/mitchellh/mapstructure" "github.com/sirupsen/logrus" "github.com/projectcalico/calico/release/internal/command" @@ -29,10 +30,10 @@ import ( type Data struct { // ProductVersion is the version of the product - ProductVersion Version + ProductVersion Version `json:"productVersion"` // OperatorVersion is the version of operator - OperatorVersion Version + OperatorVersion Version `json:"operatorVersion"` } // Version represents a version, and contains methods for working with versions. @@ -82,6 +83,22 @@ func (v *Version) Semver() *semver.Version { return ver } +func ProductVersion(versions map[string]any) (*Version, error) { + var d Data + if err := mapstructure.Decode(versions, &d); err != nil { + return nil, err + } + return &d.ProductVersion, nil +} + +func OperatorVersion(versions map[string]any) (*Version, error) { + var d Data + if err := mapstructure.Decode(versions, &d); err != nil { + return nil, err + } + return &d.OperatorVersion, nil +} + // GitVersion returns the current git version of the directory as a Version object. func GitVersion() Version { // First, determine the git revision. diff --git a/release/pkg/errors/errors.go b/release/pkg/errors/errors.go new file mode 100644 index 00000000000..65221df1a3c --- /dev/null +++ b/release/pkg/errors/errors.go @@ -0,0 +1,51 @@ +// Copyright (c) 2024 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package errors + +import ( + "fmt" + + "github.com/projectcalico/calico/release/internal/registry" +) + +type ErrInvalidImages struct { + ReleaseName string + Stream string + Versions map[string]string + FailedImages []registry.Component +} + +func (e ErrInvalidImages) Error() string { + return fmt.Sprintf("%s hashrelease has %d invalid images: %v", e.ReleaseName, len(e.FailedImages), e.FailedImages) +} + +func (e ErrInvalidImages) Unwrap() error { + return fmt.Errorf("invalid images: %v", e.FailedImages) +} + +type ErrHashreleaseAlreadyExists struct { + ReleaseName string + Hash string + Stream string + Versions map[string]string +} + +func (e ErrHashreleaseAlreadyExists) Error() string { + return fmt.Sprintf("hashrelease %s (%s) already exists", e.ReleaseName, e.Hash) +} + +func (e ErrHashreleaseAlreadyExists) Unwrap() error { + return fmt.Errorf("hashrelease %s (%s) already exists", e.ReleaseName, e.Hash) +} diff --git a/release/pkg/manager/branch/manager.go b/release/pkg/manager/branch/manager.go index 64690cbd108..517a8e6a129 100644 --- a/release/pkg/manager/branch/manager.go +++ b/release/pkg/manager/branch/manager.go @@ -88,13 +88,13 @@ func NewManager(opts ...Option) *BranchManager { return b } -func (b *BranchManager) CutVersionedBranch(version string) error { +func (b *BranchManager) CutVersionedBranch(stream string) error { if b.validate { if err := b.PreBranchCutValidation(); err != nil { return fmt.Errorf("pre-branch cut validation failed: %s", err) } } - newBranchName := fmt.Sprintf("%s-%s", b.releaseBranchPrefix, version) + newBranchName := fmt.Sprintf("%s-%s", b.releaseBranchPrefix, stream) logrus.WithField("branch", newBranchName).Info("Creating new release branch") if _, err := b.git("checkout", "-b", newBranchName); err != nil { return err diff --git a/release/pkg/manager/calico/manager.go b/release/pkg/manager/calico/manager.go index cc8db01864f..952eaac8aa0 100644 --- a/release/pkg/manager/calico/manager.go +++ b/release/pkg/manager/calico/manager.go @@ -522,7 +522,7 @@ func (r *CalicoManager) releasePrereqs() error { } // If we are releasing to projectcalico/calico, make sure we are releasing to the default registries. - if r.githubOrg == "projectcalico" && r.repo == "calico" { + if r.githubOrg == utils.CalicoOrg && r.repo == utils.CalicoRepo { if !reflect.DeepEqual(r.imageRegistries, defaultRegistries) { return fmt.Errorf("image registries cannot be different from default registries for a release") } @@ -554,7 +554,8 @@ func (r *CalicoManager) hashreleasePrereqs() error { return fmt.Errorf("missing hashrelease server configuration") } } - images, err := pinnedversion.RetrieveComponentsToValidate(r.tmpDir) + pinned := pinnedversion.New(map[string]any{}, r.tmpDir) + images, err := pinned.ComponentsToValidate() if err != nil { return fmt.Errorf("failed to get components to validate: %s", err) } @@ -595,7 +596,11 @@ func (r *CalicoManager) hashreleasePrereqs() error { imageList = append(imageList, component.String()) } imageScanner := imagescanner.New(r.imageScanningConfig) - err := imageScanner.Scan(imageList, r.hashrelease.Stream, false, r.tmpDir) + stream, err := r.hashrelease.Stream() + if err != nil { + return fmt.Errorf("failed to get stream: %s", err) + } + err = imageScanner.Scan(imageList, stream, false, r.tmpDir) if err != nil { // Error is logged and ignored as this is not considered a fatal error logrus.WithError(err).Error("Failed to scan images") diff --git a/release/pkg/manager/calico/options.go b/release/pkg/manager/calico/options.go index bb43961a09f..dab103c7f71 100644 --- a/release/pkg/manager/calico/options.go +++ b/release/pkg/manager/calico/options.go @@ -50,7 +50,7 @@ func WithReleaseBranchValidation(validate bool) Option { } } -func WithVersions(versions *version.Data) Option { +func WithVersions(versions version.Data) Option { return func(r *CalicoManager) error { r.calicoVersion = versions.ProductVersion.FormattedString() r.operatorVersion = versions.OperatorVersion.FormattedString() diff --git a/release/pkg/manager/operator/manager.go b/release/pkg/manager/operator/manager.go index dc4fcdae900..195a6691ddc 100644 --- a/release/pkg/manager/operator/manager.go +++ b/release/pkg/manager/operator/manager.go @@ -46,6 +46,9 @@ type OperatorManager struct { // calicoDir is the absolute path to the root directory of the calico repository calicoDir string + // tmpDir is the absolute path to the temporary directory + tmpDir string + // origin remote repository remote string @@ -58,6 +61,10 @@ type OperatorManager struct { // branch is the branch to use branch string + // releaseStream is the release stream to use + // for cutting release branch + releaseStream string + // devTag is the development tag identifier devTagIdentifier string @@ -101,16 +108,16 @@ func NewManager(opts ...Option) *OperatorManager { return o } -func (o *OperatorManager) Build(outputDir string) error { +func (o *OperatorManager) Build() error { if !o.isHashRelease { return fmt.Errorf("operator manager builds only for hash releases") } if o.validate { - if err := o.PreBuildValidation(outputDir); err != nil { + if err := o.PreBuildValidation(); err != nil { return err } } - component, componentsVersionPath, err := pinnedversion.GenerateOperatorComponents(outputDir) + component, componentsVersionPath, err := pinnedversion.New(map[string]any{}, o.tmpDir).OperatorComponent() if err != nil { return err } @@ -152,7 +159,7 @@ func (o *OperatorManager) Build(outputDir string) error { return o.docker.TagImage(currentTag, newTag) } -func (o *OperatorManager) PreBuildValidation(outputDir string) error { +func (o *OperatorManager) PreBuildValidation() error { if !o.isHashRelease { return fmt.Errorf("operator manager builds only for hash releases") } @@ -179,17 +186,18 @@ func (o *OperatorManager) PreBuildValidation(outputDir string) error { if len(o.architectures) == 0 { errStack = errors.Join(errStack, fmt.Errorf("no architectures specified")) } - operatorComponent, err := pinnedversion.RetrievePinnedOperator(outputDir) + + pinned, err := pinnedversion.New(map[string]any{}, o.tmpDir).Get() if err != nil { - return fmt.Errorf("failed to get operator component: %s", err) + return fmt.Errorf("failed to get pinned version: %s", err) } - if operatorComponent.Version != o.version { - errStack = errors.Join(errStack, fmt.Errorf("operator version mismatch: expected %s, got %s", o.version, operatorComponent.Version)) + if pinned.TigeraOperator.Version != o.version { + errStack = errors.Join(errStack, fmt.Errorf("operator version mismatch: expected %s, got %s", o.version, pinned.TigeraOperator.Version)) } return errStack } -func (o *OperatorManager) Publish(outputDir string) error { +func (o *OperatorManager) Publish() error { if o.validate { if err := o.PrePublishValidation(); err != nil { return err @@ -200,11 +208,11 @@ func (o *OperatorManager) Publish(outputDir string) error { logrus.Warn("Skipping publish is set, will treat as dry-run") fields["dry-run"] = "true" } - operatorComponent, err := pinnedversion.RetrievePinnedOperator(outputDir) + pinned, err := pinnedversion.New(map[string]any{}, o.tmpDir).Get() if err != nil { - logrus.WithError(err).Error("Failed to get operator component") - return err + return fmt.Errorf("failed to get pinned version: %s", err) } + operatorComponent := pinned.Operator() var imageList []string for _, arch := range o.architectures { imgName := fmt.Sprintf("%s-%s", operatorComponent.String(), arch) @@ -251,7 +259,7 @@ func (o *OperatorManager) PrePublishValidation() error { return nil } -func (o *OperatorManager) CutBranch(version string) error { +func (o *OperatorManager) CutBranch() error { m := branch.NewManager(branch.WithRepoRoot(o.dir), branch.WithRepoRemote(o.remote), branch.WithMainBranch(o.branch), @@ -262,10 +270,10 @@ func (o *OperatorManager) CutBranch(version string) error { if err := o.Clone(); err != nil { return err } - if version == "" { + if o.releaseStream == "" { return m.CutReleaseBranch() } - return m.CutVersionedBranch(version) + return m.CutVersionedBranch(o.releaseStream) } func (o *OperatorManager) Clone() error { diff --git a/release/pkg/manager/operator/options.go b/release/pkg/manager/operator/options.go index 685a7bbc78c..3d5bdfd71cd 100644 --- a/release/pkg/manager/operator/options.go +++ b/release/pkg/manager/operator/options.go @@ -30,6 +30,13 @@ func WithCalicoDirectory(dir string) Option { } } +func WithTmpDirectory(dir string) Option { + return func(o *OperatorManager) error { + o.tmpDir = dir + return nil + } +} + func WithRepoRemote(remote string) Option { return func(o *OperatorManager) error { o.remote = remote @@ -58,6 +65,13 @@ func WithBranch(branch string) Option { } } +func WithReleaseStream(stream string) Option { + return func(o *OperatorManager) error { + o.releaseStream = stream + return nil + } +} + func WithDevTagIdentifier(devTag string) Option { return func(o *OperatorManager) error { o.devTagIdentifier = devTag diff --git a/release/pkg/manager/operator/values.go b/release/pkg/manager/operator/values.go new file mode 100644 index 00000000000..7e370487f00 --- /dev/null +++ b/release/pkg/manager/operator/values.go @@ -0,0 +1,28 @@ +// Copyright (c) 2024 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package operator + +import "github.com/projectcalico/calico/release/internal/registry" + +const ( + DefaultImage = "tigera/operator" + DefaultOrg = "tigera" + DefaultRepoName = "operator" + DefaultRemote = "origin" + DefaultBranchName = "master" + DefaultReleaseBranchPrefix = "release" + DefaultDevTagSuffix = "0.dev" + DefaultRegistry = registry.QuayRegistry +) diff --git a/release/pkg/tasks/hashrelease.go b/release/pkg/tasks/hashrelease.go index f5b3f75f8a9..ba77a762f28 100644 --- a/release/pkg/tasks/hashrelease.go +++ b/release/pkg/tasks/hashrelease.go @@ -21,57 +21,25 @@ import ( "github.com/sirupsen/logrus" - "github.com/projectcalico/calico/release/internal/config" "github.com/projectcalico/calico/release/internal/hashreleaseserver" - "github.com/projectcalico/calico/release/internal/imagescanner" "github.com/projectcalico/calico/release/internal/pinnedversion" - "github.com/projectcalico/calico/release/internal/slack" "github.com/projectcalico/calico/release/internal/utils" ) // HashreleasePublished checks if the hashrelease has already been published. // If it has, the process is halted. -func HashreleasePublished(cfg *config.Config, hash string) (bool, error) { - if !cfg.HashreleaseServerConfig.Valid() { +func HashreleasePublished(cfg *hashreleaseserver.Config, hash string, ci bool) (bool, error) { + if !cfg.Valid() { // Check if we're running in CI - if so, we should fail if this configuration is missing. // Otherwise, we should just log and continue. - if cfg.CI.IsCI { + if ci { return false, fmt.Errorf("missing hashrelease server configuration") } logrus.Info("Missing hashrelease server configuration, skipping remote hashrelease check") return false, nil } - return hashreleaseserver.HasHashrelease(hash, &cfg.HashreleaseServerConfig), nil -} - -// HashreleaseSlackMessage sends a slack message to notify that a hashrelease has been published. -func HashreleaseSlackMessage(cfg *config.Config, hashrel *hashreleaseserver.Hashrelease) error { - scanResultURL := imagescanner.RetrieveResultURL(cfg.TmpFolderPath()) - if scanResultURL == "" { - logrus.Warn("No image scan result URL found") - } - slackMsg := slack.Message{ - Config: cfg.SlackConfig, - Data: slack.MessageData{ - ReleaseName: hashrel.Name, - Product: utils.DisplayProductName(), - Stream: hashrel.Stream, - Version: hashrel.ProductVersion, - OperatorVersion: hashrel.OperatorVersion, - DocsURL: hashrel.URL(), - CIURL: cfg.CI.URL(), - ImageScanResultURL: scanResultURL, - }, - } - if err := slackMsg.SendSuccess(logrus.IsLevelEnabled(logrus.DebugLevel)); err != nil { - logrus.WithError(err).Error("Failed to send slack message") - } - logrus.WithFields(logrus.Fields{ - "name": hashrel.Name, - "URL": hashrel.URL(), - }).Info("Sent hashrelease publish notification to slack") - return nil + return hashreleaseserver.HasHashrelease(hash, cfg), nil } // ReformatHashrelease modifies the generated release output to match @@ -81,35 +49,33 @@ func HashreleaseSlackMessage(cfg *config.Config, hashrel *hashreleaseserver.Hash // - Copy the windows zip file to files/windows/calico-windows-.zip // - Copy tigera-operator-.tgz to tigera-operator.tgz // - Copy ocp.tgz to manifests/ocp.tgz -func ReformatHashrelease(cfg *config.Config, dir string) error { +func ReformatHashrelease(hashreleaseDir, tmpDir string) error { logrus.Info("Modifying hashrelease output to match legacy format") - pinned, err := pinnedversion.RetrievePinnedVersion(cfg.TmpFolderPath()) + pinned, err := pinnedversion.New(map[string]any{}, tmpDir).(*pinnedversion.CalicoPinnedVersions).Get() if err != nil { return err } - ver := pinned.Components["calico"].Version // Copy the windows zip file to files/windows/calico-windows-.zip - if err := os.MkdirAll(filepath.Join(dir, "files", "windows"), 0o755); err != nil { + if err := os.MkdirAll(filepath.Join(hashreleaseDir, "files", "windows"), 0o755); err != nil { return err } - windowsZip := filepath.Join(dir, fmt.Sprintf("calico-windows-%s.zip", ver)) - windowsZipDst := filepath.Join(dir, "files", "windows", fmt.Sprintf("calico-windows-%s.zip", ver)) + windowsZip := filepath.Join(hashreleaseDir, fmt.Sprintf("calico-windows-%s.zip", pinned.ProductVersion())) + windowsZipDst := filepath.Join(hashreleaseDir, "files", "windows", fmt.Sprintf("calico-windows-%s.zip", pinned.ProductVersion())) if err := utils.CopyFile(windowsZip, windowsZipDst); err != nil { return err } // Copy the ocp.tgz to manifests/ocp.tgz - ocpTarball := filepath.Join(dir, "ocp.tgz") - ocpTarballDst := filepath.Join(dir, "manifests", "ocp.tgz") + ocpTarball := filepath.Join(hashreleaseDir, "ocp.tgz") + ocpTarballDst := filepath.Join(hashreleaseDir, "manifests", "ocp.tgz") if err := utils.CopyFile(ocpTarball, ocpTarballDst); err != nil { return err } // Copy the operator tarball to tigera-operator.tgz - helmChartVersion := ver - operatorTarball := filepath.Join(dir, fmt.Sprintf("tigera-operator-%s.tgz", helmChartVersion)) - operatorTarballDst := filepath.Join(dir, "tigera-operator.tgz") + operatorTarball := filepath.Join(hashreleaseDir, fmt.Sprintf("tigera-operator-%s.tgz", pinned.HelmChartVersion())) + operatorTarballDst := filepath.Join(hashreleaseDir, "tigera-operator.tgz") if err := utils.CopyFile(operatorTarball, operatorTarballDst); err != nil { return err } diff --git a/release/pkg/tasks/notification.go b/release/pkg/tasks/notification.go new file mode 100644 index 00000000000..85a84242717 --- /dev/null +++ b/release/pkg/tasks/notification.go @@ -0,0 +1,52 @@ +// Copyright (c) 2024 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tasks + +import ( + "github.com/mitchellh/mapstructure" + + "github.com/projectcalico/calico/release/internal/hashreleaseserver" + "github.com/projectcalico/calico/release/internal/imagescanner" + "github.com/projectcalico/calico/release/internal/slack" + "github.com/projectcalico/calico/release/internal/utils" + "github.com/projectcalico/calico/release/internal/version" +) + +// AnnouceHashrelease sends a notification to announce a release +func AnnouceHashrelease(slackCfg slack.Config, hashrelease hashreleaseserver.Hashrelease, CIURL, tmpDir string) error { + var versions version.Data + if err := mapstructure.Decode(hashrelease.Versions, &versions); err != nil { + return err + } + stream, err := hashrelease.Stream() + if err != nil { + return err + } + m := slack.SuccessMessage{ + Data: slack.SuccessMessageData{ + BaseMessageData: slack.BaseMessageData{ + ReleaseName: hashrelease.Name, + Product: utils.Calico, + Stream: stream, + ReleaseType: utils.ReleaseTypeHashrelease, + CIURL: CIURL, + Versions: versions, + }, + DocsURL: hashrelease.URL(), + ImageScanResultURL: imagescanner.RetrieveResultURL(tmpDir), + }, + } + return m.Send(slackCfg) +}