Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply GAR cleanup policies to all repositories #9

Merged
merged 9 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
name: Build and push image and chart
on:
push:
branches:
- main
on: push
env:
NAME: nais-api-reconcilers
IMAGE_REPOSITORY: oci://europe-north1-docker.pkg.dev/nais-io/nais
Expand Down
63 changes: 58 additions & 5 deletions internal/reconcilers/google/gar/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ package google_gar_reconciler
import (
"context"
"fmt"
"maps"
"net/http"
"time"

artifactregistry "cloud.google.com/go/artifactregistry/apiv1"
"cloud.google.com/go/artifactregistry/apiv1/artifactregistrypb"
"cloud.google.com/go/iam/apiv1/iampb"
"github.com/nais/api-reconcilers/internal/gcp"
"github.com/nais/api-reconcilers/internal/google_token_source"
"github.com/nais/api-reconcilers/internal/reconcilers"
github_team_reconciler "github.com/nais/api-reconcilers/internal/reconcilers/github/team"
str "github.com/nais/api-reconcilers/internal/strings"
"github.com/nais/api/pkg/apiclient"
"github.com/nais/api/pkg/protoapi"
"github.com/sirupsen/logrus"
Expand All @@ -23,8 +20,16 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"k8s.io/utils/ptr"

"github.com/nais/api-reconcilers/internal/gcp"
"github.com/nais/api-reconcilers/internal/google_token_source"
"github.com/nais/api-reconcilers/internal/reconcilers"
github_team_reconciler "github.com/nais/api-reconcilers/internal/reconcilers/github/team"
str "github.com/nais/api-reconcilers/internal/strings"
)

