From 842dc8483d80577e12249a5eeb6135efbdff49fb Mon Sep 17 00:00:00 2001 From: Walt Date: Thu, 13 Jan 2022 19:52:15 -0800 Subject: [PATCH] Conditionally publish deb packages (#9496) This patch makes a couple changes: 1. deb archives are not published to apt if they're not the latest release ever 2. both rpm and deb archives are no longer published to yum / apt if they contain any pre-release indicator or build metadata 3. nothing is published if the commit isn't tagged. Contributes to https://github.com/gravitational/teleport/issues/8166 (cherry picked from commit 854053326a8d871f965bb4668b0d7c2256f2ee56) --- .drone.yml | 42 ++++--- build.assets/version-check/go.mod | 17 +++ build.assets/version-check/go.sum | 40 +++++++ build.assets/version-check/main.go | 151 ++++++++++++++++++++++++ build.assets/version-check/main_test.go | 126 ++++++++++++++++++++ 5 files changed, 357 insertions(+), 19 deletions(-) create mode 100644 build.assets/version-check/go.mod create mode 100644 build.assets/version-check/go.sum create mode 100644 build.assets/version-check/main.go create mode 100644 build.assets/version-check/main_test.go diff --git a/.drone.yml b/.drone.yml index 037ef3dd5a820..b83f8584cfaaa 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4098,6 +4098,11 @@ clone: disable: true steps: + - name: Check if commit is tagged + image: alpine + commands: + - "[ -n ${DRONE_TAG} ] || (echo 'DRONE_TAG is not set. Is the commit tagged?' && exit 1)" + - name: Download artifacts from S3 image: amazon/aws-cli environment: @@ -4251,25 +4256,14 @@ steps: # NOTE: all mandatory steps for a release promotion need to go BEFORE this # step, as there is a chance that everything afterwards will be skipped. # - # this step exits early and skips all remanining steps in the pipeline if the - # tag looks like a pre-release, to avoid publishing RPMs for pre-release builds. - - name: Determine whether RPM/DEB packages should be published to repos - image: docker + # this step exits early and skips all remaining steps in the pipeline if the + # tag looks like a pre-release, to avoid pushing pre-release RPMs and DEBs to + # our yum / apt repos. + - name: Check if tag is prerelease + image: golang:1.17-alpine commands: - - | - if [ "${DRONE_REPO}" != "gravitational/teleport" ]; then - echo "---> Not publishing ${DRONE_REPO} packages to repos" - exit 78 - fi - # length will be 0 after filtering if this is a pre-release, >0 otherwise - FILTERED_TAG_LENGTH=$(echo ${DRONE_TAG} | egrep -v '(alpha|beta|dev|rc)' | wc -c) - if [ $$FILTERED_TAG_LENGTH -eq 0 ]; then - echo "---> ${DRONE_TAG} looks like a pre-release, not publishing packages to repos" - # exit pipeline early with success status - exit 78 - else - echo "---> Publishing packages to repos for ${DRONE_TAG}" - fi + - cd build.assets/version-check + - go run . -tag ${DRONE_TAG} -check prerelease || (echo '---> Not publishing ${DRONE_REPO} packages to RPM and DEB repos' && exit 78) - name: "RPM: Download RPM repository" image: amazon/aws-cli @@ -4351,6 +4345,16 @@ steps: commands: - aws s3 sync /rpmrepo/teleport/ s3://$AWS_S3_BUCKET/teleport/ + # This step skips all remaining steps in the pipeline if the tag + # is not the highest semver *ever* released, to avoid publishing DEBs + # that would cause apt users to downgrade. For more info see: + # https://github.com/gravitational/teleport/issues/8166 + - name: Check if tag is latest + image: golang:1.17-alpine + commands: + - cd build.assets/version-check + - go run . -tag ${DRONE_TAG} -check latest || (echo '---> Not publishing ${DRONE_REPO} packages to DEB repo' && exit 78) + - name: Download DEB repo contents image: amazon/aws-cli environment: @@ -4461,6 +4465,6 @@ volumes: name: drone-s3-debrepo-pvc --- kind: signature -hmac: f29561054dffe09593ca1018f3240f6078ddff950c05672b824c78124a204e21 +hmac: fe9ab7ccd45dc1c1107db1582f8a9465c11bb298867d4f89a72509215b12c285 ... diff --git a/build.assets/version-check/go.mod b/build.assets/version-check/go.mod new file mode 100644 index 0000000000000..9e362acc3b753 --- /dev/null +++ b/build.assets/version-check/go.mod @@ -0,0 +1,17 @@ +module github.com/gravitational/teleport/build.assets/version-check + +go 1.17 + +require github.com/google/go-github/v41 v41.0.0 + +require ( + github.com/google/go-querystring v1.1.0 // indirect + github.com/gravitational/trace v1.1.15 // indirect + github.com/jonboulle/clockwork v0.2.2 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect + golang.org/x/mod v0.5.1 // indirect + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect +) diff --git a/build.assets/version-check/go.sum b/build.assets/version-check/go.sum new file mode 100644 index 0000000000000..cebafe0cb3c6b --- /dev/null +++ b/build.assets/version-check/go.sum @@ -0,0 +1,40 @@ +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg= +github.com/google/go-github/v41 v41.0.0/go.mod h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/gravitational/trace v1.1.15 h1:dfaFcARt110nCX6RSvrcRUbvRawEYAasXyCqnhXo0Xg= +github.com/gravitational/trace v1.1.15/go.mod h1:RvdOUHE4SHqR3oXlFFKnGzms8a5dugHygGw1bqDstYI= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= diff --git a/build.assets/version-check/main.go b/build.assets/version-check/main.go new file mode 100644 index 0000000000000..6a8796804dc04 --- /dev/null +++ b/build.assets/version-check/main.go @@ -0,0 +1,151 @@ +/* +Copyright 2022 Gravitational, Inc. + +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. +*/ + +// Command version-check validates that a tag is not a prerelease +// or that it is the latest version ever. version-check exits non-zero +// if tag fails this check. This allows us to avoid updating "latest" +// packages or tags when publishing releases for older branches. +package main + +import ( + "context" + "flag" + "log" + "sort" + "strings" + "time" + + "golang.org/x/mod/semver" + + "github.com/gravitational/trace" + + go_github "github.com/google/go-github/v41/github" +) + +func main() { + tag, check, err := parseFlags() + if err != nil { + log.Fatalf("Failed to parse flags: %v.", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + switch check { + case "latest": + err = checkLatest(ctx, tag, newGitHub()) + case "prerelease": + err = checkPrerelease(tag) + default: + log.Fatalf("invalid check: %v", check) + } + + if err != nil { + log.Fatalf("Check failed: %v.", err) + } +} + +func parseFlags() (string, string, error) { + tag := flag.String("tag", "", "tag to validate") + check := flag.String("check", "", "check to run [latest, prerelease]") + flag.Parse() + + if *tag == "" { + return "", "", trace.BadParameter("tag missing") + } + if *check == "" { + return "", "", trace.BadParameter("check missing") + } + switch *check { + case "latest", "prerelease": + default: + return "", "", trace.BadParameter("invalid check: %v", *check) + } + + return *tag, *check, nil +} + +func checkLatest(ctx context.Context, tag string, gh github) error { + releases, err := gh.ListReleases(ctx, "gravitational", "teleport") + if err != nil { + return trace.Wrap(err) + } + sort.SliceStable(releases, func(i int, j int) bool { + return releases[i] > releases[j] + }) + if len(releases) == 0 { + return trace.BadParameter("failed to find any releases on GitHub") + } + + if semver.Compare(tag, releases[0]) <= 0 { + return trace.BadParameter("found newer version of release, not releasing. Latest release: %v, tag: %v", releases[0], tag) + } + + return nil +} + +func checkPrerelease(tag string) error { + if semver.Prerelease(tag) != "" { // https://semver.org/#spec-item-9 + return trace.BadParameter("version is pre-release: %v", tag) + } + if strings.Contains(tag, "+") { // https://semver.org/#spec-item-10 + return trace.BadParameter("version contains build metadata: %v", tag) + } + return nil +} + +type github interface { + ListReleases(ctx context.Context, organization string, repository string) ([]string, error) +} + +type ghClient struct { + client *go_github.Client +} + +func newGitHub() *ghClient { + return &ghClient{ + client: go_github.NewClient(nil), + } +} + +func (c *ghClient) ListReleases(ctx context.Context, organization string, repository string) ([]string, error) { + var releases []string + + opt := &go_github.ListOptions{ + Page: 0, + PerPage: 100, + } + for n := 0; n < 100; n++ { + page, resp, err := c.client.Repositories.ListReleases(ctx, + organization, + repository, + opt) + if err != nil { + return nil, trace.Wrap(err) + } + + for _, p := range page { + releases = append(releases, p.GetTagName()) + } + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + return releases, nil +} diff --git a/build.assets/version-check/main_test.go b/build.assets/version-check/main_test.go new file mode 100644 index 0000000000000..a397d586c43d2 --- /dev/null +++ b/build.assets/version-check/main_test.go @@ -0,0 +1,126 @@ +/* +Copyright 2022 Gravitational, Inc. + +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" + "testing" +) + +func TestCheckPrerelease(t *testing.T) { + tests := []struct { + desc string + tag string + releases []string + wantErr bool + }{ + { + desc: "fail-rc", + tag: "v9.0.0-rc.1", + wantErr: true, + }, + { // this build was published to the deb repos on 2021-10-06 + desc: "fail-debug", + tag: "v6.2.14-debug.4", + wantErr: true, + }, + { + desc: "fail-metadata", + tag: "v8.0.7+1a2b3c4d", + wantErr: true, + }, + { + desc: "pass", + tag: "v8.0.1", + wantErr: false, + }, + } + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + err := checkPrerelease(test.tag) + if test.wantErr && err == nil { + t.Errorf("Expected an error, got nil.") + } + if !test.wantErr && err != nil { + t.Errorf("Did not expect and error, got: %v", err) + } + }) + } + +} + +func TestCheckLatest(t *testing.T) { + tests := []struct { + desc string + tag string + releases []string + wantErr bool + }{ + { + desc: "fail-old-releases", + tag: "v7.3.3", + releases: []string{ + "v8.0.0", + "v7.3.2", + "v7.0.0", + }, + wantErr: true, + }, + { + desc: "fail-same-releases", + tag: "v8.0.0", + releases: []string{ + "v8.0.0", + "v7.3.2", + "v7.0.0", + }, + wantErr: true, + }, + { + desc: "pass-new-releases", + tag: "v8.0.1", + releases: []string{ + "v8.0.0", + "v7.3.2", + "v7.0.0", + }, + wantErr: false, + }, + } + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + gh := &fakeGitHub{ + releases: test.releases, + } + err := checkLatest(context.Background(), test.tag, gh) + if test.wantErr && err == nil { + t.Errorf("Expected an error, got nil.") + } + if !test.wantErr && err != nil { + t.Errorf("Did not expect and error, got: %v", err) + } + }) + } + +} + +type fakeGitHub struct { + releases []string +} + +func (f *fakeGitHub) ListReleases(ctx context.Context, organization string, repository string) ([]string, error) { + return f.releases, nil +}