Skip to content

feat: Unkey Builds#4098

Merged
chronark merged 42 commits intomainfrom
depot-poc
Oct 27, 2025
Merged

feat: Unkey Builds#4098
chronark merged 42 commits intomainfrom
depot-poc

Conversation

@ogzhanolguncu
Copy link
Contributor

@ogzhanolguncu ogzhanolguncu commented Oct 14, 2025

What does this PR do?

This PR adds a two different ways of building images from scratch for Unkey Deploy. The first one is, like before, a fully local docker setup no additional setup required. Second one is, depot.dev. Depot allows us to build images in isolation so we can safely run untrusted user code. Also, we are using their registry to upload right after a successful build. Depot also helps us build images really fast coz of their monstrous machines and heavy caching.

This PR also gets rid of docker-image arg, it was kinda half baked in our code and until we decide how we handle that scenario I wanted to have a cleaner code. Our code now mostly relies on depot|docker and S3 - any blob storage will work.

Flow of execution is as follows:

  • CLI calls Controlplane and requests a presignedUrl for uploading tar file. We now tar user's code on the client before we upload it to S3, because it's faster to upload zipped files.
  • CLI uploads the tar file
  • CLI gets back the contextKey of the uploaded content.
  • CLI calls createDeployment using this contextKey + Dockerfile path
  • createDeployment calls the createBuild service
  • createBuild requests a presignedGetUrl and passes that to depot directly. Since depot uses buildkit and buildkit can work with URLs directly that makes things easy for us.
  • After depot is done building and pushing to registry, we let krane service handle the rest.

The flow above is for depot, but docker is even simpler than that - we just do everything like above but we don't push to depot we build it locally.

By the way, there' duplicated piece of code called generate_build_url.go I wasn't sure where to put that file so I duplicated it for both of our build backend. I'm open to suggestions.

P.S: I made a db change to store depot_project_id for mapping our clients to depot projects for better isolation. We'll need a schema update.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How should this be tested?

  • Read the QUICKSTART-DEPLOY and setup your environment.
  • Go to /deployments and run ./setup-build-backend.sh docker to test it with docker.
  • Setup your R2|S3 account if you wanna test depot.dev and probably ask an invite from me or Andreas, so I can give a key to you. coz you will need a depot token.
  • Grab yourself a projectID
  • Then run this in /go dir
go run . deploy \
       --context=./demo_api \
       --project-id="proj_your_project_id" \
       --control-plane-url="http://127.0.0.1:7091" \
       --api-key="your-local-dev-key"
  • Then make sure everything works locally.
  • If you tested with docker make sure to test it with depot as well.
  • If you run into an issue with minio locally read the setup section in QUICKSTART-DEPLOY again. You probably missed /etc/host update part.

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Contributing Guide
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand areas
  • Ran pnpm build
  • Ran pnpm fmt
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Unkey Docs if changes were necessary

@changeset-bot
Copy link

changeset-bot bot commented Oct 14, 2025

⚠️ No Changeset found

Latest commit: 3bec7a7

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Oct 14, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
dashboard Ignored Ignored Preview Oct 27, 2025 10:21am
engineering Ignored Ignored Preview Oct 27, 2025 10:21am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 14, 2025

📝 Walkthrough

Walkthrough

Adds a pluggable S3-backed build system and BuildService (CreateBuild, GenerateUploadURL); implements Docker and Depot build backends, presigned upload flow and CLI/control-plane refactor to upload build contexts; updates protobufs and DB schema for BuildContext and GitCommitInfo; wires build-then-deploy through the control plane and krane.

Changes

