Skip to content

bug: PUT /retentions/{id} returns 500 when trigger kind is "Manual" (stored by POST but rejected on update) #23278

@EvannDev

Description

@EvannDev

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions