From 08e7e342a0e08caaa4a3739b89c77f62c70fbe42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20H=C3=B6rl?= Date: Tue, 21 Jan 2020 12:15:48 +0000 Subject: [PATCH 01/10] [path-announce] Use secureEnv for secrets We encrypt the secrets GITHUB_TOKEN & SENDGRID_API_KEY with the cloud KMS and use those in GCB. This means, that those envs won't get logged in the GCB console. --- announce-patch | 4 +--- gcb/patch-announce/cloudbuild.yaml | 29 +++++++++++++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/announce-patch b/announce-patch index 35c9f40e53c..b9e185783f4 100755 --- a/announce-patch +++ b/announce-patch @@ -21,7 +21,7 @@ PROG=${0##*/} #+ $PROG - generate and send the patch release announcement mail #+ #+ SYNOPSIS -#+ $PROG --github-token=[gh-token] --sendgrid-api-key=[sendgrid-key] \ +#+ $PROG \ #+ --freeze-date=[freeze-date] --cut-date=[cut-date] \ #+ --from-name=[sender-name] --from-email=[sender-email] \ #+ [release-branch] @@ -119,8 +119,6 @@ main() { # mandatory flags subst+=( "_K8S_GIT_BRANCH=${branch_name}" ) - subst+=( "_GITHUB_TOKEN=$( flag_or_env_or_default 'github_token' )" ) - subst+=( "_SENDGRID_API_KEY=$( flag_or_env_or_default 'sendgrid_api_key' )" ) subst+=( "_FREEZE_DATE=$( flag_or_env_or_default 'freeze_date' )" ) subst+=( "_CUT_DATE=$( flag_or_env_or_default 'cut_date' )" ) subst+=( "_FROM_NAME=$( flag_or_env_or_default 'from_name' )" ) diff --git a/gcb/patch-announce/cloudbuild.yaml b/gcb/patch-announce/cloudbuild.yaml index f7aeb33b4cd..46f7d45b508 100644 --- a/gcb/patch-announce/cloudbuild.yaml +++ b/gcb/patch-announce/cloudbuild.yaml @@ -1,5 +1,25 @@ timeout: 1200s +#### SECURITY NOTICE #### +# Google Cloud Build (GCB) supports the usage of secrets for build requests. +# Secrets appear within GCB configs as base64-encoded strings. +# These secrets are GCP Cloud KMS-encrypted and cannot be decrypted by any human or system +# outside of GCP Cloud KMS for the GCP project this encrypted resource was created for. +# Seeing the base64-encoded encrypted blob here is not a security event for the project. +# +# More details on using encrypted resources on Google Cloud Build can be found here: +# https://cloud.google.com/cloud-build/docs/securing-builds/use-encrypted-secrets-credentials +# +# (Please do not remove this security notice.) +secrets: +- kmsKeyName: projects/kubernetes-release-test/locations/global/keyRings/anago/cryptoKeys/k8s-release-robot + secretEnv: + GITHUB_TOKEN: CiQAveh8wGJqpEkcVluO3tBntlehynOxiOPDD9u1XCONx3vuozoSUgCMoh/IJuNkqhcDTP2om2tyOStft8myMSrvnGd7NvTo+H+fI0EkLMNzkVOHxl/C0piktBm74uN70QVE+e4TTa9wwA//qpiSm/UuqYmEYeMIpyY= +# TODO: move to the kubernetes-release-test +- kmsKeyName: projects/cf-london-servces-k8s/locations/global/keyRings/hhorl-k8s-release/cryptoKeys/sendgrid + secretEnv: + SENDGRID_API_KEY: CiQAkn5y0WUR0kZIqj1v4UW2McFdlceel2zptSa468+Ir/gDeh0SbwDMTYRJdFtqt2x2hGPN7oOoMs2lCLFoY+oh4Mb1b/nQehooMYQuNwupxOpnwE+d/GbEFhwtMg9wbA5zhOMYPe0OLsSAXvt4w/IC1Z701Ev3CMdT6ZjicillZoBlFYhVlFPQLWcm8jtobdQcDj91eg== + steps: - id: clone-k8s waitFor: [ '-' ] @@ -21,8 +41,6 @@ steps: - id: prepare-and-send name: "gcr.io/${PROJECT_ID}/k8s-cloud-builder" env: - - "GITHUB_TOKEN=${_GITHUB_TOKEN}" - - "SENDGRID_API_KEY=${_SENDGRID_API_KEY}" - "REL_MGR_NAME=${_REL_MGR_NAME}" - "REL_MGR_EMAIL=${_REL_MGR_EMAIL}" - "REL_MGR_SLACK=${_REL_MGR_SLACK}" @@ -31,16 +49,15 @@ steps: - "FREEZE_DATE=${_FREEZE_DATE}" - "CUT_DATE=${_CUT_DATE}" - "RUN_TYPE=${_RUN_TYPE}" + secretEnv: + - GITHUB_TOKEN + - SENDGRID_API_KEY args: - go/src/k8s.io/release/patch-release/announce substitutions: # The branch of k/kubernetes to check out, the branch to announce a patch release for (e.g.: release-1.15) _K8S_GIT_BRANCH: null - # A github token, used to collect the changelog - _GITHUB_TOKEN: null - # API key for sendgrid to send out mails - _SENDGRID_API_KEY: null # Date of CP freeze, ISO 8601 (e.g.: 2019-12-06) _FREEZE_DATE: null # Date of planned cut, ISO 8601 (e.g.: 2019-12-11) From 786b7a06381e77975fd180ad30979570dfb5b7f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20H=C3=B6rl?= Date: Tue, 21 Jan 2020 12:17:47 +0000 Subject: [PATCH 02/10] [patch-announce] Allow setting the GCB project --- announce-patch | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/announce-patch b/announce-patch index b9e185783f4..c84615a62ce 100755 --- a/announce-patch +++ b/announce-patch @@ -63,6 +63,7 @@ set -e set -o pipefail readonly PROG="${0##*/}" +readonly PROJECT="${PROJECT:-kubernetes-release-test}" readonly BASE_ROOT="$(dirname "$(readlink -e "${BASH_SOURCE[0]}" 2>&1)")" # shellcheck source=./lib/common.sh @@ -141,7 +142,7 @@ main() { opts+=( '--async' ) fi - gcloud builds submit \ + gcloud "--project=${PROJECT}" builds submit \ "${opts[@]}" \ --substitutions "$( common::join ',' "${subst[@]}" )" } From 406b14ac68d3903a3447c947d65c31ce21cc3447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20H=C3=B6rl?= Date: Tue, 21 Jan 2020 12:20:07 +0000 Subject: [PATCH 03/10] [patch-announce] Update inline docs --- announce-patch | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/announce-patch b/announce-patch index c84615a62ce..0e4412fede6 100755 --- a/announce-patch +++ b/announce-patch @@ -37,26 +37,32 @@ PROG=${0##*/} #+ The mail is sent out via sendgrid, GCB does not allow to send mail directly. #+ #+ OPTIONS -#+ release-branch - The branch we want to cut from, e.g.: 'release-1.15'. -#+ --nomock - By default the mail will be sent to the mail address -#+ that is set as the sender (--from-email). When this -#+ flag is set, we send it to the kubernetes mailing -#+ lists. -#+ --github-token - The github token that will be used for generating the -#+ changelog. -#+ --sendgrid-api-key - The API key for sendgrid to send out the email. -#+ --freeze-date - The date we will freeze the branch and will not accept -#+ cherry-picks anymore. -#+ --cut-date - The date we will cut and publish the release. -#+ --from-name - The sender's name. -#+ --from-email - The sender's email address. Will also be used as a -#+ receiver when not in nomock mode. -#+ [--tail] - Stays attached to the cloud build process and streams -#+ in the logs -#+ [--k8s-git-url] - The git URL to clone kubernetes/kubernetes from. -#+ [--release-git-url] - The git URL to clone kubernetes/release from. -#+ [--release-git-branch] - The branch of kubernetes/release to use. +#+ release-branch - The branch we want to cut from, e.g.: 'release-1.15'. +#+ --nomock - By default the mail will be sent to the mail address +#+ that is set as the sender (--from-email). When this +#+ flag is set, we send it to the kubernetes mailing +#+ lists. +#+ --freeze-date - The date we will freeze the branch and will not accept +#+ cherry-picks anymore. +#+ --cut-date - The date we will cut and publish the release. +#+ --from-name - The sender's name. +#+ --from-email - The sender's email address. Will also be used as a +#+ receiver when not in nomock mode. +#+ [--tail] - Stays attached to the cloud build process and streams +#+ in the logs +#+ [--k8s-git-url] - The git URL to clone kubernetes/kubernetes from. +#+ [--release-git-url] - The git URL to clone kubernetes/release from. +#+ [--release-git-branch] - The branch of kubernetes/release to use. #+ +#+ ENVIRONMENT +#+ The GCP project can be changed by setting the PROJECT environment +#+ variable. It defaults to 'kubernetes-release-test'. +#+ +#+ All command line flags can also be provided by setting an environment +#+ variable, e.g. instead of using `--freeze-date` flag you can also use +#+ the environment variable `FREEZE_DATE`. This works for all flags but not +#+ for the release-branch argument, this one must explicitly specified as a +#+ command line argument. set -e # set -u @@ -71,7 +77,7 @@ source "${BASE_ROOT}/lib/common.sh" # For some reason, using $FLAG_xxx seems to replace ' ' with '\n'. Sendgrid # does not really like names with newlines in it and treats them as two -# spearate recipients. +# separate recipients. # Newlines do not make too much sense for other variables, so we globally # replace newlines with spaces. replace_nl() { From 0eef4d5a50a04af23555a569c08ce390b98a94e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20H=C3=B6rl?= Date: Tue, 21 Jan 2020 12:20:41 +0000 Subject: [PATCH 04/10] [patch-announce] Remove duplicate setup --- announce-patch | 2 -- 1 file changed, 2 deletions(-) diff --git a/announce-patch b/announce-patch index 0e4412fede6..40f6832a7ad 100755 --- a/announce-patch +++ b/announce-patch @@ -15,8 +15,6 @@ # limitations under the License. # -PROG=${0##*/} - #+ NAME #+ $PROG - generate and send the patch release announcement mail #+ From 7db9810dc16e6b1c2901c1bc47e2ec413fe7f9ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20H=C3=B6rl?= Date: Thu, 23 Jan 2020 12:02:03 +0000 Subject: [PATCH 05/10] Make log setup available to sub commands By moving the log setup from `PreRunE` to `PersistentPreRunE` the log setup also run for and therefore configures logging for sub commands. --- cmd/krel/cmd/root.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/krel/cmd/root.go b/cmd/krel/cmd/root.go index 2e90eb90650..7b27c71d06b 100644 --- a/cmd/krel/cmd/root.go +++ b/cmd/krel/cmd/root.go @@ -28,9 +28,9 @@ import ( // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "krel", - Short: "krel", - PreRunE: initLogging, + Use: "krel", + Short: "krel", + PersistentPreRunE: initLogging, } type rootOptions struct { From 7624e3b87467c5034eb00667fe80be7d36500fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20H=C3=B6rl?= Date: Mon, 27 Jan 2020 13:57:46 +0000 Subject: [PATCH 06/10] Disable gocritic for all fakes --- .golangci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.golangci.yml b/.golangci.yml index 92e25f7923f..c7716266dda 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,7 +4,9 @@ run: deadline: 5m issues: exclude-rules: - - path: fake_client\.go + # counterfeiter fakes are usually named 'fake_.go' + # TODO: figure out why golangci-lint does not treat counterfeiter files as generated files + - path: fake_.*\.go linters: - gocritic - dupl From 7f7ec9d488b04a5da6a01e108ce28e55ad97af65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20H=C3=B6rl?= Date: Thu, 23 Jan 2020 13:29:24 +0000 Subject: [PATCH 07/10] [patch-announce] Port patch announcement mailer to go Some parts (release notes, workspace, pandoc) currently still shell out. We can change that gradually by change the implementations of the respective structs. --- BUILD.bazel | 1 + cmd/krel/cmd/BUILD.bazel | 22 +- cmd/krel/cmd/patch-announce.go | 98 +++ go.mod | 3 + go.sum | 19 + pkg/log/BUILD.bazel | 5 +- pkg/log/log.go | 83 ++ pkg/patch/BUILD.bazel | 43 + pkg/patch/announce.go | 283 +++++++ pkg/patch/announce_templates.go | 58 ++ pkg/patch/announce_test.go | 267 +++++++ pkg/patch/internal/BUILD.bazel | 57 ++ pkg/patch/internal/exec.go | 97 +++ pkg/patch/internal/formatter.go | 65 ++ pkg/patch/internal/formatter_test.go | 88 +++ pkg/patch/internal/internalfakes/BUILD.bazel | 35 + pkg/patch/internal/internalfakes/fake_cmd.go | 746 ++++++++++++++++++ .../internal/internalfakes/fake_formatter.go | 133 ++++ .../internalfakes/fake_mail_sender.go | 276 +++++++ .../internalfakes/fake_release_noter.go | 122 +++ .../internalfakes/fake_sendgrid_client.go | 133 ++++ .../internal/internalfakes/fake_workspace.go | 122 +++ pkg/patch/internal/mail_sender.go | 129 +++ pkg/patch/internal/mail_sender_test.go | 183 +++++ pkg/patch/internal/release_notes.go | 73 ++ pkg/patch/internal/release_notes_test.go | 104 +++ pkg/patch/internal/testing/BUILD.bazel | 23 + pkg/patch/internal/testing/testing.go | 50 ++ pkg/patch/internal/workspace.go | 67 ++ pkg/patch/internal/workspace_test.go | 86 ++ repos.bzl | 28 +- 31 files changed, 3486 insertions(+), 13 deletions(-) create mode 100644 cmd/krel/cmd/patch-announce.go create mode 100644 pkg/log/log.go create mode 100644 pkg/patch/BUILD.bazel create mode 100644 pkg/patch/announce.go create mode 100644 pkg/patch/announce_templates.go create mode 100644 pkg/patch/announce_test.go create mode 100644 pkg/patch/internal/BUILD.bazel create mode 100644 pkg/patch/internal/exec.go create mode 100644 pkg/patch/internal/formatter.go create mode 100644 pkg/patch/internal/formatter_test.go create mode 100644 pkg/patch/internal/internalfakes/BUILD.bazel create mode 100644 pkg/patch/internal/internalfakes/fake_cmd.go create mode 100644 pkg/patch/internal/internalfakes/fake_formatter.go create mode 100644 pkg/patch/internal/internalfakes/fake_mail_sender.go create mode 100644 pkg/patch/internal/internalfakes/fake_release_noter.go create mode 100644 pkg/patch/internal/internalfakes/fake_sendgrid_client.go create mode 100644 pkg/patch/internal/internalfakes/fake_workspace.go create mode 100644 pkg/patch/internal/mail_sender.go create mode 100644 pkg/patch/internal/mail_sender_test.go create mode 100644 pkg/patch/internal/release_notes.go create mode 100644 pkg/patch/internal/release_notes_test.go create mode 100644 pkg/patch/internal/testing/BUILD.bazel create mode 100644 pkg/patch/internal/testing/testing.go create mode 100644 pkg/patch/internal/workspace.go create mode 100644 pkg/patch/internal/workspace_test.go diff --git a/BUILD.bazel b/BUILD.bazel index 54adfb968bf..ea3a18408ad 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -37,6 +37,7 @@ filegroup( "//pkg/kubepkg:all-srcs", "//pkg/log:all-srcs", "//pkg/notes:all-srcs", + "//pkg/patch:all-srcs", "//pkg/release:all-srcs", "//pkg/util:all-srcs", "//pkg/version:all-srcs", diff --git a/cmd/krel/cmd/BUILD.bazel b/cmd/krel/cmd/BUILD.bazel index 6db9bec2a07..94f3e913d72 100644 --- a/cmd/krel/cmd/BUILD.bazel +++ b/cmd/krel/cmd/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "changelog.go", "ff.go", + "patch-announce.go", "push.go", "root.go", "version.go", @@ -16,6 +17,7 @@ go_library( "//pkg/log:go_default_library", "//pkg/notes:go_default_library", "//pkg/notes/options:go_default_library", + "//pkg/patch:go_default_library", "//pkg/release:go_default_library", "//pkg/util:go_default_library", "//pkg/version:go_default_library", @@ -28,6 +30,16 @@ go_library( ], ) +go_test( + name = "go_default_test", + srcs = [ + "changelog_test.go", + "root_test.go", + ], + embed = [":go_default_library"], + deps = ["@com_github_stretchr_testify//require:go_default_library"], +) + filegroup( name = "package-srcs", srcs = glob(["**"]), @@ -41,13 +53,3 @@ filegroup( tags = ["automanaged"], visibility = ["//visibility:public"], ) - -go_test( - name = "go_default_test", - srcs = [ - "changelog_test.go", - "root_test.go", - ], - embed = [":go_default_library"], - deps = ["@com_github_stretchr_testify//require:go_default_library"], -) diff --git a/cmd/krel/cmd/patch-announce.go b/cmd/krel/cmd/patch-announce.go new file mode 100644 index 00000000000..682c22e6410 --- /dev/null +++ b/cmd/krel/cmd/patch-announce.go @@ -0,0 +1,98 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 cmd + +import ( + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "k8s.io/release/pkg/log" + "k8s.io/release/pkg/patch" + "k8s.io/release/pkg/util" +) + +// slap the subcommand onto the parent/root +func init() { + cmd := patchAnnounceCommand() + rootCmd.AddCommand(cmd) +} + +func patchAnnounceCommand() *cobra.Command { + opts := patch.AnnounceOptions{} + + cmd := &cobra.Command{ + Use: "patch-announce", + Short: "Send out patch release announcement mails", + SilenceUsage: true, + SilenceErrors: true, + Args: cobra.MaximumNArgs(0), // no additional/positional args allowed + } + + cobra.OnInitialize(initConfig) // ? + + // setup local flags + cmd.PersistentFlags().StringVarP(&opts.SenderName, "sender-name", "n", "", "email sender's name") + cmd.PersistentFlags().StringVarP(&opts.SenderEmail, "sender-email", "e", "", "email sender's address") + cmd.PersistentFlags().StringVarP(&opts.FreezeDate, "freeze-date", "f", "", "date when no CPs are allowed anymore") + cmd.PersistentFlags().StringVarP(&opts.CutDate, "cut-date", "c", "", "date when the patch release is planned to be cut") + cmd.PersistentFlags().StringVarP(&opts.ReleaseRepoPath, "release-repo", "r", "./release", "local path of the k/release checkout") + + // TODO: figure out, how we can read env vars and also be able to set the flags to required in a cobra-native way + cmd.PersistentFlags().StringVarP(&opts.SendgridAPIKey, "sendgrid-api-key", "s", util.EnvDefault("SENDGRID_API_KEY", ""), "API key for sendgrid") + cmd.PersistentFlags().StringVarP(&opts.GithubToken, "github-token", "g", util.EnvDefault("GITHUB_TOKEN", ""), "a GitHub token, used r/o for generating the release notes") + + cmd.PreRunE = func(cmd *cobra.Command, _ []string) error { + // TODO: make github-token & sendgrid-api-key required too + if err := setFlagsRequired(cmd, "sender-name", "sender-email", "freeze-date", "cut-date"); err != nil { + return err + } + + var err error + if opts.Nomock, err = cmd.Flags().GetBool("nomock"); err != nil { + return err + } + if opts.K8sRepoPath, err = cmd.Flags().GetString("repo"); err != nil { + return err + } + return nil + } + + cmd.RunE = func(cmd *cobra.Command, args []string) error { + // Get the global logger, add the command's name as an initial tracing + // field and use that from here on + localLogger := logrus.NewEntry(logrus.StandardLogger()) + logger := log.AddTracePath(localLogger, cmd.Name()).WithField("mock", !opts.Nomock) + + announcer := &patch.Announcer{ + Opts: opts, + } + announcer.SetLogger(logger, "announcer") + + logger.Debug("run announcer") + return announcer.Run() + } + + return cmd +} + +func setFlagsRequired(cmd *cobra.Command, flags ...string) error { + for _, f := range flags { + if err := cmd.MarkPersistentFlagRequired(f); err != nil { + return err + } + } + return nil +} diff --git a/go.mod b/go.mod index 52876b32206..1668094958a 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,8 @@ require ( github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 github.com/pkg/errors v0.8.1 github.com/psampaz/go-mod-outdated v0.5.0 + github.com/sendgrid/rest v2.4.1+incompatible + github.com/sendgrid/sendgrid-go v3.5.0+incompatible github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/sirupsen/logrus v1.4.2 github.com/spf13/cobra v0.0.5 @@ -20,4 +22,5 @@ require ( golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d gopkg.in/russross/blackfriday.v2 v2.0.0 gopkg.in/src-d/go-git.v4 v4.13.1 + k8s.io/utils v0.0.0-20200117235808-5f6fbceb4c31 ) diff --git a/go.sum b/go.sum index b791564a634..e18200897ee 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,7 @@ github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrU github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= @@ -63,11 +64,14 @@ github.com/go-critic/go-critic v0.4.1/go.mod h1:7/14rZGnZbY6E38VEGk2kVhoq6itzc1E github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0= github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= +github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g= github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= @@ -95,6 +99,7 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b h1:ekuhfTjngPhisSjOJ0QWKpPQE8/rbknHaes6WVJj5Hw= github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= @@ -210,6 +215,7 @@ github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM52 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -290,6 +296,10 @@ github.com/sclevine/spec v1.2.0 h1:1Jwdf9jSfDl9NVmt8ndHqbTZ7XCCPbh1jI3hkDBHVYA= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= github.com/securego/gosec v0.0.0-20200103095621-79fbf3af8d83 h1:AtnWoOvTioyDXFvu96MWEeE8qj4COSQnJogzLy/u41A= github.com/securego/gosec v0.0.0-20200103095621-79fbf3af8d83/go.mod h1:vvbZ2Ae7AzSq3/kywjUDxSNq2SJ27RxCz2un0H3ePqE= +github.com/sendgrid/rest v2.4.1+incompatible h1:HDib/5xzQREPq34lN3YMhQtMkdXxS/qLp5G3k9a5++4= +github.com/sendgrid/rest v2.4.1+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= +github.com/sendgrid/sendgrid-go v3.5.0+incompatible h1:kosbgHyNVYVaqECDYvFVLVD9nvThweBd6xp7vaCT3GI= +github.com/sendgrid/sendgrid-go v3.5.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc= @@ -313,6 +323,8 @@ github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34c github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= @@ -368,13 +380,16 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.1 h1:8dP3SGL7MPB94crU3bEPplMPe83FI4EouesJUeFHv50= go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= @@ -514,6 +529,7 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.1 h1:q4XQuHFC6I28BKZpo6IYyb3mNO+l7lSOxRuYTCiDfXk= @@ -559,7 +575,10 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= k8s.io/apimachinery v0.0.0-20190816221834-a9f1d8a9c101/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/utils v0.0.0-20200117235808-5f6fbceb4c31 h1:KCcLuc/HD1RogJgEbZi9ObRuLv1bgiRCfAbidLKrUpg= +k8s.io/utils v0.0.0-20200117235808-5f6fbceb4c31/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= diff --git a/pkg/log/BUILD.bazel b/pkg/log/BUILD.bazel index 0f973aa44d5..15d56871a4a 100644 --- a/pkg/log/BUILD.bazel +++ b/pkg/log/BUILD.bazel @@ -2,7 +2,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", - srcs = ["hooks.go"], + srcs = [ + "hooks.go", + "log.go", + ], importpath = "k8s.io/release/pkg/log", visibility = ["//visibility:public"], deps = ["@com_github_sirupsen_logrus//:go_default_library"], diff --git a/pkg/log/log.go b/pkg/log/log.go new file mode 100644 index 00000000000..5b3ae8d761e --- /dev/null +++ b/pkg/log/log.go @@ -0,0 +1,83 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 log + +import ( + "io/ioutil" + "strings" + + "github.com/sirupsen/logrus" +) + +const ( + logTraceKey = "trace" + logTraceSep = "." +) + +// AddTracePath adds a path element to the logrus entry's field 'trace'. This +// is meant to be done everytime you hand off a logger/entry to a different +// component to have a clear trace how we ended up here. When logs are emitted +// by this logger entry, the field might look something like: +// trace=patch-announce.announcer.release-noter +func AddTracePath(l *logrus.Entry, newPathElement string) *logrus.Entry { + if newPathElement == "" { + // get a copy with the same data, err, context, ... + return l.WithFields(l.Data) + } + + newPath := "" + + curPathInt, ok := l.Data[logTraceKey] + if !ok { + newPath = newPathElement + } else { + curPath, ok := curPathInt.(string) + if !ok { + newPath = "" + logTraceSep + newPathElement + } else { + newPath = curPath + logTraceSep + newPathElement + } + } + + return l.WithField(logTraceKey, newPath) +} + +func NullLogger() *logrus.Entry { + logger := logrus.New() + logger.SetOutput(ioutil.Discard) + logger.SetLevel(logrus.PanicLevel) + return logrus.NewEntry(logger) +} + +// Logger can be embedded in other struct to enable logging and keep the +// zero-value of the struct useful. +// Examples of the usage can be found in k8s.io/release/pkg/patch/... +type Mixin struct { + logger *logrus.Entry +} + +func (l *Mixin) Logger() *logrus.Entry { + if l.logger == nil { + l.logger = NullLogger() + } + return l.logger +} + +func (l *Mixin) SetLogger(logger *logrus.Entry, tracePaths ...string) { + p := strings.Join(tracePaths, logTraceSep) + l.logger = AddTracePath(logger, p) +} diff --git a/pkg/patch/BUILD.bazel b/pkg/patch/BUILD.bazel new file mode 100644 index 00000000000..8c1f5a1f1c0 --- /dev/null +++ b/pkg/patch/BUILD.bazel @@ -0,0 +1,43 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "announce.go", + "announce_templates.go", + ], + importpath = "k8s.io/release/pkg/patch", + visibility = ["//visibility:public"], + deps = [ + "//pkg/log:go_default_library", + "//pkg/patch/internal:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["announce_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/patch/internal/internalfakes:go_default_library", + "//pkg/patch/internal/testing:go_default_library", + "@com_github_stretchr_testify//require:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//pkg/patch/internal:all-srcs", + ], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/patch/announce.go b/pkg/patch/announce.go new file mode 100644 index 00000000000..5b7683877b8 --- /dev/null +++ b/pkg/patch/announce.go @@ -0,0 +1,283 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 patch + +import ( + "bytes" + "fmt" + "strings" + "text/template" + "time" + + "k8s.io/release/pkg/log" + "k8s.io/release/pkg/patch/internal" +) + +type AnnounceOptions struct { + SenderEmail string + SenderName string + FreezeDate string + CutDate string + Nomock bool + K8sRepoPath string + ReleaseRepoPath string + SendgridAPIKey string + GithubToken string +} + +type Announcer struct { + log.Mixin + + Opts AnnounceOptions + ReleaseNoter ReleaseNoter + MailSender MailSender + Formatter Formatter + Workspace Workspace +} + +const ( + KDevName = "Kubernetes developer/contributor discussion" + KDevEmail = "kubernetes-dev@googlegroups.com" + KDevAnnounceName = "kubernetes-dev-announce" + KDevAnnounceEmail = "kubernetes-dev-announce@googlegroups.com" + + ReleaseManagerName = "Kubernetes Release Managers" + ReleaseManagerTag = "@kubernetes/patch-release-team" + ReleaseManagerEmail = "release-managers@kubernetes.io" + ReleaseManagerSlackChannel = "release-management" +) + +func (a *Announcer) Run() error { + ver, err := a.getUpcomingVer() + if err != nil { + a.Logger().WithError(err).Debug("getting upcoming version failed") + return err + } + + a.Logger().Infof("Running for %q", ver) + + freezeDate, err := time.Parse(dateLayoutISO8601, a.Opts.FreezeDate) + if err != nil { + a.Logger().WithError(err).Debug("parsing freeze date failed") + return err + } + cutDate, err := time.Parse(dateLayoutISO8601, a.Opts.CutDate) + if err != nil { + a.Logger().WithError(err).Debug("parsing cut date failed") + return err + } + + subject := "Kubernetes " + ver + " cut planned for " + dateFormatHuman(cutDate) + + head, err := a.getMailHead(ver, freezeDate, cutDate) + if err != nil { + a.Logger().WithError(err).Debug("getting mail head failed") + return err + } + + relNotes, err := a.getReleaseNotes() + if err != nil { + a.Logger().WithError(err).Debug("getting release notes failed") + return err + } + + body, err := a.formatAsHTML(subject, head, relNotes) + if err != nil { + a.Logger().WithError(err).Debug("formatting mail as html failed") + return err + } + + a.Logger(). + WithField("emailBody", body). + WithField("emailSubject", subject). + Trace("email content generated") + + if err := a.sendMail(body, subject); err != nil { + a.Logger().WithError(err).Debug("sending mail failed") + return err + } + + return nil +} + +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate + +//counterfeiter:generate -o internal/internalfakes/fake_mail_sender.go . MailSender +type MailSender interface { + SetSender(name, email string) error + SetRecipients(recipients ...string) error + Send(content, subject string) error +} + +//counterfeiter:generate -o internal/internalfakes/fake_release_noter.go . ReleaseNoter +type ReleaseNoter interface { + GetMarkdown() (relnotes string, err error) +} + +//counterfeiter:generate -o internal/internalfakes/fake_formatter.go . Formatter +type Formatter interface { + MarkdownToHTML(markdown, title string) (html string, err error) +} + +//counterfeiter:generate -o internal/internalfakes/fake_workspace.go . Workspace +type Workspace interface { + Status() (status map[string]string, err error) +} + +// The date/time layouts to parse and format `time.Time`s +// Reference date is 'Mon Jan 2 15:04:05 MST 2006' +const ( + dateLayoutISO8601 = "2006-01-02" + dateLayoutDayISO8601 = "Monday, 2006-01-02" +) + +func dateFormatHuman(t time.Time) string { + return t.Format(dateLayoutDayISO8601) +} + +func loadTemplate(tmplString string) (*template.Template, error) { + funcs := template.FuncMap{ + "dateFormatHuman": dateFormatHuman, + "code": func(s string) string { return "`" + s + "`" }, + "codeBlock": func(s string) string { return "```\n" + s + "\n```" }, + "link": func(n, t string) string { return "[" + n + "](" + t + ")" }, + } + return template.New("main").Funcs(funcs).Parse(tmplString) +} + +func (a *Announcer) getMailHead(version string, freezeDate, cutDate time.Time) (string, error) { + tmpl, err := loadTemplate(MailHeadMarkdown) + if err != nil { + return "", err + } + + templated := &bytes.Buffer{} + templateData := struct { + DateFreeze time.Time + DateCut time.Time + Version string + ReleaseManagerName string + ReleaseManagerEmail string + ReleaseManagerTag string + ReleaseManagerSlackChannel string + }{ + DateFreeze: freezeDate, + DateCut: cutDate, + Version: version, + ReleaseManagerName: ReleaseManagerName, + ReleaseManagerTag: ReleaseManagerTag, + ReleaseManagerEmail: ReleaseManagerEmail, + ReleaseManagerSlackChannel: ReleaseManagerSlackChannel, + } + + err = tmpl.Execute(templated, templateData) + if err != nil { + return "", err + } + + return templated.String(), nil +} + +func (a *Announcer) getUpcomingVer() (string, error) { + if a.Workspace == nil { + w := &internal.Workspace{ + K8sRepoPath: a.Opts.K8sRepoPath, + } + w.SetLogger(a.Logger(), "workspace") + a.Workspace = w + a.Logger().Debug("new workspace created") + } + + status, err := a.Workspace.Status() + if err != nil { + return "", err + } + + // v1.18.0-alpha.2.121+e92a7cfd2a82cd-dirty + gitVersion, ok := status["gitVersion"] + if !ok { + return "", fmt.Errorf("workspace status has no field 'gitVersion': %q", status) + } + + i := strings.IndexRune(gitVersion, '-') + if i < 0 { + return "", fmt.Errorf("cannot extract upcoming version from gitVersion %q", gitVersion) + } + + return gitVersion[:i], nil +} + +func (a *Announcer) formatAsHTML(title string, parts ...string) (string, error) { + if a.Formatter == nil { + f := &internal.Formatter{ + Style: MailStyle, + } + f.SetLogger(a.Logger(), "formatter") + a.Formatter = f + a.Logger().Debug("new formatter instance created") + } + + html, err := a.Formatter.MarkdownToHTML(strings.Join(parts, hr), title) + if err != nil { + return "", err + } + + return html, nil +} + +func (a *Announcer) sendMail(mail, subject string) error { + if a.MailSender == nil { + ms := &internal.MailSender{ + APIKey: a.Opts.SendgridAPIKey, + } + ms.SetLogger(a.Logger(), "mail-sender") + a.MailSender = ms + a.Logger().Debug("new instance of mail sender created") + } + + if err := a.MailSender.SetSender(a.Opts.SenderName, a.Opts.SenderEmail); err != nil { + return err + } + + receipients := []string{a.Opts.SenderName, a.Opts.SenderEmail} + if a.Opts.Nomock { + receipients = []string{KDevName, KDevEmail, KDevAnnounceName, KDevAnnounceEmail} + a.Logger().WithField("receipients", receipients).Info("setting mail recipients in nomock mode") + } + if err := a.MailSender.SetRecipients(receipients...); err != nil { + return err + } + + a.Logger().Debug("calling the mail sender") + return a.MailSender.Send(mail, subject) +} + +func (a *Announcer) getReleaseNotes() (string, error) { + if a.ReleaseNoter == nil { + rn := &internal.ReleaseNoter{ + K8sDir: a.Opts.K8sRepoPath, + ReleaseToolsDir: a.Opts.ReleaseRepoPath, + GithubToken: a.Opts.GithubToken, + } + rn.SetLogger(a.Logger(), "release-noter") + a.ReleaseNoter = rn + a.Logger().Debug("new instance of release-noter created") + } + + a.Logger().Debug("calling relase note generator") + return a.ReleaseNoter.GetMarkdown() +} diff --git a/pkg/patch/announce_templates.go b/pkg/patch/announce_templates.go new file mode 100644 index 00000000000..50bf24f4be3 --- /dev/null +++ b/pkg/patch/announce_templates.go @@ -0,0 +1,58 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 patch + +const hr = "\n\n----\n\n" + +const MailHeadMarkdown = ` +Below is a draft of the generated changelog for {{ .Version }}. If you submitted a cherrypick, please make sure it's listed and has an **accurate release note**. + +If you have a pending cherrypick for {{ .Version }}, make sure it merges by end of day on **{{ dateFormatHuman .DateFreeze }}**. +Please tag {{ code .ReleaseManagerTag }} on the GitHub issue/PR, email {{ link .ReleaseManagerName (print "mailto:" .ReleaseManagerEmail) }}, or reach out in {{ link (print "#" .ReleaseManagerSlackChannel) (printf "https://kubernetes.slack.com/messages/%s/" .ReleaseManagerSlackChannel) }} on Slack if your cherrypick appears to be blocked on something out of your control. + +If you've already spoken to the patch release team about PRs that are not yet merged or listed below, don't worry, we're tracking them. +` + +const MailStyle = ` + +` diff --git a/pkg/patch/announce_test.go b/pkg/patch/announce_test.go new file mode 100644 index 00000000000..269e7bdbff5 --- /dev/null +++ b/pkg/patch/announce_test.go @@ -0,0 +1,267 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 patch_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/release/pkg/patch" + "k8s.io/release/pkg/patch/internal/internalfakes" + it "k8s.io/release/pkg/patch/internal/testing" +) + +type opts = patch.AnnounceOptions + +type testCase struct { + releaseNoterErr error + releaseNoterOutput string + mailerSenderErr error + mailerSetRecipientsErr error + mailerSetSenderErr error + formatterOutput string + formattterErr error + workspaceStatus map[string]string + workspaceErr error + opts opts + + expectedReleaseNoterNOTToBeCalled bool + expectedFormatterNOTToBeCalled bool + expectedMailerNOTToBeCalled bool + + expectedFormatterMarkdown []*regexp.Regexp + expectedMailerBody []*regexp.Regexp + expectedFormatterSubject []*regexp.Regexp + expectedMailerSubject []*regexp.Regexp + expectedErrMsg string + expectedRecipients *[]string + expectedSender [2]string +} + +func getOpts(funcs ...func(*opts)) opts { + o := patch.AnnounceOptions{ + FreezeDate: "2010-11-05", + CutDate: "2010-11-12", + } + for _, f := range funcs { + f(&o) + } + return o +} + +func res(ss ...string) []*regexp.Regexp { + res := make([]*regexp.Regexp, len(ss)) + for i, s := range ss { + res[i] = regexp.MustCompile(s) + } + return res +} + +func TestAnnounce(t *testing.T) { + t.Parallel() + + tests := map[string]testCase{ + "happy path": { + opts: getOpts(), + workspaceStatus: map[string]string{"gitVersion": "v1.13.10-beta.0-16-g48844ef5e7"}, + releaseNoterOutput: "some release notes content", + expectedFormatterSubject: res("^Kubernetes v1.13.10 cut planned for Friday, 2010-11-12$"), + expectedFormatterMarkdown: res( + "v1.13.10", + "Friday, 2010-11-05", + "some release notes content", + ), + formatterOutput: "some formatted html", + expectedMailerBody: res("^some formatted html$"), + expectedMailerSubject: res("^Kubernetes v1.13.10 cut planned for Friday, 2010-11-12$"), + }, + "when getting the workspace status returns an error, the error bubbles up and the mail is never sent": { + workspaceErr: fmt.Errorf("git describe err"), + expectedErrMsg: "git describe err", + expectedReleaseNoterNOTToBeCalled: true, + expectedMailerNOTToBeCalled: true, + expectedFormatterNOTToBeCalled: true, + }, + "when the workspace status does not hold a git version, the error bubbles up and the mail is never sent": { + workspaceStatus: map[string]string{}, + expectedErrMsg: "has no field", + expectedReleaseNoterNOTToBeCalled: true, + expectedMailerNOTToBeCalled: true, + expectedFormatterNOTToBeCalled: true, + }, + "when release notes returns an error, the error bubbles up and the mail is never sent": { + workspaceStatus: map[string]string{"gitVersion": "v1.13.10-beta.0-16-g48844ef5e7"}, + opts: getOpts(), + expectedFormatterNOTToBeCalled: true, + expectedMailerNOTToBeCalled: true, + releaseNoterErr: fmt.Errorf("some release notes error"), + expectedErrMsg: "some release notes error", + }, + "when the formatter fails, the error bubbles up and the mail is never sent": { + workspaceStatus: map[string]string{"gitVersion": "v1.13.10-beta.0-16-g48844ef5e7"}, + opts: getOpts(), + formattterErr: fmt.Errorf("some formatter error"), + expectedMailerNOTToBeCalled: true, + expectedErrMsg: "some formatter error", + }, + "when the mail sender fails, the error bubbles up": { + workspaceStatus: map[string]string{"gitVersion": "v1.13.10-beta.0-16-g48844ef5e7"}, + opts: getOpts(), + mailerSenderErr: fmt.Errorf("some mail sender error"), + expectedErrMsg: "some mail sender error", + }, + "when in mock mode, sets the sender as the recipient": { + workspaceStatus: map[string]string{"gitVersion": "v1.13.10-beta.0-16-g48844ef5e7"}, + opts: getOpts(func(o *opts) { + o.SenderEmail = "sender email" + o.SenderName = "sender name" + }), + expectedRecipients: &[]string{"sender name", "sender email"}, + expectedSender: [...]string{"sender name", "sender email"}, + }, + "when in nomock mode, sets the mailinglists as the recipient": { + workspaceStatus: map[string]string{"gitVersion": "v1.13.10-beta.0-16-g48844ef5e7"}, + opts: getOpts(func(o *opts) { o.Nomock = true }), + expectedRecipients: &[]string{ + patch.KDevName, patch.KDevEmail, + patch.KDevAnnounceName, patch.KDevAnnounceEmail, + }, + }, + "when setting the recipients fails, the error bubbles up": { + workspaceStatus: map[string]string{"gitVersion": "v1.13.10-beta.0-16-g48844ef5e7"}, + opts: getOpts(), + expectedMailerNOTToBeCalled: true, + mailerSetRecipientsErr: fmt.Errorf("some recipients error"), + expectedErrMsg: "some recipients error", + }, + "when setting the sender fails, the error bubbles up": { + workspaceStatus: map[string]string{"gitVersion": "v1.13.10-beta.0-16-g48844ef5e7"}, + opts: getOpts(), + expectedMailerNOTToBeCalled: true, + mailerSetSenderErr: fmt.Errorf("some sender error"), + expectedErrMsg: "some sender error", + }, + "when cut date parsing fails, the error bubbles up": { + workspaceStatus: map[string]string{"gitVersion": "v1.13.10-beta.0-16-g48844ef5e7"}, + opts: getOpts(func(o *opts) { o.CutDate = "invalid cut date" }), + expectedReleaseNoterNOTToBeCalled: true, + expectedMailerNOTToBeCalled: true, + expectedFormatterNOTToBeCalled: true, + expectedErrMsg: `cannot parse "invalid cut date"`, + }, + "when freeze date parsing fails, the error bubbles up": { + workspaceStatus: map[string]string{"gitVersion": "v1.13.10-beta.0-16-g48844ef5e7"}, + opts: getOpts(func(o *opts) { o.FreezeDate = "invalid freeze date" }), + expectedReleaseNoterNOTToBeCalled: true, + expectedMailerNOTToBeCalled: true, + expectedFormatterNOTToBeCalled: true, + expectedErrMsg: `cannot parse "invalid freeze date"`, + }, + } + + for name, tc := range tests { + tc := tc + + it.Run(t, name, func(t *testing.T) { + ws := &internalfakes.FakeWorkspace{} + ws.StatusReturns(tc.workspaceStatus, tc.workspaceErr) + + rn := &internalfakes.FakeReleaseNoter{} + rn.GetMarkdownReturns(tc.releaseNoterOutput, tc.releaseNoterErr) + + f := &internalfakes.FakeFormatter{} + f.MarkdownToHTMLReturns(tc.formatterOutput, tc.formattterErr) + + ms := &internalfakes.FakeMailSender{} + ms.SendReturns(tc.mailerSenderErr) + ms.SetRecipientsReturns(tc.mailerSetRecipientsErr) + ms.SetSenderReturns(tc.mailerSetSenderErr) + + announcer := &patch.Announcer{ + Opts: tc.opts, + Workspace: ws, + ReleaseNoter: rn, + MailSender: ms, + Formatter: f, + } + + err := announcer.Run() + it.CheckErrSub(t, err, tc.expectedErrMsg) + + require.Equal(t, 1, ws.StatusCallCount(), "Workspace#Status call count") + + checkReleaseNoter(t, rn, &tc) + checkFormatter(t, f, &tc) + checkMailSender(t, ms, &tc) + }) + } +} + +func checkReleaseNoter(t *testing.T, rn *internalfakes.FakeReleaseNoter, tc *testCase) { + cc := rn.GetMarkdownCallCount() + if tc.expectedReleaseNoterNOTToBeCalled { + require.Equal(t, 0, cc, "ReleaseNoter#GetMarkdown call count") + return + } + require.Equal(t, 1, cc, "ReleaseNoter#GetMarkdown call count") +} + +func checkFormatter(t *testing.T, f *internalfakes.FakeFormatter, tc *testCase) { + cc := f.MarkdownToHTMLCallCount() + if tc.expectedFormatterNOTToBeCalled { + require.Equal(t, 0, cc, "Formatter#MarkdownToHTML call count") + return + } + require.Equal(t, 1, cc, "Formatter#MarkdownToHTML call count") + + content, subject := f.MarkdownToHTMLArgsForCall(0) + + for _, re := range tc.expectedFormatterMarkdown { + require.Regexp(t, re, content) + } + for _, re := range tc.expectedFormatterSubject { + require.Regexp(t, re, subject) + } +} + +func checkMailSender(t *testing.T, ms *internalfakes.FakeMailSender, tc *testCase) { + cc := ms.SendCallCount() + if tc.expectedMailerNOTToBeCalled { + require.Equal(t, 0, cc, "Mailer#Send call count") + return + } + require.Equal(t, 1, cc, "Mailer#Send call count") + + body, subject := ms.SendArgsForCall(0) + + for _, re := range tc.expectedMailerBody { + require.Regexp(t, re, body) + } + for _, re := range tc.expectedMailerSubject { + require.Regexp(t, re, subject) + } + + if r := tc.expectedRecipients; r != nil { + require.Equal(t, *r, ms.SetRecipientsArgsForCall(0)) + } + + sName, sEmail := ms.SetSenderArgsForCall(0) + require.Equalf(t, tc.expectedSender[0], sName, "Sender name") + require.Equalf(t, tc.expectedSender[1], sEmail, "Sender email") +} diff --git a/pkg/patch/internal/BUILD.bazel b/pkg/patch/internal/BUILD.bazel new file mode 100644 index 00000000000..693fee05a00 --- /dev/null +++ b/pkg/patch/internal/BUILD.bazel @@ -0,0 +1,57 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "exec.go", + "formatter.go", + "mail_sender.go", + "release_notes.go", + "workspace.go", + ], + importpath = "k8s.io/release/pkg/patch/internal", + visibility = ["//pkg/patch:__subpackages__"], + deps = [ + "//pkg/log:go_default_library", + "@com_github_sendgrid_rest//:go_default_library", + "@com_github_sendgrid_sendgrid_go//:go_default_library", + "@com_github_sendgrid_sendgrid_go//helpers/mail:go_default_library", + "@io_k8s_utils//exec:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "formatter_test.go", + "mail_sender_test.go", + "release_notes_test.go", + "workspace_test.go", + ], + embed = [":go_default_library"], + deps = [ + "//pkg/patch/internal/internalfakes:go_default_library", + "//pkg/patch/internal/testing:go_default_library", + "@com_github_sendgrid_rest//:go_default_library", + "@com_github_stretchr_testify//require:go_default_library", + "@io_k8s_utils//exec:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//pkg/patch/internal/internalfakes:all-srcs", + "//pkg/patch/internal/testing:all-srcs", + ], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/patch/internal/exec.go b/pkg/patch/internal/exec.go new file mode 100644 index 00000000000..81750e22ba6 --- /dev/null +++ b/pkg/patch/internal/exec.go @@ -0,0 +1,97 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 internal + +import ( + "fmt" + + exec "k8s.io/utils/exec" +) + +//counterfeiter:generate . Cmd +type Cmd = exec.Cmd + +type CommandCreator func(path string, args ...string) Cmd + +func (c CommandCreator) create(path string, args ...string) Cmd { + if c == nil { + c = exec.New().Command + } + return c(path, args...) +} + +func cmdOutput(cmd Cmd) (string, *execErr) { + b, err := cmd.Output() + s := string(b) + if err != nil { + return "", &execErr{ + Err: err, + stdout: s, + stderr: getStderr(err), + } + } + return s, nil +} + +type execErr struct { + Err error + stdout string + stderr string +} + +func (ee *execErr) Error() string { + return fmt.Sprintf("%s %s", ee.Err, ee.stdio(35)) +} +func (ee *execErr) FullError() string { + return fmt.Sprintf("%s %s", ee.Err, ee.stdio(0)) +} +func (ee *execErr) stdio(maxLen int) string { + truncater := func(s string) string { + if maxLen < 1 { + return s + } + return trunc(maxLen, s) + } + var s string + if o := ee.stdout; o != "" { + s += "Stdout: " + truncater(o) + } else { + s += "no Stdout" + } + s += ", " + if o := ee.stderr; o != "" { + s += "Stderr: " + truncater(o) + } else { + s += "no Stderr" + } + return "(" + s + ")" +} + +func trunc(maxLen int, s string) string { + if len(s) <= maxLen { + return s + } + partLen := (maxLen - 5) / 2 + return s[:partLen] + " ... " + s[len(s)-partLen:] +} + +func getStderr(err error) string { + if eer, ok := err.(*exec.ExitErrorWrapper); ok { + return string(eer.Stderr) + } + return "" +} diff --git a/pkg/patch/internal/formatter.go b/pkg/patch/internal/formatter.go new file mode 100644 index 00000000000..72df8f3bda0 --- /dev/null +++ b/pkg/patch/internal/formatter.go @@ -0,0 +1,65 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 internal + +import ( + "fmt" + "strings" + + "k8s.io/release/pkg/log" +) + +type Formatter struct { + log.Mixin + + CommandCreator CommandCreator + Style string +} + +const formatterScript = ` +set -euo pipefail +pandoc \ + --standalone \ + --columns=10000000 \ + --from=gfm \ + --to=html5 \ + --metadata=pagetitle="${TITLE}" \ + --include-in-header=<(echo "${STYLE}") \ + --output=- +` + +func (r *Formatter) MarkdownToHTML(markdown, title string) (string, error) { + cmd := r.CommandCreator.create("bash", "-c", formatterScript) + if cmd == nil { + return "", fmt.Errorf("command is nil") + } + r.Logger().Debug("command created") + + cmd.SetStdin(strings.NewReader(markdown)) + cmd.SetEnv([]string{ + "TITLE=" + title, + "STYLE=" + r.Style, + }) + + s, err := cmdOutput(cmd) + if err != nil { + r.Logger().WithError(err).Debug("execing & getting output failed") + r.Logger().WithField("error", err.FullError()).Trace("full exec error") + return "", err + } + return s, nil +} diff --git a/pkg/patch/internal/formatter_test.go b/pkg/patch/internal/formatter_test.go new file mode 100644 index 00000000000..bfc8c2f64d7 --- /dev/null +++ b/pkg/patch/internal/formatter_test.go @@ -0,0 +1,88 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 internal_test + +import ( + "fmt" + "io/ioutil" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/release/pkg/patch/internal" + "k8s.io/release/pkg/patch/internal/internalfakes" + it "k8s.io/release/pkg/patch/internal/testing" + "k8s.io/utils/exec" +) + +func TestFormatter(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + cmdOutput []byte + cmdErr error + content string + title string + style string + + expectedErr string + expectedOutput string + }{ + "happy path": { + content: "some input", + title: "some title", + style: "some additional style", + cmdOutput: []byte("converted content"), + expectedOutput: "converted content", + }, + "when the command returns an error, the error bubbles up": { + cmdErr: fmt.Errorf("some cmd error"), + expectedErr: "some cmd error", + }, + } + + for name, tc := range tests { + tc := tc + + it.Run(t, name, func(t *testing.T) { + cmd := &internalfakes.FakeCmd{} + cmd.OutputReturns(tc.cmdOutput, tc.cmdErr) + + f := &internal.Formatter{ + Style: tc.style, + CommandCreator: func(exe string, args ...string) exec.Cmd { + require.Equal(t, "bash", exe) + require.Contains(t, args[1], "pandoc") + return cmd + }, + } + + output, err := f.MarkdownToHTML(tc.content, tc.title) + it.CheckErrSub(t, err, tc.expectedErr) + require.Equal(t, 1, cmd.OutputCallCount(), "Command#Output call count") + + require.Equal(t, tc.expectedOutput, output, "output") + + require.Contains(t, cmd.SetEnvArgsForCall(0), "STYLE="+tc.style) + require.Contains(t, cmd.SetEnvArgsForCall(0), "TITLE="+tc.title) + + r := cmd.SetStdinArgsForCall(0) + b, err := ioutil.ReadAll(r) + require.NoError(t, err) + require.EqualValues(t, string(b), tc.content) + }) + } +} diff --git a/pkg/patch/internal/internalfakes/BUILD.bazel b/pkg/patch/internal/internalfakes/BUILD.bazel new file mode 100644 index 00000000000..6c53f5a0e37 --- /dev/null +++ b/pkg/patch/internal/internalfakes/BUILD.bazel @@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "fake_cmd.go", + "fake_formatter.go", + "fake_mail_sender.go", + "fake_release_noter.go", + "fake_sendgrid_client.go", + "fake_workspace.go", + ], + importpath = "k8s.io/release/pkg/patch/internal/internalfakes", + visibility = ["//pkg/patch:__subpackages__"], + deps = [ + "//pkg/patch:go_default_library", + "//pkg/patch/internal:go_default_library", + "@com_github_sendgrid_rest//:go_default_library", + "@com_github_sendgrid_sendgrid_go//helpers/mail:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/patch/internal/internalfakes/fake_cmd.go b/pkg/patch/internal/internalfakes/fake_cmd.go new file mode 100644 index 00000000000..cf213baf86c --- /dev/null +++ b/pkg/patch/internal/internalfakes/fake_cmd.go @@ -0,0 +1,746 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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. +*/ + +// Code generated by counterfeiter. DO NOT EDIT. +package internalfakes + +import ( + "io" + "sync" + + "k8s.io/release/pkg/patch/internal" +) + +type FakeCmd struct { + CombinedOutputStub func() ([]byte, error) + combinedOutputMutex sync.RWMutex + combinedOutputArgsForCall []struct { + } + combinedOutputReturns struct { + result1 []byte + result2 error + } + combinedOutputReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + OutputStub func() ([]byte, error) + outputMutex sync.RWMutex + outputArgsForCall []struct { + } + outputReturns struct { + result1 []byte + result2 error + } + outputReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + RunStub func() error + runMutex sync.RWMutex + runArgsForCall []struct { + } + runReturns struct { + result1 error + } + runReturnsOnCall map[int]struct { + result1 error + } + SetDirStub func(string) + setDirMutex sync.RWMutex + setDirArgsForCall []struct { + arg1 string + } + SetEnvStub func([]string) + setEnvMutex sync.RWMutex + setEnvArgsForCall []struct { + arg1 []string + } + SetStderrStub func(io.Writer) + setStderrMutex sync.RWMutex + setStderrArgsForCall []struct { + arg1 io.Writer + } + SetStdinStub func(io.Reader) + setStdinMutex sync.RWMutex + setStdinArgsForCall []struct { + arg1 io.Reader + } + SetStdoutStub func(io.Writer) + setStdoutMutex sync.RWMutex + setStdoutArgsForCall []struct { + arg1 io.Writer + } + StartStub func() error + startMutex sync.RWMutex + startArgsForCall []struct { + } + startReturns struct { + result1 error + } + startReturnsOnCall map[int]struct { + result1 error + } + StderrPipeStub func() (io.ReadCloser, error) + stderrPipeMutex sync.RWMutex + stderrPipeArgsForCall []struct { + } + stderrPipeReturns struct { + result1 io.ReadCloser + result2 error + } + stderrPipeReturnsOnCall map[int]struct { + result1 io.ReadCloser + result2 error + } + StdoutPipeStub func() (io.ReadCloser, error) + stdoutPipeMutex sync.RWMutex + stdoutPipeArgsForCall []struct { + } + stdoutPipeReturns struct { + result1 io.ReadCloser + result2 error + } + stdoutPipeReturnsOnCall map[int]struct { + result1 io.ReadCloser + result2 error + } + StopStub func() + stopMutex sync.RWMutex + stopArgsForCall []struct { + } + WaitStub func() error + waitMutex sync.RWMutex + waitArgsForCall []struct { + } + waitReturns struct { + result1 error + } + waitReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeCmd) CombinedOutput() ([]byte, error) { + fake.combinedOutputMutex.Lock() + ret, specificReturn := fake.combinedOutputReturnsOnCall[len(fake.combinedOutputArgsForCall)] + fake.combinedOutputArgsForCall = append(fake.combinedOutputArgsForCall, struct { + }{}) + fake.recordInvocation("CombinedOutput", []interface{}{}) + fake.combinedOutputMutex.Unlock() + if fake.CombinedOutputStub != nil { + return fake.CombinedOutputStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.combinedOutputReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeCmd) CombinedOutputCallCount() int { + fake.combinedOutputMutex.RLock() + defer fake.combinedOutputMutex.RUnlock() + return len(fake.combinedOutputArgsForCall) +} + +func (fake *FakeCmd) CombinedOutputCalls(stub func() ([]byte, error)) { + fake.combinedOutputMutex.Lock() + defer fake.combinedOutputMutex.Unlock() + fake.CombinedOutputStub = stub +} + +func (fake *FakeCmd) CombinedOutputReturns(result1 []byte, result2 error) { + fake.combinedOutputMutex.Lock() + defer fake.combinedOutputMutex.Unlock() + fake.CombinedOutputStub = nil + fake.combinedOutputReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *FakeCmd) CombinedOutputReturnsOnCall(i int, result1 []byte, result2 error) { + fake.combinedOutputMutex.Lock() + defer fake.combinedOutputMutex.Unlock() + fake.CombinedOutputStub = nil + if fake.combinedOutputReturnsOnCall == nil { + fake.combinedOutputReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.combinedOutputReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *FakeCmd) Output() ([]byte, error) { + fake.outputMutex.Lock() + ret, specificReturn := fake.outputReturnsOnCall[len(fake.outputArgsForCall)] + fake.outputArgsForCall = append(fake.outputArgsForCall, struct { + }{}) + fake.recordInvocation("Output", []interface{}{}) + fake.outputMutex.Unlock() + if fake.OutputStub != nil { + return fake.OutputStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.outputReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeCmd) OutputCallCount() int { + fake.outputMutex.RLock() + defer fake.outputMutex.RUnlock() + return len(fake.outputArgsForCall) +} + +func (fake *FakeCmd) OutputCalls(stub func() ([]byte, error)) { + fake.outputMutex.Lock() + defer fake.outputMutex.Unlock() + fake.OutputStub = stub +} + +func (fake *FakeCmd) OutputReturns(result1 []byte, result2 error) { + fake.outputMutex.Lock() + defer fake.outputMutex.Unlock() + fake.OutputStub = nil + fake.outputReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *FakeCmd) OutputReturnsOnCall(i int, result1 []byte, result2 error) { + fake.outputMutex.Lock() + defer fake.outputMutex.Unlock() + fake.OutputStub = nil + if fake.outputReturnsOnCall == nil { + fake.outputReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.outputReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *FakeCmd) Run() error { + fake.runMutex.Lock() + ret, specificReturn := fake.runReturnsOnCall[len(fake.runArgsForCall)] + fake.runArgsForCall = append(fake.runArgsForCall, struct { + }{}) + fake.recordInvocation("Run", []interface{}{}) + fake.runMutex.Unlock() + if fake.RunStub != nil { + return fake.RunStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.runReturns + return fakeReturns.result1 +} + +func (fake *FakeCmd) RunCallCount() int { + fake.runMutex.RLock() + defer fake.runMutex.RUnlock() + return len(fake.runArgsForCall) +} + +func (fake *FakeCmd) RunCalls(stub func() error) { + fake.runMutex.Lock() + defer fake.runMutex.Unlock() + fake.RunStub = stub +} + +func (fake *FakeCmd) RunReturns(result1 error) { + fake.runMutex.Lock() + defer fake.runMutex.Unlock() + fake.RunStub = nil + fake.runReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeCmd) RunReturnsOnCall(i int, result1 error) { + fake.runMutex.Lock() + defer fake.runMutex.Unlock() + fake.RunStub = nil + if fake.runReturnsOnCall == nil { + fake.runReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.runReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeCmd) SetDir(arg1 string) { + fake.setDirMutex.Lock() + fake.setDirArgsForCall = append(fake.setDirArgsForCall, struct { + arg1 string + }{arg1}) + fake.recordInvocation("SetDir", []interface{}{arg1}) + fake.setDirMutex.Unlock() + if fake.SetDirStub != nil { + fake.SetDirStub(arg1) + } +} + +func (fake *FakeCmd) SetDirCallCount() int { + fake.setDirMutex.RLock() + defer fake.setDirMutex.RUnlock() + return len(fake.setDirArgsForCall) +} + +func (fake *FakeCmd) SetDirCalls(stub func(string)) { + fake.setDirMutex.Lock() + defer fake.setDirMutex.Unlock() + fake.SetDirStub = stub +} + +func (fake *FakeCmd) SetDirArgsForCall(i int) string { + fake.setDirMutex.RLock() + defer fake.setDirMutex.RUnlock() + argsForCall := fake.setDirArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeCmd) SetEnv(arg1 []string) { + var arg1Copy []string + if arg1 != nil { + arg1Copy = make([]string, len(arg1)) + copy(arg1Copy, arg1) + } + fake.setEnvMutex.Lock() + fake.setEnvArgsForCall = append(fake.setEnvArgsForCall, struct { + arg1 []string + }{arg1Copy}) + fake.recordInvocation("SetEnv", []interface{}{arg1Copy}) + fake.setEnvMutex.Unlock() + if fake.SetEnvStub != nil { + fake.SetEnvStub(arg1) + } +} + +func (fake *FakeCmd) SetEnvCallCount() int { + fake.setEnvMutex.RLock() + defer fake.setEnvMutex.RUnlock() + return len(fake.setEnvArgsForCall) +} + +func (fake *FakeCmd) SetEnvCalls(stub func([]string)) { + fake.setEnvMutex.Lock() + defer fake.setEnvMutex.Unlock() + fake.SetEnvStub = stub +} + +func (fake *FakeCmd) SetEnvArgsForCall(i int) []string { + fake.setEnvMutex.RLock() + defer fake.setEnvMutex.RUnlock() + argsForCall := fake.setEnvArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeCmd) SetStderr(arg1 io.Writer) { + fake.setStderrMutex.Lock() + fake.setStderrArgsForCall = append(fake.setStderrArgsForCall, struct { + arg1 io.Writer + }{arg1}) + fake.recordInvocation("SetStderr", []interface{}{arg1}) + fake.setStderrMutex.Unlock() + if fake.SetStderrStub != nil { + fake.SetStderrStub(arg1) + } +} + +func (fake *FakeCmd) SetStderrCallCount() int { + fake.setStderrMutex.RLock() + defer fake.setStderrMutex.RUnlock() + return len(fake.setStderrArgsForCall) +} + +func (fake *FakeCmd) SetStderrCalls(stub func(io.Writer)) { + fake.setStderrMutex.Lock() + defer fake.setStderrMutex.Unlock() + fake.SetStderrStub = stub +} + +func (fake *FakeCmd) SetStderrArgsForCall(i int) io.Writer { + fake.setStderrMutex.RLock() + defer fake.setStderrMutex.RUnlock() + argsForCall := fake.setStderrArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeCmd) SetStdin(arg1 io.Reader) { + fake.setStdinMutex.Lock() + fake.setStdinArgsForCall = append(fake.setStdinArgsForCall, struct { + arg1 io.Reader + }{arg1}) + fake.recordInvocation("SetStdin", []interface{}{arg1}) + fake.setStdinMutex.Unlock() + if fake.SetStdinStub != nil { + fake.SetStdinStub(arg1) + } +} + +func (fake *FakeCmd) SetStdinCallCount() int { + fake.setStdinMutex.RLock() + defer fake.setStdinMutex.RUnlock() + return len(fake.setStdinArgsForCall) +} + +func (fake *FakeCmd) SetStdinCalls(stub func(io.Reader)) { + fake.setStdinMutex.Lock() + defer fake.setStdinMutex.Unlock() + fake.SetStdinStub = stub +} + +func (fake *FakeCmd) SetStdinArgsForCall(i int) io.Reader { + fake.setStdinMutex.RLock() + defer fake.setStdinMutex.RUnlock() + argsForCall := fake.setStdinArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeCmd) SetStdout(arg1 io.Writer) { + fake.setStdoutMutex.Lock() + fake.setStdoutArgsForCall = append(fake.setStdoutArgsForCall, struct { + arg1 io.Writer + }{arg1}) + fake.recordInvocation("SetStdout", []interface{}{arg1}) + fake.setStdoutMutex.Unlock() + if fake.SetStdoutStub != nil { + fake.SetStdoutStub(arg1) + } +} + +func (fake *FakeCmd) SetStdoutCallCount() int { + fake.setStdoutMutex.RLock() + defer fake.setStdoutMutex.RUnlock() + return len(fake.setStdoutArgsForCall) +} + +func (fake *FakeCmd) SetStdoutCalls(stub func(io.Writer)) { + fake.setStdoutMutex.Lock() + defer fake.setStdoutMutex.Unlock() + fake.SetStdoutStub = stub +} + +func (fake *FakeCmd) SetStdoutArgsForCall(i int) io.Writer { + fake.setStdoutMutex.RLock() + defer fake.setStdoutMutex.RUnlock() + argsForCall := fake.setStdoutArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeCmd) Start() error { + fake.startMutex.Lock() + ret, specificReturn := fake.startReturnsOnCall[len(fake.startArgsForCall)] + fake.startArgsForCall = append(fake.startArgsForCall, struct { + }{}) + fake.recordInvocation("Start", []interface{}{}) + fake.startMutex.Unlock() + if fake.StartStub != nil { + return fake.StartStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.startReturns + return fakeReturns.result1 +} + +func (fake *FakeCmd) StartCallCount() int { + fake.startMutex.RLock() + defer fake.startMutex.RUnlock() + return len(fake.startArgsForCall) +} + +func (fake *FakeCmd) StartCalls(stub func() error) { + fake.startMutex.Lock() + defer fake.startMutex.Unlock() + fake.StartStub = stub +} + +func (fake *FakeCmd) StartReturns(result1 error) { + fake.startMutex.Lock() + defer fake.startMutex.Unlock() + fake.StartStub = nil + fake.startReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeCmd) StartReturnsOnCall(i int, result1 error) { + fake.startMutex.Lock() + defer fake.startMutex.Unlock() + fake.StartStub = nil + if fake.startReturnsOnCall == nil { + fake.startReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.startReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeCmd) StderrPipe() (io.ReadCloser, error) { + fake.stderrPipeMutex.Lock() + ret, specificReturn := fake.stderrPipeReturnsOnCall[len(fake.stderrPipeArgsForCall)] + fake.stderrPipeArgsForCall = append(fake.stderrPipeArgsForCall, struct { + }{}) + fake.recordInvocation("StderrPipe", []interface{}{}) + fake.stderrPipeMutex.Unlock() + if fake.StderrPipeStub != nil { + return fake.StderrPipeStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.stderrPipeReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeCmd) StderrPipeCallCount() int { + fake.stderrPipeMutex.RLock() + defer fake.stderrPipeMutex.RUnlock() + return len(fake.stderrPipeArgsForCall) +} + +func (fake *FakeCmd) StderrPipeCalls(stub func() (io.ReadCloser, error)) { + fake.stderrPipeMutex.Lock() + defer fake.stderrPipeMutex.Unlock() + fake.StderrPipeStub = stub +} + +func (fake *FakeCmd) StderrPipeReturns(result1 io.ReadCloser, result2 error) { + fake.stderrPipeMutex.Lock() + defer fake.stderrPipeMutex.Unlock() + fake.StderrPipeStub = nil + fake.stderrPipeReturns = struct { + result1 io.ReadCloser + result2 error + }{result1, result2} +} + +func (fake *FakeCmd) StderrPipeReturnsOnCall(i int, result1 io.ReadCloser, result2 error) { + fake.stderrPipeMutex.Lock() + defer fake.stderrPipeMutex.Unlock() + fake.StderrPipeStub = nil + if fake.stderrPipeReturnsOnCall == nil { + fake.stderrPipeReturnsOnCall = make(map[int]struct { + result1 io.ReadCloser + result2 error + }) + } + fake.stderrPipeReturnsOnCall[i] = struct { + result1 io.ReadCloser + result2 error + }{result1, result2} +} + +func (fake *FakeCmd) StdoutPipe() (io.ReadCloser, error) { + fake.stdoutPipeMutex.Lock() + ret, specificReturn := fake.stdoutPipeReturnsOnCall[len(fake.stdoutPipeArgsForCall)] + fake.stdoutPipeArgsForCall = append(fake.stdoutPipeArgsForCall, struct { + }{}) + fake.recordInvocation("StdoutPipe", []interface{}{}) + fake.stdoutPipeMutex.Unlock() + if fake.StdoutPipeStub != nil { + return fake.StdoutPipeStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.stdoutPipeReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeCmd) StdoutPipeCallCount() int { + fake.stdoutPipeMutex.RLock() + defer fake.stdoutPipeMutex.RUnlock() + return len(fake.stdoutPipeArgsForCall) +} + +func (fake *FakeCmd) StdoutPipeCalls(stub func() (io.ReadCloser, error)) { + fake.stdoutPipeMutex.Lock() + defer fake.stdoutPipeMutex.Unlock() + fake.StdoutPipeStub = stub +} + +func (fake *FakeCmd) StdoutPipeReturns(result1 io.ReadCloser, result2 error) { + fake.stdoutPipeMutex.Lock() + defer fake.stdoutPipeMutex.Unlock() + fake.StdoutPipeStub = nil + fake.stdoutPipeReturns = struct { + result1 io.ReadCloser + result2 error + }{result1, result2} +} + +func (fake *FakeCmd) StdoutPipeReturnsOnCall(i int, result1 io.ReadCloser, result2 error) { + fake.stdoutPipeMutex.Lock() + defer fake.stdoutPipeMutex.Unlock() + fake.StdoutPipeStub = nil + if fake.stdoutPipeReturnsOnCall == nil { + fake.stdoutPipeReturnsOnCall = make(map[int]struct { + result1 io.ReadCloser + result2 error + }) + } + fake.stdoutPipeReturnsOnCall[i] = struct { + result1 io.ReadCloser + result2 error + }{result1, result2} +} + +func (fake *FakeCmd) Stop() { + fake.stopMutex.Lock() + fake.stopArgsForCall = append(fake.stopArgsForCall, struct { + }{}) + fake.recordInvocation("Stop", []interface{}{}) + fake.stopMutex.Unlock() + if fake.StopStub != nil { + fake.StopStub() + } +} + +func (fake *FakeCmd) StopCallCount() int { + fake.stopMutex.RLock() + defer fake.stopMutex.RUnlock() + return len(fake.stopArgsForCall) +} + +func (fake *FakeCmd) StopCalls(stub func()) { + fake.stopMutex.Lock() + defer fake.stopMutex.Unlock() + fake.StopStub = stub +} + +func (fake *FakeCmd) Wait() error { + fake.waitMutex.Lock() + ret, specificReturn := fake.waitReturnsOnCall[len(fake.waitArgsForCall)] + fake.waitArgsForCall = append(fake.waitArgsForCall, struct { + }{}) + fake.recordInvocation("Wait", []interface{}{}) + fake.waitMutex.Unlock() + if fake.WaitStub != nil { + return fake.WaitStub() + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.waitReturns + return fakeReturns.result1 +} + +func (fake *FakeCmd) WaitCallCount() int { + fake.waitMutex.RLock() + defer fake.waitMutex.RUnlock() + return len(fake.waitArgsForCall) +} + +func (fake *FakeCmd) WaitCalls(stub func() error) { + fake.waitMutex.Lock() + defer fake.waitMutex.Unlock() + fake.WaitStub = stub +} + +func (fake *FakeCmd) WaitReturns(result1 error) { + fake.waitMutex.Lock() + defer fake.waitMutex.Unlock() + fake.WaitStub = nil + fake.waitReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeCmd) WaitReturnsOnCall(i int, result1 error) { + fake.waitMutex.Lock() + defer fake.waitMutex.Unlock() + fake.WaitStub = nil + if fake.waitReturnsOnCall == nil { + fake.waitReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.waitReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeCmd) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.combinedOutputMutex.RLock() + defer fake.combinedOutputMutex.RUnlock() + fake.outputMutex.RLock() + defer fake.outputMutex.RUnlock() + fake.runMutex.RLock() + defer fake.runMutex.RUnlock() + fake.setDirMutex.RLock() + defer fake.setDirMutex.RUnlock() + fake.setEnvMutex.RLock() + defer fake.setEnvMutex.RUnlock() + fake.setStderrMutex.RLock() + defer fake.setStderrMutex.RUnlock() + fake.setStdinMutex.RLock() + defer fake.setStdinMutex.RUnlock() + fake.setStdoutMutex.RLock() + defer fake.setStdoutMutex.RUnlock() + fake.startMutex.RLock() + defer fake.startMutex.RUnlock() + fake.stderrPipeMutex.RLock() + defer fake.stderrPipeMutex.RUnlock() + fake.stdoutPipeMutex.RLock() + defer fake.stdoutPipeMutex.RUnlock() + fake.stopMutex.RLock() + defer fake.stopMutex.RUnlock() + fake.waitMutex.RLock() + defer fake.waitMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeCmd) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ internal.Cmd = new(FakeCmd) diff --git a/pkg/patch/internal/internalfakes/fake_formatter.go b/pkg/patch/internal/internalfakes/fake_formatter.go new file mode 100644 index 00000000000..a9a1836ee41 --- /dev/null +++ b/pkg/patch/internal/internalfakes/fake_formatter.go @@ -0,0 +1,133 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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. +*/ + +// Code generated by counterfeiter. DO NOT EDIT. +package internalfakes + +import ( + "sync" + + "k8s.io/release/pkg/patch" +) + +type FakeFormatter struct { + MarkdownToHTMLStub func(string, string) (string, error) + markdownToHTMLMutex sync.RWMutex + markdownToHTMLArgsForCall []struct { + arg1 string + arg2 string + } + markdownToHTMLReturns struct { + result1 string + result2 error + } + markdownToHTMLReturnsOnCall map[int]struct { + result1 string + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeFormatter) MarkdownToHTML(arg1 string, arg2 string) (string, error) { + fake.markdownToHTMLMutex.Lock() + ret, specificReturn := fake.markdownToHTMLReturnsOnCall[len(fake.markdownToHTMLArgsForCall)] + fake.markdownToHTMLArgsForCall = append(fake.markdownToHTMLArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("MarkdownToHTML", []interface{}{arg1, arg2}) + fake.markdownToHTMLMutex.Unlock() + if fake.MarkdownToHTMLStub != nil { + return fake.MarkdownToHTMLStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.markdownToHTMLReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeFormatter) MarkdownToHTMLCallCount() int { + fake.markdownToHTMLMutex.RLock() + defer fake.markdownToHTMLMutex.RUnlock() + return len(fake.markdownToHTMLArgsForCall) +} + +func (fake *FakeFormatter) MarkdownToHTMLCalls(stub func(string, string) (string, error)) { + fake.markdownToHTMLMutex.Lock() + defer fake.markdownToHTMLMutex.Unlock() + fake.MarkdownToHTMLStub = stub +} + +func (fake *FakeFormatter) MarkdownToHTMLArgsForCall(i int) (string, string) { + fake.markdownToHTMLMutex.RLock() + defer fake.markdownToHTMLMutex.RUnlock() + argsForCall := fake.markdownToHTMLArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeFormatter) MarkdownToHTMLReturns(result1 string, result2 error) { + fake.markdownToHTMLMutex.Lock() + defer fake.markdownToHTMLMutex.Unlock() + fake.MarkdownToHTMLStub = nil + fake.markdownToHTMLReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeFormatter) MarkdownToHTMLReturnsOnCall(i int, result1 string, result2 error) { + fake.markdownToHTMLMutex.Lock() + defer fake.markdownToHTMLMutex.Unlock() + fake.MarkdownToHTMLStub = nil + if fake.markdownToHTMLReturnsOnCall == nil { + fake.markdownToHTMLReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.markdownToHTMLReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeFormatter) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.markdownToHTMLMutex.RLock() + defer fake.markdownToHTMLMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeFormatter) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ patch.Formatter = new(FakeFormatter) diff --git a/pkg/patch/internal/internalfakes/fake_mail_sender.go b/pkg/patch/internal/internalfakes/fake_mail_sender.go new file mode 100644 index 00000000000..d82cd84933c --- /dev/null +++ b/pkg/patch/internal/internalfakes/fake_mail_sender.go @@ -0,0 +1,276 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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. +*/ + +// Code generated by counterfeiter. DO NOT EDIT. +package internalfakes + +import ( + "sync" + + "k8s.io/release/pkg/patch" +) + +type FakeMailSender struct { + SendStub func(string, string) error + sendMutex sync.RWMutex + sendArgsForCall []struct { + arg1 string + arg2 string + } + sendReturns struct { + result1 error + } + sendReturnsOnCall map[int]struct { + result1 error + } + SetRecipientsStub func(...string) error + setRecipientsMutex sync.RWMutex + setRecipientsArgsForCall []struct { + arg1 []string + } + setRecipientsReturns struct { + result1 error + } + setRecipientsReturnsOnCall map[int]struct { + result1 error + } + SetSenderStub func(string, string) error + setSenderMutex sync.RWMutex + setSenderArgsForCall []struct { + arg1 string + arg2 string + } + setSenderReturns struct { + result1 error + } + setSenderReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeMailSender) Send(arg1 string, arg2 string) error { + fake.sendMutex.Lock() + ret, specificReturn := fake.sendReturnsOnCall[len(fake.sendArgsForCall)] + fake.sendArgsForCall = append(fake.sendArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("Send", []interface{}{arg1, arg2}) + fake.sendMutex.Unlock() + if fake.SendStub != nil { + return fake.SendStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.sendReturns + return fakeReturns.result1 +} + +func (fake *FakeMailSender) SendCallCount() int { + fake.sendMutex.RLock() + defer fake.sendMutex.RUnlock() + return len(fake.sendArgsForCall) +} + +func (fake *FakeMailSender) SendCalls(stub func(string, string) error) { + fake.sendMutex.Lock() + defer fake.sendMutex.Unlock() + fake.SendStub = stub +} + +func (fake *FakeMailSender) SendArgsForCall(i int) (string, string) { + fake.sendMutex.RLock() + defer fake.sendMutex.RUnlock() + argsForCall := fake.sendArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeMailSender) SendReturns(result1 error) { + fake.sendMutex.Lock() + defer fake.sendMutex.Unlock() + fake.SendStub = nil + fake.sendReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeMailSender) SendReturnsOnCall(i int, result1 error) { + fake.sendMutex.Lock() + defer fake.sendMutex.Unlock() + fake.SendStub = nil + if fake.sendReturnsOnCall == nil { + fake.sendReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.sendReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeMailSender) SetRecipients(arg1 ...string) error { + fake.setRecipientsMutex.Lock() + ret, specificReturn := fake.setRecipientsReturnsOnCall[len(fake.setRecipientsArgsForCall)] + fake.setRecipientsArgsForCall = append(fake.setRecipientsArgsForCall, struct { + arg1 []string + }{arg1}) + fake.recordInvocation("SetRecipients", []interface{}{arg1}) + fake.setRecipientsMutex.Unlock() + if fake.SetRecipientsStub != nil { + return fake.SetRecipientsStub(arg1...) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.setRecipientsReturns + return fakeReturns.result1 +} + +func (fake *FakeMailSender) SetRecipientsCallCount() int { + fake.setRecipientsMutex.RLock() + defer fake.setRecipientsMutex.RUnlock() + return len(fake.setRecipientsArgsForCall) +} + +func (fake *FakeMailSender) SetRecipientsCalls(stub func(...string) error) { + fake.setRecipientsMutex.Lock() + defer fake.setRecipientsMutex.Unlock() + fake.SetRecipientsStub = stub +} + +func (fake *FakeMailSender) SetRecipientsArgsForCall(i int) []string { + fake.setRecipientsMutex.RLock() + defer fake.setRecipientsMutex.RUnlock() + argsForCall := fake.setRecipientsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeMailSender) SetRecipientsReturns(result1 error) { + fake.setRecipientsMutex.Lock() + defer fake.setRecipientsMutex.Unlock() + fake.SetRecipientsStub = nil + fake.setRecipientsReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeMailSender) SetRecipientsReturnsOnCall(i int, result1 error) { + fake.setRecipientsMutex.Lock() + defer fake.setRecipientsMutex.Unlock() + fake.SetRecipientsStub = nil + if fake.setRecipientsReturnsOnCall == nil { + fake.setRecipientsReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setRecipientsReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeMailSender) SetSender(arg1 string, arg2 string) error { + fake.setSenderMutex.Lock() + ret, specificReturn := fake.setSenderReturnsOnCall[len(fake.setSenderArgsForCall)] + fake.setSenderArgsForCall = append(fake.setSenderArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("SetSender", []interface{}{arg1, arg2}) + fake.setSenderMutex.Unlock() + if fake.SetSenderStub != nil { + return fake.SetSenderStub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + fakeReturns := fake.setSenderReturns + return fakeReturns.result1 +} + +func (fake *FakeMailSender) SetSenderCallCount() int { + fake.setSenderMutex.RLock() + defer fake.setSenderMutex.RUnlock() + return len(fake.setSenderArgsForCall) +} + +func (fake *FakeMailSender) SetSenderCalls(stub func(string, string) error) { + fake.setSenderMutex.Lock() + defer fake.setSenderMutex.Unlock() + fake.SetSenderStub = stub +} + +func (fake *FakeMailSender) SetSenderArgsForCall(i int) (string, string) { + fake.setSenderMutex.RLock() + defer fake.setSenderMutex.RUnlock() + argsForCall := fake.setSenderArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeMailSender) SetSenderReturns(result1 error) { + fake.setSenderMutex.Lock() + defer fake.setSenderMutex.Unlock() + fake.SetSenderStub = nil + fake.setSenderReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeMailSender) SetSenderReturnsOnCall(i int, result1 error) { + fake.setSenderMutex.Lock() + defer fake.setSenderMutex.Unlock() + fake.SetSenderStub = nil + if fake.setSenderReturnsOnCall == nil { + fake.setSenderReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setSenderReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeMailSender) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.sendMutex.RLock() + defer fake.sendMutex.RUnlock() + fake.setRecipientsMutex.RLock() + defer fake.setRecipientsMutex.RUnlock() + fake.setSenderMutex.RLock() + defer fake.setSenderMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeMailSender) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ patch.MailSender = new(FakeMailSender) diff --git a/pkg/patch/internal/internalfakes/fake_release_noter.go b/pkg/patch/internal/internalfakes/fake_release_noter.go new file mode 100644 index 00000000000..d9c6b45dc2f --- /dev/null +++ b/pkg/patch/internal/internalfakes/fake_release_noter.go @@ -0,0 +1,122 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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. +*/ + +// Code generated by counterfeiter. DO NOT EDIT. +package internalfakes + +import ( + "sync" + + "k8s.io/release/pkg/patch" +) + +type FakeReleaseNoter struct { + GetMarkdownStub func() (string, error) + getMarkdownMutex sync.RWMutex + getMarkdownArgsForCall []struct { + } + getMarkdownReturns struct { + result1 string + result2 error + } + getMarkdownReturnsOnCall map[int]struct { + result1 string + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeReleaseNoter) GetMarkdown() (string, error) { + fake.getMarkdownMutex.Lock() + ret, specificReturn := fake.getMarkdownReturnsOnCall[len(fake.getMarkdownArgsForCall)] + fake.getMarkdownArgsForCall = append(fake.getMarkdownArgsForCall, struct { + }{}) + fake.recordInvocation("GetMarkdown", []interface{}{}) + fake.getMarkdownMutex.Unlock() + if fake.GetMarkdownStub != nil { + return fake.GetMarkdownStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.getMarkdownReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeReleaseNoter) GetMarkdownCallCount() int { + fake.getMarkdownMutex.RLock() + defer fake.getMarkdownMutex.RUnlock() + return len(fake.getMarkdownArgsForCall) +} + +func (fake *FakeReleaseNoter) GetMarkdownCalls(stub func() (string, error)) { + fake.getMarkdownMutex.Lock() + defer fake.getMarkdownMutex.Unlock() + fake.GetMarkdownStub = stub +} + +func (fake *FakeReleaseNoter) GetMarkdownReturns(result1 string, result2 error) { + fake.getMarkdownMutex.Lock() + defer fake.getMarkdownMutex.Unlock() + fake.GetMarkdownStub = nil + fake.getMarkdownReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeReleaseNoter) GetMarkdownReturnsOnCall(i int, result1 string, result2 error) { + fake.getMarkdownMutex.Lock() + defer fake.getMarkdownMutex.Unlock() + fake.GetMarkdownStub = nil + if fake.getMarkdownReturnsOnCall == nil { + fake.getMarkdownReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.getMarkdownReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeReleaseNoter) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.getMarkdownMutex.RLock() + defer fake.getMarkdownMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeReleaseNoter) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ patch.ReleaseNoter = new(FakeReleaseNoter) diff --git a/pkg/patch/internal/internalfakes/fake_sendgrid_client.go b/pkg/patch/internal/internalfakes/fake_sendgrid_client.go new file mode 100644 index 00000000000..4f40db599cc --- /dev/null +++ b/pkg/patch/internal/internalfakes/fake_sendgrid_client.go @@ -0,0 +1,133 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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. +*/ + +// Code generated by counterfeiter. DO NOT EDIT. +package internalfakes + +import ( + "sync" + + "github.com/sendgrid/rest" + "github.com/sendgrid/sendgrid-go/helpers/mail" + "k8s.io/release/pkg/patch/internal" +) + +type FakeSendgridClient struct { + SendStub func(*mail.SGMailV3) (*rest.Response, error) + sendMutex sync.RWMutex + sendArgsForCall []struct { + arg1 *mail.SGMailV3 + } + sendReturns struct { + result1 *rest.Response + result2 error + } + sendReturnsOnCall map[int]struct { + result1 *rest.Response + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeSendgridClient) Send(arg1 *mail.SGMailV3) (*rest.Response, error) { + fake.sendMutex.Lock() + ret, specificReturn := fake.sendReturnsOnCall[len(fake.sendArgsForCall)] + fake.sendArgsForCall = append(fake.sendArgsForCall, struct { + arg1 *mail.SGMailV3 + }{arg1}) + fake.recordInvocation("Send", []interface{}{arg1}) + fake.sendMutex.Unlock() + if fake.SendStub != nil { + return fake.SendStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.sendReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeSendgridClient) SendCallCount() int { + fake.sendMutex.RLock() + defer fake.sendMutex.RUnlock() + return len(fake.sendArgsForCall) +} + +func (fake *FakeSendgridClient) SendCalls(stub func(*mail.SGMailV3) (*rest.Response, error)) { + fake.sendMutex.Lock() + defer fake.sendMutex.Unlock() + fake.SendStub = stub +} + +func (fake *FakeSendgridClient) SendArgsForCall(i int) *mail.SGMailV3 { + fake.sendMutex.RLock() + defer fake.sendMutex.RUnlock() + argsForCall := fake.sendArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeSendgridClient) SendReturns(result1 *rest.Response, result2 error) { + fake.sendMutex.Lock() + defer fake.sendMutex.Unlock() + fake.SendStub = nil + fake.sendReturns = struct { + result1 *rest.Response + result2 error + }{result1, result2} +} + +func (fake *FakeSendgridClient) SendReturnsOnCall(i int, result1 *rest.Response, result2 error) { + fake.sendMutex.Lock() + defer fake.sendMutex.Unlock() + fake.SendStub = nil + if fake.sendReturnsOnCall == nil { + fake.sendReturnsOnCall = make(map[int]struct { + result1 *rest.Response + result2 error + }) + } + fake.sendReturnsOnCall[i] = struct { + result1 *rest.Response + result2 error + }{result1, result2} +} + +func (fake *FakeSendgridClient) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.sendMutex.RLock() + defer fake.sendMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeSendgridClient) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ internal.SendgridClient = new(FakeSendgridClient) diff --git a/pkg/patch/internal/internalfakes/fake_workspace.go b/pkg/patch/internal/internalfakes/fake_workspace.go new file mode 100644 index 00000000000..01d73f1cf4f --- /dev/null +++ b/pkg/patch/internal/internalfakes/fake_workspace.go @@ -0,0 +1,122 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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. +*/ + +// Code generated by counterfeiter. DO NOT EDIT. +package internalfakes + +import ( + "sync" + + "k8s.io/release/pkg/patch" +) + +type FakeWorkspace struct { + StatusStub func() (map[string]string, error) + statusMutex sync.RWMutex + statusArgsForCall []struct { + } + statusReturns struct { + result1 map[string]string + result2 error + } + statusReturnsOnCall map[int]struct { + result1 map[string]string + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeWorkspace) Status() (map[string]string, error) { + fake.statusMutex.Lock() + ret, specificReturn := fake.statusReturnsOnCall[len(fake.statusArgsForCall)] + fake.statusArgsForCall = append(fake.statusArgsForCall, struct { + }{}) + fake.recordInvocation("Status", []interface{}{}) + fake.statusMutex.Unlock() + if fake.StatusStub != nil { + return fake.StatusStub() + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.statusReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeWorkspace) StatusCallCount() int { + fake.statusMutex.RLock() + defer fake.statusMutex.RUnlock() + return len(fake.statusArgsForCall) +} + +func (fake *FakeWorkspace) StatusCalls(stub func() (map[string]string, error)) { + fake.statusMutex.Lock() + defer fake.statusMutex.Unlock() + fake.StatusStub = stub +} + +func (fake *FakeWorkspace) StatusReturns(result1 map[string]string, result2 error) { + fake.statusMutex.Lock() + defer fake.statusMutex.Unlock() + fake.StatusStub = nil + fake.statusReturns = struct { + result1 map[string]string + result2 error + }{result1, result2} +} + +func (fake *FakeWorkspace) StatusReturnsOnCall(i int, result1 map[string]string, result2 error) { + fake.statusMutex.Lock() + defer fake.statusMutex.Unlock() + fake.StatusStub = nil + if fake.statusReturnsOnCall == nil { + fake.statusReturnsOnCall = make(map[int]struct { + result1 map[string]string + result2 error + }) + } + fake.statusReturnsOnCall[i] = struct { + result1 map[string]string + result2 error + }{result1, result2} +} + +func (fake *FakeWorkspace) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.statusMutex.RLock() + defer fake.statusMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeWorkspace) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ patch.Workspace = new(FakeWorkspace) diff --git a/pkg/patch/internal/mail_sender.go b/pkg/patch/internal/mail_sender.go new file mode 100644 index 00000000000..686864ba76d --- /dev/null +++ b/pkg/patch/internal/mail_sender.go @@ -0,0 +1,129 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 internal + +import ( + "fmt" + + "github.com/sendgrid/rest" + "github.com/sendgrid/sendgrid-go" + "github.com/sendgrid/sendgrid-go/helpers/mail" + "k8s.io/release/pkg/log" +) + +type MailSender struct { + log.Mixin + + SendgridClientCreator SendgridClientCreator + APIKey string + + sender *mail.Email + recipients []*mail.Email +} + +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate + +//counterfeiter:generate . SendgridClient +type SendgridClient interface { + Send(*mail.SGMailV3) (*rest.Response, error) +} + +type SendgridClientCreator func(apiKey string) SendgridClient + +func (c SendgridClientCreator) create(apiKey string) SendgridClient { + if c == nil { + c = defaultSendgridClientCreator + } + return c(apiKey) +} + +var defaultSendgridClientCreator = func(apiKey string) SendgridClient { + return sendgrid.NewSendClient(apiKey) +} + +func (m *MailSender) Send(body, subject string) error { + html := mail.NewContent("text/html", body) + + p := mail.NewPersonalization() + p.AddTos(m.recipients...) + + msg := mail.NewV3Mail(). + SetFrom(m.sender). + AddContent(html). + AddPersonalizations(p) + msg.Subject = subject + + m.Logger().WithField("message", msg).Trace("message prepared") + + client := m.SendgridClientCreator.create(m.APIKey) + res, err := client.Send(msg) + if err != nil { + return err + } + if res == nil { + return &SendError{code: -1, resBody: ""} + } + if c := res.StatusCode; c < 200 || c >= 300 { + return &SendError{code: res.StatusCode, resBody: res.Body, resHeaders: fmt.Sprintf("%#v", res.Headers)} + } + + m.Logger().Debug("mail successfully sent") + return nil +} + +type SendError struct { + code int + resBody string + resHeaders string +} + +func (e *SendError) Error() string { + return fmt.Sprintf("got code %d while sending: Body: %q, Header: %q", e.code, e.resBody, e.resHeaders) +} + +func (m *MailSender) SetSender(name, email string) error { + if email == "" { + return fmt.Errorf("email must not be empty") + } + m.sender = mail.NewEmail(name, email) + m.Logger().WithField("sender", m.sender).Debugf("sender set") + return nil +} + +func (m *MailSender) SetRecipients(recipientArgs ...string) error { + l := len(recipientArgs) + + if l%2 != 0 { + return fmt.Errorf("must be called with alternating recipient's names and email addresses") + } + + recipients := make([]*mail.Email, l/2) + + for i := range recipients { + name := recipientArgs[i*2] + email := recipientArgs[i*2+1] + if email == "" { + return fmt.Errorf("email must not be empty") + } + recipients[i] = mail.NewEmail(name, email) + } + + m.recipients = recipients + m.Logger().WithField("recipients", m.sender).Debugf("recipients set") + + return nil +} diff --git a/pkg/patch/internal/mail_sender_test.go b/pkg/patch/internal/mail_sender_test.go new file mode 100644 index 00000000000..b631d973f81 --- /dev/null +++ b/pkg/patch/internal/mail_sender_test.go @@ -0,0 +1,183 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 internal_test + +import ( + "fmt" + "testing" + + "github.com/sendgrid/rest" + "github.com/stretchr/testify/require" + "k8s.io/release/pkg/patch/internal" + "k8s.io/release/pkg/patch/internal/internalfakes" + it "k8s.io/release/pkg/patch/internal/testing" +) + +func TestMailSender(t *testing.T) { + t.Parallel() + + it.Run(t, "SetRecipients", testRecipient) + it.Run(t, "SetSender", testSender) + it.Run(t, "Send", testSend) + + it.Run(t, "main", func(t *testing.T) { + m := &internal.MailSender{ + SendgridClientCreator: func(_ string) internal.SendgridClient { + c := &internalfakes.FakeSendgridClient{} + c.SendReturns(&rest.Response{ + Body: "some API response", + StatusCode: 202, + }, nil) + return c + }, + } + require.NoError(t, m.SetSender("Jane Doe", "djane@example.org")) + require.NoError(t, m.SetRecipients("Max Mustermann", "mmustermann@example.org")) + require.NoError(t, m.Send("some content", "some subject")) + }) +} + +func testSend(t *testing.T) { + tests := map[string]struct { + sendgridSendResponse *rest.Response + sendgridSendErr error + message string + subject string + apiKey string + + expectedSendgridAPIKey string + expectedErr string + }{ + "the token is used": { + apiKey: "some key", + sendgridSendResponse: simpleRespons("", 202), + expectedSendgridAPIKey: "some key", + }, + "when #Send returns an error, bubble it up": { + sendgridSendErr: fmt.Errorf("some sendgrid err"), + expectedErr: "some sendgrid err", + }, + "when #Send returns an empty response, an error is returned": { + expectedErr: "empty API response", + }, + "when #Send returns an invalid status code, an error holding the API response is returned": { + sendgridSendResponse: simpleRespons("some API response", 500), + expectedErr: "some API response", + }, + } + + for name, tc := range tests { + tc := tc + + it.Run(t, name, func(t *testing.T) { + m := &internal.MailSender{ + APIKey: tc.apiKey, + } + + sgClient := &internalfakes.FakeSendgridClient{} + sgClient.SendReturns(tc.sendgridSendResponse, tc.sendgridSendErr) + + m.SendgridClientCreator = func(apiKey string) internal.SendgridClient { + require.Equal(t, tc.expectedSendgridAPIKey, apiKey, "SendgridClient#creator arg") + return sgClient + } + + err := m.Send(tc.message, tc.subject) + it.CheckErrSub(t, err, tc.expectedErr) + + require.Equal(t, 1, sgClient.SendCallCount(), "SendgridClient#Send call count") + + mail := sgClient.SendArgsForCall(0) + require.Equalf(t, tc.subject, mail.Subject, "the mail's subject") + require.Equalf(t, tc.message, mail.Content[0].Value, "the mail's body") + }) + } +} + +func simpleRespons(body string, code int) *rest.Response { + return &rest.Response{Body: body, StatusCode: code} +} + +func testSender(t *testing.T) { + tests := map[string]struct { + senderName string + senderEmail string + expectedErr string + }{ + "happy path": { + senderName: "name", + senderEmail: "email", + }, + "when email is empty, error": { + senderName: "name", + senderEmail: "", + expectedErr: "email must not be empty", + }, + } + + for name, tc := range tests { + tc := tc + it.Run(t, name, func(t *testing.T) { + m := &internal.MailSender{} + err := m.SetSender(tc.senderName, tc.senderEmail) + it.CheckErr(t, err, tc.expectedErr) + }) + } +} + +func testRecipient(t *testing.T) { + tests := map[string]struct { + recipientArgs [][]string + expectedErr string + }{ + "when # of recipient args is even, succeed": { + recipientArgs: [][]string{ + {}, + {"name", "email"}, + {"name", "email", "otherName", "otherEmail"}, + }, + }, + "when # of recipients args is not even, error": { + recipientArgs: [][]string{ + {"one"}, + {"one", "two", "three"}, + }, + expectedErr: "must be called with alternating recipient's names and email addresses", + }, + "when email is empty, error": { + recipientArgs: [][]string{ + {"name", ""}, + {"name", "email", "otherName", ""}, + }, + expectedErr: "email must not be empty", + }, + } + + for name, tc := range tests { + tc := tc + + it.Run(t, name, func(t *testing.T) { + for _, args := range tc.recipientArgs { + it.Run(t, "", func(t *testing.T) { + m := &internal.MailSender{} + err := m.SetRecipients(args...) + it.CheckErr(t, err, tc.expectedErr) + }) + } + }) + } +} diff --git a/pkg/patch/internal/release_notes.go b/pkg/patch/internal/release_notes.go new file mode 100644 index 00000000000..176de8fa8b5 --- /dev/null +++ b/pkg/patch/internal/release_notes.go @@ -0,0 +1,73 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 internal + +import ( + "fmt" + "path/filepath" + + "k8s.io/release/pkg/log" +) + +const relnoteScript = ` +set -euo pipefail +tmp="$( mktemp )" +trap 'rm -f -- "${tmp}"' EXIT +%q --htmlize-md --preview --quiet --markdown-file="${tmp}" >&2 +cat "${tmp}" +` + +type ReleaseNoter struct { + log.Mixin + + ReleaseToolsDir string + K8sDir string + GithubToken string + + CommandCreator CommandCreator +} + +func (r *ReleaseNoter) GetMarkdown() (string, error) { + binPath, err := filepath.Abs(filepath.Join(r.ReleaseToolsDir, "relnotes")) + if err != nil { + return "", fmt.Errorf("could not determine current working directory") + } + r.Logger().WithField("binpath", binPath).Debug("binpath set") + + cmd := r.CommandCreator.create( + "bash", "-c", fmt.Sprintf(relnoteScript, binPath), + ) + if cmd == nil { + return "", fmt.Errorf("command is nil") + } + r.Logger().Debug("command created") + + cmd.SetDir(r.K8sDir) + cmd.SetEnv([]string{ + "GITHUB_TOKEN=" + r.GithubToken, + }) + + r.Logger().WithField("workdir", r.K8sDir).Info("starting release notes gatherer ... this may take a while ...") + + s, eerr := cmdOutput(cmd) + if eerr != nil { + r.Logger().WithError(eerr).Debug("execing & getting output failed") + r.Logger().WithField("error", eerr.FullError()).Trace("full exec error") + return "", eerr + } + return s, nil +} diff --git a/pkg/patch/internal/release_notes_test.go b/pkg/patch/internal/release_notes_test.go new file mode 100644 index 00000000000..69f3ea842a1 --- /dev/null +++ b/pkg/patch/internal/release_notes_test.go @@ -0,0 +1,104 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 internal_test + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/release/pkg/patch/internal" + "k8s.io/release/pkg/patch/internal/internalfakes" + it "k8s.io/release/pkg/patch/internal/testing" + "k8s.io/utils/exec" +) + +func TestReleaseNoter(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + releaseToolsDir string + k8sDir string + githubToken string + + commandOutput []byte + commandErr error + + expectedCommandPath string + expectedErr string + expectedOutput string + }{ + "happy path": { + k8sDir: "/some/dir/k8s", + releaseToolsDir: "/some/dir/release", + githubToken: "some github token", + commandOutput: []byte("some output"), + expectedCommandPath: "/some/dir/release/relnotes", + expectedOutput: "some output", + }, + "when the command returns an error, the error bubbles up": { + commandErr: fmt.Errorf("some random error"), + expectedErr: "some random error", + expectedCommandPath: abs(t, "relnotes"), + }, + "when the release dir is a relative path": { + releaseToolsDir: "../release", + expectedCommandPath: abs(t, "../release/relnotes"), + }, + "when the k8s dir is a relative path": { + k8sDir: "../k8s", + expectedCommandPath: abs(t, "relnotes"), + }, + } + + for name, tc := range tests { + tc := tc + + it.Run(t, name, func(t *testing.T) { + command := &internalfakes.FakeCmd{} + command.OutputReturns(tc.commandOutput, tc.commandErr) + + rn := &internal.ReleaseNoter{ + K8sDir: tc.k8sDir, + ReleaseToolsDir: tc.releaseToolsDir, + GithubToken: tc.githubToken, + CommandCreator: func(exe string, args ...string) exec.Cmd { + require.Equal(t, "bash", exe) + require.Contains(t, args[1], tc.expectedCommandPath) + return command + }, + } + + output, err := rn.GetMarkdown() + it.CheckErrSub(t, err, tc.expectedErr) + require.Equal(t, tc.expectedOutput, output, "output") + require.Equal(t, tc.k8sDir, command.SetDirArgsForCall(0), "Command#SetDir arg") + require.Contains(t, command.SetEnvArgsForCall(0), "GITHUB_TOKEN="+tc.githubToken) + }) + } +} + +func abs(t *testing.T, path string) string { + t.Helper() + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("Cannot determine current working directory") + } + return filepath.Join(cwd, path) +} diff --git a/pkg/patch/internal/testing/BUILD.bazel b/pkg/patch/internal/testing/BUILD.bazel new file mode 100644 index 00000000000..3ced3b97592 --- /dev/null +++ b/pkg/patch/internal/testing/BUILD.bazel @@ -0,0 +1,23 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["testing.go"], + importpath = "k8s.io/release/pkg/patch/internal/testing", + visibility = ["//pkg/patch:__subpackages__"], + deps = ["@com_github_stretchr_testify//require:go_default_library"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/patch/internal/testing/testing.go b/pkg/patch/internal/testing/testing.go new file mode 100644 index 00000000000..aa276227480 --- /dev/null +++ b/pkg/patch/internal/testing/testing.go @@ -0,0 +1,50 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 testing + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func CheckErr(t *testing.T, err error, expectedMsg string) { + t.Helper() + if expectedMsg == "" { + require.NoError(t, err) + return + } + require.EqualError(t, err, expectedMsg) +} + +func CheckErrSub(t *testing.T, err error, expectedSubstring string) { + t.Helper() + if expectedSubstring == "" { + require.NoError(t, err) + return + } + require.Contains(t, err.Error(), expectedSubstring) +} + +// Run is a small wrapper around t.Run which enables parallel runs +// unconditionally +func Run(t *testing.T, name string, f func(*testing.T)) { + t.Run(name, func(t *testing.T) { + t.Parallel() + f(t) + }) +} diff --git a/pkg/patch/internal/workspace.go b/pkg/patch/internal/workspace.go new file mode 100644 index 00000000000..9ff4c8f2723 --- /dev/null +++ b/pkg/patch/internal/workspace.go @@ -0,0 +1,67 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 internal + +import ( + "fmt" + "strings" + + "k8s.io/release/pkg/log" +) + +type Workspace struct { + log.Mixin + + K8sRepoPath string + CommandCreator CommandCreator +} + +type status = map[string]string + +func (w *Workspace) Status() (status, error) { + execPath := "hack/print-workspace-status.sh" + + cmd := w.CommandCreator.create(execPath) + if cmd == nil { + return nil, fmt.Errorf("command is nil") + } + + cmd.SetDir(w.K8sRepoPath) + + s, eerr := cmdOutput(cmd) + if eerr != nil { + w.Logger().WithError(eerr).Debug("execing & getting output failed") + w.Logger().WithField("error", eerr.FullError()).Trace("full exec error") + return nil, eerr + } + + lines := strings.Split(s, "\n") + statuses := make(status, len(lines)) + + for _, line := range lines { + if line == "" { + continue + } + t := strings.SplitN(line, " ", 2) + if len(t) != 2 { + return nil, fmt.Errorf("cannot parse workspace status line %q", line) + } + statuses[t[0]] = t[1] + } + + return statuses, nil +} diff --git a/pkg/patch/internal/workspace_test.go b/pkg/patch/internal/workspace_test.go new file mode 100644 index 00000000000..ce8fc8ada4c --- /dev/null +++ b/pkg/patch/internal/workspace_test.go @@ -0,0 +1,86 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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 internal_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/release/pkg/patch/internal" + "k8s.io/release/pkg/patch/internal/internalfakes" + it "k8s.io/release/pkg/patch/internal/testing" + "k8s.io/utils/exec" +) + +func TestWorkspace(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + k8sRepoPath string + cmdOutput []byte + cmdErr error + + expectedStatuses map[string]string + expectedErrMsg string + }{ + "happy path": { + k8sRepoPath: "some dir", + cmdOutput: []byte("" + + "\n" + + "blipp blapp\n" + + "\n" + + "foo bar baz\n" + + "zap zip \n" + + "\n", + ), + expectedStatuses: map[string]string{ + "zap": " zip ", // not sure about that, should leading or trailing spaces be ignored? + "foo": "bar baz", + "blipp": "blapp", + }, + }, + "when the command errors, return the error": { + cmdErr: fmt.Errorf("some cmd error"), + expectedErrMsg: "some cmd error", + }, + } + + for name, tc := range tests { + tc := tc + + it.Run(t, name, func(t *testing.T) { + cmd := &internalfakes.FakeCmd{} + cmd.OutputReturns(tc.cmdOutput, tc.cmdErr) + + w := &internal.Workspace{ + K8sRepoPath: tc.k8sRepoPath, + CommandCreator: func(exe string, args ...string) exec.Cmd { + require.Equalf(t, 0, len(args), "command args count") + require.Equalf(t, "hack/print-workspace-status.sh", exe, "command executable") + return cmd + }, + } + + statuses, err := w.Status() + it.CheckErrSub(t, err, tc.expectedErrMsg) + require.Equal(t, 1, cmd.OutputCallCount(), "Command#Output call count") + require.Equal(t, tc.k8sRepoPath, cmd.SetDirArgsForCall(0), "Command#SetDir args") + require.Equal(t, tc.expectedStatuses, statuses) + }) + } +} diff --git a/repos.bzl b/repos.bzl index e8f2f586bd0..87692959275 100644 --- a/repos.bzl +++ b/repos.bzl @@ -614,8 +614,8 @@ def go_repositories(): build_file_generation = "on", build_file_proto_mode = "disable", importpath = "github.com/spf13/afero", - sum = "h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=", - version = "v1.1.2", + sum = "h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=", + version = "v1.2.2", ) go_repository( name = "com_github_spf13_cast", @@ -2016,3 +2016,27 @@ def go_repositories(): sum = "h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=", version = "v1.10.0", ) + go_repository( + name = "com_github_sendgrid_rest", + build_file_generation = "on", + build_file_proto_mode = "disable", + importpath = "github.com/sendgrid/rest", + sum = "h1:HDib/5xzQREPq34lN3YMhQtMkdXxS/qLp5G3k9a5++4=", + version = "v2.4.1+incompatible", + ) + go_repository( + name = "com_github_sendgrid_sendgrid_go", + build_file_generation = "on", + build_file_proto_mode = "disable", + importpath = "github.com/sendgrid/sendgrid-go", + sum = "h1:kosbgHyNVYVaqECDYvFVLVD9nvThweBd6xp7vaCT3GI=", + version = "v3.5.0+incompatible", + ) + go_repository( + name = "io_k8s_utils", + build_file_generation = "on", + build_file_proto_mode = "disable", + importpath = "k8s.io/utils", + sum = "h1:KCcLuc/HD1RogJgEbZi9ObRuLv1bgiRCfAbidLKrUpg=", + version = "v0.0.0-20200117235808-5f6fbceb4c31", + ) From 1b393f687287ca2867a411d267af5290051fb792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20H=C3=B6rl?= Date: Mon, 27 Jan 2020 15:50:53 +0000 Subject: [PATCH 08/10] [patch-announce] Move to golang tool Remove the bash implementation, move to the golang based tool. The tool currently still shells out and has some external dependencies. Therefore we, for now, just `go run` it in the `k8s-cloud-builder` images, where we have all the tools available. --- announce-patch | 16 ++-- gcb/patch-announce/cloudbuild.yaml | 60 ++++++++++---- patch-release/announce | 125 ----------------------------- patch-release/mail-head.md.tmpl | 7 -- patch-release/mail-style.css | 27 ------- 5 files changed, 53 insertions(+), 182 deletions(-) delete mode 100755 patch-release/announce delete mode 100644 patch-release/mail-head.md.tmpl delete mode 100644 patch-release/mail-style.css diff --git a/announce-patch b/announce-patch index 40f6832a7ad..7775ba18b26 100755 --- a/announce-patch +++ b/announce-patch @@ -43,14 +43,15 @@ #+ --freeze-date - The date we will freeze the branch and will not accept #+ cherry-picks anymore. #+ --cut-date - The date we will cut and publish the release. -#+ --from-name - The sender's name. -#+ --from-email - The sender's email address. Will also be used as a +#+ --sender-name - The sender's name. +#+ --sender-email - The sender's email address. Will also be used as a #+ receiver when not in nomock mode. #+ [--tail] - Stays attached to the cloud build process and streams #+ in the logs #+ [--k8s-git-url] - The git URL to clone kubernetes/kubernetes from. #+ [--release-git-url] - The git URL to clone kubernetes/release from. #+ [--release-git-branch] - The branch of kubernetes/release to use. +#+ [--log-level] - Set the loglevel for the patch announcement tool #+ #+ ENVIRONMENT #+ The GCP project can be changed by setting the PROJECT environment @@ -116,7 +117,9 @@ main() { return 1 fi - local subst=() + local subst=( + '_NOMOCK=false' + ) local opts=( '--no-source' "--config=${BASE_ROOT}/gcb/patch-announce/cloudbuild.yaml" @@ -126,18 +129,19 @@ main() { subst+=( "_K8S_GIT_BRANCH=${branch_name}" ) subst+=( "_FREEZE_DATE=$( flag_or_env_or_default 'freeze_date' )" ) subst+=( "_CUT_DATE=$( flag_or_env_or_default 'cut_date' )" ) - subst+=( "_FROM_NAME=$( flag_or_env_or_default 'from_name' )" ) - subst+=( "_FROM_EMAIL=$( flag_or_env_or_default 'from_email' )" ) + subst+=( "_SENDER_NAME=$( flag_or_env_or_default 'sender_name' )" ) + subst+=( "_SENDER_EMAIL=$( flag_or_env_or_default 'sender_email' )" ) # optional flags subst+=( "_K8S_GIT_URL=$( flag_or_env_or_default 'k8s_git_url' 'https://github.com/kubernetes/kubernetes' )" ) subst+=( "_RELEASE_GIT_URL=$( flag_or_env_or_default 'release_git_url' 'https://github.com/kubernetes/release' )" ) subst+=( "_RELEASE_GIT_BRANCH=$( flag_or_env_or_default 'release_git_branch' 'master' )" ) + subst+=( "_LOG_LEVEL=$( flag_or_env_or_default 'log-level' 'info' )" ) # shellcheck disable=2154 # ... because that is set by magick when sourcing common.sh if ((FLAGS_nomock)) ; then - subst+=( "_RUN_TYPE=nomock" ) + subst+=( "_NOMOCK=true" ) fi # shellcheck disable=2154 diff --git a/gcb/patch-announce/cloudbuild.yaml b/gcb/patch-announce/cloudbuild.yaml index 46f7d45b508..33f269f591c 100644 --- a/gcb/patch-announce/cloudbuild.yaml +++ b/gcb/patch-announce/cloudbuild.yaml @@ -41,19 +41,49 @@ steps: - id: prepare-and-send name: "gcr.io/${PROJECT_ID}/k8s-cloud-builder" env: - - "REL_MGR_NAME=${_REL_MGR_NAME}" - - "REL_MGR_EMAIL=${_REL_MGR_EMAIL}" - - "REL_MGR_SLACK=${_REL_MGR_SLACK}" - - "FROM_NAME=${_FROM_NAME}" - - "FROM_EMAIL=${_FROM_EMAIL}" + - "SENDER_NAME=${_SENDER_NAME}" + - "SENDER_EMAIL=${_SENDER_EMAIL}" - "FREEZE_DATE=${_FREEZE_DATE}" - "CUT_DATE=${_CUT_DATE}" - - "RUN_TYPE=${_RUN_TYPE}" + - "NOMOCK=${_NOMOCK}" + - "LOG_LEVEL=${_LOG_LEVEL}" secretEnv: - GITHUB_TOKEN - SENDGRID_API_KEY + dir: "go/src/k8s.io/release" + entrypoint: bash args: - - go/src/k8s.io/release/patch-release/announce + - -c + - | + set -e + set -u + set -o pipefail + + export GOPATH="/workspace/go" + export GOBIN="$${GOPATH}/bin" + export PATH="$${PATH}:$${GOBIN}" + + # TODO: Until we've switched away from the shell release note tool, we + # still need this binary in the PATH + go install ./cmd/blocking-testgrid-tests + + # Notes: + # - GITHUB_TOKEN & SENDGRID_API_KEY need to be picked up by the + # application from the env, we don't want to log or pass those + # sensitive values as flags. + # - Once we've removed all external dependencies and the tool does not + # shell out any more, we can also compile, bake and release this tool + # as a container image and use that instead of `go run`ing it. + go run ./cmd/krel \ + patch-announce \ + --repo=../kubernetes \ + --release-repo=. \ + --sender-name="$${SENDER_NAME}" \ + --sender-email="$${SENDER_EMAIL}" \ + --cut-date="$${CUT_DATE}" \ + --freeze-date="$${FREEZE_DATE}" \ + --log-level="$${LOG_LEVEL}" \ + --nomock="$${NOMOCK}" substitutions: # The branch of k/kubernetes to check out, the branch to announce a patch release for (e.g.: release-1.15) @@ -63,20 +93,16 @@ substitutions: # Date of planned cut, ISO 8601 (e.g.: 2019-12-11) _CUT_DATE: null # The mail sender's name. Will also be used as a receipient in mock mode. (e.g.: "Jane Doe") - _FROM_NAME: null + _SENDER_NAME: null # The mail sender's email. Will also be used as a receipient in mock mode. (e.g.: jane.doe@example.org") - _FROM_EMAIL: null - # For the real run, set to 'nomock' (e.g.: "mock" or "nomock") - _RUN_TYPE: 'mock' + _SENDER_EMAIL: null + # For the real run, set to 'nomock' (e.g.: 0,1,f,t,false,true) + _NOMOCK: '0' # git-clone'able URL for k/kubernetes _K8S_GIT_URL: https://github.com/kubernetes/kubernetes # git-clone'able URL for k/release _RELEASE_GIT_URL: https://github.com/kubernetes/release # The branch of k/release to check out _RELEASE_GIT_BRANCH: master - # For the email message: the name of the group to contact - _REL_MGR_NAME: 'Kubernetes Release Managers' - # For the email message: the email of the group to contact - _REL_MGR_EMAIL: 'release-managers@kubernetes.io' - # For the email message: the slack channel of the group to contact - _REL_MGR_SLACK: 'sig-release' + # The log level to be used with the patch-announce tool + _LOG_LEVEL: info diff --git a/patch-release/announce b/patch-release/announce deleted file mode 100755 index da8fbd2839d..00000000000 --- a/patch-release/announce +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright 2019 The Kubernetes Authors 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. - -set -e -set -u -set -o pipefail - -tmpDir="$( mktemp -d )" -trap 'rm -rf -- "$tmpDir"' EXIT - -BUILD_BASE="$(pwd)" -RELEASE_DIR="${BUILD_BASE}/go/src/k8s.io/release" -K8S_DIR="${BUILD_BASE}/go/src/k8s.io/kubernetes" - -export GOPATH="${BUILD_BASE}/go" -export PATH="${PATH}:${GOPATH}/bin" - -cd "${RELEASE_DIR}" -go install k8s.io/release/cmd/blocking-testgrid-tests - -cd "${K8S_DIR}" - -md="${tmpDir}/relnotes.md" -bash ../release/relnotes \ - --htmlize-md \ - --preview \ - --markdown-file="${md}" \ - >/dev/null - -# v1.13.10-beta.0-16-g48844ef5e7 -> v1.13.10 -UPCOMING_VERSION="$( git describe | cut -d- -f1 )" -# prepend the day of week -FREEZE_DATE="$(date -d "$FREEZE_DATE" '+%A'), ${FREEZE_DATE}" -CUT_DATE="$(date -d "$CUT_DATE" '+%A'), ${CUT_DATE}" -EMAIL_SUBJECT="Kubernetes ${UPCOMING_VERSION} cut planned for ${CUT_DATE}" - -# All vars used in the intro template (via envsubst) need to be exported. -export UPCOMING_VERSION FREEZE_DATE CUT_DATE EMAIL_SUBJECT - -# by default, send the mail to yourself -recipients="$( - jq -n \ - --arg name "$FROM_NAME" --arg email "$FROM_EMAIL" \ - '[{ "to": [{name:$name, email:$email}] }]' -)" - -# if we run with nomock mode, actually send to the mailinglists -if [ "${RUN_TYPE}" = 'nomock' ] -then - echo >&2 'Running with --nomock, setting recipients to the k8s google groups' - recipients='[{ - "to": [ - { - "name": "Kubernetes developer/contributor discussion", - "email": "kubernetes-dev@googlegroups.com" - },{ - "name": "kubernetes-dev-announce", - "email": "kubernetes-dev-announce@googlegroups.com" - } - ] - }]' -fi - -buildEmailMd() { - cat "${RELEASE_DIR}/patch-release/mail-head.md.tmpl" | envsubst - echo '' ; echo '----' ; echo '' - cat "$md" -} - -emailBody="$( - buildEmailMd \ - | pandoc \ - -s \ - --metadata pagetitle="$EMAIL_SUBJECT" \ - --columns=100000 \ - -f gfm /dev/stdin \ - -H "${RELEASE_DIR}/patch-release/mail-style.css" \ - -t html5 -o - -)" - -# shellcheck disable=SC2016 -# ... because that's the template we will use with jq. -sendgridPayloadTmpl='{ - "personalizations": $recipients, - "from": {"email": $fromEmail, "name": $fromName}, - "subject": $subject, - "content": [ - {"type": "text/html", "value": env.emailBody} - ] -}' - -# Safe that in a file, in case it gets big -sendgridPayload="${tmpDir}/sendgridPayload.json" - -# 'emailBody' needs to be in the env when we run jq with the -# 'sendgridPayloadTmpl' template -emailBody="$emailBody" \ - jq -n \ - --argjson recipients "$recipients" \ - --arg fromName "$FROM_NAME" \ - --arg fromEmail "$FROM_EMAIL" \ - --arg subject "$EMAIL_SUBJECT" \ - "$sendgridPayloadTmpl" \ - > "$sendgridPayload" - - -echo >&2 "Curling the sendgrid API with '$sendgridPayload'" -curl --silent --show-error --fail \ - --url https://api.sendgrid.com/v3/mail/send \ - --header "Authorization: Bearer ${SENDGRID_API_KEY}" \ - --header 'Content-Type: application/json' \ - --data "@${sendgridPayload}" diff --git a/patch-release/mail-head.md.tmpl b/patch-release/mail-head.md.tmpl deleted file mode 100644 index 2b5cd72053e..00000000000 --- a/patch-release/mail-head.md.tmpl +++ /dev/null @@ -1,7 +0,0 @@ -Below is a draft of the generated changelog for ${UPCOMING_VERSION}. If you submitted a cherrypick, please make sure it's listed and has an **accurate release note**. - -If you have a pending cherrypick for ${UPCOMING_VERSION}, make sure it merges by end of day on **${FREEZE_DATE}**. -Please tag `@kubernetes/patch-release-team` on the GitHub issue/PR, email [${REL_MGR_NAME}](mailto:${REL_MGR_EMAIL}), or reach out in [#${REL_MGR_SLACK}](https://kubernetes.slack.com/messages/${REL_MGR_SLACK}/) on Slack if your cherrypick appears to be blocked on something out of your control. - -If you've already spoken to the patch release team about PRs that are not yet merged or listed below, don't worry, we're tracking them. - diff --git a/patch-release/mail-style.css b/patch-release/mail-style.css deleted file mode 100644 index 65f15803be6..00000000000 --- a/patch-release/mail-style.css +++ /dev/null @@ -1,27 +0,0 @@ - From d15f30b2e845d87f104feaacf9100c276c1cfe8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20H=C3=B6rl?= Date: Thu, 30 Jan 2020 15:21:08 +0000 Subject: [PATCH 09/10] [patch-announce] Replace command to submit the announcemnt job to GCB This is deliberatly NOT a krel subcommand, as this is just a small wrapper around `gcloud` for submitting the a job running `krel patch-announce ...`. --- BUILD.bazel | 1 + announce-patch | 158 ----------------------------- cmd/krel/cmd/root.go | 10 +- cmd/patch-announce/BUILD.bazel | 34 +++++++ cmd/patch-announce/main.go | 179 +++++++++++++++++++++++++++++++++ pkg/log/log.go | 12 +++ 6 files changed, 227 insertions(+), 167 deletions(-) delete mode 100755 announce-patch create mode 100644 cmd/patch-announce/BUILD.bazel create mode 100755 cmd/patch-announce/main.go diff --git a/BUILD.bazel b/BUILD.bazel index ea3a18408ad..244fabaddfb 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -30,6 +30,7 @@ filegroup( "//cmd/blocking-testgrid-tests:all-srcs", "//cmd/krel:all-srcs", "//cmd/kubepkg:all-srcs", + "//cmd/patch-announce:all-srcs", "//cmd/release-notes:all-srcs", "//lib:all-srcs", "//pkg/command:all-srcs", diff --git a/announce-patch b/announce-patch deleted file mode 100755 index 7775ba18b26..00000000000 --- a/announce-patch +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright 2019 The Kubernetes Authors 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. -# - -#+ NAME -#+ $PROG - generate and send the patch release announcement mail -#+ -#+ SYNOPSIS -#+ $PROG \ -#+ --freeze-date=[freeze-date] --cut-date=[cut-date] \ -#+ --from-name=[sender-name] --from-email=[sender-email] \ -#+ [release-branch] -#+ -#+ DESCRIPTION -#+ This tool generates the patch release announcement and posts it to -#+ the 'kubernetes-dev@googlegroups.com' & 'kubernetes-dev-announce@googlegroups.com' -#+ lists. -#+ -#+ The mails hold the freeze date, cut date, a preview of the changelog and a -#+ list of open cherry-pick PRs. -#+ -#+ The mail is sent out via sendgrid, GCB does not allow to send mail directly. -#+ -#+ OPTIONS -#+ release-branch - The branch we want to cut from, e.g.: 'release-1.15'. -#+ --nomock - By default the mail will be sent to the mail address -#+ that is set as the sender (--from-email). When this -#+ flag is set, we send it to the kubernetes mailing -#+ lists. -#+ --freeze-date - The date we will freeze the branch and will not accept -#+ cherry-picks anymore. -#+ --cut-date - The date we will cut and publish the release. -#+ --sender-name - The sender's name. -#+ --sender-email - The sender's email address. Will also be used as a -#+ receiver when not in nomock mode. -#+ [--tail] - Stays attached to the cloud build process and streams -#+ in the logs -#+ [--k8s-git-url] - The git URL to clone kubernetes/kubernetes from. -#+ [--release-git-url] - The git URL to clone kubernetes/release from. -#+ [--release-git-branch] - The branch of kubernetes/release to use. -#+ [--log-level] - Set the loglevel for the patch announcement tool -#+ -#+ ENVIRONMENT -#+ The GCP project can be changed by setting the PROJECT environment -#+ variable. It defaults to 'kubernetes-release-test'. -#+ -#+ All command line flags can also be provided by setting an environment -#+ variable, e.g. instead of using `--freeze-date` flag you can also use -#+ the environment variable `FREEZE_DATE`. This works for all flags but not -#+ for the release-branch argument, this one must explicitly specified as a -#+ command line argument. - -set -e -# set -u -set -o pipefail - -readonly PROG="${0##*/}" -readonly PROJECT="${PROJECT:-kubernetes-release-test}" - -readonly BASE_ROOT="$(dirname "$(readlink -e "${BASH_SOURCE[0]}" 2>&1)")" -# shellcheck source=./lib/common.sh -source "${BASE_ROOT}/lib/common.sh" - -# For some reason, using $FLAG_xxx seems to replace ' ' with '\n'. Sendgrid -# does not really like names with newlines in it and treats them as two -# separate recipients. -# Newlines do not make too much sense for other variables, so we globally -# replace newlines with spaces. -replace_nl() { - tr -s $'\n' ' ' -} - -flag_or_env_or_default() { - local -r flag_name="${1//-/_}" - local -r default="${2:-}" - - local -r flag_var_name="FLAGS_${flag_name}" - local -r env_var_name="${flag_name^^}" - - if [ -n "${!flag_var_name:-}" ] ; then - echo -n "${!flag_var_name}" | replace_nl - return - fi - - if [ -n "${!env_var_name:-}" ] ; then - echo -n "${!env_var_name}" | replace_nl - return - fi - - if [ -n "$default" ] ; then - echo -n "$default" | replace_nl - return - fi - - >&2 echo "${FATAL} flag --${flag_name//_/-} or setting '\$${env_var_name}' is mandatory" - return 1 -} - -main() { - local branch_name="${POSITIONAL_ARGV[0]}" - - if [ -z "$branch_name" ] ; then - common::manpage -help - return 1 - fi - - local subst=( - '_NOMOCK=false' - ) - local opts=( - '--no-source' - "--config=${BASE_ROOT}/gcb/patch-announce/cloudbuild.yaml" - ) - - # mandatory flags - subst+=( "_K8S_GIT_BRANCH=${branch_name}" ) - subst+=( "_FREEZE_DATE=$( flag_or_env_or_default 'freeze_date' )" ) - subst+=( "_CUT_DATE=$( flag_or_env_or_default 'cut_date' )" ) - subst+=( "_SENDER_NAME=$( flag_or_env_or_default 'sender_name' )" ) - subst+=( "_SENDER_EMAIL=$( flag_or_env_or_default 'sender_email' )" ) - - # optional flags - subst+=( "_K8S_GIT_URL=$( flag_or_env_or_default 'k8s_git_url' 'https://github.com/kubernetes/kubernetes' )" ) - subst+=( "_RELEASE_GIT_URL=$( flag_or_env_or_default 'release_git_url' 'https://github.com/kubernetes/release' )" ) - subst+=( "_RELEASE_GIT_BRANCH=$( flag_or_env_or_default 'release_git_branch' 'master' )" ) - subst+=( "_LOG_LEVEL=$( flag_or_env_or_default 'log-level' 'info' )" ) - - # shellcheck disable=2154 - # ... because that is set by magick when sourcing common.sh - if ((FLAGS_nomock)) ; then - subst+=( "_NOMOCK=true" ) - fi - - # shellcheck disable=2154 - # ... because that is set by magick when sourcing common.sh - if ! ((FLAGS_tail)) ; then - opts+=( '--async' ) - fi - - gcloud "--project=${PROJECT}" builds submit \ - "${opts[@]}" \ - --substitutions "$( common::join ',' "${subst[@]}" )" -} - -main "$@" diff --git a/cmd/krel/cmd/root.go b/cmd/krel/cmd/root.go index 7b27c71d06b..1fbe03fec93 100644 --- a/cmd/krel/cmd/root.go +++ b/cmd/krel/cmd/root.go @@ -64,13 +64,5 @@ func initConfig() { } func initLogging(*cobra.Command, []string) error { - logrus.SetFormatter(&logrus.TextFormatter{DisableTimestamp: true}) - lvl, err := logrus.ParseLevel(rootOpts.logLevel) - if err != nil { - return err - } - logrus.SetLevel(lvl) - logrus.AddHook(log.NewFilenameHook()) - logrus.Debugf("Using log level %q", lvl) - return nil + return log.SetupGlobalLogger(rootOpts.logLevel) } diff --git a/cmd/patch-announce/BUILD.bazel b/cmd/patch-announce/BUILD.bazel new file mode 100644 index 00000000000..d89052772bd --- /dev/null +++ b/cmd/patch-announce/BUILD.bazel @@ -0,0 +1,34 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "go_default_library", + srcs = ["main.go"], + importpath = "k8s.io/release/cmd/patch-announce", + visibility = ["//visibility:private"], + deps = [ + "//pkg/log:go_default_library", + "//pkg/patch:go_default_library", + "@com_github_sirupsen_logrus//:go_default_library", + "@com_github_spf13_cobra//:go_default_library", + ], +) + +go_binary( + name = "patch-announce", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/cmd/patch-announce/main.go b/cmd/patch-announce/main.go new file mode 100755 index 00000000000..88adcd07356 --- /dev/null +++ b/cmd/patch-announce/main.go @@ -0,0 +1,179 @@ +/* +Copyright 2020 The Kubernetes Authors. + +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" + "os/exec" + "path/filepath" + "runtime" + "strings" + "syscall" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "k8s.io/release/pkg/log" + "k8s.io/release/pkg/patch" +) + +type opts struct { + patch.AnnounceOptions + K8sRepoURL string + K8sBranch string + ReleaseRepoURL string + ReleaseBranch string + ProjectID string + BuildConfigPath string + Loglevel string + Tail bool +} + +const ( + defaultK8sRepoURL = "https://github.com/kubernetes/kubernetes" + defaultK8sBranch = "master" + defaultReleaseRepoURL = "https://github.com/kubernetes/release" + defaultReleaseBranch = "master" + defaultGCPorjectID = "kubernetes-release-test" + defaultLogLevel = "info" + + // separator which hopefully never appears in any of our keys/values. + sep = "\001\002\001" +) + +func main() { + cmd := getCommand() + if err := cmd.Execute(); err != nil { + os.Exit(1) + } +} + +func getCommand() *cobra.Command { + opts := opts{} + + cmd := &cobra.Command{ + Use: "patch-announce ", + Long: "submits a GCB job to run `krel patch-announce` in the clouds", + SilenceUsage: true, + Args: cobra.ExactArgs(0), + } + + cmd.Flags().StringVarP(&opts.SenderName, "sender-name", "n", "", "email sender's name") + cmd.Flags().StringVarP(&opts.SenderEmail, "sender-email", "m", "", "email sender's address") + cmd.Flags().StringVarP(&opts.FreezeDate, "freeze-date", "f", "", "date when no CPs are allowed anymore") + cmd.Flags().StringVarP(&opts.CutDate, "cut-date", "c", "", "date when the patch release is planned to be cut") + cmd.Flags().StringVarP(&opts.BuildConfigPath, "config", "C", "", "file path to the patch-announce cloudbuild.yaml") + cmd.Flags().StringVarP(&opts.ProjectID, "project-id", "p", defaultGCPorjectID, "Google Project ID") + cmd.Flags().StringVarP(&opts.K8sRepoURL, "kubernetes-repo-url", "r", defaultK8sRepoURL, `git URL for the kubernetes repo ("k/k")`) + cmd.Flags().StringVarP(&opts.K8sBranch, "kubernetes-branch", "b", defaultK8sBranch, `branch to checkout for the kubernetes repo ("k/k")`) + cmd.Flags().StringVarP(&opts.ReleaseRepoURL, "release-repo-url", "R", defaultReleaseRepoURL, `git URL for the release repo ("k/release)`) + cmd.Flags().StringVarP(&opts.ReleaseBranch, "release-branch", "B", defaultReleaseBranch, `branch to checkout for the release repo ("k/release")`) + cmd.Flags().StringVarP(&opts.Loglevel, "log-level", "l", defaultLogLevel, "log level on the GCB job") + cmd.Flags().BoolVarP(&opts.Tail, "tail", "t", false, "tail the build") + cmd.Flags().BoolVar(&opts.Nomock, "nomock", false, `run in nomock (="real") mode or not`) + + cmd.PersistentPreRunE = func(_ *cobra.Command, _ []string) error { + return log.SetupGlobalLogger(opts.Loglevel) + } + + cmd.PreRunE = func(cmd *cobra.Command, _ []string) error { + for _, f := range []string{"sender-name", "sender-email", "freeze-date", "cut-date"} { + if err := cmd.MarkFlagRequired(f); err != nil { + return err + } + } + + if opts.BuildConfigPath == "" { + p, err := kReleaseLocalPath() + if err != nil { + return err + } + opts.BuildConfigPath = filepath.Join(p, "..", "..", "gcb", "patch-announce", "cloudbuild.yaml") + logrus.Debugf("no config filed specified, defaulting to %q", opts.BuildConfigPath) + } + + return nil + } + + cmd.RunE = func(cmd *cobra.Command, _ []string) error { + exeName := "gcloud" + exe, err := exec.LookPath(exeName) + if err != nil { + return err + } + + args := []string{ + exeName, "builds", "submit", + "--no-source", + "--project=" + opts.ProjectID, + "--config=" + opts.BuildConfigPath, + } + + if !opts.Tail { + args = append(args, "--async") + } + + subst := substitutions{ + "_K8S_GIT_URL": opts.K8sRepoURL, + "_K8S_GIT_BRANCH": opts.K8sBranch, + "_FREEZE_DATE": opts.FreezeDate, + "_CUT_DATE": opts.CutDate, + "_SENDER_NAME": opts.SenderName, + "_SENDER_EMAIL": opts.SenderEmail, + "_RELEASE_GIT_URL": opts.ReleaseRepoURL, + "_RELEASE_GIT_BRANCH": opts.ReleaseBranch, + "_LOG_LEVEL": opts.Loglevel, + "_NOMOCK": fmt.Sprintf("%t", opts.Nomock), + } + logrus.Debugf("about to run %q with substitutions [%s]", args, subst.human()) + + args = append(args, "--substitutions="+subst.string()) + + logrus.Infof("execing %q", exeName) + return syscall.Exec(exe, args, os.Environ()) + } + + return cmd +} + +type substitutions map[string]string + +func (s substitutions) join(sep string) string { + a := []string{} + + for k, v := range s { + a = append(a, k+"="+v) + } + + return strings.Join(a, sep) +} + +func (s substitutions) string() string { + // https://cloud.google.com/sdk/gcloud/reference/topic/escaping + return "^" + sep + "^" + s.join(sep) +} + +func (s substitutions) human() string { + return s.join(", ") +} + +func kReleaseLocalPath() (string, error) { + if _, filename, _, ok := runtime.Caller(0); ok { + return filepath.Dir(filename), nil + } + return "", fmt.Errorf("could not find the local path to k/release") +} diff --git a/pkg/log/log.go b/pkg/log/log.go index 5b3ae8d761e..e10475ec103 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -28,6 +28,18 @@ const ( logTraceSep = "." ) +func SetupGlobalLogger(level string) error { + logrus.SetFormatter(&logrus.TextFormatter{DisableTimestamp: true}) + lvl, err := logrus.ParseLevel(level) + if err != nil { + return err + } + logrus.SetLevel(lvl) + logrus.AddHook(NewFilenameHook()) + logrus.Debugf("Using log level %q", lvl) + return nil +} + // AddTracePath adds a path element to the logrus entry's field 'trace'. This // is meant to be done everytime you hand off a logger/entry to a different // component to have a clear trace how we ended up here. When logs are emitted From 2b8cce8e748db71b77f39e380a51f9197acf4f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20H=C3=B6rl?= Date: Wed, 5 Feb 2020 11:25:26 +0000 Subject: [PATCH 10/10] [krel] Remove registering hooks on cobra's init 1. It was a no-op anyway 1. It was called for each sub-command, called by the package's `init` function If we want to bring that back, we should do it deliberately. --- cmd/krel/cmd/changelog.go | 2 -- cmd/krel/cmd/ff.go | 2 -- cmd/krel/cmd/patch-announce.go | 2 -- cmd/krel/cmd/root.go | 6 ------ cmd/krel/cmd/version.go | 1 - 5 files changed, 13 deletions(-) diff --git a/cmd/krel/cmd/changelog.go b/cmd/krel/cmd/changelog.go index f9ce7f31508..7a415e550a0 100644 --- a/cmd/krel/cmd/changelog.go +++ b/cmd/krel/cmd/changelog.go @@ -89,8 +89,6 @@ const ( ) func init() { - cobra.OnInitialize(initConfig) - const ( tagFlag = "tag" tarsFlag = "tars" diff --git a/cmd/krel/cmd/ff.go b/cmd/krel/cmd/ff.go index 5d962ec78c6..adb26db4c6e 100644 --- a/cmd/krel/cmd/ff.go +++ b/cmd/krel/cmd/ff.go @@ -53,8 +53,6 @@ var ffCmd = &cobra.Command{ } func init() { - cobra.OnInitialize(initConfig) - ffCmd.PersistentFlags().StringVar(&ffOpts.branch, "branch", "", "branch") ffCmd.PersistentFlags().StringVar(&ffOpts.masterRef, "ref", kgit.DefaultMasterRef, "ref on master") ffCmd.PersistentFlags().StringVar(&ffOpts.org, "org", kgit.DefaultGithubOrg, "org to run tool against") diff --git a/cmd/krel/cmd/patch-announce.go b/cmd/krel/cmd/patch-announce.go index 682c22e6410..294c985add8 100644 --- a/cmd/krel/cmd/patch-announce.go +++ b/cmd/krel/cmd/patch-announce.go @@ -41,8 +41,6 @@ func patchAnnounceCommand() *cobra.Command { Args: cobra.MaximumNArgs(0), // no additional/positional args allowed } - cobra.OnInitialize(initConfig) // ? - // setup local flags cmd.PersistentFlags().StringVarP(&opts.SenderName, "sender-name", "n", "", "email sender's name") cmd.PersistentFlags().StringVarP(&opts.SenderEmail, "sender-email", "e", "", "email sender's address") diff --git a/cmd/krel/cmd/root.go b/cmd/krel/cmd/root.go index 1fbe03fec93..1598bd509e2 100644 --- a/cmd/krel/cmd/root.go +++ b/cmd/krel/cmd/root.go @@ -51,18 +51,12 @@ func Execute() { } func init() { - cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().BoolVar(&rootOpts.nomock, "nomock", false, "nomock flag") rootCmd.PersistentFlags().BoolVar(&rootOpts.cleanup, "cleanup", false, "cleanup flag") rootCmd.PersistentFlags().StringVar(&rootOpts.repoPath, "repo", filepath.Join(os.TempDir(), "k8s"), "the local path to the repository to be used") rootCmd.PersistentFlags().StringVar(&rootOpts.logLevel, "log-level", "info", "the logging verbosity, either 'panic', 'fatal', 'error', 'warn', 'warning', 'info', 'debug' or 'trace'") } -// initConfig reads in config file and ENV variables if set. -func initConfig() { -} - func initLogging(*cobra.Command, []string) error { return log.SetupGlobalLogger(rootOpts.logLevel) } diff --git a/cmd/krel/cmd/version.go b/cmd/krel/cmd/version.go index cbf326417dc..7591e40e777 100644 --- a/cmd/krel/cmd/version.go +++ b/cmd/krel/cmd/version.go @@ -44,7 +44,6 @@ var versionCmd = &cobra.Command{ } func init() { - cobra.OnInitialize(initConfig) versionCmd.PersistentFlags().BoolVarP(&versionOpts.json, "json", "j", false, "print JSON instead of text") rootCmd.AddCommand(versionCmd)