Cohort / File(s) Summary
Docs & config
QUICKSTART-DEPLOY.md, .gitignore
Quickstart rewritten for configurable build backend (Docker or Depot), DNS/certs, troubleshooting; .gitignore adds deployment/config/depot.json.
Compose & env script
deployment/docker-compose.yaml, deployment/setup-build-backend.sh
Compose adds build/depot and registry env vars; MinIO port adjusted; setup-build-backend.sh interactive script emits .env for chosen backend.
Control-plane config & runtime
go/apps/ctrl/config.go, go/apps/ctrl/run.go, go/cmd/ctrl/main.go
New BuildBackend type/constants; Config extended with BuildBackend/BuildS3/Depot/Registry fields; CLI flags wired; runtime initializes and exposes BuildService and S3 build storage.
S3 storage implementation
go/apps/ctrl/services/build/storage/s3.go
New S3 storage with internal/external presign support, bucket creation, GenerateUploadURL/GenerateDownloadURL and presign logic.
Build backends — Docker
go/apps/ctrl/services/build/backend/docker/*.go
New Docker backend: service constructor, GenerateUploadURL, CreateBuild implementing remote-context Docker build with streamed log parsing and error handling.
Build backends — Depot
go/apps/ctrl/services/build/backend/depot/*.go
New Depot backend: service constructor, GenerateUploadURL, CreateBuild implementing Depot project lookup/create, BuildKit orchestration, registry auth, and push flow.
Deployment service & workflow
go/apps/ctrl/services/deployment/service.go, go/apps/ctrl/services/deployment/create_deployment.go, go/apps/ctrl/workflows/deploy/service.go, go/apps/ctrl/workflows/deploy/deploy_handler.go
CreateDeploymentRequest now supports oneof source (BuildContext or docker_image); GitCommitInfo added; BuildService injected; workflow builds via CreateBuild when context provided before Krane deploy.
Krane & docker backend changes
go/apps/krane/config.go, go/apps/krane/backend/docker/*.go, go/apps/krane/run.go, go/apps/krane/backend/kubernetes/create_deployment.go
Added depot/registry config fields; docker backend now accepts Config (socket + registry creds), supports registry auth and ensureImageExists; k8s sets imagePullSecrets for depot registry.
CLI deploy refactor
go/cmd/deploy/main.go, go/cmd/deploy/control_plane.go, (deleted) go/cmd/deploy/build_docker.go
Replaced local Docker build/push CLI flow with upload-to-presigned-url context flow; added tar/upload helpers; removed large local build code.
Protobuf/API changes
go/proto/ctrl/v1/build.proto, go/proto/ctrl/v1/deployment.proto, go/proto/hydra/v1/deployment.proto
New BuildService (CreateBuild, GenerateUploadURL); CreateDeploymentRequest uses oneof source (BuildContext or docker_image) and nested GitCommitInfo; Hydra DeployRequest gains build_context_path and dockerfile_path.
DB schema & generated code
go/pkg/db/schema.sql, go/pkg/db/models_generated.go, go/pkg/db/project_find_by_id.sql_generated.go, go/pkg/db/project_update_depot_id.sql_generated.go, go/pkg/db/querier_generated.go, go/pkg/db/queries/project_update_depot_id.sql, internal/db/src/schema/projects.ts
Adds nullable projects.depot_project_id; updates generated Go/TS models and adds UpdateProjectDepotID SQL and generated methods.
Vault storage & middleware
go/pkg/vault/storage/interface.go, go/pkg/vault/storage/memory.go, go/pkg/vault/storage/middleware/tracing.go, go/pkg/vault/storage/s3.go
ErrObjectNotFound moved to standalone var; tracing instrumentation added for GetObject/ListObjectKeys; small formatting tweaks.
CLI/server wiring & deps
go/go.mod, go/cmd/ctrl/main.go, go/apps/ctrl/*
Added/updated dependencies (depot API, Docker/Moby, BuildKit, connectrpc/codegen updates); CLI flags and wiring extended for build/depot settings.
Tests & build tooling
go/apps/ctrl/services/deployment/create_deployment_simple_test.go, go/Dockerfile.tilt
Tests updated for nested GitCommitInfo and BuildContext; Dockerfile.tilt base switched to alpine and installs CA certs.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant CLI as Developer CLI
  participant CP as Control Plane (ctrl)
  participant S3 as S3 Storage
  participant WF as Deploy Workflow
  participant BE as Build Backend (Docker/Depot)
  participant KR as Krane

  CLI->>CP: GenerateUploadURL(unkey_project_id)
  CP-->>CLI: {upload_url, context_key}
  CLI->>S3: PUT tar.gz -> upload_url
  CLI->>CP: CreateDeployment(context_key, dockerfile_path?)
  CP->>WF: Start workflow

  rect rgba(230,245,255,0.6)
    note over WF: Build phase (if BuildContext)
    WF->>CP: CreateBuild(context_key, dockerfile_path, unkey_project_id, deployment_id)
    CP->>BE: CreateBuild(...)
    BE-->>CP: {image_name, build_id}
  end

  rect rgba(240,255,230,0.6)
    note over WF: Deploy phase
    WF->>KR: CreateDeployment(image_name,...)
    KR-->>WF: status updates
  end

  WF-->>CP: Final status
  CP-->>CLI: Deployment result
Loading
sequenceDiagram
  autonumber
  participant CP as Control Plane
  participant Depot as Depot API
  participant S3 as S3 Storage
  participant BK as BuildKit
  participant Registry as Image Registry

  CP->>S3: Presign PUT for context_key
  S3-->>CP: upload_url

  CP->>Depot: Ensure/get-or-create project
  CP->>Depot: Create build (context presigned URL)
  Depot->>BK: Acquire machine / run build
  BK-->>Depot: build stream / push to Registry
  Registry-->>CP: image available
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Pay special attention to:

  • Build backends (docker vs depot): correctness of orchestration, auth, streaming parsing and error handling.
  • S3 presign logic and choice between internal vs external presign endpoints.
  • Protobuf changes and generated code alignment (regeneration, tag ordering).
  • CLI deploy refactor and tar/upload helpers (platform path/permission edge cases).
  • DB migration and generated SQL for depot_project_id.

Possibly related PRs

Suggested labels

Core Team

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "feat: Unkey Builds" is directly related to the main change in this PR, which introduces a new build system supporting two backends (Docker and Depot) for Unkey Deploy. The title uses proper conventional commit format with the "feat:" prefix and clearly indicates that new build functionality is being added. While the title could be more specific (e.g., "Build Backends" or "Build Infrastructure"), it effectively communicates that this PR adds build capabilities to Unkey and is not misleading or vague. A developer scanning the git history would understand this introduces build-related functionality.
Description Check ✅ Passed The PR description comprehensively addresses all required template sections. The "What does this PR do?" section provides detailed context about the two build backends, the rationale (isolation, caching, speed), removed functionality, and the execution flow. The type of change is properly selected as "New feature." The "How should this be tested?" section provides clear, actionable instructions including setup steps, configuration guidance, and a concrete CLI command example. All required checklist items are checked off, indicating the author completed self-review, testing, and code quality checks. While the appreciated section items are not completed, all critical sections are thorough and complete.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch depot-poc

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ogzhanolguncu ogzhanolguncu marked this pull request as ready for review October 14, 2025 15:09
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
go/apps/ctrl/services/deployment/create_deployment_simple_test.go (1)

239-462: Test duplicates service logic instead of testing the actual service.

Lines 411-440 manually replicate the extraction and mapping logic that should exist in the service implementation. This test validates its own reimplementation rather than the actual service behavior. If the service's mapping logic differs or changes, this test will pass while the service may be broken.

Recommended solutions:

  1. Call the actual service (preferred): Refactor the test to invoke CreateDeployment with the request and verify the resulting database parameters.
  2. Extract shared mapping logic: If direct service testing is impractical, extract the mapping logic (lines 411-440) into a standalone, reusable function in the service package and test that function here.

Example for solution 1:

func TestCreateDeploymentFieldMapping(t *testing.T) {
	t.Parallel()
	// ... existing test cases ...
	
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			
			// Mock dependencies and call actual service
			svc := setupTestService(t) // setup with mocked DB, etc.
			
			result, err := svc.CreateDeployment(ctx, tt.request)
			require.NoError(t, err)
			
			// Verify the deployment params passed to DB match expected values
			capturedParams := mockDB.GetInsertDeploymentParams()
			require.Equal(t, tt.expected.gitCommitSha, capturedParams.GitCommitSha.String)
			// ... assert other fields ...
		})
	}
}
♻️ Duplicate comments (5)
go/apps/ctrl/services/build/backend/docker/generate_upload_url.go (1)

13-42: Deduplicate with Depot.GenerateUploadURL via shared helper

Extract common logic into a helper (e.g., services/build/common.GenerateUploadURL) used by both backends to ensure identical behavior and single-point changes. I can open an issue or draft the refactor on request.

go/cmd/ctrl/main.go (1)

79-80: Clarify when external S3 URL is required

-		cli.String("build-s3-external-url", "S3 Compatible Endpoint URL for build contexts (external/public)",
+		cli.String("build-s3-external-url", "S3 Compatible Endpoint URL for build contexts (external/public). Required when the CLI or Depot runs outside the Docker network.",
 			cli.EnvVar("UNKEY_BUILD_S3_EXTERNAL_URL")),
go/apps/ctrl/services/build/backend/depot/create_build.go (1)

227-236: Fix logging key/value mismatch and use string value for NullString

-		s.logger.Info(
-			"Returning existing depot project",
-			"depot_project_id",
-			project.DepotProjectID,
-			"unkey_project_id",
-			"project_name",
-			projectName,
-		)
+		s.logger.Info(
+			"Returning existing depot project",
+			"depot_project_id", project.DepotProjectID.String,
+			"unkey_project_id", unkeyProjectID,
+			"project_name", projectName,
+		)
go/apps/ctrl/config.go (1)

143-154: Require full Depot config (API URL, registry URL, username)

 	case BuildBackendDepot:
-		return assert.All(
+		return assert.All(
+			assert.NotEmpty(c.Depot.APIUrl, "Depot API URL is required when using Depot backend"),
+			assert.NotEmpty(c.Depot.RegistryUrl, "Depot registry URL is required when using Depot backend"),
+			assert.NotEmpty(c.Depot.Username, "Depot registry username is required when using Depot backend"),
 			assert.NotEmpty(c.BuildS3.URL, "build S3 URL is required when using Depot backend"),
 			assert.NotEmpty(c.BuildS3.Bucket, "build S3 bucket is required when using Depot backend"),
 			assert.NotEmpty(c.BuildS3.AccessKeyID, "build S3 access key ID is required when using Depot backend"),
 			assert.NotEmpty(c.BuildS3.AccessKeySecret, "build S3 access key secret is required when using Depot backend"),
 			assert.NotEmpty(c.Depot.AccessToken, "Depot access token is required when using Depot backend"),
 			assert.NotEmpty(c.Depot.BuildPlatform, "Depot build platform is required when using Depot backend"), // ADD THIS
 			assert.NotEmpty(c.Depot.ProjectRegion, "Depot project region is required when using Depot backend"), // ADD THIS TOO
 		)
go/apps/ctrl/run.go (1)

315-323: Register the HTTP handler with the handler impl; pass client into deployment service

-	mux.Handle(ctrlv1connect.NewBuildServiceHandler(buildService, connectOptions...))
+	mux.Handle(ctrlv1connect.NewBuildServiceHandler(buildHandler, connectOptions...))
 	mux.Handle(ctrlv1connect.NewCtrlServiceHandler(ctrl.New(cfg.InstanceID, database), connectOptions...))
 	mux.Handle(ctrlv1connect.NewDeploymentServiceHandler(deployment.New(deployment.Config{
-		Database:     database,
-		PartitionDB:  partitionDB,
-		Restate:      restateClient,
-		BuildService: buildService,
-		Logger:       logger,
+		Database:    database,
+		PartitionDB: partitionDB,
+		Restate:     restateClient,
+		BuildService: buildClient,
+		Logger:      logger,
 	}), connectOptions...))
🧹 Nitpick comments (9)
go/apps/ctrl/services/deployment/create_deployment_simple_test.go (1)

155-179: Simplify test to focus on timestamp validation.

The test constructs a full CreateDeploymentRequest but only validates the timestamp field using the validateTimestamp helper. Either simplify this test to directly call validateTimestamp with various timestamp values (avoiding the unnecessary request construction), or integrate it with the actual service to validate end-to-end behavior.

Apply this diff to simplify the test:

-// TestCreateDeploymentTimestampValidation_InvalidSecondsFormat tests timestamp validation
-func TestCreateDeploymentTimestampValidation_InvalidSecondsFormat(t *testing.T) {
+// TestValidateTimestamp_InvalidSecondsFormat tests timestamp validation with seconds-based input
+func TestValidateTimestamp_InvalidSecondsFormat(t *testing.T) {
 	t.Parallel()
 
-	// Create proto request directly with seconds timestamp (should be rejected)
-	req := &ctrlv1.CreateDeploymentRequest{
-		ProjectId:       "proj_test456",
-		Branch:          "main",
-		EnvironmentSlug: "production",
-		Source: &ctrlv1.CreateDeploymentRequest_BuildContext{
-			BuildContext: &ctrlv1.BuildContext{
-				ContextKey:     "test-key",
-				DockerfilePath: ptr.P("Dockerfile"),
-			},
-		},
-		GitCommit: &ctrlv1.GitCommitInfo{
-			CommitSha: "abc123def456",
-			Timestamp: time.Now().Unix(), // This is in seconds - should be rejected
-		},
-	}
+	// Seconds-based timestamp (should be rejected)
+	secondsTimestamp := time.Now().Unix()
 
-	// Use shared validation helper
-	isValid := validateTimestamp(req.GetGitCommit().GetTimestamp())
+	isValid := validateTimestamp(secondsTimestamp)
 	require.False(t, isValid, "Seconds-based timestamp should be considered invalid")
 }
go/apps/ctrl/services/build/backend/depot/create_build.go (3)

158-165: Only set BuildKit platform attr when resolved

Avoid passing empty platform; build attr map first, then conditionally set.

-	solverOptions := client.SolveOpt{
-		Frontend: "dockerfile.v0",
-		FrontendAttrs: map[string]string{
-			"platform": platform,
-			"context":  contextURL,
-			"filename": dockerfilePath,
-		},
+	attrs := map[string]string{
+		"context":  contextURL,
+		"filename": dockerfilePath,
+	}
+	if platform != "" {
+		attrs["platform"] = platform
+	}
+	solverOptions := client.SolveOpt{
+		Frontend:      "dockerfile.v0",
+		FrontendAttrs: attrs,

240-246: Add HTTP client timeout for Depot API calls

Avoid hung requests by setting a sane timeout.

-	httpClient := &http.Client{}
+	httpClient := &http.Client{
+		Timeout: 15 * time.Second,
+	}

177-186: Optional: enable registry cache to speed rebuilds

Leverage BuildKit cache import/export to cut build times.

 		Exports: []client.ExportEntry{
 			{
 				Type: "image",
 				Attrs: map[string]string{
 					"name":           imageName,
 					"oci-mediatypes": "true",
 					"push":           "true",
+					// "compression":  "estargz", // optional
 				},
 			},
 		},

Outside this hunk, also consider:

// Before Solve:
solverOptions.FrontendAttrs["cache-from"] = "type=registry,ref=" + imageName
solverOptions.Exports[0].Attrs["cache-to"] = "type=registry,ref=" + imageName + ",mode=max"
deployment/docker-compose.yaml (2)

362-368: Set external S3 URL for local CLI/Depot access

When using Depot or running the CLI outside Docker, expose MinIO via host URL so presigned GET/PUT are reachable.

-      UNKEY_BUILD_S3_EXTERNAL_URL: "${UNKEY_BUILD_S3_EXTERNAL_URL:-}" # For CLI/external access
+      UNKEY_BUILD_S3_EXTERNAL_URL: "${UNKEY_BUILD_S3_EXTERNAL_URL:-http://localhost:3902}" # For CLI/depot access from host

289-292: Optional: move registry creds into .env for dev

Reduce accidental commits of credentials; source via env_file.

go/apps/ctrl/services/build/backend/depot/generate_upload_url.go (1)

37-41: Optional: avoid magic number for expiry

Use a named constant to keep request/response aligned.

-	return connect.NewResponse(&ctrlv1.GenerateUploadURLResponse{
-		UploadUrl:  uploadURL,
-		ContextKey: buildContextPath,
-		ExpiresIn:  900, // 15 minutes
-	}), nil
+	const expires = int64(15 * 60)
+	return connect.NewResponse(&ctrlv1.GenerateUploadURLResponse{
+		UploadUrl:  uploadURL,
+		ContextKey: buildContextPath,
+		ExpiresIn:  expires,
+	}), nil
go/cmd/ctrl/main.go (1)

93-96: Optional: default Depot username to “x-token”

That’s the common value for token-based auth; saves config friction.

-		cli.String("depot-username", "Depot registry username",
-			cli.EnvVar("UNKEY_DEPOT_USERNAME")),
+		cli.String("depot-username", "Depot registry username (often 'x-token')",
+			cli.EnvVar("UNKEY_DEPOT_USERNAME"), cli.Default("x-token")),
go/apps/ctrl/run.go (1)

173-180: Avoid import shadowing and clarify presign URL comment

Rename local var to buildS3; update comment to reflect Depot also needs a public URL when outside Docker network.

-	buildStorage, err := buildStorage.NewS3(buildStorage.S3Config{
+	buildS3, err := buildStorage.NewS3(buildStorage.S3Config{
 		Logger:            logger,
 		S3URL:             cfg.BuildS3.URL,
-		S3PresignURL:      cfg.BuildS3.ExternalURL, // Empty for Depot, set for Docker
+		S3PresignURL:      cfg.BuildS3.ExternalURL, // Required when callers (CLI/Depot BuildKit) are outside the Docker network
 		S3Bucket:          cfg.BuildS3.Bucket,
 		S3AccessKeyID:     cfg.BuildS3.AccessKeyID,
 		S3AccessKeySecret: cfg.BuildS3.AccessKeySecret,
 	})
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b920b57 and a2e39b3.

📒 Files selected for processing (10)
  • deployment/docker-compose.yaml (3 hunks)
  • go/apps/ctrl/config.go (4 hunks)
  • go/apps/ctrl/run.go (3 hunks)
  • go/apps/ctrl/services/build/backend/depot/create_build.go (1 hunks)
  • go/apps/ctrl/services/build/backend/depot/generate_upload_url.go (1 hunks)
  • go/apps/ctrl/services/build/backend/depot/service.go (1 hunks)
  • go/apps/ctrl/services/build/backend/docker/generate_upload_url.go (1 hunks)
  • go/apps/ctrl/services/build/storage/s3.go (1 hunks)
  • go/apps/ctrl/services/deployment/create_deployment_simple_test.go (18 hunks)
  • go/cmd/ctrl/main.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • go/apps/ctrl/services/build/backend/depot/service.go
  • go/apps/ctrl/services/build/storage/s3.go
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-15T18:12:01.503Z
Learnt from: mcstepp
PR: unkeyed/unkey#3952
File: go/apps/ctrl/run.go:212-213
Timestamp: 2025-09-15T18:12:01.503Z
Learning: In Go connect-go generated handlers, mux.Handle can accept New*ServiceHandler functions directly even though they return (string, http.Handler) tuples. The pattern mux.Handle(ctrlv1connect.New*ServiceHandler(...)) is valid and compiles successfully in unkey codebase.

Applied to files:

  • go/apps/ctrl/run.go
📚 Learning: 2025-08-08T14:55:11.981Z
Learnt from: imeyer
PR: unkeyed/unkey#3755
File: deployment/docker-compose.yaml:179-184
Timestamp: 2025-08-08T14:55:11.981Z
Learning: In deployment/docker-compose.yaml (development only), MinIO is configured with API on 3902 and console on 3903; ports should map 3902:3902 and 3903:3903 to match MINIO_API_PORT_NUMBER and MINIO_CONSOLE_PORT_NUMBER.

Applied to files:

  • deployment/docker-compose.yaml
🧬 Code graph analysis (7)
go/cmd/ctrl/main.go (3)
go/pkg/cli/flag.go (4)
  • String (419-451)
  • Default (364-416)
  • EnvVar (320-339)
  • Required (298-317)
go/apps/ctrl/config.go (3)
  • BuildBackend (11-11)
  • S3Config (18-24)
  • DepotConfig (57-70)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (11-23)
go/apps/ctrl/services/build/backend/docker/generate_upload_url.go (3)
go/apps/ctrl/services/build/backend/docker/service.go (1)
  • Docker (11-17)
internal/proto/generated/ctrl/v1/build_pb.ts (2)
  • GenerateUploadURLRequest (94-101)
  • GenerateUploadURLResponse (114-135)
go/gen/proto/ctrl/v1/build.pb.go (6)
  • GenerateUploadURLRequest (152-157)
  • GenerateUploadURLRequest (170-170)
  • GenerateUploadURLRequest (185-187)
  • GenerateUploadURLResponse (196-203)
  • GenerateUploadURLResponse (216-216)
  • GenerateUploadURLResponse (231-233)
go/apps/ctrl/run.go (7)
go/apps/ctrl/services/build/storage/s3.go (2)
  • NewS3 (33-107)
  • S3Config (24-31)
go/apps/ctrl/config.go (5)
  • S3Config (18-24)
  • BuildBackend (11-11)
  • BuildBackendDocker (15-15)
  • Config (72-133)
  • BuildBackendDepot (14-14)
go/gen/proto/ctrl/v1/ctrlv1connect/build.connect.go (1)
  • BuildServiceClient (45-48)
go/apps/ctrl/services/build/backend/depot/service.go (3)
  • New (40-54)
  • Config (25-38)
  • Depot (11-23)
go/apps/ctrl/services/build/backend/docker/service.go (2)
  • New (26-34)
  • Config (19-24)
go/apps/ctrl/workflows/deploy/service.go (2)
  • New (57-66)
  • Config (36-54)
go/apps/ctrl/services/deployment/service.go (2)
  • New (27-36)
  • Config (19-25)
go/apps/ctrl/config.go (3)
go/apps/ctrl/services/build/storage/s3.go (1)
  • S3Config (24-31)
go/pkg/vault/storage/s3.go (1)
  • S3Config (25-31)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (11-23)
go/apps/ctrl/services/build/backend/depot/generate_upload_url.go (3)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (11-23)
internal/proto/generated/ctrl/v1/build_pb.ts (2)
  • GenerateUploadURLRequest (94-101)
  • GenerateUploadURLResponse (114-135)
go/gen/proto/ctrl/v1/build.pb.go (6)
  • GenerateUploadURLRequest (152-157)
  • GenerateUploadURLRequest (170-170)
  • GenerateUploadURLRequest (185-187)
  • GenerateUploadURLResponse (196-203)
  • GenerateUploadURLResponse (216-216)
  • GenerateUploadURLResponse (231-233)
go/apps/ctrl/services/deployment/create_deployment_simple_test.go (3)
internal/proto/generated/ctrl/v1/deployment_pb.ts (3)
  • GitCommitInfo (114-141)
  • CreateDeploymentRequest (21-74)
  • BuildContext (87-101)
go/gen/proto/ctrl/v1/deployment.pb.go (11)
  • GitCommitInfo (314-323)
  • GitCommitInfo (336-336)
  • GitCommitInfo (351-353)
  • CreateDeploymentRequest (136-154)
  • CreateDeploymentRequest (167-167)
  • CreateDeploymentRequest (182-184)
  • CreateDeploymentRequest_BuildContext (250-252)
  • CreateDeploymentRequest_BuildContext (258-258)
  • BuildContext (262-268)
  • BuildContext (281-281)
  • BuildContext (296-298)
go/pkg/db/deployment_insert.sql_generated.go (1)
  • InsertDeploymentParams (51-67)
go/apps/ctrl/services/build/backend/depot/create_build.go (3)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (11-23)
go/gen/proto/ctrl/v1/build.pb.go (3)
  • CreateBuildRequest (24-32)
  • CreateBuildRequest (45-45)
  • CreateBuildRequest (60-62)
go/pkg/db/project_update_depot_id.sql_generated.go (1)
  • UpdateProjectDepotIDParams (21-25)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build / Build
  • GitHub Check: Test Packages / Test
🔇 Additional comments (3)
go/apps/ctrl/services/deployment/create_deployment_simple_test.go (2)

11-11: LGTM!

The ptr import is appropriately used to construct optional protobuf fields in test data (e.g., DockerfilePath: ptr.P("Dockerfile")).


1-462: Re: past comment—file provides value but needs refactoring.

In response to the past review comment questioning the file's value: the tests cover important edge cases (special characters, NULL handling, timestamp boundaries) that are worth preserving. However, as flagged in the separate comment on TestCreateDeploymentFieldMapping, the current approach duplicates service logic rather than testing the actual service implementation.

Recommendation: Refactor the file to either:

  1. Test the actual CreateDeployment service with these inputs and verify database parameters, or
  2. Keep only the validation helper tests and remove the field mapping test.

Based on learnings

deployment/docker-compose.yaml (1)

169-175: MinIO console port mapping corrected

3903:3903 matches MINIO_CONSOLE_PORT_NUMBER. LGTM.

@ogzhanolguncu ogzhanolguncu requested a review from Flo4604 October 21, 2025 14:07
@ogzhanolguncu
Copy link
Contributor Author

@Flo4604 I'll rename contextKey after rebasing from your branch. fyi

@ogzhanolguncu
Copy link
Contributor Author

@Flo4604 I renamed contextKey to contextBuildPath and double checked everything again. Everything seems to work, can you just check k8s setup for me?

Copy link
Member

Flo4604 commented Oct 23, 2025

yeah will do it in a bit

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (9)
go/apps/ctrl/services/build/backend/docker/generate_upload_url.go (1)

13-42: Consolidate duplicate GenerateUploadURL implementations.

This mirrors the Depot version; extract to a shared helper and delegate.

go/apps/ctrl/services/build/backend/docker/create_build.go (2)

31-36: Validate DeploymentId is required.

Prevents ambiguous image naming and early client feedback.

 if err := assert.All(
   assert.NotEmpty(req.Msg.BuildContextPath, "build_context_path is required"),
   assert.NotEmpty(req.Msg.UnkeyProjectId, "unkey_project_id is required"),
+  assert.NotEmpty(req.Msg.DeploymentId, "deployment_id is required"),
 ); err != nil {

68-74: Tag the image (and avoid case-collision); current name defaults to :latest.

Untagged repo ⇒ K8s likely pulls docker.io/:latest; collisions and stale pulls risk.

-// Docker requires lowercase repository names
-imageName := strings.ToLower(fmt.Sprintf("%s-%s",
-  req.Msg.UnkeyProjectId,
-  req.Msg.DeploymentId,
-))
+// Lowercase repository; include unique tag to avoid :latest ambiguity
+repo := strings.ToLower(fmt.Sprintf("%s-%s", req.Msg.UnkeyProjectId, req.Msg.DeploymentId))
+imageName := fmt.Sprintf("%s:%d", repo, timestamp)

Optionally validate inputs are lowercase to avoid case-only collisions, or append a short hash of original IDs.

go/apps/ctrl/services/deployment/create_deployment.go (1)

121-131: Avoid logging pointer values; always log a concrete docker_image string

Compute a safe string once and use it in all logs to prevent pointer addresses from appearing and to keep logs uniform.

@@
-    // Log deployment source
-    if buildContextKey != "" {
+    // Log deployment source
+    img := ""
+    if dockerImage != nil {
+        img = *dockerImage
+    }
+    if buildContextKey != "" {
         s.logger.Info("deployment will build from source",
             "deployment_id", deploymentID,
             "context_key", buildContextKey,
             "dockerfile", dockerfilePath)
     } else {
         s.logger.Info("deployment will use prebuilt image",
             "deployment_id", deploymentID,
-            "image", *dockerImage)
+            "image", img)
     }
@@
-    s.logger.Info("starting deployment workflow",
+    s.logger.Info("starting deployment workflow",
         "deployment_id", deploymentID,
         "workspace_id", workspaceID,
         "project_id", req.Msg.GetProjectId(),
         "environment", env.ID,
-        "context_key", buildContextKey,
-        "docker_image", dockerImage,
+        "context_key", buildContextKey,
+        "docker_image", img,
     )

Also applies to: 160-167

go/cmd/deploy/control_plane.go (2)

189-201: Don’t send empty optional strings; set KeyspaceId only when non‑empty

Avoid marking oneof/optionals as “set” with empty values.

-    req := &ctrlv1.CreateDeploymentRequest{
+    req := &ctrlv1.CreateDeploymentRequest{
         ProjectId:       c.opts.ProjectID,
-        KeyspaceId:      &c.opts.KeyspaceID,
         Branch:          c.opts.Branch,
         EnvironmentSlug: c.opts.Environment,
         GitCommit: &ctrlv1.GitCommitInfo{
             CommitSha:       commitInfo.CommitSHA,
             CommitMessage:   commitInfo.Message,
             AuthorHandle:    commitInfo.AuthorHandle,
             AuthorAvatarUrl: commitInfo.AuthorAvatarURL,
             Timestamp:       commitInfo.CommitTimestamp,
         },
     }
+    if c.opts.KeyspaceID != "" {
+        req.KeyspaceId = ptr.P(c.opts.KeyspaceID)
+    }

152-170: Harden temp handling; check tar availability; drop world‑writable perms

Fixed path /tmp/ctrl with 0777 and chmod 0666 is unsafe; also guard for tar presence. Keep files 0600 by default. Respecting .dockerignore is tracked, but add TODO to avoid leaking secrets.

@@
-    absContextPath, err := filepath.Abs(contextPath)
+    absContextPath, err := filepath.Abs(contextPath)
     if err != nil {
         return "", fmt.Errorf("failed to get absolute context path: %w", err)
     }
 
-    sharedDir := "/tmp/ctrl"
-    if err := os.MkdirAll(sharedDir, 0o777); err != nil {
+    // Ensure tar is available
+    if _, err := exec.LookPath("tar"); err != nil {
+        return "", fmt.Errorf("required 'tar' binary not found in PATH: %w", err)
+    }
+
+    // Use a private temp dir
+    sharedDir := filepath.Join(os.TempDir(), "ctrl")
+    if err := os.MkdirAll(sharedDir, 0o700); err != nil {
         return "", fmt.Errorf("failed to create shared dir: %w", err)
     }
 
     tmpFile, err := os.CreateTemp(sharedDir, "build-context-*.tar.gz")
     if err != nil {
         return "", fmt.Errorf("failed to create temp file: %w", err)
     }
     tmpFile.Close()
     tarPath := tmpFile.Name()
 
-    if err := os.Chmod(tarPath, 0o666); err != nil {
-        os.Remove(tarPath)
-        return "", fmt.Errorf("failed to set file permissions: %w", err)
-    }
+    // File inherits 0600 from CreateTemp; keep it private
@@
-    cmd := exec.Command("tar", "-czf", tarPath, "-C", absContextPath, ".")
+    // TODO: respect .dockerignore/.gitignore to avoid uploading secrets or huge directories.
+    cmd := exec.Command("tar", "-czf", tarPath, "-C", absContextPath, ".")

Also applies to: 169-174

go/apps/ctrl/services/build/backend/depot/create_build.go (3)

50-62: Make platform parsing robust; support empty/“dynamic” and default to amd64

Current split/assert fails when unset and rejects “dynamic”. Default to a safe arch and only constrain BuildKit when explicitly set.

-    buildPlatform := strings.TrimPrefix(s.buildPlatform, "/")
-    parts := strings.Split(buildPlatform, "/")
-
-    if err := assert.All(
-        assert.Equal(len(parts), 2, fmt.Sprintf("invalid build platform format: %s (expected format: linux/amd64)", s.buildPlatform)),
-        assert.Equal(parts[0], "linux", fmt.Sprintf("unsupported OS: %s (only linux is supported)", parts[0])),
-    ); err != nil {
-        return nil, connect.NewError(connect.CodeInvalidArgument, err)
-    }
-
-    platform := buildPlatform
-    architecture := parts[1]
+    buildPlatform := strings.TrimPrefix(s.buildPlatform, "/")
+    var platform string
+    var architecture string
+    if buildPlatform == "" || buildPlatform == "dynamic" {
+        // Let BuildKit choose; default to a common arch for machine acquisition
+        platform = ""          // do not set FrontendAttrs["platform"]
+        architecture = "amd64" // TODO: make configurable
+    } else {
+        parts := strings.Split(buildPlatform, "/")
+        if err := assert.All(
+            assert.Equal(len(parts), 2, fmt.Sprintf("invalid build platform format: %s (expected: linux/amd64 or dynamic)", s.buildPlatform)),
+            assert.Equal(parts[0], "linux", fmt.Sprintf("unsupported OS: %s (only linux is supported)", parts[0])),
+        ); err != nil {
+            return nil, connect.NewError(connect.CodeInvalidArgument, err)
+        }
+        platform = buildPlatform
+        architecture = parts[1]
+    }

110-114: Log both platform and architecture (label was wrong)

Label was “platform” but value was architecture. Include both for clarity.

-    s.logger.Info("Acquiring build machine",
-        "build_id", buildResp.ID,
-        "platform", architecture,
-        "unkey_project_id", req.Msg.UnkeyProjectId)
+    s.logger.Info("Acquiring build machine",
+        "build_id", buildResp.ID,
+        "platform", platform,
+        "architecture", architecture,
+        "unkey_project_id", req.Msg.UnkeyProjectId)

229-236: Fix logging key/value mismatch and use concrete strings

Pairs are misaligned and DepotProjectID is logged as sql.NullString.

-    s.logger.Info(
-        "Returning existing depot project",
-        "depot_project_id",
-        project.DepotProjectID,
-        "unkey_project_id",
-        "project_name",
-        projectName,
-    )
+    s.logger.Info(
+        "Returning existing depot project",
+        "depot_project_id", project.DepotProjectID.String,
+        "unkey_project_id", unkeyProjectID,
+        "project_name", projectName,
+    )
🧹 Nitpick comments (16)
deployment/docker-compose.yaml (1)

361-376: Clarify the section comment and verify S3 external URL setup.

Lines 371 and 376 both reference "Depot configuration," but line 371 actually begins the build/backend/Docker configuration section. Rename the line 371 comment to "Build configuration" for clarity.

Additionally, UNKEY_BUILD_S3_EXTERNAL_URL (line 364) defaults to empty. This is intended for CLI/external access to S3. Ensure that documentation or the setup script (setup-build-backend.sh mentioned in PR objectives) explains how to configure this for local or remote deployments.

Apply this diff to improve clarity:

-      # API key for simple authentication (temporary, will be replaced with JWT)
+      # API key for simple authentication (temporary, will be replaced with JWT)
       UNKEY_API_KEY: "your-local-dev-key"
-      # Depot configuration
+      # Build configuration
       UNKEY_BUILD_BACKEND: "${UNKEY_BUILD_BACKEND:-docker}"
go/apps/ctrl/services/build/backend/docker/create_build.go (3)

38-40: Make build platform configurable.

Hardcoding linux/amd64 can break on ARM hosts or incur slow QEMU emulation. Read from config (mirroring Depot’s buildPlatform) or derive sensible default and allow override.


106-113: Scanner buffer may truncate long JSON lines.

Increase buffer or use json.Decoder to avoid ErrTooLong on verbose builds.

-scanner := bufio.NewScanner(buildResponse.Body)
+scanner := bufio.NewScanner(buildResponse.Body)
+scanner.Buffer(make([]byte, 0, 1<<20), 1<<20) // up to 1MB per line

115-121: Prefer robust error extraction from build stream.

Fallback to resp.Error when ErrorDetail.Message is empty for clearer diagnostics.

- if resp.Error != "" {
-   buildError = fmt.Errorf("%s", resp.ErrorDetail.Message)
+ if resp.Error != "" {
+   msg := resp.ErrorDetail.Message
+   if msg == "" {
+     msg = resp.Error
+   }
+   buildError = fmt.Errorf("%s", msg)
go/apps/ctrl/services/build/backend/depot/generate_upload_url.go (2)

22-41: Consolidate with Docker.GenerateUploadURL to remove duplication.

Extract shared logic into a common helper (e.g., services/build/common.GenerateUploadURL) and delegate from both backends.


30-36: Align log field names with API field.

Use build_context_path in logs for consistency.

- s.logger.Error("Failed to generate presigned URL", "error", err, "context_key", buildContextPath)
+ s.logger.Error("Failed to generate presigned URL", "error", err, "build_context_path", buildContextPath)
...
- s.logger.Info("Generated upload URL", "context_key", buildContextPath, "unkey_project_id", req.Msg.UnkeyProjectId)
+ s.logger.Info("Generated upload URL", "build_context_path", buildContextPath, "unkey_project_id", req.Msg.UnkeyProjectId)
go/apps/ctrl/workflows/deploy/deploy_handler.go (3)

281-286: HTTP calls lack timeouts; risk of hanging step.

Use an http.Client with a reasonable Timeout.

- resp, err := http.DefaultClient.Get(openapiURL)
+ client := &http.Client{Timeout: 5 * time.Second}
+ resp, err := client.Get(openapiURL)

320-329: Hardcoded source type.

Plumb actual source type from request (build vs prebuilt) to buildDomains for accurate domain stickiness/routing.


83-91: Prefer typed status constants for steps.

Replace string literals ("pending"/"completed") with generated enum/consts for consistency and to avoid typos.

Also applies to: 147-156, 412-420

go/apps/ctrl/services/build/backend/docker/generate_upload_url.go (1)

30-36: Rename log field to build_context_path for consistency.

Matches API and other logs.

- s.logger.Error("Failed to generate presigned URL", "error", err, "context_key", buildContextPath)
+ s.logger.Error("Failed to generate presigned URL", "error", err, "build_context_path", buildContextPath)
...
- s.logger.Info("Generated upload URL", "context_key", buildContextPath, "unkey_project_id", req.Msg.UnkeyProjectId)
+ s.logger.Info("Generated upload URL", "build_context_path", buildContextPath, "unkey_project_id", req.Msg.UnkeyProjectId)
go/proto/ctrl/v1/deployment.proto (1)

17-22: Remove unused SourceType enum

This enum is no longer referenced after introducing the oneof source; keeping it invites drift. Please remove it to avoid confusion.

go/apps/ctrl/services/deployment/create_deployment.go (2)

65-73: Fix inaccurate comment about 1e12 ms threshold

1,000,000,000,000 ms ≈ September 2001, not January 1, 2001. Update the comment to avoid confusion.

-        // Reject timestamps that are clearly in seconds format (< 1_000_000_000_000)
-        // This corresponds to January 1, 2001 in milliseconds
+        // Reject timestamps that are clearly in seconds format (< 1_000_000_000_000).
+        // Note: 1e12 ms is ~Sep 2001; used here only to distinguish seconds vs. milliseconds.

211-216: Trim by runes to avoid breaking multi‑byte characters

Current byte-slice truncation can split UTF‑8 (e.g., emojis). Use rune slicing.

-func trimLength(s string, characters int) string {
-    if len(s) > characters {
-        return s[:characters]
-    }
-    return s
-}
+func trimLength(s string, characters int) string {
+    r := []rune(s)
+    if len(r) > characters {
+        return string(r[:characters])
+    }
+    return s
+}
go/apps/ctrl/services/build/backend/depot/create_build.go (1)

158-164: Set FrontendAttrs["platform"] only when resolved

Skip the platform key when letting BuildKit decide dynamically.

-    solverOptions := client.SolveOpt{
+    frontendAttrs := map[string]string{
+        "context":  contextURL,
+        "filename": dockerfilePath,
+    }
+    if platform != "" {
+        frontendAttrs["platform"] = platform
+    }
+    solverOptions := client.SolveOpt{
         Frontend: "dockerfile.v0",
-        FrontendAttrs: map[string]string{
-            "platform": platform,
-            "context":  contextURL,
-            "filename": dockerfilePath,
-        },
+        FrontendAttrs: frontendAttrs,
         Session: []session.Attachable{
go/apps/ctrl/services/deployment/create_deployment_simple_test.go (1)

239-462: Test simulates service logic instead of testing actual service code.

Lines 411-434 manually simulate the field extraction and mapping logic that should exist in the actual service. This approach has several drawbacks:

  1. False confidence: The test passes based on simulated logic, not actual service behavior.
  2. Maintenance burden: Service changes won't be caught unless test simulation is also updated.
  3. Code duplication: Mapping logic exists in both test and (presumably) service.

Consider either:

  • Option 1: Test the actual CreateDeployment service method with these inputs to verify real mapping behavior.
  • Option 2: Remove this test if the service has integration tests that cover field mapping.

Based on learnings from past review comment.

go/cmd/deploy/main.go (1)

220-255: Implementation correctly handles both deployment paths.

The separation between prebuilt image (lines 226-235) and build context upload (lines 237-255) is clear and logically sound. Error handling and UI messaging are appropriate for both flows.

Minor suggestion: Consider scoping deploymentID and err within each branch since they're not shared between the two paths. This would improve locality:

 	// Determine deployment source: prebuilt image or build from context
 	if opts.DockerImage != "" {
 		// Use prebuilt Docker image
 		ui.Print(MsgCreatingDeployment)
-		deploymentID, err = controlPlane.CreateDeployment(ctx, "", opts.DockerImage)
+		deploymentID, err := controlPlane.CreateDeployment(ctx, "", opts.DockerImage)
 		if err != nil {
 			ui.PrintError(MsgFailedToCreateDeployment)
 			ui.PrintErrorDetails(err.Error())
 			return err
 		}
 		ui.PrintSuccess(fmt.Sprintf("%s: %s", MsgDeploymentCreated, deploymentID))
 	} else {
 		// Build from context
 		ui.Print(MsgUploadingBuildContext)
-		buildContextPath, err := controlPlane.UploadBuildContext(ctx, opts.Context)
+		buildContextPath, err := controlPlane.UploadBuildContext(ctx, opts.Context)
 		if err != nil {
 			ui.PrintError(MsgFailedToUploadContext)
 			ui.PrintErrorDetails(err.Error())
 			return err
 		}
 		ui.PrintSuccess(fmt.Sprintf("%s: %s", MsgBuildContextUploaded, buildContextPath))
 
 		ui.Print(MsgCreatingDeployment)
-		deploymentID, err = controlPlane.CreateDeployment(ctx, buildContextPath, "")
+		deploymentID, err := controlPlane.CreateDeployment(ctx, buildContextPath, "")
 		if err != nil {
 			ui.PrintError(MsgFailedToCreateDeployment)
 			ui.PrintErrorDetails(err.Error())
 			return err
 		}
 		ui.PrintSuccess(fmt.Sprintf("%s: %s", MsgDeploymentCreated, deploymentID))
 	}
+
+	// Use deploymentID for polling

However, this requires updating line 273 to use the scoped variable. The current approach works fine.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68191a0 and ff12361.

⛔ Files ignored due to path filters (6)
  • go/gen/proto/ctrl/v1/build.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • go/gen/proto/ctrl/v1/deployment.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • go/gen/proto/hydra/v1/deployment.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • go/go.sum is excluded by !**/*.sum
  • internal/proto/generated/ctrl/v1/build_pb.ts is excluded by !**/generated/**
  • internal/proto/generated/ctrl/v1/deployment_pb.ts is excluded by !**/generated/**
📒 Files selected for processing (16)
  • deployment/docker-compose.yaml (3 hunks)
  • go/apps/ctrl/services/build/backend/depot/create_build.go (1 hunks)
  • go/apps/ctrl/services/build/backend/depot/generate_upload_url.go (1 hunks)
  • go/apps/ctrl/services/build/backend/docker/create_build.go (1 hunks)
  • go/apps/ctrl/services/build/backend/docker/generate_upload_url.go (1 hunks)
  • go/apps/ctrl/services/deployment/create_deployment.go (5 hunks)
  • go/apps/ctrl/services/deployment/create_deployment_simple_test.go (18 hunks)
  • go/apps/ctrl/services/deployment/service.go (3 hunks)
  • go/apps/ctrl/workflows/deploy/deploy_handler.go (3 hunks)
  • go/cmd/deploy/control_plane.go (5 hunks)
  • go/cmd/deploy/main.go (5 hunks)
  • go/go.mod (10 hunks)
  • go/pkg/db/querier_generated.go (2 hunks)
  • go/proto/ctrl/v1/build.proto (1 hunks)
  • go/proto/ctrl/v1/deployment.proto (1 hunks)
  • go/proto/hydra/v1/deployment.proto (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • go/pkg/db/querier_generated.go
  • go/proto/hydra/v1/deployment.proto
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-15T10:12:40.810Z
Learnt from: Flo4604
PR: unkeyed/unkey#4098
File: go/proto/ctrl/v1/deployment.proto:33-36
Timestamp: 2025-10-15T10:12:40.810Z
Learning: In the Unkey codebase proto files (ctrl/v1/build.proto, ctrl/v1/deployment.proto, hydra/v1/deployment.proto), use `dockerfile_path` (not `docker_file_path`) for consistency in generated Go field names.

Applied to files:

  • go/proto/ctrl/v1/deployment.proto
  • go/apps/ctrl/services/deployment/create_deployment.go
  • go/cmd/deploy/control_plane.go
📚 Learning: 2025-08-08T14:55:11.981Z
Learnt from: imeyer
PR: unkeyed/unkey#3755
File: deployment/docker-compose.yaml:179-184
Timestamp: 2025-08-08T14:55:11.981Z
Learning: In deployment/docker-compose.yaml (development only), MinIO is configured with API on 3902 and console on 3903; ports should map 3902:3902 and 3903:3903 to match MINIO_API_PORT_NUMBER and MINIO_CONSOLE_PORT_NUMBER.

Applied to files:

  • deployment/docker-compose.yaml
🧬 Code graph analysis (10)
go/apps/ctrl/services/deployment/service.go (2)
go/gen/proto/ctrl/v1/ctrlv1connect/build.connect.go (1)
  • BuildServiceClient (45-48)
go/apps/ctrl/workflows/deploy/service.go (2)
  • New (57-66)
  • Config (36-54)
go/apps/ctrl/services/build/backend/depot/create_build.go (3)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (11-23)
go/gen/proto/ctrl/v1/build.pb.go (6)
  • CreateBuildRequest (24-32)
  • CreateBuildRequest (45-45)
  • CreateBuildRequest (60-62)
  • CreateBuildResponse (92-99)
  • CreateBuildResponse (112-112)
  • CreateBuildResponse (127-129)
go/pkg/db/project_update_depot_id.sql_generated.go (1)
  • UpdateProjectDepotIDParams (21-25)
go/apps/ctrl/services/build/backend/depot/generate_upload_url.go (3)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (11-23)
internal/proto/generated/ctrl/v1/build_pb.ts (2)
  • GenerateUploadURLRequest (94-101)
  • GenerateUploadURLResponse (114-135)
go/gen/proto/ctrl/v1/build.pb.go (6)
  • GenerateUploadURLRequest (152-157)
  • GenerateUploadURLRequest (170-170)
  • GenerateUploadURLRequest (185-187)
  • GenerateUploadURLResponse (196-203)
  • GenerateUploadURLResponse (216-216)
  • GenerateUploadURLResponse (231-233)
go/apps/ctrl/workflows/deploy/deploy_handler.go (3)
go/pkg/db/models_generated.go (2)
  • DeploymentsStatusBuilding (197-197)
  • DeploymentsStatusDeploying (198-198)
go/pkg/db/deployment_step_insert.sql_generated.go (1)
  • InsertDeploymentStepParams (33-40)
go/gen/proto/ctrl/v1/build.pb.go (3)
  • CreateBuildRequest (24-32)
  • CreateBuildRequest (45-45)
  • CreateBuildRequest (60-62)
go/apps/ctrl/services/build/backend/docker/generate_upload_url.go (3)
go/apps/ctrl/services/build/backend/docker/service.go (1)
  • Docker (11-17)
internal/proto/generated/ctrl/v1/build_pb.ts (2)
  • GenerateUploadURLRequest (94-101)
  • GenerateUploadURLResponse (114-135)
go/gen/proto/ctrl/v1/build.pb.go (6)
  • GenerateUploadURLRequest (152-157)
  • GenerateUploadURLRequest (170-170)
  • GenerateUploadURLRequest (185-187)
  • GenerateUploadURLResponse (196-203)
  • GenerateUploadURLResponse (216-216)
  • GenerateUploadURLResponse (231-233)
go/apps/ctrl/services/deployment/create_deployment.go (2)
go/gen/proto/ctrl/v1/deployment.pb.go (7)
  • CreateDeploymentRequest_BuildContext (250-252)
  • CreateDeploymentRequest_BuildContext (258-258)
  • BuildContext (262-268)
  • BuildContext (281-281)
  • BuildContext (296-298)
  • CreateDeploymentRequest_DockerImage (254-256)
  • CreateDeploymentRequest_DockerImage (260-260)
go/gen/proto/hydra/v1/deployment.pb.go (3)
  • DeployRequest (25-35)
  • DeployRequest (48-48)
  • DeployRequest (63-65)
go/apps/ctrl/services/build/backend/docker/create_build.go (3)
go/apps/ctrl/services/build/backend/docker/service.go (1)
  • Docker (11-17)
internal/proto/generated/ctrl/v1/build_pb.ts (2)
  • CreateBuildRequest (21-47)
  • CreateBuildResponse (60-81)
go/gen/proto/ctrl/v1/build.pb.go (6)
  • CreateBuildRequest (24-32)
  • CreateBuildRequest (45-45)
  • CreateBuildRequest (60-62)
  • CreateBuildResponse (92-99)
  • CreateBuildResponse (112-112)
  • CreateBuildResponse (127-129)
go/cmd/deploy/main.go (1)
go/cmd/deploy/control_plane.go (1)
  • NewControlPlaneClient (48-58)
go/apps/ctrl/services/deployment/create_deployment_simple_test.go (2)
go/gen/proto/ctrl/v1/deployment.pb.go (11)
  • GitCommitInfo (314-323)
  • GitCommitInfo (336-336)
  • GitCommitInfo (351-353)
  • CreateDeploymentRequest (136-154)
  • CreateDeploymentRequest (167-167)
  • CreateDeploymentRequest (182-184)
  • CreateDeploymentRequest_BuildContext (250-252)
  • CreateDeploymentRequest_BuildContext (258-258)
  • BuildContext (262-268)
  • BuildContext (281-281)
  • BuildContext (296-298)
go/pkg/db/deployment_insert.sql_generated.go (1)
  • InsertDeploymentParams (51-67)
go/cmd/deploy/control_plane.go (5)
go/gen/proto/ctrl/v1/ctrlv1connect/build.connect.go (2)
  • BuildServiceClient (45-48)
  • NewBuildServiceClient (57-74)
go/cmd/deploy/main.go (1)
  • DeployOptions (70-86)
go/gen/proto/ctrl/v1/build.pb.go (3)
  • GenerateUploadURLRequest (152-157)
  • GenerateUploadURLRequest (170-170)
  • GenerateUploadURLRequest (185-187)
internal/proto/generated/ctrl/v1/deployment_pb.ts (5)
  • CreateDeploymentRequest (21-74)
  • GitCommitInfo (114-141)
  • BuildContext (87-101)
  • Deployment (215-339)
  • GetDeploymentRequest (179-184)
go/gen/proto/ctrl/v1/deployment.pb.go (19)
  • CreateDeploymentRequest (136-154)
  • CreateDeploymentRequest (167-167)
  • CreateDeploymentRequest (182-184)
  • GitCommitInfo (314-323)
  • GitCommitInfo (336-336)
  • GitCommitInfo (351-353)
  • CreateDeploymentRequest_BuildContext (250-252)
  • CreateDeploymentRequest_BuildContext (258-258)
  • BuildContext (262-268)
  • BuildContext (281-281)
  • BuildContext (296-298)
  • CreateDeploymentRequest_DockerImage (254-256)
  • CreateDeploymentRequest_DockerImage (260-260)
  • Deployment (530-564)
  • Deployment (577-577)
  • Deployment (592-594)
  • GetDeploymentRequest (442-447)
  • GetDeploymentRequest (460-460)
  • GetDeploymentRequest (475-477)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test Packages / Test
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Build / Build
🔇 Additional comments (16)
deployment/docker-compose.yaml (2)

168-176: MinIO port mapping is correct.

The addition of port 3903:3903 correctly maps the MinIO console port to match MINIO_CONSOLE_PORT_NUMBER (line 175). This aligns with development requirements for accessing the MinIO UI and API.

Based on learnings from PR #3755, this mapping ensures both the API (3902) and console (3903) are accessible for development.


289-291: Registry credentials are defined in krane but missing from ctrl.

The environment block in krane adds registry credentials (lines 289-291), but these same credentials are not added to the ctrl service (lines 342-384). Verify that this asymmetry is intentional based on the build/deployment architecture.

From the PR description, the flow appears to be: CLI uploads tar → ctrl creates build → krane executes build. If ctrl also needs to interact with the registry directly, these credentials should be added there as well.

Can you confirm whether ctrl requires registry credentials for its build coordination role, or if only krane (the executor) needs them?

go/go.mod (4)

1-63: Dependency changes align with build backend PR objectives.

The addition of depot-go, docker/cli, docker/docker, moby/buildkit, and related transitive dependencies appropriately support the new Docker and Depot build backends, presigned S3 flows, and BuildKit integration described in the PR. The dependency set is coherent with the architectural changes.


153-163: No compatibility issues found; review comment misidentifies separate intentional modules.

The dependencies are correctly versioned as separate Go modules designed to coexist: github.com/containerd/containerd/api v1.9.0 is the protocol buffer API definitions, while github.com/containerd/containerd/v2 v2.1.4 is the containerd v2 runtime. The containerd v2 runtime uses the api v1.x module for its public APIs, and most gRPC clients using the official API/protobufs continue to work. These are intentionally separate modules with no conflicting symbols. The go.mod configuration is correct and expected.

Likely an incorrect or invalid review comment.


8-10: Verify whether the September 15, 2025 buf-generated versions are officially released.

The web search found buf-generated versions from July 2, 2025, but go.mod contains newer versions from September 15, 2025 (v1.19.0 and v1.36.10). While the timestamp-based versioning pattern is consistent with buf codegen practices, confirm these September versions are officially released and not development snapshots, particularly given the version increments and newer dates.


160-160: No action needed—v1.0.0-rc.1 is the latest available release from maintainers.

The latest published release of containerd/platforms is v1.0.0-rc.1 (Jan 13, 2025), and there is no stable v1.0.0 release available. The go.mod dependency is already pinned to the current upstream release. The RC version is not an accidental or outdated choice—it's the latest release from the package maintainers. No stable upgrade path exists at this time.

go/apps/ctrl/services/build/backend/depot/generate_upload_url.go (1)

22-26: Validate/sanitize unkey_project_id for path safety.

If IDs can contain path separators or unusual chars, normalize or reject to avoid unintended S3 key hierarchies.

go/apps/ctrl/services/deployment/service.go (1)

19-23: Wiring looks good.

BuildService is plumbed through Config → Service; matches usage in workflows.

Also applies to: 32-38, 46-46

go/apps/ctrl/workflows/deploy/deploy_handler.go (1)

200-209: Go version 1.25 supports range 300 syntax—no changes required.

The codebase specifies go 1.25 in ./go/go.mod, which is well above the Go 1.22+ requirement for the range 300 loop syntax. The code is compatible and valid.

go/proto/ctrl/v1/deployment.proto (1)

43-46: Field name consistency looks good

dockerfile_path matches the convention across protos and generated Go (DockerfilePath).

Based on learnings

go/apps/ctrl/services/build/backend/depot/create_build.go (1)

131-142: Good: BuildKit client is closed

Nice cleanup; prevents leaks on early returns.

go/proto/ctrl/v1/build.proto (2)

6-9: Service surface looks solid

CreateBuild and GenerateUploadURL separation is clear and maps well to the new flow.


11-16: Field naming and comments are consistent

dockerfile_path is consistent; expires_in semantics are clear.

Also applies to: 24-32

go/apps/ctrl/services/deployment/create_deployment_simple_test.go (1)

1-238: Address the previous reviewer's concern about test file purpose.

Flo4604 previously asked "what does this file even test im pretty certain we can remove it". Looking at lines 27-179, these tests primarily verify:

  • Protobuf getter behavior (generated code)
  • sql.NullString handling (standard library)
  • Basic timestamp conversion

These tests don't exercise actual service logic, which raises the question of their value. The tests are not incorrect, but they may be testing library behavior rather than application-specific logic.

Could you clarify whether these tests provide sufficient value to maintain, or if they should be consolidated/removed as the previous reviewer suggested?

go/cmd/deploy/main.go (2)

40-44: LGTM!

The new message constants align well with the build context upload flow and follow the existing naming conventions.


313-315: LGTM!

The updated logic correctly displays either the Docker image or build context path based on the deployment source, aligning with the new build flow.

@ogzhanolguncu ogzhanolguncu requested a review from imeyer October 24, 2025 10:13
Copy link
Member

@Flo4604 Flo4604 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly nits but LGTM

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (9)
deployment/docker-compose.yaml (1)

371-383: Add explicit Depot access token and keep registry credentials separate.

Ctrl currently reuses registry password for Depot API. Add a dedicated env to split concerns:

   # Registry configuration (used by both Docker and Depot backends)
   UNKEY_REGISTRY_URL: "${UNKEY_REGISTRY_URL:-registry.depot.dev}"
   UNKEY_REGISTRY_USERNAME: "${UNKEY_REGISTRY_USERNAME:-x-token}"
   UNKEY_REGISTRY_PASSWORD: "${UNKEY_REGISTRY_PASSWORD:-${DEPOT_TOKEN:-}}"
   # Depot-specific configuration (only needed when UNKEY_BUILD_BACKEND=depot)
   UNKEY_DEPOT_API_URL: "https://api.depot.dev"
   UNKEY_DEPOT_PROJECT_REGION: "us-east-1"
+  UNKEY_DEPOT_ACCESS_TOKEN: "${UNKEY_DEPOT_ACCESS_TOKEN:-${DEPOT_TOKEN:-}}"

Then use UNKEY_DEPOT_ACCESS_TOKEN in the Depot backend for API auth.

go/apps/ctrl/services/build/backend/depot/create_build.go (2)

232-241: Use a dedicated Depot API access token, not the registry password.

Create/use DepotConfig.AccessToken and read it from UNKEY_DEPOT_ACCESS_TOKEN. Then:

- req.Header().Set("Authorization", "Bearer "+s.registryConfig.Password)
+ req.Header().Set("Authorization", "Bearer "+s.depotConfig.AccessToken)

This keeps builder and registry credentials independent.


223-229: Fix logging key/value mismatch and value type.

Use the string value and keep pairs aligned.

- "depot_project_id", project.DepotProjectID,
+ "depot_project_id", project.DepotProjectID.String,
  "unkey_project_id", unkeyProjectID,
QUICKSTART-DEPLOY.md (1)

12-15: Clarify env placement or point to env_file.

If possible, prefer a single env file (env_file in compose) over generating deployment/.env to reduce confusion, as discussed previously.

go/apps/ctrl/services/build/storage/s3.go (1)

33-107: Deduplicate S3 client setup shared with vault storage.

Extract a shared pkg (e.g., go/pkg/storage/s3) to centralize endpoint resolver, config loading, bucket ensure, and presigner init. Reduces drift.

go/apps/ctrl/services/build/backend/docker/create_build.go (1)

32-37: Validate deployment_id.

Avoid ambiguous image names and downstream surprises.

 if err := assert.All(
   assert.NotEmpty(req.Msg.BuildContextPath, "build_context_path is required"),
   assert.NotEmpty(req.Msg.UnkeyProjectId, "unkey_project_id is required"),
+  assert.NotEmpty(req.Msg.DeploymentId, "deployment_id is required"),
 ); err != nil {
go/apps/ctrl/run.go (3)

173-183: Comment is misleading: Depot BuildKit also requires external presign URL

Line 176's comment states "Empty for Depot, set for Docker", but Depot's BuildKit also needs a public presigned URL to fetch the build context from outside the Docker network. The storage layer defaults to the internal URL if ExternalURL is empty, but that will fail for Depot if BuildKit cannot reach the internal endpoint.


185-210: Compile-time type mismatch: handler assigned to client variable

buildService is declared as BuildServiceClient (line 185) but assigned handler implementations from docker.New and depot.New (lines 188, 197). Later, line 313 passes it to NewBuildServiceHandler (expecting a handler) and lines 221/319 pass it as BuildClient (expecting a client). This will not compile.

The previous review suggested splitting into distinct handler and client variables and using a thin adapter for in-process calls. See the detailed fix in the past review comments at lines 185-211.


313-321: Service wiring depends on handler/client split

Lines 313 and 319 both use buildService, but one context expects a handler (for HTTP serving) and the other expects a client (for in-process calls). Once the type mismatch at lines 185-210 is resolved by splitting into handler and client variables, update these lines to use the appropriate variable in each context.

🧹 Nitpick comments (12)
deployment/docker-compose.yaml (1)

289-292: Avoid inlining registry credentials in compose; prefer env_file or secrets.

Keep defaults empty and source sensitive values from an env file or Docker secrets to prevent accidental leaks. Example using env_file:

   environment:
-    UNKEY_REGISTRY_URL: "${UNKEY_REGISTRY_URL:-}"
-    UNKEY_REGISTRY_USERNAME: "${UNKEY_REGISTRY_USERNAME:-}"
-    UNKEY_REGISTRY_PASSWORD: "${UNKEY_REGISTRY_PASSWORD:-}"
+    UNKEY_REGISTRY_URL: "${UNKEY_REGISTRY_URL}"
+    UNKEY_REGISTRY_USERNAME: "${UNKEY_REGISTRY_USERNAME}"
+    UNKEY_REGISTRY_PASSWORD: "${UNKEY_REGISTRY_PASSWORD}"
+  env_file:
+    - ./deployment/.env
go/apps/ctrl/services/build/backend/depot/create_build.go (2)

101-105: Log platform alongside architecture when acquiring the machine.

Helps correlate build selection.

- s.logger.Info("Acquiring build machine",
-   "build_id", buildResp.ID,
-   "architecture", architecture,
-   "unkey_project_id", req.Msg.UnkeyProjectId)
+ s.logger.Info("Acquiring build machine",
+   "build_id", buildResp.ID,
+   "platform", platform,
+   "architecture", architecture,
+   "unkey_project_id", req.Msg.UnkeyProjectId)

150-157: Set FrontendAttrs["platform"] only when resolved (skip for “dynamic”).

Avoid constraining BuildKit when platform is unset/dynamic.

- solverOptions := client.SolveOpt{
+ attrs := map[string]string{
+   "context":  contextURL,
+   "filename": dockerfilePath,
+ }
+ if platform != "" && platform != "dynamic" {
+   attrs["platform"] = platform
+ }
+ solverOptions := client.SolveOpt{
    Frontend: "dockerfile.v0",
-   FrontendAttrs: map[string]string{
-     "platform": platform,
-     "context":  contextURL,
-     "filename": dockerfilePath,
-   },
+   FrontendAttrs: attrs,
QUICKSTART-DEPLOY.md (1)

166-176: Add languages to fenced code blocks (markdownlint).

Specify a language to satisfy MD040.

-```
+```text
 http://localhost:3000

- +text
http://localhost:3000/projects


-```
+```text
http://localhost:3000/deployments



Also applies to: 252-254

</blockquote></details>
<details>
<summary>go/apps/ctrl/services/build/storage/s3.go (1)</summary><blockquote>

`33-57`: **Accept a context in NewS3 (avoid context.Background) and timeouts.**

Let callers control cancellation/timeouts during AWS config and bucket creation.

```diff
-func NewS3(config S3Config) (*S3, error) {
+func NewS3(ctx context.Context, config S3Config) (*S3, error) {
 ...
- internalCfg, err := awsConfig.LoadDefaultConfig(context.Background(),
+ internalCfg, err := awsConfig.LoadDefaultConfig(ctx,
 ...
- _, err = internalClient.CreateBucket(context.Background(), &awsS3.CreateBucketInput{
+ _, err = internalClient.CreateBucket(ctx, &awsS3.CreateBucketInput{
go/apps/ctrl/services/build/backend/docker/service.go (1)

11-14: Avoid duplicating BuildPlatform across backends.

Move BuildPlatform to a shared package (e.g., build/types) and reuse from both docker and depot to keep parsing/validation consistent.

go/cmd/ctrl/main.go (3)

74-77: Default build-backend to “docker” for local-first experience.

Compose and docs use docker locally. Align the flag default.

- cli.String("build-backend", "Build backend to use: 'docker' for local, 'depot' for production. Default: depot",
-   cli.Default("depot"), cli.EnvVar("UNKEY_BUILD_BACKEND")),
+ cli.String("build-backend", "Build backend to use: 'docker' (local) or 'depot' (remote). Default: docker",
+   cli.Default("docker"), cli.EnvVar("UNKEY_BUILD_BACKEND")),

96-101: Add Depot access token flag.

Provide a dedicated flag/env for Depot API auth instead of reusing the registry password.

 // Depot Build Backend Configuration
 cli.String("depot-api-url", "Depot API endpoint URL",
   cli.EnvVar("UNKEY_DEPOT_API_URL")),
 cli.String("depot-project-region", "Build data will be stored in the chosen region ('us-east-1','eu-central-1')",
   cli.EnvVar("UNKEY_DEPOT_PROJECT_REGION"), cli.Default("us-east-1")),
+cli.String("depot-access-token", "Depot API access token (separate from registry creds)",
+  cli.EnvVar("UNKEY_DEPOT_ACCESS_TOKEN")),

Wire into config.Depot later.


187-192: Plumb depot-access-token into config.Depot.

Assuming DepotConfig gains AccessToken:

 Depot: ctrl.DepotConfig{
   APIUrl:        cmd.String("depot-api-url"),
   ProjectRegion: cmd.String("depot-project-region"),
+  AccessToken:   cmd.String("depot-access-token"),
 },
go/apps/ctrl/services/build/backend/docker/create_build.go (2)

72-77: Consider tagging deterministically to aid caching and debugging.

Optional: include a tag (e.g., buildID or short hash) instead of implicit :latest.

- imageName := strings.ToLower(fmt.Sprintf("%s-%s",
+ tag := uid.New(uid.BuildPrefix)
+ imageName := strings.ToLower(fmt.Sprintf("%s-%s:%s",
   req.Msg.UnkeyProjectId,
   req.Msg.DeploymentId,
+  tag,
 ))

90-96: Local-only image is fine if Krane shares the same Docker engine; otherwise add an optional push.

If Krane runs remotely (K8s), add a config toggle to push to a registry and return a pullable ref.

go/apps/ctrl/config.go (1)

148-151: Consider consolidating duplicated BuildPlatform type

The BuildPlatform struct is duplicated across config, depot, and docker packages with identical fields. Lines 191 and 201 of run.go use type conversions to map between these. Consider defining BuildPlatform once in a shared package (e.g., pkg/build or within the build/storage package) to reduce duplication and maintenance overhead.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff12361 and 863e684.

📒 Files selected for processing (11)
  • QUICKSTART-DEPLOY.md (4 hunks)
  • deployment/docker-compose.yaml (3 hunks)
  • go/apps/ctrl/config.go (5 hunks)
  • go/apps/ctrl/run.go (3 hunks)
  • go/apps/ctrl/services/build/backend/depot/create_build.go (1 hunks)
  • go/apps/ctrl/services/build/backend/depot/service.go (1 hunks)
  • go/apps/ctrl/services/build/backend/docker/create_build.go (1 hunks)
  • go/apps/ctrl/services/build/backend/docker/service.go (1 hunks)
  • go/apps/ctrl/services/build/storage/s3.go (1 hunks)
  • go/cmd/ctrl/main.go (3 hunks)
  • go/k8s/manifests/ctrl.yaml (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • go/k8s/manifests/ctrl.yaml
  • go/apps/ctrl/services/build/backend/depot/service.go
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-24T18:57:34.843Z
Learnt from: mcstepp
PR: unkeyed/unkey#4010
File: QUICKSTART-DEPLOY.md:17-17
Timestamp: 2025-09-24T18:57:34.843Z
Learning: In the Unkey deployment platform, API key environment variables use component-specific naming but share the same secret value: UNKEY_API_KEY for the ctrl service (validator), API_KEY for the CLI client, and CTRL_API_KEY for the dashboard client. The ctrl service acts as the source of truth for validation.

Applied to files:

  • deployment/docker-compose.yaml
📚 Learning: 2025-08-08T14:55:11.981Z
Learnt from: imeyer
PR: unkeyed/unkey#3755
File: deployment/docker-compose.yaml:179-184
Timestamp: 2025-08-08T14:55:11.981Z
Learning: In deployment/docker-compose.yaml (development only), MinIO is configured with API on 3902 and console on 3903; ports should map 3902:3902 and 3903:3903 to match MINIO_API_PORT_NUMBER and MINIO_CONSOLE_PORT_NUMBER.

Applied to files:

  • deployment/docker-compose.yaml
📚 Learning: 2025-09-15T18:12:01.503Z
Learnt from: mcstepp
PR: unkeyed/unkey#3952
File: go/apps/ctrl/run.go:212-213
Timestamp: 2025-09-15T18:12:01.503Z
Learning: In Go connect-go generated handlers, mux.Handle can accept New*ServiceHandler functions directly even though they return (string, http.Handler) tuples. The pattern mux.Handle(ctrlv1connect.New*ServiceHandler(...)) is valid and compiles successfully in unkey codebase.

Applied to files:

  • go/apps/ctrl/run.go
🧬 Code graph analysis (7)
go/apps/ctrl/services/build/backend/docker/service.go (3)
go/apps/ctrl/config.go (2)
  • BuildPlatform (148-151)
  • Config (71-146)
go/apps/ctrl/services/build/backend/depot/service.go (3)
  • BuildPlatform (11-14)
  • New (48-59)
  • Config (38-46)
go/apps/ctrl/services/build/storage/s3.go (1)
  • S3 (18-22)
go/apps/ctrl/run.go (7)
go/apps/ctrl/services/build/storage/s3.go (2)
  • NewS3 (33-107)
  • S3Config (24-31)
go/apps/ctrl/config.go (8)
  • S3Config (19-25)
  • BuildBackend (12-12)
  • BuildBackendDocker (16-16)
  • Config (71-146)
  • BuildPlatform (148-151)
  • BuildBackendDepot (15-15)
  • RegistryConfig (65-69)
  • DepotConfig (58-63)
go/gen/proto/ctrl/v1/ctrlv1connect/build.connect.go (1)
  • BuildServiceClient (45-48)
go/apps/ctrl/services/build/backend/depot/service.go (5)
  • New (48-59)
  • Config (38-46)
  • BuildPlatform (11-14)
  • RegistryConfig (21-25)
  • DepotConfig (16-19)
go/apps/ctrl/services/build/backend/docker/service.go (3)
  • New (33-42)
  • Config (25-31)
  • BuildPlatform (11-14)
go/apps/ctrl/services/deployment/service.go (2)
  • New (40-49)
  • Config (32-38)
go/apps/ctrl/workflows/deploy/service.go (2)
  • New (57-66)
  • Config (36-54)
go/apps/ctrl/services/build/backend/docker/create_build.go (3)
go/apps/ctrl/services/build/backend/docker/service.go (2)
  • Docker (16-23)
  • New (33-42)
go/gen/proto/ctrl/v1/build.pb.go (6)
  • CreateBuildRequest (24-32)
  • CreateBuildRequest (45-45)
  • CreateBuildRequest (60-62)
  • CreateBuildResponse (92-99)
  • CreateBuildResponse (112-112)
  • CreateBuildResponse (127-129)
go/pkg/uid/uid.go (1)
  • BuildPrefix (45-45)
go/apps/ctrl/services/build/backend/depot/create_build.go (3)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (27-36)
go/gen/proto/ctrl/v1/build.pb.go (6)
  • CreateBuildRequest (24-32)
  • CreateBuildRequest (45-45)
  • CreateBuildRequest (60-62)
  • CreateBuildResponse (92-99)
  • CreateBuildResponse (112-112)
  • CreateBuildResponse (127-129)
go/pkg/db/project_update_depot_id.sql_generated.go (1)
  • UpdateProjectDepotIDParams (21-25)
go/apps/ctrl/config.go (5)
go/apps/ctrl/services/build/storage/s3.go (1)
  • S3Config (24-31)
go/apps/ctrl/services/build/backend/depot/service.go (5)
  • BuildPlatform (11-14)
  • Depot (27-36)
  • DepotConfig (16-19)
  • Config (38-46)
  • RegistryConfig (21-25)
go/apps/ctrl/services/build/backend/docker/service.go (2)
  • BuildPlatform (11-14)
  • Config (25-31)
go/apps/krane/backend/docker/service.go (1)
  • Config (33-38)
go/apps/krane/config.go (1)
  • Config (24-103)
go/apps/ctrl/services/build/storage/s3.go (3)
go/apps/ctrl/config.go (1)
  • S3Config (19-25)
go/pkg/vault/storage/s3.go (2)
  • S3Config (25-31)
  • NewS3 (33-70)
go/pkg/fault/wrap.go (2)
  • Wrap (25-67)
  • Internal (75-89)
go/cmd/ctrl/main.go (3)
go/apps/ctrl/config.go (4)
  • BuildPlatform (148-151)
  • BuildBackend (12-12)
  • S3Config (19-25)
  • DepotConfig (58-63)
go/apps/ctrl/services/build/storage/s3.go (1)
  • S3Config (24-31)
go/pkg/vault/storage/s3.go (1)
  • S3Config (25-31)
🪛 Gitleaks (8.28.0)
QUICKSTART-DEPLOY.md

[high] 89-89: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 126-126: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 markdownlint-cli2 (0.18.1)
QUICKSTART-DEPLOY.md

166-166: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


174-174: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


196-196: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


202-202: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


252-252: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Docs
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Build / Build
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Test Packages / Test
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (5)
deployment/docker-compose.yaml (1)

170-170: MinIO console port mapping is correct.

3903:3903 matches MINIO_CONSOLE_PORT_NUMBER. Good catch. Based on learnings

go/cmd/ctrl/main.go (2)

78-86: The review comment is incorrect and contradicts the actual validation logic.

The review suggests marking build-s3-external-url as required "when --build-backend=depot", but the validation in go/apps/ctrl/config.go (lines 213-234) shows the opposite: ExternalURL is required only for the Docker backend, not Depot. The code comment at go/apps/ctrl/run.go:176 confirms this: "Empty for Depot, set for Docker". The validation is already correctly implemented—when BuildBackendDepot is used, ExternalURL is not validated; when BuildBackendDocker is used, it is required and validated. The help text suggestion would mislead users.

Likely an incorrect or invalid review comment.


216-219: Original review comment is incorrect and should be dismissed.

The codebase intentionally does NOT require BuildS3.ExternalURL for the Depot backend. The Validate() function (go/apps/ctrl/config.go:213-224) validates ExternalURL only for Docker backend (line 230), not Depot. This is by design, as documented in the code comment (go/apps/ctrl/run.go:176): "Empty for Depot, set for Docker."

Likely an incorrect or invalid review comment.

go/apps/ctrl/config.go (2)

154-192: LGTM: Build platform parsing and getters are well-structured

The parseBuildPlatform function properly validates the format and OS (lines 158-163), and the getter methods correctly rely on prior validation. The contract that GetBuildPlatform should only be called after Validate() is clearly documented.


202-237: LGTM: Validation logic is comprehensive and correct

The validation properly checks build platform format, registry config for Depot, and all required S3/Depot fields per backend. The assert.All usage and fmt.Errorf for the default case address previous review feedback.

Minor note: Lines 217 and 228 check NotEmpty(c.BuildPlatform, ...), which is redundant since parseBuildPlatform would already fail on empty strings (split "" → [""], length 1 ≠ 2). This is harmless and may improve error messages, so it's acceptable as-is.

Comment on lines +362 to +368
# Build configuration
UNKEY_BUILD_S3_URL: "${UNKEY_BUILD_S3_URL:-http://s3:3902}"
UNKEY_BUILD_S3_EXTERNAL_URL: "${UNKEY_BUILD_S3_EXTERNAL_URL:-}" # For CLI/external access
UNKEY_BUILD_S3_BUCKET: "build-contexts"
UNKEY_BUILD_S3_ACCESS_KEY_ID: "${UNKEY_BUILD_S3_ACCESS_KEY_ID:-minio_root_user}"
UNKEY_BUILD_S3_ACCESS_KEY_SECRET: "${UNKEY_BUILD_S3_ACCESS_KEY_SECRET:-minio_root_password}"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Depot builds need an externally reachable S3 URL.

When UNKEY_BUILD_BACKEND=depot, UNKEY_BUILD_S3_EXTERNAL_URL must be set so Depot/CLI can fetch the presigned URLs. Otherwise builds will fail at runtime. Suggest enforcing this in config.Validate() and documenting it here.

🤖 Prompt for AI Agents
In deployment/docker-compose.yaml around lines 362 to 368, the compose config
allows UNKEY_BUILD_BACKEND=depot without requiring an externally reachable
UNKEY_BUILD_S3_EXTERNAL_URL which causes Depot/CLI fetches to fail at runtime;
update the application config validation to assert that when UNKEY_BUILD_BACKEND
equals "depot" the UNKEY_BUILD_S3_EXTERNAL_URL env var is non-empty and return a
clear validation error if missing, and add a short comment in this
docker-compose section documenting that UNKEY_BUILD_S3_EXTERNAL_URL must be set
for depot builds with an example value.

Comment on lines +42 to +47
if err := assert.All(
assert.NotEmpty(req.Msg.BuildContextPath, "build_context_path is required"),
assert.NotEmpty(req.Msg.UnkeyProjectId, "unkey_project_id is required"),
); err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate deployment_id.

deployment_id is used in the image tag; enforce it’s present to avoid ambiguous tags.

  if err := assert.All(
    assert.NotEmpty(req.Msg.BuildContextPath, "build_context_path is required"),
    assert.NotEmpty(req.Msg.UnkeyProjectId, "unkey_project_id is required"),
+   assert.NotEmpty(req.Msg.DeploymentId, "deployment_id is required"),
  ); err != nil {
🤖 Prompt for AI Agents
In go/apps/ctrl/services/build/backend/depot/create_build.go around lines 42 to
47, the request validation is missing deployment_id which is required for image
tagging; add an assertion to validate req.Msg.DeploymentId is not empty (e.g.,
assert.NotEmpty(req.Msg.DeploymentId, "deployment_id is required")) alongside
the existing assertions so the function returns an InvalidArgument error if
deployment_id is missing.

Comment on lines +75 to +108
### Kubernetes: Additional Setup for Depot and S3

If you're deploying to Kubernetes, you must create secrets for Depot and S3 credentials:

**1. Create Depot credentials secret:**

```bash
kubectl create secret generic depot-credentials \
--from-literal=token=depot_org_YOUR_TOKEN_HERE \
--from-literal=s3-url=https://your-s3-endpoint.com \
--from-literal=s3-access-key-id=your_access_key \
--from-literal=s3-access-key-secret=your_secret_key \
--namespace=unkey
```

**2. Create Depot registry credentials secret:**

```bash
kubectl create secret docker-registry depot-registry \
--docker-server=registry.depot.dev \
--docker-username=x-token \
--docker-password=depot_org_YOUR_TOKEN_HERE \
--namespace=unkey
```

**Critical:** These secrets must exist before deploying the `ctrl` service to Kubernetes. The `ctrl` deployment references these secrets for:

- Building user code via Depot
- Storing build contexts in S3
- Pulling built images from Depot's registry

Without these secrets, deployments will fail with authentication errors.

## Step 2: Configure API Keys
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Call out build S3 external URL as mandatory for Depot.

Add a note that when using Depot, the presigned URLs must be reachable from Depot/CLI and UNKEY_BUILD_S3_EXTERNAL_URL must point to a public/host-reachable endpoint (e.g., http://127.0.0.1:3902 for MinIO in local).

🧰 Tools
🪛 Gitleaks (8.28.0)

[high] 89-89: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In QUICKSTART-DEPLOY.md around lines 75 to 108, add a mandatory note that when
using Depot the S3 presigned URLs must be reachable by Depot/CLI and therefore
UNKEY_BUILD_S3_EXTERNAL_URL must point to an externally reachable endpoint (not
just localhost inside a pod); instruct users to set UNKEY_BUILD_S3_EXTERNAL_URL
to a public or host-reachable URL (eg. http://127.0.0.1:3902 for local MinIO
with proper host network/port-forwarding or a public MinIO endpoint) and ensure
any ingress/firewall and S3 endpoint configuration allows Depot to fetch
presigned URLs so builds and image pulls succeed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (9)
go/apps/ctrl/services/build/backend/depot/generate_upload_url.go (1)

13-41: Consolidate duplicate GenerateUploadURL across backends.

This mirrors docker/backend’s implementation; extract a shared helper to eliminate drift.

go/apps/ctrl/services/build/backend/depot/create_build.go (2)

221-228: Fix log value: pass string, not sql.NullString; keep pairs aligned.

 	s.logger.Info(
 		"Returning existing depot project",
-		"depot_project_id", project.DepotProjectID,
+		"depot_project_id", project.DepotProjectID.String,
 		"unkey_project_id", unkeyProjectID,
 		"project_name", projectName,
 	)

42-47: Validate deployment_id (required for image tag uniqueness).

Missing guard can produce ambiguous tags and downstream failures.

 if err := assert.All(
 	assert.NotEmpty(req.Msg.BuildContextPath, "build_context_path is required"),
 	assert.NotEmpty(req.Msg.UnkeyProjectId, "unkey_project_id is required"),
+	assert.NotEmpty(req.Msg.DeploymentId, "deployment_id is required"),
 ); err != nil {
go/apps/ctrl/services/build/backend/docker/generate_upload_url.go (3)

27-41: Single‑source duration and align log key name.

-	uploadURL, err := s.storage.GenerateUploadURL(ctx, buildContextPath, 15*time.Minute)
+	exp := 15 * time.Minute
+	uploadURL, err := s.storage.GenerateUploadURL(ctx, buildContextPath, exp)
 	if err != nil {
-		s.logger.Error("Failed to generate presigned URL", "error", err, "context_key", buildContextPath)
+		s.logger.Error("Failed to generate presigned URL", "error", err, "build_context_path", buildContextPath)
 		return nil, connect.NewError(connect.CodeInternal,
 			fmt.Errorf("failed to generate presigned URL: %w", err))
 	}
 
-	s.logger.Info("Generated upload URL", "context_key", buildContextPath, "unkey_project_id", req.Msg.UnkeyProjectId)
+	s.logger.Info("Generated upload URL", "build_context_path", buildContextPath, "unkey_project_id", req.Msg.UnkeyProjectId)
 
 	return connect.NewResponse(&ctrlv1.GenerateUploadURLResponse{
 		UploadUrl:        uploadURL,
 		BuildContextPath: buildContextPath,
-		ExpiresIn:        900, // 15 minutes
+		ExpiresIn:        int64(exp.Seconds()), // 15 minutes
 	}), nil

22-26: Use deterministic UID instead of raw timestamp.

-	buildContextPath := fmt.Sprintf("%s/%d.tar.gz",
-		req.Msg.UnkeyProjectId,
-		time.Now().UnixNano())
+	buildContextPath := fmt.Sprintf("%s/%s.tar.gz",
+		req.Msg.UnkeyProjectId,
+		uid.New(uid.BuildPrefix))

Add import:

import "github.com/unkeyed/unkey/go/pkg/uid"

13-41: Deduplicate with depot backend helper.

Both backends implement identical GenerateUploadURL; extract to shared helper to avoid drift.

go/apps/ctrl/services/build/storage/s3.go (2)

45-56: Add retries and align with existing S3 setup (vault).

 	internalCfg, err := awsConfig.LoadDefaultConfig(context.Background(),
 		awsConfig.WithEndpointResolverWithOptions(internalResolver),
 		awsConfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
 			config.S3AccessKeyID,
 			config.S3AccessKeySecret,
 			"",
 		)),
-		awsConfig.WithRegion("auto"),
+		awsConfig.WithRegion("auto"),
+		awsConfig.WithRetryMode(aws.RetryModeStandard),
+		awsConfig.WithRetryMaxAttempts(3),
 	)

Apply same retry options to presignCfg below.


1-107: Consolidate S3 client init with go/pkg/vault/storage/s3.go.

There’s substantial duplication; move common constructor to a shared package and reuse.

go/apps/ctrl/services/build/backend/docker/create_build.go (1)

32-37: Validate deployment_id.

Required for unique, traceable image tags.

 if err := assert.All(
 	assert.NotEmpty(req.Msg.BuildContextPath, "build_context_path is required"),
 	assert.NotEmpty(req.Msg.UnkeyProjectId, "unkey_project_id is required"),
+	assert.NotEmpty(req.Msg.DeploymentId, "deployment_id is required"),
 ); err != nil {
🧹 Nitpick comments (8)
go/apps/ctrl/services/build/backend/depot/generate_upload_url.go (2)

27-41: Keep duration single‑sourced and align log key name.

Use one duration var to both presign and ExpiresIn; log “build_context_path” to match proto field.

-	// Generate presigned URL (15 minutes expiration)
-	uploadURL, err := s.storage.GenerateUploadURL(ctx, buildContextPath, 15*time.Minute)
+	// Generate presigned URL (15 minutes expiration)
+	exp := 15 * time.Minute
+	uploadURL, err := s.storage.GenerateUploadURL(ctx, buildContextPath, exp)
 	if err != nil {
-		s.logger.Error("Failed to generate presigned URL", "error", err, "context_key", buildContextPath)
+		s.logger.Error("Failed to generate presigned URL", "error", err, "build_context_path", buildContextPath)
 		return nil, connect.NewError(connect.CodeInternal,
 			fmt.Errorf("failed to generate presigned URL: %w", err))
 	}
 
-	s.logger.Info("Generated upload URL", "context_key", buildContextPath, "unkey_project_id", req.Msg.UnkeyProjectId)
+	s.logger.Info("Generated upload URL", "build_context_path", buildContextPath, "unkey_project_id", req.Msg.UnkeyProjectId)
 
 	return connect.NewResponse(&ctrlv1.GenerateUploadURLResponse{
 		UploadUrl:        uploadURL,
 		BuildContextPath: buildContextPath,
-		ExpiresIn:        900, // 15 minutes
+		ExpiresIn:        int64(exp.Seconds()), // 15 minutes
 	}), nil

22-26: Prefer collision‑resistant IDs over raw timestamps.

Swap UnixNano for a sortable UID to avoid rare collisions and improve traceability.

-	buildContextPath := fmt.Sprintf("%s/%d.tar.gz",
-		req.Msg.UnkeyProjectId,
-		time.Now().UnixNano())
+	buildContextPath := fmt.Sprintf("%s/%s.tar.gz",
+		req.Msg.UnkeyProjectId,
+		uid.New(uid.BuildPrefix))

Add import:

import "github.com/unkeyed/unkey/go/pkg/uid"
go/apps/ctrl/services/build/backend/depot/create_build.go (3)

150-156: Only set FrontendAttrs["platform"] when configured.

Supports “dynamic” platform or empty config without forcing BuildKit constraint.

-	FrontendAttrs: map[string]string{
-		"platform": platform,
-		"context":  contextURL,
-		"filename": dockerfilePath,
-	},
+	FrontendAttrs: func() map[string]string {
+		m := map[string]string{
+			"context":  contextURL,
+			"filename": dockerfilePath,
+		}
+		if platform != "" {
+			m["platform"] = platform
+		}
+		return m
+	}(),

101-105: Include platform in acquire log for clarity.

 	s.logger.Info("Acquiring build machine",
 		"build_id", buildResp.ID,
-		"architecture", architecture,
+		"platform", platform,
+		"architecture", architecture,
 		"unkey_project_id", req.Msg.UnkeyProjectId)

58-66: Consider longer/make configurable presign expiry.

15m may be tight under queueing; expose via config (e.g., BuildContextURLTTL) to reduce flaky fetches.

go/apps/ctrl/services/build/storage/s3.go (2)

61-66: Avoid blocking CreateBucket: add timeout context.

-	_, err = internalClient.CreateBucket(context.Background(), &awsS3.CreateBucketInput{
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+	_, err = internalClient.CreateBucket(ctx, &awsS3.CreateBucketInput{
 		Bucket: aws.String(config.S3Bucket),
 	})

76-79: Tone down noisy logs in prod.

“s3 storage initialized” is useful during setup; consider logger.Debug to reduce noise.

go/apps/ctrl/services/build/backend/docker/create_build.go (1)

90-96: Set platform only when provided (parity with Depot).

-	buildOptions := build.ImageBuildOptions{
+	buildOptions := build.ImageBuildOptions{
 		Tags:          []string{imageName},
 		Dockerfile:    dockerfilePath,
-		Platform:      platform,
+		Platform:      platform, // leave empty if not configured
 		Remove:        true,
 		RemoteContext: contextURL,
 	}

If platform can be empty/dynamic, preserve empty string to let Docker choose.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 863e684 and 3bec7a7.

📒 Files selected for processing (5)
  • go/apps/ctrl/services/build/backend/depot/create_build.go (1 hunks)
  • go/apps/ctrl/services/build/backend/depot/generate_upload_url.go (1 hunks)
  • go/apps/ctrl/services/build/backend/docker/create_build.go (1 hunks)
  • go/apps/ctrl/services/build/backend/docker/generate_upload_url.go (1 hunks)
  • go/apps/ctrl/services/build/storage/s3.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
go/apps/ctrl/services/build/backend/docker/generate_upload_url.go (2)
go/apps/ctrl/services/build/backend/docker/service.go (1)
  • Docker (16-23)
go/gen/proto/ctrl/v1/build.pb.go (6)
  • GenerateUploadURLRequest (152-157)
  • GenerateUploadURLRequest (170-170)
  • GenerateUploadURLRequest (185-187)
  • GenerateUploadURLResponse (196-203)
  • GenerateUploadURLResponse (216-216)
  • GenerateUploadURLResponse (231-233)
go/apps/ctrl/services/build/backend/depot/create_build.go (3)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (27-36)
go/gen/proto/ctrl/v1/build.pb.go (3)
  • CreateBuildRequest (24-32)
  • CreateBuildRequest (45-45)
  • CreateBuildRequest (60-62)
go/pkg/db/project_update_depot_id.sql_generated.go (1)
  • UpdateProjectDepotIDParams (21-25)
go/apps/ctrl/services/build/backend/depot/generate_upload_url.go (2)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (27-36)
go/gen/proto/ctrl/v1/build.pb.go (6)
  • GenerateUploadURLRequest (152-157)
  • GenerateUploadURLRequest (170-170)
  • GenerateUploadURLRequest (185-187)
  • GenerateUploadURLResponse (196-203)
  • GenerateUploadURLResponse (216-216)
  • GenerateUploadURLResponse (231-233)
go/apps/ctrl/services/build/backend/docker/create_build.go (3)
go/apps/ctrl/services/build/backend/docker/service.go (2)
  • Docker (16-23)
  • New (33-42)
go/gen/proto/ctrl/v1/build.pb.go (6)
  • CreateBuildRequest (24-32)
  • CreateBuildRequest (45-45)
  • CreateBuildRequest (60-62)
  • CreateBuildResponse (92-99)
  • CreateBuildResponse (112-112)
  • CreateBuildResponse (127-129)
go/pkg/uid/uid.go (1)
  • BuildPrefix (45-45)
go/apps/ctrl/services/build/storage/s3.go (3)
go/apps/ctrl/config.go (1)
  • S3Config (19-25)
go/pkg/vault/storage/s3.go (2)
  • S3Config (25-31)
  • NewS3 (33-70)
go/pkg/fault/wrap.go (2)
  • Wrap (25-67)
  • Internal (75-89)

Comment on lines +81 to +84
buildResp, err := build.NewBuild(ctx, &cliv1.CreateBuildRequest{
ProjectId: depotProjectID,
}, s.registryConfig.Password)
if err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Likely wrong credential passed to Depot SDK.

Using s.registryConfig.Password as the Depot auth token is risky/incorrect; use the Depot API token instead.

Please confirm which field in DepotConfig holds the API token (e.g., AccessToken/Token) and use it here.

Expected fix (adjust field name to match actual):

-	buildResp, err := build.NewBuild(ctx, &cliv1.CreateBuildRequest{
+	buildResp, err := build.NewBuild(ctx, &cliv1.CreateBuildRequest{
 		ProjectId: depotProjectID,
-	}, s.registryConfig.Password)
+	}, s.depotConfig.AccessToken)

🏁 Script executed:

#!/bin/bash
# Inspect DepotConfig fields to confirm token name/usage
rg -nP "type\s+DepotConfig\b|DepotConfig\s*\{" -n go/ --type go -C3
rg -nP "\bdepotConfig\.\w+" go/ --type go -C2

Length of output: 2506


🏁 Script executed:

#!/bin/bash
# Get full DepotConfig struct from config.go
rg -nP "type\s+DepotConfig\b" go/apps/ctrl/config.go -A 15

# Get full DepotConfig struct from service.go  
rg -nP "type\s+DepotConfig\b" go/apps/ctrl/services/build/backend/depot/service.go -A 10

# Inspect Service struct and its fields in service.go
rg -nP "type\s+Service\s+struct\b" go/apps/ctrl/services/build/backend/depot/service.go -A 15

# Get context around line 81-84 in create_build.go
sed -n '70,95p' go/apps/ctrl/services/build/backend/depot/create_build.go

Length of output: 1560


🏁 Script executed:

#!/bin/bash
# Find build.NewBuild() function signature
rg -nP "func\s+NewBuild\s*\(" go/ --type go -A 5

# Get full Service struct definition
rg -nP "type\s+Service\s+struct\b" go/apps/ctrl/services/build/backend/depot/service.go -A 20

# Check how Service is instantiated/initialized
rg -nP "func\s+New\s*\(" go/apps/ctrl/services/build/backend/depot/service.go -A 15

Length of output: 529


🏁 Script executed:

#!/bin/bash
# Check imports in create_build.go
head -30 go/apps/ctrl/services/build/backend/depot/create_build.go

# Search for Service or Depot struct more carefully
rg -nP "type\s+(Service|Depot)\s+struct" go/apps/ctrl/services/build/backend/depot/service.go -A 25

# Check if there's a depot SDK dependency and what version
rg -nP "github\.com.*depot|depot" go.mod --type=go-mod -C1

Length of output: 1647


🌐 Web query:

depot-go build.NewBuild() function signature API token authentication

💡 Result:

Short answer

  • Call: build.NewBuild(ctx, req, token) — where ctx is context.Context, req is a CreateBuildRequest (depot build CLI proto), and token is your API token. It returns a Build object and an error (e.g. build, err := build.NewBuild(ctx, req, token)). [1][2]

  • Auth: the token you pass is your organization/API token (Authorization: Bearer ) used to create the build. The CreateBuild response (the Build object) contains the build ID and a one‑time build token that you then use to acquire a machine / get BuildKit endpoints. [1][2]

Sources

  • Depot Go example / blog showing NewBuild usage. [1]
  • Depot API docs describing Authorization header, create-build behavior, and returned build token. [2]

🏁 Script executed:

#!/bin/bash
# Check where DepotConfig is initialized/populated to see if token field was missed
rg -nP "DepotConfig\s*\{" go/cmd/ctrl/main.go -A 5

# Search for any token/apikey related fields in depot package
rg -nP "Token|APIKey|AuthToken|APIToken" go/apps/ctrl/services/build/backend/depot/ --type go

Length of output: 332


Add missing Depot API token to DepotConfig and use it instead of registry password.

The code passes s.registryConfig.Password (Docker registry credential) to build.NewBuild(), but Depot SDK requires the Depot organization API token. DepotConfig is missing the auth token field entirely—it only contains APIUrl and ProjectRegion.

  1. Add an API token field to DepotConfig struct (in both go/apps/ctrl/config.go and go/apps/ctrl/services/build/backend/depot/service.go)
  2. Populate it during initialization in go/cmd/ctrl/main.go
  3. Pass s.depotConfig.<TokenFieldName> instead of s.registryConfig.Password at line 84
🤖 Prompt for AI Agents
In go/apps/ctrl/services/build/backend/depot/create_build.go around lines 81-84,
the call to build.NewBuild is passing s.registryConfig.Password (registry
credential) but the Depot SDK expects the Depot organization API token; add a
new token field (e.g. APIToken or OrgToken) to DepotConfig in
go/apps/ctrl/config.go and in
go/apps/ctrl/services/build/backend/depot/service.go, update all usages to
include the new field, populate that field during initialization in
go/cmd/ctrl/main.go (read from config/env where registry password was read), and
change the call at line 84 to pass s.depotConfig.<TokenFieldName> instead of
s.registryConfig.Password.

Comment on lines +232 to +241
httpClient := &http.Client{}
authInterceptor := connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
req.Header().Set("Authorization", "Bearer "+s.registryConfig.Password)
return next(ctx, req)
}
})

projectClient := corev1connect.NewProjectServiceClient(httpClient, s.depotConfig.APIUrl, connect.WithInterceptors(authInterceptor))
createResp, err := projectClient.CreateProject(ctx, connect.NewRequest(&corev1.CreateProjectRequest{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use Depot token in interceptor and set HTTP timeout.

Avoid leaking registry creds and add a client timeout.

-	httpClient := &http.Client{}
+	httpClient := &http.Client{Timeout: 15 * time.Second}
 	authInterceptor := connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
 		return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
-			req.Header().Set("Authorization", "Bearer "+s.registryConfig.Password)
+			req.Header().Set("Authorization", "Bearer "+s.depotConfig.AccessToken)
 			return next(ctx, req)
 		}
 	})
🤖 Prompt for AI Agents
In go/apps/ctrl/services/build/backend/depot/create_build.go around lines 232 to
241, the interceptor currently injects the registry password and the http.Client
has no timeout; change the interceptor to use the Depot token (e.g.,
s.depotConfig.Token) instead of s.registryConfig.Password to avoid leaking
registry credentials, and create the http.Client with a sensible Timeout (for
example http.Client{Timeout: 10 * time.Second}) before passing it to
NewProjectServiceClient so requests time out properly.

Comment on lines +72 to +77
// Docker requires lowercase repository names
imageName := strings.ToLower(fmt.Sprintf("%s-%s",
req.Msg.UnkeyProjectId,
req.Msg.DeploymentId,
))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Image not pushable/pullable: return a fully‑qualified ref or push the image.

The Docker path builds locally and returns a bare name; Krane in a remote cluster cannot pull it. Either:

  • Push to a configured registry and return registry/repo:tag, or
  • Explicitly scope this backend to local‑only deploys and set imagePullPolicy accordingly.

Right now this will break remote deploys.


I'll verify the review comment by investigating the codebase. Let me examine how the image name is used and whether it's actually pushed to a registry or consumed remotely.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find where imageName is used after being created
rg -n "imageName" go/apps/ctrl/services/build/backend/docker/ -A 5 -B 2

Length of output: 5374


🏁 Script executed:

#!/bin/bash
# Search for Docker registry/push operations in the build backend
rg -n "registry|Registry|push|Push" go/apps/ctrl/services/build/backend/ -C 2

Length of output: 4983


🏁 Script executed:

#!/bin/bash
# Search for Krane references to understand remote deployment
rg -n "krane|Krane" go/ -C 3

Length of output: 104670


🏁 Script executed:

#!/bin/bash
# Search for imagePullPolicy configuration
rg -n "imagePullPolicy|ImagePullPolicy" go/ -C 2

Length of output: 1870


🏁 Script executed:

#!/bin/bash
# Find create_build.go context - full function signature and return value
cat go/apps/ctrl/services/build/backend/docker/create_build.go | head -100

Length of output: 2892



---

🏁 Script executed:

```shell
#!/bin/bash
# Find how build backend is selected/routed
rg -n "docker|Docker|depot|Depot" go/apps/ctrl/services/build/ -C 1 | head -80

Length of output: 5813


🏁 Script executed:

#!/bin/bash
# Find build service initialization and backend selection
rg -n "NewBuild|newBuild|BuildClient" go/apps/ctrl/ -C 3

Length of output: 2720


🏁 Script executed:

#!/bin/bash
# Check how imageName is used after being returned from CreateBuild
rg -n "ImageName|image_name" go/apps/ctrl/workflows/deploy/ -B 2 -A 2

Length of output: 600



---

🏁 Script executed:

```shell
#!/bin/bash
# Find build service backend selection and initialization
rg -n "backend|Backend" go/apps/ctrl/run.go -B 2 -A 2

Length of output: 1082


🏁 Script executed:

#!/bin/bash
# Check if there's a build service factory that routes to docker or depot
rg -n "buildService|BuildService" go/apps/ctrl/run.go -B 3 -A 3

Length of output: 1564


🏁 Script executed:

#!/bin/bash
# See how Krane's CreateDeployment uses the image parameter
cat go/apps/krane/backend/kubernetes/create_deployment.go | grep -A 30 "GetImage\|image"

Length of output: 3530


🏁 Script executed:

#!/bin/bash
# Check Krane docker backend usage of image
cat go/apps/krane/backend/docker/create_deployment.go | grep -A 10 "GetImage\|Image:"

Length of output: 677


Bare image names cannot be pulled by remote Kubernetes clusters; use registry backend or scope Docker backend to local-only deployments.

The review comment is accurate. The Docker build backend returns bare image names (e.g., "projectid-deploymentid") that cannot be pulled by Kubernetes clusters. When this image is passed to Krane's Kubernetes backend for remote deployments, the cluster cannot resolve or fetch the image. The Kubernetes backend expects either fully-qualified registry references (like Depot's "registry.depot.dev/...") or images available locally. Bare names lack both a registry location and local availability on remote nodes.

The fix requires either: (1) configuring deployments to use the Depot build backend which returns registry-qualified names, or (2) restricting the Docker backend to local development and setting appropriate imagePullPolicy for remote scenarios.

🤖 Prompt for AI Agents
In go/apps/ctrl/services/build/backend/docker/create_build.go around lines
72-77, the Docker backend is producing bare image names (e.g.,
"projectid-deploymentid") which remote Kubernetes clusters cannot pull; update
the backend to either 1) produce fully-qualified registry image references
(prefix imageName with the configured registry hostname/namespace used by Depot
or the cluster, e.g., registry.example.com/<namespace>/<image>) when deployments
target remote Kubernetes, or 2) enforce local-only usage by detecting non-local
deployments and returning an error or a clear message instructing callers to use
the Depot/registry backend; additionally, if you keep local-only behavior ensure
callers set imagePullPolicy to Never/IfNotPresent for local dev flows so remote
clusters are not expected to pull bare names.

Comment on lines +110 to +114
scanner := bufio.NewScanner(buildResponse.Body)
var buildError error

for scanner.Scan() {
var resp dockerBuildResponse
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Scanner default 64K token limit can truncate/err on large lines.

Use json.Decoder or raise the scanner buffer to handle long build output.

-	scanner := bufio.NewScanner(buildResponse.Body)
+	scanner := bufio.NewScanner(buildResponse.Body)
+	// Increase from default 64K to 10MB to handle long JSON lines from docker build
+	buf := make([]byte, 0, 64*1024)
+	scanner.Buffer(buf, 10*1024*1024)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
scanner := bufio.NewScanner(buildResponse.Body)
var buildError error
for scanner.Scan() {
var resp dockerBuildResponse
scanner := bufio.NewScanner(buildResponse.Body)
// Increase from default 64K to 10MB to handle long JSON lines from docker build
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 10*1024*1024)
var buildError error
for scanner.Scan() {
var resp dockerBuildResponse
🤖 Prompt for AI Agents
In go/apps/ctrl/services/build/backend/docker/create_build.go around lines 110
to 114, the bufio.Scanner usage uses the default 64KB token limit which can
truncate or error on long Docker build output; replace the scanner loop with a
streaming json.Decoder that decodes each dockerBuildResponse from the response
body (decoder.Decode(&resp) in a loop) and handle EOF and decode errors, or if
you must keep bufio.Scanner call scanner.Buffer(make([]byte, 0, 256*1024),
10*1024*1024) before scanning to raise the token limit and preserve the same
loop; also ensure buildResponse.Body is closed and any decode/scan errors are
propagated to buildError.

@chronark chronark merged commit 38b1177 into main Oct 27, 2025
17 of 18 checks passed
@chronark chronark deleted the depot-poc branch October 27, 2025 11:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants