What Is the Role of the Controller Layer in Harbor?

The Controller Layer in Harbor serves as the central business logic coordinator that orchestrates complex operations, manages database transactions, enforces policies, and publishes events to bridge HTTP handlers and low-level data managers.

The Controller Layer operates as the cognitive core of the goharbor/harbor architecture, residing under src/controller/ and implementing the rules that govern container registry operations. Unlike the API layer that handles HTTP concerns or the Manager layer that executes raw database queries, controllers coordinate multi-step workflows to ensure data consistency across projects, repositories, and artifacts. This architectural separation allows Harbor to maintain atomic transactions while integrating with external systems like notification webhooks, OIDC providers, and vulnerability scanners.

Core Responsibilities of the Controller Layer

Controllers in Harbor encapsulate six primary responsibilities that transform simple HTTP requests into reliable, auditable system operations.

Orchestrating Multi-Step Operations

Controllers combine multiple manager calls into single logical actions. When creating a project, project.Controller.Create (implemented in src/controller/project/controller.go) orchestrates several distinct operations within one workflow: it persists the project via projectMgr.Create, initializes an empty CVE allowlist through allowlistMgr.CreateEmpty, and attaches metadata using metaMgr.Add. This aggregation prevents API handlers from needing to understand the underlying dependency chain between these entities.

Transaction Management and Atomicity

Database consistency relies on the controller's ability to wrap related writes in atomic transactions. The repository.Controller.Ensure method in src/controller/repository/controller.go demonstrates this pattern by utilizing orm.WithTransaction to create repository records. If any step fails—such as a conflict with an existing repository—the entire transaction rolls back, leaving the database in a consistent state rather than persisting partial data.

Input Validation and Data Enrichment

Before executing business logic, controllers parse and verify incoming data. In src/controller/artifact/controller.go, the artifact.Controller.Ensure method extracts the project name from repository strings (e.g., parsing "myproject/myrepo" to identify "myproject") and validates the project's existence before proceeding with artifact creation. This validation layer ensures that downstream managers receive pre-verified, context-rich data structures.

Policy Enforcement and Security Checks

Controllers enforce Harbor's governance policies including immutable tag rules, quota limits, and CVE allowlists. The artifact controller references immutableMtr to validate immutable tag constraints before allowing updates to existing artifacts, preventing accidental or unauthorized overwrites of critical images. These checks occur after API authentication but before database persistence, creating a centralized enforcement point.

Event Publishing and System Integration

Harbor's event-driven architecture depends on controllers to emit notifications. After successfully creating a project, project.Controller.Create invokes notification.AddEvent with a CreateProjectEventMetadata payload. This decoupled approach allows replication services, audit loggers, and webhook notification systems to react to state changes without the controller needing direct knowledge of these subscribers.

Cross-Component Coordination

Complex deletion operations demonstrate the controller's coordination role. The repository.Controller.Delete method in src/controller/repository/controller.go implements cascade logic: it first iterates through all associated artifacts, verifies no external references exist, removes accessory objects, and finally deletes the repository record itself. This sequencing prevents orphaned database records and ensures referential integrity across Harbor's relational schema.

Where the Controller Layer Lives in the Codebase

All controller implementations reside under the src/controller/ directory, following a consistent package-per-domain structure:

Each package exposes a global singleton variable (e.g., project.Ctl, artifact.Ctl, user.Ctl) initialized via var Ctl = NewController(), allowing API handlers to invoke business logic without manual controller instantiation.

Practical Examples: Using Controllers in Harbor

The following examples illustrate how Harbor's API layer interacts with controllers to execute business operations.

Creating a Project with Business Logic

API handlers delegate complex creation workflows to the project controller:

func CreateProjectHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    var proj models.Project
    
    // Decode JSON body (omitted for brevity)
    
    // Delegate to controller - handles transactions, allowlists, and events
    projectID, err := project.Ctl.Create(ctx, &proj)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    json.NewEncoder(w).Encode(map[string]int64{"project_id": projectID})
}

Behind the scenes, project.Ctl.Create executes the full workflow defined in src/controller/project/controller.go (lines 86-119), including transaction management and event emission.

Ensuring Repository Existence

Controllers provide idempotent operations for scenarios where a repository might already exist:

created, repoID, err := repository.Ctl.Ensure(ctx, "library/nginx")
if err != nil {
    log.Fatalf("Repository ensure failed: %v", err)
}
if created {
    fmt.Printf("Created new repository with ID: %d\n", repoID)
} else {
    fmt.Printf("Repository exists with ID: %d\n", repoID)
}

The Ensure method in src/controller/repository/controller.go (lines 77-115) handles the conditional logic, creating the repository only if absent while maintaining transactional safety.

Handling Artifact Pushes with Event Publishing

When container images are pushed, the artifact controller manages tagging and fires notification events:

artCreated, artID, err := artifact.Ctl.Ensure(ctx,
    "production/backend", "sha256:abc123...", &artifact.ArtOption{
        Tags: []string{"v2.1", "latest"},
    })
if err != nil {
    handleError(err)
}

if artCreated {
    // Controller automatically published PushArtifactEventMetadata
    log.Printf("New artifact created: %d\n", artID)
}

As implemented in src/controller/artifact/controller.go (lines 65-102), this method checks for proxy-cache configurations to avoid duplicate events, validates immutable tag policies, and publishes PushArtifactEventMetadata for downstream notification processing.

Summary

  • The Controller Layer in Harbor sits between API routers and data managers, implementing the registry's core business logic.
  • Controllers orchestrate multi-step operations—such as project creation with metadata and allowlists—while maintaining atomicity through database transactions.
  • Policy enforcement occurs at the controller level, including immutable tag validation, quota checks, and CVE allowlist verification.
  • Global singletons (project.Ctl, artifact.Ctl, etc.) provide convenient access points for API handlers throughout the codebase.
  • Event publishing within controllers enables Harbor's notification system, replication services, and audit logging to react to state changes asynchronously.
  • All controller implementations reside under src/controller/, with each domain (project, artifact, repository, user) maintaining its own package and controller.go file.

Frequently Asked Questions

What is the difference between the Controller and Manager layers in Harbor?

The Controller Layer contains business logic, orchestration, and policy enforcement, while the Manager/DAO Layer handles direct database and storage operations. Controllers call managers to execute queries but add transaction coordination, input validation, and cross-component coordination that managers do not handle. For example, the project controller coordinates creation of CVE allowlists and metadata, while the project manager only executes the SQL INSERT statements.

How does the Controller Layer ensure database consistency?

Controllers utilize orm.WithTransaction (from Harbor's ORM package) to wrap multiple database operations in atomic transactions. As seen in src/controller/repository/controller.go, the Ensure method begins a transaction, attempts to create the repository, and rolls back automatically if any step encounters an error. This prevents partial writes that would leave the database in an inconsistent state.

Why does Harbor expose controllers as global singletons like project.Ctl?

Each controller package exposes a Ctl variable initialized via var Ctl = NewController() to provide dependency injection without complex wiring. This pattern allows API handlers to invoke project.Ctl.Create() or artifact.Ctl.Ensure() directly without needing to understand controller constructor requirements or manage instance lifecycles. The implementation resides in files like src/controller/project/controller.go (lines 36-38).

How does the Controller Layer trigger webhook notifications?

After successfully completing operations, controllers call notification.AddEvent() with specific metadata structures like CreateProjectEventMetadata or PushArtifactEventMetadata. This occurs within the controller method—for instance, in src/controller/project/controller.go (lines 112-119)—ensuring events only fire when data persistence succeeds. The notification system then asynchronously processes these events to deliver webhook callbacks.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →