Harness Open Source (Drone/Gitness) — Interfaces#

Interface catalog#

git.Interface#

  • Package: github.com/harness/gitness/git
  • File: git/interface.go
  • Methods: ~50 methods spanning repository CRUD (CreateRepository, DeleteRepository), tree/blob operations (GetTreeNode, ListTreeNodes, GetBlob), refs (CreateBranch, DeleteBranch, UpdateRef, GetRef), commits (GetCommit, ListCommits, CommitFiles, GetCommitDivergences), diffs (Diff, RawDiff, DiffStats, DiffCut, GetDiffHunkHeaders), merges (Merge, Revert), blame, protocol (GetInfoRefs, ServicePack), and utilities (ScanSecrets, GeneratePipeline, OptimizeRepository, Archive)
  • Purpose: The single abstraction behind the entire native git engine. All consumers (the repo controller, githook controller, SSH server, pipeline subsystem) depend only on this interface, not on the concrete implementation.
  • Implementations: One: the concrete git.Implementation struct in git/git.go, which delegates through git/api/ to the system git binary via git/command/.
  • Design quality: Intentionally broad — this is a facade interface over an entire subsystem, not an ISP-compliant narrow contract. The tradeoff is explicit: it allows the git engine to be swapped as a unit (e.g., replacing exec-based with libgit2) without touching callers. The breadth is justified by the domain scope.

authn.Authenticator#

  • Package: github.com/harness/gitness/app/auth/authn
  • File: app/auth/authn/authenticator.go
  • Methods: Authenticate(r *http.Request) (*auth.Session, error)
  • Purpose: Extracts and validates a principal identity from an HTTP request. Returns a Session on success, ErrNoAuthData when no credentials are present (so a chain of authenticators can be tried), or a hard error when credentials are present but invalid.
  • Implementations: Multiple: JWT bearer token, PAT (personal access token), service account token, cookie-based. Composed via a chain/first-match pattern at the middleware level.
  • Design quality: Textbook minimal interface. One method, one concern. The ErrNoAuthData sentinel value is an elegant way to support authenticator chains without requiring a separate “can handle?” check.

authz.Authorizer#

  • Package: github.com/harness/gitness/app/auth/authz
  • File: app/auth/authz/authz.go
  • Methods:
    • Check(ctx, session, scope, resource, permission) (bool, error) — checks a single permission
    • CheckAll(ctx, session, ...permissionChecks) (bool, error) — checks multiple permissions atomically
  • Purpose: Authorization boundary for all domain controllers. Every controller method that touches a protected resource calls Check or CheckAll before proceeding. Scope includes space/repository hierarchy.
  • Implementations: MembershipAuthorizer (in-process RBAC against the membership store), PublicAccessAuthorizer (wrapper that short-circuits Check for public resources). In Harness Enterprise, an additional implementation delegates to a remote policy engine.
  • Design quality: Well-designed. The CheckAll method avoids N sequential permission checks for multi-operation handlers (e.g., creating a repo requires checking both space write and repo create). The bool+error return avoids a panic-on-deny pattern.

store.*Store family#

  • Package: github.com/harness/gitness/app/store
  • File: app/store/database.go
  • Key interfaces:
    • PrincipalStore (~30 methods): users, service accounts, and services CRUD; FindByEmail, FindManyByUID, etc.
    • SpaceStore (~20 methods): space hierarchy CRUD; GetAncestorIDs, GetDescendantsIDs, soft delete, restore
    • RepoStore (~20 methods): repository CRUD; UpdateOptLock (optimistic locking), SoftDelete, Purge, Restore
    • PullReqStore, PipelineStore, ExecutionStore, StageStore, StepStore, WebhookStore, MembershipStore, TokenStore, PublicKeyStore, JobStore, LabelStore, RuleStore — each domain entity has its own store interface
  • Purpose: All persistence is behind these interfaces. The architecture ensures that no business logic package directly imports a database driver or SQL query.
  • Implementations: Two layers per store: (1) database.*Store in app/store/database/ — raw sqlx SQL, no ORM; (2) cache.* wrappers in app/store/cache/ — Redis-backed TTL cache that decorates the SQL implementation for hot paths (space and repo lookups). The cache layer implements the same store interface, so callers are unaware of caching.
  • Design quality: Strong separation. The dual-layer implementation (SQL + cache via same interface) is a clean decorator pattern. Each store interface is well-segregated by domain entity. UpdateOptLock is a notably practical pattern: it takes a mutateFn func(*T) error closure, wraps the read-modify-write in a transaction, and retries on conflict — hiding optimistic locking complexity from callers.

