Skip to content

ADR-027: S2I Build Strategy for Git Integration

Status: Superseded Date: 2025-01-08 Updated: 2025-11-10 Related: ADR-009 (Secret Management), ADR-023 (S2I Build - Archived), ADR-028 (Tekton Task Strategy), ADR-031 (Tekton Build - Primary)

Status

Superseded

Context

During testing on OpenShift 4.18, we encountered a security constraint issue where the alpine/git:latest image used for git-clone init containers runs as root, violating OpenShift's runAsNonRoot security context requirement.

The Problem

Error: container has runAsNonRoot and image will run as root 
(pod: "test-basic-math-validation_default(...)", container: git-clone)

Initial Solution (Quick Fix)

We initially fixed this by switching to bitnami/git:latest, which supports running as a non-root user. This works but has limitations: - Git clone happens at runtime (slower pod startup) - Network dependency at pod creation time - Init container overhead - Dependencies installed at runtime via pip

The Insight 💡

User's Key Observation: "This is a perfect use case for the build config or tekton!"

This insight is absolutely correct and represents a fundamental shift in thinking:

Instead of working around the git image issue, we should eliminate the need for a git-clone init container entirely by using S2I/Tekton builds.

Decision

We will support both approaches with S2I/Tekton builds as the recommended production approach:

Approach 1: Init Container (Development/Quick Tests)

  • Use bitnami/git:latest for git-clone init container
  • Quick iteration, simple debugging
  • Good for development and ad-hoc testing
  • When to use: Development, quick tests, debugging
  • Build custom image with notebook repository baked in
  • No init container needed
  • All dependencies pre-installed
  • Faster pod startup, better security
  • When to use: Production, CI/CD pipelines, performance-critical workloads

Rationale

Why S2I/Tekton is Superior

1. Performance

Init Container Approach:
├── Pod Creation: 0s
├── Init Container (git-clone): 10-30s
│   ├── Pull bitnami/git image: 5-10s
│   ├── Clone repository: 5-15s
│   └── Verify notebook: 1-2s
├── Main Container Start: 0s
├── Install dependencies (pip): 30-120s
└── Execute notebook: Variable
Total Startup: 40-150s

S2I Build Approach:
├── Build (one-time): 5-10 minutes
│   ├── Clone repository
│   ├── Install dependencies
│   └── Create image
├── Pod Creation: 0s
├── Pull built image: 5-10s
└── Execute notebook: Variable
Total Startup (after build): 5-10s

Result: 4-15x faster pod startup after initial build!

2. Security

Init Container:
- Runtime dependency installation (pip install)
- Network access required at pod creation
- Dependencies not vetted/scanned
- Git credentials in pod environment

S2I Build:
- Dependencies installed at build time
- Image can be scanned for vulnerabilities
- No runtime network dependency
- Git credentials only in build environment
- Immutable artifact

3. Reproducibility

Init Container:
- Git ref can change (branch moves)
- Dependency versions can change (pip install latest)
- Network issues can cause failures
- Different results on different runs

S2I Build:
- Image is immutable
- Same image = same result
- Can be tagged and versioned
- Cacheable and reusable

4. OpenShift Native

S2I is an OpenShift-native feature:
- Integrated with OpenShift Build API
- Automatic image stream management
- Built-in security scanning
- RBAC integration
- Audit trail

Why Keep Init Container Approach?

  1. Development Velocity: Quick iteration without build overhead
  2. Debugging: Easier to debug git clone issues
  3. Flexibility: Can test different branches/commits quickly
  4. Fallback: Works when S2I/Tekton not available

Implementation

Development Best Practice: CRD Research First

Key Learning: Always research the actual CRDs on the target cluster before implementing build/pipeline logic.

# Research OpenShift Build API
oc api-resources | grep build
oc explain buildconfig.spec --recursive
oc explain buildconfig.spec.triggers
oc explain build.status

# Research Tekton Pipeline API
oc api-resources | grep -E "tekton|pipeline"
oc explain pipelinerun --recursive
oc explain pipeline --recursive

Why This Matters: 1. Discover Auto-Trigger Mechanisms: Found ConfigChangeBuildTriggerType for automatic build triggering 2. Understand API Capabilities: Learn what the platform provides natively 3. Avoid Reinventing: Don't implement what the platform already does 4. Correct API Usage: Use proper subresources and trigger mechanisms

S2I Build Workflow

apiVersion: mlops.mlops.dev/v1alpha1
kind: NotebookValidationJob
metadata:
  name: production-validation
spec:
  notebook:
    git:
      url: "https://github.com/org/notebooks.git"
      ref: "main"
    path: "notebooks/model-validation.ipynb"

  podConfig:
    buildConfig:
      enabled: true                    # Enable S2I build
      strategy: "s2i"                  # Use S2I strategy
      baseImage: "quay.io/jupyter/minimal-notebook:latest"
      requirementsFile: "requirements.txt"
      timeout: "15m"

    containerImage: "quay.io/jupyter/minimal-notebook:latest"  # Fallback

Controller Logic

