diff --git a/.koapps.yaml b/.koapps.yaml index bdd61a6fdba0..1e2143207583 100644 --- a/.koapps.yaml +++ b/.koapps.yaml @@ -12,7 +12,6 @@ apps: - ko://github.com/kyma-project/test-infra/cmd/tools/ipcleaner - ko://github.com/kyma-project/test-infra/cmd/tools/orphanremover - ko://github.com/kyma-project/test-infra/cmd/tools/dnscollector - - ko://github.com/kyma-project/test-infra/cmd/tools/gcrcleaner - ko://github.com/kyma-project/test-infra/cmd/tools/externalsecretschecker - ko://github.com/kyma-project/test-infra/cmd/gardener-rotate - ko://github.com/kyma-project/test-infra/cmd/cloud-run/gardener-sa-rotate diff --git a/cmd/tools/gcrcleaner/README.md b/cmd/tools/gcrcleaner/README.md deleted file mode 100644 index 225a6bcf3521..000000000000 --- a/cmd/tools/gcrcleaner/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# GCR Cleaner - -## Overview - -This command finds and removes old GCR images created by Jobs in the Google Cloud project. - -There are two conditions used to find images for removal: -- The repository name pattern is not on the ignored list. -- The `creationTimestamp` value of the images, which is used to find addresses, exists at least for a preconfigured number of hours. - -GCR images that meet all these conditions are subject to removal. - -## Usage - -For safety reasons, the dry-run mode is the default one. -To run it, use: -```bash -env GOOGLE_APPLICATION_CREDENTIALS={PATH_TO_SERVICE_ACCOUNT_FILE} go run main.go \ - --repository={GCLOUD_REPOSITORY_URL} -``` - -To turn the dry-run mode off, use: -```bash -env GOOGLE_APPLICATION_CREDENTIALS={PATH_TO_SERVICE_ACCOUNT_FILE} go run main.go \ - --repository={GCLOUD_REPOSITORY_URL} \ - --dry-run=false -``` - -### Flags - -See the list of available flags: - -| Name | Required | Description | -| :------------------------ | :------: | :--------------------------------------------------------------------------------------------------- | -| **--repository** | Yes | The GCR repository name. -| **--dry-run** | No | The boolean value that controls the dry-run mode. Defaults to `true`. -| **--age-in-hours** | No | The integer value for the number of hours. It only matches images older than `now()-ageInHours`. Defaults to `24`. -| **--gcr-exclude-name-regex** | Yes | The string value with a valid Go regexp. Used to exclude matched repositories by their name. - -### Environment Variables - -See the list of available environment variables: - -| Name | Required | Description | -| :------------------------------------ | :------: | :--------------------------------------------------------------------------------------------------- | -| **GOOGLE_APPLICATION_CREDENTIALS** | Yes | The path to the service account file. The service account requires at least the `browser` and `roles/storage.legacyBucketOwner` Google IAM roles. | diff --git a/cmd/tools/gcrcleaner/main.go b/cmd/tools/gcrcleaner/main.go deleted file mode 100644 index c6d7862bd82b..000000000000 --- a/cmd/tools/gcrcleaner/main.go +++ /dev/null @@ -1,74 +0,0 @@ -// See https://cloud.google.com/docs/authentication/. -// Use GOOGLE_APPLICATION_CREDENTIALS environment variable to specify -// a service account key file to authenticate to the API. -package main - -import ( - "context" - "flag" - "fmt" - "os" - "regexp" - - "github.com/kyma-project/test-infra/pkg/tools/common" - "github.com/kyma-project/test-infra/pkg/tools/gcrcleaner" - - gcrgoogle "github.com/google/go-containerregistry/pkg/v1/google" - log "github.com/sirupsen/logrus" -) - -const defaultGcrNameIgnoreRegex = "" - -var ( - repository = flag.String("repository", "", "Name of GCR repository [Required]") - dryRun = flag.Bool("dry-run", true, "Dry Run enabled, nothing is deleted") - ageInHours = flag.Int("age-in-hours", 24, "Address age in hours. images older than: now()-ageInHours are considered for removal.") - gcrNameIgnoreRegex = flag.String("gcr-exclude-name-regex", defaultGcrNameIgnoreRegex, "Ignored GCR name regex. Matching paths are not considered for removal.") -) - -func main() { - flag.Parse() - - if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") == "" { - log.Fatalf("Requires the environment variable GOOGLE_APPLICATION_CREDENTIALS to be set to a GCP service account file.") - } - - if *repository == "" { - fmt.Fprintln(os.Stderr, "missing -repository flag") - flag.Usage() - os.Exit(2) - } - - common.ShoutFirst("Running with arguments: repository: \"%s\", dryRun: %t, ageInHours: %d, gcrNameIgnoreRegex: \"%s\"", *repository, *dryRun, *ageInHours, *gcrNameIgnoreRegex) - - ctx := context.Background() - - //auth := gcrgoogle.NewJSONKeyAuthenticator(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")) - // TODO replace? - auth, err := gcrgoogle.NewEnvAuthenticator(context.Background()) - if err != nil { - log.Fatalf("failed to setup authenticator: %s", err) - } - - regexRepo := regexp.MustCompile(*gcrNameIgnoreRegex) - repoFilter := gcrcleaner.NewRepoFilter(regexRepo) - - imageFilter := gcrcleaner.NewImageFilter(*ageInHours) - - repoAPI := &gcrcleaner.RepoAPIWrapper{Context: ctx, Auth: auth} - - imageAPI := &gcrcleaner.ImageAPIWrapper{Context: ctx, Auth: auth} - - gcrCleaner := gcrcleaner.New(repoAPI, imageAPI, repoFilter, imageFilter) - - allSucceeded, err := gcrCleaner.Run(*repository, !(*dryRun)) - if err != nil { - log.Fatalf("GCR cleaner error: %v", err) - } - - if !allSucceeded { - log.Warn("Some operations failed.") - } - - common.Shout("Finished") -} diff --git a/pkg/tools/gcrcleaner/automock/image_api.go b/pkg/tools/gcrcleaner/automock/image_api.go deleted file mode 100644 index 7dd542a8be9f..000000000000 --- a/pkg/tools/gcrcleaner/automock/image_api.go +++ /dev/null @@ -1,50 +0,0 @@ -// Code generated by mockery 2.9.0. DO NOT EDIT. - -package automock - -import ( - google "github.com/google/go-containerregistry/pkg/v1/google" - mock "github.com/stretchr/testify/mock" -) - -// ImageAPI is an autogenerated mock type for the ImageAPI type -type ImageAPI struct { - mock.Mock -} - -// DeleteImage provides a mock function with given fields: registry, repoName, digest, manifest -func (_m *ImageAPI) DeleteImage(registry string, repoName string, digest string, manifest google.ManifestInfo) error { - ret := _m.Called(registry, repoName, digest, manifest) - - var r0 error - if rf, ok := ret.Get(0).(func(string, string, string, google.ManifestInfo) error); ok { - r0 = rf(registry, repoName, digest, manifest) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// ListImages provides a mock function with given fields: registry, repoName -func (_m *ImageAPI) ListImages(registry string, repoName string) (map[string]google.ManifestInfo, error) { - ret := _m.Called(registry, repoName) - - var r0 map[string]google.ManifestInfo - if rf, ok := ret.Get(0).(func(string, string) map[string]google.ManifestInfo); ok { - r0 = rf(registry, repoName) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(map[string]google.ManifestInfo) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(string, string) error); ok { - r1 = rf(registry, repoName) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} diff --git a/pkg/tools/gcrcleaner/automock/repo_api.go b/pkg/tools/gcrcleaner/automock/repo_api.go deleted file mode 100644 index 2ec85f057916..000000000000 --- a/pkg/tools/gcrcleaner/automock/repo_api.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code generated by mockery 2.9.0. DO NOT EDIT. - -package automock - -import mock "github.com/stretchr/testify/mock" - -// RepoAPI is an autogenerated mock type for the RepoAPI type -type RepoAPI struct { - mock.Mock -} - -// ListSubrepositories provides a mock function with given fields: repoName -func (_m *RepoAPI) ListSubrepositories(repoName string) ([]string, error) { - ret := _m.Called(repoName) - - var r0 []string - if rf, ok := ret.Get(0).(func(string) []string); ok { - r0 = rf(repoName) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]string) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(repoName) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} diff --git a/pkg/tools/gcrcleaner/cleaner.go b/pkg/tools/gcrcleaner/cleaner.go deleted file mode 100644 index 386666e41e2a..000000000000 --- a/pkg/tools/gcrcleaner/cleaner.go +++ /dev/null @@ -1,121 +0,0 @@ -package gcrcleaner - -import ( - "regexp" - "time" - - gcrname "github.com/google/go-containerregistry/pkg/name" - gcrgoogle "github.com/google/go-containerregistry/pkg/v1/google" - log "github.com/sirupsen/logrus" -) - -//go:generate mockery --name=RepoAPI --output=automock --outpkg=automock --case=underscore - -// RepoAPI abstracts access to Docker repos -type RepoAPI interface { - ListSubrepositories(repoName string) ([]string, error) -} - -//go:generate mockery --name=ImageAPI --output=automock --outpkg=automock --case=underscore - -// ImageAPI abstracts access to Docker images -type ImageAPI interface { - ListImages(registry, repoName string) (map[string]gcrgoogle.ManifestInfo, error) - DeleteImage(registry, repoName string, digest string, manifest gcrgoogle.ManifestInfo) error -} - -// GCRCleaner deletes Docker images created by prow jobs. -type GCRCleaner struct { - repoAPI RepoAPI - imageAPI ImageAPI - shouldRemoveRepo RepoRemovalPredicate - shouldRemoveImage ImageRemovalPredicate -} - -// New returns a new instance of GCRCleaner -func New(repoAPI RepoAPI, imageAPI ImageAPI, shouldRemoveRepo RepoRemovalPredicate, shouldRemoveImage ImageRemovalPredicate) *GCRCleaner { - return &GCRCleaner{repoAPI, imageAPI, shouldRemoveRepo, shouldRemoveImage} -} - -// Run executes image removal process for specified Docker repository -func (gcrc *GCRCleaner) Run(repoName string, makeChanges bool) (allSucceeded bool, err error) { - var msgPrefix string - if !makeChanges { - msgPrefix = "[DRY RUN] " - } - allSucceeded = true - - repo, err := gcrname.NewRepository(repoName) - if err != nil { - return false, err - } - registry := repo.Registry.RegistryStr() - - repos, err := gcrc.repoAPI.ListSubrepositories(repoName) - if err != nil { - return false, err - } - - for _, repo := range repos { - if gcrc.shouldRemoveRepo(repo) { - log.Infof("Cleaning images in %s/%s repository", registry, repo) - - // get all images for each repo - images, err := gcrc.imageAPI.ListImages(registry, repo) - if err != nil { - return false, err - } - for sha, image := range images { - - // check if the image is a candidate for deletion - if gcrc.shouldRemoveImage(&image) { - if makeChanges { - err = gcrc.imageAPI.DeleteImage(registry, repo, sha, image) - } - if err != nil { - log.Errorf("deleting image %s/%s@%s: %#v", registry, repo, sha, err) - allSucceeded = false - } else { - log.Infof("%sImage %s/%s@%s deleted", msgPrefix, registry, repo, sha) - } - - } - } - } - } - return allSucceeded, nil -} - -// RepoRemovalPredicate returns true when images in repo should be considered for deletion (name matches removal criteria) -type RepoRemovalPredicate func(string) bool - -// ImageRemovalPredicate returns true when image should be deleted (matches removal criteria) -type ImageRemovalPredicate func(*gcrgoogle.ManifestInfo) bool - -// NewRepoFilter is a default RepoRemovalPredicate factory -// Repo is matching the criteria if it's: -// - Name does not match gcrNameIgnoreRegex -func NewRepoFilter(gcrNameIgnoreRegex *regexp.Regexp) RepoRemovalPredicate { - return func(repo string) bool { - nameMatches := false - if gcrNameIgnoreRegex.String() != "" { - nameMatches = gcrNameIgnoreRegex.MatchString(repo) - } - return !nameMatches - - } -} - -// NewImageFilter is a default ImageRemovalPredicate factory -// Image is matching the criteria if it's: -// - CreationTimestamp indicates that it is created more than ageInHours ago. -func NewImageFilter(ageInHours int) ImageRemovalPredicate { - return func(image *gcrgoogle.ManifestInfo) bool { - oldEnough := false - - imageAgeHours := time.Since(image.Created).Hours() - float64(ageInHours) - oldEnough = imageAgeHours > 0 - - return oldEnough - } -} diff --git a/pkg/tools/gcrcleaner/cleaner_test.go b/pkg/tools/gcrcleaner/cleaner_test.go deleted file mode 100644 index 7e044f1ae5b1..000000000000 --- a/pkg/tools/gcrcleaner/cleaner_test.go +++ /dev/null @@ -1,168 +0,0 @@ -package gcrcleaner - -import ( - "errors" - "github.com/kyma-project/test-infra/pkg/tools/gcrcleaner/automock" - "regexp" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - gcrgoogle "github.com/google/go-containerregistry/pkg/v1/google" -) - -var ( - gcrNameIgnoreRegexPattern = "test/filtered/repo" - regexRepo = regexp.MustCompile(gcrNameIgnoreRegexPattern) - emptyRegex = regexp.MustCompile("") - repoFilter = NewRepoFilter(regexRepo) - emptyFilter = NewRepoFilter(emptyRegex) - imageFilter = NewImageFilter(1) // age is 1 hour - timeNow = time.Now() - timeTwoHoursAgo = timeNow.Add(time.Duration(-1) * time.Hour) -) - -func TestNewRepoFilter(t *testing.T) { - var testCases = []struct { - name string - expectedFilterValue bool - repo string - }{ - { - name: "should filter matching repo", - expectedFilterValue: false, - repo: "test/filtered/repos", - }, - { - name: "should skip repo without matching name", - expectedFilterValue: true, - repo: "test/unfiltered/repo", - }, - } - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - //when - collected := repoFilter(testCase.repo) - - //then - assert.Equal(t, testCase.expectedFilterValue, collected) - }) - - // test that empty filter will take all images into consideration - t.Run("Should delete all images", func(t *testing.T) { - //when - collected := emptyFilter(testCase.repo) - - //then - assert.Equal(t, true, collected) - }) - } -} - -func TestNewImageFilter(t *testing.T) { - var testCases = []struct { - name string - expectedFilterValue bool - created time.Time - }{ - { - name: "should filter older image", - expectedFilterValue: true, - created: timeTwoHoursAgo, - }, - { - name: "should skip recently created image", - expectedFilterValue: false, - created: timeNow, - }, - } - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - //when - manifest := createImageManifest(testCase.created, make([]string, 0)) - collected := imageFilter(&manifest) - - //then - assert.Equal(t, testCase.expectedFilterValue, collected) - }) - } -} - -func TestImageRemoval(t *testing.T) { - - imageCorrect := createImageManifest(timeTwoHoursAgo, make([]string, 0)) - - t.Run("ListSubrepositories() should find addresses to remove", func(t *testing.T) { - mockRepoAPI := &automock.RepoAPI{} - defer mockRepoAPI.AssertExpectations(t) - - mockImageAPI := &automock.ImageAPI{} - defer mockImageAPI.AssertExpectations(t) - - registry := "eu.gcr.io" - repo := "test" - repository := registry + "/" + repo - mockRepoAPI.On("ListSubrepositories", "eu.gcr.io/test").Return([]string{"test/repo", "test/another"}, nil) - - repos, err := mockRepoAPI.ListSubrepositories(repository) - require.NoError(t, err) - - assert.Len(t, repos, 2) - assert.Equal(t, "test/repo", repos[0]) - assert.Equal(t, "test/another", repos[1]) - }) - - t.Run("Run(makeChanges=true) should continue process if a call fails", func(t *testing.T) { - mockRepoAPI := &automock.RepoAPI{} - defer mockRepoAPI.AssertExpectations(t) - - mockImageAPI := &automock.ImageAPI{} - defer mockImageAPI.AssertExpectations(t) - - registry := "eu.gcr.io" - repo := "test" - repository := registry + "/" + repo - mockRepoAPI.On("ListSubrepositories", "eu.gcr.io/test").Return([]string{"test/repo", "test/another"}, nil) - - mockImageAPI.On("ListImages", "eu.gcr.io", "test/repo").Return(map[string]gcrgoogle.ManifestInfo{"sha256:abcd": imageCorrect}, nil) - mockImageAPI.On("ListImages", "eu.gcr.io", "test/another").Return(map[string]gcrgoogle.ManifestInfo{"sha256:efgh": imageCorrect}, nil) - - mockImageAPI.On("DeleteImage", registry, "test/repo", "sha256:abcd", imageCorrect).Return(errors.New("test error")) // Called first, returns error - mockImageAPI.On("DeleteImage", registry, "test/another", "sha256:efgh", imageCorrect).Return(nil) // Called although the previous call failed - - gcrc := New(mockRepoAPI, mockImageAPI, repoFilter, imageFilter) - - allSucceeded, err := gcrc.Run(repository, true) - require.NoError(t, err) - assert.False(t, allSucceeded) - }) - - t.Run("Run(makeChanges=false) should not remove anything (dry run)", func(t *testing.T) { - mockRepoAPI := &automock.RepoAPI{} - defer mockRepoAPI.AssertExpectations(t) - - mockImageAPI := &automock.ImageAPI{} - defer mockImageAPI.AssertExpectations(t) - - repository := "eu.gcr.io/test" - mockRepoAPI.On("ListSubrepositories", "eu.gcr.io/test").Return([]string{"test/repo", "test/another"}, nil) - - mockImageAPI.On("ListImages", "eu.gcr.io", "test/repo").Return(map[string]gcrgoogle.ManifestInfo{"sha256:abcd": imageCorrect}, nil) - mockImageAPI.On("ListImages", "eu.gcr.io", "test/another").Return(map[string]gcrgoogle.ManifestInfo{"sha256:efgh": imageCorrect}, nil) - - gcrc := New(mockRepoAPI, mockImageAPI, repoFilter, imageFilter) - - allSucceeded, err := gcrc.Run(repository, false) - require.NoError(t, err) - assert.True(t, allSucceeded) - }) -} - -func createImageManifest(created time.Time, tags []string) gcrgoogle.ManifestInfo { - return gcrgoogle.ManifestInfo{ - Created: created, - Tags: tags, - } -} diff --git a/pkg/tools/gcrcleaner/wrappers.go b/pkg/tools/gcrcleaner/wrappers.go deleted file mode 100644 index 91ecfeb5126f..000000000000 --- a/pkg/tools/gcrcleaner/wrappers.go +++ /dev/null @@ -1,87 +0,0 @@ -package gcrcleaner - -import ( - "context" - "sort" - "strings" - - "github.com/google/go-containerregistry/pkg/authn" - gcrname "github.com/google/go-containerregistry/pkg/name" - gcrgoogle "github.com/google/go-containerregistry/pkg/v1/google" - gcrremote "github.com/google/go-containerregistry/pkg/v1/remote" - log "github.com/sirupsen/logrus" -) - -// RepoAPIWrapper abstracts Docker repo API -type RepoAPIWrapper struct { - Context context.Context - Auth authn.Authenticator -} - -// ImageAPIWrapper abstracts Docker image API -type ImageAPIWrapper struct { - Context context.Context - Auth authn.Authenticator - // repository string -} - -// ListSubrepositories implements RepoAPIW.ListSubrepositories function -func (raw *RepoAPIWrapper) ListSubrepositories(repoName string) ([]string, error) { - var repositories []string - repo, err := gcrname.NewRepository(repoName) - if err != nil { - return nil, err - } - - childRepos, err := gcrremote.Catalog(raw.Context, repo.Registry, gcrremote.WithAuth(raw.Auth)) - if err != nil { - return nil, err - } - - //add only repos that start with wanted repo name - for _, childRepo := range childRepos { - if strings.HasPrefix(childRepo, repo.RepositoryStr()) { - repositories = append(repositories, childRepo) - } - } - sort.Strings(repositories) - return repositories, nil -} - -// ListImages implements ImageAPI.ListImages function -func (iw *ImageAPIWrapper) ListImages(registry, repoName string) (map[string]gcrgoogle.ManifestInfo, error) { - repo, err := gcrname.NewRepository(registry + "/" + repoName) - if err != nil { - return nil, err - } - - tags, err := gcrgoogle.List(repo, gcrgoogle.WithAuth(iw.Auth)) - if err != nil { - return nil, err - } - - // return map of manifests, each key is in "sha256:XXXXXXXXXXX" format - return tags.Manifests, nil -} - -// DeleteImage implements ImageAPI.DeleteImage function -func (iw *ImageAPIWrapper) DeleteImage(registry, repoName string, digest string, manifest gcrgoogle.ManifestInfo) error { - repo, err := gcrname.NewRepository(registry + "/" + repoName) - if err != nil { - return err - } - - // delete all tags - for _, tag := range manifest.Tags { - taggedName := repo.Tag(tag) - log.Info(taggedName) - err := gcrremote.Delete(taggedName, gcrremote.WithAuth(iw.Auth)) - return err - } - - // delete image - reference := repo.Digest(digest) - log.Info(reference) - err = gcrremote.Delete(reference, gcrremote.WithAuth(iw.Auth)) - return err -}