Skip to content
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
1 change: 1 addition & 0 deletions docs/api-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ The following fields may be returned:
| `manifests[].gc_status` | object or omitted | Omitted if policy-guided garbage collection has not encountered this manifest yet. Otherwise contains a status report from the last GC run. If this object is shown, it will contain exactly one of the following attributes. |
| `manifests[].gc_status.protected_by_recent_upload` | true or omitted | If true, this manifest was protected from deletion during the last GC run because it was uploaded too recently (within 10 minutes of the GC run). |
| `manifests[].gc_status.protected_by_parent` | string or omitted | If shown, this manifest was protected from deletion during the last GC run because there is a parent manifest that references it. The field contains the parent manifest's digest. If the manifest is referenced by multiple parent manifests, it is not defined which parent manifest's digest will be shown. |
| `manifests[].gc_status.protected_by_subject` | string or omitted | If shown, this manifest was protected from deletion during the last GC run because the subject digest it references exists. The field contains the subject digest of the target image. |
| `manifests[].gc_status.protected_by_policy` | object or omitted | If shown, this manifest was protected from deletion during the last GC run because of a matching policy with the "protect" action. The object will contain the policy definition in the same format as described above for `accounts[].gc_policies[]`. |
| `manifests[].gc_status.relevant_policies` | array of objects or omitted | If shown, this manifest was not protected from deletion during the last GC run, but no deleting policy matched either. The array will contain the definitions of all deleting policies that could apply to this manifest, in the same format as described above for `accounts[].gc_policies[]`. |
| `manifests[].vulnerability_status` | string | Either `Clean` (no vulnerabilities have been found in this image), `Pending` (vulnerability scanning is not enabled on this server or is still in progress for this image or has failed for this image), `Error` (vulnerability scanning failed for this image or an image referenced in this manifest), or any of the following severity strings: `Unknown`, `Low`, `Medium`, `High`, `Critical`. The full vulnerability report can be retrieved with [a separate API call](#delete-keppelv1accountsnamerepositoriesname_manifestsdigesttrivy_report). |
Expand Down
11 changes: 7 additions & 4 deletions internal/keppel/gc_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,10 +250,13 @@ type GCStatus struct {
// protected from GC.
ProtectedByRecentUpload bool `json:"protected_by_recent_upload,omitempty"`
// If a parent manifest references this manifest and thus protects it from GC,
// contains the parent manifest's digest.
// this contains the parent manifest's digest.
ProtectedByParentManifest string `json:"protected_by_parent,omitempty"`
// If a policy with action "protect" applies to this image, contains the
// definition of the policy.
// If this manifest references a subject and is thus protected from GC,
// this contains the subject's digest.
ProtectedBySubjectManifest string `json:"protected_by_subject,omitempty"`
// If a policy with action "protect" applies to this image,
// this contains the definition of the policy.
ProtectedByPolicy *GCPolicy `json:"protected_by_policy,omitempty"`
// If the image is not protected, contains all policies with action "delete"
// that could delete this image in the future.
Expand All @@ -262,5 +265,5 @@ type GCStatus struct {

// IsProtected returns whether any of the ProtectedBy... fields is filled.
func (s GCStatus) IsProtected() bool {
return s.ProtectedByRecentUpload || s.ProtectedByParentManifest != "" || s.ProtectedByPolicy != nil
return s.ProtectedByRecentUpload || s.ProtectedByParentManifest != "" || s.ProtectedBySubjectManifest != "" || s.ProtectedByPolicy != nil
}
15 changes: 15 additions & 0 deletions internal/tasks/image_gc.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,21 @@ func (j *Janitor) executeGCPolicies(ctx context.Context, account models.ReducedA
}
}

// check if the subject target digest manifest exists
outer:
for _, manifest := range manifests {
if manifest.Manifest.SubjectDigest == "" {
continue
}

for _, m := range manifests {
if m.Manifest.Digest == manifest.Manifest.SubjectDigest {
manifest.GCStatus.ProtectedBySubjectManifest = manifest.Manifest.SubjectDigest.String()
continue outer
}
}
}

// evaluate policies in order
proc := j.processor()
for _, policy := range policies {
Expand Down
37 changes: 37 additions & 0 deletions internal/tasks/image_gc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ package tasks
import (
"database/sql"
"fmt"
"strings"
"testing"
"time"

imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sapcc/go-api-declarations/cadf"
"github.com/sapcc/go-bits/easypg"

Expand Down Expand Up @@ -383,3 +385,38 @@ func TestGCProtectComesTooLate(t *testing.T) {
s.Clock.Now().Add(1*time.Hour).Unix(),
)
}

func TestGCProtectSubject(t *testing.T) {
j, s := setup(t)

image := test.GenerateOCIImage(test.OCIArgs{
ConfigMediaType: imgspecv1.MediaTypeImageManifest,
})
image.MustUpload(t, s, fooRepoRef, "latest")

subjectManifest := test.GenerateOCIImage(test.OCIArgs{
ConfigMediaType: imgspecv1.MediaTypeImageManifest,
SubjectDigest: image.Manifest.Digest,
})
subjectManifest.MustUpload(t, s, fooRepoRef, strings.ReplaceAll(image.Manifest.Digest.String(), ":", "-"))

deletingGCPolicyJSON := `[{"match_repository":".*","time_constraint":{"on":"pushed_at","older_than":{"value":2,"unit":"h"}},"action":"delete"}]`
mustExec(t, s.DB, `UPDATE accounts SET gc_policies_json = $1`, deletingGCPolicyJSON)

tr, _ := easypg.NewTracker(t, s.DB.Db)
garbageJob := j.ManifestGarbageCollectionJob(s.Registry)

// skip an hour to avoid protected_by_recent_upload
s.Clock.StepBy(1 * time.Hour)

// nothing should be deleted here
expectSuccess(t, garbageJob.ProcessOne(s.Ctx))
expectError(t, sql.ErrNoRows.Error(), garbageJob.ProcessOne(s.Ctx))
tr.DBChanges().AssertEqualf(`
UPDATE manifests SET gc_status_json = '{"protected_by_subject":"%[1]s"}' WHERE repo_id = 1 AND digest = '%[2]s';
UPDATE manifests SET gc_status_json = '{"relevant_policies":%[3]s}' WHERE repo_id = 1 AND digest = '%[1]s';
UPDATE repos SET next_gc_at = %[4]d WHERE id = 1 AND account_name = 'test1' AND name = 'foo';
`,
image.Manifest.Digest, subjectManifest.Manifest.Digest, deletingGCPolicyJSON, s.Clock.Now().Add(1*time.Hour).Unix(),
)
}
Loading