Skip to content

feat: Elastic IP support and documentation improvements#55

Merged
poyrazK merged 31 commits intomainfrom
feature/elastic-ip
Feb 12, 2026
Merged

feat: Elastic IP support and documentation improvements#55
poyrazK merged 31 commits intomainfrom
feature/elastic-ip

Conversation

@poyrazK
Copy link
Copy Markdown
Owner

@poyrazK poyrazK commented Feb 12, 2026

Overview

This PR implements comprehensive support for Elastic IPs (EIPs) within 'The Cloud' platform, following the hexagonal architecture patterns. It includes full CRUD operations, association logic with instances, and enhanced documentation.

Changes

  • Core Logic: Implemented ElasticIPService with robust error handling and audit logging.
  • Data Layer: Added PostgreSQL repository for EIP persistence with proper row scanning error checks.
  • API Layer:
    • New endpoints for IP allocation, association, and release.
    • Safe UUID parsing and standardized error responses.
    • Updated Swagger annotations and regenerated swagger.yaml.
  • Testing:
    • Refactored E2E tests with polling for instance readiness.
    • Table-driven handler tests and hardened service unit tests.
  • Documentation: Updated docs/api-reference.md and Swagger spec to ensure a consistent API contract.

Verification Results

  • Passed unit tests for services and handlers.
  • Passed integration tests and E2E suites in CI.
  • Verified Swagger doc consistency via make swagger.

Summary by CodeRabbit

  • New Features

    • Elastic IP management API (allocate, list, get, associate, disassociate, release) and new RBAC permissions; Global Load Balancers and Volumes added to API.
  • Documentation

    • Updated architecture, API reference, backend, database, vision, and feature docs to cover Elastic IPs, GLBs, and volumes.
  • CI & Infrastructure

    • Workflows and compose now use configurable Docker network/env vars; E2E captures API logs on failure and image tagging unified to SHA.
  • Tests

    • New unit and E2E tests for Elastic IPs; test utilities made environment-configurable and timeouts adjusted.

poyrazK and others added 24 commits February 11, 2026 14:05
fix(ci): fix E2E tests and docker network isolation
Copilot AI review requested due to automatic review settings February 12, 2026 10:51
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

Adds Elastic IP management (domain, repo, service, handlers, migrations, tests, docs, and Swagger), introduces Docker network configurability for runtime/tests/CI via DOCKER_DEFAULT_NETWORK / TEST_DOCKER_NETWORK, and updates CI/E2E workflows and docker-compose to use those variables.

Changes

