From 8b4a0fb70613e8f29ba507c874e923248a9f7337 Mon Sep 17 00:00:00 2001 From: Stephen Augustus Date: Wed, 17 Nov 2021 17:26:33 -0500 Subject: [PATCH 1/5] kpromo: Add `krel promote-images` functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From k/release at 8742f143657ad3be35ad6c044e74e87f91cf39dc. Signed-off-by: Stephen Augustus Co-authored-by: Adolfo GarcĂ­a Veytia (Puerco) Co-authored-by: Sascha Grunert Co-authored-by: Carlos Panato Co-authored-by: Cecile Robert-Michon Co-authored-by: Nabarun Pal --- cmd/kpromo/cmd/pr/pr.go | 358 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 cmd/kpromo/cmd/pr/pr.go diff --git a/cmd/kpromo/cmd/pr/pr.go b/cmd/kpromo/cmd/pr/pr.go new file mode 100644 index 00000000..630e1fdc --- /dev/null +++ b/cmd/kpromo/cmd/pr/pr.go @@ -0,0 +1,358 @@ +/* +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 ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "k8s.io/release/pkg/release" + reg "sigs.k8s.io/promo-tools/v3/legacy/dockerregistry" + "sigs.k8s.io/release-sdk/git" + "sigs.k8s.io/release-sdk/github" + "sigs.k8s.io/release-utils/util" +) + +const ( + k8sioRepo = "k8s.io" + k8sioDefaultBranch = "main" + promotionBranchSuffix = "-image-promotion" + defaultProject = release.Kubernetes + defaultReviewers = "@kubernetes/release-engineering" +) + +// promoteCommand is the krel subcommand to promote conainer images +var imagePromoteCommand = &cobra.Command{ + Use: "promote-images", + Short: "Starts an image promotion for a tag of kubernetes or kubernetes-sigs images", + Long: `krel promote + +The 'promote' subcommand of krel updates the image promoter manifests +and creates a PR in kubernetes/k8s.io`, + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + // Run the PR creation function + return runPromote(promoteOpts) + }, +} + +type promoteOptions struct { + project string + userFork string + tags []string + reviewers string + interactiveMode bool +} + +func (o *promoteOptions) Validate() error { + if len(o.tags) == 0 { + return errors.New("cannot start promotion --tag is required") + } + if o.userFork == "" { + return errors.New("cannot start promotion --fork is required") + } + + // Check the fork slug + if _, _, err := git.ParseRepoSlug(o.userFork); err != nil { + return errors.Wrap(err, "checking user's fork") + } + + // Verify we got a valid tag + for _, tag := range o.tags { + if _, err := util.TagStringToSemver(tag); err != nil { + return errors.Wrapf(err, "verifying tag: %s", tag) + } + } + + // Check that the GitHub token is set + token, isSet := os.LookupEnv(github.TokenEnvKey) + if !isSet || token == "" { + return fmt.Errorf("cannot promote images if GitHub token env var %s is not set", github.TokenEnvKey) + } + return nil +} + +var promoteOpts = &promoteOptions{} + +func init() { + imagePromoteCommand.PersistentFlags().StringVar( + &promoteOpts.project, + "project", + defaultProject, + "the name of the project to promote images for", + ) + + imagePromoteCommand.PersistentFlags().StringSliceVarP( + &promoteOpts.tags, + "tag", + "t", + []string{}, + "version tag of the images we will promote", + ) + + imagePromoteCommand.PersistentFlags().StringVar( + &promoteOpts.userFork, + "fork", + "", + "the user's fork of kubernetes/k8s.io", + ) + + imagePromoteCommand.PersistentFlags().StringVar( + &promoteOpts.reviewers, + "reviewers", + defaultReviewers, + "the list of GitHub users or teams to assign to the PR", + ) + + imagePromoteCommand.PersistentFlags().BoolVarP( + &promoteOpts.interactiveMode, + "interactive", + "i", + false, + "interactive mode, asks before every step", + ) + + for _, flagName := range []string{"tag", "fork"} { + if err := imagePromoteCommand.MarkPersistentFlagRequired(flagName); err != nil { + logrus.Error(errors.Wrapf(err, "marking tag %s as required", flagName)) + } + } + + rootCmd.AddCommand(imagePromoteCommand) +} + +func runPromote(opts *promoteOptions) error { + // Check the cmd line opts + if err := opts.Validate(); err != nil { + return errors.Wrap(err, "checking command line options") + } + + ctx := context.Background() + + // Validate options + branchname := opts.project + "-" + opts.tags[0] + promotionBranchSuffix + + // Get the github org and repo from the fork slug + userForkOrg, userForkRepo, err := git.ParseRepoSlug(opts.userFork) + if err != nil { + return errors.Wrap(err, "parsing user's fork") + } + if userForkRepo == "" { + userForkRepo = k8sioRepo + } + + // Check Environment + gh := github.New() + + // Verify the repository is a fork of k8s.io + if err = verifyFork( + branchname, userForkOrg, userForkRepo, git.DefaultGithubOrg, k8sioRepo, + ); err != nil { + return errors.Wrapf(err, "while checking fork of %s/%s ", git.DefaultGithubOrg, k8sioRepo) + } + + // Clone k8s.io + repo, err := prepareFork(branchname, git.DefaultGithubOrg, k8sioRepo, userForkOrg, userForkRepo) + if err != nil { + return errors.Wrap(err, "while preparing k/k8s.io fork") + } + + defer func() { + if mustRun(opts, "Clean fork directory?") { + err = repo.Cleanup() + } else { + logrus.Infof("All modified files will be left untouched in %s", repo.Dir()) + } + }() + + // Path to the promoter image list + imagesListPath := filepath.Join( + release.GCRIOPathProd, + "images", + filepath.Base(release.GCRIOPathStagingPrefix)+opts.project, + "images.yaml", + ) + + // Read the current manifest to check later if new images come up + oldlist := make([]byte, 0) + + // Run the promoter manifest grower + if mustRun(opts, "Grow the manifests to add the new tags?") { + if util.Exists(filepath.Join(repo.Dir(), imagesListPath)) { + logrus.Debug("Reading the current image promoter manifest (image list)") + oldlist, err = os.ReadFile(filepath.Join(repo.Dir(), imagesListPath)) + if err != nil { + return errors.Wrap(err, "while reading the current promoter image list") + } + } + + for _, tag := range opts.tags { + opt := reg.GrowManifestOptions{} + if err := opt.Populate( + filepath.Join(repo.Dir(), release.GCRIOPathProd), + release.GCRIOPathStagingPrefix+opts.project, "", "", tag); err != nil { + return errors.Wrapf(err, "populating image promoter options for tag %s", tag) + } + + if err := opt.Validate(); err != nil { + return errors.Wrapf(err, "validate promoter options for tag %s", tag) + } + + logrus.Infof("Growing manifests with images matching tag %s", tag) + if err := reg.GrowManifest(ctx, &opt); err != nil { + return errors.Wrapf(err, "Growing manifest with tag %s", tag) + } + } + } + + // Re-write the image list without the mock images + rawImageList, err := release.NewPromoterImageListFromFile(filepath.Join(repo.Dir(), imagesListPath)) + if err != nil { + return errors.Wrap(err, "parsing the current manifest") + } + + // Create a new imagelist to copy the non-mock images + newImageList := &release.ImagePromoterImages{} + + // Copy all non mock-images: + for _, imageData := range *rawImageList { + if !strings.Contains(imageData.Name, "mock/") { + *newImageList = append(*newImageList, imageData) + } + } + + // Write the modified manifest + if err := newImageList.Write(filepath.Join(repo.Dir(), imagesListPath)); err != nil { + return errors.Wrap(err, "while writing the promoter image list") + } + + // Check if the image list was modified + if len(oldlist) > 0 { + logrus.Debug("Checking if the image list was modified") + // read the newly modified manifest + newlist, err := os.ReadFile(filepath.Join(repo.Dir(), imagesListPath)) + if err != nil { + return errors.Wrap(err, "while reading the modified manifest images list") + } + + // If the manifest was not modified, exit now + if string(newlist) == string(oldlist) { + logrus.Info("No changes detected in the promoter images list, exiting without changes") + return nil + } + } + + // add the modified manifest to staging + logrus.Debugf("Adding %s to staging area", imagesListPath) + if err := repo.Add(imagesListPath); err != nil { + return errors.Wrap(err, "adding image manifest to staging area") + } + + commitMessage := "releng: Image promotion for " + opts.project + " " + strings.Join(opts.tags, " / ") + + // Commit files + logrus.Debug("Creating commit") + if err := repo.UserCommit(commitMessage); err != nil { + return errors.Wrapf(err, "Error creating commit in %s/%s", git.DefaultGithubOrg, k8sioRepo) + } + + // Push to fork + if mustRun(opts, fmt.Sprintf("Push changes to user's fork at %s/%s?", userForkOrg, userForkRepo)) { + logrus.Infof("Pushing manifest changes to %s/%s", userForkOrg, userForkRepo) + if err := repo.PushToRemote(userForkName, branchname); err != nil { + return errors.Wrapf(err, "pushing %s to %s/%s", userForkName, userForkOrg, userForkRepo) + } + } else { + // Exit if no push was made + + logrus.Infof("Exiting without creating a PR since changes were not pushed to %s/%s", userForkOrg, userForkRepo) + return nil + } + + // Create the Pull Request + if mustRun(opts, "Create pull request?") { + pr, err := gh.CreatePullRequest( + git.DefaultGithubOrg, k8sioRepo, k8sioDefaultBranch, + fmt.Sprintf("%s:%s", userForkOrg, branchname), + commitMessage, generatePRBody(opts), + ) + if err != nil { + return errors.Wrap(err, "creating the pull request in k/k8s.io") + } + logrus.Infof( + "Successfully created PR: %s%s/%s/pull/%d", + github.GitHubURL, git.DefaultGithubOrg, k8sioRepo, pr.GetNumber(), + ) + } + + // Success! + return nil +} + +// mustRun avoids running when a users chooses n in interactive mode +func mustRun(opts *promoteOptions, question string) bool { + if !opts.interactiveMode { + return true + } + _, success, err := util.Ask(fmt.Sprintf("%s (Y/n)", question), "y:Y:yes|n:N:no|y", 10) + if err != nil { + logrus.Error(err) + if err.(util.UserInputError).IsCtrlC() { + os.Exit(1) + } + return false + } + if success { + return true + } + return false +} + +// generatePRBody creates the body of the Image Promotion Pull Request +func generatePRBody(opts *promoteOptions) string { + args := fmt.Sprintf("--fork %s", opts.userFork) + if opts.interactiveMode { + args += " --interactive" + } + + if opts.project != defaultProject { + args += " --project" + opts.project + } + + if opts.reviewers != defaultReviewers { + args += " --reviewers" + opts.reviewers + } + + for _, tag := range opts.tags { + args += " --tag " + tag + } + + prBody := fmt.Sprintf("Image promotion for %s %s\n", opts.project, strings.Join(opts.tags, " / ")) + prBody += "This is an automated PR generated from `krel The Kubernetes Release Toolbox`\n" + prBody += fmt.Sprintf("```\nkrel promote-images %s\n```\n\n", args) + prBody += fmt.Sprintf("/hold\ncc: %s\n", opts.reviewers) + + return prBody +} From 7a361676dfb7b252540726b21549c3a398c7b7f8 Mon Sep 17 00:00:00 2001 From: Stephen Augustus Date: Wed, 17 Nov 2021 18:20:14 -0500 Subject: [PATCH 2/5] kpromo(pr): Remove dependency on k8s.io/release Signed-off-by: Stephen Augustus --- cmd/kpromo/cmd/pr/pr.go | 210 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 201 insertions(+), 9 deletions(-) diff --git a/cmd/kpromo/cmd/pr/pr.go b/cmd/kpromo/cmd/pr/pr.go index 630e1fdc..e29f3b59 100644 --- a/cmd/kpromo/cmd/pr/pr.go +++ b/cmd/kpromo/cmd/pr/pr.go @@ -21,13 +21,14 @@ import ( "fmt" "os" "path/filepath" + "sort" "strings" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "gopkg.in/yaml.v2" - "k8s.io/release/pkg/release" reg "sigs.k8s.io/promo-tools/v3/legacy/dockerregistry" "sigs.k8s.io/release-sdk/git" "sigs.k8s.io/release-sdk/github" @@ -38,8 +39,9 @@ const ( k8sioRepo = "k8s.io" k8sioDefaultBranch = "main" promotionBranchSuffix = "-image-promotion" - defaultProject = release.Kubernetes - defaultReviewers = "@kubernetes/release-engineering" + // TODO: Consider a more descriptive name for this constant + defaultProject = Kubernetes + defaultReviewers = "@kubernetes/release-engineering" ) // promoteCommand is the krel subcommand to promote conainer images @@ -189,9 +191,9 @@ func runPromote(opts *promoteOptions) error { // Path to the promoter image list imagesListPath := filepath.Join( - release.GCRIOPathProd, + GCRIOPathProd, "images", - filepath.Base(release.GCRIOPathStagingPrefix)+opts.project, + filepath.Base(GCRIOPathStagingPrefix)+opts.project, "images.yaml", ) @@ -211,8 +213,8 @@ func runPromote(opts *promoteOptions) error { for _, tag := range opts.tags { opt := reg.GrowManifestOptions{} if err := opt.Populate( - filepath.Join(repo.Dir(), release.GCRIOPathProd), - release.GCRIOPathStagingPrefix+opts.project, "", "", tag); err != nil { + filepath.Join(repo.Dir(), GCRIOPathProd), + GCRIOPathStagingPrefix+opts.project, "", "", tag); err != nil { return errors.Wrapf(err, "populating image promoter options for tag %s", tag) } @@ -228,13 +230,13 @@ func runPromote(opts *promoteOptions) error { } // Re-write the image list without the mock images - rawImageList, err := release.NewPromoterImageListFromFile(filepath.Join(repo.Dir(), imagesListPath)) + rawImageList, err := NewPromoterImageListFromFile(filepath.Join(repo.Dir(), imagesListPath)) if err != nil { return errors.Wrap(err, "parsing the current manifest") } // Create a new imagelist to copy the non-mock images - newImageList := &release.ImagePromoterImages{} + newImageList := &ImagePromoterImages{} // Copy all non mock-images: for _, imageData := range *rawImageList { @@ -356,3 +358,193 @@ func generatePRBody(opts *promoteOptions) string { return prBody } + +// TODO: Consider moving this section to sigs.k8s.io/release-sdk + +// Copied from https://github.com/kubernetes/release/blob/df4a45eead2cfb79deb1337a9817e137c9739d41/cmd/krel/cmd/release_notes.go + +const ( + // userForkName The name we will give to the user's remote when adding it to repos + userForkName = "userfork" +) + +// prepareFork Prepare a branch a repo +func prepareFork(branchName, upstreamOrg, upstreamRepo, myOrg, myRepo string) (repo *git.Repo, err error) { + // checkout the upstream repository + logrus.Infof("Cloning/updating repository %s/%s", upstreamOrg, upstreamRepo) + + repo, err = git.CleanCloneGitHubRepo( + upstreamOrg, upstreamRepo, false, + ) + if err != nil { + return nil, errors.Wrapf(err, "cloning %s/%s", upstreamOrg, upstreamRepo) + } + + // test if the fork remote is already existing + url := git.GetRepoURL(myOrg, myRepo, false) + if repo.HasRemote(userForkName, url) { + logrus.Infof( + "Using already existing remote %s (%s) in repository", + userForkName, url, + ) + } else { + // add the user's fork as a remote + err = repo.AddRemote(userForkName, myOrg, myRepo) + if err != nil { + return nil, errors.Wrap(err, "adding user's fork as remote repository") + } + } + + // checkout the new branch + err = repo.Checkout("-B", branchName) + if err != nil { + return nil, errors.Wrapf(err, "creating new branch %s", branchName) + } + + return repo, nil +} + +// verifyFork does a pre-check of a fork to see if we can create a PR from it +func verifyFork(branchName, forkOwner, forkRepo, parentOwner, parentRepo string) error { + logrus.Infof("Checking if a PR can be created from %s/%s", forkOwner, forkRepo) + gh := github.New() + + // Check th PR + isrepo, err := gh.RepoIsForkOf( + forkOwner, forkRepo, parentOwner, parentRepo, + ) + if err != nil { + return errors.Wrapf( + err, "while checking if repository is a fork of %s/%s", + parentOwner, parentRepo, + ) + } + + if !isrepo { + return errors.Errorf( + "cannot create PR, %s/%s is not a fork of %s/%s", + forkOwner, forkRepo, parentOwner, parentRepo, + ) + } + + // verify the branch does not previously exist + branchExists, err := gh.BranchExists( + forkOwner, forkRepo, branchName, + ) + if err != nil { + return errors.Wrap(err, "while checking if branch can be created") + } + + if branchExists { + return errors.Errorf( + "a branch named %s already exists in %s/%s", + branchName, forkOwner, forkRepo, + ) + } + return nil +} + +// TODO: Consider moving this section to its own package + +// Copied from https://github.com/kubernetes/release/blob/971affe6bdc00c8cdb770c4b7930584e2d13a8eb/pkg/release/release.go + +const ( + // name of the kubernetes project + // TODO: Consider a more descriptive name for this constant + Kubernetes = "kubernetes" + + // Production registry root URL + GCRIOPathProd = "k8s.gcr.io" + + // Staging registry root URL prefix + GCRIOPathStagingPrefix = "gcr.io/k8s-staging-" +) + +// NewPromoterImageListFromFile parses an image promoter manifest file +func NewPromoterImageListFromFile(manifestPath string) (imagesList *ImagePromoterImages, err error) { + if !util.Exists(manifestPath) { + return nil, errors.New("could not find image promoter manifest") + } + yamlCode, err := os.ReadFile(manifestPath) + if err != nil { + return nil, errors.Wrap(err, "reading yaml code from file") + } + + imagesList = &ImagePromoterImages{} + if err := imagesList.Parse(yamlCode); err != nil { + return nil, errors.Wrap(err, "parsing manifest yaml") + } + + return imagesList, nil +} + +// ImagePromoterImages abtracts the manifest used by the image promoter +type ImagePromoterImages []struct { + Name string `json:"name"` + DMap map[string][]string `json:"dmap"` // eg "sha256:ef9493aff21f7e368fb3968b46ff2542b0f6863a5de2b9bc58d8d151d8b0232c": ["v1.17.12-rc.0"] +} + +// Parse reads yaml code into an ImagePromoterManifest object +func (imagesList *ImagePromoterImages) Parse(yamlCode []byte) error { + if err := yaml.Unmarshal(yamlCode, imagesList); err != nil { + return err + } + return nil +} + +// Write writes the promoter image list into an YAML file. +func (imagesList *ImagePromoterImages) Write(filePath string) error { + yamlCode, err := imagesList.ToYAML() + if err != nil { + return errors.Wrap(err, "while marshalling image list") + } + // Write the yaml into the specified file + if err := os.WriteFile(filePath, yamlCode, os.FileMode(0o644)); err != nil { + return errors.Wrap(err, "writing yaml code into file") + } + + return nil +} + +// ToYAML serializes an image list into an YAML file. +// We serialize the data by hand to emulate the way it's done by the image promoter +func (imagesList *ImagePromoterImages) ToYAML() ([]byte, error) { + // The image promoter code sorts images by: + // 1. Name 2. Digest SHA (asc) 3. Tag + + // First, sort by name (sort #1) + sort.Slice(*imagesList, func(i, j int) bool { + return (*imagesList)[i].Name < (*imagesList)[j].Name + }) + + // Let's build the YAML code + yamlCode := "" + for _, imgData := range *imagesList { + // Add the new name key (it is not sorted in the promoter code) + yamlCode += fmt.Sprintf("- name: %s\n", imgData.Name) + yamlCode += " dmap:\n" + + // Now, lets sort by the digest sha (sort #2) + keys := make([]string, 0, len(imgData.DMap)) + for k := range imgData.DMap { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, digestSHA := range keys { + // Finally, sort bt tag (sort #3) + tags := imgData.DMap[digestSHA] + sort.Strings(tags) + yamlCode += fmt.Sprintf(" %q: [", digestSHA) + for i, tag := range tags { + if i > 0 { + yamlCode += "," + } + yamlCode += fmt.Sprintf("%q", tag) + } + yamlCode += "]\n" + } + } + + return []byte(yamlCode), nil +} From 8bda48b158fd390563c60af6201cc04bc66c85a9 Mon Sep 17 00:00:00 2001 From: Stephen Augustus Date: Wed, 17 Nov 2021 18:33:00 -0500 Subject: [PATCH 3/5] kpromo(pr): Enable as subcommand of `kpromo` Signed-off-by: Stephen Augustus --- cmd/kpromo/cmd/pr/pr.go | 34 ++++++++++++++++------------------ cmd/kpromo/cmd/root.go | 2 ++ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/cmd/kpromo/cmd/pr/pr.go b/cmd/kpromo/cmd/pr/pr.go index e29f3b59..73dc0e6a 100644 --- a/cmd/kpromo/cmd/pr/pr.go +++ b/cmd/kpromo/cmd/pr/pr.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cmd +package pr import ( "context" @@ -44,14 +44,14 @@ const ( defaultReviewers = "@kubernetes/release-engineering" ) -// promoteCommand is the krel subcommand to promote conainer images -var imagePromoteCommand = &cobra.Command{ - Use: "promote-images", - Short: "Starts an image promotion for a tag of kubernetes or kubernetes-sigs images", - Long: `krel promote +// PRCmd is the kpromo subcommand to promote container images +var PRCmd = &cobra.Command{ + Use: "pr", + Short: "Starts an image promotion for a given image tag", + Long: `kpromo pr -The 'promote' subcommand of krel updates the image promoter manifests -and creates a PR in kubernetes/k8s.io`, +This command updates image promoter manifests and creates a PR in +kubernetes/k8s.io`, SilenceUsage: true, SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { @@ -99,14 +99,14 @@ func (o *promoteOptions) Validate() error { var promoteOpts = &promoteOptions{} func init() { - imagePromoteCommand.PersistentFlags().StringVar( + PRCmd.PersistentFlags().StringVar( &promoteOpts.project, "project", defaultProject, "the name of the project to promote images for", ) - imagePromoteCommand.PersistentFlags().StringSliceVarP( + PRCmd.PersistentFlags().StringSliceVarP( &promoteOpts.tags, "tag", "t", @@ -114,21 +114,21 @@ func init() { "version tag of the images we will promote", ) - imagePromoteCommand.PersistentFlags().StringVar( + PRCmd.PersistentFlags().StringVar( &promoteOpts.userFork, "fork", "", "the user's fork of kubernetes/k8s.io", ) - imagePromoteCommand.PersistentFlags().StringVar( + PRCmd.PersistentFlags().StringVar( &promoteOpts.reviewers, "reviewers", defaultReviewers, "the list of GitHub users or teams to assign to the PR", ) - imagePromoteCommand.PersistentFlags().BoolVarP( + PRCmd.PersistentFlags().BoolVarP( &promoteOpts.interactiveMode, "interactive", "i", @@ -137,12 +137,10 @@ func init() { ) for _, flagName := range []string{"tag", "fork"} { - if err := imagePromoteCommand.MarkPersistentFlagRequired(flagName); err != nil { + if err := PRCmd.MarkPersistentFlagRequired(flagName); err != nil { logrus.Error(errors.Wrapf(err, "marking tag %s as required", flagName)) } } - - rootCmd.AddCommand(imagePromoteCommand) } func runPromote(opts *promoteOptions) error { @@ -352,8 +350,8 @@ func generatePRBody(opts *promoteOptions) string { } prBody := fmt.Sprintf("Image promotion for %s %s\n", opts.project, strings.Join(opts.tags, " / ")) - prBody += "This is an automated PR generated from `krel The Kubernetes Release Toolbox`\n" - prBody += fmt.Sprintf("```\nkrel promote-images %s\n```\n\n", args) + prBody += "This is an automated PR generated from `kpromo`\n" + prBody += fmt.Sprintf("```\nkpromo pr %s\n```\n\n", args) prBody += fmt.Sprintf("/hold\ncc: %s\n", opts.reviewers) return prBody diff --git a/cmd/kpromo/cmd/root.go b/cmd/kpromo/cmd/root.go index f50e7996..89921747 100644 --- a/cmd/kpromo/cmd/root.go +++ b/cmd/kpromo/cmd/root.go @@ -25,6 +25,7 @@ import ( "sigs.k8s.io/promo-tools/v3/cmd/kpromo/cmd/cip" "sigs.k8s.io/promo-tools/v3/cmd/kpromo/cmd/gh" "sigs.k8s.io/promo-tools/v3/cmd/kpromo/cmd/manifest" + "sigs.k8s.io/promo-tools/v3/cmd/kpromo/cmd/pr" "sigs.k8s.io/promo-tools/v3/cmd/kpromo/cmd/run" "sigs.k8s.io/promo-tools/v3/cmd/kpromo/cmd/version" "sigs.k8s.io/release-utils/log" @@ -77,6 +78,7 @@ func init() { rootCmd.AddCommand(cip.CipCmd) rootCmd.AddCommand(gh.GHCmd) rootCmd.AddCommand(manifest.ManifestCmd) + rootCmd.AddCommand(pr.PRCmd) rootCmd.AddCommand(version.VersionCmd) } From c298abda9c9d6bfb87dba80de8fdf37ce1e0c03a Mon Sep 17 00:00:00 2001 From: Stephen Augustus Date: Wed, 17 Nov 2021 19:26:08 -0500 Subject: [PATCH 4/5] kpromo(pr): Move image promoter constructs to separate package Signed-off-by: Stephen Augustus --- cmd/kpromo/cmd/pr/pr.go | 125 +++---------------------------- image/image.go | 133 +++++++++++++++++++++++++++++++++ image/image_test.go | 159 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 301 insertions(+), 116 deletions(-) create mode 100644 image/image.go create mode 100644 image/image_test.go diff --git a/cmd/kpromo/cmd/pr/pr.go b/cmd/kpromo/cmd/pr/pr.go index 73dc0e6a..89f3fe14 100644 --- a/cmd/kpromo/cmd/pr/pr.go +++ b/cmd/kpromo/cmd/pr/pr.go @@ -21,14 +21,13 @@ import ( "fmt" "os" "path/filepath" - "sort" "strings" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "gopkg.in/yaml.v2" + "sigs.k8s.io/promo-tools/v3/image" reg "sigs.k8s.io/promo-tools/v3/legacy/dockerregistry" "sigs.k8s.io/release-sdk/git" "sigs.k8s.io/release-sdk/github" @@ -39,9 +38,8 @@ const ( k8sioRepo = "k8s.io" k8sioDefaultBranch = "main" promotionBranchSuffix = "-image-promotion" - // TODO: Consider a more descriptive name for this constant - defaultProject = Kubernetes - defaultReviewers = "@kubernetes/release-engineering" + defaultProject = image.StagingRepoSuffix + defaultReviewers = "@kubernetes/release-engineering" ) // PRCmd is the kpromo subcommand to promote container images @@ -189,9 +187,9 @@ func runPromote(opts *promoteOptions) error { // Path to the promoter image list imagesListPath := filepath.Join( - GCRIOPathProd, + image.ProdRegistry, "images", - filepath.Base(GCRIOPathStagingPrefix)+opts.project, + filepath.Base(image.StagingRepoPrefix)+opts.project, "images.yaml", ) @@ -211,8 +209,8 @@ func runPromote(opts *promoteOptions) error { for _, tag := range opts.tags { opt := reg.GrowManifestOptions{} if err := opt.Populate( - filepath.Join(repo.Dir(), GCRIOPathProd), - GCRIOPathStagingPrefix+opts.project, "", "", tag); err != nil { + filepath.Join(repo.Dir(), image.ProdRegistry), + image.StagingRepoPrefix+opts.project, "", "", tag); err != nil { return errors.Wrapf(err, "populating image promoter options for tag %s", tag) } @@ -228,13 +226,13 @@ func runPromote(opts *promoteOptions) error { } // Re-write the image list without the mock images - rawImageList, err := NewPromoterImageListFromFile(filepath.Join(repo.Dir(), imagesListPath)) + rawImageList, err := image.NewManifestListFromFile(filepath.Join(repo.Dir(), imagesListPath)) if err != nil { return errors.Wrap(err, "parsing the current manifest") } // Create a new imagelist to copy the non-mock images - newImageList := &ImagePromoterImages{} + newImageList := &image.ManifestList{} // Copy all non mock-images: for _, imageData := range *rawImageList { @@ -441,108 +439,3 @@ func verifyFork(branchName, forkOwner, forkRepo, parentOwner, parentRepo string) } return nil } - -// TODO: Consider moving this section to its own package - -// Copied from https://github.com/kubernetes/release/blob/971affe6bdc00c8cdb770c4b7930584e2d13a8eb/pkg/release/release.go - -const ( - // name of the kubernetes project - // TODO: Consider a more descriptive name for this constant - Kubernetes = "kubernetes" - - // Production registry root URL - GCRIOPathProd = "k8s.gcr.io" - - // Staging registry root URL prefix - GCRIOPathStagingPrefix = "gcr.io/k8s-staging-" -) - -// NewPromoterImageListFromFile parses an image promoter manifest file -func NewPromoterImageListFromFile(manifestPath string) (imagesList *ImagePromoterImages, err error) { - if !util.Exists(manifestPath) { - return nil, errors.New("could not find image promoter manifest") - } - yamlCode, err := os.ReadFile(manifestPath) - if err != nil { - return nil, errors.Wrap(err, "reading yaml code from file") - } - - imagesList = &ImagePromoterImages{} - if err := imagesList.Parse(yamlCode); err != nil { - return nil, errors.Wrap(err, "parsing manifest yaml") - } - - return imagesList, nil -} - -// ImagePromoterImages abtracts the manifest used by the image promoter -type ImagePromoterImages []struct { - Name string `json:"name"` - DMap map[string][]string `json:"dmap"` // eg "sha256:ef9493aff21f7e368fb3968b46ff2542b0f6863a5de2b9bc58d8d151d8b0232c": ["v1.17.12-rc.0"] -} - -// Parse reads yaml code into an ImagePromoterManifest object -func (imagesList *ImagePromoterImages) Parse(yamlCode []byte) error { - if err := yaml.Unmarshal(yamlCode, imagesList); err != nil { - return err - } - return nil -} - -// Write writes the promoter image list into an YAML file. -func (imagesList *ImagePromoterImages) Write(filePath string) error { - yamlCode, err := imagesList.ToYAML() - if err != nil { - return errors.Wrap(err, "while marshalling image list") - } - // Write the yaml into the specified file - if err := os.WriteFile(filePath, yamlCode, os.FileMode(0o644)); err != nil { - return errors.Wrap(err, "writing yaml code into file") - } - - return nil -} - -// ToYAML serializes an image list into an YAML file. -// We serialize the data by hand to emulate the way it's done by the image promoter -func (imagesList *ImagePromoterImages) ToYAML() ([]byte, error) { - // The image promoter code sorts images by: - // 1. Name 2. Digest SHA (asc) 3. Tag - - // First, sort by name (sort #1) - sort.Slice(*imagesList, func(i, j int) bool { - return (*imagesList)[i].Name < (*imagesList)[j].Name - }) - - // Let's build the YAML code - yamlCode := "" - for _, imgData := range *imagesList { - // Add the new name key (it is not sorted in the promoter code) - yamlCode += fmt.Sprintf("- name: %s\n", imgData.Name) - yamlCode += " dmap:\n" - - // Now, lets sort by the digest sha (sort #2) - keys := make([]string, 0, len(imgData.DMap)) - for k := range imgData.DMap { - keys = append(keys, k) - } - sort.Strings(keys) - - for _, digestSHA := range keys { - // Finally, sort bt tag (sort #3) - tags := imgData.DMap[digestSHA] - sort.Strings(tags) - yamlCode += fmt.Sprintf(" %q: [", digestSHA) - for i, tag := range tags { - if i > 0 { - yamlCode += "," - } - yamlCode += fmt.Sprintf("%q", tag) - } - yamlCode += "]\n" - } - } - - return []byte(yamlCode), nil -} diff --git a/image/image.go b/image/image.go new file mode 100644 index 00000000..795c9093 --- /dev/null +++ b/image/image.go @@ -0,0 +1,133 @@ +/* +Copyright 2021 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 image + +import ( + "fmt" + "os" + "sort" + + "github.com/pkg/errors" + "gopkg.in/yaml.v2" + + "sigs.k8s.io/release-utils/util" +) + +const ( + // Production registry root URL + ProdRegistry = "k8s.gcr.io" + + // Staging repository root URL prefix + StagingRepoPrefix = "gcr.io/k8s-staging-" + + // The suffix of the default image repository to promote images from + // i.e., gcr.io/- + // e.g., gcr.io/k8s-staging-foo + StagingRepoSuffix = "kubernetes" +) + +// ManifestList abstracts the manifest used by the image promoter +type ManifestList []struct { + Name string `json:"name"` + + // A digest to tag(s) map used in the promoter manifest e.g., + // "sha256:ef9493aff21f7e368fb3968b46ff2542b0f6863a5de2b9bc58d8d151d8b0232c": ["v1.17.12-rc.0", "foo", "bar"] + DMap map[string][]string `json:"dmap"` +} + +// NewManifestListFromFile parses an image promoter manifest file +func NewManifestListFromFile(manifestPath string) (imagesList *ManifestList, err error) { + if !util.Exists(manifestPath) { + return nil, errors.New("could not find image promoter manifest") + } + yamlCode, err := os.ReadFile(manifestPath) + if err != nil { + return nil, errors.Wrap(err, "reading yaml code from file") + } + + imagesList = &ManifestList{} + if err := imagesList.Parse(yamlCode); err != nil { + return nil, errors.Wrap(err, "parsing manifest yaml") + } + + return imagesList, nil +} + +// Parse reads yaml code into an ImagePromoterManifest object +func (imagesList *ManifestList) Parse(yamlCode []byte) error { + if err := yaml.Unmarshal(yamlCode, imagesList); err != nil { + return err + } + return nil +} + +// Write writes the promoter image list into an YAML file. +func (imagesList *ManifestList) Write(filePath string) error { + yamlCode, err := imagesList.ToYAML() + if err != nil { + return errors.Wrap(err, "while marshalling image list") + } + // Write the yaml into the specified file + if err := os.WriteFile(filePath, yamlCode, os.FileMode(0o644)); err != nil { + return errors.Wrap(err, "writing yaml code into file") + } + + return nil +} + +// ToYAML serializes an image list into an YAML file. +// We serialize the data by hand to emulate the way it's done by the image promoter +func (imagesList *ManifestList) ToYAML() ([]byte, error) { + // The image promoter code sorts images by: + // 1. Name 2. Digest SHA (asc) 3. Tag + + // First, sort by name (sort #1) + sort.Slice(*imagesList, func(i, j int) bool { + return (*imagesList)[i].Name < (*imagesList)[j].Name + }) + + // Let's build the YAML code + yamlCode := "" + for _, imgData := range *imagesList { + // Add the new name key (it is not sorted in the promoter code) + yamlCode += fmt.Sprintf("- name: %s\n", imgData.Name) + yamlCode += " dmap:\n" + + // Now, lets sort by the digest sha (sort #2) + keys := make([]string, 0, len(imgData.DMap)) + for k := range imgData.DMap { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, digestSHA := range keys { + // Finally, sort bt tag (sort #3) + tags := imgData.DMap[digestSHA] + sort.Strings(tags) + yamlCode += fmt.Sprintf(" %q: [", digestSHA) + for i, tag := range tags { + if i > 0 { + yamlCode += "," + } + yamlCode += fmt.Sprintf("%q", tag) + } + yamlCode += "]\n" + } + } + + return []byte(yamlCode), nil +} diff --git a/image/image_test.go b/image/image_test.go new file mode 100644 index 00000000..9e720357 --- /dev/null +++ b/image/image_test.go @@ -0,0 +1,159 @@ +/* +Copyright 2021 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 image + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewManifestListFromFile(t *testing.T) { + listYAML := "- name: pause\n" + listYAML += " dmap:\n" + listYAML += " \"sha256:927d98197ec1141a368550822d18fa1c60bdae27b78b0c004f705f548c07814f\": [\"3.2\"]\n" + listYAML += " \"sha256:a319ac2280eb7e3a59e252e54b76327cb4a33cf8389053b0d78277f22bbca2fa\": [\"3.3\"]\n" + + tempFile, err := os.CreateTemp("", "release-test") + require.Nil(t, err, "creating temp file") + defer os.Remove(tempFile.Name()) + + _, err = tempFile.Write([]byte(listYAML)) + require.Nil(t, err, "wrinting temporary promoter image list") + + imageList, err := NewManifestListFromFile(tempFile.Name()) + require.Nil(t, err) + + require.Equal(t, 1, len(*imageList)) + require.Equal(t, 2, len((*imageList)[0].DMap)) +} + +func TestPromoterImageParse(t *testing.T) { + listYAML := "- name: kube-apiserver-amd64\n dmap:\n" + listYAML += " \"sha256:365063a9b0df28cb8b72525138214079085ce8376e47a8654e34d16766c432f9\": [\"v1.18.9-rc.0\"]\n" + listYAML += " \"sha256:3c65dfd9682ca03989ac8ae9db230ea908e2ba00a8db002b33b09ca577f5c05c\": [\"v1.19.2-rc.0\"]\n" + listYAML += " \"sha256:43374266764aee719ce342c3611d34a12c68315a64a4197a2571b7434bb42f82\": [\"v1.19.1\"]\n" + listYAML += " \"sha256:4da7d4a9176971d2af0a5e4bd6f764677648db4ad2814574fbb76962458c7bbb\": [\"v1.19.0-rc.2\"]\n" + listYAML += " \"sha256:4fd1a6d25b5fe5db3647ed1d368c671b618efafb6ddbe06fc64697d2ba271aa9\": [\"v1.18.8\"]\n" + listYAML += " \"sha256:5b6b95cc8c06262719d10149964ca59496b234e28ef3e3fcdf7323f46c83ce04\": [\"v1.19.0-rc.4\"]\n" + listYAML += " \"sha256:6257f45b4908eed0a4b84d8efeaf2751096ce516006daf74690b321b785e6cc4\": [\"v1.19.0\"]\n" + listYAML += "- name: pause\n dmap:\n" + listYAML += " \"sha256:927d98197ec1141a368550822d18fa1c60bdae27b78b0c004f705f548c07814f\": [\"3.2\"]\n" + listYAML += " \"sha256:a319ac2280eb7e3a59e252e54b76327cb4a33cf8389053b0d78277f22bbca2fa\": [\"3.3\"]\n" + + imageList := &ManifestList{} + err := imageList.Parse([]byte(listYAML)) + require.Nil(t, err, "parsing image list yaml") + + require.Equal(t, 2, len(*imageList)) + require.Equal(t, 7, len((*imageList)[0].DMap)) + require.Equal(t, 2, len((*imageList)[1].DMap)) + require.Equal(t, "kube-apiserver-amd64", (*imageList)[0].Name) + require.Equal(t, "pause", (*imageList)[1].Name) + require.Equal(t, "v1.19.0", (*imageList)[0].DMap["sha256:6257f45b4908eed0a4b84d8efeaf2751096ce516006daf74690b321b785e6cc4"][0]) + require.Equal(t, "3.3", (*imageList)[1].DMap["sha256:a319ac2280eb7e3a59e252e54b76327cb4a33cf8389053b0d78277f22bbca2fa"][0]) +} + +func TestPromoterImageToYAML(t *testing.T) { + imageList := &ManifestList{ + struct { + Name string "json:\"name\"" + DMap map[string][]string "json:\"dmap\"" + }{ + Name: "hyperkube", + DMap: map[string][]string{ + "sha256:54cdd8d3b74f9c577c8bb4f43e50813f0190006e66efe861bd810ee3f5e7cc7d": {"v1.18.8"}, + "sha256:03427dcf5ab5fc5fd3cdfb24170373e8afbed13356270666c823573d7e2a1342": {"v1.16.16-rc.0"}, + "sha256:9f35b65ee834239ffbbd0ddfb54e0317cf99f10a75d8e8af372af45286d069ab": {"v1.17.10"}, + }, + }, + struct { + Name string "json:\"name\"" + DMap map[string][]string "json:\"dmap\"" + }{ + Name: "conformance", + DMap: map[string][]string{ + "sha256:17fcac56c871a58a093ff36915816161b1dbbb9eca0add9c968d9c27c4ba1881": {"v1.19.0"}, + }, + }, + struct { + Name string "json:\"name\"" + DMap map[string][]string "json:\"dmap\"" + }{ + Name: "kube-proxy", + DMap: map[string][]string{ + "sha256:c752ecbd04bc4517168a19323bb60fb45324eee1e480b2b97d3fd6ea0a54f42d": {"v1.18.8", "v1.19.0"}, + }, + }, + } + + // Expected yaml output, must be sorted correctly, according to the image promoter sort order + expectedYAML := "- name: conformance\n dmap:\n" + expectedYAML += " \"sha256:17fcac56c871a58a093ff36915816161b1dbbb9eca0add9c968d9c27c4ba1881\": [\"v1.19.0\"]\n" + expectedYAML += "- name: hyperkube\n dmap:\n" + expectedYAML += " \"sha256:03427dcf5ab5fc5fd3cdfb24170373e8afbed13356270666c823573d7e2a1342\": [\"v1.16.16-rc.0\"]\n" + expectedYAML += " \"sha256:54cdd8d3b74f9c577c8bb4f43e50813f0190006e66efe861bd810ee3f5e7cc7d\": [\"v1.18.8\"]\n" + expectedYAML += " \"sha256:9f35b65ee834239ffbbd0ddfb54e0317cf99f10a75d8e8af372af45286d069ab\": [\"v1.17.10\"]\n" + expectedYAML += "- name: kube-proxy\n dmap:\n" + expectedYAML += " \"sha256:c752ecbd04bc4517168a19323bb60fb45324eee1e480b2b97d3fd6ea0a54f42d\": [\"v1.18.8\",\"v1.19.0\"]\n" + + yamlCode, err := imageList.ToYAML() + require.Nil(t, err, "serilizing imagelist to yaml") + require.Equal(t, expectedYAML, string(yamlCode), "checking promoter image list yaml output") +} + +func TestPromoterImageWrite(t *testing.T) { + imageList := &ManifestList{ + struct { + Name string "json:\"name\"" + DMap map[string][]string "json:\"dmap\"" + }{ + Name: "kube-controller-manager-s390x", + DMap: map[string][]string{ + "sha256:594b8333e79ecca96c9ff0cb72a001db181c199d83274ffbe5ccdaedca23bfd7": {"v1.19.1"}, + }, + }, + struct { + Name string "json:\"name\"" + DMap map[string][]string "json:\"dmap\"" + }{ + Name: "kube-scheduler", + DMap: map[string][]string{ + "sha256:022b81d70447014f63fdc734df48cb9e3a2854c48f65acdca67aac5c1974fc22": {"v1.19.0-rc.2"}, + }, + }, + } + + expectedFile := "- name: kube-controller-manager-s390x\n dmap:\n" + expectedFile += " \"sha256:594b8333e79ecca96c9ff0cb72a001db181c199d83274ffbe5ccdaedca23bfd7\": [\"v1.19.1\"]\n" + expectedFile += "- name: kube-scheduler\n dmap:\n" + expectedFile += " \"sha256:022b81d70447014f63fdc734df48cb9e3a2854c48f65acdca67aac5c1974fc22\": [\"v1.19.0-rc.2\"]\n" + + tempFile, err := os.CreateTemp("", "release-test") + require.Nil(t, err, "creating temp file") + defer os.Remove(tempFile.Name()) + + err = imageList.Write(tempFile.Name()) + require.Nil(t, err, "writing data to disk") + + // Read back the file to see if it correct + fileContents, err := os.ReadFile(tempFile.Name()) + require.Nil(t, err, "reading temporary file") + + require.Equal(t, expectedFile, string(fileContents)) +} From 5eb42fcbe3777253280df4a7daf48c7c845fcc00 Mon Sep 17 00:00:00 2001 From: Stephen Augustus Date: Wed, 17 Nov 2021 19:50:17 -0500 Subject: [PATCH 5/5] kpromo: Build v3.3.0-beta.2-1 image Signed-off-by: Stephen Augustus --- VERSION | 2 +- cloudbuild.yaml | 2 +- dependencies.yaml | 4 ++-- workspace_status.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/VERSION b/VERSION index 77535e4b..1bf0a931 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.0-beta.1 +3.3.0-beta.2 diff --git a/cloudbuild.yaml b/cloudbuild.yaml index 0cb99177..1cc797ee 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -31,7 +31,7 @@ substitutions: # vYYYYMMDD-hash, and can be used as a substitution _GIT_TAG: '12345' _PULL_BASE_REF: 'dev' - _IMG_VERSION: 'v3.3.0-beta.1-3' + _IMG_VERSION: 'v3.3.0-beta.2-1' tags: - 'kpromo' diff --git a/dependencies.yaml b/dependencies.yaml index add32f17..e79fac08 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -1,7 +1,7 @@ dependencies: # Release version - name: "repo release version" - version: 3.3.0-beta.1 + version: 3.3.0-beta.2 refPaths: - path: VERSION @@ -57,7 +57,7 @@ dependencies: match: go \d+.\d+ - name: "k8s.gcr.io/artifact-promoter/kpromo" - version: v3.3.0-beta.1-3 + version: v3.3.0-beta.2-1 refPaths: - path: cloudbuild.yaml match: "_IMG_VERSION: 'v((([0-9]+)\\.([0-9]+)\\.([0-9]+)(?:-([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)-([0-9]+)'" diff --git a/workspace_status.sh b/workspace_status.sh index a7f4359f..c6640148 100755 --- a/workspace_status.sh +++ b/workspace_status.sh @@ -65,7 +65,7 @@ p_ IMG_REGISTRY gcr.io p_ IMG_REPOSITORY k8s-staging-artifact-promoter p_ IMG_NAME kpromo p_ IMG_TAG "${image_tag}" -p_ IMG_VERSION v3.3.0-beta.1-3 +p_ IMG_VERSION v3.3.0-beta.2-1 p_ TEST_AUDIT_PROD_IMG_REPOSITORY us.gcr.io/k8s-gcr-audit-test-prod p_ TEST_AUDIT_STAGING_IMG_REPOSITORY gcr.io/k8s-gcr-audit-test-prod p_ TEST_AUDIT_PROJECT_ID k8s-gcr-audit-test-prod