Skip to content

api refactor: deployments to ID-based APIs#240

Merged
peterj merged 23 commits intomainfrom
api-refactor-7.2
Mar 4, 2026
Merged

api refactor: deployments to ID-based APIs#240
peterj merged 23 commits intomainfrom
api-refactor-7.2

Conversation

@ilackarms
Copy link
Copy Markdown
Contributor

Description

This PR finalizes the deployment API refactor as a clean break, removing compatibility shims and aligning client/server behavior with the new /v0 design.

  • Motivation: Eliminate ambiguous deployment identity and deprecated payload shapes, and enforce one canonical contract.
  • What changed:
    • Deployment create/get/delete flows now consistently use deployment id (UUID) as the only identifier.
    • Removed handler-side ID override so IDs come from the service/adapter layer source of truth.
    • Updated client methods to ID-based deployment operations (GetDeploymentByID, RemoveDeploymentByID), removing legacy name/version deployment paths.
    • Removed deprecated deployment response compatibility fields from client (config, runtime), keeping canonical env.
    • Simplified client create semantics (removed Push* compatibility wrappers; Create* directly maps to canonical endpoints).
    • Fixed client retry behavior and restored SSE no-timeout behavior for streaming indexing.
    • Added/updated tests to cover ID round-trip behavior and JSONObject conversion correctness.

Change Type

/kind breaking_change
/kind fix
/kind cleanup

Changelog

BREAKING: deployment APIs and client operations are now strictly ID-based (`/v0/deployments/{id}`), and deprecated deployment compatibility fields (`config`, `runtime`) have been removed in favor of canonical `env`.

Additional Notes

  • This PR intentionally does not preserve backwards compatibility for deprecated deployment fields or legacy deployment route shapes.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR completes the break to the new /v0 deployment contract by making deployment operations strictly ID-based, removing legacy compatibility fields, and aligning server/client behavior around canonical deployment metadata (env, provider config/metadata, and deployment summaries in _meta).

Changes:

  • Refactors deployment model + storage to use env plus provider-agnostic provider_config / provider_metadata JSON, and adds deployment summaries into server/agent response metadata.
  • Updates service + handlers to dispatch deployment lifecycle operations through platform adapters via new service methods (CreateDeployment, UndeployDeployment, GetDeploymentLogs, CancelDeployment).
  • Updates the client + tests for ID-based deployment operations and restores SSE “no-timeout” behavior for streaming indexing.

Reviewed changes

