diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/components/client.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/components/client.tsx index d0016760f0..f5f499b5b5 100644 --- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/components/client.tsx +++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/components/client.tsx @@ -1,5 +1,6 @@ "use client"; +import type { ChangelogEntry } from "@unkey/proto"; import { AlertTriangle, BarChart3, @@ -20,10 +21,8 @@ import { } from "lucide-react"; import type React from "react"; import { useMemo, useState } from "react"; -import type { DiffChange, DiffData } from "../types"; - interface DiffViewerProps { - diffData: DiffData; + changelog: ChangelogEntry[]; fromDeployment?: string; toDeployment?: string; } @@ -31,12 +30,12 @@ interface DiffViewerProps { type ViewMode = "changes" | "side-by-side" | "timeline"; export const DiffViewer: React.FC = ({ - diffData, + changelog, fromDeployment = "v1", toDeployment = "v2", }) => { // Early return if no diffData - if (!diffData) { + if (!changelog) { return (
@@ -62,22 +61,20 @@ export const DiffViewer: React.FC = ({ // Statistics const stats = useMemo(() => { - const changes = diffData?.changes || []; - const breaking = changes.filter((c) => c.level === 3).length; - const warning = changes.filter((c) => c.level === 2).length; - const info = changes.filter((c) => c.level === 1).length; - const total = changes.length; + const breaking = changelog.filter((c) => c.level === 3).length; + const warning = changelog.filter((c) => c.level === 2).length; + const info = changelog.filter((c) => c.level === 1).length; + const total = changelog.length; - const operations = [...new Set(changes.map((c) => c.operation))]; - const paths = [...new Set(changes.map((c) => c.path))]; + const operations = [...new Set(changelog.map((c) => c.operation))]; + const paths = [...new Set(changelog.map((c) => c.path))]; return { breaking, warning, info, total, operations, paths }; - }, [diffData?.changes]); + }, [changelog]); // Filter changes const filteredChanges = useMemo(() => { - const changes = diffData?.changes || []; - return changes.filter((change) => { + return changelog.filter((change) => { if (filters.level !== null && change.level !== filters.level) { return false; } @@ -94,11 +91,11 @@ export const DiffViewer: React.FC = ({ } return true; }); - }, [diffData?.changes, filters]); + }, [changelog, filters]); // Group changes by path and operation const groupedChanges = useMemo(() => { - const grouped: Record> = {}; + const grouped: Record> = {}; filteredChanges.forEach((change) => { if (!grouped[change.path]) { @@ -186,7 +183,7 @@ export const DiffViewer: React.FC = ({ } }, { - "name": "offset", + "name": "offset", "in": "query", "schema": { "type": "integer", @@ -224,7 +221,7 @@ export const DiffViewer: React.FC = ({ "parameters": [ { "name": "pageSize", - "in": "query", + "in": "query", "schema": { "type": "integer", "default": 10 @@ -234,7 +231,7 @@ export const DiffViewer: React.FC = ({ "name": "page", "in": "query", "schema": { - "type": "integer", + "type": "integer", "default": 1 } }, @@ -375,7 +372,9 @@ export const DiffViewer: React.FC = ({ {selectedPath && selectedOperation && (
{selectedOperation} @@ -413,7 +412,9 @@ export const DiffViewer: React.FC = ({ {selectedPath} {selectedOperation} @@ -454,7 +455,7 @@ export const DiffViewer: React.FC = ({

Changes for this endpoint:

- {(diffData?.changes || []) + {changelog .filter((c) => c.path === selectedPath && c.operation === selectedOperation) .map((change, _index) => (
= ({
{change.operation} @@ -546,14 +549,14 @@ export const DiffViewer: React.FC = ({ {getChangeIcon(change.id)}

{change.text}

- {change.comment && ( + {change.text && (

- {change.comment} + {change.text}

)}
- ID: {change.id} • Section: {change.section} + ID: {change.id} • Section: {change.id}
- +
)} diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/types.ts b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/types.ts deleted file mode 100644 index 1e46eb35a2..0000000000 --- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/[...compare]/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -export interface DiffChange { - id: string; - text: string; - level: number; - operation: string; - path: string; - source: string; - section: string; - comment?: string; -} - -export interface DiffData { - changes: DiffChange[]; -} diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/page.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/page.tsx index 08a384ddd0..00b1e5af8b 100644 --- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/page.tsx +++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/diff/page.tsx @@ -30,7 +30,9 @@ export default function DiffSelectionPage(): JSX.Element { const handleCompare = () => { if (selectedFromDeployment && selectedToDeployment) { - router.push(`/projects/${projectId}/diff/${selectedFromDeployment}/${selectedToDeployment}`); + router.push( + `/${params?.workspaceSlug}/projects/${projectId}/diff/${selectedFromDeployment}/${selectedToDeployment}`, + ); } }; diff --git a/apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts b/apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts index d8986fb26b..046030906a 100644 --- a/apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts @@ -1,7 +1,11 @@ // trpc/routers/deployments/getOpenApiDiff.ts import { db } from "@/lib/db"; +import { env } from "@/lib/env"; import { requireUser, requireWorkspace, t } from "@/lib/trpc/trpc"; +import { createClient } from "@connectrpc/connect"; +import { createConnectTransport } from "@connectrpc/connect-web"; import { TRPCError } from "@trpc/server"; +import { OpenApiService } from "@unkey/proto"; import { z } from "zod"; export const getOpenApiDiff = t.procedure @@ -14,179 +18,61 @@ export const getOpenApiDiff = t.procedure }), ) .query(async ({ input, ctx }) => { - try { - // verify both deployments exist and belong to this workspace - const [oldDeployment, newDeployment] = await Promise.all([ - db.query.deployments.findFirst({ - where: (table, { eq, and }) => - and(eq(table.id, input.oldDeploymentId), eq(table.workspaceId, ctx.workspace.id)), - columns: { - id: true, - openapiSpec: true, - gitCommitSha: true, - gitBranch: true, - // TODO: add this column - //gitCommitMessage: true, - }, - with: { - environment: true, - project: { - columns: { - id: true, - name: true, - slug: true, - }, - }, - }, - }), - db.query.deployments.findFirst({ - where: (table, { eq, and }) => - and(eq(table.id, input.newDeploymentId), eq(table.workspaceId, ctx.workspace.id)), - columns: { - id: true, - openapiSpec: true, - gitCommitSha: true, - gitBranch: true, - //gitCommitMessage: true, - }, - with: { - environment: true, - project: { - columns: { - id: true, - name: true, - slug: true, - }, - }, + const { CTRL_URL, CTRL_API_KEY } = env(); + if (!CTRL_URL || !CTRL_API_KEY) { + throw new TRPCError({ + code: "PRECONDITION_FAILED", + message: "ctrl service is not configured", + }); + } + + // Here we make the client itself, combining the service + // definition with the transport. + const ctrl = createClient( + OpenApiService, + createConnectTransport({ + baseUrl: CTRL_URL, + interceptors: [ + (next) => (req) => { + req.header.set("Authorization", `Bearer ${CTRL_API_KEY}`); + return next(req); }, - }), - ]); + ], + }), + ); - if (!oldDeployment || !newDeployment) { + try { + const deployments = await db.query.deployments.findMany({ + where: (table, { and, eq, inArray }) => + and( + eq(table.workspaceId, ctx.workspace.id), + inArray(table.id, [input.oldDeploymentId, input.newDeploymentId]), + ), + }); + if (deployments.length !== 2) { throw new TRPCError({ code: "NOT_FOUND", message: "One or both deployments not found", }); } - - if (!oldDeployment.openapiSpec || !newDeployment.openapiSpec) { + if (deployments[0].projectId !== deployments[1].projectId) { throw new TRPCError({ code: "BAD_REQUEST", - message: "OpenAPI spec not available for one or both deployments", + message: "Deployments must belong to the same project", }); } - // Call control plane API - // TODO: put this in env var - let diffData: - | { - changes?: Array<{ - id: string; - text: string; - level: number; - operation: string; - path: string; - source: string; - section: string; - comment: string; - }>; - } - | undefined; - - try { - const response = await fetch( - "http://localhost:7091/ctrl.v1.OpenApiService/GetOpenApiDiff", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - old_version_id: input.oldDeploymentId, - new_version_id: input.newDeploymentId, - }), - }, - ); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - diffData = await response.json(); - } catch (error) { - // Fallback to mock data if control plane is not available - console.warn("Control plane not available, using mock diff data:", error); - diffData = { - changes: [ - { - id: "change_1", - text: "Added new endpoint /api/v1/users", - level: 1, - operation: "POST", - path: "/api/v1/users", - source: "paths", - section: "paths", - comment: "New user creation endpoint", - }, - { - id: "change_2", - text: "Modified response schema for /api/v1/users/{id}", - level: 2, - operation: "GET", - path: "/api/v1/users/{id}", - source: "responses", - section: "responses", - comment: "Added new fields to user response", - }, - { - id: "change_3", - text: "Removed deprecated endpoint /api/v1/legacy", - level: 3, - operation: "DELETE", - path: "/api/v1/legacy", - source: "paths", - section: "paths", - comment: "Breaking change: endpoint removed", - }, - ], - }; - } - - // Ensure the diff data has the expected structure - const normalizedDiff = { - changes: Array.isArray(diffData?.changes) ? diffData.changes : [], - ...diffData, - }; + const resp = await ctrl.getOpenApiDiff({ + oldDeploymentId: input.oldDeploymentId, + newDeploymentId: input.newDeploymentId, + }); - // Return the diff along with deployment context for the UI return { - diff: normalizedDiff, - context: { - oldDeployment: { - id: oldDeployment.id, - gitCommitSha: oldDeployment.gitCommitSha, - gitBranch: oldDeployment.gitBranch, - //gitCommitMessage: oldDeployment.gitCommitMessage, - environment: oldDeployment.environment, - project: oldDeployment.project, - }, - newDeployment: { - id: newDeployment.id, - gitCommitSha: newDeployment.gitCommitSha, - gitBranch: newDeployment.gitBranch, - //gitCommitMessage: newDeployment.gitCommitMessage, - environment: newDeployment.environment, - project: newDeployment.project, - }, - isSameEnvironment: oldDeployment.environment === newDeployment.environment, - isSameProject: oldDeployment.project?.id === newDeployment.project?.id, - }, + hasBreakingChanges: resp.hasBreakingChanges, + summary: resp.summary, + changes: resp.changes, }; } catch (error) { - if (error instanceof TRPCError) { - throw error; - } - console.error("Failed to get OpenAPI diff:", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", diff --git a/apps/dashboard/lib/trpc/routers/deploy/deployment/promote.ts b/apps/dashboard/lib/trpc/routers/deploy/deployment/promote.ts index ff632582e3..e9aff65b51 100644 --- a/apps/dashboard/lib/trpc/routers/deploy/deployment/promote.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/deployment/promote.ts @@ -22,8 +22,8 @@ export const promote = t.procedure ) .mutation(async ({ input, ctx }) => { // Validate that ctrl service URL is configured - const ctrlUrl = env().CTRL_URL; - if (!ctrlUrl) { + const { CTRL_URL, CTRL_API_KEY } = env(); + if (!CTRL_URL || !CTRL_API_KEY) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: "ctrl service is not configured", @@ -35,7 +35,13 @@ export const promote = t.procedure const ctrl = createClient( DeploymentService, createConnectTransport({ - baseUrl: ctrlUrl, + baseUrl: CTRL_URL, + interceptors: [ + (next) => (req) => { + req.header.set("Authorization", `Bearer ${CTRL_API_KEY}`); + return next(req); + }, + ], }), ); diff --git a/apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts b/apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts index 98343aedf3..71b5c5903e 100644 --- a/apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts @@ -22,29 +22,27 @@ export const rollback = t.procedure ) .mutation(async ({ input, ctx }) => { // Validate that ctrl service URL is configured - const ctrlUrl = env().CTRL_URL; - if (!ctrlUrl) { + const { CTRL_URL, CTRL_API_KEY } = env(); + if (!CTRL_URL || !CTRL_API_KEY) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: "ctrl service is not configured", }); } - // Here we make the client itself, combining the service // definition with the transport. const ctrl = createClient( DeploymentService, createConnectTransport({ - baseUrl: ctrlUrl, + baseUrl: CTRL_URL, interceptors: [ (next) => (req) => { - req.header.set("Authorization", `Bearer ${env().CTRL_API_KEY}`); + req.header.set("Authorization", `Bearer ${CTRL_API_KEY}`); return next(req); }, ], }), ); - try { // Verify the target deployment exists and belongs to this workspace const targetDeployment = await db.query.deployments.findFirst({ diff --git a/go/apps/ctrl/services/deployment/deploy_workflow.go b/go/apps/ctrl/services/deployment/deploy_workflow.go index 94a7b9a518..5a109bf151 100644 --- a/go/apps/ctrl/services/deployment/deploy_workflow.go +++ b/go/apps/ctrl/services/deployment/deploy_workflow.go @@ -3,7 +3,10 @@ package deployment import ( "context" "database/sql" + "encoding/base64" "fmt" + "io" + "net/http" "strings" "time" @@ -144,9 +147,9 @@ func (w *DeployWorkflow) Run(ctx hydra.WorkflowContext, req *DeployRequest) erro Namespace: hardcodedNamespace, DeploymentId: req.DeploymentID, Image: req.DockerImage, - Replicas: 3, - CpuMillicores: 1000, - MemorySizeMib: 1024, + Replicas: 1, + CpuMillicores: 512, + MemorySizeMib: 512, }, } @@ -255,6 +258,61 @@ func (w *DeployWorkflow) Run(ctx hydra.WorkflowContext, req *DeployRequest) erro if err != nil { return err } + + openapiSpec, err := hydra.Step(ctx, "scrape-openapi-spec", func(stepCtx context.Context) (string, error) { + + for _, instance := range createdInstances { + openapiURL := fmt.Sprintf("http://%s/openapi.yaml", instance.GetAddress()) + w.logger.Info("trying to scrape OpenAPI spec", "url", openapiURL, "host_port", instance.GetAddress(), "deployment_id", req.DeploymentID) + + resp, err := http.DefaultClient.Get(openapiURL) + if err != nil { + w.logger.Warn("openapi scraping failed for host address", "error", err, "host_addr", instance.GetAddress(), "deployment_id", req.DeploymentID) + continue + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + w.logger.Warn("openapi endpoint returned non-200 status", "status", resp.StatusCode, "host_addr", instance.GetAddress(), "deployment_id", req.DeploymentID) + continue + } + + // Read the OpenAPI spec + specBytes, err := io.ReadAll(resp.Body) + if err != nil { + w.logger.Warn("failed to read OpenAPI spec response", "error", err, "host_addr", instance.GetAddress(), "deployment_id", req.DeploymentID) + continue + } + + w.logger.Info("openapi spec scraped successfully", "host_addr", instance.GetAddress(), "deployment_id", req.DeploymentID, "spec_size", len(specBytes)) + return base64.StdEncoding.EncodeToString(specBytes), nil + } + // not an error really, just no OpenAPI spec found + return "", nil + + }) + + if err != nil { + return err + } + + if openapiSpec != "" { + + err = hydra.StepVoid(ctx, "update openapi for deployment", func(innerCtx context.Context) error { + + return db.Query.UpdateDeploymentOpenapiSpec(innerCtx, w.db.RW(), db.UpdateDeploymentOpenapiSpecParams{ + ID: deployment.ID, + UpdatedAt: sql.NullInt64{Valid: true, Int64: time.Now().UnixMilli()}, + OpenapiSpec: sql.NullString{Valid: true, String: openapiSpec}, + }) + + }) + + } + + w.logger.Info("openapi", + + "spec", openapiSpec) allDomains := buildDomains( workspace.Slug, project.Slug, @@ -341,22 +399,38 @@ func (w *DeployWorkflow) Run(ctx hydra.WorkflowContext, req *DeployRequest) erro // Prepare gateway configs for all non-local domains var gatewayParams []partitiondb.UpsertGatewayParams var skippedDomains []string - for _, domain := range changedDomains { if isLocalHostname(domain, w.defaultDomain) { skippedDomains = append(skippedDomains, domain) continue } - // Create gateway config for this domain - gatewayConfig, err := createGatewayConfig(req.DeploymentID, req.KeyspaceID, createdInstances) - if err != nil { - w.logger.Error("failed to create gateway config for domain", - "domain", domain, - "error", err, - "deployment_id", req.DeploymentID) - // Continue with other domains rather than failing the entire deployment - continue + // 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, + } + } + + if openapiSpec != "" { + gatewayConfig.ValidationConfig = &partitionv1.ValidationConfig{ + OpenapiSpec: openapiSpec, + } } // Marshal protobuf to bytes @@ -373,7 +447,6 @@ func (w *DeployWorkflow) Run(ctx hydra.WorkflowContext, req *DeployRequest) erro Config: configBytes, }) } - // Perform bulk upsert for all gateway configs if len(gatewayParams) > 0 { if err := partitiondb.BulkQuery.UpsertGateway(stepCtx, w.partitionDB.RW(), gatewayParams); err != nil { @@ -417,6 +490,7 @@ func (w *DeployWorkflow) Run(ctx hydra.WorkflowContext, req *DeployRequest) erro return err } } + /* // Step 23: Scrape OpenAPI spec from container (using host port mapping) diff --git a/go/apps/ctrl/services/openapi/get_diff.go b/go/apps/ctrl/services/openapi/get_diff.go index 6b6040e3bb..fcf0a3e1e3 100644 --- a/go/apps/ctrl/services/openapi/get_diff.go +++ b/go/apps/ctrl/services/openapi/get_diff.go @@ -2,6 +2,7 @@ package openapi import ( "context" + "encoding/base64" "connectrpc.com/connect" "github.com/getkin/kin-openapi/openapi3" @@ -13,7 +14,7 @@ import ( func (s *Service) GetOpenApiDiff(ctx context.Context, req *connect.Request[ctrlv1.GetOpenApiDiffRequest]) (*connect.Response[ctrlv1.GetOpenApiDiffResponse], error) { // Load old version spec - oldSpec, err := s.loadVersionSpec(ctx, req.Msg.OldVersionId) + oldSpec, err := s.loadOpenApiSpec(ctx, req.Msg.OldDeploymentId) if err != nil { return nil, connect.NewError(connect.CodeNotFound, fault.Wrap(err, fault.Internal("failed to load old version spec"), @@ -22,7 +23,7 @@ func (s *Service) GetOpenApiDiff(ctx context.Context, req *connect.Request[ctrlv } // Load new version spec - newSpec, err := s.loadVersionSpec(ctx, req.Msg.NewVersionId) + newSpec, err := s.loadOpenApiSpec(ctx, req.Msg.NewDeploymentId) if err != nil { return nil, connect.NewError(connect.CodeNotFound, fault.Wrap(err, fault.Internal("failed to load new version spec"), @@ -34,7 +35,15 @@ func (s *Service) GetOpenApiDiff(ctx context.Context, req *connect.Request[ctrlv loader := openapi3.NewLoader() loader.IsExternalRefsAllowed = true - s1, err := loader.LoadFromData([]byte(oldSpec)) + b1, err := base64.StdEncoding.DecodeString(oldSpec) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, fault.Wrap(err, + fault.Internal("failed to decode old version spec"), + fault.Public("Invalid base64 encoding in old version"), + )) + } + + s1, err := loader.LoadFromData(b1) if err != nil { return nil, connect.NewError(connect.CodeInvalidArgument, fault.Wrap(err, fault.Internal("failed to parse old OpenAPI spec"), @@ -42,7 +51,15 @@ func (s *Service) GetOpenApiDiff(ctx context.Context, req *connect.Request[ctrlv )) } - s2, err := loader.LoadFromData([]byte(newSpec)) + b2, err := base64.StdEncoding.DecodeString(newSpec) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, fault.Wrap(err, + fault.Internal("failed to decode new version spec"), + fault.Public("Invalid base64 encoding in new version"), + )) + } + + s2, err := loader.LoadFromData(b2) if err != nil { return nil, connect.NewError(connect.CodeInvalidArgument, fault.Wrap(err, fault.Internal("failed to parse new OpenAPI spec"), diff --git a/go/apps/ctrl/services/openapi/utils.go b/go/apps/ctrl/services/openapi/utils.go index 1ca4b84156..8bd4473011 100644 --- a/go/apps/ctrl/services/openapi/utils.go +++ b/go/apps/ctrl/services/openapi/utils.go @@ -7,12 +7,13 @@ import ( "github.com/unkeyed/unkey/go/pkg/fault" ) -func (s *Service) loadVersionSpec(ctx context.Context, versionID string) (string, error) { - deployment, err := db.Query.FindDeploymentById(ctx, s.db.RO(), versionID) +func (s *Service) loadOpenApiSpec(ctx context.Context, deploymentID string) (string, error) { + deployment, err := db.Query.FindDeploymentById(ctx, s.db.RO(), deploymentID) if err != nil { return "", err } + // Consider: s.logger.Debug("Deployment fetched", "id", deployment.ID, "hasSpec", deployment.OpenapiSpec.Valid) if !deployment.OpenapiSpec.Valid { return "", fault.New("deployment has no OpenAPI spec stored", fault.Public("OpenAPI specification not available for this deployment"), diff --git a/go/apps/gw/services/validation/validator.go b/go/apps/gw/services/validation/validator.go index 2d21fca70f..52b944d636 100644 --- a/go/apps/gw/services/validation/validator.go +++ b/go/apps/gw/services/validation/validator.go @@ -2,6 +2,7 @@ package validation import ( "context" + "encoding/base64" "fmt" "net/http" @@ -61,6 +62,7 @@ func (s *Service) Validate(ctx context.Context, sess *server.Session, config *pa // Get or create validator for this spec v, err := s.getOrCreateValidator(ctx, config.Deployment.Id, config.ValidationConfig.OpenapiSpec) + if err != nil { s.logger.Error("failed to get validator", "requestId", sess.RequestID(), @@ -127,7 +129,13 @@ func (s *Service) getOrCreateValidator(ctx context.Context, deploymentID string, ) // Parse OpenAPI document - document, err := libopenapi.NewDocument([]byte(spec)) + b, err := base64.StdEncoding.DecodeString(spec) + if err != nil { + return nil, fault.Wrap(err, + fault.Internal("failed to parse OpenAPI document as base64"), + ) + } + document, err := libopenapi.NewDocument(b) if err != nil { return nil, fault.Wrap(err, fault.Internal("failed to parse OpenAPI document"), diff --git a/go/apps/krane/backend/kubernetes/eviction.go b/go/apps/krane/backend/kubernetes/eviction.go index 8430c38b76..d6255c2c29 100644 --- a/go/apps/krane/backend/kubernetes/eviction.go +++ b/go/apps/krane/backend/kubernetes/eviction.go @@ -2,6 +2,7 @@ package kubernetes import ( "context" + "fmt" "time" "github.com/unkeyed/unkey/go/pkg/ptr" @@ -17,8 +18,11 @@ import ( // exceed the configured time-to-live threshold. func (k *k8s) autoEvictDeployments(ttl time.Duration) { + k.logger.Info(fmt.Sprintf("Krane setup to auto-evict deployments after %s", ttl.String())) repeat.Every(time.Minute, func() { ctx := context.Background() + k.logger.Info("evicting old deployments") + deployments, err := k.clientset.AppsV1().StatefulSets("unkey").List(ctx, metav1.ListOptions{ LabelSelector: "unkey.managed.by=krane", }) diff --git a/go/apps/krane/backend/kubernetes/get_deployment.go b/go/apps/krane/backend/kubernetes/get_deployment.go index 3de3bc7ec3..d0254b14de 100644 --- a/go/apps/krane/backend/kubernetes/get_deployment.go +++ b/go/apps/krane/backend/kubernetes/get_deployment.go @@ -41,8 +41,6 @@ func (k *k8s) GetDeployment(ctx context.Context, req *connect.Request[kranev1.Ge return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to get deployment: %w", err)) } - k.logger.Info("deployment retrieved", "deployment", sfs.String()) - // Check if this job is managed by Krane managedBy, exists := sfs.Labels["unkey.managed.by"] if !exists || managedBy != "krane" { diff --git a/go/cmd/krane/main.go b/go/cmd/krane/main.go index 2dfc8aaa17..0567ebdac4 100644 --- a/go/cmd/krane/main.go +++ b/go/cmd/krane/main.go @@ -35,8 +35,7 @@ unkey run krane # Run with default configurati cli.Default("/var/run/docker.sock"), cli.EnvVar("UNKEY_DOCKER_SOCKET")), // This has no use outside of our demo cluster and will be removed soon - cli.Duration("deployment-eviction-ttl", "Automatically delete deployments after some time. Use go duration formats such as 2h30m", - cli.Default(0), cli.EnvVar("UNKEY_DEPLOYMENT_EVICTION_TTL")), + cli.Duration("deployment-eviction-ttl", "Automatically delete deployments after some time. Use go duration formats such as 2h30m", cli.EnvVar("UNKEY_DEPLOYMENT_EVICTION_TTL")), }, Action: action, diff --git a/go/gen/proto/ctrl/v1/openapi.pb.go b/go/gen/proto/ctrl/v1/openapi.pb.go index 1e32a70265..361408c30f 100644 --- a/go/gen/proto/ctrl/v1/openapi.pb.go +++ b/go/gen/proto/ctrl/v1/openapi.pb.go @@ -22,11 +22,11 @@ const ( ) type GetOpenApiDiffRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - OldVersionId string `protobuf:"bytes,1,opt,name=old_version_id,json=oldVersionId,proto3" json:"old_version_id,omitempty"` - NewVersionId string `protobuf:"bytes,2,opt,name=new_version_id,json=newVersionId,proto3" json:"new_version_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + OldDeploymentId string `protobuf:"bytes,1,opt,name=old_deployment_id,json=oldDeploymentId,proto3" json:"old_deployment_id,omitempty"` + NewDeploymentId string `protobuf:"bytes,2,opt,name=new_deployment_id,json=newDeploymentId,proto3" json:"new_deployment_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetOpenApiDiffRequest) Reset() { @@ -59,16 +59,16 @@ func (*GetOpenApiDiffRequest) Descriptor() ([]byte, []int) { return file_ctrl_v1_openapi_proto_rawDescGZIP(), []int{0} } -func (x *GetOpenApiDiffRequest) GetOldVersionId() string { +func (x *GetOpenApiDiffRequest) GetOldDeploymentId() string { if x != nil { - return x.OldVersionId + return x.OldDeploymentId } return "" } -func (x *GetOpenApiDiffRequest) GetNewVersionId() string { +func (x *GetOpenApiDiffRequest) GetNewDeploymentId() string { if x != nil { - return x.NewVersionId + return x.NewDeploymentId } return "" } @@ -393,10 +393,10 @@ var File_ctrl_v1_openapi_proto protoreflect.FileDescriptor const file_ctrl_v1_openapi_proto_rawDesc = "" + "\n" + - "\x15ctrl/v1/openapi.proto\x12\actrl.v1\"c\n" + - "\x15GetOpenApiDiffRequest\x12$\n" + - "\x0eold_version_id\x18\x01 \x01(\tR\foldVersionId\x12$\n" + - "\x0enew_version_id\x18\x02 \x01(\tR\fnewVersionId\"\xb5\x01\n" + + "\x15ctrl/v1/openapi.proto\x12\actrl.v1\"o\n" + + "\x15GetOpenApiDiffRequest\x12*\n" + + "\x11old_deployment_id\x18\x01 \x01(\tR\x0foldDeploymentId\x12*\n" + + "\x11new_deployment_id\x18\x02 \x01(\tR\x0fnewDeploymentId\"\xb5\x01\n" + "\x0eChangelogEntry\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + "\x04text\x18\x02 \x01(\tR\x04text\x12\x14\n" + diff --git a/go/gen/proto/partition/v1/gateway.pb.go b/go/gen/proto/partition/v1/gateway.pb.go index f557cc6c8b..ff9f602092 100644 --- a/go/gen/proto/partition/v1/gateway.pb.go +++ b/go/gen/proto/partition/v1/gateway.pb.go @@ -296,8 +296,9 @@ func (x *AuthConfig) GetKeyAuthId() string { // Request validation middleware configuration type ValidationConfig struct { - state protoimpl.MessageState `protogen:"open.v1"` - OpenapiSpec string `protobuf:"bytes,1,opt,name=openapi_spec,json=openapiSpec,proto3" json:"openapi_spec,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + // Base64 standard encoding of the OpenAPI specification or empty string if not available + OpenapiSpec string `protobuf:"bytes,1,opt,name=openapi_spec,json=openapiSpec,proto3" json:"openapi_spec,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } diff --git a/go/k8s/manifests/ctrl.yaml b/go/k8s/manifests/ctrl.yaml index 5f19b426c6..a90833ed2a 100644 --- a/go/k8s/manifests/ctrl.yaml +++ b/go/k8s/manifests/ctrl.yaml @@ -72,6 +72,9 @@ spec: - name: UNKEY_DEFAULT_DOMAIN value: "unkey.local" + - name: UNKEY_API_KEY + value: "your-local-dev-key" + command: ["/unkey", "run", "ctrl"] initContainers: - name: wait-for-dependencies diff --git a/go/k8s/manifests/dashboard.yaml b/go/k8s/manifests/dashboard.yaml index 7f23d93986..5b7118414c 100644 --- a/go/k8s/manifests/dashboard.yaml +++ b/go/k8s/manifests/dashboard.yaml @@ -7,61 +7,63 @@ metadata: labels: app: dashboard spec: - replicas: 1 - selector: - matchLabels: - app: dashboard - template: - metadata: - labels: - app: dashboard - spec: - initContainers: - - name: wait-for-dependencies - image: busybox:1.36 - command: - - sh - - -c - - | - until nc -z planetscale 3900; do - echo waiting for planetscale - sleep 2 - done - containers: - - name: dashboard - image: unkey/dashboard:latest - imagePullPolicy: Never - ports: - - containerPort: 3000 - env: - # Database configuration - - name: DATABASE_HOST - value: "planetscale:3900" - # ClickHouse configuration - - name: CLICKHOUSE_URL - value: "http://unkey:password@clickhouse:8123" - # Environment - - name: NODE_ENV - value: "production" - # Instance identification - - name: UNKEY_PLATFORM - value: "kubernetes" - - name: UNKEY_REGION - value: "local" - - name: CTRL_URL - value: "http://ctrl:7091" - readinessProbe: - httpGet: - path: / - port: 3000 - initialDelaySeconds: 10 - periodSeconds: 5 - livenessProbe: - httpGet: - path: / - port: 3000 - initialDelaySeconds: 30 - periodSeconds: 10 + replicas: 1 + selector: + matchLabels: + app: dashboard + template: + metadata: + labels: + app: dashboard + spec: + initContainers: + - name: wait-for-dependencies + image: busybox:1.36 + command: + - sh + - -c + - | + until nc -z planetscale 3900; do + echo waiting for planetscale + sleep 2 + done + containers: + - name: dashboard + image: unkey/dashboard:latest + imagePullPolicy: Never + ports: + - containerPort: 3000 + env: + # Database configuration + - name: DATABASE_HOST + value: "planetscale:3900" + # ClickHouse configuration + - name: CLICKHOUSE_URL + value: "http://unkey:password@clickhouse:8123" + # Environment + - name: NODE_ENV + value: "production" + # Instance identification + - name: UNKEY_PLATFORM + value: "kubernetes" + - name: UNKEY_REGION + value: "local" + - name: CTRL_URL + value: "http://ctrl:7091" + - name: CTRL_API_KEY + value: "your-local-dev-key" + readinessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 10 --- apiVersion: v1 diff --git a/go/k8s/manifests/krane.yaml b/go/k8s/manifests/krane.yaml index 7861440edd..ca5487a9cf 100644 --- a/go/k8s/manifests/krane.yaml +++ b/go/k8s/manifests/krane.yaml @@ -34,7 +34,7 @@ spec: - name: UNKEY_HTTP_PORT value: "8080" - name: UNKEY_DEPLOYMENT_EVICTION_TTL - value: "2h" + value: "10m" --- apiVersion: v1 diff --git a/go/pkg/cli/flag.go b/go/pkg/cli/flag.go index 16956a75b9..57cf32e679 100644 --- a/go/pkg/cli/flag.go +++ b/go/pkg/cli/flag.go @@ -310,6 +310,8 @@ func Required() FlagOption { flag.required = true case *StringSliceFlag: flag.required = true + case *DurationFlag: + flag.required = true } } } @@ -330,6 +332,8 @@ func EnvVar(envVar string) FlagOption { flag.envVar = envVar case *StringSliceFlag: flag.envVar = envVar + case *DurationFlag: + flag.envVar = envVar } } } @@ -350,6 +354,8 @@ func Validate(fn ValidateFunc) FlagOption { flag.validate = fn case *StringSliceFlag: flag.validate = fn + case *DurationFlag: + flag.validate = fn } } } @@ -395,6 +401,12 @@ func Default(value any) FlagOption { } else { err = fmt.Errorf("default value for string slice flag '%s' must be []string, got %T", flag.name, value) } + case *DurationFlag: + if v, ok := value.(time.Duration); ok { + flag.value = v + } else { + err = fmt.Errorf("default value for duration flag '%s' must be time.Duration, got %T", flag.name, value) + } } if err != nil { @@ -438,7 +450,7 @@ func String(name, usage string, opts ...FlagOption) *StringFlag { return flag } -// Duration creates a new string flag with optional configuration +// Duration creates a new duration flag with optional configuration func Duration(name, usage string, opts ...FlagOption) *DurationFlag { flag := &DurationFlag{ baseFlag: baseFlag{ @@ -446,7 +458,7 @@ func Duration(name, usage string, opts ...FlagOption) *DurationFlag { usage: usage, required: false, // Default to not required }, - value: 0, // Default to empty string + value: time.Duration(0), // Default to false } // Apply options @@ -457,6 +469,11 @@ func Duration(name, usage string, opts ...FlagOption) *DurationFlag { // Check environment variable for default value if specified if flag.envVar != "" { if envValue := os.Getenv(flag.envVar); envValue != "" { + parsed, err := time.ParseDuration(envValue) + if err != nil { + Exit(fmt.Sprintf("Environment variable error: invalid duration value in %s=%q: %v", + flag.envVar, envValue, err), 1) + } // Apply validation to environment variable values if flag.validate != nil { if err := flag.validate(envValue); err != nil { @@ -464,17 +481,11 @@ func Duration(name, usage string, opts ...FlagOption) *DurationFlag { flag.envVar, envValue, err), 1) } } - var err error - flag.value, err = time.ParseDuration(envValue) - if err != nil { - Exit(fmt.Sprintf("Environment variable error: parsing duration failed for %s=%q: %v", - flag.envVar, envValue, err), 1) - } + flag.value = parsed flag.hasEnvValue = true // Don't mark as explicitly set - this is from environment } } - return flag } diff --git a/go/pkg/cli/flag_test.go b/go/pkg/cli/flag_test.go index f5f3de259b..0de0bd6220 100644 --- a/go/pkg/cli/flag_test.go +++ b/go/pkg/cli/flag_test.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/stretchr/testify/require" ) @@ -185,6 +186,90 @@ func TestFloatFlag_ValidationOnEnvVar(t *testing.T) { Float("rate", "rate flag", EnvVar("INVALID_RANGE"), Validate(validateRange)) } +func TestDurationFlag_BasicParsing(t *testing.T) { + flag := Duration("timeout", "timeout flag") + err := flag.Parse("5m30s") + require.NoError(t, err) + require.Equal(t, time.Duration(5*time.Minute+30*time.Second), flag.Value()) + require.True(t, flag.IsSet()) + require.True(t, flag.HasValue()) +} + +func TestDurationFlag_InvalidDuration(t *testing.T) { + flag := Duration("timeout", "timeout flag") + err := flag.Parse("invalid-duration") + require.Error(t, err) + require.Contains(t, err.Error(), "validation failed") +} + +func TestDurationFlag_WithDefault(t *testing.T) { + flag := Duration("timeout", "timeout flag", Default(time.Hour)) + require.Equal(t, time.Hour, flag.Value()) + require.False(t, flag.IsSet()) + require.True(t, flag.HasValue()) +} + +func TestDurationFlag_ZeroValue(t *testing.T) { + flag := Duration("timeout", "timeout flag") + require.Equal(t, time.Duration(0), flag.Value()) + require.False(t, flag.IsSet()) + require.False(t, flag.HasValue()) +} + +func TestDurationFlag_WithEnvVar(t *testing.T) { + os.Setenv("TEST_DURATION", "2h30m") + defer os.Unsetenv("TEST_DURATION") + + flag := Duration("timeout", "timeout flag", EnvVar("TEST_DURATION")) + require.Equal(t, time.Duration(2*time.Hour+30*time.Minute), flag.Value()) + require.False(t, flag.IsSet()) + require.True(t, flag.HasValue()) +} + +func TestDurationFlag_CommandOverridesEnv(t *testing.T) { + os.Setenv("TEST_DURATION", "1h") + defer os.Unsetenv("TEST_DURATION") + + flag := Duration("timeout", "timeout flag", EnvVar("TEST_DURATION")) + err := flag.Parse("30m") + require.NoError(t, err) + require.Equal(t, time.Duration(30*time.Minute), flag.Value()) + require.True(t, flag.IsSet()) +} + +func TestDurationFlag_ValidationOnEnvVar(t *testing.T) { + os.Setenv("INVALID_DURATION", "not-a-duration") + defer os.Unsetenv("INVALID_DURATION") + + exitCode, exitCalled, cleanup := mockExit() + defer cleanup() + + defer func() { + r := recover() + require.NotNil(t, r, "Expected panic from mocked Exit") + require.Equal(t, "exit called", r) + require.True(t, *exitCalled, "Exit should have been called") + require.Equal(t, 1, *exitCode, "Exit code should be 1") + }() + + Duration("timeout", "timeout flag", EnvVar("INVALID_DURATION")) +} + +func TestCommand_Duration_Integration(t *testing.T) { + cmd := &Command{ + Name: "test", + Flags: []Flag{ + Duration("timeout", "timeout for operation", Default(time.Minute*5)), + }, + } + + err := cmd.parse(context.Background(), []string{"--timeout", "2h30m"}) + require.NoError(t, err) + + duration := cmd.Duration("timeout") + require.Equal(t, time.Duration(2*time.Hour+30*time.Minute), duration) +} + func TestStringSliceFlag_CommaSeparated(t *testing.T) { flag := StringSlice("tags", "tags flag") err := flag.Parse("foo,bar,baz") diff --git a/go/pkg/cli/help.go b/go/pkg/cli/help.go index 7ccfd47bb5..feea3654d1 100644 --- a/go/pkg/cli/help.go +++ b/go/pkg/cli/help.go @@ -206,6 +206,8 @@ func (c *Command) getEnvVar(flag Flag) string { return f.EnvVar() case *StringSliceFlag: return f.EnvVar() + case *DurationFlag: + return f.EnvVar() default: return "" } @@ -234,6 +236,10 @@ func (c *Command) getDefaultValue(flag Flag) string { if val := f.Value(); len(val) > 0 { return fmt.Sprintf(`["%s"]`, strings.Join(val, `", "`)) } + case *DurationFlag: + if f.HasValue() { + return fmt.Sprintf("%s", f.Value()) + } } return "" } diff --git a/go/proto/ctrl/v1/openapi.proto b/go/proto/ctrl/v1/openapi.proto index 37225e2029..033958c8a3 100644 --- a/go/proto/ctrl/v1/openapi.proto +++ b/go/proto/ctrl/v1/openapi.proto @@ -5,8 +5,8 @@ package ctrl.v1; option go_package = "github.com/unkeyed/unkey/go/gen/proto/ctrl/v1;ctrlv1"; message GetOpenApiDiffRequest { - string old_version_id = 1; - string new_version_id = 2; + string old_deployment_id = 1; + string new_deployment_id = 2; } message ChangelogEntry { diff --git a/go/proto/partition/v1/gateway.proto b/go/proto/partition/v1/gateway.proto index aeae5af50d..3a7be51b77 100644 --- a/go/proto/partition/v1/gateway.proto +++ b/go/proto/partition/v1/gateway.proto @@ -42,5 +42,6 @@ message AuthConfig { // Request validation middleware configuration message ValidationConfig { + // Base64 standard encoding of the OpenAPI specification or empty string if not available string openapi_spec = 1; } diff --git a/internal/proto/generated/ctrl/v1/openapi_pb.ts b/internal/proto/generated/ctrl/v1/openapi_pb.ts index bf3d0b892d..bd94d2eab2 100644 --- a/internal/proto/generated/ctrl/v1/openapi_pb.ts +++ b/internal/proto/generated/ctrl/v1/openapi_pb.ts @@ -12,7 +12,7 @@ import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2 export const file_ctrl_v1_openapi: GenFile = /*@__PURE__*/ fileDesc( - "ChVjdHJsL3YxL29wZW5hcGkucHJvdG8SB2N0cmwudjEiRwoVR2V0T3BlbkFwaURpZmZSZXF1ZXN0EhYKDm9sZF92ZXJzaW9uX2lkGAEgASgJEhYKDm5ld192ZXJzaW9uX2lkGAIgASgJIoYBCg5DaGFuZ2Vsb2dFbnRyeRIKCgJpZBgBIAEoCRIMCgR0ZXh0GAIgASgJEg0KBWxldmVsGAMgASgFEhEKCW9wZXJhdGlvbhgEIAEoCRIZCgxvcGVyYXRpb25faWQYBSABKAlIAIgBARIMCgRwYXRoGAYgASgJQg8KDV9vcGVyYXRpb25faWQiQgoLRGlmZlN1bW1hcnkSDAoEZGlmZhgBIAEoCBIlCgdkZXRhaWxzGAIgASgLMhQuY3RybC52MS5EaWZmRGV0YWlscyJ/CgtEaWZmRGV0YWlscxImCgllbmRwb2ludHMYASABKAsyEy5jdHJsLnYxLkRpZmZDb3VudHMSIgoFcGF0aHMYAiABKAsyEy5jdHJsLnYxLkRpZmZDb3VudHMSJAoHc2NoZW1hcxgDIAEoCzITLmN0cmwudjEuRGlmZkNvdW50cyI+CgpEaWZmQ291bnRzEg0KBWFkZGVkGAEgASgFEg8KB2RlbGV0ZWQYAiABKAUSEAoIbW9kaWZpZWQYAyABKAUihwEKFkdldE9wZW5BcGlEaWZmUmVzcG9uc2USJQoHc3VtbWFyeRgBIAEoCzIULmN0cmwudjEuRGlmZlN1bW1hcnkSHAoUaGFzX2JyZWFraW5nX2NoYW5nZXMYAiABKAgSKAoHY2hhbmdlcxgDIAMoCzIXLmN0cmwudjEuQ2hhbmdlbG9nRW50cnkyZQoOT3BlbkFwaVNlcnZpY2USUwoOR2V0T3BlbkFwaURpZmYSHi5jdHJsLnYxLkdldE9wZW5BcGlEaWZmUmVxdWVzdBofLmN0cmwudjEuR2V0T3BlbkFwaURpZmZSZXNwb25zZSIAQjZaNGdpdGh1Yi5jb20vdW5rZXllZC91bmtleS9nby9nZW4vcHJvdG8vY3RybC92MTtjdHJsdjFiBnByb3RvMw", + "ChVjdHJsL3YxL29wZW5hcGkucHJvdG8SB2N0cmwudjEiTQoVR2V0T3BlbkFwaURpZmZSZXF1ZXN0EhkKEW9sZF9kZXBsb3ltZW50X2lkGAEgASgJEhkKEW5ld19kZXBsb3ltZW50X2lkGAIgASgJIoYBCg5DaGFuZ2Vsb2dFbnRyeRIKCgJpZBgBIAEoCRIMCgR0ZXh0GAIgASgJEg0KBWxldmVsGAMgASgFEhEKCW9wZXJhdGlvbhgEIAEoCRIZCgxvcGVyYXRpb25faWQYBSABKAlIAIgBARIMCgRwYXRoGAYgASgJQg8KDV9vcGVyYXRpb25faWQiQgoLRGlmZlN1bW1hcnkSDAoEZGlmZhgBIAEoCBIlCgdkZXRhaWxzGAIgASgLMhQuY3RybC52MS5EaWZmRGV0YWlscyJ/CgtEaWZmRGV0YWlscxImCgllbmRwb2ludHMYASABKAsyEy5jdHJsLnYxLkRpZmZDb3VudHMSIgoFcGF0aHMYAiABKAsyEy5jdHJsLnYxLkRpZmZDb3VudHMSJAoHc2NoZW1hcxgDIAEoCzITLmN0cmwudjEuRGlmZkNvdW50cyI+CgpEaWZmQ291bnRzEg0KBWFkZGVkGAEgASgFEg8KB2RlbGV0ZWQYAiABKAUSEAoIbW9kaWZpZWQYAyABKAUihwEKFkdldE9wZW5BcGlEaWZmUmVzcG9uc2USJQoHc3VtbWFyeRgBIAEoCzIULmN0cmwudjEuRGlmZlN1bW1hcnkSHAoUaGFzX2JyZWFraW5nX2NoYW5nZXMYAiABKAgSKAoHY2hhbmdlcxgDIAMoCzIXLmN0cmwudjEuQ2hhbmdlbG9nRW50cnkyZQoOT3BlbkFwaVNlcnZpY2USUwoOR2V0T3BlbkFwaURpZmYSHi5jdHJsLnYxLkdldE9wZW5BcGlEaWZmUmVxdWVzdBofLmN0cmwudjEuR2V0T3BlbkFwaURpZmZSZXNwb25zZSIAQjZaNGdpdGh1Yi5jb20vdW5rZXllZC91bmtleS9nby9nZW4vcHJvdG8vY3RybC92MTtjdHJsdjFiBnByb3RvMw", ); /** @@ -20,14 +20,14 @@ export const file_ctrl_v1_openapi: GenFile = */ export type GetOpenApiDiffRequest = Message<"ctrl.v1.GetOpenApiDiffRequest"> & { /** - * @generated from field: string old_version_id = 1; + * @generated from field: string old_deployment_id = 1; */ - oldVersionId: string; + oldDeploymentId: string; /** - * @generated from field: string new_version_id = 2; + * @generated from field: string new_deployment_id = 2; */ - newVersionId: string; + newDeploymentId: string; }; /** diff --git a/internal/proto/generated/partition/v1/gateway_pb.ts b/internal/proto/generated/partition/v1/gateway_pb.ts index 98f4f0605b..f617b79611 100644 --- a/internal/proto/generated/partition/v1/gateway_pb.ts +++ b/internal/proto/generated/partition/v1/gateway_pb.ts @@ -149,6 +149,8 @@ export const AuthConfigSchema: GenMessage = */ export type ValidationConfig = Message<"partition.v1.ValidationConfig"> & { /** + * Base64 standard encoding of the OpenAPI specification or empty string if not available + * * @generated from field: string openapi_spec = 1; */ openapiSpec: string; diff --git a/internal/proto/src/index.ts b/internal/proto/src/index.ts index 98f15a18fb..66c897cf21 100644 --- a/internal/proto/src/index.ts +++ b/internal/proto/src/index.ts @@ -1,3 +1,4 @@ // Re-export ctrl service types and services export * from "../generated/ctrl/v1/deployment_pb"; +export * from "../generated/ctrl/v1/openapi_pb"; //export * from "../generated/ctrl/v1/deployment_connect";