Native Resources Architecture
Overview
Native resources allow the CI system to perform resource operations (check, get, put) without spawning separate container images. This reduces overhead significantly, especially for common operations like git cloning.
Current State
Currently, resources in Concourse-compatible pipelines work by:
- Spawning a container with the resource type image (e.g.,
concourse/git-resource) - Running
/opt/resource/check,/opt/resource/in, or/opt/resource/out - Passing configuration via stdin as JSON
- Reading results from stdout as JSON
This adds significant overhead for simple operations.
Proposed Architecture
Resource Interface
Native resources implement the standard Concourse resource protocol but in Go:
// resources/resource.go
package resources
import "context"
// Version represents a resource version (arbitrary key-value pairs)
type Version map[string]string
// Metadata represents key-value metadata about a resource
type Metadata []MetadataField
type MetadataField struct {
Name string `json:"name"`
Value string `json:"value"`
}
// CheckRequest is the input to a Check operation
type CheckRequest struct {
Source map[string]interface{} `json:"source"`
Version Version `json:"version,omitempty"`
}
// CheckResponse is the output of a Check operation
type CheckResponse []Version
// InRequest is the input to an In (get) operation
type InRequest struct {
Source map[string]interface{} `json:"source"`
Version Version `json:"version"`
Params map[string]interface{} `json:"params,omitempty"`
}
// InResponse is the output of an In operation
type InResponse struct {
Version Version `json:"version"`
Metadata Metadata `json:"metadata,omitempty"`
}
// OutRequest is the input to an Out (put) operation
type OutRequest struct {
Source map[string]interface{} `json:"source"`
Params map[string]interface{} `json:"params,omitempty"`
}
// OutResponse is the output of an Out operation
type OutResponse struct {
Version Version `json:"version"`
Metadata Metadata `json:"metadata,omitempty"`
}
// Resource is the interface that all native resources must implement
type Resource interface {
// Name returns the resource type name (e.g., "git", "s3")
Name() string
// Check discovers new versions of the resource
Check(ctx context.Context, req CheckRequest) (CheckResponse, error)
// In fetches a specific version of the resource to the destination path
In(ctx context.Context, destDir string, req InRequest) (InResponse, error)
// Out pushes a new version of the resource from the source path
Out(ctx context.Context, srcDir string, req OutRequest) (OutResponse, error)
}Registry Pattern
Similar to how orchestra drivers self-register, resources use init():
// resources/registry.go
package resources
import (
"fmt"
"sync"
)
type Factory func() Resource
var (
registry = make(map[string]Factory)
registryMu sync.RWMutex
)
func Register(name string, factory Factory) {
registryMu.Lock()
defer registryMu.Unlock()
registry[name] = factory
}
func Get(name string) (Resource, error) {
registryMu.RLock()
defer registryMu.RUnlock()
factory, ok := registry[name]
if !ok {
return nil, fmt.Errorf("unknown resource type: %s", name)
}
return factory(), nil
}
func List() []string {
registryMu.RLock()
defer registryMu.RUnlock()
names := make([]string, 0, len(registry))
for name := range registry {
names = append(names, name)
}
return names
}
func IsNative(name string) bool {
registryMu.RLock()
defer registryMu.RUnlock()
_, ok := registry[name]
return ok
}Git Resource Implementation
Using go-git/go-git:
// resources/git/git.go
package git
import (
"context"
"fmt"
"os"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
"github.com/jtarchie/pocketci/resources"
)
type Git struct{}
func (g *Git) Name() string {
return "git"
}
func (g *Git) Check(ctx context.Context, req resources.CheckRequest) (resources.CheckResponse, error) {
// Implementation using go-git to fetch refs
}
func (g *Git) In(ctx context.Context, destDir string, req resources.InRequest) (resources.InResponse, error) {
// Clone/checkout using go-git
}
func (g *Git) Out(ctx context.Context, srcDir string, req resources.OutRequest) (resources.OutResponse, error) {
// Push using go-git
}
func init() {
resources.Register("git", func() resources.Resource {
return &Git{}
})
}Deployment Strategies for Docker/K8s
The challenge: when running in Docker or K8s, the container doesn't have the ci binary with native resources.
Strategy 1: Mount the Binary (Recommended for Docker)
When running a native resource operation in a container environment:
- Mount the
pocketcibinary into the container - Run
pocketci resource <type> <operation>instead of/opt/resource/<op> - The binary handles the resource operation natively
// When running in Docker/K8s with native resources:
container.Create(ctx, ContainerConfig{
Mounts: []Mount{
{
Type: "bind",
Source: os.Executable(), // Path to ci binary
Target: "/opt/pocketci/bin/pocketci",
ReadOnly: true,
},
},
Command: []string{"/opt/pocketci/bin/pocketci", "resource", "git", "in", "/workspace"},
})Strategy 2: Sidecar Container (K8s)
For Kubernetes, use a sidecar container with the pocketci binary:
apiVersion: v1
kind: Pod
spec:
containers:
- name: main
# Main task container
- name: ci-resource
image: ghcr.io/jtarchie/pocketci:latest
command: ["sleep", "infinity"]
volumeMounts:
- name: workspace
mountPath: /workspaceStrategy 3: Init Container + Shared Volume
Copy the binary to a shared volume:
initContainers:
- name: ci-installer
image: ghcr.io/jtarchie/pocketci:latest
command: ["cp", "/usr/local/bin/pocketci", "/pocketci-bin/"]
volumeMounts:
- name: ci-bin
mountPath: /pocketci-binStrategy 4: Fallback to Container Resources
If native execution isn't possible, fall back to container-based resources:
func ExecuteResource(ctx context.Context, rt ResourceType, op Operation, req Request) (Response, error) {
// Check if native resource is available
if resources.IsNative(rt.Name) && canExecuteNatively(ctx) {
return executeNative(ctx, rt.Name, op, req)
}
// Fallback to container-based resource
return executeContainer(ctx, rt, op, req)
}CLI Integration
Add a new resource subcommand for executing resource operations:
# For check
echo '{"source": {"uri": "..."}}' | pocketci resource git check
# For in (get)
echo '{"source": {...}, "version": {...}}' | pocketci resource git in /path/to/dest
# For out (put)
echo '{"source": {...}, "params": {...}}' | pocketci resource git out /path/to/srcRuntime Integration
Modify the job runner to check for native resources:
// In backwards/src/job_runner.ts
private async processGetStep(step: Get, pathContext: string): Promise<void> {
const resource = this.findResource(step.get);
const resourceType = this.findResourceType(resource?.type);
// Check if this is a native resource
if (runtime.isNativeResource(resourceType?.name)) {
// Use native resource execution
await runtime.executeNativeResource({
type: resourceType?.name,
operation: "check",
source: resource?.source,
});
// ... rest of native flow
} else {
// Fallback to container-based resource (existing code)
}
}File Structure
resources/
├── resource.go # Interface definitions
├── registry.go # Registration pattern
├── git/
│ ├── git.go # Git resource implementation
│ └── git_test.go
├── s3/
│ ├── s3.go # S3 resource implementation
│ └── s3_test.go
├── time/
│ ├── time.go # Time resource implementation
│ └── time_test.go
└── mock/
├── mock.go # Mock resource for testing
└── mock_test.goAdvantages
- Performance: No container startup overhead for simple operations
- Simplicity: Single binary contains all common resources
- Portability: Works with native driver without Docker
- Fallback: Can still use container resources when needed
- Extensibility: Easy to add new native resources
Migration Path
- Start with
gitresource as proof of concept - Add
timeresource (simple, good for testing) - Add
mockresource for testing - Gradually add more resources based on usage
Configuration
Allow users to prefer or require native resources:
# In pipeline config
resources:
- name: my-repo
type: git
source:
uri: https://github.com/...
native: true Prefer native implementationOr globally via CLI:
pocketci runner --prefer-native-resources pipeline.tsFuture Direction: Native-First Execution (No Containers for Resources)
The cleanest architecture is to always execute native resources directly in the pocketci process - no containers at all for resource operations. This eliminates all sidecar/binary-mounting complexity.
Why Resources Don't Need Containers
- No isolation needed - Resources read/write to a specific directory that becomes a volume mount for tasks
- The
ciprocess already has filesystem access - It creates volumes and manages the workspace - Network access is fine - Resources need to reach git repos, S3, etc. anyway
- No security boundary needed - Unlike tasks, resources are trusted code (built into the binary)
Execution Model
Pipeline Request
│
▼
┌─────────────────┐
│ ci process │
│ ┌───────────┐ │
│ │ Native │ │ ← git clone happens here (in-process)
│ │ Resource │ │
│ └───────────┘ │
│ │ │
│ ▼ │
│ ┌───────────┐ │
│ │ Volume/ │ │ ← files written to workspace
│ │ Workspace │ │
│ └───────────┘ │
└─────────────────┘
│
▼
┌─────────────────┐
│ Container │ ← Only tasks run in containers
│ (Task) │
│ ┌───────────┐ │
│ │ Volume │──┼── mounted from above
│ │ Mount │ │
│ └───────────┘ │
└─────────────────┘Flow for a Get Step
cireceives get step requestcicreates a volume (directory for native, Docker volume for Docker)ciexecutes native git resource in-process:- Clones repo directly to volume path
- No container spawned
- Volume is mounted into subsequent task containers
Volume Path Resolution by Driver
| Driver | Volume Path | How Resources Access It |
|---|---|---|
native | /tmp/pocketci-volumes/abc123 | Direct filesystem access |
docker | Host path mounted as Docker volume | Direct filesystem access (same host) |
k8s | PVC mount path on controller node | Direct access if controller has PVC mounted (see below) |
Kubernetes Consideration
For K8s, if the pocketci controller runs outside the cluster, you'd need the binary-mounting approach from the strategies above. But if ci runs inside the cluster (as a pod), it can mount the same PVCs and write directly:
# pocketci controller running in-cluster
apiVersion: v1
kind: Pod
metadata:
name: ci-controller
spec:
containers:
- name: ci
image: ghcr.io/jtarchie/pocketci:latest
volumeMounts:
- name: workspace-pvc
mountPath: /workspace
volumes:
- name: workspace-pvc
persistentVolumeClaim:
claimName: ci-workspaceBenefits of Native-First
- Zero container overhead for resource operations
- Faster pipeline execution - git clone starts immediately
- Simpler architecture - no binary mounting or sidecars needed
- Works identically across native/docker drivers
- Reduced complexity - fewer moving parts to debug
Implementation Priority
This is the recommended long-term direction. The current implementation supports both approaches, allowing gradual migration:
- Native resources execute in-process when possible
- Fall back to container-based resources for custom/unknown types
- Eventually, most common resources (git, s3, time, registry-image) will be native, making container-based resources rare