router.Interface#

  • Package: github.com/harness/gitness/app/router
  • File: app/router/interface.go
  • Methods:
    • Handle(w http.ResponseWriter, req *http.Request)
    • IsEligibleTraffic(req *http.Request) bool
    • Name() string
  • Purpose: Contract for pluggable HTTP sub-routers. The outer Router iterates a []Interface slice, calls IsEligibleTraffic on each in priority order, and dispatches to the first match. This allows completely independent routing strategies per traffic class.
  • Implementations: APIRouter (prefix /api/), GitRouter (repository path prefix, not /api/), RegistryRouter (OCI registry paths), WebRouter (catch-all SPA). The registry sub-module also provides its own AppRouter that satisfies this interface.
  • Design quality: Elegant. Three methods, zero coupling between sub-routers. Adding a new traffic class (e.g., a metrics router) requires only implementing this interface and inserting it in the wire.go slice — no changes to existing routers.

events.StreamProducer / events.StreamConsumer / events.Reader#

  • Package: github.com/harness/gitness/events
  • Files: events/stream.go, events/reader.go
  • Methods:
    • StreamProducer: Send(ctx, streamID, payload) (string, error) — single method
    • StreamConsumer: Register(streamID, handler, ...opts) error, Configure(...opts), Start(ctx) error, Errors() <-chan error, Infos() <-chan string
    • Reader: Configure(opts ...ReaderOption) — minimal marker interface; the GenericReader concrete type adds event registration via the package-level ReaderRegisterEvent[T] generic function
  • Purpose: The event bus abstraction. StreamProducer is the publisher side; StreamConsumer is the Redis Streams consumer (with consumer groups, delivery guarantees, error channels). Reader is the high-level typed interface that domain event packages expose to subscribers.
  • Implementations: StreamProducerstream.RedisProducer. StreamConsumerstream.RedisConsumer. The ReaderFactory[R Reader] generic struct acts as the factory that wires these together for a given event category (e.g., gitevents, pullreqevents).
  • Design quality: Sophisticated. The use of Go generics (ReaderFactory[R], HandlerFunc[T], ReaderRegisterEvent[T]) achieves type-safe event payload deserialization without reflection at call sites. The StreamConsumerFactoryFunc is a function type used as a factory — a clean alternative to an interface with one method.

infraprovider.InfraProvider#

  • Package: github.com/harness/gitness/infraprovider
  • File: infraprovider/infra_provider.go
  • Methods: Provision, Find, FindInfraStatus, Stop, CleanupInstanceResources, Deprovision, AvailableParams, UpdateParams, ValidateParams, TemplateParams, ProvisioningType, UpdateConfig, ValidateConfig, GenerateSetupYAML (14 methods)
  • Purpose: The primary extension point for gitspace infrastructure backends. Abstracts the lifecycle of cloud dev environments: provisioning containers (or VMs), finding existing infrastructure, stopping and deprovisioning. The AvailableParams / ValidateParams / TemplateParams group supports dynamic configuration schemas per provider.
  • Implementations: DockerInfraProvider (provisions Docker containers on the local daemon), with the interface designed to support cloud VM providers (AWS, GCP) as future implementations.
  • Design quality: Broad but internally coherent. The 14 methods cover the full infrastructure lifecycle. The schema-reflection methods (AvailableParams, TemplateParams) are somewhat unusual — they expose a []InfraProviderParameterSchema that lets the UI render provider-specific configuration forms dynamically, which is a thoughtful extensibility mechanism.