const (
Expand Down Expand Up @@ -299,6 +304,18 @@ func (r *garReconciler) updateGarRepository(ctx context.Context, repository *art
changes = append(changes, "description")
}

targetPolicies := DefaultCleanupPolicies()
policyUpToDate := maps.EqualFunc(targetPolicies, repository.CleanupPolicies, func(a, b *artifactregistrypb.CleanupPolicy) bool {
return proto.Equal(a, b)
})

if !policyUpToDate || repository.CleanupPolicyDryRun {
repository.CleanupPolicyDryRun = false
repository.CleanupPolicies = targetPolicies
changes = append(changes, "cleanup_policies")
changes = append(changes, "cleanup_policy_dry_run")
}

if len(changes) > 0 {
updateRequest := &artifactregistrypb.UpdateRepositoryRequest{
Repository: repository,
Expand Down Expand Up @@ -344,3 +361,39 @@ func serviceAccountNameAndAccountID(teamSlug, projectID string) (serviceAccountN
serviceAccountName = "projects/" + projectID + "/serviceAccounts/" + emailAddress
return
}

// Remove all images that are more than 90 days old, but keep the last 50 "versions" regardless of age.
// These numbers are also referenced in our own documentation at: https://doc.nais.io/how-to-guides/github-action/; try to keep them in sync.
//
// Each "build and push" includes artifacts such as signatures and attestations that seemingly count as "versions".
// Thus, an "image" actually consists of 5 artifacts at the worst (for images pushed through the nais/docker-build-push action)
//
// Documentation: https://cloud.google.com/artifact-registry/docs/repositories/cleanup-policy
func DefaultCleanupPolicies() map[string]*artifactregistrypb.CleanupPolicy {
var keepCount int32 = 50

keepUntilAge := time.Hour * 24 * 90
anyTagState := artifactregistrypb.CleanupPolicyCondition_ANY

return map[string]*artifactregistrypb.CleanupPolicy{
"delete_old_images": {
Id: "delete_old_images",
Action: artifactregistrypb.CleanupPolicy_DELETE,
ConditionType: &artifactregistrypb.CleanupPolicy_Condition{
Condition: &artifactregistrypb.CleanupPolicyCondition{
TagState: &anyTagState,
OlderThan: durationpb.New(keepUntilAge),
},
},
},
"keep_latest_versions": {
Id: "keep_latest_versions",
Action: artifactregistrypb.CleanupPolicy_KEEP,
ConditionType: &artifactregistrypb.CleanupPolicy_MostRecentVersions{
MostRecentVersions: &artifactregistrypb.CleanupPolicyMostRecentVersions{
KeepCount: &keepCount,
},
},
},
}
}
77 changes: 74 additions & 3 deletions internal/reconcilers/google/gar/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"maps"
"net"
"net/http"
"net/http/httptest"
Expand All @@ -15,9 +16,6 @@ import (
"cloud.google.com/go/artifactregistry/apiv1/artifactregistrypb"
"cloud.google.com/go/iam/apiv1/iampb"
"cloud.google.com/go/longrunning/autogen/longrunningpb"
github_team_reconciler "github.com/nais/api-reconcilers/internal/reconcilers/github/team"
google_gar_reconciler "github.com/nais/api-reconcilers/internal/reconcilers/google/gar"
"github.com/nais/api-reconcilers/internal/test"
"github.com/nais/api/pkg/apiclient"
"github.com/nais/api/pkg/protoapi"
"github.com/sirupsen/logrus"
Expand All @@ -33,6 +31,10 @@ import (
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"k8s.io/utils/ptr"

github_team_reconciler "github.com/nais/api-reconcilers/internal/reconcilers/github/team"
google_gar_reconciler "github.com/nais/api-reconcilers/internal/reconcilers/google/gar"
"github.com/nais/api-reconcilers/internal/test"
)

type fakeArtifactRegistry struct {
Expand Down Expand Up @@ -175,6 +177,7 @@ func TestReconcile(t *testing.T) {
"team": teamSlug,
"managed-by": "api-reconcilers",
},
CleanupPolicies: google_gar_reconciler.DefaultCleanupPolicies(),
}

ctx := context.Background()
Expand Down Expand Up @@ -548,6 +551,74 @@ func TestReconcile(t *testing.T) {
t.Errorf("unexpected error: %v", err)
}
})

t.Run("gar repository exists, but has no cleanup policies", func(t *testing.T) {
naisTeam := &protoapi.Team{
Slug: teamSlug,
}

mocks := mocks{
artifactRegistry: &fakeArtifactRegistry{
get: func(ctx context.Context, r *artifactregistrypb.GetRepositoryRequest) (*artifactregistrypb.Repository, error) {
repositoryWithoutCleanupPolicies := proto.Clone(&expectedRepository).(*artifactregistrypb.Repository)
repositoryWithoutCleanupPolicies.CleanupPolicies = nil

return repositoryWithoutCleanupPolicies, nil
},
update: func(ctx context.Context, r *artifactregistrypb.UpdateRepositoryRequest) (*artifactregistrypb.Repository, error) {
if r.Repository.CleanupPolicies == nil {
t.Errorf("expected cleanup policies to be set, got nil")
}

if r.Repository.CleanupPolicyDryRun {
t.Errorf("expected cleanup policy dry run to be false, got true")
}

expectedPolicies := google_gar_reconciler.DefaultCleanupPolicies()
policyUpToDate := maps.EqualFunc(expectedPolicies, r.Repository.CleanupPolicies, func(a, b *artifactregistrypb.CleanupPolicy) bool {
return proto.Equal(a, b)
})

if !policyUpToDate {
t.Errorf("expected cleanup policies to be %v, got %v", expectedPolicies, r.Repository.CleanupPolicies)
}

return nil, abortTestErr
},
},
iam: test.HttpServerWithHandlers(t, []http.HandlerFunc{
// get service account
func(w http.ResponseWriter, r *http.Request) {
if err := json.NewEncoder(w).Encode(expectedServiceAccount); err != nil {
t.Fatalf("unexpected error: %v", err)
}
},
// set iam policy
func(w http.ResponseWriter, r *http.Request) {
if err := json.NewEncoder(w).Encode(&iam.Policy{}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
},
}),
}

artifactregistryClient, iamService := mocks.start(t, ctx)

apiClient, mockServer := apiclient.NewMockClient(t)
mockServer.Reconcilers.EXPECT().
State(mock.Anything, &protoapi.GetReconcilerStateRequest{ReconcilerName: "github:team", TeamSlug: teamSlug}).
Return(&protoapi.GetReconcilerStateResponse{}, nil).
Once()

reconciler, err := google_gar_reconciler.New(ctx, serviceAccountEmail, managementProjectID, workloadIdentityPoolName, google_gar_reconciler.WithGarClient(artifactregistryClient), google_gar_reconciler.WithIAMService(iamService))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if err = reconciler.Reconcile(ctx, apiClient, naisTeam, log); !strings.Contains(err.Error(), "abort test") {
t.Errorf("unexpected error: %v", err)
}
})
}

func TestDelete(t *testing.T) {
Expand Down