Skip to content
Merged
34 changes: 12 additions & 22 deletions go/apps/ctrl/services/deployment/create_deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,7 @@ func (s *Service) CreateDeployment(
ctx context.Context,
req *connect.Request[ctrlv1.CreateDeploymentRequest],
) (*connect.Response[ctrlv1.CreateDeploymentResponse], error) {
// Validate workspace exists
_, err := db.Query.FindWorkspaceByID(ctx, s.db.RO(), req.Msg.GetWorkspaceId())
if err != nil {
if db.IsNotFound(err) {
return nil, connect.NewError(connect.CodeNotFound,
fmt.Errorf("workspace not found: %s", req.Msg.GetWorkspaceId()))
}
return nil, connect.NewError(connect.CodeInternal, err)
}

// Validate project exists and belongs to workspace
// Lookup project and infer workspace from it
project, err := db.Query.FindProjectById(ctx, s.db.RO(), req.Msg.GetProjectId())
if err != nil {
if db.IsNotFound(err) {
Expand All @@ -47,23 +37,18 @@ func (s *Service) CreateDeployment(
return nil, connect.NewError(connect.CodeInternal, err)
}

// Verify project belongs to the specified workspace
if project.WorkspaceID != req.Msg.GetWorkspaceId() {
return nil, connect.NewError(connect.CodeInvalidArgument,
fmt.Errorf("project %s does not belong to workspace %s",
req.Msg.GetProjectId(), req.Msg.GetWorkspaceId()))
}
workspaceID := project.WorkspaceID

env, err := db.Query.FindEnvironmentByProjectIdAndSlug(ctx, s.db.RO(), db.FindEnvironmentByProjectIdAndSlugParams{
WorkspaceID: req.Msg.GetWorkspaceId(),
WorkspaceID: workspaceID,
ProjectID: project.ID,
Slug: req.Msg.GetEnvironmentSlug(),
})
if err != nil {
if db.IsNotFound(err) {
return nil, connect.NewError(connect.CodeNotFound,
fmt.Errorf("environment '%s' not found in workspace '%s'",
req.Msg.GetEnvironmentSlug(), req.Msg.GetWorkspaceId()))
req.Msg.GetEnvironmentSlug(), workspaceID))
}
return nil, connect.NewError(connect.CodeInternal,
fmt.Errorf("failed to lookup environment: %w", err))
Expand Down Expand Up @@ -112,7 +97,7 @@ func (s *Service) CreateDeployment(
// Insert deployment into database
err = db.Query.InsertDeployment(ctx, s.db.RW(), db.InsertDeploymentParams{
ID: deploymentID,
WorkspaceID: req.Msg.GetWorkspaceId(),
WorkspaceID: workspaceID,
ProjectID: req.Msg.GetProjectId(),
EnvironmentID: env.ID,
RuntimeConfig: json.RawMessage(`{
Expand All @@ -138,16 +123,21 @@ func (s *Service) CreateDeployment(

s.logger.Info("starting deployment workflow for deployment",
"deployment_id", deploymentID,
"workspace_id", req.Msg.GetWorkspaceId(),
"workspace_id", workspaceID,
"project_id", req.Msg.GetProjectId(),
"environment", env.ID,
)

// Start the deployment workflow directly
keyspaceID := req.Msg.GetKeyspaceId()
var keyAuthID *string
if keyspaceID != "" {
keyAuthID = &keyspaceID
}
deployReq := &hydrav1.DeployRequest{
DeploymentId: deploymentID,
DockerImage: req.Msg.GetDockerImage(),
KeyAuthId: req.Msg.KeyspaceId,
KeyAuthId: keyAuthID,
}
// this is ugly, but we're waiting for
// https://github.com/restatedev/sdk-go/issues/103
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ func TestGitFieldValidation_NullHandling(t *testing.T) {

// Test empty protobuf fields
req := &ctrlv1.CreateDeploymentRequest{
WorkspaceId: "ws_test",
ProjectId: "proj_test",
GitCommitMessage: "",
GitCommitAuthorAvatarUrl: "",
Expand Down Expand Up @@ -158,7 +157,6 @@ func TestCreateVersionTimestampValidation_InvalidSecondsFormat(t *testing.T) {

// Create proto request directly with seconds timestamp (should be rejected)
req := &ctrlv1.CreateDeploymentRequest{
WorkspaceId: "ws_test123",
ProjectId: "proj_test456",
Branch: "main",
SourceType: ctrlv1.SourceType_SOURCE_TYPE_GIT,
Expand All @@ -177,7 +175,6 @@ func TestCreateVersionTimestampValidation_ValidMillisecondsFormat(t *testing.T)

// Create proto request directly with milliseconds timestamp
req := &ctrlv1.CreateDeploymentRequest{
WorkspaceId: "ws_test123",
ProjectId: "proj_test456",
Branch: "main",
SourceType: ctrlv1.SourceType_SOURCE_TYPE_GIT,
Expand Down Expand Up @@ -275,7 +272,6 @@ func TestCreateVersionFieldMapping(t *testing.T) {
{
name: "all_git_fields_populated",
request: &ctrlv1.CreateDeploymentRequest{
WorkspaceId: "ws_test123",
ProjectId: "proj_test456",
Branch: "feature/test-branch",
SourceType: ctrlv1.SourceType_SOURCE_TYPE_GIT,
Expand Down Expand Up @@ -320,7 +316,6 @@ func TestCreateVersionFieldMapping(t *testing.T) {
{
name: "empty_git_fields",
request: &ctrlv1.CreateDeploymentRequest{
WorkspaceId: "ws_test123",
ProjectId: "proj_test456",
Branch: "main",
SourceType: ctrlv1.SourceType_SOURCE_TYPE_GIT,
Expand Down Expand Up @@ -365,7 +360,6 @@ func TestCreateVersionFieldMapping(t *testing.T) {
{
name: "mixed_populated_and_empty_fields",
request: &ctrlv1.CreateDeploymentRequest{
WorkspaceId: "ws_test123",
ProjectId: "proj_test456",
Branch: "hotfix/urgent-fix",
SourceType: ctrlv1.SourceType_SOURCE_TYPE_GIT,
Expand Down Expand Up @@ -417,7 +411,7 @@ func TestCreateVersionFieldMapping(t *testing.T) {
// This tests the actual field wiring that happens in the service
params := db.InsertDeploymentParams{
ID: "test_deployment_id",
WorkspaceID: tt.request.GetWorkspaceId(),
WorkspaceID: "ws_test123", // In production, this is inferred from ProjectID via DB lookup, just hardcoded for the test
ProjectID: tt.request.GetProjectId(),
EnvironmentID: "todo",
// Git field mappings - this is what we're testing
Expand Down
44 changes: 17 additions & 27 deletions go/cmd/deploy/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,19 @@ import (
)

var (
ErrWorkspaceIDRequired = errors.New("workspace ID is required (use --workspace-id flag or edit unkey.json)")
ErrProjectIDRequired = errors.New("project ID is required (use --project-id flag or edit unkey.json)")
ErrConfigPathResolve = errors.New("failed to resolve config path")
ErrConfigFileRead = errors.New("failed to read config file")
ErrConfigFileParse = errors.New("failed to parse config file")
ErrConfigFileWrite = errors.New("failed to write config file")
ErrConfigMarshal = errors.New("failed to marshal config")
ErrDirectoryCreate = errors.New("failed to create directory")
ErrProjectIDRequired = errors.New("project ID is required (use --project-id flag or edit unkey.json)")
ErrConfigPathResolve = errors.New("failed to resolve config path")
ErrConfigFileRead = errors.New("failed to read config file")
ErrConfigFileParse = errors.New("failed to parse config file")
ErrConfigFileWrite = errors.New("failed to write config file")
ErrConfigMarshal = errors.New("failed to marshal config")
ErrDirectoryCreate = errors.New("failed to create directory")
)

type Config struct {
WorkspaceID string `json:"workspace_id"`
KeyspaceID string `json:"keyspace_id"`
ProjectID string `json:"project_id"`
Context string `json:"context"`
KeyspaceID string `json:"keyspace_id"`
ProjectID string `json:"project_id"`
Context string `json:"context"`
}

// loadConfig loads configuration from unkey.json in the specified directory.
Expand Down Expand Up @@ -80,16 +78,15 @@ func getConfigFilePath(configDir string) string {
}

// createConfigWithValues creates a new unkey.json file with the provided values
func createConfigWithValues(configDir, workspaceID, projectID, context string) error {
func createConfigWithValues(configDir, projectID, context string) error {
// Create directory if it doesn't exist
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("%w %s: %w", ErrDirectoryCreate, configDir, err)
}

config := &Config{
WorkspaceID: workspaceID,
ProjectID: projectID,
Context: context,
ProjectID: projectID,
Context: context,
}

configPath := filepath.Join(configDir, "unkey.json")
Expand All @@ -115,17 +112,13 @@ func writeConfig(configPath string, config *Config) error {
}

// mergeWithFlags merges config values with command flags, with flags taking precedence
func (c *Config) mergeWithFlags(workspaceID, projectID, keyspaceID, context string) *Config {
func (c *Config) mergeWithFlags(projectID, keyspaceID, context string) *Config {
merged := &Config{
WorkspaceID: c.WorkspaceID,
KeyspaceID: c.KeyspaceID,
ProjectID: c.ProjectID,
Context: c.Context,
KeyspaceID: c.KeyspaceID,
ProjectID: c.ProjectID,
Context: c.Context,
}
// Flags override config values
if workspaceID != "" {
merged.WorkspaceID = workspaceID
}
if projectID != "" {
merged.ProjectID = projectID
}
Expand All @@ -144,9 +137,6 @@ func (c *Config) mergeWithFlags(workspaceID, projectID, keyspaceID, context stri

// validate checks if required fields are present and not placeholder values
func (c *Config) validate() error {
if c.WorkspaceID == "" || c.WorkspaceID == "ws_your_workspace_id" {
return ErrWorkspaceIDRequired
}
if c.ProjectID == "" || c.ProjectID == "proj_your_project_id" {
return ErrProjectIDRequired
}
Expand Down
1 change: 0 additions & 1 deletion go/cmd/deploy/control_plane.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ func (c *ControlPlaneClient) CreateDeployment(ctx context.Context, dockerImage s
// Get git commit information
commitInfo := git.GetInfo()
createReq := connect.NewRequest(&ctrlv1.CreateDeploymentRequest{
WorkspaceId: c.opts.WorkspaceID,
ProjectId: c.opts.ProjectID,
KeyspaceId: &c.opts.KeyspaceID,
Branch: c.opts.Branch,
Expand Down
10 changes: 2 additions & 8 deletions go/cmd/deploy/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,6 @@ func handleInit(cmd *cli.Command, ui *UI) error {
// Interactive prompts for configuration
fmt.Printf("Please provide the following configuration details:\n\n")

fmt.Printf("Workspace ID: ")
workspaceID := readLine()
if workspaceID == "" {
return fmt.Errorf("workspace ID is required")
}

fmt.Printf("Project ID: ")
projectID := readLine()
if projectID == "" {
Expand All @@ -62,7 +56,7 @@ func handleInit(cmd *cli.Command, ui *UI) error {

// Create configuration with user input
ui.Print("Creating configuration file")
if err := createConfigWithValues(configDir, workspaceID, projectID, context); err != nil {
if err := createConfigWithValues(configDir, projectID, context); err != nil {
ui.PrintError("Failed to create config file")
return fmt.Errorf("failed to create config file: %w", err)
}
Expand All @@ -80,7 +74,7 @@ func printInitNextSteps() {
fmt.Printf(" unkey deploy\n")
fmt.Printf("\n")
fmt.Printf("Or override specific values:\n")
fmt.Printf(" unkey deploy --workspace-id=ws_different\n")
fmt.Printf(" unkey deploy --project-id=proj_different\n")
fmt.Printf(" unkey deploy --context=./other-app\n")
}

Expand Down
20 changes: 7 additions & 13 deletions go/cmd/deploy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@ const (
DefaultBranch = "main"
DefaultDockerfile = "Dockerfile"
DefaultRegistry = "ghcr.io/unkeyed/deploy"
DefaultControlPlaneURL = "http://localhost:7091"
DefaultControlPlaneURL = "https://ctrl.unkey.cloud"
DefaultAuthToken = "ctrl-secret-token"
DefaultEnvironment = "Production"
DefaultEnvironment = "preview"

// Environment variables
EnvWorkspaceID = "UNKEY_WORKSPACE_ID"
EnvKeyspaceID = "UNKEY_KEYSPACE_ID"
EnvRegistry = "UNKEY_REGISTRY"
EnvKeyspaceID = "UNKEY_KEYSPACE_ID"
EnvRegistry = "UNKEY_REGISTRY"

// URL prefixes
HTTPSPrefix = "https://"
Expand Down Expand Up @@ -81,7 +80,6 @@ var stepSequence = map[string]string{

// DeployOptions contains all configuration for deployment
type DeployOptions struct {
WorkspaceID string
ProjectID string
KeyspaceID string
Context string
Expand All @@ -106,22 +104,21 @@ var DeployFlags = []cli.Flag{
cli.Bool("init", "Initialize configuration file in the specified directory"),
cli.Bool("force", "Force overwrite existing configuration file when using --init"),
// Required flags (can be provided via config file)
cli.String("workspace-id", "Workspace ID", cli.EnvVar(EnvWorkspaceID)),
cli.String("project-id", "Project ID", cli.EnvVar("UNKEY_PROJECT_ID")),
cli.String("keyspace-id", "Keyspace ID for API key authentication", cli.EnvVar(EnvKeyspaceID)),
// Optional flags with defaults
cli.String("context", "Build context path"),
cli.String("context", "Build context path", cli.Default(".")),
cli.String("branch", "Git branch", cli.Default(DefaultBranch)),
cli.String("docker-image", "Pre-built docker image"),
cli.String("dockerfile", "Path to Dockerfile", cli.Default(DefaultDockerfile)),
cli.String("commit", "Git commit SHA"),
cli.String("registry", "Container registry",
cli.Default(DefaultRegistry),
cli.EnvVar(EnvRegistry)),
cli.String("env", "Environment slug to deploy to", cli.Default("preview")),
cli.String("env", "Environment slug to deploy to", cli.Default(DefaultEnvironment)),
cli.Bool("skip-push", "Skip pushing to registry (for local testing)"),
cli.Bool("verbose", "Show detailed output for build and deployment operations"),
cli.Bool("linux", "Build Docker image for linux/amd64 platform (for deployment to cloud clusters)"),
cli.Bool("linux", "Build Docker image for linux/amd64 platform (for deployment to cloud clusters)", cli.Default(true)),
// Control plane flags (internal)
cli.String("control-plane-url", "Control plane URL", cli.Default(DefaultControlPlaneURL)),
cli.String("auth-token", "Control plane auth token", cli.Default(DefaultAuthToken)),
Expand Down Expand Up @@ -153,7 +150,6 @@ unkey deploy --init --config=./my-project # Initialize with custom location
unkey deploy --init --force # Force overwrite existing configuration
unkey deploy # Standard deployment (uses ./unkey.json)
unkey deploy --config=./production # Deploy from specific config directory
unkey deploy --workspace-id=ws_production_123 # Override workspace from config file
unkey deploy --context=./api # Deploy with custom build context
unkey deploy --skip-push # Local development (build only, no push)
unkey deploy --docker-image=ghcr.io/user/app:v1.0.0 # Deploy pre-built image
Expand Down Expand Up @@ -182,7 +178,6 @@ func DeployAction(ctx context.Context, cmd *cli.Command) error {

// Merge config with command flags (flags take precedence)
finalConfig := cfg.mergeWithFlags(
cmd.String("workspace-id"),
cmd.String("project-id"),
cmd.String("keyspace-id"),
cmd.String("context"),
Expand All @@ -194,7 +189,6 @@ func DeployAction(ctx context.Context, cmd *cli.Command) error {
}

opts := DeployOptions{
WorkspaceID: finalConfig.WorkspaceID,
KeyspaceID: finalConfig.KeyspaceID,
ProjectID: finalConfig.ProjectID,
Context: finalConfig.Context,
Expand Down
21 changes: 6 additions & 15 deletions go/gen/proto/ctrl/v1/deployment.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go/proto/ctrl/v1/deployment.proto
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ enum SourceType {
}

message CreateDeploymentRequest {
string workspace_id = 1;
reserved 1; // was workspace_id, now inferred from project_id
string project_id = 2;
string branch = 3;
// Source information
Expand Down
Loading