livelog.LogStream#

  • Package: github.com/harness/gitness/livelog
  • File: livelog/livelog.go
  • Methods: Create(ctx, stepID), Delete(ctx, stepID), Write(ctx, stepID, line), Tail(ctx, stepID) (<-chan *Line, <-chan error), Info(ctx) *LogStreamInfo
  • Purpose: Real-time log streaming for CI pipeline step execution. Tail returns a pair of channels — the standard Go idiom for streaming with error propagation. Consumed by SSE handlers that push log lines to browser clients.
  • Implementations: Redis-backed pub/sub implementation. Log lines are written by the drone runner (via the manager RPC) and tailed by browser clients.
  • Design quality: Clean 5-method interface. The dual-channel Tail return (<-chan *Line, <-chan error) is idiomatic Go for streaming with cancellation.

cache.Cache[K,V] / cache.ExtendedCache[K,V]#

  • Package: github.com/harness/gitness/cache
  • File: cache/cache.go
  • Methods:
    • Cache[K any, V any]: Stats() (int64, int64), Get(ctx, key K) (V, error), Evict(ctx, key K)
    • ExtendedCache[K comparable, V Identifiable[K]]: embeds Cache[K,V] + Map(ctx, keys []K) (map[K]V, error)
    • Supporting: Identifiable[K] (Identifier() K), Getter[K,V] (Find), ExtendedGetter[K,V] (Find + FindMany)
  • Purpose: Generic cache layer for hot-path lookups (space IDs, repo refs). The Getter/ExtendedGetter interfaces define the data source that backs a cache instance.
  • Implementations: RedisCache (in cache/redis_cache.go), with Encoder[V]/Decoder[V]/Codec[V] interfaces for serialization.
  • Design quality: Excellent use of Go 1.18 generics. The Identifiable[K] constraint enables ExtendedCache.Map to build a result map without requiring a separate key-extraction function. The Getter/Cache separation (data source vs cache behavior) is clean composition.

pipeline.Scheduler#

  • Package: github.com/harness/gitness/app/pipeline/scheduler
  • File: app/pipeline/scheduler/scheduler.go
  • Methods: Schedule(ctx, stage) error, Request(ctx, filter) (*Stage, error), Cancel(ctx, buildID) error, Cancelled(ctx, buildID) (bool, error)
  • Purpose: Assigns CI pipeline stages to available drone runners. Request is a long-polling method called by runners; it blocks until a matching stage is available. Cancelled is similarly blocking — runners poll this to detect cancellation mid-execution.
  • Implementations: Internal scheduler struct (queue + canceler composition) in the same package.
  • Design quality: Well-designed. The Filter struct (OS, arch, labels) supports heterogeneous runner pools. The blocking Request / Cancelled methods expose the long-poll pattern explicitly in the interface — a rare but honest design choice.

ide.IDE#

  • Package: github.com/harness/gitness/app/gitspace/orchestrator/ide
  • File: app/gitspace/orchestrator/ide/ide.go
  • Methods: Setup(ctx, exec, args, logger) error, Run(ctx, exec, args, logger) error, Port() *GitspacePort, Type() enum.IDEType, GenerateURL(...) string, GeneratePluginURL(...) string
  • Purpose: Abstracts IDE lifecycle management inside gitspace containers. Each IDE type (VS Code Web, Cursor, JetBrains) implements Setup (install/configure) and Run (start the IDE server process) differently, but the orchestrator interacts with all through this interface.
  • Implementations: VSCode, Cursor, JetBrains concrete types in sibling packages under ide/.
  • Design quality: Clean. The args map[IDEArg]any pattern for Setup/Run allows IDE-specific arguments without requiring interface changes per IDE. The GitspaceLogger parameter is passed through for container-side log capture.