Cohort / File(s) Summary
Domain & RBAC
internal/core/domain/elastic_ip.go, internal/core/domain/rbac.go
Adds ElasticIP model, ElasticIPStatus enum, validation, and four RBAC permissions (eip:allocate, eip:release, eip:read, eip:associate).
Ports / Interfaces
internal/core/ports/elastic_ip.go
Defines ElasticIPRepository and ElasticIPService interfaces (CRUD and lifecycle methods: allocate, release, associate, disassociate).
Service Implementation & Unit Tests
internal/core/services/elastic_ip.go, internal/core/services/elastic_ip_test.go
Implements ElasticIP service with deterministic CGNAT IP generation, lifecycle rules, instance validation, audit logging, and comprehensive unit tests.
Persistence & Migrations
internal/repositories/postgres/elastic_ip_repo.go, internal/repositories/postgres/migrations/060_create_elastic_ips.up.sql, internal/repositories/postgres/migrations/060_create_elastic_ips.down.sql
Postgres repository with tenant-scoped CRUD, scan helpers, and migration creating elastic_ips table with indexes and partial unique constraint on instance_id.
HTTP Handlers & Handler Tests
internal/handlers/elastic_ip_handler.go, internal/handlers/elastic_ip_handler_test.go
Adds ElasticIP HTTP handler (allocate, list, get, release, associate, disassociate) with validation and unit tests using a mock service.
Routing & DI
internal/api/setup/router.go, internal/api/setup/dependencies.go
Wires ElasticIP repository/service/handler into DI and registers /elastic-ips routes with auth, tenancy, and RBAC.
Instance Service & Config
internal/core/services/instance.go, internal/platform/config.go
Adds DockerNetwork param to InstanceService and DockerDefaultNetwork config field; instance provisioning uses configured Docker network for Docker backend.
Test Utilities & Test Changes
pkg/testutil/constants.go, pkg/testutil/container.go, tests/*, internal/core/services/setup_test.go, tests/elastic_ip_e2e_test.go, tests/...
Makes test URLs/ports and Docker network configurable (TestDockerNetwork / TEST_DOCKER_NETWORK), increases Postgres container startup timeout, updates test docker-compose usage and compose project naming, adds E2E Elastic IP test, and ensures elastic_ips cleaned in test DB.
CI / E2E Workflows & Compose
.github/workflows/ci.yml, .github/workflows/e2e.yml, docker-compose.yml
CI/E2E workflows and docker-compose updated to use TEST_DOCKER_NETWORK / DOCKER_DEFAULT_NETWORK, create network via env var, export variables consistently, and use sha tagging for images; compose file uses env-based ports and removes fixed container_name entries.
Docs & OpenAPI / Swagger
docs/*, docs/swagger/docs.go, docs/swagger/swagger.json, docs/swagger/swagger.yaml
Adds Elastic IP docs across FEATURES/architecture/backend/database/vision, expands API reference and Swagger with domain.ElasticIP, ElasticIPStatus, AssociateIPRequest, new endpoints under /elastic-ips, and RBAC permissions; also adds Volumes and Global LB doc entries.
Gateway / Tests Observability
tests/gateway_e2e_test.go, tests/helpers/helpers_test.go, tests/chaos_test.go
Improves status checks and error messages in gateway tests, switches some docker commands to docker-compose with COMPOSE_PROJECT_NAME, and uses testutil values for base URL and network.
Misc / Packaging
pkg/testutil/*, other small updates
Introduces getEnvVar helper and switches several test constants to env-driven vars (TestBaseURL, TestDatabaseURL, TestPort, TestProdPort, TestDockerNetwork).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler as ElasticIP Handler
    participant Service as ElasticIP Service
    participant EIPRepo as ElasticIP Repo
    participant InstRepo as Instance Repo
    participant Audit as Audit Service
    participant DB as Database

    rect rgba(200, 150, 255, 0.5)
        Note over Client,DB: Allocate Elastic IP
        Client->>Handler: POST /elastic-ips
        Handler->>Service: AllocateIP(ctx)
        Service->>Service: generateDeterministicIP(uuid)
        Service->>EIPRepo: Create(elasticIP)
        EIPRepo->>DB: INSERT INTO elastic_ips
        DB-->>EIPRepo: OK
        Service->>Audit: LogAction("allocate_ip")
        Audit-->>Service: OK
        Service-->>Handler: ElasticIP
        Handler-->>Client: 201 Created
    end

    rect rgba(150, 200, 255, 0.5)
        Note over Client,DB: Associate Elastic IP
        Client->>Handler: POST /elastic-ips/:id/associate {instance_id}
        Handler->>Service: AssociateIP(eipID, instanceID)
        Service->>EIPRepo: GetByID(eipID)
        EIPRepo->>DB: SELECT ...
        DB-->>EIPRepo: ElasticIP
        Service->>InstRepo: GetByID(instanceID)
        InstRepo->>DB: SELECT ...
        DB-->>InstRepo: Instance
        Service->>EIPRepo: Update(elasticIP -> associated)
        EIPRepo->>DB: UPDATE elastic_ips
        DB-->>EIPRepo: OK
        Service->>Audit: LogAction("associate_ip")
        Audit-->>Service: OK
        Service-->>Handler: ElasticIP
        Handler-->>Client: 200 OK
    end

    rect rgba(255, 200, 150, 0.5)
        Note over Client,DB: Release Elastic IP
        Client->>Handler: DELETE /elastic-ips/:id
        Handler->>Service: ReleaseIP(eipID)
        Service->>EIPRepo: GetByID(eipID)
        EIPRepo->>DB: SELECT ...
        DB-->>EIPRepo: ElasticIP
        Service->>EIPRepo: Delete(eipID)
        EIPRepo->>DB: DELETE FROM elastic_ips
        DB-->>EIPRepo: OK
        Service->>Audit: LogAction("release_ip")
        Audit-->>Service: OK
        Service-->>Handler: success
        Handler-->>Client: 200 OK
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A hop for code, a nibble of net,
New Elastic IPs in a cozy set.
Allocate, attach, then gently roam,
Tests, docs, and CI guide them home.
🥕 Networks now sing — no more hard-set!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Elastic IP support and documentation improvements' directly summarizes the main change: adding Elastic IP support to the platform with accompanying documentation updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/elastic-ip

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

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

Copy link
Copy Markdown

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

Adds Elastic IP (EIP) support across domain/service/repository/HTTP layers and updates test infrastructure + documentation to cover the new API and Docker network configurability.

Changes:

  • Introduces EIP domain model, service logic (allocate/associate/disassociate/release), Postgres repository, migrations, and HTTP handlers/routes with RBAC permissions.
  • Improves test configurability (env-driven URLs/ports/network), adds EIP E2E + unit/handler tests, and adjusts chaos/E2E workflows for docker compose projects.
  • Updates docs (DB schema, architecture, features, API reference) and regenerates Swagger artifacts.

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/helpers/helpers_test.go Uses shared testutil.TestBaseURL instead of hardcoded localhost URL.
tests/elastic_ip_e2e_test.go New end-to-end test for EIP allocate/list/get/associate/disassociate/release flows.
tests/chaos_test.go Switches container operations to docker compose service-based commands.
pkg/testutil/container.go Increases Postgres testcontainer startup timeout to reduce flakiness.
pkg/testutil/constants.go Makes key test constants env-configurable (base URL, DB URL, ports, docker network).
internal/repositories/postgres/migrations/060_create_elastic_ips.up.sql Adds elastic_ips table + indexes (EIP persistence layer).
internal/repositories/postgres/migrations/060_create_elastic_ips.down.sql Adds down migration for EIP table/indexes.
internal/repositories/postgres/elastic_ip_repo.go New Postgres repository implementing EIP CRUD + tenant-scoped queries.
internal/platform/config.go Adds DockerDefaultNetwork config for docker backend network selection.
internal/handlers/elastic_ip_handler_test.go Adds handler tests for allocate/list/associate endpoints.
internal/handlers/elastic_ip_handler.go New HTTP handler implementing EIP endpoints.
internal/core/services/setup_test.go Ensures elastic_ips is included in DB cleanup for service tests.
internal/core/services/instance_test.go Uses env-configurable docker network + instance port selection for tests.
internal/core/services/instance.go Supports configurable docker network fallback when resolving network config.
internal/core/services/elastic_ip_test.go Adds service-level tests for EIP lifecycle behavior.
internal/core/services/elastic_ip.go Implements core EIP business logic + audit logging.
internal/core/ports/elastic_ip.go Defines ports/interfaces for EIP service and repository.
internal/core/domain/rbac.go Adds RBAC permission constants for EIP operations.
internal/core/domain/elastic_ip.go Adds EIP domain entity + status enum + validation.
internal/api/setup/router.go Registers /elastic-ips routes with auth/tenant/RBAC middleware.
internal/api/setup/dependencies.go Wires EIP repository/service and passes docker network config into InstanceService.
docs/vision.md Mentions Elastic IP management in the product roadmap.
docs/swagger/swagger.yaml Swagger spec update for EIP models/endpoints/permissions.
docs/swagger/swagger.json Swagger JSON update for EIP models/endpoints/permissions.
docs/swagger/docs.go Regenerated embedded swagger docs including EIP endpoints.
docs/database.md Documents elastic_ips table schema.
docs/backend.md Documents ElasticIPService and /elastic-ips endpoints at a high level.
docs/architecture.md Adds ElasticIPService to the architecture overview.
docs/api-reference.md Adds API reference section for Elastic IP endpoints.
docs/FEATURES.md Expands networking section to include Elastic IPs.
docker-compose.yml Removes fixed container names, makes ports configurable, propagates docker network name to API.
.github/workflows/e2e.yml Uses unique compose project names + passes docker network into API/tests; adds failure log dump.
.github/workflows/ci.yml Uses env-driven docker network creation and adjusts image tag publishing behavior.

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

// EIPStatusAssociated indicates the IP is currently mapped to a compute instance.
EIPStatusAssociated ElasticIPStatus = "associated"

// EIPStatusReleased indicates the IP has been returned to the pool (soft delete state).
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

EIPStatusReleased is documented as a “soft delete state”, but the current ReleaseIP logic hard-deletes the row. This makes the enum value/comment misleading (and exposes a status that can never occur). Either implement a released/soft-delete flow or remove/adjust this status.

Suggested change
// EIPStatusReleased indicates the IP has been returned to the pool (soft delete state).
// EIPStatusReleased indicates the IP has been released and is no longer allocated to a user.

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +40
// Allocate reserves a new Elastic IP.
// @Summary Allocate an Elastic IP
// @Tags elastic-ips
// @Security APIKeyAuth
// @Produce json
// @Success 201 {object} domain.ElasticIP
// @Failure 400 {object} httputil.Response
// @Failure 404 {object} httputil.Response
// @Failure 500 {object} httputil.Response
// @Router /elastic-ips [post]
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

These @Success annotations declare responses as domain.ElasticIP, but handlers call httputil.Success, which wraps payloads in httputil.Response{data=...}. Consider updating the annotations to httputil.Response{data=domain.ElasticIP} / ...{data=[]domain.ElasticIP} to keep the generated Swagger accurate (see AuthHandler for the pattern).

Copilot uses AI. Check for mistakes.
Comment thread docs/database.md
Comment on lines +209 to +212
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
instance_id UUID REFERENCES instances(id) ON DELETE SET NULL,
vpc_id UUID REFERENCES vpcs(id) ON DELETE SET NULL,
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

This documented elastic_ips schema (notably the tenant_id / user_id FKs and delete behavior) doesn’t match the 060_create_elastic_ips migration added in this PR. Please align the docs and migration so operators don’t provision the wrong schema.

Suggested change
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
instance_id UUID REFERENCES instances(id) ON DELETE SET NULL,
vpc_id UUID REFERENCES vpcs(id) ON DELETE SET NULL,
user_id UUID NOT NULL,
tenant_id UUID NOT NULL,
instance_id UUID,
vpc_id UUID,

Copilot uses AI. Check for mistakes.
Comment thread docs/api-reference.md
"id": "uuid",
"public_ip": "100.64.x.y",
"status": "allocated",
"arn": "arn:thecloud:vpc:local:tenant:eip/uuid"
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The service generates EIP ARNs as arn:thecloud:vpc:local:<user-id>:eip/<eip-id>, but this example uses tenant in that position. Consider updating the example to match the actual ARN format.

Suggested change
"arn": "arn:thecloud:vpc:local:tenant:eip/uuid"
"arn": "arn:thecloud:vpc:local:<user-id>:eip/<eip-id>"

Copilot uses AI. Check for mistakes.
Comment thread tests/elastic_ip_e2e_test.go Outdated
Comment on lines +129 to +133
checkResp := getRequest(t, client, testutil.TestBaseURL+testutil.TestRouteInstances+"/"+instanceID, token)
var statusRes struct {
Data struct {
Status domain.InstanceStatus `json:"status"`
} `json:"data"`
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The instance readiness polling loop ignores the GET response status code and the JSON decode error. If the instance endpoint returns an error/HTML or a non-200, the test may silently wait until timeout. Consider asserting checkResp.StatusCode and handling decode errors during polling for clearer failures.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +16
-- Optimization indexes
CREATE INDEX idx_elastic_ips_tenant_id ON elastic_ips(tenant_id);
CREATE INDEX idx_elastic_ips_instance_id ON elastic_ips(instance_id);
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The migrator reruns all *.up.sql on every startup and only logs errors, so non-idempotent CREATE INDEX statements will repeatedly error after the first run. Consider using CREATE INDEX IF NOT EXISTS ... for the new elastic_ips indexes (including the partial unique index below).

Copilot uses AI. Check for mistakes.
CREATE TABLE IF NOT EXISTS elastic_ips (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
tenant_id UUID NOT NULL,
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

tenant_id is created without a FK, while other tenant-scoped tables typically use tenant_id UUID ... REFERENCES tenants(id) (e.g. later tenant migrations). If elastic_ips is tenant-scoped, consider adding a FK (likely via a follow-up migration after the tenants table exists, or by moving this migration later).

Suggested change
tenant_id UUID NOT NULL,
tenant_id UUID NOT NULL REFERENCES tenants(id),

Copilot uses AI. Check for mistakes.

// generateDeterministicIP creates a consistent "public" IP for a given UUID within the 100.64.0.0/10 range.
func (s *elasticIPService) generateDeterministicIP(u uuid.UUID) string {
// 100.64.0.0 + bytes 12-15 of UUID
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The comment says “100.64.0.0 + bytes 12-15 of UUID”, but the implementation only uses bytes 12-14 and ignores byte 15, reducing the address space and making the comment inaccurate. Consider incorporating u[15] (and/or updating the comment) so the algorithm matches the stated intent.

Suggested change
// 100.64.0.0 + bytes 12-15 of UUID
// 100.64.0.0 + bytes 12-14 of UUID

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

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

Caution

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

⚠️ Outside diff range comments (1)
.github/workflows/ci.yml (1)

322-326: ⚠️ Potential issue | 🟡 Minor

Deploy tags no longer include the Git ref name (e.g., v1.2.3).

Both staging and production image pushes now tag with :staging/:latest and :${{ github.sha }} only. For production releases (triggered by refs/tags/v*), the semver tag (e.g., ghcr.io/.../thecloud:v1.2.3) is no longer pushed. This makes it harder to pull a specific release version by its semantic version. Consider adding ${{ github.ref_name }} back for tag-triggered builds.

Proposed fix for production deploy
          tags: |
            ghcr.io/${{ steps.repo_name.outputs.REPO_LC }}:latest
            ghcr.io/${{ steps.repo_name.outputs.REPO_LC }}:${{ github.sha }}
+           ghcr.io/${{ steps.repo_name.outputs.REPO_LC }}:${{ github.ref_name }}

Also applies to: 358-362

🤖 Fix all issues with AI agents
In `@docker-compose.yml`:
- Line 109: The DOCKER_DEFAULT_NETWORK default value may not match the actual
Compose network name because the explicit network name ("name: cloud-network")
is commented out, so Compose prefixes the network with the project name; update
either the docker-compose network block to uncomment and set name: cloud-network
so the network is created with that exact name, or change the default for
DOCKER_DEFAULT_NETWORK to the prefixed form your project uses (e.g.,
<project>_cloud-network) so the runtime lookup that uses DOCKER_DEFAULT_NETWORK
succeeds; look for the DOCKER_DEFAULT_NETWORK env var reference and the network
block containing the commented "name: cloud-network" to make the corresponding
change.

In `@internal/core/services/elastic_ip.go`:
- Around line 18-32: Replace the current NewElasticIPService signature with a
constructor that accepts a single params struct (e.g., ElasticIPServiceParams)
containing the existing dependencies (repo ports.ElasticIPRepository,
instanceRepo ports.InstanceRepository, auditSvc ports.AuditService, logger
*slog.Logger), update the constructor body to initialize and return
&elasticIPService using those params, and update any callers to pass the params
struct instead of separate arguments; keep the returned type as
ports.ElasticIPService and retain the elasticIPService struct fields unchanged.
- Around line 183-187: The function generateDeterministicIP in elasticIPService
uses magic-number octets for the CGNAT range (100, 64, and the 64 mask offset
and byte indices 12–15); replace these literals with clearly named constants
(e.g., CGNAT_FIRST_OCTET, CGNAT_SECOND_OCTET_BASE, CGNAT_SECOND_OCTET_MASK,
UUID_IP_BYTE_1, UUID_IP_BYTE_2, UUID_IP_BYTE_3) and use those constants in the
IPv4 construction so the intent (100.64.0.0/10 and which UUID bytes are used) is
explicit and maintainable.

In `@internal/handlers/elastic_ip_handler_test.go`:
- Line 136: The test silently ignores errors from http.NewRequest (req, _ :=
http.NewRequest(...)); change this to capture the error (req, err :=
http.NewRequest(...)) and immediately assert success with require.NoError(t,
err) (or require.NoError(tt.t, err) if the table test stores a testing.T
reference) so the test fails fast on unexpected request construction failures.

In `@tests/chaos_test.go`:
- Around line 35-45: The docker compose calls in tests/chaos_test.go use
exec.Command("docker", "compose", ...) without specifying the compose file or
project context, which can mis-target containers when tests run from another
cwd; update the exec.Command invocations that stop/start Redis (the lines
creating cmd := exec.Command("docker", "compose", "stop", "redis") and the
deferred exec.Command("docker", "compose", "start", "redis").Run()) to pass an
explicit "-f", "<path-to-docker-compose.yml>" (or compute the repo/test
directory at runtime via os.Getwd()/runtime.Caller and build the path) and
include "-p" if you want a deterministic project name, ensuring both stop and
start use the same flags and paths and surface errors (use require.NoError or
handle err) rather than swallowing them.

In `@tests/elastic_ip_e2e_test.go`:
- Line 135: Replace the silent decode with proper error handling: capture the
error from json.NewDecoder(checkResp.Body).Decode(&statusRes) into a variable
(e.g., err), and when non-nil log it with context (including
checkResp.StatusCode and the response body if feasible) using the test logger
(t.Logf or t.Errorf) inside the polling loop so decoding failures are visible;
keep using checkResp and statusRes but do not ignore the error or assign it to
_.
🧹 Nitpick comments (12)
internal/core/services/setup_test.go (1)

117-119: Pre-existing: silent failures in cleanup loop.

Line 118 discards both return values with _, _, which the coding guidelines flag. If a DELETE fails (e.g., due to a missing table after a failed migration), tests will silently proceed with stale data, making failures hard to diagnose. Consider logging or failing on error.

♻️ Suggested improvement
 	for _, table := range tables {
-		_, _ = db.Exec(ctx, "DELETE FROM "+table+" CASCADE")
+		_, err := db.Exec(ctx, "DELETE FROM "+table+" CASCADE")
+		if err != nil {
+			t.Logf("Warning: failed to clean table %s: %v", table, err)
+		}
 	}

As per coding guidelines, "Do not use silent failures - avoid blank identifier assignment like _ = someFunc()".

internal/repositories/postgres/migrations/060_create_elastic_ips.up.sql (1)

14-21: The regular index on instance_id (line 16) partially overlaps with the partial unique index (lines 19-21).

The partial unique index already covers lookups for non-NULL instance_id values. The regular index on line 16 adds value only for queries scanning NULL instance_id rows (e.g., "find unassociated EIPs"). If that query pattern is expected, both indexes are justified; otherwise the regular index is redundant overhead on writes.

docker-compose.yml (1)

10-10: DB_PORT_MAPPING takes the full host:container pair as a single variable.

This is unusual — most compose files use separate variables for the host port only (e.g., "${DB_HOST_PORT:-5433}:5432"). The current approach means a misconfigured value (e.g., DB_PORT_MAPPING=5433) without the container port would silently break the mapping. Consider splitting this for clarity and safety.

pkg/testutil/constants.go (2)

8-13: getEnvVar duplicates getEnv in internal/platform/config.go.

Both helpers do the same thing (read env with fallback). Consider extracting a shared utility to avoid duplication, or at minimum note that getEnv in config.go uses os.LookupEnv (distinguishes empty from unset) while this one uses os.Getenv (treats empty as unset). The behavioral difference means an explicitly set empty env var will be treated differently by each helper.


15-112: Switching from const to var makes all test constants mutable.

This is necessary for the env-var–driven values, but the remaining ~40 plain string literals (e.g., TestIPLocalhost, TestCIDR) are now mutable too. Not a practical risk in tests, but if you want to preserve immutability for the static values, you could keep them in a separate const block.

internal/core/services/instance_test.go (2)

81-91: Silent failures in test setup may hide configuration issues.

Lines 84, 86-90 silently discard errors. While understandable for idempotent setup (network may already exist, pre-pull is best-effort), consider at minimum logging errors at t.Logf level so CI failures are diagnosable. As per coding guidelines, "Do not use silent failures - avoid blank identifier assignment like _ = someFunc()".


133-137: Consider exporting getEnvVar from testutil or adding a TestInstancePort constant.

This manual env-var-with-fallback pattern mirrors getEnvVar in pkg/testutil/constants.go, but that helper is unexported. Either export it or add TestInstancePort alongside TestPort in the constants file for consistency.

internal/handlers/elastic_ip_handler_test.go (1)

75-142: Table-driven test structure is undermined by name-based conditionals in the loop.

Lines 126-128 and 131-133 use string comparisons against tt.name and tt.url to conditionally build the URL and body, bypassing the table-driven data. The body field on line 109 is declared but never actually used. This couples the loop logic to specific test case names, making it fragile and hard to extend.

Consider moving URL and body construction into the table (e.g., via a buildRequest func field) so each case is self-contained.

Also, Get, Release, and Disassociate handler methods are not covered.

♻️ Suggested approach
 	tests := []struct {
 		name           string
 		method         string
-		url            string
-		setupMock      func(svc *mockElasticIPService, eipID, instID uuid.UUID)
-		body           interface{}
+		buildURL       func(eipID uuid.UUID) string
+		setupMock      func(svc *mockElasticIPService, eipID, instID uuid.UUID)
+		buildBody      func(instID uuid.UUID) []byte
 		expectedStatus int
 	}{
 		{
 			name:   "Allocate",
 			method: "POST",
-			url:    "/elastic-ips",
+			buildURL: func(_ uuid.UUID) string { return "/elastic-ips" },
 			setupMock: func(svc *mockElasticIPService, eipID, instID uuid.UUID) {
 				svc.On("AllocateIP", mock.Anything).Return(&domain.ElasticIP{ID: eipID, PublicIP: "1.2.3.4"}, nil).Once()
 			},
+			buildBody:      func(_ uuid.UUID) []byte { return nil },
 			expectedStatus: http.StatusCreated,
 		},

Then in the loop, simply call url := tt.buildURL(eipID) and body := tt.buildBody(instID).

internal/core/services/elastic_ip_test.go (1)

20-33: Service tests use real Postgres repositories instead of mocks.

This file uses postgres.NewElasticIPRepository(db) and other real repositories. While these integration tests are valuable, the coding guidelines for internal/core/services/**/*_test.go require mocked repositories in service tests. Consider either:

  1. Adding a separate unit test file with mocked repositories (e.g., elastic_ip_unit_test.go) for fast, isolated testing.
  2. Moving these integration tests to a dedicated integration test directory.

