fix: worker deployments and JWT_SECRET auto-provisioning
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
RC-1: Workers now get a Kubernetes Deployment on component creation.
NeedsPort() (port assignment) was incorrectly used to gate Deployment
creation - workers have no HTTP port but still need a Deployment so
CI `kubectl set image` can succeed. Added NeedsDeployment() returning
true for service/worker/app-react/app-astro/app-nextjs. AddIngressPath
is now guarded by port > 0 so workers don't attempt HTTP routing.
RC-2: JWT_SECRET is now auto-provisioned per-project when the first
code component is added. The skeleton service template fatally requires
JWT_SECRET at startup; previously fetchProjectCredentials() never fetched
it. ensureProjectJWTSecret() generates a cryptographically random 32-byte
secret, stores it as "{projectID}:JWT_SECRET", and JWT_SECRET is now
included in projectScopedKeys so it's injected into every deployment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9be5c7d81b
commit
3247ce3ca0
@ -88,6 +88,14 @@ func (c ComponentType) NeedsPort() bool {
|
||||
return c == ComponentTypeService || c == ComponentTypeAppAstro || c == ComponentTypeAppReact || c == ComponentTypeAppNextJS
|
||||
}
|
||||
|
||||
// NeedsDeployment returns true if this component type requires a Kubernetes Deployment.
|
||||
// All code components except CLI need a Deployment so CI can use kubectl set image.
|
||||
// Workers have no HTTP port but still need a Deployment to run as background processes.
|
||||
func (c ComponentType) NeedsDeployment() bool {
|
||||
return c == ComponentTypeService || c == ComponentTypeWorker ||
|
||||
c == ComponentTypeAppAstro || c == ComponentTypeAppReact || c == ComponentTypeAppNextJS
|
||||
}
|
||||
|
||||
// IsGoComponent returns true if this component type uses Go (and needs go.work entry).
|
||||
func (c ComponentType) IsGoComponent() bool {
|
||||
return c == ComponentTypeService || c == ComponentTypeWorker || c == ComponentTypeCLI
|
||||
|
||||
@ -77,4 +77,7 @@ const (
|
||||
|
||||
// Resend (email provider for per-project domain provisioning)
|
||||
CredKeyResendAPIKey = "RESEND_API_KEY"
|
||||
|
||||
// Project-scoped auth secret (unique per project, auto-generated on first code component)
|
||||
CredKeyJWTSecret = "JWT_SECRET"
|
||||
)
|
||||
|
||||
@ -3,6 +3,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
@ -237,7 +238,11 @@ func (s *ComponentService) AddComponent(ctx context.Context, projectID string, r
|
||||
Dependencies: []string{}, // Could be parsed from component.yaml
|
||||
}
|
||||
|
||||
// 13. Create initial K8s deployment for components that need one.
|
||||
// 13. Ensure a JWT_SECRET exists for this project (required by skeleton service startup).
|
||||
// Generated once per project on the first code component; reused for all subsequent components.
|
||||
s.ensureProjectJWTSecret(ctx, projectID)
|
||||
|
||||
// 14. Create initial K8s deployment for components that need one.
|
||||
// This ensures kubectl set image will find the deployment when CI runs.
|
||||
s.createInitialComponentDeployment(ctx, projectID, projectDomain, component)
|
||||
|
||||
@ -327,6 +332,58 @@ func (s *ComponentService) prepareMonorepoUpdates(
|
||||
return fileOps, nil
|
||||
}
|
||||
|
||||
// ensureProjectJWTSecret generates and stores a random JWT_SECRET for the project
|
||||
// if one does not already exist. Called on first code component add.
|
||||
// The secret is stored as "{projectID}:JWT_SECRET" in the credential store.
|
||||
func (s *ComponentService) ensureProjectJWTSecret(ctx context.Context, projectID string) {
|
||||
if s.credentialStore == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log := logging.FromContext(ctx).WithService("component")
|
||||
|
||||
key := projectID + ":" + domain.CredKeyJWTSecret
|
||||
existing, err := s.credentialStore.Get(ctx, key)
|
||||
if err != nil {
|
||||
log.Warn("failed to check JWT secret existence",
|
||||
logging.FieldProjectID, projectID,
|
||||
logging.FieldError, err,
|
||||
)
|
||||
return
|
||||
}
|
||||
if existing != "" {
|
||||
return // Already provisioned - don't overwrite
|
||||
}
|
||||
|
||||
// Generate a cryptographically random 32-byte secret
|
||||
secret := make([]byte, 32)
|
||||
if _, err := rand.Read(secret); err != nil {
|
||||
log.Warn("failed to generate JWT secret",
|
||||
logging.FieldProjectID, projectID,
|
||||
logging.FieldError, err,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
secretValue := base64.URLEncoding.EncodeToString(secret)
|
||||
if err := s.credentialStore.Set(ctx, domain.Credential{
|
||||
Key: key,
|
||||
Value: secretValue,
|
||||
Description: "JWT signing secret for project " + projectID,
|
||||
Category: "project",
|
||||
}); err != nil {
|
||||
log.Warn("failed to store JWT secret",
|
||||
logging.FieldProjectID, projectID,
|
||||
logging.FieldError, err,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("provisioned JWT secret for project",
|
||||
logging.FieldProjectID, projectID,
|
||||
)
|
||||
}
|
||||
|
||||
// findFirstServiceComponent returns the first service component in a project, or nil.
|
||||
func (s *ComponentService) findFirstServiceComponent(ctx context.Context, projectID string) *domain.Component {
|
||||
components, err := s.ListComponents(ctx, projectID)
|
||||
|
||||
@ -23,7 +23,7 @@ func (s *ComponentService) createInitialComponentDeployment(
|
||||
component *domain.Component,
|
||||
) {
|
||||
// Skip if no deployer or component doesn't need a deployment
|
||||
if s.deployer == nil || !component.Type.NeedsPort() {
|
||||
if s.deployer == nil || !component.Type.NeedsDeployment() {
|
||||
return
|
||||
}
|
||||
|
||||
@ -67,16 +67,18 @@ func (s *ComponentService) createInitialComponentDeployment(
|
||||
return
|
||||
}
|
||||
|
||||
// Add path to project's unified Ingress
|
||||
serviceName := spec.DeploymentName()
|
||||
if err := s.deployer.AddIngressPath(ctx, projectID, projectDomain, basePath, serviceName, component.Port); err != nil {
|
||||
log.Warn("failed to add ingress path for component",
|
||||
logging.FieldProjectID, projectID,
|
||||
"component", component.Name,
|
||||
"path", basePath,
|
||||
logging.FieldError, err,
|
||||
)
|
||||
// Continue anyway - the deployment/service exist and CI will work
|
||||
// Add path to project's unified Ingress (only for components with an HTTP port)
|
||||
if component.Port > 0 && basePath != "" {
|
||||
serviceName := spec.DeploymentName()
|
||||
if err := s.deployer.AddIngressPath(ctx, projectID, projectDomain, basePath, serviceName, component.Port); err != nil {
|
||||
log.Warn("failed to add ingress path for component",
|
||||
logging.FieldProjectID, projectID,
|
||||
"component", component.Name,
|
||||
"path", basePath,
|
||||
logging.FieldError, err,
|
||||
)
|
||||
// Continue anyway - the deployment exists and CI will work
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("created initial component deployment",
|
||||
@ -185,6 +187,7 @@ func (s *ComponentService) fetchProjectCredentials(ctx context.Context, projectI
|
||||
domain.CredKeyNotifyAPIKey,
|
||||
domain.CredKeyNotifyHost,
|
||||
domain.CredKeyNotifyFrom,
|
||||
domain.CredKeyJWTSecret,
|
||||
}
|
||||
|
||||
// Global credentials (stored without project prefix, shared across all projects)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user