Copilot reviewed 27 out of 27 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
pkg/types/types.go Adds ExtraRoutes hook for integrations to register routes under the same API/prefix.
pkg/models/server_response.go Adds deployments metadata pointer to server response meta.
pkg/models/agent.go Adds deployments metadata pointer to agent response meta.
pkg/models/deployment.go Refactors deployment model (env, JSONObject, provider config/metadata) and adds deployment summary/meta structs.
pkg/models/deployment_test.go Adds tests for JSONObject conversion helpers.
pkg/cli/root.go Removes legacy globals and wires client setup through setters only.
internal/registry/registry_app.go Plumbs ExtraRoutes into router options.
internal/registry/api/router/v0.go Invokes ExtraRoutes callback after registering core /v0 routes.
internal/registry/api/handlers/v0/deployments.go Routes deployment create/delete/logs/cancel through new service dispatch methods and updates request docs.
internal/registry/api/handlers/v0/deployment_adapters.go Splits local/kubernetes adapters into explicit types with stricter validation.
internal/registry/api/handlers/v0/deployment_meta_bridge.go Adds deployment→resource indexing and attaches deployment summaries into server/agent responses.
internal/registry/api/handlers/v0/servers.go, agents.go, edit.go Enriches list/get/create/edit responses with deployment metadata.
internal/registry/api/handlers/v0/*_test.go Updates/extends handler tests for adapter dispatch + metadata behavior.
internal/registry/service/service.go Extends RegistryService interface with adapter-dispatch lifecycle methods.
internal/registry/service/registry_service.go Implements adapter resolution and dispatch methods; updates deployment/env handling and provider metadata.
internal/registry/service/testing/fake_registry.go Extends fake service for new deployment lifecycle dispatch methods.
internal/registry/service/registry_service_test.go Updates tests for env/provider handling and cleanup signatures.
internal/registry/database/postgres.go Updates deployment persistence to store env + provider config/metadata and removes dropped columns from queries.
internal/registry/database/migrations/007_unify_deployment_provider_metadata.sql Adds provider_config/provider_metadata; drops region/cloud resource fields and related indexes.
internal/client/client.go Removes legacy client helpers; adds ID-based deployment methods and improved SSE request/client construction.
internal/client/client_integration_test.go Adds in-process integration tests for catalog + ID-based deployments + client behavior.
internal/cli/mcp/deploy.go Removes preflight server existence lookup; deploys by name/version directly.
internal/mcp/registryserver/deployments_test.go Updates deployment tool tests to use Env.
go.mod Promotes github.com/google/uuid to a direct dependency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

ResourceType: resourceType,
Origin: "managed",
Env: input.Body.Env,
ProviderConfig: input.Body.ProviderConfig,
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

models.Deployment.ProviderConfig is a models.JSONObject, but this assigns a map[string]any (input.Body.ProviderConfig). Go won’t allow assigning a plain map to a distinct named map type, so this should be converted (or the field type adjusted) before constructing deploymentReq.

Suggested change
ProviderConfig: input.Body.ProviderConfig,
ProviderConfig: models.JSONObject(input.Body.ProviderConfig),

Copilot uses AI. Check for mistakes.
if err == nil && strings.ToLower(strings.TrimSpace(provider.Platform)) == platformKubernetes {
s.cleanupKubernetesResources(ctx, existing, serverName, version, resourceType)
}
if existing != nil && platform == platformKubernetes {
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

cleanupExistingDeployment decides whether to run Kubernetes runtime cleanup based on the incoming platform argument rather than the platform of the existing deployment being removed. If a stale record belongs to a different provider/platform than the new request, this can either skip necessary Kubernetes cleanup (leaking resources) or attempt Kubernetes deletes for a non-Kubernetes deployment. Consider resolving the platform from existing.ProviderID (similar to removeDeploymentRecord) instead of using the caller-provided platform.

Suggested change
if existing != nil && platform == platformKubernetes {
if existing != nil && existing.ProviderID == kubernetesProviderID {

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +21
func deploymentResourceIndex(ctx context.Context, registry service.RegistryService) map[deploymentResourceKey][]models.DeploymentSummary {
deployments, err := registry.GetDeployments(ctx, nil)
if err != nil {
return map[deploymentResourceKey][]models.DeploymentSummary{}
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

deploymentResourceIndex calls registry.GetDeployments(ctx, nil) (no filter) and builds an index over all deployments on every servers/agents request that attaches deployment metadata. This can become a scalability bottleneck as the deployments table grows. If possible, narrow the query to the relevant resource type and names for the current response (or add a targeted DB/API method) to avoid full-table reads per request.

Copilot uses AI. Check for mistakes.
"github.com/stretchr/testify/require"
)

func TestDeploymentResourceIndexFiltersInactiveStatuses(t *testing.T) {
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

This test name suggests inactive statuses are filtered out, but the assertions expect both a deployed and a cancelled deployment to be present in the index. Renaming the test to reflect the actual behavior would make the intent clearer.

Suggested change
func TestDeploymentResourceIndexFiltersInactiveStatuses(t *testing.T) {
func TestDeploymentResourceIndexIncludesAllStatuses(t *testing.T) {

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +32
return []*models.Deployment{
{ID: "dep-active", ServerName: "io.test/server", ResourceType: "mcp", Status: "deployed"},
{ID: "dep-cancelled", ServerName: "io.test/server", ResourceType: "mcp", Status: "cancelled"},
}, nil
},
}

index := deploymentResourceIndex(context.Background(), reg)
key := deploymentResourceKey{resourceType: "mcp", resourceName: "io.test/server"}

require.Len(t, index[key], 2)
assert.Equal(t, "dep-active", index[key][0].ID)
assert.Equal(t, "deployed", index[key][0].Status)
assert.Equal(t, "dep-cancelled", index[key][1].ID)
assert.Equal(t, "cancelled", index[key][1].Status)
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

deploymentResourceIndex sorts summaries by UpdatedAt, but in this test both deployments have zero UpdatedAt values. Since slices.SortFunc is not stable, relying on the original order (and asserting [0]/[1] IDs) can be flaky. Set distinct UpdatedAt timestamps in the fixtures or assert order-insensitively.

Copilot uses AI. Check for mistakes.
deployment, err := registry.CreateDeployment(ctx, deploymentReq, platform)
if err != nil {
if errors.Is(err, database.ErrInvalidInput) {
return nil, huma.Error400BadRequest("Invalid deployment request")
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

registry.CreateDeployment can return database.ErrInvalidInput for multiple reasons (e.g., missing/unsupported platform adapter). Mapping all ErrInvalidInput cases to a generic "Invalid deployment request" makes unsupported provider/platform failures hard to diagnose. Consider returning a more specific error message/status when the platform isn’t supported vs. when the request body is invalid.

Suggested change
return nil, huma.Error400BadRequest("Invalid deployment request")
// Distinguish unsupported provider/platform from generic invalid input
lowerMsg := strings.ToLower(err.Error())
if strings.Contains(lowerMsg, "platform") || strings.Contains(lowerMsg, "provider") {
return nil, huma.Error400BadRequest("Unsupported provider or platform for deployment")
}
return nil, huma.Error400BadRequest("Invalid deployment request: " + err.Error())

Copilot uses AI. Check for mistakes.
// Deployment record already exists — clean up stale record and retry
log.Printf("Deployment for %s/%s already exists, replacing stale record", serverName, deployment.Version)
if cleanupErr := s.cleanupExistingDeployment(ctx, deployment.ID, provider.Platform); cleanupErr != nil {
if cleanupErr := s.cleanupExistingDeployment(ctx, serverName, deployment.Version, resourceTypeMCP, provider.Platform); cleanupErr != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if we're not using the ID, does that mean I can only deploy an artifact once -- like one MCPServer or Agent deployment in kubernetes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes although this is already the existing behavior; current db shape still enforces single-row identity from legacy semantics (not ID semantics). supporting parallel deployments across providers would require a schema + service semantics change (we can leave this for the post-api phase of the refactor)

CREATE TABLE deployments (
    -- Primary identifiers
    server_name VARCHAR(255) NOT NULL,
    version VARCHAR(255) NOT NULL,
    ...
    -- Primary key
    CONSTRAINT deployments_pkey PRIMARY KEY (server_name, version)
);

}
}

func buildMCPDeploymentCounts(deployedServers []*client.DeploymentResponse) map[string]map[string]int {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

would it make sense to extract these two functions to a common place, since they are more or less the same as the one in agent list?

return sanitizeK8sName(base)
}

func RemoteMCPResourceName(name, deploymentID string) string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

these two functions are exactly the same


// cleanupKubernetesResources deletes Kubernetes runtime resources for a stale deployment.
// Errors are logged but not returned, since the resources may already be gone.
func (s *registryServiceImpl) cleanupKubernetesResources(ctx context.Context, existing *models.Deployment) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

are we not cleaning these up anymore?

please make sure this scenario works:

  1. deploy an agent to k8s (arctl agent deploy)
  2. manually delete the agent from k8s
  3. deploy the agent again to k8s (arctl agent deploy)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

we no longer do name/version cleanup on deploy, because that breaks once multiple deployments can exist for the same registry artifact. deployments are now 1:1 by deployment ID, and k8s resources are named/labeled with that ID.

for your scenario (deploy -> manually delete in k8s -> deploy again), this should work. redeploy creates a new deployment ID and does not collide with previous runtime state.

note that deleting the deployment from the DB if it is deleted directly from k8s will require a follow up PR (as we discussed to implement db reconciliation feature)

i plan to address this once the api refactor is done

@peterj peterj added this pull request to the merge queue Mar 4, 2026
Merged via the queue into main with commit 4671b1e Mar 4, 2026
6 checks passed
@peterj peterj deleted the api-refactor-7.2 branch March 4, 2026 00:36
christian-posta pushed a commit to christian-posta/agentregistry that referenced this pull request Mar 9, 2026
# Description

This PR finalizes the deployment API refactor as a clean break, removing
compatibility shims and aligning client/server behavior with the new
`/v0` design.

- **Motivation:** Eliminate ambiguous deployment identity and deprecated
payload shapes, and enforce one canonical contract.
- **What changed:**
- Deployment create/get/delete flows now consistently use deployment
`id` (UUID) as the only identifier.
- Removed handler-side ID override so IDs come from the service/adapter
layer source of truth.
- Updated client methods to ID-based deployment operations
(`GetDeploymentByID`, `RemoveDeploymentByID`), removing legacy
name/version deployment paths.
- Removed deprecated deployment response compatibility fields from
client (`config`, `runtime`), keeping canonical `env`.
- Simplified client create semantics (removed `Push*` compatibility
wrappers; `Create*` directly maps to canonical endpoints).
- Fixed client retry behavior and restored SSE no-timeout behavior for
streaming indexing.
- Added/updated tests to cover ID round-trip behavior and `JSONObject`
conversion correctness.

# Change Type

/kind breaking_change  
/kind fix  
/kind cleanup

# Changelog

```release-note
BREAKING: deployment APIs and client operations are now strictly ID-based (`/v0/deployments/{id}`), and deprecated deployment compatibility fields (`config`, `runtime`) have been removed in favor of canonical `env`.
```

# Additional Notes


- This PR intentionally does **not** preserve backwards compatibility
for deprecated deployment fields or legacy deployment route shapes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants