Skip to content

Commit 4fea8a6

Browse files
committed
refactor: deduplicate deployment creation into deployutil package
Extract shared deployment logic (secrets blob, insert record, build deploy request, group env vars) into svc/ctrl/internal/deployutil to eliminate duplication between handle_push.go and authorize_deployment.go. Move approval-blocking logic into its own block_deployment.go file.
1 parent 7f18d4e commit 4fea8a6

File tree

4 files changed

+267
-269
lines changed

4 files changed

+267
-269
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package deployutil
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"time"
7+
8+
ctrlv1 "github.com/unkeyed/unkey/gen/proto/ctrl/v1"
9+
hydrav1 "github.com/unkeyed/unkey/gen/proto/hydra/v1"
10+
"github.com/unkeyed/unkey/pkg/db"
11+
"github.com/unkeyed/unkey/pkg/uid"
12+
"google.golang.org/protobuf/encoding/protojson"
13+
)
14+
15+
// GitCommitInfo holds git commit metadata needed for creating a deployment.
16+
type GitCommitInfo struct {
17+
SHA string
18+
Branch string
19+
Message string
20+
AuthorHandle string
21+
AuthorAvatarURL string
22+
Timestamp int64 // Unix milliseconds, 0 if unknown
23+
}
24+
25+
// BuildSecretsBlob marshals environment variables into a protobuf SecretsConfig blob.
26+
// Returns an empty byte slice if there are no env vars.
27+
func BuildSecretsBlob(envVars []db.ListEnvVarsForRepoConnectionsRow) ([]byte, error) {
28+
if len(envVars) == 0 {
29+
return []byte{}, nil
30+
}
31+
32+
secretsConfig := &ctrlv1.SecretsConfig{
33+
Secrets: make(map[string]string, len(envVars)),
34+
}
35+
for _, ev := range envVars {
36+
secretsConfig.Secrets[ev.Key] = ev.Value
37+
}
38+
return protojson.Marshal(secretsConfig)
39+
}
40+
41+
// InsertDeploymentRecord creates a deployment and its initial queued step in a single transaction.
42+
func InsertDeploymentRecord(
43+
ctx context.Context,
44+
rw *db.Replica,
45+
row db.ListRepoConnectionDeployContextsRow,
46+
commit GitCommitInfo,
47+
secretsBlob []byte,
48+
status db.DeploymentsStatus,
49+
) (string, error) {
50+
deploymentID := uid.New(uid.DeploymentPrefix)
51+
now := time.Now().UnixMilli()
52+
53+
project := row.Project
54+
env := row.Environment
55+
app := row.App
56+
runtimeSettings := row.AppRuntimeSetting
57+
58+
err := db.Tx(ctx, rw, func(txCtx context.Context, tx db.DBTX) error {
59+
if txErr := db.Query.InsertDeployment(txCtx, tx, db.InsertDeploymentParams{
60+
ID: deploymentID,
61+
K8sName: uid.DNS1035(12),
62+
WorkspaceID: project.WorkspaceID,
63+
ProjectID: project.ID,
64+
AppID: app.ID,
65+
EnvironmentID: env.ID,
66+
SentinelConfig: runtimeSettings.SentinelConfig,
67+
EncryptedEnvironmentVariables: secretsBlob,
68+
Command: runtimeSettings.Command,
69+
Status: status,
70+
CreatedAt: now,
71+
UpdatedAt: sql.NullInt64{Valid: false},
72+
GitCommitSha: sql.NullString{String: commit.SHA, Valid: commit.SHA != ""},
73+
GitBranch: sql.NullString{String: commit.Branch, Valid: commit.Branch != ""},
74+
GitCommitMessage: sql.NullString{String: commit.Message, Valid: commit.Message != ""},
75+
GitCommitAuthorHandle: sql.NullString{String: commit.AuthorHandle, Valid: commit.AuthorHandle != ""},
76+
GitCommitAuthorAvatarUrl: sql.NullString{String: commit.AuthorAvatarURL, Valid: commit.AuthorAvatarURL != ""},
77+
GitCommitTimestamp: sql.NullInt64{Int64: commit.Timestamp, Valid: commit.Timestamp != 0},
78+
OpenapiSpec: sql.NullString{Valid: false},
79+
CpuMillicores: runtimeSettings.CpuMillicores,
80+
MemoryMib: runtimeSettings.MemoryMib,
81+
Port: runtimeSettings.Port,
82+
ShutdownSignal: db.DeploymentsShutdownSignal(runtimeSettings.ShutdownSignal),
83+
Healthcheck: runtimeSettings.Healthcheck,
84+
}); txErr != nil {
85+
return txErr
86+
}
87+
88+
return db.Query.InsertDeploymentStep(txCtx, tx, db.InsertDeploymentStepParams{
89+
WorkspaceID: app.WorkspaceID,
90+
ProjectID: app.ProjectID,
91+
AppID: app.ID,
92+
EnvironmentID: env.ID,
93+
DeploymentID: deploymentID,
94+
Step: db.DeploymentStepsStepQueued,
95+
StartedAt: uint64(now),
96+
})
97+
})
98+
if err != nil {
99+
return "", err
100+
}
101+
return deploymentID, nil
102+
}
103+
104+
// BuildDeployRequest constructs a DeployRequest for a git-sourced deployment.
105+
func BuildDeployRequest(
106+
deploymentID string,
107+
row db.ListRepoConnectionDeployContextsRow,
108+
commitSHA string,
109+
) *hydrav1.DeployRequest {
110+
return &hydrav1.DeployRequest{
111+
DeploymentId: deploymentID,
112+
Source: &hydrav1.DeployRequest_Git{
113+
Git: &hydrav1.GitSource{
114+
InstallationId: row.GithubRepoConnection.InstallationID,
115+
Repository: row.GithubRepoConnection.RepositoryFullName,
116+
CommitSha: commitSHA,
117+
ContextPath: row.AppBuildSetting.DockerContext,
118+
DockerfilePath: row.AppBuildSetting.Dockerfile,
119+
},
120+
},
121+
}
122+
}
123+
124+
// GroupEnvVarsByApp groups environment variables by app ID for efficient lookup.
125+
func GroupEnvVarsByApp(envVars []db.ListEnvVarsForRepoConnectionsRow) map[string][]db.ListEnvVarsForRepoConnectionsRow {
126+
result := make(map[string][]db.ListEnvVarsForRepoConnectionsRow)
127+
for _, ev := range envVars {
128+
result[ev.AppID] = append(result[ev.AppID], ev)
129+
}
130+
return result
131+
}