As per coding guidelines, "Mock repositories in service tests".

tests/elastic_ip_e2e_test.go (1)

35-63: Sequential subtests sharing state — be aware of skip propagation.

The subtests share eipID and publicIP via closure and depend on sequential execution order. If AllocateIP is skipped or fails, subsequent subtests (ListIPs, GetIP, etc.) will operate on empty eipID, leading to confusing 404 failures rather than clear skip signals. Consider adding a guard at the top of dependent subtests:

if eipID == "" {
    t.Skip("skipping: no EIP allocated")
}
internal/core/domain/elastic_ip.go (1)

39-53: Harden Validate against unknown status values.

Right now any non-empty status passes. Enforcing the known lifecycle states avoids persisting invalid values.

♻️ Proposed refactor
 func (e *ElasticIP) Validate() error {
 	if e.UserID == uuid.Nil {
 		return errors.New("user ID is required")
 	}
 	if e.TenantID == uuid.Nil {
 		return errors.New("tenant ID is required")
 	}
 	if e.PublicIP == "" {
 		return errors.New("public IP address is required")
 	}
 	if e.Status == "" {
 		return errors.New("status is required")
 	}
+	switch e.Status {
+	case EIPStatusAllocated, EIPStatusAssociated, EIPStatusReleased:
+		// valid
+	default:
+		return errors.New("status is invalid")
+	}
 	return nil
 }
