Skip to content

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

Copy link

changeset-bot bot commented Oct 14, 2025

⚠️ No Changeset found

Latest commit: b920b57

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

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 17, 2025 2:43pm
engineering Ignored Ignored Preview Oct 17, 2025 2:43pm

Copy link
Contributor

coderabbitai bot commented Oct 14, 2025

📝 Walkthrough

Walkthrough

Adds a pluggable S3-backed build system and BuildService (CreateBuild, GenerateUploadURL); switches deployments to a BuildContext/docker_image oneof with nested GitCommitInfo; implements Docker and Depot build backends, S3 presign storage, CLI upload flow, docs/scripts, and DB schema to track depot_project_id.

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.
Deployment orchestration & env
deployment/docker-compose.yaml, deployment/setup-build-backend.sh
MinIO port changed; new UNKEY_BUILD_* and UNKEY_DEPOT_* env vars; interactive script to choose backend and emit .env.
Control-plane config & wiring
go/apps/ctrl/config.go, go/apps/ctrl/run.go, go/cmd/ctrl/main.go
New BuildBackend type/constants; Config extends BuildBackend, BuildS3, Depot; CLI flags wired; runtime selects/wires Docker or Depot BuildService and exposes Build HTTP handler.
Build storage (S3)
go/apps/ctrl/services/build/storage/s3.go
New S3 storage with internal/external presign clients, bucket ensure-on-init, Get/Put presign APIs.
Build backend — Docker
go/apps/ctrl/services/build/backend/docker/*.go
New Docker backend: constructor, GenerateUploadURL, CreateBuild implementing remote-context Docker build with streamed JSON log parsing and error handling.
Build backend — Depot
go/apps/ctrl/services/build/backend/depot/*.go
New Depot backend: constructor, GenerateUploadURL, CreateBuild implementing Depot project ensure/create, BuildKit orchestration, registry auth, and build streaming.
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 BuildContext or docker_image (oneof); GitCommitInfo added; BuildService injected; workflow builds via CreateBuild when context_key provided before Krane deploy.
Krane adjustments
go/apps/krane/config.go, go/apps/krane/backend/docker/*.go, go/apps/krane/run.go
Added registry/depot-related config fields; docker backend accepts Config (socket + registry creds); ensureImageExists pull-if-missing behavior; imagePullSecrets added in K8s.
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 file.
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 now has oneof source (BuildContext or docker_image) and nested GitCommitInfo; Hydra DeployRequest gains context_key/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 models/queries; adds UpdateProjectDepotID SQL and generated methods; TypeScript schema updated.
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; minor formatting changes.
Dependencies & builds
go/go.mod
Added/updated dependencies (depot API, Docker/Moby, BuildKit, connectrpc bump, protobufs, containerd modules and many indirects).
Tests
go/apps/ctrl/services/deployment/create_deployment_simple_test.go
Tests updated to use nested GitCommitInfo and BuildContext accessors matching proto changes.

Sequence Diagram(s)

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

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

  rect rgba(230,245,255,0.6)
    note over WF: Build phase
    WF->>Ctrl: CreateBuild(context_key,dockerfile_path,unkey_project_id,deployment_id)
    Ctrl->>BE: CreateBuild(...)
    BE-->>Ctrl: {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-->>Ctrl: Final status
  Ctrl-->>CLI: Deployment result
Loading
sequenceDiagram
  autonumber
  participant Ctrl as Control Plane
  participant Depot as Depot API/Registry
  participant S3 as S3 Storage
  participant Docker as Local Docker

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

  Ctrl->>Depot: Ensure/get-or-create project
  Ctrl->>Depot: Create build
  Depot-->>Ctrl: Build ID / build stream
  Depot->>Ctrl: BuildKit session / build stream
  Ctrl->>Docker: Pull built image from registry
  Docker-->>Ctrl: Image available locally
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

Core Team

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "feat: Unkey Builds" is directly related to the main change in the pull request, which introduces build functionality to Unkey Deploy by adding two build backends (Docker and Depot). The title uses the conventional commit format and is concise and clear, accurately summarizing the primary purpose of the changeset without being vague or overly generic. While it could be slightly more specific about which backends are being added, it effectively communicates that this PR adds build capabilities to the platform.
Description Check ✅ Passed The PR description comprehensively addresses all required template sections. The "What does this PR do?" section provides detailed explanation of the two build backends, the execution flow, removal of the docker-image argument, and notes about the duplicated generate_build_url.go file and database schema changes. The "Type of change" section clearly marks "New feature" as applicable. The "How should this be tested?" section includes thorough testing instructions with specific commands and environment setup details. The "Checklist" section shows that most required items have been completed, indicating thorough preparation before submission. The description is well-structured, informative, and follows the repository's template guidelines.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ 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

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

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

📒 Files selected for processing (1)
  • go/apps/ctrl/services/build/backend/depot/create_build.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
go/apps/ctrl/services/build/backend/depot/create_build.go (3)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (11-21)
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)
⏰ 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 API / API Test Local
  • GitHub Check: Build / Build
  • GitHub Check: Test Packages / Test
  • GitHub Check: Test Go API Local / Test
🔇 Additional comments (4)
go/apps/ctrl/services/build/backend/depot/create_build.go (4)

1-30: LGTM: Clean package structure and auth transport.

The imports are well-organized and the authTransport wrapper provides a clean Bearer token injection mechanism for Depot API calls.


65-73: Verify presigned URL expiration is sufficient.

The 15-minute expiration for the build context presigned URL may be too short for large or complex builds. If a build takes longer than 15 minutes, Depot's BuildKit may fail to fetch the context.

Consider increasing the expiration to 30-60 minutes or making it configurable. Verify your typical build times and add buffer for network issues or slow builds.


125-210: LGTM: BuildKit integration and build execution are well-structured.

The BuildKit connection, auth provider setup, and build execution are correctly implemented. The defer for buildkitClient.Close() properly handles cleanup, and the comprehensive error handling with logging provides good observability.


276-281: LGTM: Clean Bearer token injection.

The RoundTrip implementation correctly adds the Authorization header for Depot API authentication.


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

github-actions bot commented Oct 15, 2025

Thank you for following the naming conventions for pull request titles! 🙏

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 (5)
go/cmd/deploy/control_plane.go (2)

178-214: The previously flagged empty string pointer issue remains unresolved.

Lines 184 and 189 still pass empty string pointers for optional fields (KeyspaceId and DockerFilePath), which prevents server-side defaults from being applied.

As noted in the past review, use a helper that returns nil for empty strings. The maintainer preference is to use pkg/ptr for this purpose rather than a custom helper.

Example using pkg/ptr:

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

// In CreateDeploymentRequest:
KeyspaceId:     ptr.String(c.opts.KeyspaceID),     // returns nil if empty
DockerFilePath: ptr.String(c.opts.Dockerfile),     // returns nil if empty

135-176: The previously flagged security and privacy issues remain unresolved.

The concerns raised in the earlier review are still present:

  1. Security: /tmp/ctrl with 0o777 and tar file with 0o666 create insecure permissions
  2. Privacy: No .dockerignore handling risks uploading secrets (.env, .git, etc.)
  3. Robustness: No check for tar command availability

These issues were acknowledged by the team and deferred to a subtask. Please ensure they are tracked and addressed before production use.

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

259-259: Logging key/value mismatch and incomplete values

The log pairs are misaligned and unkey_project_id value is missing; also pass the string value for depot_project_id.

Apply this diff:

-		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)

140-146: Missing validation for required Depot configuration fields.

When using the Depot backend, APIUrl, RegistryUrl, and Username are also required (not just AccessToken). Missing these values will cause runtime failures during build operations.

Apply this diff:

 	case BuildBackendDepot:
 		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.Depot.AccessToken, "Depot access token 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"),
 		)
go/apps/ctrl/services/build/backend/docker/create_build.go (1)

31-36: Missing validation for required DeploymentId field.

The DeploymentId is used in the image name construction (line 72), but it's not validated here. If empty, it would produce a malformed image name ending with a hyphen.

Apply this diff:

 	if err := assert.All(
 		assert.NotEmpty(req.Msg.ContextKey, "contextKey is required"),
 		assert.NotEmpty(req.Msg.UnkeyProjectId, "unkeyProjectID is required"),
+		assert.NotEmpty(req.Msg.DeploymentId, "deploymentId is required"),
 	); err != nil {
 		return nil, connect.NewError(connect.CodeInvalidArgument, err)
 	}
🧹 Nitpick comments (8)
go/apps/ctrl/services/build/backend/depot/create_build.go (3)

60-61: Consider making architecture configurable.

The architecture is hardcoded to amd64. While past discussions indicate this is acceptable for now, consider making this configurable via the DepotConfig to support multi-architecture deployments in the future.


273-273: Hardcoded region limits deployment flexibility.

The Depot project region is hardcoded to us-east-1. Consider making this configurable via DepotConfig to support deployments closer to users or to comply with data residency requirements.

Example configuration addition:

type DepotConfig struct {
	// ... existing fields ...
	Region string // e.g., "us-east-1", "eu-west-1"
}

146-147: Document fragile ordering dependency more clearly.

The comment indicates that the order of s.registryUrl and depotProjectID must never change, but this coupling is fragile and error-prone. Consider extracting this logic into a named function with validation to prevent future mistakes.

Example refactor:

// buildDepotImageName constructs the image name for Depot registry.
// The order of registry and projectID is required by Depot's API.
func buildDepotImageName(registryUrl, depotProjectID, unkeyProjectID, deploymentID string) string {
	return fmt.Sprintf("%s/%s:%s-%s", registryUrl, depotProjectID, unkeyProjectID, deploymentID)
}
go/apps/ctrl/services/build/backend/docker/create_build.go (2)

38-39: Architecture mismatch between backends.

The Docker backend uses amd64 while the Depot backend (create_build.go:60) also uses amd64. This is consistent, but consider extracting this as a shared constant to ensure both backends remain aligned.

Example:

// In a shared package or constants file:
const DefaultBuildArchitecture = "amd64"
const DefaultBuildPlatform = "linux/" + DefaultBuildArchitecture

111-112: Silently ignoring JSON parse errors.

If JSON unmarshaling fails, the error is silently ignored with continue. This could mask legitimate build output parsing issues. Consider logging the unmarshal error at debug level.

 		var resp dockerBuildResponse
 		if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
+			d.logger.Debug("Failed to parse build output line", "error", err)
 			continue
 		}
go/apps/ctrl/services/build/storage/s3.go (3)

36-46: Using deprecated AWS SDK v2 endpoint resolver.

The code uses EndpointResolverWithOptionsFunc which is deprecated in AWS SDK v2. While it works with the nolint directive, consider migrating to the newer BaseEndpoint resolver approach for long-term maintainability.

Based on learnings: AWS SDK v2 recommends using WithBaseEndpoint for custom endpoints:

cfg, err := awsConfig.LoadDefaultConfig(context.Background(),
	awsConfig.WithBaseEndpoint(config.S3URL),
	awsConfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
		config.S3AccessKeyID, config.S3AccessKeySecret, "")),
	awsConfig.WithRegion("auto"),
	awsConfig.WithRetryMode(aws.RetryModeStandard),
	awsConfig.WithRetryMaxAttempts(3),
)

48-48: "auto" region may not work with all S3-compatible services.

While "auto" region works with Cloudflare R2, not all S3-compatible services support this. Consider making the region configurable in S3Config for broader compatibility.


60-65: Bucket creation error handling could be more specific.

The code checks if the error string contains "BucketAlreadyOwnedByYou", but this is fragile string matching. Consider using AWS error type checking for more robust error handling.

import (
	"github.com/aws/smithy-go"
)

_, err = client.CreateBucket(context.Background(), &awsS3.CreateBucketInput{
	Bucket: aws.String(config.S3Bucket),
})
if err != nil {
	var ae smithy.APIError
	if errors.As(err, &ae) && ae.ErrorCode() == "BucketAlreadyOwnedByYou" {
		// Bucket already exists, continue
	} else {
		return nil, fmt.Errorf("failed to create bucket: %w", err)
	}
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b456c7 and 18ebfca.

⛔ Files ignored due to path filters (2)
  • 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 (7)
  • go/apps/ctrl/config.go (4 hunks)
  • go/apps/ctrl/services/build/backend/depot/create_build.go (1 hunks)
  • go/apps/ctrl/services/build/backend/docker/create_build.go (1 hunks)
  • go/apps/ctrl/services/build/storage/s3.go (1 hunks)
  • go/cmd/deploy/control_plane.go (4 hunks)
  • go/pkg/db/schema.sql (1 hunks)
  • internal/db/src/schema/projects.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • go/pkg/db/schema.sql
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-15T10:12:40.775Z
Learnt from: Flo4604
PR: unkeyed/unkey#4098
File: go/proto/ctrl/v1/deployment.proto:33-36
Timestamp: 2025-10-15T10:12:40.775Z
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/cmd/deploy/control_plane.go
🧬 Code graph analysis (5)
go/apps/ctrl/services/build/storage/s3.go (2)
go/apps/ctrl/config.go (1)
  • S3Config (18-23)
go/pkg/vault/storage/s3.go (2)
  • S3Config (25-31)
  • NewS3 (33-70)
go/apps/ctrl/services/build/backend/docker/create_build.go (2)
go/apps/ctrl/services/build/backend/docker/service.go (1)
  • Docker (11-17)
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/apps/ctrl/services/build/backend/depot/create_build.go (3)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (11-21)
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 (3)
go/apps/ctrl/services/build/storage/s3.go (1)
  • S3Config (24-30)
go/pkg/vault/storage/s3.go (1)
  • S3Config (25-31)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (11-21)
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 (80-95)
internal/proto/generated/ctrl/v1/build_pb.ts (1)
  • GenerateUploadURLRequest (94-101)
internal/proto/generated/ctrl/v1/deployment_pb.ts (3)
  • CreateDeploymentRequest (21-99)
  • Deployment (173-297)
  • GetDeploymentRequest (137-142)
go/gen/proto/ctrl/v1/deployment.pb.go (13)
  • CreateDeploymentRequest (136-157)
  • CreateDeploymentRequest (170-170)
  • CreateDeploymentRequest (185-187)
  • SourceType (87-87)
  • SourceType (119-121)
  • SourceType (123-125)
  • SourceType (132-134)
  • Deployment (413-447)
  • Deployment (460-460)
  • Deployment (475-477)
  • GetDeploymentRequest (325-330)
  • GetDeploymentRequest (343-343)
  • GetDeploymentRequest (358-360)
⏰ 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). (3)
  • GitHub Check: Build / Build
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Test Packages / Test
🔇 Additional comments (8)
go/cmd/deploy/control_plane.go (5)

40-57: LGTM! Clean dual-client architecture.

The separation of deployment and build clients improves modularity and follows single responsibility principle.


59-94: LGTM! Well-structured upload flow.

The method properly orchestrates presigned URL generation, tar creation, upload, and cleanup with appropriate error handling and validation.


96-133: LGTM! Robust presigned URL upload implementation.

Proper file handling, correct status code checks (200/204 for S3), reasonable 5-minute timeout, and thorough error handling.


216-234: LGTM! Clean migration to deployment ID.

The method correctly uses the new deploymentID parameter and deploymentClient.


236-288: LGTM! Improved logging formatting.

The multiline logging format enhances readability while maintaining the same polling logic.

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

11-16: LGTM!

The BuildBackend type and constants provide a clear abstraction for backend selection.


56-65: LGTM!

The DepotConfig struct is well-documented with clear field descriptions.


154-155: LGTM!

Good use of fmt.Errorf for clear error messaging when an unknown backend is provided.

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: 1

♻️ Duplicate comments (3)
go/cmd/deploy/control_plane.go (2)

135-176: Harden tar creation: secure temp, avoid world-w perms, check tar, respect ignores

  • Security: avoid 0777 dir and 0666 files under fixed /tmp/ctrl. Use os.TempDir(), 0700 dir, and keep CreateTemp’s 0600.
  • Robustness: guard with exec.LookPath("tar"); fail fast if missing.
  • Privacy/size: at minimum, exclude .git, node_modules, and .env; follow-up to parse .dockerignore.

Apply:

-	sharedDir := "/tmp/ctrl"
-	if err := os.MkdirAll(sharedDir, 0o777); err != nil {
+	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.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)
-	}
+	// Keep 0600 from CreateTemp
 
-	cmd := exec.Command("tar", "-czf", tarPath, "-C", absContextPath, ".")
+	if _, err := exec.LookPath("tar"); err != nil {
+		os.Remove(tarPath)
+		return "", fmt.Errorf("tar not found in PATH: %w", err)
+	}
+	cmd := exec.Command(
+		"tar",
+		"-czf", tarPath,
+		"--exclude=.git",
+		"--exclude=node_modules",
+		"--exclude=.env",
+		"-C", absContextPath,
+		".",
+	)

Follow-up: parse .dockerignore for full fidelity.


182-195: Don’t send empty optional strings; send nil when unset

Passing &c.opts.KeyspaceID / &c.opts.Dockerfile when empty marks fields as set. Use nil for empty to let server defaults apply.

Apply:

-	createReq := connect.NewRequest(&ctrlv1.CreateDeploymentRequest{
+	var keyspaceIDPtr *string
+	if c.opts.KeyspaceID != "" {
+		keyspaceIDPtr = &c.opts.KeyspaceID
+	}
+	var dockerfilePtr *string
+	if c.opts.Dockerfile != "" {
+		dockerfilePtr = &c.opts.Dockerfile
+	}
+	createReq := connect.NewRequest(&ctrlv1.CreateDeploymentRequest{
 		ProjectId:                c.opts.ProjectID,
-		KeyspaceId:               &c.opts.KeyspaceID,
+		KeyspaceId:               keyspaceIDPtr,
 		Branch:                   c.opts.Branch,
 		SourceType:               ctrlv1.SourceType_SOURCE_TYPE_CLI_UPLOAD,
 		EnvironmentSlug:          c.opts.Environment,
 		ContextKey:               contextKey,
-		DockerfilePath:           &c.opts.Dockerfile,
+		DockerfilePath:           dockerfilePtr,
 		GitCommitSha:             commitInfo.CommitSHA,
 		GitCommitMessage:         commitInfo.Message,
 		GitCommitAuthorHandle:    commitInfo.AuthorHandle,
 		GitCommitAuthorAvatarUrl: commitInfo.AuthorAvatarURL,
 		GitCommitTimestamp:       commitInfo.CommitTimestamp,
 	})
go/proto/ctrl/v1/deployment.proto (1)

33-47: Add docker_image to support image-only deployments (non-breaking)

We need a path to deploy pre-built images. Add an optional docker_image (new tag) and document XOR with context_key. Keep existing tags to avoid wire breaks.

Apply:

 message CreateDeploymentRequest {
@@
   // context_key must be provided
   string context_key = 6; // S3 key for uploaded build context
   optional string dockerfile_path = 7; // Path to Dockerfile within context (default: "Dockerfile")
@@
   // Keyspace ID for authentication
   optional string keyspace_id = 13;
+
+  // Optional: deploy a pre-built image; when set, server must ignore context_key/dockerfile_path
+  optional string docker_image = 14;
 }
🧹 Nitpick comments (2)
go/apps/ctrl/services/deployment/create_deployment.go (2)

114-135: Persist build source for traceability (context_key/dockerfile_path)

I don’t see these fields stored in the deployment record. Without them, debugging and audit are harder. Please persist them (columns or metadata JSON), or confirm they are recorded elsewhere.


141-147: Avoid logging full context_key (possible leakage)

If the S3 key is sensitive, log a hash or prefix instead of the full key.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 18ebfca and fa1177a.

⛔ Files ignored due to path filters (3)
  • go/gen/proto/ctrl/v1/deployment.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • 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 (3)
  • go/apps/ctrl/services/deployment/create_deployment.go (3 hunks)
  • go/cmd/deploy/control_plane.go (4 hunks)
  • go/proto/ctrl/v1/deployment.proto (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-15T10:12:40.775Z
Learnt from: Flo4604
PR: unkeyed/unkey#4098
File: go/proto/ctrl/v1/deployment.proto:33-36
Timestamp: 2025-10-15T10:12:40.775Z
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/cmd/deploy/control_plane.go
🧬 Code graph analysis (1)
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 (80-95)
internal/proto/generated/ctrl/v1/build_pb.ts (1)
  • GenerateUploadURLRequest (94-101)
go/pkg/git/git.go (1)
  • GetInfo (43-100)
go/gen/proto/ctrl/v1/deployment.pb.go (10)
  • CreateDeploymentRequest (136-157)
  • CreateDeploymentRequest (170-170)
  • CreateDeploymentRequest (185-187)
  • SourceType (87-87)
  • SourceType (119-121)
  • SourceType (123-125)
  • SourceType (132-134)
  • Deployment (413-447)
  • Deployment (460-460)
  • Deployment (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). (3)
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Test Packages / Test
  • GitHub Check: Build / Build
🔇 Additional comments (2)
go/proto/ctrl/v1/deployment.proto (1)

33-36: Field naming LGTM (dockerfile_path) and context_key addition

Consistent with other protos. Good.

Based on learnings

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

97-133: Confirm presigned URL header expectations

Some S3 presigns fail if unexpected Content-Type is sent. Verify server signs without Content-Type constraints, or align the header value (e.g., application/octet-stream).

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

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/workflows/deploy/deploy_handler.go (1)

203-209: Syntax error: invalid for-range over integer literal

Use a counted loop.

- for i range 300 {
+ for i := 0; i < 300; i++ {
♻️ Duplicate comments (3)
go/apps/ctrl/services/build/backend/depot/create_build.go (1)

257-261: Fix misaligned log key/value pairs and value types [duplicate of prior review]

Pass proper values and use the string of DepotProjectID.

- 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/cmd/deploy/control_plane.go (2)

136-177: Harden temp handling; avoid permissive perms; check tar; respect ignores

World-writable /tmp/ctrl (0777) and 0666 files are unsafe, and relying on external tar without .dockerignore filtering risks secret leakage and portability issues.

Apply minimal hardening:

-	sharedDir := "/tmp/ctrl"
-	if err := os.MkdirAll(sharedDir, 0o777); err != nil {
+	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")
+	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)
-	}
+	// Keep 0600 from CreateTemp; no chmod needed
 
+	if _, err := exec.LookPath("tar"); err != nil {
+		os.Remove(tarPath)
+		return "", fmt.Errorf("tar not found in PATH: %w", err)
+	}
 	cmd := exec.Command("tar", "-czf", tarPath, "-C", absContextPath, ".")

Follow-ups:

  • Respect .dockerignore/.gitignore during archiving to avoid uploading secrets and bloat. We can help wire an exclude list or implement Go-based tar with filtering. (As discussed earlier)

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

Empty string pointers mark fields as “set” in protobuf oneofs/optionals. Only set KeyspaceId when non-empty to allow server defaults.

Apply:

 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)
+}

Also applies to: 203-217

🧹 Nitpick comments (8)
go/proto/hydra/v1/deployment.proto (1)

18-21: Enforce XOR with oneof; align with ctrl proto

Use oneof for context_key vs docker_image to encode the contract in the schema.

Apply:

-  // Build source fields, exactly one of (context_key, docker_image) must be set
-  optional string context_key = 3;
-  optional string dockerfile_path = 4;
-  optional string docker_image = 5;
+  // Build source: exactly one must be set
+  oneof source {
+    string context_key = 3;
+    string docker_image = 5;
+  }
+  optional string dockerfile_path = 4;

As per coding guidelines and learnings

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

182-188: Propagate defaulted dockerfile_path to Hydra request

You resolve a default dockerfilePath but don’t set it on deployReq if unset in the incoming message.

 case *ctrlv1.CreateDeploymentRequest_BuildContext:
-  deployReq.ContextKey = proto.String(source.BuildContext.GetContextKey())
-  deployReq.DockerfilePath = source.BuildContext.DockerfilePath
+  deployReq.ContextKey = proto.String(contextKey)
+  // Always pass the resolved path (defaults to ./Dockerfile if empty)
+  deployReq.DockerfilePath = proto.String(dockerfilePath)

Also applies to: 105-112

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

192-200: Don’t spam stdout with build status

Streaming raw JSON to os.Stdout is noisy; use logger at debug level or buffer and print on error.

- buildStatusCh := make(chan *client.SolveStatus, 10)
- go func() {
-   enc := json.NewEncoder(os.Stdout)
-   enc.SetIndent("", "  ")
-   for status := range buildStatusCh {
-     _ = enc.Encode(status)
-   }
- }()
+ buildStatusCh := make(chan *client.SolveStatus, 10)
+ // Optionally: forward summarized events to logger at debug level
+ // or drain silently unless there is an error.

Refer to prior discussion in review thread

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

217-221: Log deployment status value, not the whole message

Use the enum/status field.

- w.logger.Info("deployment status",
-   "deployment_id", deployment.ID,
-   "status", resp.Msg,
- )
+ w.logger.Info("deployment status",
+   "deployment_id", deployment.ID,
+   "status", resp.Msg.GetStatus(),
+ )

276-305: Add HTTP timeout for OpenAPI scraping (optional)

Avoid indefinite hangs on unresponsive instances.

- resp, err := http.DefaultClient.Get(openapiURL)
+ httpClient := &http.Client{Timeout: 5 * time.Second}
+ resp, err := httpClient.Get(openapiURL)
go/apps/ctrl/services/deployment/create_deployment_simple_test.go (1)

14-24: Avoid duplicating service validation logic in tests

validateTimestamp mirrors service behavior and can drift. Prefer importing a shared validator or exposing a small internal helper for tests.

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

268-269: Make build polling timeout configurable and increase default. Fixed 5 minutes may time out remote backend builds; introduce a CLI/config flag and set the default to 20–30 minutes.

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

69-86: Remove unused DeployOptions fields: Registry, SkipPush, Linux

These fields are populated from CLI flags but never read anywhere in the codebase. They are dead code from the previous client-side build flow and should be removed or explicitly marked as deprecated to avoid confusion.

Lines 78, 80, 85 (field definitions) and 187, 189, 194 (flag assignments) can be safely removed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa1177a and dd6b3c6.

⛔ Files ignored due to path filters (3)
  • 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/**
  • internal/proto/generated/ctrl/v1/deployment_pb.ts is excluded by !**/generated/**
📒 Files selected for processing (8)
  • go/apps/ctrl/services/build/backend/depot/create_build.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/workflows/deploy/deploy_handler.go (3 hunks)
  • go/cmd/deploy/control_plane.go (5 hunks)
  • go/cmd/deploy/main.go (5 hunks)
  • go/proto/ctrl/v1/deployment.proto (1 hunks)
  • go/proto/hydra/v1/deployment.proto (1 hunks)
🧰 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/hydra/v1/deployment.proto
  • go/apps/ctrl/workflows/deploy/deploy_handler.go
  • go/apps/ctrl/services/deployment/create_deployment.go
  • go/proto/ctrl/v1/deployment.proto
  • go/cmd/deploy/control_plane.go
📚 Learning: 2025-09-01T15:10:44.959Z
Learnt from: imeyer
PR: unkeyed/unkey#3899
File: go/proto/metald/v1/deployment.proto:7-15
Timestamp: 2025-09-01T15:10:44.959Z
Learning: In the unkey/unkey repository, the metald service receives deployment_id values from upstream services rather than generating them internally. The deployment_id field in DeploymentRequest is part of a service-to-service communication pattern.

Applied to files:

  • go/proto/hydra/v1/deployment.proto
🧬 Code graph analysis (6)
go/apps/ctrl/services/build/backend/depot/create_build.go (3)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (11-21)
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/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/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/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)
go/pkg/git/git.go (1)
  • GetInfo (43-100)
go/gen/proto/ctrl/v1/deployment.pb.go (16)
  • 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)
⏰ 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). (3)
  • GitHub Check: Build / Build
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Test Packages / Test
🔇 Additional comments (6)
go/apps/ctrl/services/deployment/create_deployment.go (1)

170-180: Verify KeyAuth vs Keyspace mapping

Hydra expects KeyAuthId but ctrl request carries keyspace_id. Confirm these IDs are the same domain object; otherwise, add a distinct field or translation.

As per coding guidelines

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

30-46: Schema surface looks consistent (oneof + dockerfile_path)

The oneof source and dockerfile_path naming align with the rest of the system.

Based on learnings

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

40-45: New step messages look good

Clear, consistent with the new context-key flow.


220-255: Context-upload + createDeployment flow LGTM

Good separation of prebuilt-image vs context build; clear UX messages and error surfacing.


100-101: Clarify product intent: docker-image flag contradicts PR summary

The docker-image flag and prebuilt image support are fully implemented and active in the codebase, contradicting the PR summary stating they were removed. The flag is:

  • Exposed in CLI (go/cmd/deploy/main.go:100)
  • Integrated end-to-end through control plane → service → workflow layers
  • A first-class oneof source alongside build_context at proto level
  • Documented with example usage (go/cmd/deploy/main.go:143)

Confirm whether this PR intends to:

  • Keep prebuilt image support (expected given the code), and update the PR summary, or
  • Remove it, in which case all these references require cleanup.
go/apps/ctrl/services/deployment/create_deployment_simple_test.go (1)

265-487: Field mapping assertions are thorough

Covers populated/empty/mixed Git fields and timestamp validity against db insert params. Good guardrails.

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: 0

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

13-42: Duplication acknowledged in previous review.

This implementation duplicates depot/generate_upload_url.go as noted in previous comments. The team has agreed to consolidate this in a follow-up.

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

173-223: Type mismatch already flagged in previous review.

The past review correctly identified that buildService is declared as BuildServiceClient but assigned handler implementations. The suggested fix using an adapter or separate handler/client variables remains valid.


328-336: Client/handler confusion continues from line 173.

This follows from the type mismatch at line 173. Once that's resolved with separate handler and client variables, pass the handler to NewBuildServiceHandler and the client to deployment.New.

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

68-73: Image name normalization risk acknowledged.

Lowercasing the concatenated IDs could cause collisions if projects differ only by case. This was noted in previous reviews as a potential issue for case-sensitive uniqueness.


31-36: Add DeploymentId validation.

DeploymentId is required for generating the image name (line 72) but is not validated. Add it to the validation checks to fail fast on missing input.

Apply this diff:

 	if err := assert.All(
 		assert.NotEmpty(req.Msg.ContextKey, "contextKey is required"),
 		assert.NotEmpty(req.Msg.UnkeyProjectId, "unkeyProjectID is required"),
+		assert.NotEmpty(req.Msg.DeploymentId, "deploymentId is required"),
 	); err != nil {
 		return nil, connect.NewError(connect.CodeInvalidArgument, err)
 	}
go/apps/ctrl/services/build/backend/depot/create_build.go (3)

59-61: Architecture should default to amd64.

Hardcoding arm64 will break common x86_64 environments. As noted in previous reviews, default to amd64 or make this configurable.


247-250: Fix logging key/value alignment.

The log statement has misaligned key/value pairs and incomplete values, as flagged in previous reviews.

Apply this diff:

-		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,
+		)

313-324: Use standard Docker registry auth types.

As noted in previous reviews, use the engine's registry.AuthConfig and StdEncoding instead of CLI types and URL encoding.

Apply this diff:

+	// import "github.com/docker/docker/api/types/registry"
-	authConfig := types.AuthConfig{
+	authConfig := registry.AuthConfig{
 		Username:      s.username,
 		Password:      s.accessToken,
 		ServerAddress: s.registryUrl,
 	}
 
 	// Encode auth to base64 JSON
 	authJSON, err := json.Marshal(authConfig)
 	if err != nil {
 		return fmt.Errorf("failed to marshal auth: %w", err)
 	}
-	encodedAuth := base64.URLEncoding.EncodeToString(authJSON)
+	encodedAuth := base64.StdEncoding.EncodeToString(authJSON)
go/apps/ctrl/services/build/storage/s3.go (1)

1-180: Code duplication with vault storage acknowledged.

Previous reviews identified significant overlap with go/pkg/vault/storage/s3.go. The team has agreed to extract shared logic into a common package in a follow-up.

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

139-147: Missing validation for Depot configuration fields.

The Depot backend requires APIUrl, RegistryUrl, and Username (as seen in depot/service.go), but the validation only checks AccessToken. Add validation for these required fields to prevent runtime errors.

Apply this diff:

 	case BuildBackendDepot:
 		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 username is required when using Depot backend"),
+			assert.NotEmpty(c.Depot.AccessToken, "Depot access token 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"),
 		)
🧹 Nitpick comments (1)
QUICKSTART-DEPLOY.md (1)

1-278: Documentation is comprehensive and well-structured.

The quickstart guide clearly explains the new build backend flow, configuration options, and troubleshooting steps. The Depot and Docker backend options are well-documented with concrete examples.

Optional: Consider adding language specifiers to fenced code blocks (lines 133, 141, 219) for better syntax highlighting, though this is a minor documentation style preference.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd6b3c6 and caa937f.

📒 Files selected for processing (9)
  • QUICKSTART-DEPLOY.md (4 hunks)
  • deployment/docker-compose.yaml (2 hunks)
  • go/apps/ctrl/config.go (4 hunks)
  • go/apps/ctrl/run.go (4 hunks)
  • go/apps/ctrl/services/build/backend/depot/create_build.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)
  • go/cmd/ctrl/main.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • go/cmd/ctrl/main.go
🧰 Additional context used
🧠 Learnings (2)
📚 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 (6)
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/config.go (3)
go/apps/ctrl/services/build/storage/s3.go (1)
  • S3Config (25-32)
go/pkg/vault/storage/s3.go (1)
  • S3Config (25-31)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (11-21)
go/apps/ctrl/services/build/backend/docker/create_build.go (2)
go/apps/ctrl/services/build/backend/docker/service.go (1)
  • Docker (11-17)
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/apps/ctrl/services/build/backend/depot/create_build.go (4)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (11-21)
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/pkg/db/types/null_string.go (1)
  • NullString (10-10)
go/apps/ctrl/run.go (7)
go/gen/proto/ctrl/v1/ctrlv1connect/build.connect.go (2)
  • BuildServiceClient (45-48)
  • NewBuildServiceHandler (103-127)
go/apps/ctrl/config.go (5)
  • BuildBackend (11-11)
  • BuildBackendDocker (15-15)
  • S3Config (18-24)
  • Config (68-129)
  • BuildBackendDepot (14-14)
go/apps/ctrl/services/build/storage/s3.go (2)
  • NewS3 (34-111)
  • S3Config (25-32)
go/apps/ctrl/services/build/backend/docker/service.go (2)
  • New (26-34)
  • Config (19-24)
go/apps/ctrl/services/build/backend/depot/service.go (3)
  • New (34-46)
  • Config (23-32)
  • Depot (11-21)
go/apps/ctrl/services/deployment/service.go (2)
  • New (27-36)
  • Config (19-25)
go/apps/ctrl/workflows/deploy/service.go (2)
  • New (57-66)
  • Config (36-54)
go/apps/ctrl/services/build/storage/s3.go (3)
go/apps/ctrl/config.go (1)
  • S3Config (18-24)
go/pkg/vault/storage/s3.go (2)
  • S3Config (25-31)
  • NewS3 (33-70)
go/pkg/fault/wrap.go (3)
  • Wrap (25-67)
  • Internal (75-89)
  • Public (97-111)
🪛 Gitleaks (8.28.0)
QUICKSTART-DEPLOY.md

[high] 212-213: Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.

(curl-auth-header)

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

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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


163-163: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


169-169: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


219-219: 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). (3)
  • GitHub Check: Build / Build
  • GitHub Check: Test Packages / Test
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (1)
deployment/docker-compose.yaml (1)

170-170: LGTM! Port mapping corrected.

The port mapping now correctly aligns with MINIO_CONSOLE_PORT_NUMBER.

Based on learnings.

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

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

13-42: Duplication acknowledged; consider collision-resistant key generation.

The duplication with depot/generate_upload_url.go is already tracked for consolidation. Additionally, using time.Now().UnixNano() for uniqueness could theoretically have collisions if multiple requests arrive in rapid succession (though unlikely in practice).

As mentioned in past reviews, consolidate the shared logic into a common helper. For additional robustness, consider using a UUID or combining timestamp with a random component:

+ "crypto/rand"
+ "encoding/hex"

  // Generate unique S3 key for this build context
+ randomBytes := make([]byte, 8)
+ rand.Read(randomBytes)
  contextKey := fmt.Sprintf("%s/%d-%s.tar.gz",
    req.Msg.UnkeyProjectId,
    time.Now().UnixNano(),
+   hex.EncodeToString(randomBytes))
-   time.Now().UnixNano())

This eliminates theoretical collision risk while maintaining chronological ordering.

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

31-36: Add validation for DeploymentId.

The validation checks ContextKey and UnkeyProjectId but omits DeploymentId, which is used in the image name construction (line 70-73). Missing this validation could lead to ambiguous image names or runtime errors.

Apply this diff to add the validation:

  if err := assert.All(
    assert.NotEmpty(req.Msg.ContextKey, "contextKey is required"),
    assert.NotEmpty(req.Msg.UnkeyProjectId, "unkeyProjectID is required"),
+   assert.NotEmpty(req.Msg.DeploymentId, "deploymentId is required"),
  ); err != nil {
    return nil, connect.NewError(connect.CodeInvalidArgument, err)
  }

68-73: Image name normalization could cause collisions.

Lowercasing the concatenated image name can cause unintended collisions if UnkeyProjectId and DeploymentId differ only in case.

Consider one of these approaches:

  1. Validate that inputs are already lowercase and reject mixed-case identifiers
  2. Include a deterministic hash component to preserve uniqueness

Example validation approach:

+ if strings.ToLower(req.Msg.UnkeyProjectId) != req.Msg.UnkeyProjectId {
+   return nil, connect.NewError(connect.CodeInvalidArgument,
+     fmt.Errorf("unkeyProjectId must be lowercase"))
+ }
+ if strings.ToLower(req.Msg.DeploymentId) != req.Msg.DeploymentId {
+   return nil, connect.NewError(connect.CodeInvalidArgument,
+     fmt.Errorf("deploymentId must be lowercase"))
+ }
  imageName := strings.ToLower(fmt.Sprintf("%s-%s",
    req.Msg.UnkeyProjectId,
    req.Msg.DeploymentId,
  ))
go/apps/krane/config.go (1)

62-77: Add validation for depot and registry configuration.

The new fields (DepotToken, RegistryURL, RegistryUsername, RegistryPassword) lack validation in the Validate() method. If these fields are required for specific backend configurations, their absence could cause runtime failures.

Based on coding guidelines, add validation to ensure required fields are present when depot backend is selected:

  func (c Config) Validate() error {
    if c.Backend == Docker && c.DockerSocketPath == "" {
      return fmt.Errorf("--docker-socket is required when backend is docker")
    }
+   
+   // Validate registry credentials are complete if any are provided
+   registryFieldsSet := 0
+   if c.RegistryURL != "" { registryFieldsSet++ }
+   if c.RegistryUsername != "" { registryFieldsSet++ }
+   if c.RegistryPassword != "" { registryFieldsSet++ }
+   if registryFieldsSet > 0 && registryFieldsSet < 3 {
+     return fmt.Errorf("all registry fields (url, username, password) must be provided together")
+   }

    return nil
  }

Note: If there's a BuildBackend field indicating depot usage, add validation that DepotToken is required when BuildBackend == BuildBackendDepot.

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

33-107: Deduplicate S3 init with vault storage; extract shared helper

This duplicates go/pkg/vault/storage/s3.go. Extract common endpoint resolver, config loading, bucket creation, and presigner setup into a shared package (e.g., go/pkg/storage/s3) and use it in both places.

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

54-56: Default platform should be amd64 and configurable (avoid hardcoding arm64)

Hardcoding arm64 breaks common x86_64 environments. Default to amd64; make it configurable later.

Apply:

- const architecture = "arm64"
+ // TODO: make configurable via config/request; default to the most common architecture
+ const architecture = "amd64"
  platform := fmt.Sprintf("linux/%s", architecture)
@@
- buildkit, buildErr := machine.Acquire(ctx, buildResp.ID, buildResp.Token, architecture)
+ buildkit, buildErr := machine.Acquire(ctx, buildResp.ID, buildResp.Token, architecture)

Also applies to: 113-121


226-233: Fix logging key/value mismatch and use string for depot_project_id

Pairs are misaligned and DepotProjectID is a sql.NullString. Log .String and include unkey_project_id.

Apply:

-    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/run.go (3)

185-210: Compile-time mismatch: handler assigned to client variable and passed as handler later

You assign a server handler to a BuildServiceClient and pass that same var to NewBuildServiceHandler (which expects a handler). This will not compile. Keep distinct handler and client vars; wrap the handler with a thin in-process client.

Apply:

-    var buildService ctrlv1connect.BuildServiceClient
+    var buildHandler ctrlv1connect.BuildServiceHandler
+    var buildClient  ctrlv1connect.BuildServiceClient
     switch cfg.BuildBackend {
     case BuildBackendDocker:
-        buildService = docker.New(docker.Config{
+        buildHandler = docker.New(docker.Config{
             DB:      database,
             Logger:  logger,
-            Storage: buildStorage,
+            Storage: buildS3,
         })
         logger.Info("Using Docker build backend", "presign_url", cfg.BuildS3.ExternalURL)

     case BuildBackendDepot:
-        buildService = depot.New(depot.Config{
+        buildHandler = depot.New(depot.Config{
             InstanceID:  cfg.InstanceID,
             DB:          database,
             APIUrl:      cfg.Depot.APIUrl,
             RegistryUrl: cfg.Depot.RegistryUrl,
             Username:    cfg.Depot.Username,
             AccessToken: cfg.Depot.AccessToken,
             Logger:      logger,
-            Storage:     buildStorage,
+            Storage:     buildS3,
         })
         logger.Info("Using Depot build backend")

     default:
         return fmt.Errorf("unknown build backend: %s (must be 'docker' or 'depot')", cfg.BuildBackend)
     }
+    // Local in-process client adapter
+    buildClient = buildHandlerClient{h: buildHandler}

Add this adapter near the bottom/top of the file:

type buildHandlerClient struct{ h ctrlv1connect.BuildServiceHandler }

func (c buildHandlerClient) CreateBuild(ctx context.Context, req *connect.Request[ctrlv1.CreateBuildRequest]) (*connect.Response[ctrlv1.CreateBuildResponse], error) {
    return c.h.CreateBuild(ctx, req)
}
func (c buildHandlerClient) GenerateUploadURL(ctx context.Context, req *connect.Request[ctrlv1.GenerateUploadURLRequest]) (*connect.Response[ctrlv1.GenerateUploadURLResponse], error) {
    return c.h.GenerateUploadURL(ctx, req)
}

Based on learnings


212-214: Pass the client to workflows; keep handler for HTTP mux

Ensure the deploy workflow gets the client, not the handler.

Apply:

-    restateClient := restateIngress.NewClient(cfg.Restate.IngressURL)
+    restateClient := restateIngress.NewClient(cfg.Restate.IngressURL)
@@
-        BuildClient:   buildService,
+        BuildClient:   buildClient,

Also applies to: 221-223


313-321: Use the handler in HTTP mux; pass the client into services

Swap buildService for buildHandler in mux; pass buildClient into deployment service.

Apply:

- mux.Handle(ctrlv1connect.NewBuildServiceHandler(buildService, connectOptions...))
+ mux.Handle(ctrlv1connect.NewBuildServiceHandler(buildHandler, connectOptions...))
@@
-        BuildService: buildService,
+        BuildService: buildClient,
🧹 Nitpick comments (11)
go/Dockerfile.tilt (2)

1-1: Consider pinning the Alpine version for reproducible builds.

Using alpine:latest introduces non-deterministic builds as the base image can change over time, potentially causing unexpected breakages or behavior changes.

Apply this diff to pin to a specific Alpine version:

-FROM alpine:latest
+FROM alpine:3.21

9-10: The chmod command may be redundant.

If the source binary in bin/unkey already has executable permissions, the chmod +x command is unnecessary. COPY preserves file permissions by default.

Verify if the source binary already has executable permissions. If so, you can remove these lines:

-# Make it executable
-RUN chmod +x /unkey
-
go/apps/ctrl/services/build/backend/docker/create_build.go (1)

109-123: Only the first build error is captured.

The loop breaks on the first error encountered, which means subsequent errors in the build stream won't be logged. This could hide valuable debugging information about cascading failures.

Consider collecting all errors or at least logging them before breaking:

  for scanner.Scan() {
    var resp dockerBuildResponse
    if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
      continue
    }

    if resp.Error != "" {
-     buildError = fmt.Errorf("%s", resp.ErrorDetail.Message)
+     if buildError == nil {
+       buildError = fmt.Errorf("%s", resp.ErrorDetail.Message)
+     }
      d.logger.Error("Build failed",
        "error", resp.ErrorDetail.Message,
        "image_name", imageName,
        "unkey_project_id", req.Msg.UnkeyProjectId)
-     break
+     // Continue to log all errors
    }
  }
go/apps/krane/backend/kubernetes/create_deployment.go (1)

150-160: Consider making registry configuration values configurable.

The registry URL prefix (registry.depot.dev/) and secret name (depot-registry) are hardcoded. If additional registries or different secret names are needed in the future, this will require code changes.

Consider accepting these as configuration parameters in the k8s struct or as part of the deployment request. This would allow for more flexible registry support without code changes.

go/k8s/manifests/ctrl.yaml (1)

81-82: Consider making the S3 bucket name configurable.

The bucket name build-contexts is hardcoded. If different environments or tenants require separate buckets, this will need to be changed in the manifest.

Consider moving this to the depot-credentials secret or a ConfigMap to allow environment-specific configuration without manifest changes.

deployment/docker-compose.yaml (1)

289-291: Registry and build configuration is duplicated across services.

The registry, build S3, and depot configuration blocks appear in multiple services (krane, ctrl, restate). While docker-compose doesn't have built-in config inheritance, this duplication increases maintenance burden.

Consider using YAML anchors to reduce duplication:

x-registry-config: &registry-config
  UNKEY_REGISTRY_URL: "${UNKEY_REGISTRY_URL:-}"
  UNKEY_REGISTRY_USERNAME: "${UNKEY_REGISTRY_USERNAME:-}"
  UNKEY_REGISTRY_PASSWORD: "${UNKEY_REGISTRY_PASSWORD:-}"

x-build-config: &build-config
  UNKEY_BUILD_S3_URL: "${UNKEY_BUILD_S3_URL:-http://s3:3902}"
  UNKEY_BUILD_S3_EXTERNAL_URL: "${UNKEY_BUILD_S3_EXTERNAL_URL:-}"
  UNKEY_BUILD_S3_BUCKET: "build-contexts"
  # ... etc

services:
  krane:
    environment:
      <<: *registry-config
      # other krane-specific vars

  ctrl:
    environment:
      <<: *build-config
      # other ctrl-specific vars

Also applies to: 362-380

go/apps/krane/backend/docker/service.go (2)

55-56: Docker host string may be malformed (extra slash) depending on SocketPath

fmt.Sprintf("unix:///%s", cfg.SocketPath) yields unix:////var/run/docker.sock when SocketPath starts with “/”. Normalize to exactly unix:///….

Apply:

-    client.WithHost(fmt.Sprintf("unix:///%s", cfg.SocketPath)),
+    // Ensure exactly one leading slash after unix://
+    client.WithHost(func() string {
+        p := cfg.SocketPath
+        if strings.HasPrefix(p, "/") {
+            return "unix://" + p       // -> unix:///var/run/docker.sock
+        }
+        return "unix:///" + p
+    }()),

(Import "strings".)


106-139: Differentiate “not found” from other inspect errors; avoid masking real failures

Treating any ImageInspect error as “missing” hides permission/daemon errors. Use Docker’s errdefs.IsNotFound.

Apply:

-  _, err := d.client.ImageInspect(ctx, imageName)
-  if err == nil {
+  _, err := d.client.ImageInspect(ctx, imageName)
+  if err == nil {
       d.logger.Info("using existing local image", "image", imageName)
       return nil
-  }
+  }
+  if !errdefs.IsNotFound(err) {
+      return fmt.Errorf("failed to inspect image %q: %w", imageName, err)
+  }

(Import "github.com/docker/docker/errdefs".)

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

237-252: Region is hardcoded; consider making it configurable

RegionId: "us-east-1" might be wrong for some accounts. Read from config/env or infer from project/tenant settings.

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

173-180: Variable name shadows import; prefer a distinct name

buildStorage := buildStorage.NewS3(...) shadows the imported package alias. Rename the variable (e.g., buildS3) to avoid confusion.

Apply:

- buildStorage, err := buildStorage.NewS3(buildStorage.S3Config{
+ buildS3, err := buildStorage.NewS3(buildStorage.S3Config{

…and update subsequent references in this function.

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

86-100: Consider adding retry options and (if needed) path-style addressing

  • Align with vault S3 client by enabling standard retry mode and limited attempts.
  • If targeting MinIO/R2 variants that require path-style, set awsS3.NewFromConfig(cfg, func(o *awsS3.Options){ o.UsePathStyle = true }).
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between caa937f and 1a17fb8.

📒 Files selected for processing (16)
  • deployment/docker-compose.yaml (3 hunks)
  • deployment/setup-build-backend.sh (1 hunks)
  • go/Dockerfile.tilt (1 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/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)
  • go/apps/krane/backend/docker/create_deployment.go (1 hunks)
  • go/apps/krane/backend/docker/service.go (2 hunks)
  • go/apps/krane/backend/kubernetes/create_deployment.go (2 hunks)
  • go/apps/krane/config.go (1 hunks)
  • go/apps/krane/run.go (1 hunks)
  • go/cmd/ctrl/main.go (2 hunks)
  • go/cmd/krane/main.go (3 hunks)
  • go/k8s/manifests/ctrl.yaml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • go/apps/krane/backend/docker/create_deployment.go
  • go/cmd/krane/main.go
  • deployment/setup-build-backend.sh
🧰 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 (8)
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/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 (68-129)
  • 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 (34-46)
  • Config (23-32)
  • Depot (11-21)
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/cmd/ctrl/main.go (5)
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-66)
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-21)
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/krane/run.go (2)
go/apps/krane/backend/docker/service.go (2)
  • New (49-101)
  • Config (33-38)
go/apps/krane/config.go (1)
  • Config (24-103)
go/apps/ctrl/services/build/storage/s3.go (2)
go/apps/ctrl/config.go (1)
  • S3Config (18-24)
go/pkg/vault/storage/s3.go (2)
  • S3Config (25-31)
  • NewS3 (33-70)
go/apps/ctrl/services/build/backend/depot/create_build.go (3)
go/apps/ctrl/services/build/backend/depot/service.go (1)
  • Depot (11-21)
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/krane/backend/docker/service.go (1)
go/apps/krane/config.go (1)
  • Config (24-103)
⏰ 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). (3)
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Build / Build
  • GitHub Check: Test Packages / Test
🔇 Additional comments (6)
go/apps/ctrl/services/build/backend/docker/create_build.go (1)

38-39: Platform is hardcoded to linux/amd64.

The architecture is fixed to amd64, which may not suit all deployment targets. Consider whether multi-architecture support or configurable platform selection is needed.

If multi-architecture builds are planned, consider making the platform configurable via the request or service configuration.

deployment/docker-compose.yaml (1)

169-170: Port mapping correction aligns with MinIO configuration.

The change from 2903:3903 to 3903:3903 correctly maps the host port to match the MINIO_CONSOLE_PORT_NUMBER environment variable. This ensures the MinIO console is accessible on the expected port.

Based on learnings

go/apps/krane/run.go (1)

85-90: LGTM! Clean refactor to use configuration struct.

The change from passing individual parameters to using a docker.Config struct improves maintainability and aligns with the broader registry integration. The addition of registry credentials enables authenticated image pulls from private registries.

go/apps/krane/backend/kubernetes/create_deployment.go (1)

152-152: The prefix check is correct and aligns with how images are constructed.

Based on verification of the codebase:

  • Image names are constructed as registry.depot.dev/project:tag (line 141 of create_build.go via fmt.Sprintf("%s/%s:%s-%s", s.registryUrl, ...))
  • Configuration consistently defines RegistryUrl as registry.depot.dev without protocol (config.go, krane/config.go)
  • No code prepends protocol prefixes to image strings
  • The registry.depot.dev/ prefix check at line 152 correctly matches the actual format used throughout the codebase

The review comment's concern about protocol prefixes is unsupported by evidence.

Likely an incorrect or invalid review comment.

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

169-177: LGTM: Build S3 config wiring

Properly maps internal URL and optional ExternalURL to ctrl config.


179-186: Depot credentials are optional here; ensure backend-specific validation

For build-backend=depot, validate depot-api-url, depot-registry-url, depot-username, and depot-access-token are non-empty in config.Validate(); otherwise CreateBuild will fail at runtime.

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.

3 participants