svc/ctrl/services/deployment/authorize_deployment.go

Lines changed: 21 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,13 @@ package deployment
22

33
import (
44
"context"
5-
"database/sql"
65
"fmt"
7-
"time"
86

97
"connectrpc.com/connect"
108
ctrlv1 "github.com/unkeyed/unkey/gen/proto/ctrl/v1"
11-
hydrav1 "github.com/unkeyed/unkey/gen/proto/hydra/v1"
129
"github.com/unkeyed/unkey/pkg/db"
1310
"github.com/unkeyed/unkey/pkg/logger"
14-
"github.com/unkeyed/unkey/pkg/uid"
15-
"google.golang.org/protobuf/encoding/protojson"
11+
"github.com/unkeyed/unkey/svc/ctrl/internal/deployutil"
1612
)
1713

1814
// AuthorizeDeployment authorizes a deployment for an external contributor's push.
@@ -26,7 +22,6 @@ func (s *Service) AuthorizeDeployment(ctx context.Context, req *connect.Request[
2622
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("project_id and branch are required"))
2723
}
2824

29-
// Find repo connection for this project
3025
repoConn, err := db.Query.FindGithubRepoConnectionByProjectId(ctx, s.db.RO(), projectID)
3126
if err != nil {
3227
if db.IsNotFound(err) {
@@ -35,13 +30,11 @@ func (s *Service) AuthorizeDeployment(ctx context.Context, req *connect.Request[
3530
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to find repo connection: %w", err))
3631
}
3732

38-
// Fetch current HEAD of the branch from GitHub — this is the SHA we deploy
3933
headCommit, err := s.github.GetBranchHeadCommit(repoConn.InstallationID, repoConn.RepositoryFullName, branch)
4034
if err != nil {
4135
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to fetch branch HEAD from GitHub: %w", err))
4236
}
4337

44-
// Find all deploy contexts for this branch
4538
contexts, err := db.Query.ListRepoConnectionDeployContexts(ctx, s.db.RO(), db.ListRepoConnectionDeployContextsParams{
4639
InstallationID: repoConn.InstallationID,
4740
RepositoryID: repoConn.RepositoryID,
@@ -55,7 +48,6 @@ func (s *Service) AuthorizeDeployment(ctx context.Context, req *connect.Request[
5548
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("no deploy contexts found for project %s branch %s", projectID, branch))
5649
}
5750

58-
// Fetch env vars for all matched apps
5951
allEnvVars, err := db.Query.ListEnvVarsForRepoConnections(ctx, s.db.RO(), db.ListEnvVarsForRepoConnectionsParams{
6052
InstallationID: repoConn.InstallationID,
6153
RepositoryID: repoConn.RepositoryID,
@@ -65,102 +57,32 @@ func (s *Service) AuthorizeDeployment(ctx context.Context, req *connect.Request[
6557
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to list env vars: %w", err))
6658
}
6759

68-
envVarsByApp := make(map[string][]db.ListEnvVarsForRepoConnectionsRow)
69-
for _, ev := range allEnvVars {
70-
envVarsByApp[ev.AppID] = append(envVarsByApp[ev.AppID], ev)
71-
}
60+
envVarsByApp := deployutil.GroupEnvVarsByApp(allEnvVars)
7261

73-
commitTimestamp := headCommit.Timestamp.UnixMilli()
62+
commit := deployutil.GitCommitInfo{
63+
SHA: headCommit.SHA,
64+
Branch: branch,
65+
Message: headCommit.Message,
66+
AuthorHandle: headCommit.AuthorHandle,
67+
AuthorAvatarURL: headCommit.AuthorAvatarURL,
68+
Timestamp: headCommit.Timestamp.UnixMilli(),
69+
}
7470

7571
for _, row := range contexts {
76-
project := row.Project
77-
env := row.Environment
78-
app := row.App
79-
runtimeSettings := row.AppRuntimeSetting
80-
buildSettings := row.AppBuildSetting
81-
repo := row.GithubRepoConnection
82-
83-
// Build secrets blob from env vars
84-
appEnvVars := envVarsByApp[app.ID]
85-
secretsBlob := []byte{}
86-
if len(appEnvVars) > 0 {
87-
secretsConfig := &ctrlv1.SecretsConfig{
88-
Secrets: make(map[string]string, len(appEnvVars)),
89-
}
90-
for _, ev := range appEnvVars {
91-
secretsConfig.Secrets[ev.Key] = ev.Value
92-
}
93-
var marshalErr error
94-
secretsBlob, marshalErr = protojson.Marshal(secretsConfig)
95-
if marshalErr != nil {
96-
logger.Error("failed to marshal secrets config", "appId", app.ID, "error", marshalErr)
97-
continue
98-
}
72+
secretsBlob, marshalErr := deployutil.BuildSecretsBlob(envVarsByApp[row.App.ID])
73+
if marshalErr != nil {
74+
logger.Error("failed to marshal secrets config", "appId", row.App.ID, "error", marshalErr)
75+
continue
9976
}
10077

101-
deploymentID := uid.New(uid.DeploymentPrefix)
102-
now := time.Now().UnixMilli()
103-
104-
err = db.Tx(ctx, s.db.RW(), func(txCtx context.Context, tx db.DBTX) error {
105-
if txErr := db.Query.InsertDeployment(txCtx, tx, db.InsertDeploymentParams{
106-
ID: deploymentID,
107-
K8sName: uid.DNS1035(12),
108-
WorkspaceID: project.WorkspaceID,
109-
ProjectID: project.ID,
110-
AppID: app.ID,
111-
EnvironmentID: env.ID,
112-
SentinelConfig: runtimeSettings.SentinelConfig,
113-
EncryptedEnvironmentVariables: secretsBlob,
114-
Command: runtimeSettings.Command,
115-
Status: db.DeploymentsStatusPending,
116-
CreatedAt: now,
117-
UpdatedAt: sql.NullInt64{Valid: false},
118-
GitCommitSha: sql.NullString{String: headCommit.SHA, Valid: true},
119-
GitBranch: sql.NullString{String: branch, Valid: true},
120-
GitCommitMessage: sql.NullString{String: headCommit.Message, Valid: headCommit.Message != ""},
121-
GitCommitAuthorHandle: sql.NullString{String: headCommit.AuthorHandle, Valid: headCommit.AuthorHandle != ""},
122-
GitCommitAuthorAvatarUrl: sql.NullString{String: headCommit.AuthorAvatarURL, Valid: headCommit.AuthorAvatarURL != ""},
123-
GitCommitTimestamp: sql.NullInt64{Int64: commitTimestamp, Valid: true},
124-
OpenapiSpec: sql.NullString{Valid: false},
125-
CpuMillicores: runtimeSettings.CpuMillicores,
126-
MemoryMib: runtimeSettings.MemoryMib,
127-
Port: runtimeSettings.Port,
128-
ShutdownSignal: db.DeploymentsShutdownSignal(runtimeSettings.ShutdownSignal),
129-
Healthcheck: runtimeSettings.Healthcheck,
130-
}); txErr != nil {
131-
return txErr
132-
}
133-
134-
return db.Query.InsertDeploymentStep(txCtx, tx, db.InsertDeploymentStepParams{
135-
WorkspaceID: app.WorkspaceID,
136-
ProjectID: app.ProjectID,
137-
AppID: app.ID,
138-
EnvironmentID: env.ID,
139-
DeploymentID: deploymentID,
140-
Step: db.DeploymentStepsStepQueued,
141-
StartedAt: uint64(now),
142-
})
143-
})
144-
if err != nil {
145-
logger.Error("failed to insert deployment for authorization", "appId", app.ID, "error", err)
78+
deploymentID, insertErr := deployutil.InsertDeploymentRecord(ctx, s.db.RW(), row, commit, secretsBlob, db.DeploymentsStatusPending)
79+
if insertErr != nil {
80+
logger.Error("failed to insert deployment for authorization", "appId", row.App.ID, "error", insertErr)
14681
continue
14782
}
14883

149-
// Fire deploy workflow via Restate
150-
_, sendErr := s.deploymentClient(project.ID).
151-
Deploy().
152-
Send(ctx, &hydrav1.DeployRequest{
153-
DeploymentId: deploymentID,
154-
Source: &hydrav1.DeployRequest_Git{
155-
Git: &hydrav1.GitSource{
156-
InstallationId: repo.InstallationID,
157-
Repository: repo.RepositoryFullName,
158-
CommitSha: headCommit.SHA,
159-
ContextPath: buildSettings.DockerContext,
160-
DockerfilePath: buildSettings.Dockerfile,
161-
},
162-
},
163-
})
84+
deployReq := deployutil.BuildDeployRequest(deploymentID, row, headCommit.SHA)
85+
_, sendErr := s.deploymentClient(row.Project.ID).Deploy().Send(ctx, deployReq)
16486
if sendErr != nil {
16587
logger.Error("failed to trigger deploy workflow after authorization",
16688
"deployment_id", deploymentID,
@@ -171,8 +93,8 @@ func (s *Service) AuthorizeDeployment(ctx context.Context, req *connect.Request[
17193

17294
logger.Info("deployment authorized and workflow triggered",
17395
"deployment_id", deploymentID,
174-
"project_id", project.ID,
175-
"app_id", app.ID,
96+
"project_id", row.Project.ID,
97+
"app_id", row.App.ID,
17698
"branch", branch,
17799
"commit_sha", headCommit.SHA,
178100
)

0 commit comments

Comments
 (0)