Skip to content

feat: openapi diffing api#4035

Merged
chronark merged 3 commits intomainfrom
09-29-feat_openapi_diffing_api
Sep 30, 2025
Merged

feat: openapi diffing api#4035
chronark merged 3 commits intomainfrom
09-29-feat_openapi_diffing_api

Conversation

@chronark
Copy link
Collaborator

@chronark chronark commented Sep 29, 2025

What does this PR do?

https://app.cushion.so/unkey/post/post_qMsRR1fEzWRN6dAjAg1n7

Refactors the OpenAPI diff viewer to use the new ChangelogEntry type from the proto package instead of the custom DiffChange type. This change integrates the frontend with the backend API for OpenAPI diff functionality, allowing users to compare API changes between deployments.

Key changes:

  • Updated the DiffViewer component to accept changelog: ChangelogEntry[] instead of the custom diffData type
  • Removed unused types and constants files related to the old diff implementation
  • Updated the TRPC router to use the control plane API for fetching OpenAPI diffs
  • Added proper authentication to control plane API calls
  • Enhanced the deployment workflow to scrape and store OpenAPI specs from deployed services
  • Fixed the gateway configuration to include OpenAPI specs for validation

Type of change

  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)

How should this be tested?

  • Deploy two versions of an API with different OpenAPI specs
  • Navigate to the diff comparison page and verify that changes are properly displayed
  • Check that the severity levels, operations, and paths are correctly categorized
  • Verify that filtering and search functionality works with the new data structure

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

@vercel
Copy link

vercel bot commented Sep 29, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
dashboard Ready Ready Preview Comment Sep 29, 2025 6:13pm
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
engineering Ignored Ignored Preview Sep 29, 2025 6:13pm

@changeset-bot
Copy link

changeset-bot bot commented Sep 29, 2025

⚠️ No Changeset found

Latest commit: 72b139b

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 29, 2025

📝 Walkthrough

Walkthrough

Refactors the dashboard diff UI to accept a changelog: ChangelogEntry[] (removing legacy DiffData/types/constants), updates routing to include workspace slug, integrates TRPC calls with the control plane using CTRL_API_KEY, and adds OpenAPI base64 handling/scraping and related Kubernetes/CLI/proto changes.

Changes

Cohort / File(s) Summary of changes
Dashboard Diff UI refactor
apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/components/client.tsx, .../constants.ts, .../page.tsx, .../types.ts
Replace diffData: DiffData with changelog: ChangelogEntry[]; update all references, memo deps, filters, groupings, rendering, and keyboard/button interactions; remove sampleDiffData and DiffChange/DiffData types; page passes changelog={diffData.changes}.
Compare route update
apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/page.tsx
Prefix compare navigation with /{workspaceSlug} when routing to comparisons.
TRPC → Control plane auth & calls
apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts, .../promote.ts, .../rollback.ts
Add control-plane client using CTRL_URL and CTRL_API_KEY; inject Authorization header; validate env vars; fetch deployments in batch and call getOpenApiDiff; unify error handling and return shape.
Control plane OpenAPI diff & utils
go/apps/ctrl/services/openapi/get_diff.go, go/apps/ctrl/services/openapi/utils.go
Rename loader to loadOpenApiSpec(deploymentID); base64-decode stored specs before parsing; adjust callers and error messages.
Deploy workflow: OpenAPI scrape & gateway config
go/apps/ctrl/services/deployment/deploy_workflow.go
Reduce initial resources; concurrently scrape /openapi.yaml from instances, base64-encode and upsert spec; build gateway config inline (including ValidationConfig with base64 spec); add conditional logging and non-fatal error paths.
Gateway validation changes
go/apps/gw/services/validation/validator.go
Decode base64 OpenAPI spec before parsing; add decode error handling; import encoding/base64.
Krane logging & flag change
go/apps/krane/backend/kubernetes/eviction.go, .../get_deployment.go, go/cmd/krane/main.go
Add eviction logs; remove a deployment-fetch log; remove explicit default from deployment-eviction-ttl flag declaration.
CLI Duration flag support & tests
go/pkg/cli/flag.go, .../flag_test.go, .../help.go
Add DurationFlag support for required/env/default/validate, parse env with time.ParseDuration, add tests for duration behaviors, and show duration env/default in help.
Kubernetes manifests
go/k8s/manifests/ctrl.yaml, .../dashboard.yaml, .../krane.yaml
Add UNKEY_API_KEY/CTRL_API_KEY env vars; expand/restructure dashboard Deployment and add Service resource; change Krane TTL env to 10m.
Protobuf & TS exports
go/proto/ctrl/v1/openapi.proto, go/proto/partition/v1/gateway.proto, internal/proto/src/index.ts
Rename old_version_id/new_version_idold_deployment_id/new_deployment_id; add doc comment for base64 openapi_spec; re-export OpenAPI protobuf types in TS.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as Dashboard User
  participant UI as Dashboard UI
  participant TRPC as TRPC Router
  participant CTRL as Control Plane (OpenAPI Service)

  User->>UI: Open Compare View
  UI->>TRPC: getOpenApiDiff({fromId, toId})
  TRPC->>CTRL: GetOpenApiDiff(old_deployment_id, new_deployment_id)\n(Auth: Bearer CTRL_API_KEY)
  CTRL-->>TRPC: { changes[], summary, hasBreakingChanges }
  TRPC-->>UI: { changes[], ... }
  UI->>UI: Render DiffViewer(changelog=changes)