Interface patterns#

  • Size distribution: Heavily bimodal. Infrastructure/extension-point interfaces (git.Interface, InfraProvider, PrincipalStore) have 14–50 methods because they are facades over entire subsystems. Cross-cutting security interfaces (Authenticator, router.Interface) and infrastructure abstractions (LogStream, Scheduler) have 1–5 methods. The majority of interfaces fall in the 3–10 method range.

  • Embedding: cache.ExtendedCache embeds cache.Cache. cache.ExtendedGetter embeds cache.Getter. This is the only significant interface embedding in the core codebase; most interfaces are self-contained.

  • Implicit satisfaction: Consistently defined by consumers, not providers. The git.Interface is defined in the git/ package but consumed by app/api/controller/repo/; the store.*Store interfaces are defined in app/store/ but implemented by app/store/database/. This is idiomatic Go and enables clean layering.

  • Stdlib interfaces used: io.Writer appears in git.Interface (RawDiff, CommitDiff, Archive — streaming large outputs directly to a writer avoids buffering entire diffs in memory). net/http.Handler semantics flow through router.Interface.Handle. context.Context is universal — every interface method that does I/O takes a ctx first argument.

  • Generics: Three interface families use Go 1.18+ type parameters: cache.Cache[K,V] / cache.ExtendedCache[K,V], events.ReaderFactory[R Reader], and events.HandlerFunc[T]. The cache generics are the cleanest example — they eliminate the per-type cache boilerplate that pre-generics Go required.


Key abstractions#

  1. git.Interface — The single facade that hides the entire native git engine. Every controller that touches git goes through this interface. It is the largest interface in the codebase and the hardest to split further without forcing callers to manage multiple dependencies.

  2. authn.Authenticator + authz.Authorizer — Together these form the security boundary. Every HTTP request passes through Authenticator (authentication middleware), and every controller action calls Authorizer.Check before touching data. Their separation (who are you? vs. what can you do?) is architecturally clean.

  3. store.*Store family — The persistence boundary. ~15 domain-specific store interfaces ensure that no business logic package directly touches a database driver. The decorator pattern (SQL store wrapped by cache store, same interface) is one of the most elegant patterns in the codebase.

  4. router.Interface — The HTTP dispatch strategy. Three methods, zero coupling between sub-routers, trivially extensible. This interface embodies the four-router dispatch pattern that is one of the project’s defining architectural decisions.

  5. infraprovider.InfraProvider — The primary extension point for the gitspace feature. Adding support for a new cloud provider (AWS, GCP) means implementing this interface — no other changes required. The schema-introspection methods (AvailableParams, TemplateParams) make this a self-describing plugin interface.


Interface-driven extensibility#

The project uses interfaces for extensibility in three distinct patterns:

Backend swapping (store + cache): The store.*Store interfaces allow the SQL implementation to be transparently wrapped by a Redis cache layer. The DI system (Wire) selects which implementation to inject. Adding a new persistence backend (e.g., CockroachDB) requires only a new *Store implementation — all controllers remain unchanged.

Sub-router plugins (router.Interface): New traffic classes can be added by implementing the three-method router.Interface and registering it in the Wire set. The registry sub-module uses this to inject its own AppRouter alongside the core routers.

IDE and infra provider plugins: The ide.IDE and infraprovider.InfraProvider interfaces are explicit plugin contracts. The gitspace feature is designed around the expectation that multiple IDE types and infrastructure providers will coexist. The parameter schema methods (AvailableParams) make these interfaces self-describing, enabling dynamic UI configuration without code changes.

Event bus abstraction: The events.StreamConsumer / events.StreamProducer interfaces isolate the Redis Streams implementation from the domain event packages. In principle, a different streaming backend could be substituted by providing alternative implementations of these interfaces — the domain event code and the service subscribers would be unaffected.