docs/swagger/swagger.yaml (1)

2887-2939: Questionable 404 responses on list and allocate endpoints.

  • GET /elastic-ips (list): A 404 response is unusual for a collection endpoint — an empty list with 200 is the standard REST convention.
  • POST /elastic-ips (allocate): A 404 response doesn't make sense for resource creation/allocation.

These likely come from the Swagger annotations on the Go handlers. Consider removing the 404 response declarations from the list and allocate handler annotations to align with REST conventions and with how other list/create endpoints in this spec are defined (e.g., GET /instances, POST /instances).

Comment thread docker-compose.yml
Comment thread internal/core/services/elastic_ip.go
Comment thread internal/handlers/elastic_ip_handler_test.go Outdated
Comment thread tests/chaos_test.go Outdated
Comment thread tests/elastic_ip_e2e_test.go Outdated
Copy link
Copy Markdown

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

Caution

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

⚠️ Outside diff range comments (1)
tests/chaos_test.go (1)

64-64: ⚠️ Potential issue | 🟡 Minor

Silent failure on io.ReadAll.

body, _ := io.ReadAll(respH.Body) discards the error. Consider logging on failure for diagnostic visibility. As per coding guidelines, "Do not use silent failures - avoid blank identifier assignment like _ = someFunc()".

Proposed fix
-			body, _ := io.ReadAll(respH.Body)
+			body, readErr := io.ReadAll(respH.Body)
+			if readErr != nil {
+				t.Logf("failed to read health check body: %v", readErr)
+			}
🤖 Fix all issues with AI agents
In `@internal/core/services/elastic_ip.go`:
- Around line 207-211: Update the function comment in generateDeterministicIP to
correctly state which UUID bytes are used (bytes 12-14) to match the
implementation that builds the IPv4 address using UUID_IP_BYTE_1,
UUID_IP_BYTE_2, and UUID_IP_BYTE_3; do not change the logic—just correct the
docstring so it no longer says "bytes 12-15".

