Skip to content

Commit c626bcc

Browse files
feat(jobs): example to clean registry tags
1 parent 98089d9 commit c626bcc

File tree

6 files changed

+253
-0
lines changed

6 files changed

+253
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Use the alpine version of the golang image as the base image
2+
FROM golang:1.24-alpine
3+
4+
# Set the working directory inside the container to /app
5+
WORKDIR /app
6+
7+
# Copy the go.mod and go.sum files to the working directory
8+
COPY go.mod ./
9+
COPY go.sum ./
10+
11+
# Copy the Go source files to the working directory
12+
COPY *.go ./
13+
14+
# Build the executable named reg-clean from the Go source files
15+
RUN go build -o /reg-clean
16+
17+
# Set the default command to run the reg-clean executable when the container starts
18+
CMD ["/reg-clean"]
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Scaleway Container Registry Tag Cleaner
2+
3+
This project aims to clean up Scaleway Container Registry tags to keep only the N latest tags for each image. It is useful for managing disk space and keeping the registry organized.
4+
5+
## Usage
6+
7+
1. **Build the Application:**
8+
```bash
9+
go build -o reg-clean
10+
```
11+
12+
2. **Run the Application:**
13+
```bash
14+
./reg-clean
15+
```
16+
17+
## Environment Variables
18+
19+
The application requires the following environment variables to be set:
20+
21+
- `SCW_DEFAULT_ORGANIZATION_ID`: Your Scaleway organization ID.
22+
- `SCW_ACCESS_KEY`: Your Scaleway access key.
23+
- `SCW_SECRET_KEY`: Your Scaleway secret key.
24+
- `SCW_PROJECT_ID`: Your Scaleway project ID.
25+
- `SCW_NUMBER_VERSIONS_TO_KEEP`: The number of latest tags to keep for each image.
26+
- `SCW_NO_DRY_RUN` (optional): Set to `true` to perform actual deletions. If not set, the application will run in dry-run mode, only logging the actions that would be taken.
27+
28+
## Example
29+
30+
To run the application in dry-run mode and keep the 5 latest tags for each image, set the following environment variables and run:
31+
32+
```bash
33+
export SCW_DEFAULT_ORGANIZATION_ID=your-organization-id
34+
export SCW_ACCESS_KEY=your-access-key
35+
export SCW_SECRET_KEY=your-secret-key
36+
export SCW_PROJECT_ID=your-project-id
37+
export SCW_NUMBER_VERSIONS_TO_KEEP=5
38+
39+
./reg-clean
40+
```
41+
42+
To run the application and actually delete the tags, set `SCW_NO_DRY_RUN` to `true`:
43+
44+
```bash
45+
export SCW_NO_DRY_RUN=true
46+
47+
./reg-clean
48+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module github.com/scaleway/serverless-examples/jobs/registry-version-based-retention
2+
3+
go 1.24.0
4+
5+
require github.com/scaleway/scaleway-sdk-go v1.0.0-beta.32
6+
7+
require gopkg.in/yaml.v2 v2.4.0 // indirect
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.32 h1:4+LP7qmsLSGbmc66m1s5dKRMBwztRppfxFKlYqYte/c=
2+
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.32/go.mod h1:kzh+BSAvpoyHHdHBCDhmSWtBc1NbLMZ2lWHqnBoxFks=
3+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
4+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
5+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package main
2+
3+
import (
4+
"log/slog"
5+
"os"
6+
"strconv"
7+
"strings"
8+
9+
"github.com/scaleway/scaleway-sdk-go/scw"
10+
)
11+
12+
const (
13+
envOrgID = "SCW_DEFAULT_ORGANIZATION_ID"
14+
envAccessKey = "SCW_ACCESS_KEY"
15+
envSecretKey = "SCW_SECRET_KEY"
16+
envProjectID = "SCW_PROJECT_ID"
17+
18+
envNTagsToKeep = "SCW_NUMBER_VERSIONS_TO_KEEP"
19+
20+
// If set to true, older tags will be deleted.
21+
envNoDryRun = "SCW_NO_DRY_RUN"
22+
)
23+
24+
// Check for mandatory variables before starting to work.
25+
func init() {
26+
mandatoryVariables := [...]string{envOrgID, envAccessKey, envSecretKey, envProjectID, envNTagsToKeep}
27+
28+
for idx := range mandatoryVariables {
29+
if os.Getenv(mandatoryVariables[idx]) == "" {
30+
panic("missing environment variable " + mandatoryVariables[idx])
31+
}
32+
}
33+
}
34+
35+
func main() {
36+
slog.Info("cleaning container registry tags...")
37+
38+
// Create a Scaleway client with credentials from environment variables.
39+
client, err := scw.NewClient(
40+
// Get your organization ID at https://console.scaleway.com/organization/settings
41+
scw.WithDefaultOrganizationID(os.Getenv(envOrgID)),
42+
43+
// Get your credentials at https://console.scaleway.com/iam/api-keys
44+
scw.WithAuth(os.Getenv(envAccessKey), os.Getenv(envSecretKey)),
45+
46+
// Get more about our availability
47+
// zones at https://www.scaleway.com/en/docs/console/my-account/reference-content/products-availability/
48+
scw.WithDefaultRegion(scw.RegionFrPar),
49+
)
50+
if err != nil {
51+
panic(err)
52+
}
53+
54+
regAPI := NewRegistryAPI(client, os.Getenv(scw.ScwDefaultProjectIDEnv))
55+
56+
numberTagsToKeep, err := strconv.Atoi(os.Getenv(envNTagsToKeep))
57+
if err != nil {
58+
panic(err)
59+
}
60+
61+
tagsToDelete, err := regAPI.GetTagsAfterNVersions(numberTagsToKeep)
62+
if err != nil {
63+
panic(err)
64+
}
65+
66+
dryRun := true
67+
noDryRunEnv := os.Getenv(envNoDryRun)
68+
69+
if strings.EqualFold(noDryRunEnv, "true") {
70+
dryRun = false
71+
}
72+
73+
if err := regAPI.DeleteTags(tagsToDelete, dryRun); err != nil {
74+
panic(err)
75+
}
76+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
"strings"
7+
8+
registry "github.com/scaleway/scaleway-sdk-go/api/registry/v1"
9+
"github.com/scaleway/scaleway-sdk-go/scw"
10+
)
11+
12+
// RegistryAPI represents a Scaleway Container Registry accessor and extends
13+
// capabilities to clean images.
14+
type RegistryAPI struct {
15+
// regClient Scaleway Container Registry accessor
16+
regClient *registry.API
17+
18+
// projectID if null, all projects will be checked.
19+
projectID *string
20+
21+
// disableProtection if set to true (DANGEROUS) it will delete images potentially used
22+
// in Serverless Jobs, Functions and Containers.
23+
disableProtection bool
24+
}
25+
26+
// NewRegistryAPI used to create a new RegistryAPI to manage the Scaleway Container Registry API.
27+
func NewRegistryAPI(client *scw.Client, projectID string) *RegistryAPI {
28+
// if projectID is empty, no project ID will be passed to Scaleway SDK to use default settings. Generally is to apply
29+
// on all projects.
30+
var ptrProjectID *string
31+
if projectID != "" {
32+
ptrProjectID = &projectID
33+
}
34+
35+
return &RegistryAPI{
36+
regClient: registry.NewAPI(client),
37+
projectID: ptrProjectID,
38+
disableProtection: false,
39+
}
40+
}
41+
42+
func (r *RegistryAPI) GetTagsAfterNVersions(numberVersionsToKeep int) ([]*registry.Tag, error) {
43+
images, err := r.regClient.ListImages(&registry.ListImagesRequest{ProjectID: r.projectID}, scw.WithAllPages())
44+
if err != nil {
45+
return nil, fmt.Errorf("error listing container images %w", err)
46+
}
47+
48+
if numberVersionsToKeep <= 1 {
49+
return nil, fmt.Errorf("number of versions to keep <= 1 is dangereous")
50+
}
51+
52+
tagsToDelete := make([]*registry.Tag, 0)
53+
54+
for _, image := range images.Images {
55+
// Unfortunately a request to list tags has to be done for each image.
56+
tags, err := r.regClient.ListTags(&registry.ListTagsRequest{
57+
ImageID: image.ID,
58+
OrderBy: registry.ListTagsRequestOrderByCreatedAtDesc,
59+
}, scw.WithAllPages())
60+
if err != nil {
61+
return nil, fmt.Errorf("error listing tags %w", err)
62+
}
63+
64+
if len(tags.Tags) <= numberVersionsToKeep {
65+
// not enough versions to delete, skipping
66+
continue
67+
}
68+
69+
slog.Info("appending tags for image: " + image.Name)
70+
71+
tagsToDelete = append(tagsToDelete, tags.Tags[numberVersionsToKeep:]...)
72+
}
73+
74+
return tagsToDelete, nil
75+
}
76+
77+
func (r *RegistryAPI) DeleteTags(tagsToDelete []*registry.Tag, dryRun bool) error {
78+
if dryRun {
79+
slog.Info("Dry run mode ENABLED")
80+
81+
for k := range tagsToDelete {
82+
slog.Info("dry-run: deleting tag: " + tagsToDelete[k].Name + " id: " + tagsToDelete[k].ID)
83+
}
84+
} else {
85+
slog.Warn("Dry run DISABLED")
86+
87+
for k := range tagsToDelete {
88+
// dont delete latest:
89+
if !strings.EqualFold(tagsToDelete[k].Name, "latest") {
90+
_, err := r.regClient.DeleteTag(&registry.DeleteTagRequest{TagID: tagsToDelete[k].ID})
91+
if err != nil {
92+
return fmt.Errorf("error deleting registry tag %w", err)
93+
}
94+
}
95+
}
96+
}
97+
98+
return nil
99+
}

0 commit comments

Comments
 (0)