Loading
sequenceDiagram
  autonumber
  participant Deploy as Deploy Workflow
  participant K8s as Cluster
  participant App as App Pods
  participant DB as Control DB
  participant GW as Gateway Service

  rect rgba(200,230,255,0.25)
    note right of Deploy: OpenAPI scrape (new)
    Deploy->>K8s: Create deployment (1 replica, reduced resources)
    Deploy->>App: Probe /openapi.yaml (concurrent)
    App-->>Deploy: 200 YAML? (optional)
    Deploy->>Deploy: Base64-encode spec
    Deploy->>DB: Upsert OpenAPI spec
  end

  rect rgba(220,255,220,0.25)
    note right of Deploy: Gateway config (updated)
    Deploy->>GW: Upsert gateway config { validation{ openapi_spec (base64) } }
    GW->>GW: Init validator (decode base64 → parse)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

Core Team, UI

Suggested reviewers

  • perkinsjr
  • ogzhanolguncu
  • imeyer
  • MichaelUnkey
  • mcstepp

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "feat: openapi diffing api" is directly related to the main changes in the pull request. The changeset introduces a new OpenAPI diffing API by refactoring the diff viewer to use ChangelogEntry types from the proto package, updating the TRPC router to fetch diffs from the control plane API, and enhancing the deployment workflow to scrape and store OpenAPI specs. The title clearly conveys the primary feature being added: an API for OpenAPI diffing functionality. While it could be slightly more specific about the integration aspect, it accurately summarizes the core change from a developer's perspective.
Description Check ✅ Passed The pull request description is comprehensive and follows the repository template structure well. It includes a clear summary referencing the Cushion post, explains the refactoring from DiffChange to ChangelogEntry types, and lists all key changes. The type of change section is properly marked as both enhancement and new feature. The "How should this be tested" section provides specific, actionable test scenarios covering deployment comparison, diff display verification, and functionality testing. All required checklist items are marked as complete, demonstrating the author followed the contribution guidelines thoroughly.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 09-29-feat_openapi_diffing_api

📜 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 7c80561 and 0b70eff.

📒 Files selected for processing (1)
  • go/apps/ctrl/services/openapi/utils.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • go/apps/ctrl/services/openapi/utils.go
⏰ 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: Test Go API Local / Test

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

Tip

🧪 Early access (models): enabled

We are currently testing Sonnet 4.5 code review models, which should lead to better review quality. However, this model may result in higher noise levels in the review comments. Please disable the early access features if the noise level causes any inconvenience.

Note:

  • Public repositories are always opted into early access features.
  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
go/apps/ctrl/services/deployment/deploy_workflow.go (2)

331-391: Error handling bug: earlier domain errors get overwritten

err is reassigned inside the loop and inspected only after the loop, so failures in earlier iterations are dropped if later iterations succeed.