In `@tests/chaos_test.go`:
- Around line 45-51: The deferred cleanup uses require.NoError which calls
FailNow inside a defer — replace that non-fatal assurance with a non-fatal
check: in the deferred func that restarts Redis (the anonymous defer func
containing t.Log, exec.Command("docker", "compose", ... "start", "redis").Run(),
and time.Sleep), remove require.NoError and instead use assert.NoError(t, err)
or if you prefer the testing.T API, check err and call t.Errorf("Failed to start
Redis container: %v", err); keep the restart command and the sleep as-is so
cleanup proceeds without calling FailNow from within defer.
🧹 Nitpick comments (3)
internal/handlers/elastic_ip_handler_test.go (2)

128-136: Table-driven test pattern is undermined by name-based conditionals.

The if url == "/elastic-ips/{id}/associate" and if tt.name == "Associate" checks couple the test loop to specific test-case names/URLs rather than letting the table data drive behavior. Consider adding a buildURL and buildBody func field (or similar) to each table entry so the loop stays generic.

Also, body, _ = json.Marshal(...) on line 135 silently discards the error. As per coding guidelines, "Do not use silent failures - avoid blank identifier assignment like _ = someFunc()".


78-115: Test coverage is limited to happy paths only.

The table only covers successful Allocate, List, and Associate. Consider adding error cases (e.g., service returns an error, invalid UUID in path) and covering the remaining handler methods (Get, Release, Disassociate) to improve handler test coverage.

internal/core/services/elastic_ip.go (1)

191-205: Constants use non-idiomatic Go naming convention.

Go convention (and the coding guidelines' examples like StatusRunning, MaxPortsPerInstance) favors PascalCase for exported constants. SCREAMING_SNAKE_CASE is atypical in Go. Since these are only used within this file, they could also be unexported.

Suggested naming
 const (
-	CGNAT_FIRST_OCTET = 100
-	CGNAT_SECOND_OCTET_BASE = 64
-	CGNAT_SECOND_OCTET_MASK = 64
-	UUID_IP_BYTE_1 = 12
-	UUID_IP_BYTE_2 = 13
-	UUID_IP_BYTE_3 = 14
+	cgnatFirstOctet     = 100
+	cgnatSecondOctetBase = 64
+	cgnatSecondOctetMask = 64
+	uuidIPByte1         = 12
+	uuidIPByte2         = 13
+	uuidIPByte3         = 14
 )

Comment on lines +207 to +211
// generateDeterministicIP creates a consistent "public" IP for a given UUID within the 100.64.0.0/10 range.
func (s *elasticIPService) generateDeterministicIP(u uuid.UUID) string {
// 100.64.0.0 + bytes 12-15 of UUID
ip := net.IPv4(CGNAT_FIRST_OCTET, CGNAT_SECOND_OCTET_BASE+u[UUID_IP_BYTE_1]%CGNAT_SECOND_OCTET_MASK, u[UUID_IP_BYTE_2], u[UUID_IP_BYTE_3])
return ip.String()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Comment says "bytes 12-15" but only bytes 12-14 are used.

Line 209 comment references bytes 12-15, but the implementation uses indices 12, 13, and 14 (three bytes, not four). Minor documentation inaccuracy.

Fix
-	// 100.64.0.0 + bytes 12-15 of UUID
+	// 100.64.0.0 + bytes 12-14 of UUID
📝 Committable suggestion

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

Suggested change
// generateDeterministicIP creates a consistent "public" IP for a given UUID within the 100.64.0.0/10 range.
func (s *elasticIPService) generateDeterministicIP(u uuid.UUID) string {
// 100.64.0.0 + bytes 12-15 of UUID
ip := net.IPv4(CGNAT_FIRST_OCTET, CGNAT_SECOND_OCTET_BASE+u[UUID_IP_BYTE_1]%CGNAT_SECOND_OCTET_MASK, u[UUID_IP_BYTE_2], u[UUID_IP_BYTE_3])
return ip.String()
// generateDeterministicIP creates a consistent "public" IP for a given UUID within the 100.64.0.0/10 range.
func (s *elasticIPService) generateDeterministicIP(u uuid.UUID) string {
// 100.64.0.0 + bytes 12-14 of UUID
ip := net.IPv4(CGNAT_FIRST_OCTET, CGNAT_SECOND_OCTET_BASE+u[UUID_IP_BYTE_1]%CGNAT_SECOND_OCTET_MASK, u[UUID_IP_BYTE_2], u[UUID_IP_BYTE_3])
return ip.String()
}
🤖 Prompt for AI Agents
In `@internal/core/services/elastic_ip.go` around lines 207 - 211, Update the
function comment in generateDeterministicIP to correctly state which UUID bytes
are used (bytes 12-14) to match the implementation that builds the IPv4 address
using UUID_IP_BYTE_1, UUID_IP_BYTE_2, and UUID_IP_BYTE_3; do not change the
logic—just correct the docstring so it no longer says "bytes 12-15".

Comment thread tests/chaos_test.go
Comment on lines 45 to 51
defer func() {
t.Log("Restarting Redis container...")
_ = exec.Command("docker", "start", "cloud-redis").Run()
err := exec.Command("docker", "compose", "-f", composeFile, "-p", projectName, "start", "redis").Run()
require.NoError(t, err, "Failed to start Redis container")
// Wait for Redis to be ready again
time.Sleep(2 * time.Second)
}()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid require.NoError inside a defer — use assert or t.Errorf instead.

require.NoError calls t.FailNow(), which triggers runtime.Goexit(). When called inside a deferred function, this can cause confusing behavior and may mask the original test failure. Use a non-fatal assertion for cleanup.

Proposed fix
 		defer func() {
 			t.Log("Restarting Redis container...")
 			err := exec.Command("docker", "compose", "-f", composeFile, "-p", projectName, "start", "redis").Run()
-			require.NoError(t, err, "Failed to start Redis container")
+			assert.NoError(t, err, "Failed to start Redis container")
 			// Wait for Redis to be ready again
 			time.Sleep(2 * time.Second)
 		}()
📝 Committable suggestion

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

Suggested change
defer func() {
t.Log("Restarting Redis container...")
_ = exec.Command("docker", "start", "cloud-redis").Run()
err := exec.Command("docker", "compose", "-f", composeFile, "-p", projectName, "start", "redis").Run()
require.NoError(t, err, "Failed to start Redis container")
// Wait for Redis to be ready again
time.Sleep(2 * time.Second)
}()
defer func() {
t.Log("Restarting Redis container...")
err := exec.Command("docker", "compose", "-f", composeFile, "-p", projectName, "start", "redis").Run()
assert.NoError(t, err, "Failed to start Redis container")
// Wait for Redis to be ready again
time.Sleep(2 * time.Second)
}()
🤖 Prompt for AI Agents
In `@tests/chaos_test.go` around lines 45 - 51, The deferred cleanup uses
require.NoError which calls FailNow inside a defer — replace that non-fatal
assurance with a non-fatal check: in the deferred func that restarts Redis (the
anonymous defer func containing t.Log, exec.Command("docker", "compose", ...
"start", "redis").Run(), and time.Sleep), remove require.NoError and instead use
assert.NoError(t, err) or if you prefer the testing.T API, check err and call
t.Errorf("Failed to start Redis container: %v", err); keep the restart command
and the sleep as-is so cleanup proceeds without calling FailNow from within
defer.

Copilot AI review requested due to automatic review settings February 12, 2026 11:36
Copy link
Copy Markdown

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

Caution

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

⚠️ Outside diff range comments (1)
.github/workflows/e2e.yml (1)

112-126: ⚠️ Potential issue | 🟡 Minor

The existing "Debug Logs on Failure" step (Line 122) is missing COMPOSE_PROJECT_NAME.

The new step correctly sets COMPOSE_PROJECT_NAME, but the pre-existing step at Line 122 doesn't. With a dynamic project name (thecloud-${{ github.run_id }}), docker compose logs at Line 126 will target the wrong (default) project and produce no useful output.

Either add the env var to the old step or consolidate the two failure steps into one.

Proposed fix — add env to existing step
       - name: Debug Logs on Failure
         if: failure()
+        env:
+          COMPOSE_PROJECT_NAME: thecloud-${{ github.run_id }}
         run: |
           echo "Dumping container logs for debugging..."
           docker compose logs --tail=200
🤖 Fix all issues with AI agents
In @.github/workflows/e2e.yml:
- Around line 39-43: The Postgres readiness loop "until docker compose exec -T
postgres pg_isready -U cloud; do ... done" can hang indefinitely; wrap the loop
in a timeout (matching the API readiness check, e.g., use "timeout 90") or
prefix the until command with a timeout so the check fails after a bounded
period; ensure the job exits non-zero if timeout triggers so the workflow stops
(mirror the approach used for the API readiness check that uses "timeout 90").

Comment thread .github/workflows/e2e.yml
Comment on lines +39 to 43
# Wait for Postgres to be healthy
until docker compose exec -T postgres pg_isready -U cloud; do
echo "Waiting for postgres..."
sleep 2
done
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a timeout to the Postgres readiness loop.

Unlike the API readiness check (Line 85) which uses timeout 90, this loop will run indefinitely if Postgres fails to start, relying only on the job-level timeout (default 6h).

Proposed fix
           # Wait for Postgres to be healthy
-          until docker compose exec -T postgres pg_isready -U cloud; do
+          timeout 60 bash -c 'until docker compose exec -T postgres pg_isready -U cloud; do
             echo "Waiting for postgres..."
             sleep 2
-          done
+          done' || { echo "Postgres failed to become ready"; docker compose logs postgres; exit 1; }
📝 Committable suggestion

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

Suggested change
# Wait for Postgres to be healthy
until docker compose exec -T postgres pg_isready -U cloud; do
echo "Waiting for postgres..."
sleep 2
done
# Wait for Postgres to be healthy
timeout 60 bash -c 'until docker compose exec -T postgres pg_isready -U cloud; do
echo "Waiting for postgres..."
sleep 2
done' || { echo "Postgres failed to become ready"; docker compose logs postgres; exit 1; }
🤖 Prompt for AI Agents
In @.github/workflows/e2e.yml around lines 39 - 43, The Postgres readiness loop
"until docker compose exec -T postgres pg_isready -U cloud; do ... done" can
hang indefinitely; wrap the loop in a timeout (matching the API readiness check,
e.g., use "timeout 90") or prefix the until command with a timeout so the check
fails after a bounded period; ensure the job exits non-zero if timeout triggers
so the workflow stops (mirror the approach used for the API readiness check that
uses "timeout 90").

Copy link
Copy Markdown

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

Copilot reviewed 33 out of 33 changed files in this pull request and generated 8 comments.

Comments suppressed due to low confidence (1)

internal/core/services/instance_test.go:124

  • setupInstanceServiceTest now creates a Docker network using testutil.TestDockerNetwork, but the InstanceService under test is constructed without setting DockerNetwork, so it will still fall back to the hard-coded "cloud-network" in resolveNetworkConfig. If TEST_DOCKER_NETWORK is set to a non-default value, the test will create one network but the service will use another. Pass DockerNetwork: testutil.TestDockerNetwork into InstanceServiceParams here to keep the test consistent with the new configurability.
	// In integration tests, we frequently rely on a shared Docker network.
	// We ensure it exists here so that Provisioning (which uses it as a fallback) succeeds.
	if compute.Type() == "docker" {
		_, _ = compute.CreateNetwork(ctx, testutil.TestDockerNetwork)
		// Pre-pull test image to prevent flakes in CI environments with slow registries or restrictive daemons
		_, _ = compute.LaunchInstanceWithOptions(ctx, coreports.CreateInstanceOptions{
			Name:      "pre-pull-image",
			ImageName: testImage,
		})
		_ = compute.DeleteInstance(ctx, "pre-pull-image")
	}

	// Ensure default instance type exists
	defaultType := &domain.InstanceType{
		ID:       testInstanceType,
		Name:     "Basic 2",
		VCPUs:    1,
		MemoryMB: 128,
		DiskGB:   1,
	}
	_, _ = itRepo.Create(ctx, defaultType)

	eventRepo := postgres.NewEventRepository(db)
	eventSvc := services.NewEventService(eventRepo, nil, slog.Default())

	auditRepo := postgres.NewAuditRepository(db)
	auditSvc := services.NewAuditService(auditRepo)

	taskQueue := &InMemoryTaskQueue{}
	network := noop.NewNoopNetworkAdapter(slog.Default())

	svc := services.NewInstanceService(services.InstanceServiceParams{
		Repo:             repo,
		VpcRepo:          vpcRepo,
		SubnetRepo:       subnetRepo,
		VolumeRepo:       volumeRepo,
		InstanceTypeRepo: itRepo,
		Compute:          compute,
		Network:          network,
		EventSvc:         eventSvc,
		AuditSvc:         auditSvc,
		TaskQueue:        taskQueue,
		Logger:           slog.Default(),
	})

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

Comment thread docs/database.md
Comment on lines +205 to +224
#### `elastic_ips` - Static/Elastic IPs
```sql
CREATE TABLE elastic_ips (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
instance_id UUID REFERENCES instances(id) ON DELETE SET NULL,
vpc_id UUID REFERENCES vpcs(id) ON DELETE SET NULL,
public_ip VARCHAR(45) NOT NULL UNIQUE,
status VARCHAR(50) NOT NULL DEFAULT 'allocated',
arn VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE UNIQUE INDEX idx_elastic_ips_instance_id_unique
ON elastic_ips(instance_id)
WHERE instance_id IS NOT NULL;
CREATE INDEX idx_elastic_ips_tenant_id ON elastic_ips(tenant_id);
```
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The documented elastic_ips schema here doesn’t match the actual migration added in this PR (060): the migration does not add FKs to tenants, does not specify ON DELETE CASCADE, and does not declare arn as UNIQUE. Please update the documentation to reflect the real schema (or update the migration/schema to match this doc).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
CREATE TABLE IF NOT EXISTS elastic_ips (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

This new migration file is missing the -- +goose Up directive that appears at the top of other migration files in this repo. Even if the in-app migrator doesn’t parse it, keeping the directive consistent helps tooling/readability for humans and any external migration runners.

Copilot uses AI. Check for mistakes.
Comment thread tests/chaos_test.go
Comment on lines +18 to +21
const (
composeFile = "../docker-compose.yml"
projectName = "cloud"
)
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

projectName is hard-coded to "cloud" for docker compose commands, but docker-compose.yml no longer sets container_name, and CI sets COMPOSE_PROJECT_NAME dynamically. With -p cloud, these commands will target the wrong project and likely fail (or stop the wrong containers). Read the project name from COMPOSE_PROJECT_NAME (or omit -p and rely on env) to match how the stack is started.

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +56
// Simulate public IP allocation from CGNAT range 100.64.0.0/10 for demo/simulation
// In a real system, this would come from an IP pool manager or provider SDK
publicIP := s.generateDeterministicIP(id)

eip := &domain.ElasticIP{
ID: id,
UserID: userID,
TenantID: tenantID,
PublicIP: publicIP,
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Deterministic IP allocation based on only 3 UUID bytes gives a pool of ~4.2M addresses (100.64.0.0/10 subset), so collisions are guaranteed at scale and will manifest as occasional unique-constraint failures on elastic_ips.public_ip. AllocateIP currently doesn’t retry on conflict, so allocation can fail nondeterministically. Consider allocating from a larger space (e.g., use 4 UUID bytes) and/or retrying with a new UUID/IP when repo.Create fails due to a uniqueness violation.

Copilot uses AI. Check for mistakes.

// generateDeterministicIP creates a consistent "public" IP for a given UUID within the 100.64.0.0/10 range.
func (s *elasticIPService) generateDeterministicIP(u uuid.UUID) string {
// 100.64.0.0 + bytes 12-15 of UUID
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The comment says the generated IP is based on UUID bytes 12–15, but the implementation uses bytes 12–14 (plus the fixed first octet and derived second octet). Update the comment (or the implementation) so they match.

Suggested change
// 100.64.0.0 + bytes 12-15 of UUID
// Use bytes 12-14 of the UUID to derive an IP within 100.64.0.0/10 (12th byte affects the second octet, 13th and 14th bytes are the third and fourth octets).

Copilot uses AI. Check for mistakes.
Comment thread docs/api-reference.md
"id": "uuid",
"public_ip": "100.64.x.y",
"status": "allocated",
"arn": "arn:thecloud:vpc:local:tenant:eip/uuid"
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The example ARN format here uses tenant in the ARN (arn:thecloud:vpc:local:tenant:eip/uuid), but ElasticIPService currently builds the ARN with the user ID (...:local:<userID>:eip/<id>). Update the docs example (or the ARN generation) so API consumers get the correct ARN format.

Suggested change
"arn": "arn:thecloud:vpc:local:tenant:eip/uuid"
"arn": "arn:thecloud:vpc:local:user-id:eip/uuid"

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +115
func TestElasticIPHandler(t *testing.T) {
tests := []struct {
name string
method string
url string
setupMock func(svc *mockElasticIPService, eipID, instID uuid.UUID)
body interface{}
expectedStatus int
}{
{
name: "Allocate",
method: "POST",
url: eipRoute,
setupMock: func(svc *mockElasticIPService, eipID, instID uuid.UUID) {
svc.On("AllocateIP", mock.Anything).Return(&domain.ElasticIP{ID: eipID, PublicIP: "1.2.3.4"}, nil).Once()
},
expectedStatus: http.StatusCreated,
},
{
name: "List",
method: "GET",
url: eipRoute,
setupMock: func(svc *mockElasticIPService, eipID, instID uuid.UUID) {
svc.On("ListElasticIPs", mock.Anything).Return([]*domain.ElasticIP{}, nil).Once()
},
expectedStatus: http.StatusOK,
},
{
name: "Associate",
method: "POST",
url: "/elastic-ips/{id}/associate",
setupMock: func(svc *mockElasticIPService, eipID, instID uuid.UUID) {
svc.On("AssociateIP", mock.Anything, eipID, instID).Return(&domain.ElasticIP{}, nil).Once()
},
body: map[string]string{"instance_id": ""}, // Filled in loop
expectedStatus: http.StatusOK,
},
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Handler tests currently cover Allocate, List, and Associate success paths only. For consistency with other handler test files in this repo, add coverage for Get/Release/Disassociate and key error cases (invalid UUID path param, invalid/missing JSON body, service errors) to ensure standardized error responses stay correct.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +21
-- Optimization indexes
CREATE INDEX idx_elastic_ips_tenant_id ON elastic_ips(tenant_id);
CREATE INDEX idx_elastic_ips_instance_id ON elastic_ips(instance_id);

-- Business rule: An instance can have at most one Elastic IP
CREATE UNIQUE INDEX idx_elastic_ips_instance_unique
ON elastic_ips(instance_id)
WHERE instance_id IS NOT NULL;
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Migration creates indexes without IF NOT EXISTS. Since RunMigrations executes all *.up.sql files on every startup and only logs errors, these CREATE INDEX statements will repeatedly error after the first run and spam logs. Use CREATE INDEX IF NOT EXISTS (including the partial unique index) or otherwise make index creation idempotent.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 12, 2026 11:56
Copy link
Copy Markdown

@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.

Caution

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

⚠️ Outside diff range comments (2)
tests/gateway_e2e_test.go (2)

210-223: ⚠️ Potential issue | 🟡 Minor

Missing status code assertions on route creation responses.

In VerifyPriorityMatching, the postRequest responses for the general and specific routes (resGen, resSpec) are closed without verifying StatusCreated. Other subtests (e.g., VerifyMethodMatching at lines 284, 295) already do this. A silent creation failure here would make the priority-matching assertions misleading.

Proposed fix
 	resGen := postRequest(t, client, testutil.TestBaseURL+gatewayRoutesPath, token, payloadGen)
+	require.Equal(t, http.StatusCreated, resGen.StatusCode, "general pattern route creation failed")
 	_ = resGen.Body.Close()
 
 	// 2. Specific exact route (High priority implied by length or explicit)
 ...
 	resSpec := postRequest(t, client, testutil.TestBaseURL+gatewayRoutesPath, token, payloadSpec)
+	require.Equal(t, http.StatusCreated, resSpec.StatusCode, "specific exact route creation failed")
 	_ = resSpec.Body.Close()

253-254: ⚠️ Potential issue | 🟡 Minor

Same missing status check on route creation in VerifyExtensionMatching.

resExt is closed without asserting StatusCreated, same pattern as the VerifyPriorityMatching issue above.

Proposed fix
 	resExt := postRequest(t, client, testutil.TestBaseURL+gatewayRoutesPath, token, payload)
+	require.Equal(t, http.StatusCreated, resExt.StatusCode, "extension route creation failed")
 	_ = resExt.Body.Close()

Copy link
Copy Markdown

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

Copilot reviewed 34 out of 34 changed files in this pull request and generated 5 comments.


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

Comment thread docs/database.md
Comment on lines +207 to +223
CREATE TABLE elastic_ips (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
instance_id UUID REFERENCES instances(id) ON DELETE SET NULL,
vpc_id UUID REFERENCES vpcs(id) ON DELETE SET NULL,
public_ip VARCHAR(45) NOT NULL UNIQUE,
status VARCHAR(50) NOT NULL DEFAULT 'allocated',
arn VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE UNIQUE INDEX idx_elastic_ips_instance_id_unique
ON elastic_ips(instance_id)
WHERE instance_id IS NOT NULL;
CREATE INDEX idx_elastic_ips_tenant_id ON elastic_ips(tenant_id);
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

This documented elastic_ips schema does not match the actual migration in 060_create_elastic_ips.up.sql (e.g., tenant_id/user_id foreign keys + ON DELETE CASCADE, status/arn uniqueness, and index naming). Please update the docs to reflect the real DB schema (or update the migration to match what’s documented) so operators don’t provision the wrong structure.

Suggested change
CREATE TABLE elastic_ips (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
instance_id UUID REFERENCES instances(id) ON DELETE SET NULL,
vpc_id UUID REFERENCES vpcs(id) ON DELETE SET NULL,
public_ip VARCHAR(45) NOT NULL UNIQUE,
status VARCHAR(50) NOT NULL DEFAULT 'allocated',
arn VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_elastic_ips_instance_id_unique
ON elastic_ips(instance_id)
WHERE instance_id IS NOT NULL;
CREATE INDEX idx_elastic_ips_tenant_id ON elastic_ips(tenant_id);
-- NOTE: The authoritative schema for this table is defined in:
-- migrations/060_create_elastic_ips.up.sql
--
-- High-level behavior:
-- * Tracks static/elastic IP allocations per tenant/user.
-- * Enforces foreign keys to users, tenants, instances, and VPCs with
-- appropriate ON DELETE behavior.
-- * Ensures uniqueness of allocation state/ARN and instance bindings via
-- unique indexes and supporting indexes for common lookups.
--
-- Refer to the migration file above for the exact column definitions,
-- constraints, and index names.

Copilot uses AI. Check for mistakes.
Comment on lines +81 to 85
// In integration tests, we frequently rely on a shared Docker network.
// We ensure it exists here so that Provisioning (which uses it as a fallback) succeeds.
if compute.Type() == "docker" {
_, _ = compute.CreateNetwork(ctx, "cloud-network")
_, _ = compute.CreateNetwork(ctx, testutil.TestDockerNetwork)
// Pre-pull test image to prevent flakes in CI environments with slow registries or restrictive daemons
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The test creates a Docker network using testutil.TestDockerNetwork, but the InstanceService constructed below is not passed DockerNetwork, so it will still default to cloud-network. If TEST_DOCKER_NETWORK is set to something else, the service may try to attach containers to a network that wasn’t created. Pass DockerNetwork: testutil.TestDockerNetwork when constructing the service (or keep the created network name aligned with the service default).

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +65
// Simulate public IP allocation from CGNAT range 100.64.0.0/10 for demo/simulation
// In a real system, this would come from an IP pool manager or provider SDK
publicIP := s.generateDeterministicIP(id)

eip := &domain.ElasticIP{
ID: id,
UserID: userID,
TenantID: tenantID,
PublicIP: publicIP,
Status: domain.EIPStatusAllocated,
ARN: fmt.Sprintf("arn:thecloud:vpc:local:%s:eip/%s", userID, id),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}

if err := s.repo.Create(ctx, eip); err != nil {
return nil, err
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

generateDeterministicIP maps a UUID down to a 100.64.0.0/10 address space (~4M IPs), so collisions are mathematically possible and will surface as DB unique-constraint failures on public_ip during allocation. Consider adding a retry loop that regenerates the UUID/IP when the insert fails due to a unique violation, or switch to an allocation strategy that guarantees uniqueness (e.g., an IP pool table/sequence).

Copilot uses AI. Check for mistakes.

var body []byte
if tt.name == "Associate" {
body, _ = json.Marshal(map[string]string{"instance_id": instID.String()})
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

json.Marshal errors are ignored here. Even though it’s unlikely to fail for this payload, it can hide real issues and makes debugging harder. Please assert/require the marshal error is nil.

Suggested change
body, _ = json.Marshal(map[string]string{"instance_id": instID.String()})
var err error
body, err = json.Marshal(map[string]string{"instance_id": instID.String()})
require.NoError(t, err)

Copilot uses AI. Check for mistakes.
instance_id UUID REFERENCES instances(id) ON DELETE SET NULL,
vpc_id UUID REFERENCES vpcs(id) ON DELETE SET NULL,
status VARCHAR(20) NOT NULL DEFAULT 'allocated',
arn VARCHAR(255) NOT NULL,
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

arn is declared NOT NULL but not UNIQUE here, while other resources often enforce ARN uniqueness at the DB layer. If the API expects ARN to uniquely identify an EIP (and to avoid accidental duplicates), add a UNIQUE constraint/index on arn.

Suggested change
arn VARCHAR(255) NOT NULL,
arn VARCHAR(255) NOT NULL UNIQUE,

Copilot uses AI. Check for mistakes.
@poyrazK poyrazK merged commit 4933881 into main Feb 12, 2026
40 of 41 checks passed
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.

2 participants