Expected behavior
PUT /retentions/{id} should accept any trigger kind that POST /retentions accepted and stored.
Actual behavior
POST /retentions succeeds and stores a retention policy with trigger.kind = "Manual".
PUT /retentions/{id} then returns HTTP 500 with {"errors":[{"code":"UNKNOWN","message":"unknown: not support Trigger Manual"}]} when the policy is round-tripped unchanged.
This means it is impossible to update any field of a retention policy whose trigger was created as "Manual" — including rules, algorithm, or scope — without first knowing to switch the trigger kind.
Steps to reproduce
# 1. Create a retention policy with Manual trigger
curl -X POST http://harbor/api/v2.0/retentions \
-u admin:Harbor12345 \
-H "Content-Type: application/json" \
-d '{
"algorithm": "or",
"rules": [{"action":"retain","template":"always","tag_selectors":[{"kind":"doublestar","pattern":"**","decoration":"matches"}]}],
"trigger": {"kind": "Manual"},
"scope": {"level": "project", "ref": 2}
}'
# → 201 Created, Location: /retentions/1
# 2. GET the policy back
curl http://harbor/api/v2.0/retentions/1 -u admin:Harbor12345
# → 200 OK, body includes "trigger":{"kind":"Manual"}
# 3. PUT the exact same payload back
curl -X PUT http://harbor/api/v2.0/retentions/1 \
-u admin:Harbor12345 \
-H "Content-Type: application/json" \
-d '<body from step 2>'
# → 500 Internal Server Error
Harbor core logs show:
[ERROR] [/lib/http/error.go:58]: {"errors":[{"code":"UNKNOWN","message":"unknown: not support Trigger Manual"}]}
Root cause
The bug is in src/controller/retention/controller.go in the UpdateRetention function.
CreateRetention stores any trigger kind without restriction — it only checks if kind == "Schedule" to decide whether to schedule a cron job. "Manual" is stored as-is in the database.
UpdateRetention has a switch on p.Trigger.Kind inside the else branch (when the trigger kind is unchanged between old and new policy), with a default case that returns an error:
// UpdateRetention — src/controller/retention/controller.go
if p0.Trigger.Kind != p.Trigger.Kind {
// handles Schedule ↔ other transitions, no error for "Manual"
} else {
switch p.Trigger.Kind {
case policy.TriggerKindSchedule:
// cron change handling
case "":
// no-op
default:
return fmt.Errorf("not support Trigger %s", p.Trigger.Kind) // ← rejects "Manual"
}
}
ValidateRetentionPolicy() in models.go does not reject "Manual" — it only validates the cron string when kind is "Schedule". So the policy passes validation but then fails in the controller logic.
Suggested fix
Add "Manual" (and potentially other valid non-scheduling trigger kinds) as an explicit case in the switch:
switch p.Trigger.Kind {
case policy.TriggerKindSchedule:
// existing cron change handling
case "", "Manual":
// no scheduling needed, no-op
default:
return fmt.Errorf("not support Trigger %s", p.Trigger.Kind)
}
Environment
- Harbor version: v2.15.0
- Kubernetes: kind cluster
- Discovered while implementing a Crossplane provider for Harbor (provider-harbor)
Expected behavior
PUT /retentions/{id}should accept any trigger kind thatPOST /retentionsaccepted and stored.Actual behavior
POST /retentionssucceeds and stores a retention policy withtrigger.kind = "Manual".PUT /retentions/{id}then returns HTTP 500 with{"errors":[{"code":"UNKNOWN","message":"unknown: not support Trigger Manual"}]}when the policy is round-tripped unchanged.This means it is impossible to update any field of a retention policy whose trigger was created as
"Manual"— including rules, algorithm, or scope — without first knowing to switch the trigger kind.Steps to reproduce
Harbor core logs show:
Root cause
The bug is in
src/controller/retention/controller.goin theUpdateRetentionfunction.CreateRetentionstores any trigger kind without restriction — it only checksif kind == "Schedule"to decide whether to schedule a cron job."Manual"is stored as-is in the database.UpdateRetentionhas aswitchonp.Trigger.Kindinside theelsebranch (when the trigger kind is unchanged between old and new policy), with adefaultcase that returns an error:ValidateRetentionPolicy()inmodels.godoes not reject"Manual"— it only validates the cron string when kind is"Schedule". So the policy passes validation but then fails in the controller logic.Suggested fix
Add
"Manual"(and potentially other valid non-scheduling trigger kinds) as an explicitcasein theswitch:Environment