-	for _, domain := range allDomains {
-		err = hydra.StepVoid(ctx, fmt.Sprintf("create-domain-entry-%s", domain.domain), func(stepCtx context.Context) error {
+	for _, domain := range allDomains {
+		if err := hydra.StepVoid(ctx, fmt.Sprintf("create-domain-entry-%s", domain.domain), func(stepCtx context.Context) error {
 			...
-		})
+		}); err != nil {
+			return err
+		}
 	}
-
-	if err != nil {
-		return err
-	}

230-233: Align VM resource metrics with actual deployment resources

You deploy with 512m CPU and 512Mi memory, but persist 1000/1024 — skewing capacity/analytics.

-					CpuMillicores: 1000,   // TODO derive from spec
-					MemoryMb:      1024,   // TODO derive from spec
+					CpuMillicores: 512,    // TODO derive from deployment request/spec
+					MemoryMb:      512,    // TODO derive from deployment request/spec

Alternatively, plumb these from deploymentReq.Deployment to avoid drift.

🧹 Nitpick comments (29)
apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/page.tsx (1)

33-35: Guard workspace slug before building the route

useParams() can yield string | string[] | undefined, so if workspaceSlug is ever absent (SSR fallback, malformed URL, tests), we’d push /undefined/.... Fold the slug presence check into the existing guard before routing:

-  const handleCompare = () => {
-    if (selectedFromDeployment && selectedToDeployment) {
-      router.push(
-        `/${params?.workspaceSlug}/projects/${projectId}/diff/${selectedFromDeployment}/${selectedToDeployment}`,
-      );
-    }
-  };
+  const handleCompare = () => {
+    const workspaceSlug = params?.workspaceSlug as string | undefined;
+    if (!workspaceSlug || !selectedFromDeployment || !selectedToDeployment) {
+      return;
+    }
+    router.push(
+      `/${workspaceSlug}/projects/${projectId}/diff/${selectedFromDeployment}/${selectedToDeployment}`,
+    );
+  };
go/apps/krane/backend/kubernetes/eviction.go (1)

5-25: Prefer structured logging over fmt.Sprintf here

k.logger.Info already supports key-value pairs (see the rest of this file), so we can drop the extra fmt dependency and keep logs queryable:

-import (
-	"context"
-	"fmt"
-	"time"
+import (
+	"context"
+	"time"
@@
-	k.logger.Info(fmt.Sprintf("Krane setup to auto-evict deployments after %s", ttl.String()))
+	k.logger.Info("Krane setup to auto-evict deployments", "ttl", ttl.String())
go/k8s/manifests/dashboard.yaml (1)

55-66: Prefer a cheap health endpoint for probes.

Probing “/” can be expensive for SSR apps and may flap under load. Expose a lightweight “/healthz” (or similar) and point both probes there.

go/apps/gw/services/validation/validator.go (4)

114-116: Cache key risks staleness; include spec hash.

If a deployment’s spec is upserted post-deploy, keying only by deploymentID can serve a stale validator. Include a content hash.

-	// Create cache key based on deployment ID (spec content could be large)
-	cacheKey := fmt.Sprintf("validator:%s", deploymentID)
+	// Include spec hash to avoid stale validators after upserts
+	sum := sha256.Sum256([]byte(spec))
+	cacheKey := fmt.Sprintf("validator:%s:%x", deploymentID, sum)

And add:

 import (
 	"context"
 	"encoding/base64"
 	"fmt"
+	"crypto/sha256"
 	"net/http"

131-137: Base64 decoding: be tolerant to whitespace/padding.

Trim whitespace and optionally fall back to RawStdEncoding for specs without padding.

-	b, err := base64.StdEncoding.DecodeString(spec)
+	spec = strings.TrimSpace(spec)
+	b, err := base64.StdEncoding.DecodeString(spec)
+	if err != nil {
+		if rb, rerr := base64.RawStdEncoding.DecodeString(spec); rerr == nil {
+			b, err = rb, nil
+		}
+	}
 	if err != nil {
 		return nil, fault.Wrap(err,
 			fault.Internal("failed to parse OpenAPI document as base64"),
 		)
 	}

Also import:

-	"fmt"
+	"fmt"
+	"strings"

79-93: *Avoid shadowing the imported errors package and drop redundant (v).

Rename local variable and call method on the pointer directly for clarity.

-	valid, errors := (*v).ValidateHttpRequest(req)
+	valid, valErrs := v.ValidateHttpRequest(req)
 ...
-	validationErr := s.buildValidationError(errors)
+	validationErr := s.buildValidationError(valErrs)

126-131: Reduce duplicate validator builds under concurrency (optional).

Two simultaneous cache misses for the same key will both build validators. Consider singleflight for creation.

I can wire a singleflight.Group into Service if you want.

apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts (1)

75-81: Optional: map control-plane errors to user-facing codes.

Currently all failures become 500. If ctrl returns “not found/invalid argument”, consider mapping to NOT_FOUND/BAD_REQUEST to improve UX.

apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/components/client.tsx (6)

69-70: Normalize HTTP methods to uppercase for filters and colors.

Operation case may vary; normalize to avoid mismatches.

-    const operations = [...new Set(changelog.map((c) => c.operation))];
+    const operations = [...new Set(changelog.map((c) => c.operation.toUpperCase()))];
-      if (filters.operation !== "all" && change.operation !== filters.operation) {
+      if (filters.operation !== "all" && change.operation.toUpperCase() !== filters.operation) {
         return false;
       }
-  const getOperationColor = (operation: string) => {
+  const getOperationColor = (operation: string) => {
+    const key = operation.toUpperCase();
     const colors: Record<string, string> = {
       GET: "bg-gray-100 text-gray-800 border border-gray-200",
       POST: "bg-brand text-brand-foreground border border-gray-200",
       PUT: "bg-amber-2 text-amber-11 border border-gray-200",
       PATCH: "bg-gray-200 text-gray-700 border border-gray-300",
       DELETE: "bg-red-2 text-red-11 border border-gray-200",
     };
-    return colors[operation] || "bg-gray-100 text-gray-800 border border-gray-200";
+    return colors[key] || "bg-gray-100 text-gray-800 border border-gray-200";
   };

No UI change, but filters and badges will be consistent across data sources. Based on learnings.

Also applies to: 81-83, 146-155, 529-533, 864-867, 896-901


552-555: Remove duplicated change text.

The timeline and change item both repeat change.text.

-                  {change.text && (
-                    <p className="text-xs text-content-subtle italic bg-background p-2 rounded">
-                      {change.text}
-                    </p>
-                  )}
-                                            {change.text && (
-                                              <p className="text-xs text-content-subtle italic bg-background/70 p-2 rounded border-l-2 border-gray-300">
-                                                💡 {change.text}
-                                              </p>
-                                            )}

Also applies to: 936-940


559-560: Fix “Section” label; show operationId when available.

There’s no section field now; display operationId instead.

-                      ID: {change.id} • Section: {change.id}
+                      ID: {change.id}
+                      {change.operationId ? ` • Operation: ${change.operationId}` : ""}
-                                                <span>Section: {change.id}</span>
+                                                <span>Operation: {change.operationId ?? "—"}</span>

Also applies to: 946-947


52-54: Default expanded paths to none (POC defaults leak UI bias).

Starting expanded for “/users” endpoints can be confusing for other specs.

-  const [expandedPaths, setExpandedPaths] = useState<Set<string>>(
-    new Set(["/users", "/users/{userId}"]),
-  );
+  const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set());

If you want a sensible default, expand the first N paths from stats.paths. Based on learnings.


157-168: Icon heuristic should not rely on id contents.

ids aren’t semantic. Derive icons from text (or a backend-provided kind) instead.

Minimal change: switch getChangeIcon() to check change.text and update its call sites.

-  const getChangeIcon = (changeId: string) => {
-    if (changeId.includes("added") || changeId.includes("new")) {
+  const getChangeIcon = (t: string) => {
+    const s = t.toLowerCase();
+    if (s.includes("added") || s.includes("new")) {
       return <Plus className="w-4 h-4 text-gray-600" />;
     }
-    if (changeId.includes("removed") || changeId.includes("deleted")) {
+    if (s.includes("removed") || s.includes("deleted")) {
       return <Minus className="w-4 h-4 text-alert" />;
     }
-    if (changeId.includes("changed") || changeId.includes("modified")) {
+    if (s.includes("changed") || s.includes("modified")) {
       return <Edit className="w-4 h-4 text-brand" />;
     }
     return <Info className="w-4 h-4 text-gray-600" />;
   };
-                    {getChangeIcon(change.id)}
+                    {getChangeIcon(change.text)}
-                                            {getChangeIcon(change.id)}
+                                            {getChangeIcon(change.text)}

Longer-term: have the control plane include a “kind” enum for each entry. Based on learnings.

Also applies to: 549-550, 929-931


38-46: Empty-state copy could be clearer (optional).

Consider “No changes found for the selected deployments” and show a CTA to pick different deployments.

Also applies to: 455-470

apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/page.tsx (1)

309-309: Consider defaulting to an empty changelog array.

Because the count above already guards with optional chaining, the control-plane response can leave diffData.changes undefined. Normalizing to [] here keeps DiffViewer’s downstream memoization logic working without extra null checks.

-              <DiffViewer changelog={diffData.changes} />
+              <DiffViewer changelog={diffData.changes ?? []} />
go/apps/ctrl/services/openapi/get_diff.go (2)

16-27: Backward‑compat shim for request IDs (if you re‑add deprecated fields)

If you reintroduce deprecated old_version_id/new_version_id in the proto for JSON compatibility, prefer reading either field here to avoid breaking older clients.

- // Load old version spec
- oldSpec, err := s.loadOpenApiSpec(ctx, req.Msg.OldDeploymentId)
+ // Load old version spec
+ oldID := req.Msg.OldDeploymentId
+ if oldID == "" { oldID = req.Msg.OldVersionId } // deprecated field fallback
+ oldSpec, err := s.loadOpenApiSpec(ctx, oldID)
...
- // Load new version spec
- newSpec, err := s.loadOpenApiSpec(ctx, req.Msg.NewDeploymentId)
+ // Load new version spec
+ newID := req.Msg.NewDeploymentId
+ if newID == "" { newID = req.Msg.NewVersionId } // deprecated field fallback
+ newSpec, err := s.loadOpenApiSpec(ctx, newID)

38-45: Harden base64 decoding for OpenAPI specs
Trim whitespace, strip an optional data:...;base64, prefix, and explicitly error on empty payload before loading in get_diff.go. Replace direct calls to base64.StdEncoding.DecodeString for both oldSpec and newSpec with:

- b1, err := base64.StdEncoding.DecodeString(oldSpec)
+ b1, err := decodeBase64Spec(oldSpec)
  if err != nil {
    // existing error handling
  }
+ if len(b1) == 0 {
+   return nil, connect.NewError(connect.CodeInvalidArgument, fault.Wrap(fmt.Errorf("empty spec"),
+     fault.Internal("old version spec is empty after decoding"),
+     fault.Public("Empty OpenAPI specification in old version"),
+   ))
+ }
  s1, err := loader.LoadFromData(b1)

(and the same pattern for newSpec)

Add this helper:

import "strings"

func decodeBase64Spec(spec string) ([]byte, error) {
  s := strings.TrimSpace(spec)
  if strings.HasPrefix(s, "data:") {
    if idx := strings.LastIndex(s, ","); idx >= 0 {
      s = s[idx+1:]
    }
  }
  return base64.StdEncoding.DecodeString(s)
}
go/proto/ctrl/v1/openapi.proto (1)

12-19: Model severity as an enum for clarity and type‑safety

int32 level is opaque. Define an enum (e.g., CHANGE_LEVEL_INFO/WARN/ERR) and use it here. It improves readability across languages and prevents invalid values.

+enum ChangeLevel {
+  CHANGE_LEVEL_UNSPECIFIED = 0;
+  CHANGE_LEVEL_INFO = 1;
+  CHANGE_LEVEL_WARN = 2;
+  CHANGE_LEVEL_ERR = 3;
+}
 message ChangelogEntry {
   string id = 1;
   string text = 2;
-  int32 level = 3;
+  ChangeLevel level = 3;
   string operation = 4;
   optional string operation_id = 5;
   string path = 6;
 }
go/pkg/cli/flag_test.go (2)

33-40: Use t.Setenv to avoid cross‑test env leakage

Replace manual os.Setenv + defer Unsetenv with t.Setenv, which is cleaner and automatically restores state, especially important if tests become parallelized.

Example:

- os.Setenv("TEST_DURATION", "2h30m")
- defer os.Unsetenv("TEST_DURATION")
+ t.Setenv("TEST_DURATION", "2h30m")

Also applies to: 43-59, 71-87, 131-138, 155-161, 164-187, 220-227, 230-238, 241-256, 288-296, 324-338, 539-546


198-203: Prefer using ErrValidationFailed in TestDurationFlag_InvalidDuration instead of a hard-coded string
Replace the literal "validation failed" with ErrValidationFailed.Error() for consistency with other validation-error tests.
Optionally, if you’d rather align parse errors with the ErrInvalid*Value pattern, introduce an ErrInvalidDurationValue constant and have DurationFlag.Parse return it (updating tests accordingly).

go/apps/ctrl/services/openapi/utils.go (1)

18-24: Treat empty string spec as missing

A Valid=true but empty string yields base64 decode to empty and later parse error. Fail early here with a clearer error.

- if !deployment.OpenapiSpec.Valid {
+ if !deployment.OpenapiSpec.Valid || strings.TrimSpace(deployment.OpenapiSpec.String) == "" {
   return "", fault.New("deployment has no OpenAPI spec stored",
     fault.Public("OpenAPI specification not available for this deployment"),
   )
 }

Add import:

 import (
   "context"
+  "strings"
   ...
 )
go/cmd/krane/main.go (1)

38-39: Clarify that 0 disables eviction in help text

Make the flag behavior explicit for operators.

- cli.Duration("deployment-eviction-ttl", "Automatically delete deployments after some time. Use go duration formats such as 2h30m", cli.EnvVar("UNKEY_DEPLOYMENT_EVICTION_TTL")),
+ cli.Duration("deployment-eviction-ttl", "Automatically delete deployments after some time (0 disables). Use Go duration formats, e.g. 2h30m", cli.EnvVar("UNKEY_DEPLOYMENT_EVICTION_TTL")),
go/apps/ctrl/services/deployment/deploy_workflow.go (3)

202-205: Avoid logging full response message as “status”

"status", resp.Msg likely dumps the whole proto. Prefer a concise field (e.g., instance counts) to keep logs structured.

-			w.logger.Info("deployment status",
-				"deployment_id", req.DeploymentID,
-				"status", resp.Msg,
-			)
+			w.logger.Info("deployment poll",
+				"deployment_id", req.DeploymentID,
+				"instances", len(resp.Msg.GetInstances()),
+			)

408-434: Reuse helper to avoid duplicated gateway config construction

This block duplicates createGatewayConfig() below. Use the helper and set ValidationConfig when openapiSpec exists to keep one source of truth.

-			// Create VM protobuf objects for gateway config
-			gatewayConfig := &partitionv1.GatewayConfig{
-				Deployment: &partitionv1.Deployment{
-					Id:        req.DeploymentID,
-					IsEnabled: true,
-				},
-				Vms: make([]*partitionv1.VM, len(createdInstances)),
-			}
-
-			for i, vm := range createdInstances {
-				gatewayConfig.Vms[i] = &partitionv1.VM{
-					Id: vm.Id,
-				}
-			}
-
-			// Only add AuthConfig if we have a KeyspaceID
-			if req.KeyspaceID != "" {
-				gatewayConfig.AuthConfig = &partitionv1.AuthConfig{
-					KeyAuthId: req.KeyspaceID,
-				}
-			}
+			gatewayConfig, _ := createGatewayConfig(req.DeploymentID, req.KeyspaceID, createdInstances)
 
 			if openapiSpec != "" {
 				gatewayConfig.ValidationConfig = &partitionv1.ValidationConfig{
 					OpenapiSpec: openapiSpec,
 				}
 			}

26-26: Namespace TODO

Hardcoding unkey will bite multitenancy/workspace scoping later. Consider deriving namespace from workspace (or config) soon.

go/pkg/cli/flag.go (4)

90-105: Unify DurationFlag parse/validate order

Parse() validates before parsing, but env-var handling parses first; other numeric flags (int/float/bool) also parse first. For consistency and clearer error precedence, parse first, then validate.

 func (f *DurationFlag) Parse(value string) error {
-	// Run validation if provided
-	if f.validate != nil {
-		if err := f.validate(value); err != nil {
-			return newValidationError(f.name, err)
-		}
-	}
-	var err error
-	f.value, err = time.ParseDuration(value)
-	if err != nil {
-		return newValidationError(f.name, err)
-	}
+	parsed, err := time.ParseDuration(value)
+	if err != nil {
+		return newValidationError(f.name, err)
+	}
+	// Run validation if provided (on the original string, like other flags)
+	if f.validate != nil {
+		if err := f.validate(value); err != nil {
+			return newValidationError(f.name, err)
+		}
+	}
+	f.value = parsed
 	f.set = true
 	return nil
 }

107-112: Comment nit: “string value” → “duration value”

The receiver returns time.Duration. Update the comment for clarity.

-// Value returns the current string value
+// Value returns the current duration value

453-463: Comment nit: default text

time.Duration(0) comment says “Default to false”. Suggest “Default to 0”.

-		value: time.Duration(0), // Default to false
+		value: time.Duration(0), // Default to 0

363-410: Ergonomics: consider string defaults for Duration

As a nicety, allow Default("500ms") for *DurationFlag by parsing strings when provided.

Example pattern:

case *DurationFlag:
    switch v := value.(type) {
    case time.Duration:
        flag.value = v
    case string:
        if d, err := time.ParseDuration(v); err == nil {
            flag.value = d
        } else { err = fmt.Errorf("default value for duration flag '%s' invalid: %v", flag.name, err) }
    default:
        err = fmt.Errorf("default value for duration flag '%s' must be time.Duration or string, got %T", flag.name, value)
    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3b4062 and 7c80561.

⛔ Files ignored due to path filters (4)
  • go/gen/proto/ctrl/v1/openapi.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • go/gen/proto/partition/v1/gateway.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • internal/proto/generated/ctrl/v1/openapi_pb.ts is excluded by !**/generated/**
  • internal/proto/generated/partition/v1/gateway_pb.ts is excluded by !**/generated/**
📒 Files selected for processing (24)
  • apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/components/client.tsx (20 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/constants.ts (0 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/types.ts (0 hunks)
  • apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/page.tsx (1 hunks)
  • apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts (2 hunks)
  • apps/dashboard/lib/trpc/routers/deploy/deployment/promote.ts (2 hunks)
  • apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts (1 hunks)
  • go/apps/ctrl/services/deployment/deploy_workflow.go (5 hunks)
  • go/apps/ctrl/services/openapi/get_diff.go (4 hunks)
  • go/apps/ctrl/services/openapi/utils.go (1 hunks)
  • go/apps/gw/services/validation/validator.go (3 hunks)
  • go/apps/krane/backend/kubernetes/eviction.go (2 hunks)
  • go/apps/krane/backend/kubernetes/get_deployment.go (0 hunks)
  • go/cmd/krane/main.go (1 hunks)
  • go/k8s/manifests/ctrl.yaml (1 hunks)
  • go/k8s/manifests/dashboard.yaml (1 hunks)
  • go/k8s/manifests/krane.yaml (1 hunks)
  • go/pkg/cli/flag.go (6 hunks)
  • go/pkg/cli/flag_test.go (2 hunks)
  • go/pkg/cli/help.go (2 hunks)
  • go/proto/ctrl/v1/openapi.proto (1 hunks)
  • go/proto/partition/v1/gateway.proto (1 hunks)
  • internal/proto/src/index.ts (1 hunks)
💤 Files with no reviewable changes (3)
  • apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/types.ts
  • go/apps/krane/backend/kubernetes/get_deployment.go
  • apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/constants.ts
🧰 Additional context used
🧠 Learnings (10)
📓 Common learnings
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/getOpenApiDiff.ts:110-147
Timestamp: 2025-07-25T19:11:00.208Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/getOpenApiDiff.ts, the user mcstepp prefers to keep mock data fallbacks in POC/demonstration code for simplicity, even if it wouldn't be production-ready. This aligns with the PR being work-in-progress for demonstration purposes.
📚 Learning: 2025-09-01T16:08:10.327Z
Learnt from: imeyer
PR: unkeyed/unkey#3899
File: go/proto/metald/v1/deployment.proto:45-52
Timestamp: 2025-09-01T16:08:10.327Z
Learning: In the unkey/unkey repository metald service, the host:port pairs in Instance messages within GetDeploymentResponse are consumed by upstream services in a tightly controlled manner, with the host:port being mapped from DNS entries rather than being used for arbitrary client connections.

Applied to files:

  • go/k8s/manifests/krane.yaml
📚 Learning: 2025-09-25T18:49:11.425Z
Learnt from: mcstepp
PR: unkeyed/unkey#4010
File: apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts:39-44
Timestamp: 2025-09-25T18:49:11.425Z
Learning: In apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts and similar files, mcstepp prefers to keep the demo API key authentication simple without additional validation complexity, since it's temporary code that will be replaced after the demo phase.

Applied to files:

  • apps/dashboard/lib/trpc/routers/deploy/deployment/promote.ts
  • apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts
  • apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts
📚 Learning: 2025-09-12T18:11:33.481Z
Learnt from: mcstepp
PR: unkeyed/unkey#3952
File: apps/dashboard/lib/trpc/routers/deploy/rollback.ts:23-24
Timestamp: 2025-09-12T18:11:33.481Z
Learning: In apps/dashboard/lib/trpc/routers/deploy/rollback.ts, the CTRL_URL environment variable should fail fast with a clear error message if missing in non-development environments, rather than defaulting to localhost which can mask production configuration issues.

Applied to files:

  • apps/dashboard/lib/trpc/routers/deploy/deployment/promote.ts
  • apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts
  • apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts
📚 Learning: 2025-07-25T19:11:00.208Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/getOpenApiDiff.ts:110-147
Timestamp: 2025-07-25T19:11:00.208Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/getOpenApiDiff.ts, the user mcstepp prefers to keep mock data fallbacks in POC/demonstration code for simplicity, even if it wouldn't be production-ready. This aligns with the PR being work-in-progress for demonstration purposes.

Applied to files:

  • apps/dashboard/lib/trpc/routers/deploy/deployment/promote.ts
  • apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts
  • apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts
📚 Learning: 2025-07-25T19:09:43.284Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/list.ts:11-11
Timestamp: 2025-07-25T19:09:43.284Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/list.ts, the listDeployments procedure intentionally queries the versions table rather than a deployments table. The user mcstepp indicated that renaming the table would require a database migration, which was deferred for the current PR focused on UI features.

Applied to files:

  • apps/dashboard/lib/trpc/routers/deploy/deployment/promote.ts
  • apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts
📚 Learning: 2025-09-24T18:57:34.843Z
Learnt from: mcstepp
PR: unkeyed/unkey#4010
File: QUICKSTART-DEPLOY.md:17-17
Timestamp: 2025-09-24T18:57:34.843Z
Learning: In the Unkey deployment platform, API key environment variables use component-specific naming but share the same secret value: UNKEY_API_KEY for the ctrl service (validator), API_KEY for the CLI client, and CTRL_API_KEY for the dashboard client. The ctrl service acts as the source of truth for validation.

Applied to files:

  • go/k8s/manifests/ctrl.yaml
📚 Learning: 2025-09-16T19:08:44.174Z
Learnt from: Flo4604
PR: unkeyed/unkey#3980
File: go/k8s/manifests/dashboard.yaml:41-42
Timestamp: 2025-09-16T19:08:44.174Z
Learning: For local development Kubernetes manifests (typically indicated by paths containing "local" or environment variables like UNKEY_REGION: "local"), hardcoded generic credentials in environment variables are acceptable for convenience. Security recommendations about using Secrets should be reserved for production or staging environments.

Applied to files:

  • go/k8s/manifests/ctrl.yaml
📚 Learning: 2025-07-28T20:38:53.244Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx:322-341
Timestamp: 2025-07-28T20:38:53.244Z
Learning: In apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx, mcstepp prefers to keep hardcoded endpoint logic in the getDiffType function during POC phases for demonstrating diff functionality, rather than implementing a generic diff algorithm. This follows the pattern of keeping simplified implementations for demonstration purposes.

Applied to files:

  • apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/page.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/components/client.tsx
  • apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/page.tsx
📚 Learning: 2025-07-15T14:45:18.920Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3564
File: go/cmd/cli/commands/deploy/flags.go:17-20
Timestamp: 2025-07-15T14:45:18.920Z
Learning: In the go/cmd/cli/commands/deploy/ directory, ogzhanolguncu prefers to keep potentially temporary features (like UNKEY_DOCKER_REGISTRY environment variable) undocumented in help text if they might be deleted in the future, to avoid documentation churn.

Applied to files:

  • go/cmd/krane/main.go
🧬 Code graph analysis (14)
go/pkg/cli/help.go (1)
go/pkg/cli/flag.go (2)
  • DurationFlag (84-88)
  • EnvVar (320-339)
go/apps/ctrl/services/openapi/get_diff.go (1)
go/pkg/fault/wrap.go (3)
  • Wrap (25-67)
  • Internal (75-89)
  • Public (97-111)
apps/dashboard/lib/trpc/routers/deploy/deployment/promote.ts (1)
apps/dashboard/lib/env.ts (1)
  • env (3-52)
go/apps/gw/services/validation/validator.go (1)
go/pkg/fault/wrap.go (2)
  • Wrap (25-67)
  • Internal (75-89)
go/pkg/cli/flag_test.go (2)
go/pkg/cli/flag.go (4)
  • Duration (454-490)
  • Default (364-416)
  • EnvVar (320-339)
  • Flag (20-27)
go/pkg/cli/command.go (1)
  • Command (24-40)
apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts (3)
apps/dashboard/lib/env.ts (1)
  • env (3-52)
internal/proto/generated/ctrl/v1/openapi_pb.ts (1)
  • OpenApiService (194-203)
internal/db/src/schema/deployments.ts (1)
  • deployments (9-56)
apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts (2)
apps/dashboard/lib/env.ts (1)
  • env (3-52)
internal/proto/generated/ctrl/v1/deployment_pb.ts (1)
  • DeploymentService (569-610)
apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/components/client.tsx (1)
internal/proto/generated/ctrl/v1/openapi_pb.ts (1)
  • ChangelogEntry (44-74)
apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/page.tsx (1)
apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/components/client.tsx (1)
  • DiffViewer (32-986)
go/apps/ctrl/services/openapi/utils.go (1)
go/apps/ctrl/services/openapi/service.go (1)
  • Service (9-13)
go/apps/krane/backend/kubernetes/eviction.go (1)
go/pkg/repeat/every.go (1)
  • Every (11-37)
go/pkg/cli/flag.go (1)
go/pkg/cli/command.go (1)
  • Exit (325-329)
go/cmd/krane/main.go (1)
go/pkg/cli/flag.go (2)
  • Duration (454-490)
  • EnvVar (320-339)
go/apps/ctrl/services/deployment/deploy_workflow.go (4)
go/pkg/hydra/step.go (2)
  • Step (67-277)
  • StepVoid (303-311)
go/pkg/db/deployment_update_openapi_spec.sql_generated.go (1)
  • UpdateDeploymentOpenapiSpecParams (19-23)
go/gen/proto/partition/v1/gateway.pb.go (15)
  • GatewayConfig (26-37)
  • GatewayConfig (50-50)
  • GatewayConfig (65-67)
  • Deployment (104-110)
  • Deployment (123-123)
  • Deployment (138-140)
  • VM (208-213)
  • VM (226-226)
  • VM (241-243)
  • AuthConfig (253-258)
  • AuthConfig (271-271)
  • AuthConfig (286-288)
  • ValidationConfig (298-304)
  • ValidationConfig (317-317)
  • ValidationConfig (332-334)
go/pkg/db/models_generated.go (1)
  • Deployment (569-587)
🪛 Checkov (3.2.334)
go/k8s/manifests/dashboard.yaml

[medium] 42-43: Basic Auth Credentials

(CKV_SECRET_4)

⏰ 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: Test Go API Local / Test
  • GitHub Check: Test Packages / Test
  • GitHub Check: Build / Build
🔇 Additional comments (10)
go/proto/partition/v1/gateway.proto (1)

45-46: Documentation clarification appreciated

Calling out that openapi_spec expects a Base64-encoded document (or empty string) aligns the proto with the new control plane contract. Thanks for tightening the docs.

apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts (1)

25-41: Control plane auth precondition looks solid

Failing fast when either CTRL_URL or CTRL_API_KEY is missing and injecting the Bearer header in one interceptor keeps the temporary demo flow simple while tightening auth. Nice improvement.

apps/dashboard/lib/trpc/routers/deploy/deployment/promote.ts (1)

25-43: Promotion endpoint now aligns with control-plane auth

Consistently requiring both env vars and applying the Bearer interceptor keeps the promote flow in lockstep with rollback—good hardening of the control-plane calls.

go/k8s/manifests/dashboard.yaml (2)

20-29: Verify busybox has nc -z, or switch readiness gating.

busybox images can vary; nc may be missing or lack -z. Consider wget/curl or rely on app readiness instead.


33-33: Dev-only settings may bite in clusters.

  • imagePullPolicy: Never will fail outside local clusters. Gate via dev overlay.
  • Service type: LoadBalancer can incur cloud cost; ensure this is intended (or use ClusterIP + Ingress for dev).

Would you like a kustomize overlay diff to scope these to dev?

Also applies to: 69-85

apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts (1)

44-74: Input validation + workspace scoping LGTM.

Batch fetch + same-project check before calling control plane looks solid.

Confirm proto field names match TS client (oldDeploymentId/newDeploymentId).

go/apps/ctrl/services/openapi/get_diff.go (1)

48-52: LGTM: clear error mapping and response assembly

Good use of connect error codes, internal/public messages, and diff+checker pipeline. No action needed.

Consider unit tests for: invalid base64; invalid OpenAPI; diff with breaking vs non‑breaking to validate HasBreakingChanges.

Also applies to: 64-68, 71-77, 96-101

go/pkg/cli/flag_test.go (1)

189-197: Duration flag tests look solid

Covers parse, default, zero value, and command integration. No change required.

Also applies to: 205-211, 212-217, 258-271

go/pkg/cli/flag.go (1)

320-338: LGTM: EnvVar now supports DurationFlag

Handling mirrors other types and parses with time.ParseDuration.

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

188-194: No changes needed
The root go/go.mod specifies Go 1.25, and other modules use Go 1.23–1.25, all ≥ 1.22, so for i := range 300 is fully supported.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@github-actions
Copy link
Contributor

github-actions bot commented Sep 29, 2025

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

@chronark chronark enabled auto-merge September 29, 2025 19:38
@chronark
Copy link
Collaborator Author

@Flo4604 :)

@chronark chronark added this pull request to the merge queue Sep 30, 2025
@graphite-app
Copy link

graphite-app bot commented Sep 30, 2025

SpongeBob gif. SpongeBob pretends to crank his fist like a jack-in-the-box, and his thumb rises and pops out for a thumbs up. He then gestures to his thumb like 'eh? What do you think?' (Added via Giphy)

@graphite-app
Copy link

graphite-app bot commented Sep 30, 2025

Graphite Automations

"Post a GIF when PR approved" took an action on this PR • (09/30/25)

1 gif was posted to this PR based on Andreas Thomas's automation.

Merged via the queue into main with commit c4cc548 Sep 30, 2025
17 checks passed
@chronark chronark deleted the 09-29-feat_openapi_diffing_api branch September 30, 2025 07:46
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