From 4d31d886c4833d5a78dce6f6e39b4d9136a7f192 Mon Sep 17 00:00:00 2001 From: tuti Date: Tue, 14 Jan 2025 09:33:24 -0800 Subject: [PATCH] allow creating a new operator based off a previous operator --- Makefile | 10 ++ go.mod | 10 +- go.sum | 9 +- hack/release-from/flags.go | 112 +++++++++++++++ hack/release-from/main.go | 284 +++++++++++++++++++++++++++++++++++++ hack/release-from/utils.go | 86 +++++++++++ 6 files changed, 505 insertions(+), 6 deletions(-) create mode 100644 hack/release-from/flags.go create mode 100644 hack/release-from/main.go create mode 100644 hack/release-from/utils.go diff --git a/Makefile b/Makefile index 19b86b60e2..aed7c14b9b 100644 --- a/Makefile +++ b/Makefile @@ -567,6 +567,7 @@ release-publish-images: release-prereqs release-check-image-exists release-github: hack/bin/gh release-notes @echo "Creating github release for $(VERSION)" hack/bin/gh release create $(VERSION) --title $(VERSION) --draft --notes-file $(VERSION)-release-notes.md + @echo "Release $(VERSION) created in draft state. Please review and publish: https://github.com/tigera/operator/releases/tag/$(VERSION) ." GITHUB_CLI_VERSION?=2.62.0 hack/bin/gh: @@ -576,6 +577,15 @@ hack/bin/gh: chmod +x $@ rm hack/bin/gh.tgz +hack/bin/release-from: $(shell find ./hack/release-from -type f) + mkdir -p hack/bin + $(CONTAINERIZED) $(CALICO_BUILD) \ + sh -c '$(GIT_CONFIG_SSH) \ + go build -buildvcs=false -o hack/bin/release-from ./hack/release-from' + +release-from: hack/bin/release-from var-require-all-VERSION-OPERATOR_BASE_VERSION var-require-one-of-EE_IMAGES_VERSIONS-OS_IMAGES_VERSIONS + hack/bin/release-from + # release-prereqs checks that the environment is configured properly to create a release. release-prereqs: ifndef VERSION diff --git a/go.mod b/go.mod index 00945833b1..400817a8ba 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,6 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/corazawaf/coraza-coreruleset/v4 v4.7.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/elastic/go-sysinfo v1.13.1 // indirect github.com/elastic/go-ucfg v0.8.8 // indirect @@ -117,16 +116,19 @@ require ( k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a // indirect k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 // indirect - sigs.k8s.io/gateway-api v1.1.0 // indirect + sigs.k8s.io/gateway-api v1.1.0 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) require ( - github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc // indirect - github.com/magefile/mage v1.14.0 // indirect + github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc + github.com/sirupsen/logrus v1.9.3 + github.com/urfave/cli/v3 v3.0.0-beta1 ) +require github.com/magefile/mage v1.14.0 // indirect + replace ( github.com/Azure/go-autorest => github.com/Azure/go-autorest v13.3.2+incompatible // Required by OLM github.com/imdario/mergo => github.com/imdario/mergo v0.3.16 // Per advice at https://github.com/darccio/mergo?tab=readme-ov-file#100 diff --git a/go.sum b/go.sum index 2bcf796112..f3c772851c 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,6 @@ github.com/containernetworking/cni v1.2.3 h1:hhOcjNVUQTnzdRJ6alC5XF+wd9mfGIUaj8F github.com/containernetworking/cni v1.2.3/go.mod h1:DuLgF+aPd3DzcTQTtp/Nvl1Kim23oFKdm2okJzBQA5M= github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc h1:OlJhrgI3I+FLUCTI3JJW8MoqyM78WbqJjecqMnqG+wc= github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc/go.mod h1:7rsocqNDkTCira5T0M7buoKR2ehh7YZiPkzxRuAgvVU= -github.com/corazawaf/coraza-coreruleset/v4 v4.7.0 h1:j02CDxQYHVFZfBxbKLWYg66jSLbPmZp1GebyMwzN9Z0= -github.com/corazawaf/coraza-coreruleset/v4 v4.7.0/go.mod h1:1FQt1p+JSQ6tYrafMqZrEEdDmhq6aVuIJdnk+bM9hMY= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -209,6 +207,8 @@ github.com/r3labs/diff/v2 v2.15.1/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -219,10 +219,13 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tigera/api v0.0.0-20230406222214-ca74195900cb h1:Y7r5Al3V235KaEoAzGBz9RYXEbwDu8CPaZoCq2PlD8w= github.com/tigera/api v0.0.0-20230406222214-ca74195900cb/go.mod h1:ZZghiX3CUsBAc0osBjRvV6y/eun2ObYdvSbjqXAoj/w= +github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg= +github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= @@ -287,6 +290,7 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -342,6 +346,7 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= diff --git a/hack/release-from/flags.go b/hack/release-from/flags.go new file mode 100644 index 0000000000..1cae7fd699 --- /dev/null +++ b/hack/release-from/flags.go @@ -0,0 +1,112 @@ +// Copyright (c) 2025 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 ( + "context" + "fmt" + "regexp" + + "github.com/urfave/cli/v3" +) + +var debugFlag = &cli.BoolFlag{ + Name: "debug", + Usage: "Enable debug logging", + Sources: cli.EnvVars("DEBUG"), +} + +var remoteFlag = &cli.StringFlag{ + Name: "remote", + Usage: "The git remote to push the release to", + Value: "origin", + Sources: cli.EnvVars("GIT_REMOTE"), +} + +var baseOperatorFlag = &cli.StringFlag{ + Name: "base-version", + Aliases: []string{"base"}, + Usage: "The version of the operator to base this new version from. " + + "It is expected in the format vX.Y.Z for releases and " + + "for hashrelease, either vX.Y.Z-n-g- (legacy) or " + + "vX.Y.Z-n-g- (new) where product-hashrelease-version is in the format vA.B.C-u-g", + Sources: cli.EnvVars("OPERATOR_BASE_VERSION"), + Required: true, + Action: func(ctx context.Context, c *cli.Command, value string) error { + if !regexp.MustCompile(baseVersionFormat).MatchString(value) { + return fmt.Errorf("base-version must be in the format vX.Y.Z or vX.Y.Z-n-g- or " + + "vX.Y.Z-n-g-") + } + return nil + }, +} + +var versionFlag = &cli.StringFlag{ + Name: "version", + Usage: "The version of the operator to release", + Sources: cli.EnvVars("OPERATOR_VERSION", "VERSION"), + Action: func(ctx context.Context, c *cli.Command, value string) error { + if value == c.String("base-version") { + return fmt.Errorf("version cannot be the same as base-version") + } + if !regexp.MustCompile(baseVersionFormat).MatchString(value) { + return fmt.Errorf("base-version must be in the format vX.Y.Z or vX.Y.Z-n-g- or " + + "vX.Y.Z-n-g-") + } + return nil + }, +} + +var exceptCalicoFlag = &cli.StringSliceFlag{ + Name: "except-calico", + Usage: "A list of Calico images and the version to use for them. " + + "This should use the format based on the config/calico_versions.yaml file. " + + "e.g. --except-calico calico/cni:vX.Y.Z --except-calico csi-node-driver-registrar:vA.B.C-n-g", + Sources: cli.EnvVars("OS_IMAGES_VERSIONS"), +} + +var exceptEnterpriseFlag = &cli.StringSliceFlag{ + Name: "except-calico-enterprise", + Aliases: []string{"except-enterprise", "except-calient"}, + Usage: "A list of Enterprise images and the versions to use for them. " + + "This should use the format based on the config/enterprise_versions.yaml file. " + + "e.g. --except-calico-enterprise linseed:vX.Y.Z --except-calico-enterprise security-event-webhooks-processor:vA.B.C-n-g", + Sources: cli.EnvVars("EE_IMAGES_VERSIONS"), + Action: func(ctx context.Context, c *cli.Command, values []string) error { + if len(values) == 0 && len(c.StringSlice("except-calico")) == 0 { + return fmt.Errorf("at least one of --except-calico or --except-enterprise must be set") + } + return nil + }, +} + +var ( + archOptions = []string{"amd64", "arm64", "ppc64le", "s390x"} + archFlag = &cli.StringSliceFlag{ + Name: "architecture", + Aliases: []string{"arch"}, + Usage: "The architecture to use for the release. Repeat for multiple architectures.", + Sources: cli.EnvVars("ARCHS"), + Value: archOptions, + Action: func(ctx context.Context, c *cli.Command, values []string) error { + for _, arch := range values { + if !contains(archOptions, arch) { + return fmt.Errorf("invalid architecture %s", arch) + } + } + return nil + }, + } +) diff --git a/hack/release-from/main.go b/hack/release-from/main.go new file mode 100644 index 0000000000..8958a1f687 --- /dev/null +++ b/hack/release-from/main.go @@ -0,0 +1,284 @@ +// Copyright (c) 2025 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 ( + "context" + "fmt" + "io" + "net/http" + "os" + "regexp" + "strings" + + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v3" + "gopkg.in/yaml.v2" +) + +type component struct { + Image string `yaml:"image"` + Version string `yaml:"version"` + Registry string `yaml:"registry"` +} + +type productConfig struct { + Title string `yaml:"title"` + Components map[string]component `yaml:"components"` +} + +func main() { + cmd := &cli.Command{ + Name: "operator-from", + Usage: "CLI tool for releasing operator using a previous release", + Flags: []cli.Flag{ + baseOperatorFlag, + versionFlag, + exceptCalicoFlag, + exceptEnterpriseFlag, + debugFlag, + }, + Before: func(ctx context.Context, c *cli.Command) (context.Context, error) { + if c.Bool(debugFlag.Name) { + logrus.SetLevel(logrus.DebugLevel) + } + // check if git repo is dirty + if version, err := gitVersion(); err != nil { + return ctx, fmt.Errorf("Error getting git version: %s", err) + } else if strings.Contains(version, "dirty") { + return ctx, fmt.Errorf("Git repo is dirty, please commit changes before releasing") + } + return ctx, nil + }, + Action: func(ctx context.Context, c *cli.Command) error { + // get root directory of operator git repo + repoRootDir, err := runCommand("git", []string{"rev-parse", "--show-toplevel"}, nil) + if err != nil { + return fmt.Errorf("Error getting git root directory: %s", err) + } + + // fetch config from the base version of the operator + if err := retrieveBaseVersionConfig(baseOperatorFlag.Name, repoRootDir); err != nil { + return fmt.Errorf("Error getting base version config: %s", err) + } + + // Apply new version overrides + calicoOverrides := c.StringSlice(exceptCalicoFlag.Name) + if len(calicoOverrides) > 0 { + if err := overrideConfig(repoRootDir, calicoConfig, calicoOverrides); err != nil { + return fmt.Errorf("Error overriding calico config: %s", err) + } + } + enterpriseOverrides := c.StringSlice(exceptEnterpriseFlag.Name) + if len(enterpriseOverrides) > 0 { + if err := overrideConfig(repoRootDir, enterpriseConfig, enterpriseOverrides); err != nil { + return fmt.Errorf("Error overriding calico config: %s", err) + } + } + + // Either build a new release or a new hashrelease operator + version := c.String(versionFlag.Name) + release, err := isRelease(version) + if err != nil { + return fmt.Errorf("Error determining if version is a release: %s", err) + } else if release { + return newOperator(repoRootDir, version, c.String(remoteFlag.Name)) + } + + return newHashreleaseOperator(repoRootDir, version, c.StringSlice(archFlag.Name)) + }, + } + // Run the app. + if err := cmd.Run(context.Background(), os.Args); err != nil { + logrus.WithError(err).Fatal("Error building new operator") + } +} + +func isRelease(version string) (bool, error) { + releaseRegex, err := regexp.Compile(releaseFormat) + if err != nil { + return false, fmt.Errorf("Error compiling release regex: %s", err) + } + return releaseRegex.MatchString(version), nil +} + +func newOperator(dir, version, remote string) error { + // TODO: Commit, tag and push changes + if _, err := runCommandInDir(dir, "git", []string{"add", "config/"}, nil); err != nil { + return fmt.Errorf("Error adding changes in git: %s", err) + } + if _, err := runCommandInDir(dir, "git", []string{"commit", "-m", fmt.Sprintf("Release %s", version)}, nil); err != nil { + return fmt.Errorf("Error committing changes in git: %s", err) + } + if _, err := runCommandInDir(dir, "git", []string{"tag", version}, nil); err != nil { + return fmt.Errorf("Error tagging release in git: %s", err) + } + if _, err := runCommandInDir(dir, "git", []string{"push", remote, version}, nil); err != nil { + return fmt.Errorf("Error pushing tag in git: %s", err) + } + return nil +} + +func newHashreleaseOperator(dir, version string, archs []string) error { + env := os.Environ() + env = append(env, fmt.Sprintf("ARCHES=%s", strings.Join(archs, " "))) + env = append(env, fmt.Sprintf("GIT_VERSION=%s", version)) + if _, err := runCommandInDir(dir, "make", []string{"image-all"}, env); err != nil { + return err + } + for _, arch := range archs { + tag := fmt.Sprintf("%s/%s:%s-%s", quayRegistry, imageName, version, arch) + if _, err := runCommand("docker", []string{ + "tag", + fmt.Sprintf("%s:latest-%s", imageName, arch), + tag, + }, env); err != nil { + return err + } + logrus.WithField("tag", tag).Debug("Built image") + } + + initTag := fmt.Sprintf("%s/%s-init:%s", quayRegistry, imageName, version) + if _, err := runCommand("docker", []string{ + "tag", + fmt.Sprintf("%s-init:latest", imageName), + fmt.Sprintf("%s/%s-init:%s", quayRegistry, imageName, version), + }, env); err != nil { + return err + } + logrus.WithField("tag", initTag).Debug("Built init image") + return publishHashreleaseOperator(version, archs) +} + +func publishHashreleaseOperator(version string, archs []string) error { + multiArchTags := []string{} + for _, arch := range archs { + tag := fmt.Sprintf("%s/%s:%s-%s", quayRegistry, imageName, version, arch) + if _, err := runCommand("docker", []string{"push", tag}, nil); err != nil { + return err + } + logrus.WithField("tag", tag).Debug("Pushed image") + multiArchTags = append(multiArchTags, tag) + } + image := fmt.Sprintf("%s/%s:%s", quayRegistry, imageName, version) + cmd := []string{"manifest", "create", image} + for _, tag := range multiArchTags { + cmd = append(cmd, "--amend", tag) + } + if _, err := runCommand("docker", cmd, nil); err != nil { + return err + } + if _, err := runCommand("docker", []string{"manifest", "push", "--purge", image}, nil); err != nil { + return err + } + logrus.WithField("image", image).Debug("Pushed manifest") + + initImage := fmt.Sprintf("%s/%s-init:%s", quayRegistry, imageName, version) + if _, err := runCommand("docker", []string{"push", initImage}, nil); err != nil { + return err + } + logrus.WithField("image", initImage).Debug("Pushed init image") + return nil +} + +func overrideConfig(repoRootDir, configFile string, overrides []string) error { + components := make(map[string]string) + for _, override := range overrides { + parts := strings.Split(override, ":") + if len(parts) != 2 { + return fmt.Errorf("Invalid override: %s", override) + } + components[parts[0]] = parts[1] + } + // open file locally + localFile := fmt.Sprintf("%s/%s", repoRootDir, configFile) + var config productConfig + if data, err := os.ReadFile(localFile); err != nil { + return fmt.Errorf("Error reading local file %s: %s", configFile, err) + } else if err = yaml.Unmarshal(data, &config); err != nil { + return fmt.Errorf("Error unmarshalling local file %s: %s", configFile, err) + } + for c, ver := range components { + if _, ok := config.Components[c]; ok { + config.Components[c] = component{ + Image: config.Components[c].Image, + Version: ver, + Registry: config.Components[c].Registry, + } + } + } + // overwrite local file with updated config + if err := os.WriteFile(localFile, []byte(fmt.Sprintf("%s\n", config)), 0o644); err != nil { + return fmt.Errorf("Error overwriting local file %s: %s", configFile, err) + } + return nil +} + +func retrieveBaseVersionConfig(baseVersion, repoRootDir string) error { + url, err := getDownloadURL(baseVersion) + if err != nil { + return fmt.Errorf("Error getting download URL: %s", err) + } + + for _, file := range []string{calicoConfig, enterpriseConfig} { + // open file locally + localFile := fmt.Sprintf("%s/%s", repoRootDir, file) + out, err := os.OpenFile(localFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) + if err != nil { + return fmt.Errorf("Error opening local file %s: %s", file, err) + } + defer out.Close() + + // download file from base version + resp, err := http.Get(fmt.Sprintf("%s/%s", url, file)) + if err != nil { + return fmt.Errorf("Error downloading %s: %s", file, err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Error downloading %s: %s", file, resp.Status) + } + + // overwrite local file with downloaded file + if _, err = io.Copy(out, resp.Body); err != nil { + return fmt.Errorf("Error overwriting local file %s: %s", localFile, err) + } + logrus.WithFields(logrus.Fields{ + "file": file, + "localPath": localFile, + "downloadPath": url, + }).Debug("Overwrote local file with downloaded file") + } + return nil +} + +func getDownloadURL(baseVersion string) (string, error) { + release, err := isRelease(baseVersion) + if err != nil { + return "", fmt.Errorf("Error determining if version is a release: %s", err) + } + if release { + return fmt.Sprintf("%s/refs/tags/%s", baseDownloadURL, baseVersion), nil + } + gitHashRegex, err := regexp.Compile(`^g([a-f0-9]{12})`) + if err != nil { + return "", fmt.Errorf("Error compiling git hash regex: %s", err) + } + matches := gitHashRegex.FindStringSubmatch(baseVersion) + if len(matches) < 1 { + return "", fmt.Errorf("Error finding git hash in base version") + } + return fmt.Sprintf("%s/blob/%s", baseDownloadURL, matches[1]), nil +} diff --git a/hack/release-from/utils.go b/hack/release-from/utils.go new file mode 100644 index 0000000000..a50b39c20c --- /dev/null +++ b/hack/release-from/utils.go @@ -0,0 +1,86 @@ +// Copyright (c) 2025 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 ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "strings" + + "github.com/sirupsen/logrus" +) + +const ( + dockerHub = "docker.io" + quayRegistry = "quay.io" + + imageName = "tigera/operator" + + baseDownloadURL = "https://raw.githubusercontent.com/tigera/operator" + + calicoConfig = "config/calico_versions.yml" + enterpriseConfig = "config/enterprise_versions.yml" + + releaseFormat = `^v\d+\.\d+\.\d+$` + hashreleaseFormat = `^v\d+\.\d+\.\d+-\d+-g[a-f0-9]{12}-[a-z0-9-]+$` + baseVersionFormat = `^v\d+\.\d+\.\d+(-\d+-g[a-f0-9]{12}-[a-z0-9-]+)?$` +) + +func contains(haystack []string, needle string) bool { + for _, item := range haystack { + if item == needle { + return true + } + } + return false +} + +func gitVersion() (string, error) { + return runCommand("git", []string{"describe", "--tags", "--always", "--long", "--abbrev=12", "--dirty"}, nil) +} + +func runCommand(name string, args, env []string) (string, error) { + return runCommandInDir("", name, args, env) +} + +func runCommandInDir(dir, name string, args, env []string) (string, error) { + cmd := exec.Command(name, args...) + if len(env) != 0 { + cmd.Env = env + } + cmd.Dir = dir + var outb, errb bytes.Buffer + if logrus.IsLevelEnabled(logrus.DebugLevel) { + // If debug level is enabled, also write to stdout. + cmd.Stdout = io.MultiWriter(os.Stdout, &outb) + cmd.Stderr = io.MultiWriter(os.Stderr, &errb) + } else { + // Otherwise, just capture the output to return. + cmd.Stdout = io.MultiWriter(&outb) + cmd.Stderr = io.MultiWriter(&errb) + } + logrus.WithFields(logrus.Fields{ + "cmd": cmd.String(), + "dir": dir, + }).Debugf("Running %s command", name) + err := cmd.Run() + if err != nil { + err = fmt.Errorf("%s: %s", err, strings.TrimSpace(errb.String())) + } + return strings.TrimSpace(outb.String()), err +}