func (r *NotebookValidationJobReconciler) reconcileValidation(ctx context.Context, job *mlopsv1alpha1.NotebookValidationJob) (ctrl.Result, error) {
    containerImage := job.Spec.PodConfig.ContainerImage

    // Check if build is enabled
    if isBuildEnabled(job) {
        logger.Info("Build integration enabled, handling build workflow")

        // Handle S2I/Tekton build
        builtImage, err := r.handleBuildIntegration(ctx, job)
        if err != nil {
            logger.Error(err, "Build failed, falling back to container image")
            // Fall back to init container approach
        } else {
            logger.Info("Build completed, using built image", "image", builtImage)
            containerImage = builtImage
            // NO INIT CONTAINER NEEDED - notebook is in the image!
        }
    }

    // Create validation pod
    pod, err := r.createValidationPod(ctx, job, containerImage)
    // ...
}

Automatic Build Triggering with ConfigChange

Problem: Builds created without triggers stay in "New" status and require manual triggering.

Solution: Add ConfigChangeBuildTriggerType to BuildConfig triggers.

// pkg/build/s2i_strategy.go - BuildConfig with auto-trigger
bc := &buildv1.BuildConfig{
    ObjectMeta: metav1.ObjectMeta{
        Name:      buildName,
        Namespace: job.Namespace,
    },
    Spec: buildv1.BuildConfigSpec{
        CommonSpec: buildv1.CommonSpec{
            Source:   buildSource,
            Strategy: buildStrategy,
            Output:   buildOutput,
        },
        // Add ConfigChange trigger to automatically start build on creation
        Triggers: []buildv1.BuildTriggerPolicy{
            {
                Type: buildv1.ConfigChangeBuildTriggerType,
            },
        },
    },
}

Result: Build automatically starts when BuildConfig is created - no manual oc start-build needed!

From CRD Research:

$ oc explain buildconfig.spec.triggers
...
- ConfigChange ConfigChangeBuildTriggerType will trigger a build on an
  initial build config creation

Build Strategy Detection

// pkg/build/s2i_strategy.go
func (s *S2IStrategy) Detect(ctx context.Context, client client.Client) (bool, error) {
    // Check if OpenShift Build API is available
    buildConfig := &buildv1.BuildConfig{}
    err := client.Get(ctx, types.NamespacedName{Name: "test", Namespace: "default"}, buildConfig)

    if err != nil {
        if errors.IsNotFound(err) {
            return true, nil  // API exists, resource doesn't
        }
        if runtime.IsNotRegisteredError(err) {
            return false, nil  // API doesn't exist
        }
        return false, err
    }

    return true, nil
}

Consequences

Positive

  1. Performance: Dramatically faster pod startup for production workloads
  2. Security: Better security posture with pre-built, scanned images
  3. Reproducibility: Immutable images ensure consistent results
  4. OpenShift Native: Leverages platform capabilities
  5. Flexibility: Supports both approaches based on use case

Negative

  1. Complexity: More complex workflow with build management
  2. Build Time: Initial build takes 5-10 minutes
  3. Storage: Built images consume registry storage
  4. Learning Curve: Users need to understand S2I/Tekton

Neutral

  1. Dual Approach: Need to maintain both code paths
  2. Documentation: Need clear guidance on when to use each approach
  3. Testing: Need to test both workflows

Migration Path

Phase 1: Fix Init Container (✅ Complete)

  • Switch to bitnami/git:latest
  • Ensure OpenShift compatibility
  • Document approach

Phase 2: Enable S2I Builds (✅ Complete)

  • Implement build integration in controller
  • Add S2I strategy detection
  • Create sample configurations
  • Register OpenShift Build API types in scheme

Phase 3: CRD Research and Auto-Trigger (✅ Complete)

  • Research OpenShift Build CRDs on cluster
  • Discover ConfigChange trigger mechanism
  • Implement automatic build triggering
  • Add ConfigChangeBuildTriggerType to BuildConfig
  • Test auto-trigger functionality

Phase 4: Test and Validate (⏳ In Progress)

  • Test init container approach with bitnami/git
  • Test S2I build workflow with auto-trigger
  • Verify builds start automatically
  • Performance benchmarking

Phase 5: Documentation and Best Practices (⏳ Pending)

  • Document both approaches
  • Provide decision matrix
  • Create migration guide
  • Update samples
  • Document CRD research best practices

Phase 6: Tekton Integration (⏳ Pending)

  • Research Tekton Pipeline CRDs on cluster
  • Implement Tekton strategy with auto-trigger
  • Support custom pipelines
  • Advanced build scenarios

Alternatives Considered

Alternative 1: Always Use Init Container

Rejected: Poor performance, runtime dependencies, security concerns

Alternative 2: Always Use S2I Build

Rejected: Too slow for development, overkill for simple tests

Alternative 3: Custom Git Image

Rejected: Maintenance burden, reinventing the wheel

Alternative 4: Pre-built Image Library

Considered for Future: Maintain library of pre-built images tagged by repo+commit

References

Notes

This ADR captures a key architectural insight: instead of working around platform constraints, leverage platform capabilities. The S2I build approach transforms a problem (git-clone security) into an opportunity (better performance and security through pre-built images).

The user's observation that "this is a perfect use case for build config" represents exactly the kind of pragmatic, platform-aware thinking that leads to better architecture.

Decision Date

2025-11-09

Decision Makers

  • Development Team
  • User (key insight)

Review Date

2025-12-09 (30 days)