Skip to content

deploy: private deploys with allowed_ips ingress whitelist (Pro+ feature)#45

Merged
mastermanas805 merged 1 commit into
masterfrom
feat/private-deploy-backend-fresh
May 12, 2026
Merged

deploy: private deploys with allowed_ips ingress whitelist (Pro+ feature)#45
mastermanas805 merged 1 commit into
masterfrom
feat/private-deploy-backend-fresh

Conversation

@mastermanas805
Copy link
Copy Markdown
Member

Summary

POST /deploy/new accepts two new multipart fields gating ingress access:

  • private ("true" / "1" / "yes") — when set, the Ingress carries
    nginx.ingress.kubernetes.io/whitelist-source-range built from allowed_ips.
  • allowed_ips — comma-separated CIDRs / IPs. Each validated via
    net.ParseCIDR / net.ParseIP; max 32 entries.

Tier gate: Pro / Pro-yearly / Team / Team-yearly / Growth only.
Hobby / anonymous / free → 402 with private_deploy_requires_pro and the
new AgentActionPrivateDeployRequiresPro constant pointing at
https://instanode.dev/pricing.

Tier-gate runs before the allowed_ips validation so low-tier callers
see only the upgrade wall — the action that matters to them.

Changes

  • Migration 020_deployment_access_control.sql — adds private BOOLEAN
    and allowed_ips TEXT to deployments (defaults preserve existing
    byte-identical behaviour for public deploys).
  • internal/handlers/deploy.go — parses + validates new fields.
  • internal/handlers/deploy_private.go — new helper file isolating the
    whole rule-set in one place for U3 audits.
  • internal/handlers/agent_action.go — two new constants
    (AgentActionPrivateDeployRequiresPro,
    AgentActionPrivateDeployRequiresAllowedIPs), both auto-passing
    TestAgentActionContract.
  • internal/providers/compute/provider.go — extends DeployOptions with
    Private bool + AllowedIPs []string.
  • internal/providers/compute/k8s/client.goapplyIngressForDeploy
    sets the nginx annotation when private && len(IPs) > 0.
  • internal/models/deployment.go — extends Deployment and
    CreateDeploymentParams with the new fields; comma-join helpers.
  • internal/handlers/openapi.go — DeployRequest, DeployResponse, 402
    error documented.

Test plan

  • make test-unit green across every package (handlers, models, k8s,
    middleware, plans, providers, quota, router, urls).
  • 7 new tests in deploy_private_test.go — all pass:
    • Pro + private + 1 IP → 202, round-trips via GET /deploy/:id
    • Hobby + private → 402 with agent_action + upgrade_url
    • Pro + private + empty IPs → 400 (private_deploy_requires_allowed_ips)
    • Pro + private + invalid IP → 400 (bad literal surfaced verbatim)
    • Pro + private + 33 IPs → 400 (too_many_allowed_ips)
    • Public default (no private field) → 202 unchanged, fields zero-valued
    • GET /api/v1/deployments lists private + allowed_ips
  • TestAgentActionContract passes — both new constants registered.
  • TestOpenAPI passes — JSON spec is still valid.
  • Live deploy verification deferred — orchestrator handles combined
    build/deploy/E2E.

🤖 Generated with Claude Code

…ure)

POST /deploy/new now accepts two new multipart fields:

- private (bool-ish: "true" / "1" / "yes") — when true the resulting Ingress
  carries an nginx.ingress.kubernetes.io/whitelist-source-range annotation
  built from allowed_ips.
- allowed_ips — comma-separated CIDRs / IPs. Each entry is validated via
  net.ParseCIDR / net.ParseIP; max 32 entries (larger lists belong in CF
  Access / a real VPN, not an nginx annotation).

Tier gate: Pro / Pro-yearly / Team / Team-yearly / Growth only.
Hobby / anonymous / free / yearly-free → 402 with the new
AgentActionPrivateDeployRequiresPro constant pointing at
https://instanode.dev/pricing.

Validation order is tier-gate FIRST so low-tier callers never see the
"missing allowed_ips" 400 — they only see the upgrade wall, which is the
only action that matters to them.

Migration 020_deployment_access_control.sql adds two columns to deployments:
private BOOLEAN NOT NULL DEFAULT false and allowed_ips TEXT NOT NULL DEFAULT ''.
Stored as a comma-joined string (not JSONB array) because the nginx
annotation already requires that exact form — round-trip is lossless and
the existing scanDeployment code path keeps its scalar-friendly shape.

Compute layer:
- compute.DeployOptions gets Private bool + AllowedIPs []string.
- K8sProvider.applyIngressForDeploy now sets the annotation when private
  is true and the slice is non-empty (belt-and-suspenders against a stray
  "allow nobody" Ingress).

OpenAPI 3.1 spec extended with the two request fields, the two response
fields, the 402 wall, and updated 400-error list.

Two new agent_action constants:
- AgentActionPrivateDeployRequiresPro (402 wall)
- AgentActionPrivateDeployRequiresAllowedIPs (400 wall for private=true
  with empty allowed_ips)

Both pass TestAgentActionContract automatically — added to the contract
case table.

Tests: 7 new cases in deploy_private_test.go (Pro accept, Hobby 402,
empty IPs 400, invalid IP 400, 33-IP cap, public default unchanged,
GET /api/v1/deployments round-trip). All `make test-unit` packages green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mastermanas805 mastermanas805 merged commit d3fa539 into master May 12, 2026
mastermanas805 added a commit that referenced this pull request May 21, 2026
OSV-Scanner flags 4 CVEs in prometheus/prometheus v0.303.0 (the
server-binary module, pulled in transitively by OTel/Grafana consumers)
but govulncheck confirms 0 reachable on the current api master.

Mirrors worker PR #45 (same suppression with same rationale).
Per CLAUDE.md rule 25: explicit reason + exit condition documented.
Suppressions auto-lift when upstream consumers bump past v0.303.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant