Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/api/src/pkg/testutil/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ export abstract class Harness {
updatedAtM: null,
deletedAtM: null,
partitionId: null,
k8sNamespace: null,
};
const userWorkspace: Workspace = {
id: newId("test"),
Expand All @@ -288,6 +289,7 @@ export abstract class Harness {
updatedAtM: null,
deletedAtM: null,
partitionId: null,
k8sNamespace: null,
};

const unkeyKeyAuth: KeyAuth = {
Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/lib/trpc/routers/workspace/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const createWorkspace = t.procedure
updatedAtM: null,
deletedAtM: null,
partitionId: null,
k8sNamespace: null,
};

await tx.insert(schema.workspaces).values(workspace);
Expand Down
41 changes: 34 additions & 7 deletions go/apps/ctrl/workflows/project/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,42 @@ func (s *Service) CreateProject(ctx restate.ObjectContext, req *hydrav1.CreatePr
return nil, err
}

k8sNamespace := workspace.K8sNamespace.String
// This should really be in a dedicated createWorkspace call I think,
// but this works for now
if k8sNamespace == "" {
k8sNamespace, err = restate.Run(ctx, func(runCtx restate.RunContext) (string, error) {
name := uid.Nano(12)
res, err := db.Query.UpdateWorkspaceK8sNamespace(runCtx, s.db.RW(), db.UpdateWorkspaceK8sNamespaceParams{
ID: workspace.ID,
K8sNamespace: sql.NullString{Valid: true, String: name},
})
if err != nil {
return "", err
}
affected, err := res.RowsAffected()
if err != nil {
return "", err
}
if affected != 1 {
return "", errors.New("failed to update workspace k8s namespace")
}
return name, nil
})
if err != nil {
return nil, err
}
}

projectID, err := restate.Run(ctx, func(runCtx restate.RunContext) (string, error) {
return uid.New(uid.ProjectPrefix), nil
}, restate.WithName("generate project ID"))
if err != nil {
return nil, err
}

_, err = restate.Run(ctx, func(runCtx restate.RunContext) (restate.Void, error) {
return restate.Void{}, db.Query.InsertProject(runCtx, s.db.RW(), db.InsertProjectParams{
err = restate.RunVoid(ctx, func(runCtx restate.RunContext) error {
return db.Query.InsertProject(runCtx, s.db.RW(), db.InsertProjectParams{
ID: projectID,
WorkspaceID: workspace.ID,
Name: req.Name,
Expand Down Expand Up @@ -81,8 +108,8 @@ func (s *Service) CreateProject(ctx restate.ObjectContext, req *hydrav1.CreatePr
return nil, err
}

_, err = restate.Run(ctx, func(runCtx restate.RunContext) (restate.Void, error) {
return restate.Void{}, db.Query.InsertEnvironment(runCtx, s.db.RW(), db.InsertEnvironmentParams{
err = restate.RunVoid(ctx, func(runCtx restate.RunContext) error {
return db.Query.InsertEnvironment(runCtx, s.db.RW(), db.InsertEnvironmentParams{
ID: environmentID,
WorkspaceID: workspace.ID,
ProjectID: projectID,
Expand All @@ -109,10 +136,10 @@ func (s *Service) CreateProject(ctx restate.ObjectContext, req *hydrav1.CreatePr
replicas = uint32(3)
}

_, err = restate.Run(ctx, func(runCtx restate.RunContext) (restate.Void, error) {
err = restate.RunVoid(ctx, func(runCtx restate.RunContext) error {
_, err := s.krane.CreateGateway(runCtx, connect.NewRequest(&kranev1.CreateGatewayRequest{
Gateway: &kranev1.GatewayRequest{
Namespace: workspace.ID,
Namespace: k8sNamespace,
WorkspaceId: workspace.ID,
GatewayId: gatewayID,
Image: "nginx:latest", // TODO
Expand All @@ -121,7 +148,7 @@ func (s *Service) CreateProject(ctx restate.ObjectContext, req *hydrav1.CreatePr
MemorySizeMib: uint64(256),
},
}))
return restate.Void{}, err
return err
}, restate.WithName("provision gateway"))

if err != nil {
Expand Down
42 changes: 21 additions & 21 deletions go/apps/krane/backend/kubernetes/deployment_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

"connectrpc.com/connect"
"github.com/unkeyed/unkey/go/apps/krane/backend/kubernetes/labels"
kranev1 "github.com/unkeyed/unkey/go/gen/proto/krane/v1"
"github.com/unkeyed/unkey/go/pkg/ptr"
appsv1 "k8s.io/api/apps/v1"
Expand Down Expand Up @@ -67,12 +68,14 @@ import (
// Returns DEPLOYMENT_STATUS_PENDING as pods may not be immediately scheduled
// and ready for traffic after creation.
func (k *k8s) CreateDeployment(ctx context.Context, req *connect.Request[kranev1.CreateDeploymentRequest]) (*connect.Response[kranev1.CreateDeploymentResponse], error) {
k8sDeploymentID := safeIDForK8s(req.Msg.GetDeployment().GetDeploymentId())
namespace := safeIDForK8s(req.Msg.GetDeployment().GetNamespace())

namespace := req.Msg.GetDeployment().GetNamespace()
deploymentID := req.Msg.GetDeployment().GetDeploymentId()
const krane = "krane"
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Extract krane constant to package level for reuse.

This constant is duplicated/missing in deployment_get.go. Based on the AI summary, there should be a shared constant in go/apps/krane/backend/kubernetes/krane.go. Consider using that or moving this to package level.

-func (k *k8s) CreateDeployment(...) {
-	const krane = "krane"
+const krane = "krane"
+
+func (k *k8s) CreateDeployment(...) {

Or import from the shared location if krane.go exports it:

#!/bin/bash
# Check if krane constant exists in krane.go
cat go/apps/krane/backend/kubernetes/krane.go 2>/dev/null || echo "File not found"
🤖 Prompt for AI Agents
In go/apps/krane/backend/kubernetes/deployment_create.go around line 74, the
local const `krane` is duplicated elsewhere; extract it to the package level or
reuse the shared constant in go/apps/krane/backend/kubernetes/krane.go: either
move the const declaration out of the function to the top of the package (so
other files can reference it) or, if krane.go already defines and exports the
constant, import/use that exported name instead; update deployment_create.go to
reference the package-level constant and remove the local duplicate, and adjust
visibility/name (exported vs unexported) in krane.go as needed so all files
compile.


k.logger.Info("creating deployment",
"namespace", namespace,
"deployment_id", k8sDeploymentID,
"deployment_id", deploymentID,
)

service, err := k.clientset.CoreV1().
Expand All @@ -88,23 +91,20 @@ func (k *k8s) CreateDeployment(ctx context.Context, req *connect.Request[kranev1
//nolint:exhaustruct
&corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: k8sDeploymentID,
Namespace: namespace,
GenerateName: "svc-",
Namespace: namespace,
Labels: map[string]string{
"unkey.deployment.id": k8sDeploymentID,
"unkey.managed.by": "krane",
},

Annotations: map[string]string{
"unkey.deployment.id": k8sDeploymentID,
labels.DeploymentID: deploymentID,
labels.ManagedBy: krane,
},
},

//nolint:exhaustruct
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeClusterIP, // Use ClusterIP for internal communication
Selector: map[string]string{
"unkey.deployment.id": k8sDeploymentID,
labels.DeploymentID: deploymentID,
labels.ManagedBy: krane,
},
ClusterIP: "None",
PublishNotReadyAddresses: true,
Expand All @@ -130,11 +130,11 @@ func (k *k8s) CreateDeployment(ctx context.Context, req *connect.Request[kranev1
//nolint: exhaustruct
&appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: k8sDeploymentID,
Namespace: namespace,
GenerateName: "dpl-",
Namespace: namespace,
Labels: map[string]string{
"unkey.deployment.id": k8sDeploymentID,
"unkey.managed.by": "krane",
labels.DeploymentID: deploymentID,
labels.ManagedBy: krane,
},
},

Expand All @@ -144,14 +144,14 @@ func (k *k8s) CreateDeployment(ctx context.Context, req *connect.Request[kranev1
Replicas: ptr.P(int32(req.Msg.GetDeployment().GetReplicas())), //nolint: gosec
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"unkey.deployment.id": k8sDeploymentID,
labels.DeploymentID: deploymentID,
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"unkey.deployment.id": k8sDeploymentID,
"unkey.managed.by": "krane",
labels.DeploymentID: deploymentID,
labels.ManagedBy: krane,
},
Annotations: map[string]string{},
},
Expand All @@ -171,7 +171,7 @@ func (k *k8s) CreateDeployment(ctx context.Context, req *connect.Request[kranev1
RestartPolicy: corev1.RestartPolicyAlways,
Containers: []corev1.Container{
{
Name: "todo",

Image: req.Msg.GetDeployment().GetImage(),
Comment on lines 172 to 175
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing required Name field for container.

Kubernetes requires every container to have a Name field. Without it, the StatefulSet creation will fail with a validation error.

 Containers: []corev1.Container{
 	{
-
+		Name:  "app",
 		Image: req.Msg.GetDeployment().GetImage(),
📝 Committable suggestion

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

Suggested change
Containers: []corev1.Container{
{
Name: "todo",
Image: req.Msg.GetDeployment().GetImage(),
Containers: []corev1.Container{
{
Name: "app",
Image: req.Msg.GetDeployment().GetImage(),
🤖 Prompt for AI Agents
In go/apps/krane/backend/kubernetes/deployment_create.go around lines 172-175,
the corev1.Container literal is missing the required Name field which will cause
Kubernetes validation to fail; set the container Name to a valid DNS-1123 label
derived from the deployment name (e.g. sanitize and lower-case
req.Msg.GetDeployment().GetName() and append "-container"), with a safe fallback
like "container" if the deployment name is empty, and ensure the resulting
string conforms to Kubernetes naming rules before assigning it to the
Container.Name field.

Ports: []corev1.ContainerPort{
{
Expand Down Expand Up @@ -219,7 +219,7 @@ func (k *k8s) CreateDeployment(ctx context.Context, req *connect.Request[kranev1
{
APIVersion: "apps/v1",
Kind: "StatefulSet",
Name: k8sDeploymentID,
Name: sfs.Name,
UID: sfs.UID,
},
}
Expand Down
67 changes: 53 additions & 14 deletions go/apps/krane/backend/kubernetes/deployment_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package kubernetes
import (
"context"
"fmt"
"strings"

"connectrpc.com/connect"
kranev1 "github.com/unkeyed/unkey/go/gen/proto/krane/v1"
Expand All @@ -15,32 +14,72 @@ import (
// DeleteDeployment removes a deployment and all associated Kubernetes resources.
//
// This method performs a complete cleanup of a deployment by removing both
// the Service and StatefulSet resources. The cleanup follows Kubernetes
// best practices for resource deletion with background propagation to
// ensure associated pods and other resources are properly terminated.
// the Service and StatefulSet resources. Resources are selected by their
// deployment-id label rather than by name, following Kubernetes best practices
// for resource management.
func (k *k8s) DeleteDeployment(ctx context.Context, req *connect.Request[kranev1.DeleteDeploymentRequest]) (*connect.Response[kranev1.DeleteDeploymentResponse], error) {
k8sDeploymentID := strings.ReplaceAll(req.Msg.GetDeploymentId(), "_", "-")
namespace := safeIDForK8s(req.Msg.GetNamespace())
deploymentID := req.Msg.GetDeploymentId()
namespace := req.Msg.GetNamespace()

k.logger.Info("deleting deployment",
"namespace", namespace,
"deployment_id", k8sDeploymentID,
"deployment_id", deploymentID,
)

// Create label selector for this deployment
labelSelector := fmt.Sprintf("deployment-id=%s", deploymentID)
Comment on lines +29 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Use the centralized label constant for consistency.

The label selector uses the hard-coded string "deployment-id" but the new labels package defines DeploymentID = "unkey.com/deployment.id". This inconsistency means resources created with the new label scheme won't be found during deletion.

+import "github.com/unkeyed/unkey/go/apps/krane/backend/kubernetes/labels"
+
 // Create label selector for this deployment
-labelSelector := fmt.Sprintf("deployment-id=%s", deploymentID)
+labelSelector := fmt.Sprintf("%s=%s", labels.DeploymentID, deploymentID)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In go/apps/krane/backend/kubernetes/deployment_delete.go around lines 29-30, the
label selector is built using the hard-coded "deployment-id" string which is
inconsistent with the centralized labels package; replace the hard-coded literal
with the labels.DeploymentID constant (e.g. fmt.Sprintf("%s=%s",
labels.DeploymentID, deploymentID)) and add the labels package to the file's
import list so deletion uses the same label key as resource creation.


//nolint: exhaustruct
err := k.clientset.CoreV1().Services(namespace).Delete(ctx, k8sDeploymentID, metav1.DeleteOptions{
deleteOptions := metav1.DeleteOptions{
PropagationPolicy: ptr.P(metav1.DeletePropagationBackground),
}

// List and delete Services with this deployment-id label
//nolint: exhaustruct
serviceList, err := k.clientset.CoreV1().Services(namespace).List(ctx, metav1.ListOptions{
LabelSelector: labelSelector,
})
if err != nil && !apierrors.IsNotFound(err) {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to delete service: %w", err))
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to list services: %w", err))
}

for _, service := range serviceList.Items {
k.logger.Debug("deleting service",
"name", service.Name,
"deployment_id", deploymentID,
)
err = k.clientset.CoreV1().Services(namespace).Delete(ctx, service.Name, deleteOptions)
if err != nil && !apierrors.IsNotFound(err) {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to delete service %s: %w", service.Name, err))
}
}

// List and delete StatefulSets with this deployment-id label
//nolint: exhaustruct
err = k.clientset.AppsV1().StatefulSets(namespace).Delete(ctx, k8sDeploymentID, metav1.DeleteOptions{
PropagationPolicy: ptr.P(metav1.DeletePropagationBackground),
statefulSetList, err := k.clientset.AppsV1().StatefulSets(namespace).List(ctx, metav1.ListOptions{
LabelSelector: labelSelector,
})
if err != nil && !apierrors.IsNotFound(err) {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to delete deployment: %w", err))
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to list statefulsets: %w", err))
}

for _, statefulSet := range statefulSetList.Items {
k.logger.Debug("deleting statefulset",
"name", statefulSet.Name,
"deployment_id", deploymentID,
)
err = k.clientset.AppsV1().StatefulSets(namespace).Delete(ctx, statefulSet.Name, deleteOptions)
if err != nil && !apierrors.IsNotFound(err) {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to delete statefulset %s: %w", statefulSet.Name, err))
}
}

k.logger.Info("deployment deleted successfully",
"namespace", namespace,
"deployment_id", deploymentID,
"services_deleted", len(serviceList.Items),
"statefulsets_deleted", len(statefulSetList.Items),
)

return connect.NewResponse(&kranev1.DeleteDeploymentResponse{}), nil
}
Loading
Loading