diff --git a/.github/workflows/build-c.yml b/.github/workflows/build-c.yml index 11a4cb5d0..ae43ef775 100644 --- a/.github/workflows/build-c.yml +++ b/.github/workflows/build-c.yml @@ -15,12 +15,23 @@ # Triggers: # - release: When a GitHub release is published # - workflow_dispatch: Manual trigger for testing +# - workflow_call: Reusable from .github/workflows/e2e-cloud.yml. +# Pass target_filter='linux-x64-gnu' to skip the +# macOS/arm64 matrix entries (only the Tokyo +# runner target is needed). name: Build C SDK on: release: types: [ published ] workflow_dispatch: + workflow_call: + inputs: + target_filter: + description: 'Optional. Build only the given target (e.g. "linux-x64-gnu"). Empty = build all matrix platforms.' + type: string + default: '' + required: false env: CARGO_TERM_COLOR: always @@ -30,6 +41,33 @@ jobs: config: uses: ./.github/workflows/config.yml + # Compute the matrix the `build` job iterates over. When called via + # workflow_call with target_filter set, narrow to that single entry. + # `matrix` context is not available in job-level if:, so we filter + # the JSON here and have `build` consume `setup_matrix.outputs.platforms`. + setup_matrix: + name: Setup matrix + needs: config + runs-on: ubuntu-latest + outputs: + platforms: ${{ steps.filter.outputs.platforms }} + steps: + - id: filter + env: + ALL_PLATFORMS: ${{ needs.config.outputs.platforms }} + TARGET_FILTER: ${{ inputs.target_filter }} + run: | + if [ -n "${TARGET_FILTER:-}" ]; then + FILTERED=$(printf '%s' "$ALL_PLATFORMS" | jq -c --arg t "$TARGET_FILTER" '[.[] | select(.target == $t)]') + COUNT=$(printf '%s' "$FILTERED" | jq 'length') + [ "$COUNT" -ge 1 ] || { echo "::error::No matrix entry matches target_filter=$TARGET_FILTER"; exit 1; } + echo "Filtered to $COUNT entry(ies): $FILTERED" + else + FILTERED="$ALL_PLATFORMS" + echo "No filter — full matrix" + fi + echo "platforms=$FILTERED" >> "$GITHUB_OUTPUT" + # ============================================================================ # BUILD # ============================================================================ @@ -48,12 +86,12 @@ jobs: # ============================================================================ build: name: Build (${{ matrix.target }}) - needs: config + needs: [config, setup_matrix] runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - include: ${{ fromJson(needs.config.outputs.platforms) }} + include: ${{ fromJson(needs.setup_matrix.outputs.platforms) }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/build-runner-binary.yml b/.github/workflows/build-runner-binary.yml index 4fe736f4b..34fd9bc44 100644 --- a/.github/workflows/build-runner-binary.yml +++ b/.github/workflows/build-runner-binary.yml @@ -11,6 +11,8 @@ # Triggers: # - workflow_run: After "Build C SDK" completes (ensures libboxlite.a exists) # - workflow_dispatch: Manual trigger for testing +# - workflow_call: Reusable callers can set libboxlite_source=build to use a +# C SDK artifact built from the same checkout. name: Build Runner Binary on: @@ -18,6 +20,21 @@ on: workflows: ["Build C SDK"] types: [completed] workflow_dispatch: + inputs: + libboxlite_source: + description: 'libboxlite.a source for manual runs' + type: choice + default: source + options: + - source + - release + workflow_call: + inputs: + libboxlite_source: + description: 'How to obtain libboxlite.a: release, build, or source' + type: string + default: release + required: false permissions: contents: read @@ -56,7 +73,14 @@ jobs: VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/') echo "version=$VERSION" >> "$GITHUB_OUTPUT" + - name: Set up Rust + if: inputs.libboxlite_source == 'source' + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ needs.config.outputs.rust-toolchain }} + - name: Download prebuilt libboxlite.a + if: inputs.libboxlite_source == '' || inputs.libboxlite_source == 'release' env: VERSION: ${{ steps.version.outputs.version }} GH_REPO: ${{ github.repository }} @@ -67,6 +91,34 @@ jobs: curl -fsSL "${URL}" | tar xz -C /tmp/ cp "/tmp/boxlite-c-v${VERSION}-linux-x64-gnu/lib/libboxlite.a" sdks/go/libboxlite.a + - name: Download libboxlite.a from sibling Build C SDK job + if: inputs.libboxlite_source == 'build' + uses: actions/download-artifact@v4 + with: + name: c-sdk-linux-x64-gnu + path: /tmp/c-sdk/ + + - name: Stage libboxlite.a from sibling artifact + if: inputs.libboxlite_source == 'build' + run: | + set -euo pipefail + ARCHIVE=$(ls /tmp/c-sdk/boxlite-c-v*-linux-x64-gnu.tar.gz | head -1) + [ -n "$ARCHIVE" ] || { echo "::error::No c-sdk archive in artifact"; exit 1; } + tar xzf "$ARCHIVE" -C /tmp/c-sdk/ + A=$(find /tmp/c-sdk -name libboxlite.a | head -1) + [ -n "$A" ] || { echo "::error::libboxlite.a not found in archive"; exit 1; } + mkdir -p sdks/go + cp "$A" sdks/go/libboxlite.a + ls -lh sdks/go/libboxlite.a + + - name: Build libboxlite.a from current source + if: inputs.libboxlite_source == 'source' + run: | + make setup:build runtime dist:go + mkdir -p sdks/go + cp target/release/libboxlite.a sdks/go/libboxlite.a + ls -lh sdks/go/libboxlite.a + - name: Rewrite go.work for minimal modules run: | printf 'go 1.25.4\n\nuse (\n\t./runner\n\t./daemon\n\t./common-go\n\t./api-client-go\n\t./libs/computer-use\n\t../sdks/go\n)\n' > apps/go.work diff --git a/.github/workflows/e2e-cloud.yml b/.github/workflows/e2e-cloud.yml new file mode 100644 index 000000000..f4869f772 --- /dev/null +++ b/.github/workflows/e2e-cloud.yml @@ -0,0 +1,810 @@ +# End-to-end suite against the deployed Tokyo stack — SDK → API → Runner → libkrun VM. +# +# Existing `make test:integration:*` uses the local PyO3 / FFI path +# (`Boxlite.default()`) and bypasses both the NestJS API and boxlite-runner, +# so bugs that surface only on the REST → API → runner chain (e.g. #563's +# exec-stdout drop, #627's attach re-drain) reach production without +# breaking CI. This workflow runs the full path against the always-on +# Tokyo stack (`boxlite-e2e-ci-*`), not a per-run local bootstrap. +# +# NOT-YET-REQUIRED: this workflow is currently NOT a branch-protection +# required check. The Tokyo stack's IAM OIDC role (provisioned by +# `scripts/ci/setup-e2e-cloud-oidc.sh`) and SSM Parameter Store entry +# `/boxlite/e2e-ci/admin-api-key` must exist before any run can pass. +# After the first green run, an admin should add the `E2E suite (Tokyo)` +# (or a re-introduced gate job's name) to branch protection on `main`. +# Trial runs available now via `workflow_dispatch`. +# +# Authentication: GitHub OIDC → AWS STS (no stored AWS credentials). +# Required AWS resources (provisioned by `scripts/ci/setup-e2e-cloud-oidc.sh`): +# - OIDC identity provider (`token.actions.githubusercontent.com`) +# - IAM role `boxlite-e2e-cloud-github-actions` (separate from +# `boxlite-e2e-github-actions` which scopes to us-east-1 self-hosted +# KVM runner provisioning). +# +# Required GitHub repo variables (Settings → Variables → Actions): +# - `AWS_ACCOUNT_ID` (shared with other workflows) +# - `AWS_E2E_CLOUD_REGION` (ap-northeast-1) +# - `AWS_E2E_CLOUD_ROLE_ARN` (arn:aws:iam:::role/boxlite-e2e-cloud-github-actions) +# +# Reads at runtime (must already exist in AWS, see apps/infra docs): +# - SSM parameter `/boxlite/e2e-ci/admin-api-key` (SecureString) +# - ELB `ApiLoadBalancer-*` (DNS resolved at runtime) +# - ECS cluster `boxlite-e2e-ci-ClusterCluster-*` / service `Api` +# - EC2 runner instance tagged `Name=boxlite-runner` +# - S3 bucket `boxlite-e2e-ci-storagebucket-*` (build artifact staging) + +name: E2E cloud + +# No path filter on triggers: must always start so branch protection +# can require this workflow's gate as a status check. Path matching +# happens inside the `changes` job and gates the expensive `e2e` job. +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: {} + +# Singleton lock — every run (every PR + every push) must serialize +# against the SHARED Tokyo stack. Per-ref grouping is wrong here: +# PR-A and PR-B have different refs but compete for the same ECS +# service / runner binary / RDS row. cancel-in-progress: false because +# a half-applied ECS rolling update is worse than waiting. +concurrency: + group: e2e-cloud-shared + cancel-in-progress: false + +env: + # AWS identifiers come from repo variables (same pattern as + # .github/workflows/e2e-test.yml). `vars.*` is stripped by GitHub + # for fork-PR runs, but this workflow is gated on same-repo branches + # (push to main + workflow_dispatch + same-repo PR), so the context + # is always populated. Set via repo Settings → Variables: + # AWS_ACCOUNT_ID, AWS_E2E_CLOUD_REGION, AWS_E2E_CLOUD_ROLE_ARN + # Keeps the account ID out of the public repo source — defense in + # depth (AWS treats account ID as "sensitive, not secret", but no + # reason to surface it when we don't have to). + AWS_REGION: ${{ vars.AWS_E2E_CLOUD_REGION }} + AWS_ROLE_ARN: ${{ vars.AWS_E2E_CLOUD_ROLE_ARN }} + STAGE: e2e-ci + STACK_PREFIX: boxlite-e2e-ci + ECS_CLUSTER_PATTERN: boxlite-e2e-ci-ClusterCluster- + ADMIN_ORG_ID: 4a1eef70-8734-4814-b7a5-0ca3522a8760 + ECR_REPO: sst-asset + +jobs: + # Cheap detector on GitHub-hosted runner: decide if the expensive + # e2e job needs to fire. Avoids burning deploy+test cycle time + # on PRs that don't touch any code path the suite exercises. + changes: + name: Detect relevant changes + runs-on: ubuntu-latest + outputs: + # Each downstream job/step gates on the narrow component that + # would actually be invalidated by its source paths, instead of + # one giant `relevant` flag — saves the ~11 min libboxlite + + # runner build chain when a PR only touches API or SDK code. + api: ${{ steps.filter.outputs.api }} + runner_chain: ${{ steps.filter.outputs.runner_chain }} + sdk_py: ${{ steps.filter.outputs.sdk_py }} + tests_or_workflow: ${{ steps.filter.outputs.tests_or_workflow }} + any: ${{ steps.filter.outputs.api == 'true' || steps.filter.outputs.runner_chain == 'true' || steps.filter.outputs.sdk_py == 'true' || steps.filter.outputs.tests_or_workflow == 'true' }} + steps: + - uses: actions/checkout@v5 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + # API container — apps/api Dockerfile.source bakes from these. + api: + - 'apps/api/**' + - 'apps/dashboard/**' + - 'apps/libs/**' + # Runner binary build chain. libboxlite.a (Rust src/**) is + # statically linked into the runner via CGo, so any Rust src + # change OR any Go runner-side change invalidates the runner + # binary. Touching scripts/build/** also invalidates the + # libboxlite build chain output. + runner_chain: + - 'apps/runner/**' + - 'apps/daemon/**' + - 'apps/common-go/**' + - 'apps/api-client-go/**' + - 'apps/libs/computer-use/**' + - 'sdks/go/**' + - 'src/boxlite/**' + - 'src/api-client/**' + - 'src/shared/**' + - 'src/deps/**' + - 'scripts/build/**' + - 'sdks/c/src/exec/**' + # Python SDK. PyO3 wheel pulls libboxlite.a sources too via + # boxlite-c crate, so Rust src changes also invalidate the + # SDK. (No separate "sdk needs libboxlite rebuild" output — + # build_c_sdk gates on either runner_chain or sdk_py.) + sdk_py: + - 'sdks/python/**' + # Test code + workflow self-modifications. Doesn't trigger + # any rebuild — just runs the existing deployed stack. + tests_or_workflow: + - 'scripts/test/e2e/**' + - '.github/workflows/e2e-cloud.yml' + - '.github/workflows/build-runner-binary.yml' + - '.github/workflows/build-c.yml' + + # Build libboxlite.a from THIS PR's Rust source using the existing + # build-c.yml workflow (manylinux container, libseccomp.a cross-build, + # fix-go-symbols.sh — same chain release builds use). target_filter + # constrains the matrix to just linux-x64-gnu (skip macos-15 + + # ubuntu-24.04-arm matrix entries; Tokyo runner is amd64). + # Uploads artifact c-sdk-linux-x64-gnu consumed by build_runner. + # + # Fires only when libboxlite source OR runner Go OR Python SDK + # changed — pure API / dashboard / test-code PRs skip the ~9 min + # libboxlite compile entirely. + build_c_sdk: + name: Build C SDK (from PR source, linux-x64-gnu only) + needs: changes + if: | + needs.changes.outputs.runner_chain == 'true' + || needs.changes.outputs.sdk_py == 'true' + || github.event_name == 'workflow_dispatch' + uses: ./.github/workflows/build-c.yml + with: + target_filter: linux-x64-gnu + permissions: + contents: write + + # Build the runner binary from THIS PR's source. Reuses + # .github/workflows/build-runner-binary.yml with libboxlite_source=build, + # which downloads the c-sdk-linux-x64-gnu artifact from build_c_sdk + # above (instead of pulling the release tarball). The e2e job + # downloads the produced runner-linux-amd64 artifact and SSM-deploys + # it to the Tokyo runner EC2. + # + # Fires only when runner_chain changed. Pure API/SDK/test PRs skip + # the ~2 min Go build + the EC2 self-update step downstream. + build_runner: + name: Build runner binary (from PR source) + needs: [changes, build_c_sdk] + if: | + needs.changes.outputs.runner_chain == 'true' + || github.event_name == 'workflow_dispatch' + uses: ./.github/workflows/build-runner-binary.yml + with: + libboxlite_source: build + # Inherit workflow defaults — the called workflow's `upload-to-release` + # job declares `contents: write`, and explicitly setting `contents: read` + # at the caller would fail GHA's "called job requests more permissions + # than caller grants" startup check even though that job's `if:` would + # skip it for our workflow_call invocation. + permissions: + contents: write + + e2e: + name: E2E suite (Tokyo) + needs: [changes, build_c_sdk, build_runner] + # Default GHA semantics: a job whose `needs:` is SKIPPED also gets + # skipped. Override via !failure() && !cancelled() — runs as long + # as nothing in the upstream chain actively failed (skipped is OK, + # which is exactly what happens on pure API / test PRs). + if: | + !failure() && !cancelled() + && (needs.changes.outputs.any == 'true' || github.event_name == 'workflow_dispatch') + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v5 + + - name: Configure AWS credentials (OIDC) + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.AWS_ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + role-session-name: e2e-cloud-${{ github.run_id }} + + # ────────────────────────────────────────────────────────────────── + # Resolve runtime resource identifiers. Cluster suffix is random + # per stack instantiation; runner instance id is found by Name + # tag, not hardcoded — so SST teardown/recreate doesn't break CI. + # Each lookup is GUARDED by an exact count assertion: if more + # than one candidate matches the pattern (e.g. orphan ALB from a + # failed teardown), the workflow fails loudly instead of silently + # binding to the wrong resource. + # ────────────────────────────────────────────────────────────────── + - name: Resolve stack resources + id: resources + run: | + set -euo pipefail + + assert_one() { + local kind="$1" count="$2" + if [ "$count" -ne 1 ]; then + echo "::error::Expected exactly 1 $kind, found $count — clean up orphans before re-running." + exit 1 + fi + } + + CLUSTER_COUNT=$(aws ecs list-clusters \ + --query "length(clusterArns[?contains(@, '${ECS_CLUSTER_PATTERN}')])" --output text) + assert_one "ECS cluster matching ${ECS_CLUSTER_PATTERN}" "$CLUSTER_COUNT" + CLUSTER=$(aws ecs list-clusters \ + --query "clusterArns[?contains(@, '${ECS_CLUSTER_PATTERN}')]|[0]" \ + --output text | awk -F/ '{print $NF}') + echo "cluster=$CLUSTER" >> "$GITHUB_OUTPUT" + + LB_COUNT=$(aws elbv2 describe-load-balancers \ + --query "length(LoadBalancers[?starts_with(LoadBalancerName,'ApiLoadBalancer-')])" --output text) + assert_one "ApiLoadBalancer-*" "$LB_COUNT" + API_LB_DNS=$(aws elbv2 describe-load-balancers \ + --query "LoadBalancers[?starts_with(LoadBalancerName,'ApiLoadBalancer-')]|[0].DNSName" \ + --output text) + API_LB_TG_ARN=$(aws elbv2 describe-load-balancers \ + --query "LoadBalancers[?starts_with(LoadBalancerName,'ApiLoadBalancer-')]|[0].LoadBalancerArn" \ + --output text \ + | xargs -I{} aws elbv2 describe-target-groups --load-balancer-arn {} \ + --query "TargetGroups[0].TargetGroupArn" --output text) + echo "api_lb_dns=$API_LB_DNS" >> "$GITHUB_OUTPUT" + echo "api_lb_tg_arn=$API_LB_TG_ARN" >> "$GITHUB_OUTPUT" + + RUNNER_COUNT=$(aws ec2 describe-instances \ + --filters "Name=tag:Name,Values=boxlite-runner" "Name=instance-state-name,Values=running" \ + --query "length(Reservations[].Instances[])" --output text) + assert_one "boxlite-runner EC2 instance" "$RUNNER_COUNT" + RUNNER_ID=$(aws ec2 describe-instances \ + --filters "Name=tag:Name,Values=boxlite-runner" "Name=instance-state-name,Values=running" \ + --query "Reservations[0].Instances[0].InstanceId" --output text) + echo "runner_id=$RUNNER_ID" >> "$GITHUB_OUTPUT" + + API_TD_ARN=$(aws ecs describe-services --cluster "$CLUSTER" --services Api \ + --query 'services[0].taskDefinition' --output text) + if [ -z "$API_TD_ARN" ] || [ "$API_TD_ARN" = "None" ]; then + echo "::error::Could not resolve Api task definition from ECS service Api." + exit 1 + fi + + API_TD_JSON=$(aws ecs describe-task-definition --task-definition "$API_TD_ARN") + S3_BUCKET=$(jq -r '.taskDefinition.containerDefinitions[].environment[]? | select(.name == "S3_DEFAULT_BUCKET") | .value' \ + <<<"$API_TD_JSON" | head -1) + if [ -z "$S3_BUCKET" ] || [ "$S3_BUCKET" = "null" ]; then + S3_BUCKET=$(jq -r '.taskDefinition.containerDefinitions[].environment[]? | select(.name == "ADMIN_OBSERVABILITY_S3_BUCKETS") | .value' \ + <<<"$API_TD_JSON" | head -1 | cut -d, -f1) + fi + if [ -z "$S3_BUCKET" ] || [ "$S3_BUCKET" = "null" ]; then + S3_BUCKETS=$(aws s3api list-buckets \ + --query "Buckets[?starts_with(Name,'${STACK_PREFIX}-storagebucket')].Name" \ + --output text) + BUCKET_COUNT=$(echo "$S3_BUCKETS" | wc -w | tr -d ' ') + assert_one "${STACK_PREFIX}-storagebucket* S3 bucket" "$BUCKET_COUNT" + S3_BUCKET=$(echo "$S3_BUCKETS" | awk '{print $1}') + fi + echo "s3_bucket=$S3_BUCKET" >> "$GITHUB_OUTPUT" + + # Preflight: ECR repo must exist (hardcoded SST default — fail + # fast if SST is reconfigured to a per-stage repo). + aws ecr describe-repositories --repository-names "${ECR_REPO}" >/dev/null \ + || { echo "::error::ECR repo ${ECR_REPO} not found — SST may have been reconfigured. Update ECR_REPO env in this workflow."; exit 1; } + + # SSM is useful for diagnostics, but runner self-update uses the + # EC2-side S3 poller. If SSM is wedged, try one reboot recovery and + # continue so the real deploy/test path decides the run. + get_ssm_ping() { + aws ssm describe-instance-information \ + --filters "Key=InstanceIds,Values=$RUNNER_ID" \ + --query "InstanceInformationList[0].PingStatus" --output text + } + + PING=$(get_ssm_ping) + if [ "$PING" != "Online" ]; then + echo "::warning::SSM agent on runner $RUNNER_ID is not Online (PingStatus=$PING). Rebooting once and waiting for recovery." + if aws ec2 reboot-instances --instance-ids "$RUNNER_ID"; then + aws ec2 wait instance-status-ok --instance-ids "$RUNNER_ID" || true + for i in $(seq 1 60); do + PING=$(get_ssm_ping) + if [ "$PING" = "Online" ]; then + break + fi + echo " [$i/60] waiting for SSM Online (current=$PING)..." + sleep 5 + done + else + echo "::warning::Could not reboot runner $RUNNER_ID. Continuing because runner deploy uses the S3 poller, not SSM." + fi + + if [ "$PING" != "Online" ]; then + echo "::warning::SSM agent on runner $RUNNER_ID is still not Online (PingStatus=$PING). Continuing; failure logs may be incomplete." + fi + fi + + echo "::notice::cluster=$CLUSTER api_lb=$API_LB_DNS runner=$RUNNER_ID (SSM=$PING) s3=$S3_BUCKET" + + # ────────────────────────────────────────────────────────────────── + # Build & push Api container image (Node.js / NestJS), tag = git sha. + # ────────────────────────────────────────────────────────────────── + - name: ECR login + id: ecr_login + uses: aws-actions/amazon-ecr-login@v2 + + # ────────────────────────────────────────────────────────────────── + # Runner deploy is ordered BEFORE the API image build/deploy: + # — fail fast on runner regressions (most pytest cases depend on + # the runner being up; broken runner = no point spending the + # 10-15 min api build + ECS rolling deploy); + # — when the build_runner job was skipped (no runner_chain change), + # both runner steps below skip too and we go straight to API. + # + # Download runner binary built by the `build_runner` job (which + # reuses .github/workflows/build-runner-binary.yml via workflow_call + # with libboxlite_source=build so libboxlite.a is compiled from + # THIS PR's source, not the v${VERSION} tarball). + # Push to the stack's S3 bucket; an EC2-side systemd-timer poller + # (installed out-of-band via PowerUserAccess) picks up new + # binaries every 30s and hot-swaps via systemctl restart. + # ────────────────────────────────────────────────────────────────── + - name: Download runner binary artifact + if: | + needs.changes.outputs.runner_chain == 'true' + || github.event_name == 'workflow_dispatch' + uses: actions/download-artifact@v4 + with: + name: runner-linux-amd64 + path: /tmp/runner-artifact/ + + # ────────────────────────────────────────────────────────────────── + # Polling-based deploy (zero IAM mutation required): + # + # 1. Upload binary to s3://${bucket}/builds/runner-deploy/binary.tar.gz + # 2. Write request marker s3://.../builds/runner-deploy/request-${SHA}.txt + # 3. EC2 poller (systemd timer installed once via PowerUserAccess + # SSM, NOT via this workflow) picks up request, swaps binary, + # writes done-${SHA}.txt with "OK" or "FAIL: ..." status + # 4. We poll for done-${SHA}.txt, gate on its content + # + # This replaces the original `aws ssm send-command` path which + # needed an IAM policy update (split condition on the + # AWS-RunShellScript document — blocked without admin perms). + # Now the only AWS interaction here is S3, which is already + # in our role's policy (`s3:PutObject`/`GetObject` on + # `${stack_prefix}-storagebucket-*/builds/*`). + # ────────────────────────────────────────────────────────────────── + - name: Upload runner binary to S3 + wait for runner self-update + if: | + needs.changes.outputs.runner_chain == 'true' + || github.event_name == 'workflow_dispatch' + run: | + set -euo pipefail + S3_BUCKET="${{ steps.resources.outputs.s3_bucket }}" + ARCHIVE=$(ls /tmp/runner-artifact/boxlite-runner-*-linux-amd64.tar.gz | head -1) + [ -n "$ARCHIVE" ] || { echo "::error::No runner artifact found"; exit 1; } + + PREFIX="builds/runner-deploy" + BIN_KEY="${PREFIX}/binary.tar.gz" + REQ_KEY="${PREFIX}/request-${GITHUB_SHA}.txt" + DONE_KEY="${PREFIX}/done-${GITHUB_SHA}.txt" + + # 1) Upload binary (canonical name; overwrite OK because the + # workflow concurrency group is a singleton lock). + aws s3 cp "$ARCHIVE" "s3://${S3_BUCKET}/${BIN_KEY}" + echo "::notice::Binary uploaded to s3://${S3_BUCKET}/${BIN_KEY}" + + # 2) Write request marker — content = git SHA so the poller + # knows which request to answer with the matching done key. + printf 'sha=%s\nrequested_at=%s\n' \ + "${GITHUB_SHA}" \ + "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + | aws s3 cp - "s3://${S3_BUCKET}/${REQ_KEY}" + echo "::notice::Request marker written to s3://${S3_BUCKET}/${REQ_KEY}" + + # 3) Wait for the poller (runs every 30s on the Tokyo EC2) + # to claim the request and write a done marker. Cap at + # 120 s (poller cron 30 s + binary swap 5 s + service + # start 5 s = ~60 s typical; double for slack). + echo "Waiting for runner self-update (max 120 s)..." + for i in $(seq 1 24); do + sleep 5 + if aws s3api head-object \ + --bucket "$S3_BUCKET" --key "$DONE_KEY" \ + >/dev/null 2>&1; then + STATUS=$(aws s3 cp "s3://${S3_BUCKET}/${DONE_KEY}" - 2>/dev/null | head -1 || echo "") + echo "::notice::Runner deploy status: $STATUS" + case "$STATUS" in + OK*) + echo "::notice::Runner self-update complete (SHA ${GITHUB_SHA})" + exit 0 ;; + FAIL*) + echo "::error::Runner self-update failed: $STATUS" + # Pull full body for context + aws s3 cp "s3://${S3_BUCKET}/${DONE_KEY}" - 2>/dev/null || true + exit 1 ;; + *) + echo "::error::Unexpected status from poller: $STATUS" + exit 1 ;; + esac + fi + echo " [$i/24] waiting for done-${GITHUB_SHA}.txt..." + done + echo "::error::Runner self-update timed out after 120 s — poller offline or SSM agent down?" + exit 1 + + - name: Build & push Api image (source mode via tsx, no webpack) + id: build_api + if: | + needs.changes.outputs.api == 'true' + || github.event_name == 'workflow_dispatch' + run: | + set -euo pipefail + REGISTRY="${{ steps.ecr_login.outputs.registry }}" + IMAGE="${REGISTRY}/${ECR_REPO}:api-${{ github.sha }}" + # Use apps/api/Dockerfile.source — runs TS directly via tsx at + # container runtime, NO webpack build, NO dashboard build, NO + # strict TS type-check gate. See that file's header for rationale. + # Build context = repo root (Dockerfile references apps/libs/...). + docker buildx build --platform linux/amd64 --load \ + -f apps/api/Dockerfile.source -t "$IMAGE" . + docker push "$IMAGE" + echo "image=$IMAGE" >> "$GITHUB_OUTPUT" + + # ────────────────────────────────────────────────────────────────── + # Deploy: register new Api task definition with the new image (all + # other env vars preserved verbatim), force-redeploy the service, + # wait for stable, then verify the ALB target group reports healthy. + # `wait services-stable` alone is NOT sufficient — ECS service can + # be stable before the ALB health check passes; an immediate test + # run can race a 503 from the LB. + # ────────────────────────────────────────────────────────────────── + - name: Deploy Api (rolling) + if: | + needs.changes.outputs.api == 'true' + || github.event_name == 'workflow_dispatch' + run: | + set -euo pipefail + CLUSTER="${{ steps.resources.outputs.cluster }}" + IMAGE="${{ steps.build_api.outputs.image }}" + TG_ARN="${{ steps.resources.outputs.api_lb_tg_arn }}" + + OLD_TD_ARN=$(aws ecs describe-services --cluster "$CLUSTER" --services Api \ + --query 'services[0].taskDefinition' --output text) + echo "Old TD: $OLD_TD_ARN" + + # jq deep-clone the task def, swap container image, strip + # readonly fields. The task def env contains plaintext + # DB_PASSWORD; never `cat`/echo new-td.json — it goes + # straight to AWS via --cli-input-json. + aws ecs describe-task-definition --task-definition "$OLD_TD_ARN" \ + --query 'taskDefinition' --output json \ + | jq --arg img "$IMAGE" ' + .containerDefinitions[0].image = $img + | del(.taskDefinitionArn, .revision, .status, .requiresAttributes, + .compatibilities, .registeredAt, .registeredBy)' \ + > /tmp/new-td.json + + NEW_TD_ARN=$(aws ecs register-task-definition \ + --cli-input-json file:///tmp/new-td.json \ + --query 'taskDefinition.taskDefinitionArn' --output text) + echo "New TD: $NEW_TD_ARN" + + aws ecs update-service --cluster "$CLUSTER" --service Api \ + --task-definition "$NEW_TD_ARN" --force-new-deployment \ + >/dev/null + aws ecs wait services-stable --cluster "$CLUSTER" --services Api + echo "Service stable — verifying PRIMARY is NEW_TD..." + + # ECS DeploymentCircuitBreaker auto-rolls-back when new tasks + # keep failing to start. After rollback the SERVICE is stable + # again (running == desired on the OLD td), so `wait + # services-stable` returns success and the ALB target group + # reports healthy — but the new image is NOT running. Both + # the ALB check and `/api/health` below would talk to the + # OLD tasks and answer 2xx, masking a failed deploy. + # Explicitly assert the PRIMARY deployment's taskDefinition + # equals NEW_TD_ARN. If it differs, ECS rolled back. + PRIMARY_TD=$(aws ecs describe-services --cluster "$CLUSTER" --services Api \ + --query 'services[0].deployments[?status==`PRIMARY`]|[0].taskDefinition' \ + --output text) + if [ "$PRIMARY_TD" != "$NEW_TD_ARN" ]; then + echo "::error::ECS rolled back. PRIMARY=$PRIMARY_TD expected $NEW_TD_ARN" + # Pull stop reasons from recent failed tasks for context + aws ecs list-tasks --cluster "$CLUSTER" --service-name Api \ + --desired-status STOPPED --max-results 5 \ + --query 'taskArns' --output text \ + | tr '\t' '\n' | head -3 \ + | while read -r TASK; do + [ -n "$TASK" ] || continue + echo "--- stopped task $(basename "$TASK") ---" + aws ecs describe-tasks --cluster "$CLUSTER" --tasks "$TASK" \ + --query 'tasks[0].[stoppedReason,containers[0].exitCode,containers[0].reason]' \ + --output text || true + done + exit 1 + fi + echo "::notice::PRIMARY deployment confirmed on $NEW_TD_ARN" + + # Now verify the ALB target group is actually serving — wait + # for at least one healthy target. Up to ~3 minutes for health + # check thresholds to clear. + for i in $(seq 1 18); do + HEALTHY=$(aws elbv2 describe-target-health \ + --target-group-arn "$TG_ARN" \ + --query "length(TargetHealthDescriptions[?TargetHealth.State=='healthy'])" \ + --output text) + if [ "$HEALTHY" -ge 1 ]; then + echo "::notice::ALB target group reports $HEALTHY healthy target(s)" + break + fi + echo "ALB target group has 0 healthy targets — retry $i/18 in 10s" + sleep 10 + done + [ "$HEALTHY" -ge 1 ] || { echo "::error::ALB target group never reported healthy"; exit 1; } + + # Belt-and-suspenders: hit /api/health through the LB + API_DNS="${{ steps.resources.outputs.api_lb_dns }}" + curl -fsS --retry 6 --retry-delay 5 --retry-all-errors --max-time 10 \ + -o /dev/null "http://${API_DNS}/api/health" + echo "::notice::/api/health returned 2xx" + + # ────────────────────────────────────────────────────────────────── + # Runner binary deploy is also omitted (no rebuild step above). + # See the "Runner binary rebuild is INTENTIONALLY OMITTED" note + # earlier in this job. The deployed Tokyo runner EC2's existing + # binary stays in place. + # ────────────────────────────────────────────────────────────────── + + # ────────────────────────────────────────────────────────────────── + # One-time init: admin-org sandbox quota. Naturally idempotent + # because the SET clause always writes the same target values; we + # drop the `AND max_cpu_per_box=0` guard the first draft had — + # it prevented convergence from partial-update states. The SELECT + # afterwards prints the post-state for the workflow log. + # + # Credentials: ECS Exec into the Api container so DB_PASSWORD is + # read from the container's env at runtime, not from the SSM + # command body. The outer single-quotes prevent the GHA shell + # from expanding $DB_PASSWORD/$DB_HOST/etc. — they only resolve + # inside the container. + # ────────────────────────────────────────────────────────────────── + - name: Init admin-org sandbox quota (idempotent) + run: | + set -euo pipefail + CLUSTER="${{ steps.resources.outputs.cluster }}" + + # Pick a task that belongs to the PRIMARY deployment (avoids + # picking the still-draining old task during a rolling deploy). + PRIMARY_TD=$(aws ecs describe-services --cluster "$CLUSTER" --services Api \ + --query 'services[0].deployments[?status==`PRIMARY`]|[0].taskDefinition' --output text) + TASK=$(aws ecs list-tasks --cluster "$CLUSTER" --service-name Api \ + --query 'taskArns[]' --output text \ + | tr '\t' '\n' \ + | while read -r arn; do + TD=$(aws ecs describe-tasks --cluster "$CLUSTER" --tasks "$arn" \ + --query 'tasks[0].taskDefinitionArn' --output text) + if [ "$TD" = "$PRIMARY_TD" ]; then echo "$arn"; break; fi + done) + [ -n "$TASK" ] || { echo "::error::No Api task on PRIMARY deployment"; exit 1; } + echo "Api task on PRIMARY deployment: $TASK" + + # Capture the ECS Exec session output to a temp file so we can + # grep it for the SELECT result. --interactive is required by + # ECS Exec, but a non-TTY shell works because we feed nothing + # to stdin and our `sh -c '…'` exits immediately after psql. + OUT=/tmp/ecs_exec.log + # psql flags: + # -A unaligned output (no column padding, no wrapping) + # -t tuples only (omit header + footer rows) + # -P pager=off defensive — psql defaults to less/more when + # attached to a TTY-like session even with --interactive, + # which wraps long lines and adds "--More--" prompts + # that broke the previous grep + # PAGER=cat belt-and-braces in case psql calls $PAGER directly + aws ecs execute-command --cluster "$CLUSTER" --task "$TASK" --container Api --interactive \ + --command 'sh -c "PAGER=cat PGPASSWORD=\"\$DB_PASSWORD\" psql -h \"\$DB_HOST\" -U \"\$DB_USERNAME\" -d \"\$DB_DATABASE\" -A -t -P pager=off -v ON_ERROR_STOP=1 -c \"UPDATE \\\"organization\\\" SET max_cpu_per_box=4, max_memory_per_box=8192, max_disk_per_box=20 WHERE id='\''4a1eef70-8734-4814-b7a5-0ca3522a8760'\''; SELECT id, max_cpu_per_box, max_memory_per_box, max_disk_per_box FROM \\\"organization\\\" WHERE id='\''4a1eef70-8734-4814-b7a5-0ca3522a8760'\'';\""' \ + 2>&1 | tee "$OUT" + + # Verify the SELECT result actually shows the target values. + # With -A -t output, the SELECT row is one line: "|4|8192|20". + # ECS Exec exit-code propagation through Session Manager is + # historically unreliable; grep the output as ground truth. + # + # ECS Exec routes through SSM Session Manager, which uses CRLF + # line endings, so each output line has a trailing \r. Strip + # CR before greping so the `$` anchor matches reliably. + tr -d '\r' < "$OUT" \ + | grep -E '\|4\|8192\|20$' \ + || { echo "::error::admin-org quota SELECT did not show target values (|4|8192|20)"; exit 1; } + + # ────────────────────────────────────────────────────────────────── + # Build SDK from THIS checkout (path_prefix and other source-tree + # additions may be ahead of the PyPI release with the same version + # string). Required so tests exercise the code under review. + # ────────────────────────────────────────────────────────────────── + - name: Build & install Python SDK from this checkout + # BOXLITE_DEPS_STUB=1 skips the vendor-submodule builds + # (bubblewrap, libkrun, e2fsprogs) — those are needed only by the + # runner inside the EC2 host, not by the Python SDK / pytest + # client. The repo's src/deps/*-sys build.rs files all honor + # this env var and emit a no-op `/nonexistent` linker hint. + # Avoids needing `git submodule update --init --recursive` + + # meson/ninja in the GHA runner image. + # + # protoc is required by boxlite-shared's build.rs (gRPC proto + # compilation); ubuntu-latest doesn't preinstall it. + env: + BOXLITE_DEPS_STUB: "1" + run: | + set -euo pipefail + sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends protobuf-compiler + cd sdks/python + pip install --break-system-packages --quiet maturin + maturin build --release + pip install --break-system-packages --force-reinstall ../../target/wheels/boxlite-*.whl + pip install --break-system-packages --quiet pytest pytest-asyncio pytest-timeout + + # ────────────────────────────────────────────────────────────────── + # Pytest configuration: write the profile pointing at Tokyo API LB, + # admin key sourced from SSM Parameter Store and immediately + # registered as a GHA log mask so it can never leak into the run + # log even if a later step accidentally `echo`s credentials.toml. + # Use printf with quoted args so a `$` inside the secret can't + # be shell-expanded. + # ────────────────────────────────────────────────────────────────── + - name: Configure pytest profile p1 + run: | + set -euo pipefail + API_DNS="${{ steps.resources.outputs.api_lb_dns }}" + ADMIN_KEY=$(aws ssm get-parameter \ + --name "/boxlite/${STAGE}/admin-api-key" \ + --with-decryption \ + --query Parameter.Value --output text) + # Register the value as a masked secret BEFORE it could ever + # appear in subsequent logging. + echo "::add-mask::$ADMIN_KEY" + + # The Python SDK's REST client builds URLs as + # `{url}/v1/{path_prefix}/{endpoint}` (api-routes proxied by + # apps/api/src/boxlite-rest/boxlite-{box,proxy}.controller.ts, + # `@Controller('v1/:prefix/boxes')`). The `:prefix` is the + # caller's organization-scoped path prefix, returned by + # `GET /api/v1/me` for the admin API key. Without it the + # SDK hits `/api/v1/boxes` which the NestJS router does NOT + # match (404 "Cannot POST /api/v1/boxes"). + PATH_PREFIX=$(curl -fsS -H "Authorization: Bearer $ADMIN_KEY" \ + "http://${API_DNS}/api/v1/me" | python3 -c 'import json,sys; print(json.load(sys.stdin)["path_prefix"])') + [ -n "$PATH_PREFIX" ] || { echo "::error::Empty path_prefix from /api/v1/me"; exit 1; } + echo "Resolved path_prefix=${PATH_PREFIX}" + + mkdir -p ~/.boxlite + chmod 700 ~/.boxlite + { + printf '[profiles.p1]\n' + printf 'url = "http://%s/api"\n' "$API_DNS" + printf 'api_key = "%s"\n' "$ADMIN_KEY" + printf 'path_prefix = "%s"\n' "$PATH_PREFIX" + } > ~/.boxlite/credentials.toml + chmod 600 ~/.boxlite/credentials.toml + + # Register snapshots the e2e suite needs. POST /api/snapshots + # is async — the runner pulls the image in the background and + # the snapshot row transitions PENDING → PULLING → ACTIVE. + # Box creation requires ACTIVE state, so poll until each + # snapshot reports active. + export ADMIN_KEY API_DNS + python3 - <<'PY' + import json, os, sys, time, urllib.request, urllib.error, urllib.parse + API = f"http://{os.environ['API_DNS']}/api" + KEY = os.environ['ADMIN_KEY'] + SNAPS = ["alpine:3.23", "ubuntu:22.04", "ubuntu:24.04"] + WAIT = 240 # seconds per snapshot + + def req(method, path, body=None): + r = urllib.request.Request( + API + path, method=method, + headers={"Authorization": f"Bearer {KEY}", + "Content-Type": "application/json"}, + data=json.dumps(body).encode() if body is not None else None, + ) + try: + with urllib.request.urlopen(r, timeout=30) as resp: + return resp.status, json.loads(resp.read() or "null") + except urllib.error.HTTPError as e: + return e.code, json.loads(e.read() or "null") + + def state_of(name): + s, b = req("GET", f"/snapshots?name={urllib.parse.quote(name)}") + if s != 200: return None + for it in (b or {}).get("items", []): + if it.get("name") == name: + return it.get("state") + return None + + for name in SNAPS: + st = state_of(name) + if st is None: + s, b = req("POST", "/snapshots", + {"name": name, "imageName": name, + "cpu": 1, "memory": 1, "disk": 2}) + if s not in (200, 201): + sys.exit(f"POST /snapshots {name} → {s} {b}") + print(f" created {name}") + else: + print(f" {name}: existing state={st}") + + deadline = time.time() + WAIT + while time.time() < deadline: + st = state_of(name) + if st == "active": + print(f" {name}: active ✓"); break + if st in ("error", "build_failed"): + sys.exit(f" {name}: {st}") + time.sleep(5) + else: + sys.exit(f" {name} not active within {WAIT}s (last state={st})") + PY + unset ADMIN_KEY + + - name: Run E2E suite + env: + BOXLITE_E2E_SKIP_PATH_VERIFY: '1' + BOXLITE_E2E_PROFILE: p1 + run: | + # pytest-timeout caps individual test wall time so a hung VM / + # wedged exec doesn't burn the full 45-minute job timeout (which + # would prevent the on-failure log capture step from running). + timeout 35m python3 -m pytest scripts/test/e2e/cases/ \ + -v --tb=short --no-header -p no:cacheprovider \ + --timeout=180 --timeout-method=thread \ + --junit-xml=pytest-junit.xml + + - name: Upload pytest junit XML + if: always() + uses: actions/upload-artifact@v4 + with: + name: pytest-junit + path: pytest-junit.xml + if-no-files-found: ignore + + # ────────────────────────────────────────────────────────────────── + # On failure, pull the last 200 lines from each side. CloudWatch + # for the Api container (its stdout is shipped via awslogs driver); + # runner journalctl via SSM with a short poll loop instead of a + # fixed sleep. + # ────────────────────────────────────────────────────────────────── + - name: Collect logs on failure + if: failure() + continue-on-error: true + run: | + set +e + CLUSTER="${{ steps.resources.outputs.cluster }}" + RUNNER="${{ steps.resources.outputs.runner_id }}" + API_TD=$(aws ecs describe-services --cluster "$CLUSTER" --services Api \ + --query 'services[0].taskDefinition' --output text) + LOG_GROUP=$(aws ecs describe-task-definition --task-definition "$API_TD" \ + --query 'taskDefinition.containerDefinitions[0].logConfiguration.options."awslogs-group"' --output text) + echo "──── Api CloudWatch logs (last 500) ────" + aws logs tail "$LOG_GROUP" --since 20m --format short | tail -500 + + echo "──── boxlite-runner journalctl (last 500) ────" + CMD_ID=$(aws ssm send-command --instance-ids "$RUNNER" \ + --document-name AWS-RunShellScript --comment "e2e-cloud failure log dump" \ + --parameters 'commands=["journalctl -u boxlite-runner --no-pager -n 500"]' \ + --query Command.CommandId --output text) + for _ in $(seq 1 12); do + SS=$(aws ssm get-command-invocation --command-id "$CMD_ID" \ + --instance-id "$RUNNER" --query Status --output text 2>/dev/null || echo Pending) + case "$SS" in + Success|Failed|Cancelled|TimedOut) break ;; + *) sleep 3 ;; + esac + done + aws ssm get-command-invocation --command-id "$CMD_ID" --instance-id "$RUNNER" \ + --query StandardOutputContent --output text | tail -500 + + # NOTE: an `e2e-gate` consolidation job has been INTENTIONALLY OMITTED + # for now. The gate's purpose is to give branch protection a stable + # always-reported status check — but adding it back as a required check + # before the Tokyo stack's AWS prerequisites are provisioned would block + # every PR (the e2e job has no path to succeed until then). Re-introduce + # the gate (and add it to branch protection) once a first green run + # against Tokyo is observed. diff --git a/.github/workflows/e2e-stack.yml b/.github/workflows/e2e-stack.yml index e815588d8..695770e9f 100644 --- a/.github/workflows/e2e-stack.yml +++ b/.github/workflows/e2e-stack.yml @@ -61,6 +61,8 @@ jobs: # code under review, not whatever wheel happened to be installed. - name: Rebuild Python SDK wheel from this checkout run: | + source "$HOME/.cargo/env" 2>/dev/null || true + export PATH="$HOME/.cargo/bin:$PATH" cd sdks/python pip install --break-system-packages --quiet maturin maturin build --release --quiet diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index c05feb7ce..5d20f0cd9 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -17,7 +17,7 @@ # # Authentication: # AWS: GitHub OIDC → AWS STS (no stored AWS credentials) -# GitHub: GitHub App → short-lived installation token (no PAT) +# GitHub App: optional; only required when creating a new self-hosted runner # # Setup: ./scripts/ci/setup-ci-runner.sh name: E2E Tests @@ -106,8 +106,22 @@ jobs: outputs: instance-id: ${{ steps.ensure-instance.outputs.instance-id }} steps: + - name: Detect GitHub App credentials + id: gh-app + env: + GH_APP_ID: ${{ vars.GH_APP_ID }} + GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} + run: | + if [ -n "$GH_APP_ID" ] && [ -n "$GH_APP_PRIVATE_KEY" ]; then + echo "available=true" >> "$GITHUB_OUTPUT" + else + echo "available=false" >> "$GITHUB_OUTPUT" + echo "::notice::GH_APP_ID/GH_APP_PRIVATE_KEY not configured; reusing existing EC2 runner only." + fi + - name: Generate GitHub App token id: app-token + if: steps.gh-app.outputs.available == 'true' uses: actions/create-github-app-token@v1 with: app-id: ${{ vars.GH_APP_ID }} @@ -171,10 +185,14 @@ jobs: else echo "Instance not found or terminated. Creating new one..." + if [ -z "${GH_APP_TOKEN:-}" ]; then + echo "::error::GH_APP_ID/GH_APP_PRIVATE_KEY are required to register a newly-created self-hosted runner." + exit 1 + fi # Generate runner registration token for user-data REG_TOKEN=$(curl -sf -X POST \ - -H "Authorization: token ${{ steps.app-token.outputs.token }}" \ + -H "Authorization: token ${GH_APP_TOKEN}" \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/${{ github.repository }}/actions/runners/registration-token" \ | jq -r .token) @@ -256,9 +274,19 @@ jobs: fi env: GH_TOKEN: ${{ steps.app-token.outputs.token }} + GH_APP_TOKEN: ${{ steps.app-token.outputs.token }} - name: Wait for runner to come online + env: + GH_APP_TOKEN: ${{ steps.app-token.outputs.token }} run: | + if [ -z "${GH_APP_TOKEN:-}" ]; then + echo "::notice::Skipping GitHub runner-online API poll because GitHub App credentials are not configured." + echo "The self-hosted e2e-tests job will queue until the existing EC2 runner connects." + sleep 30 + exit 0 + fi + TIMEOUT=180 ELAPSED=0 INTERVAL=10 @@ -267,7 +295,7 @@ jobs: while [ $ELAPSED -lt $TIMEOUT ]; do STATUS=$(curl -sf \ - -H "Authorization: token ${{ steps.app-token.outputs.token }}" \ + -H "Authorization: token ${GH_APP_TOKEN}" \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/${{ github.repository }}/actions/runners" \ | jq -r '.runners[] | select(.labels[].name == "${{ env.RUNNER_LABEL }}") | .status // empty' \ diff --git a/apps/api/src/admin/services/observability.service.ts b/apps/api/src/admin/services/observability.service.ts index 8c739fefe..f726beedd 100644 --- a/apps/api/src/admin/services/observability.service.ts +++ b/apps/api/src/admin/services/observability.service.ts @@ -8,11 +8,7 @@ import { Inject, Injectable, ServiceUnavailableException } from '@nestjs/common' import { ClickHouseService } from '../../clickhouse/clickhouse.service' import { TypedConfigService } from '../../config/typed-config.service' import { LogEntryDto } from '../../box-telemetry/dto/log-entry.dto' -import { - MetricsResponseDto, - MetricDataPointDto, - MetricSeriesDto, -} from '../../box-telemetry/dto/metrics-response.dto' +import { MetricsResponseDto, MetricDataPointDto, MetricSeriesDto } from '../../box-telemetry/dto/metrics-response.dto' import { PaginatedLogsDto } from '../../box-telemetry/dto/paginated-logs.dto' import { PaginatedTracesDto } from '../../box-telemetry/dto/paginated-traces.dto' import { TraceSpanDto } from '../../box-telemetry/dto/trace-span.dto' diff --git a/apps/api/src/app.service.ts b/apps/api/src/app.service.ts index 252366560..b847929e2 100644 --- a/apps/api/src/app.service.ts +++ b/apps/api/src/app.service.ts @@ -191,5 +191,4 @@ Admin API key ensured: ${this.maskApiKeyForLog(value)} } return `${value.slice(0, 4)}...${value.slice(-4)}` } - } diff --git a/apps/api/src/box/box.module.ts b/apps/api/src/box/box.module.ts index c38fa9026..45acecce2 100644 --- a/apps/api/src/box/box.module.ts +++ b/apps/api/src/box/box.module.ts @@ -58,16 +58,7 @@ import { BoxStateWaiterService } from './services/box-state-waiter.service' UserModule, OrganizationModule, RegionModule, - TypeOrmModule.forFeature([ - Box, - Runner, - WarmPool, - Volume, - SshAccess, - Region, - Job, - BoxLastActivity, - ]), + TypeOrmModule.forFeature([Box, Runner, WarmPool, Volume, SshAccess, Region, Job, BoxLastActivity]), ], controllers: [ BoxController, diff --git a/apps/api/src/box/constants/curated-images.constant.spec.ts b/apps/api/src/box/constants/curated-images.constant.spec.ts new file mode 100644 index 000000000..a800fc524 --- /dev/null +++ b/apps/api/src/box/constants/curated-images.constant.spec.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2025 Daytona Platforms Inc. + * Modified by BoxLite AI, 2025-2026 + * SPDX-License-Identifier: AGPL-3.0 + */ + +import { BadRequestError } from '../../exceptions/bad-request.exception' +import { CURATED_IMAGE_KEYS, resolveCuratedImageRef } from './curated-images.constant' + +describe('curated image allowlist', () => { + const ENV_KEYS = ['BOXLITE_SYSTEM_BASE_IMAGE', 'BOXLITE_SYSTEM_PYTHON_IMAGE', 'BOXLITE_SYSTEM_NODE_IMAGE'] + const saved: Record = {} + + beforeEach(() => { + // Isolate from the host env so the pinned fallback refs are deterministic. + for (const k of ENV_KEYS) { + saved[k] = process.env[k] + delete process.env[k] + } + }) + + afterEach(() => { + for (const k of ENV_KEYS) { + if (saved[k] === undefined) delete process.env[k] + else process.env[k] = saved[k] + } + }) + + it('exposes exactly the three curated keys', () => { + expect(CURATED_IMAGE_KEYS).toEqual(['base', 'python', 'node']) + }) + + it('resolves each key to its private ghcr ref', () => { + expect(resolveCuratedImageRef('base')).toContain('ghcr.io/boxlite-ai/boxlite-agent-base@sha256:') + expect(resolveCuratedImageRef('python')).toContain('ghcr.io/boxlite-ai/boxlite-agent-python@sha256:') + expect(resolveCuratedImageRef('node')).toContain('ghcr.io/boxlite-ai/boxlite-agent-node@sha256:') + }) + + it('defaults to base when no key is supplied', () => { + expect(resolveCuratedImageRef(undefined)).toBe(resolveCuratedImageRef('base')) + }) + + it('prefers the env-configured ref over the pinned fallback', () => { + process.env.BOXLITE_SYSTEM_PYTHON_IMAGE = 'ghcr.io/boxlite-ai/override@sha256:deadbeef' + expect(resolveCuratedImageRef('python')).toBe('ghcr.io/boxlite-ai/override@sha256:deadbeef') + }) + + it('rejects any key outside the allowlist at the boundary (no arbitrary OCI ref)', () => { + expect(() => resolveCuratedImageRef('alpine:3.23')).toThrow(BadRequestError) + expect(() => resolveCuratedImageRef('ghcr.io/evil/image:latest')).toThrow(BadRequestError) + expect(() => resolveCuratedImageRef('ubuntu')).toThrow(BadRequestError) + }) +}) diff --git a/apps/api/src/box/constants/curated-images.constant.ts b/apps/api/src/box/constants/curated-images.constant.ts new file mode 100644 index 000000000..b3cb11a6d --- /dev/null +++ b/apps/api/src/box/constants/curated-images.constant.ts @@ -0,0 +1,79 @@ +/* + * Copyright 2025 Daytona Platforms Inc. + * Modified by BoxLite AI, 2025-2026 + * SPDX-License-Identifier: AGPL-3.0 + */ + +import { BadRequestError } from '../../exceptions/bad-request.exception' + +/** + * Curated image keys are the only image identifiers a create request may supply. + * They are opaque keys, NOT raw OCI refs: users cannot pass an arbitrary image. + */ +export type CuratedImageKey = 'base' | 'python' | 'node' + +export const CURATED_IMAGE_KEYS: CuratedImageKey[] = ['base', 'python', 'node'] + +/** + * Reserved box label that holds the resolved OCI ref between create and the start action. + * Ephemeral storage in box.labels avoids a new entity column / migration. + */ +export const BOX_IMAGE_REF_LABEL = 'boxlite.io/image-ref' + +const DEFAULT_CURATED_IMAGE_KEY: CuratedImageKey = 'base' + +/** + * Each curated key maps to an env var holding a sha256-pinned, private ghcr OCI ref. + * These env vars are set on the Api service (apps/infra/sst.config.ts). The digests + * below are fallbacks kept in sync with that config for local/dev runs where the env + * is unset; the runner already authenticates to the private registry via its own token. + */ +const CURATED_IMAGE_ENV: Record = { + base: { + envVar: 'BOXLITE_SYSTEM_BASE_IMAGE', + fallbackRef: + 'ghcr.io/boxlite-ai/boxlite-agent-base@sha256:834dcb65465985fc2f648451d76c81d166bc7672391c9064a0a115ce6306c85f', + }, + python: { + envVar: 'BOXLITE_SYSTEM_PYTHON_IMAGE', + fallbackRef: + 'ghcr.io/boxlite-ai/boxlite-agent-python@sha256:80d562a57f4bc12def4e54dbdb9e7d26d3268fe0767a2955ab5ad718041145d6', + }, + node: { + envVar: 'BOXLITE_SYSTEM_NODE_IMAGE', + fallbackRef: + 'ghcr.io/boxlite-ai/boxlite-agent-node@sha256:fcb8b840ab68567975853666c82fb6c59a3c1d14a0cdc31d7cbf3a01e6c6d247', + }, +} + +function isCuratedImageKey(key: string): key is CuratedImageKey { + return (CURATED_IMAGE_KEYS as string[]).includes(key) +} + +/** + * Resolve a curated image key to its OCI ref. Undefined defaults to 'base'. + * Rejects any key outside the curated allowlist at the request boundary. + */ +export function resolveCuratedImageRef(key: string | undefined): string { + const resolvedKey = key ?? DEFAULT_CURATED_IMAGE_KEY + + if (!isCuratedImageKey(resolvedKey)) { + throw new BadRequestError(`Invalid image '${resolvedKey}'. Allowed images: ${CURATED_IMAGE_KEYS.join(', ')}`) + } + + const { envVar, fallbackRef } = CURATED_IMAGE_ENV[resolvedKey] + return process.env[envVar] || fallbackRef +} + +/** + * Reverse-map a resolved OCI ref back to its curated key, so API responses echo the + * opaque key the box was created with instead of the internal registry ref. Undefined + * when the ref is not one of the currently-resolved curated refs (e.g. boxes created + * before an env-var rotation). + */ +export function curatedImageKeyForRef(ref: string | undefined): CuratedImageKey | undefined { + if (!ref) { + return undefined + } + return CURATED_IMAGE_KEYS.find((key) => resolveCuratedImageRef(key) === ref) +} diff --git a/apps/api/src/box/controllers/runner.controller.ts b/apps/api/src/box/controllers/runner.controller.ts index ba4510d15..95854fdc3 100644 --- a/apps/api/src/box/controllers/runner.controller.ts +++ b/apps/api/src/box/controllers/runner.controller.ts @@ -20,15 +20,7 @@ import { } from '@nestjs/common' import { CreateRunnerDto } from '../dto/create-runner.dto' import { RunnerService } from '../services/runner.service' -import { - ApiOAuth2, - ApiTags, - ApiOperation, - ApiBearerAuth, - ApiResponse, - ApiParam, - ApiHeader, -} from '@nestjs/swagger' +import { ApiOAuth2, ApiTags, ApiOperation, ApiBearerAuth, ApiResponse, ApiParam, ApiHeader } from '@nestjs/swagger' import { SystemActionGuard } from '../../auth/system-action.guard' import { RequiredApiRole } from '../../common/decorators/required-role.decorator' import { SystemRole } from '../../user/enums/system-role.enum' diff --git a/apps/api/src/box/dto/create-box.dto.ts b/apps/api/src/box/dto/create-box.dto.ts index 9150c5b70..a6843b5dd 100644 --- a/apps/api/src/box/dto/create-box.dto.ts +++ b/apps/api/src/box/dto/create-box.dto.ts @@ -19,6 +19,10 @@ export class CreateBoxDto { @IsString() name?: string + @IsOptional() + @IsString() + image?: string + @ApiPropertyOptional({ description: 'The user associated with the project', example: 'boxlite', diff --git a/apps/api/src/box/managers/box-actions/box-start.action.ts b/apps/api/src/box/managers/box-actions/box-start.action.ts index 85993f5b1..f85d2e8f3 100644 --- a/apps/api/src/box/managers/box-actions/box-start.action.ts +++ b/apps/api/src/box/managers/box-actions/box-start.action.ts @@ -17,6 +17,7 @@ import { TypedConfigService } from '../../../config/typed-config.service' import { LockCode, RedisLockProvider } from '../../common/redis-lock.provider' import { WithSpan } from '../../../common/decorators/otel.decorator' import { BoxActivityService } from '../../services/box-activity.service' +import { BOX_IMAGE_REF_LABEL } from '../../constants/curated-images.constant' @Injectable() export class BoxStartAction extends BoxAction { @@ -56,24 +57,32 @@ export class BoxStartAction extends BoxAction { return DONT_SYNC_AGAIN } - // TODO(image-rewrite): the box boot pipeline (image resolution, runner artifact scheduling, - // pull orchestration, and runner createBox) was removed with the box_template subsystem. Boxes - // can no longer be booted until the image resolution layer is rebuilt; this handler fails the - // box explicitly instead. + // A freshly created box (UNKNOWN state, desired STARTED) boots from the curated image ref + // stashed on its labels at create time. Enqueue a CREATE_BOX job and move to CREATING; the + // job-completion path then drives CREATING -> STARTED. private async handleRunnerBoxUnknownStateOnDesiredStateStart(box: Box, lockCode: LockCode): Promise { const runner = await this.runnerService.findOneOrFail(box.runnerId) if (runner.state !== RunnerState.READY) { return DONT_SYNC_AGAIN } - await this.updateBoxState( - box, - BoxState.ERROR, - lockCode, - undefined, - 'Box image resolution is unavailable: the image/template subsystem was removed', - ) - return DONT_SYNC_AGAIN + const artifactRef = box.labels?.[BOX_IMAGE_REF_LABEL] + if (!artifactRef) { + await this.updateBoxState( + box, + BoxState.ERROR, + lockCode, + undefined, + `Box has no image ref (missing label ${BOX_IMAGE_REF_LABEL})`, + ) + return DONT_SYNC_AGAIN + } + + const runnerAdapter = await this.runnerAdapterFactory.create(runner) + await runnerAdapter.createBox(box, artifactRef) + + await this.updateBoxState(box, BoxState.CREATING, lockCode) + return SYNC_AGAIN } private async handleRunnerBoxStoppedStateOnDesiredStateStart(box: Box, lockCode: LockCode): Promise { diff --git a/apps/api/src/box/runner-adapter/runnerAdapter.ts b/apps/api/src/box/runner-adapter/runnerAdapter.ts index a55d159c2..883e3d036 100644 --- a/apps/api/src/box/runner-adapter/runnerAdapter.ts +++ b/apps/api/src/box/runner-adapter/runnerAdapter.ts @@ -46,6 +46,7 @@ export interface RunnerAdapter { runnerInfo(signal?: AbortSignal): Promise boxInfo(boxId: string): Promise + createBox(box: Box, artifactRef: string): Promise startBox( boxId: string, authToken: string, diff --git a/apps/api/src/box/runner-adapter/runnerAdapter.v0.ts b/apps/api/src/box/runner-adapter/runnerAdapter.v0.ts index 656d302ee..22860ddb0 100644 --- a/apps/api/src/box/runner-adapter/runnerAdapter.v0.ts +++ b/apps/api/src/box/runner-adapter/runnerAdapter.v0.ts @@ -144,6 +144,11 @@ export class RunnerAdapterV0 implements RunnerAdapter { } } + async createBox(_box: Box, _artifactRef: string): Promise { + // V0 (direct HTTP) create is out of MVP scope; only V2 (job-based) create is wired. + throw new Error('createBox is not supported for V0 runners') + } + async startBox( boxId: string, authToken: string, diff --git a/apps/api/src/box/runner-adapter/runnerAdapter.v2.createBox.spec.ts b/apps/api/src/box/runner-adapter/runnerAdapter.v2.createBox.spec.ts new file mode 100644 index 000000000..bac3f33cf --- /dev/null +++ b/apps/api/src/box/runner-adapter/runnerAdapter.v2.createBox.spec.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2025 Daytona Platforms Inc. + * Modified by BoxLite AI, 2025-2026 + * SPDX-License-Identifier: AGPL-3.0 + */ + +jest.mock('uuid', () => ({ v4: () => '00000000-0000-4000-8000-000000000000' })) + +import { Box } from '../entities/box.entity' +import { JobType } from '../enums/job-type.enum' +import { ResourceType } from '../enums/resource-type.enum' +import { RunnerAdapterV2 } from './runnerAdapter.v2' + +function createAdapter(createJob: jest.Mock): RunnerAdapterV2 { + const adapter = Object.create(RunnerAdapterV2.prototype) as RunnerAdapterV2 + ;(adapter as any).jobService = { createJob } + ;(adapter as any).runner = { id: 'runner-1' } + ;(adapter as any).logger = { debug: jest.fn() } + return adapter +} + +describe('RunnerAdapterV2.createBox (CREATE_BOX job payload)', () => { + const ARTIFACT_REF = 'ghcr.io/boxlite-ai/boxlite-agent-base@sha256:deadbeef' + + function buildBox(): Box { + const box = new Box('us', 'data-loader') + box.organizationId = '057963b2-60ca-4356-81fc-11503e15f249' + box.osUser = 'root' + box.cpu = 2 + box.mem = 4 + box.disk = 10 + box.gpu = 0 + return box + } + + it('enqueues a CREATE_BOX / BOX job for the box on its runner', async () => { + const createJob = jest.fn().mockResolvedValue(undefined) + const adapter = createAdapter(createJob) + const box = buildBox() + + await adapter.createBox(box, ARTIFACT_REF) + + expect(createJob).toHaveBeenCalledTimes(1) + const [, jobType, runnerId, resourceType, resourceId] = createJob.mock.calls[0] + expect(jobType).toBe(JobType.CREATE_BOX) + expect(resourceType).toBe(ResourceType.BOX) + expect(runnerId).toBe('runner-1') + expect(resourceId).toBe(box.id) + }) + + it('passes the image ref under `artifactRef` and NOT `snapshot` (Go validate:"required" trap)', async () => { + const createJob = jest.fn().mockResolvedValue(undefined) + const adapter = createAdapter(createJob) + const box = buildBox() + + await adapter.createBox(box, ARTIFACT_REF) + + const payload = createJob.mock.calls[0][5] as Record + expect(payload.artifactRef).toBe(ARTIFACT_REF) + expect('snapshot' in payload).toBe(false) + // resources must reach the runner so the VM is sized correctly + expect(payload.cpuQuota).toBe(2) + expect(payload.memoryQuota).toBe(4) + expect(payload.storageQuota).toBe(10) + }) +}) diff --git a/apps/api/src/box/runner-adapter/runnerAdapter.v2.ts b/apps/api/src/box/runner-adapter/runnerAdapter.v2.ts index b051aa305..594082632 100644 --- a/apps/api/src/box/runner-adapter/runnerAdapter.v2.ts +++ b/apps/api/src/box/runner-adapter/runnerAdapter.v2.ts @@ -114,6 +114,33 @@ export class RunnerAdapterV2 implements RunnerAdapter { } } + async createBox(box: Box, artifactRef: string): Promise { + // Hand-built payload: keys MUST match the Go dto.CreateBoxDTO json tags + // (apps/runner/pkg/api/dto/box.go). The image ref is passed as `artifactRef` — + // the runner uses it directly in runtime.Create (NOT `snapshot`). + const payload = { + id: box.id, + boxId: box.boxId, + userId: box.organizationId, + artifactRef, + osUser: box.osUser, + cpuQuota: box.cpu, + gpuQuota: box.gpu, + memoryQuota: box.mem, + storageQuota: box.disk, + env: box.env, + networkBlockAll: box.networkBlockAll, + networkAllowList: box.networkAllowList, + authToken: box.authToken, + organizationId: box.organizationId, + regionId: box.region, + } + + await this.jobService.createJob(null, JobType.CREATE_BOX, this.runner.id, ResourceType.BOX, box.id, payload) + + this.logger.debug(`Created CREATE_BOX job for box ${box.id} on runner ${this.runner.id}`) + } + async startBox( boxId: string, authToken: string, @@ -162,14 +189,7 @@ export class RunnerAdapterV2 implements RunnerAdapter { networkAllowList: box.networkAllowList, errorReason: box.errorReason, } - await this.jobService.createJob( - null, - JobType.RECOVER_BOX, - this.runner.id, - ResourceType.BOX, - box.id, - recoverBoxDTO, - ) + await this.jobService.createJob(null, JobType.RECOVER_BOX, this.runner.id, ResourceType.BOX, box.id, recoverBoxDTO) this.logger.debug(`Created RECOVER_BOX job for box ${box.id} on runner ${this.runner.id}`) } diff --git a/apps/api/src/box/services/box.service.replace-labels.spec.ts b/apps/api/src/box/services/box.service.replace-labels.spec.ts new file mode 100644 index 000000000..005175511 --- /dev/null +++ b/apps/api/src/box/services/box.service.replace-labels.spec.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2025 Daytona Platforms Inc. + * Modified by BoxLite AI, 2025-2026 + * SPDX-License-Identifier: AGPL-3.0 + */ + +import { Box } from '../entities/box.entity' +import { BoxService } from './box.service' +import { BOX_IMAGE_REF_LABEL } from '../constants/curated-images.constant' + +function createService(box: Box, update: jest.Mock): BoxService { + const service = Object.create(BoxService.prototype) as BoxService + ;(service as any).findOneByIdOrName = jest.fn().mockResolvedValue(box) + ;(service as any).boxRepository = { update } + return service +} + +describe('BoxService.replaceLabels reserved-label guard', () => { + const imageRef = 'ghcr.io/boxlite-ai/boxlite-agent-base@sha256:deadbeef' + + function boxWithImageRef(): Box { + const box = new Box('us', 'guarded-box') + box.id = '11111111-1111-4111-8111-111111111111' + box.labels = { [BOX_IMAGE_REF_LABEL]: imageRef, existing: 'keep' } + return box + } + + it('drops a user-supplied reserved image-ref label and preserves the resolved one', async () => { + const box = boxWithImageRef() + const update = jest.fn().mockImplementation((_id, { updateData }) => ({ ...box, ...updateData })) + const service = createService(box, update) + + await service.replaceLabels(box.id, { + [BOX_IMAGE_REF_LABEL]: 'ghcr.io/attacker/evil@sha256:0000', + mine: 'value', + }) + + const persistedLabels = update.mock.calls[0][1].updateData.labels + // The attacker's reserved key must not survive; the runner-pulled ref stays pinned to the + // server-resolved curated image. + expect(persistedLabels[BOX_IMAGE_REF_LABEL]).toBe(imageRef) + expect(persistedLabels.mine).toBe('value') + }) + + it('does not reintroduce the reserved label when the box never had one', async () => { + const box = new Box('us', 'plain-box') + box.id = '22222222-2222-4222-8222-222222222222' + box.labels = { foo: 'bar' } + const update = jest.fn().mockImplementation((_id, { updateData }) => ({ ...box, ...updateData })) + const service = createService(box, update) + + await service.replaceLabels(box.id, { foo: 'baz' }) + + const persistedLabels = update.mock.calls[0][1].updateData.labels + expect(persistedLabels).not.toHaveProperty(BOX_IMAGE_REF_LABEL) + expect(persistedLabels.foo).toBe('baz') + }) +}) diff --git a/apps/api/src/box/services/box.service.ts b/apps/api/src/box/services/box.service.ts index 942f78640..a937a9140 100644 --- a/apps/api/src/box/services/box.service.ts +++ b/apps/api/src/box/services/box.service.ts @@ -18,6 +18,7 @@ import { BoxError } from '../../exceptions/box-error.exception' import { BadRequestError } from '../../exceptions/bad-request.exception' import { Cron, CronExpression } from '@nestjs/schedule' import { BOX_WARM_POOL_UNASSIGNED_ORGANIZATION } from '../constants/box.constants' +import { BOX_IMAGE_REF_LABEL, resolveCuratedImageRef } from '../constants/curated-images.constant' import { BoxWarmPoolService } from './box-warm-pool.service' import { EventEmitter2, OnEvent } from '@nestjs/event-emitter' import { WarmPoolEvents } from '../constants/warmpool-events.constants' @@ -131,7 +132,12 @@ export class BoxService { box.mem = warmPoolItem.mem box.disk = warmPoolItem.disk - // TODO(image-rewrite): box image/artifact resolution removed with box_template; rebuild here. + // Warm-pool boxes have no per-request image key, so they boot from the default curated + // image. Stash its resolved ref on the same reserved label the start action reads; without + // it, box-start drives the box to ERROR (missing image ref) and the warm-pool refill loop + // recreates it indefinitely. + box.labels = { [BOX_IMAGE_REF_LABEL]: resolveCuratedImageRef(undefined) } + const runner = await this.runnerService.getRandomAvailableRunner({ regions: [box.region], boxClass: box.class, @@ -150,14 +156,18 @@ export class BoxService { try { const boxClass = this.getValidatedOrDefaultClass(createBoxDto.class) - // TODO(image-rewrite): box_template lookup + artifact resolution removed; boxes can no - // longer resolve an image at create time. Resource sizing falls back to request values - // (or Box entity defaults). Rebuild image/template resolution here. + // TODO(image-rewrite): box_template-based resource sizing removed; sizing falls back to + // request values (or Box entity defaults). Image resolution itself is handled below via + // the curated-image allowlist. const cpu = createBoxDto.cpu ?? DEFAULT_BOX_CPU const mem = createBoxDto.memory ?? DEFAULT_BOX_MEM const disk = createBoxDto.disk ?? DEFAULT_BOX_DISK const gpu = createBoxDto.gpu ?? DEFAULT_BOX_GPU + // Resolve the curated image key (default 'base') to its pinned OCI ref. Rejects any + // key outside the curated allowlist at the request boundary. + const artifactRef = resolveCuratedImageRef(createBoxDto.image) + this.organizationService.assertOrganizationIsNotSuspended(organization) // TODO(image-rewrite): warm-pool reuse path removed with box_template; rebuild here. @@ -180,7 +190,9 @@ export class BoxService { // TODO: default user should be configurable box.osUser = createBoxDto.user || 'boxlite' box.env = createBoxDto.env || {} - box.labels = createBoxDto.labels || {} + // Stash the resolved OCI ref under a reserved label so the start action can pass it + // to the runner as artifactRef. Merge into user labels; don't clobber them. + box.labels = { ...(createBoxDto.labels || {}), [BOX_IMAGE_REF_LABEL]: artifactRef } box.cpu = cpu box.gpu = gpu @@ -1175,9 +1187,19 @@ export class BoxService { async replaceLabels(boxIdOrName: string, labels: { [key: string]: string }, organizationId?: string): Promise { const box = await this.findOneByIdOrName(boxIdOrName, organizationId) - // Replace all labels + // Replace all labels, but never let a user-supplied label clobber the reserved image-ref + // label: it holds the resolved curated OCI ref the runner pulls. A user who could overwrite + // it would escape the curated allowlist and make the runner pull an arbitrary image with its + // own private-registry token. Drop any incoming reserved key and preserve the existing value. + const sanitizedLabels = { ...labels } + delete sanitizedLabels[BOX_IMAGE_REF_LABEL] + const existingImageRef = box.labels?.[BOX_IMAGE_REF_LABEL] + if (existingImageRef !== undefined) { + sanitizedLabels[BOX_IMAGE_REF_LABEL] = existingImageRef + } + const updateData: Partial = { - labels, + labels: sanitizedLabels, } return await this.boxRepository.update(box.id, { updateData, entity: box }) diff --git a/apps/api/src/box/services/box.service.warm-pool.spec.ts b/apps/api/src/box/services/box.service.warm-pool.spec.ts new file mode 100644 index 000000000..2a1d2ca09 --- /dev/null +++ b/apps/api/src/box/services/box.service.warm-pool.spec.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2025 Daytona Platforms Inc. + * Modified by BoxLite AI, 2025-2026 + * SPDX-License-Identifier: AGPL-3.0 + */ + +import { BoxService } from './box.service' +import { BoxClass } from '../enums/box-class.enum' +import { WarmPool } from '../entities/warm-pool.entity' +import { BOX_IMAGE_REF_LABEL } from '../constants/curated-images.constant' + +function warmPoolItem(): WarmPool { + const item = new WarmPool() + item.target = 'us' + item.class = BoxClass.SMALL + item.cpu = 1 + item.gpu = 0 + item.mem = 1 + item.disk = 3 + item.env = {} + return item +} + +describe('BoxService.createForWarmPool image ref', () => { + it('stashes the default curated image ref so warm-pool boxes can boot', async () => { + const insert = jest.fn().mockResolvedValue(undefined) + const getRandomAvailableRunner = jest.fn().mockResolvedValue({ id: 'runner-1' }) + + const service = Object.create(BoxService.prototype) as BoxService + ;(service as any).boxRepository = { insert } + ;(service as any).runnerService = { getRandomAvailableRunner } + + const box = await service.createForWarmPool(warmPoolItem()) + + // Without this label, box-start drives the box to ERROR ("missing image ref") and the + // warm-pool refill loop recreates it forever. It must point at the base curated image. + expect(box.labels[BOX_IMAGE_REF_LABEL]).toBeDefined() + expect(box.labels[BOX_IMAGE_REF_LABEL]).toContain('boxlite-agent-base') + expect(insert).toHaveBeenCalledWith(box) + }) +}) diff --git a/apps/api/src/box/services/job-state-handler.service.ts b/apps/api/src/box/services/job-state-handler.service.ts index cce70e13a..cd1668033 100644 --- a/apps/api/src/box/services/job-state-handler.service.ts +++ b/apps/api/src/box/services/job-state-handler.service.ts @@ -298,9 +298,7 @@ export class JobStateHandlerService { : null if (!previousState) { - this.logger.error( - `Box ${boxId} has unexpected desiredState ${box.desiredState} for RESIZE_BOX job ${job.id}`, - ) + this.logger.error(`Box ${boxId} has unexpected desiredState ${box.desiredState} for RESIZE_BOX job ${job.id}`) return } diff --git a/apps/api/src/box/services/volume.service.ts b/apps/api/src/box/services/volume.service.ts index 89fd46e87..f776f1a86 100644 --- a/apps/api/src/box/services/volume.service.ts +++ b/apps/api/src/box/services/volume.service.ts @@ -4,13 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0 */ -import { - ConflictException, - Injectable, - Logger, - NotFoundException, - ServiceUnavailableException, -} from '@nestjs/common' +import { ConflictException, Injectable, Logger, NotFoundException, ServiceUnavailableException } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { Repository, Not, In } from 'typeorm' import { Volume } from '../entities/volume.entity' diff --git a/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.spec.ts b/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.spec.ts index e708eb606..75f521b97 100644 --- a/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.spec.ts +++ b/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.spec.ts @@ -6,6 +6,7 @@ import { BoxDto } from '../../box/dto/box.dto' import { boxToBoxResponse, createBoxToCreateBox } from './box-to-box.mapper' +import { BOX_IMAGE_REF_LABEL, resolveCuratedImageRef } from '../../box/constants/curated-images.constant' describe('box-to-box mapper', () => { it('maps REST box_id from the public box boxId instead of the internal UUID', () => { @@ -45,4 +46,30 @@ describe('box-to-box mapper', () => { expect(dto.memory).toBe(2) expect(dto.disk).toBe(8) }) + + it('threads the curated image key from the REST request into the internal create dto', () => { + const dto = createBoxToCreateBox({ image: 'python' }) + + expect(dto.image).toBe('python') + }) + + it('echoes the curated key for a box whose label holds a resolved curated ref', () => { + const response = boxToBoxResponse({ + boxId: 'aB3cD4eF5gH6', + state: 'started', + labels: { [BOX_IMAGE_REF_LABEL]: resolveCuratedImageRef('node') }, + } as unknown as BoxDto) + + expect(response.image).toBe('node') + }) + + it('returns an empty image for boxes without an image-ref label', () => { + const response = boxToBoxResponse({ + boxId: 'aB3cD4eF5gH6', + state: 'started', + labels: {}, + } as unknown as BoxDto) + + expect(response.image).toBe('') + }) }) diff --git a/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.ts b/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.ts index 8f4dd211d..f0e367137 100644 --- a/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.ts +++ b/apps/api/src/boxlite-rest/mappers/box-to-box.mapper.ts @@ -9,15 +9,19 @@ import { BoxState } from '../../box/enums/box-state.enum' import { BoxResponseDto } from '../dto/box-response.dto' import { CreateBoxDto as RestCreateBoxDto } from '../dto/create-box.dto' import { CreateBoxDto } from '../../box/dto/create-box.dto' +import { BOX_IMAGE_REF_LABEL, curatedImageKeyForRef } from '../../box/constants/curated-images.constant' export function boxToBoxResponse(box: BoxDto): BoxResponseDto { + // Echo the curated key the box was created with; fall back to the raw label ref for + // boxes whose ref no longer matches the curated set (env rotation, legacy boxes). + const imageRef = box.labels?.[BOX_IMAGE_REF_LABEL] return { box_id: box.boxId, name: box.name, status: mapState(box.state), created_at: box.createdAt || new Date().toISOString(), updated_at: box.updatedAt || new Date().toISOString(), - image: '', + image: curatedImageKeyForRef(imageRef) ?? imageRef ?? '', cpus: box.cpu || 1, memory_mib: (box.memory || 1) * 1024, labels: box.labels || {}, @@ -27,6 +31,7 @@ export function boxToBoxResponse(box: BoxDto): BoxResponseDto { export function createBoxToCreateBox(dto: RestCreateBoxDto, target?: string): CreateBoxDto { const createDto = new CreateBoxDto() createDto.name = dto.name + createDto.image = dto.image createDto.user = dto.user createDto.env = dto.env createDto.cpu = dto.cpus diff --git a/apps/api/src/config/configuration.ts b/apps/api/src/config/configuration.ts index 7c7b99456..a691eb3d0 100644 --- a/apps/api/src/config/configuration.ts +++ b/apps/api/src/config/configuration.ts @@ -159,8 +159,7 @@ const configuration = { publicKey: process.env.SSH_GATEWAY_PUBLIC_KEY, url: process.env.SSH_GATEWAY_URL, }, - organizationBoxDefaultLimitedNetworkEgress: - process.env.ORGANIZATION_BOX_DEFAULT_LIMITED_NETWORK_EGRESS === 'true', + organizationBoxDefaultLimitedNetworkEgress: process.env.ORGANIZATION_BOX_DEFAULT_LIMITED_NETWORK_EGRESS === 'true', pylonAppId: process.env.PYLON_APP_ID, billingApiUrl: process.env.BILLING_API_URL, analyticsApiUrl: process.env.ANALYTICS_API_URL, @@ -246,9 +245,7 @@ const configuration = { : undefined, }, boxCreate: { - ttl: process.env.RATE_LIMIT_BOX_CREATE_TTL - ? parseInt(process.env.RATE_LIMIT_BOX_CREATE_TTL, 10) - : undefined, + ttl: process.env.RATE_LIMIT_BOX_CREATE_TTL ? parseInt(process.env.RATE_LIMIT_BOX_CREATE_TTL, 10) : undefined, limit: process.env.RATE_LIMIT_BOX_CREATE_LIMIT ? parseInt(process.env.RATE_LIMIT_BOX_CREATE_LIMIT, 10) : undefined, diff --git a/apps/api/src/migrations/1741087887225-migration.ts b/apps/api/src/migrations/1741087887225-migration.ts deleted file mode 100644 index 541f93745..000000000 --- a/apps/api/src/migrations/1741087887225-migration.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1741087887225 implements MigrationInterface { - name = 'Migration1741087887225' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE SCHEMA IF NOT EXISTS "public"`) - await queryRunner.query( - `CREATE TABLE "user" ("id" character varying NOT NULL, "name" character varying NOT NULL, "keyPair" text, "publicKeys" text NOT NULL, "total_cpu_quota" integer NOT NULL DEFAULT '10', "total_memory_quota" integer NOT NULL DEFAULT '40', "total_disk_quota" integer NOT NULL DEFAULT '100', "max_cpu_per_workspace" integer NOT NULL DEFAULT '2', "max_memory_per_workspace" integer NOT NULL DEFAULT '4', "max_disk_per_workspace" integer NOT NULL DEFAULT '10', "max_concurrent_workspaces" integer NOT NULL DEFAULT '10', "workspace_quota" integer NOT NULL DEFAULT '0', "image_quota" integer NOT NULL DEFAULT '5', "max_image_size" integer NOT NULL DEFAULT '2', "total_image_size" integer NOT NULL DEFAULT '5', CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`, - ) - await queryRunner.query( - `CREATE TABLE "team" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, CONSTRAINT "PK_f57d8293406df4af348402e4b74" PRIMARY KEY ("id"))`, - ) - await queryRunner.query( - `CREATE TABLE "workspace_usage_periods" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "workspaceId" character varying NOT NULL, "startAt" TIMESTAMP NOT NULL, "endAt" TIMESTAMP, "cpu" double precision NOT NULL, "gpu" double precision NOT NULL, "mem" double precision NOT NULL, "disk" double precision NOT NULL, "storage" double precision NOT NULL, "region" character varying NOT NULL, CONSTRAINT "PK_b8d71f79ee638064397f678e877" PRIMARY KEY ("id"))`, - ) - await queryRunner.query(`CREATE TYPE "node_class_enum" AS ENUM('small', 'medium', 'large')`) - await queryRunner.query(`CREATE TYPE "node_region_enum" AS ENUM('eu', 'us', 'asia')`) - await queryRunner.query( - `CREATE TYPE "node_state_enum" AS ENUM('initializing', 'ready', 'disabled', 'decommissioned', 'unresponsive')`, - ) - await queryRunner.query( - `CREATE TABLE "node" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "domain" character varying NOT NULL, "apiUrl" character varying NOT NULL, "apiKey" character varying NOT NULL, "cpu" integer NOT NULL, "memory" integer NOT NULL, "disk" integer NOT NULL, "gpu" integer NOT NULL, "gpuType" character varying NOT NULL, "class" "node_class_enum" NOT NULL DEFAULT 'small', "used" integer NOT NULL DEFAULT '0', "capacity" integer NOT NULL, "region" "node_region_enum" NOT NULL, "state" "node_state_enum" NOT NULL DEFAULT 'initializing', "lastChecked" TIMESTAMP, "unschedulable" boolean NOT NULL DEFAULT false, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_330d74ac3d0e349b4c73c62ad6d" UNIQUE ("domain"), CONSTRAINT "PK_8c8caf5f29d25264abe9eaf94dd" PRIMARY KEY ("id"))`, - ) - await queryRunner.query( - `CREATE TYPE "image_node_state_enum" AS ENUM('pulling_image', 'ready', 'error', 'removing')`, - ) - await queryRunner.query( - `CREATE TABLE "image_node" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "state" "image_node_state_enum" NOT NULL DEFAULT 'pulling_image', "errorReason" character varying, "image" character varying NOT NULL, "internalImageName" character varying NOT NULL DEFAULT '', "nodeId" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_6c66fc8bd2b9fb41362a50fddd0" PRIMARY KEY ("id"))`, - ) - await queryRunner.query( - `CREATE TYPE "image_state_enum" AS ENUM('pending', 'pulling_image', 'pending_validation', 'validating', 'active', 'error', 'removing')`, - ) - await queryRunner.query( - `CREATE TABLE "image" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" character varying NOT NULL, "general" boolean NOT NULL DEFAULT false, "name" character varying NOT NULL, "internalName" character varying, "enabled" boolean NOT NULL DEFAULT true, "state" "image_state_enum" NOT NULL DEFAULT 'pending', "errorReason" character varying, "size" double precision, "entrypoint" character varying, "internalRegistryId" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "lastUsedAt" TIMESTAMP, CONSTRAINT "UQ_9db6fbe71409d80375c32826db3" UNIQUE ("userId", "name"), CONSTRAINT "PK_d6db1ab4ee9ad9dbe86c64e4cc3" PRIMARY KEY ("id"))`, - ) - await queryRunner.query(`CREATE TYPE "workspace_region_enum" AS ENUM('eu', 'us', 'asia')`) - await queryRunner.query(`CREATE TYPE "workspace_class_enum" AS ENUM('small', 'medium', 'large')`) - await queryRunner.query( - `CREATE TYPE "workspace_state_enum" AS ENUM('creating', 'restoring', 'destroyed', 'destroying', 'started', 'stopped', 'starting', 'stopping', 'resizing', 'error', 'unknown', 'pulling_image', 'archiving', 'archived')`, - ) - await queryRunner.query( - `CREATE TYPE "workspace_desiredstate_enum" AS ENUM('destroyed', 'started', 'stopped', 'resized', 'archived')`, - ) - await queryRunner.query( - `CREATE TYPE "workspace_snapshotstate_enum" AS ENUM('None', 'Pending', 'InProgress', 'Completed', 'Error')`, - ) - await queryRunner.query( - `CREATE TABLE "workspace" ("id" character varying NOT NULL, "name" character varying NOT NULL, "userId" character varying NOT NULL, "region" "workspace_region_enum" NOT NULL DEFAULT 'eu', "nodeId" uuid, "prevNodeId" uuid, "class" "workspace_class_enum" NOT NULL DEFAULT 'small', "state" "workspace_state_enum" NOT NULL DEFAULT 'unknown', "desiredState" "workspace_desiredstate_enum" NOT NULL DEFAULT 'started', "image" character varying NOT NULL, "osUser" character varying NOT NULL, "errorReason" character varying, "env" text NOT NULL DEFAULT '{}', "public" boolean NOT NULL DEFAULT false, "labels" jsonb, "snapshotRegistryId" character varying, "snapshotImage" character varying, "lastSnapshotAt" TIMESTAMP, "snapshotState" "workspace_snapshotstate_enum" NOT NULL DEFAULT 'None', "existingSnapshotImages" jsonb NOT NULL DEFAULT '[]', "cpu" integer NOT NULL DEFAULT '2', "gpu" integer NOT NULL DEFAULT '0', "mem" integer NOT NULL DEFAULT '4', "disk" integer NOT NULL DEFAULT '10', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "lastActivityAt" TIMESTAMP, "autoStopInterval" integer NOT NULL DEFAULT '15', CONSTRAINT "PK_ca86b6f9b3be5fe26d307d09b49" PRIMARY KEY ("id"))`, - ) - await queryRunner.query( - `CREATE TABLE "docker_registry" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "url" character varying NOT NULL, "username" character varying NOT NULL, "password" character varying NOT NULL, "isDefault" boolean NOT NULL DEFAULT false, "project" character varying NOT NULL, "userId" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_4ad72294240279415eb57799798" PRIMARY KEY ("id"))`, - ) - await queryRunner.query( - `CREATE TABLE "api_key" ("userId" character varying NOT NULL, "name" character varying NOT NULL, "value" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL, CONSTRAINT "UQ_4b0873b633484d5de20b2d8f852" UNIQUE ("value"), CONSTRAINT "PK_1df0337a701df00e9b2a16c8a0b" PRIMARY KEY ("userId", "name"))`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "api_key"`) - await queryRunner.query(`DROP TABLE "docker_registry"`) - await queryRunner.query(`DROP TABLE "workspace"`) - await queryRunner.query(`DROP TYPE "workspace_snapshotstate_enum"`) - await queryRunner.query(`DROP TYPE "workspace_desiredstate_enum"`) - await queryRunner.query(`DROP TYPE "workspace_state_enum"`) - await queryRunner.query(`DROP TYPE "workspace_class_enum"`) - await queryRunner.query(`DROP TYPE "workspace_region_enum"`) - await queryRunner.query(`DROP TABLE "image"`) - await queryRunner.query(`DROP TYPE "image_state_enum"`) - await queryRunner.query(`DROP TABLE "image_node"`) - await queryRunner.query(`DROP TYPE "image_node_state_enum"`) - await queryRunner.query(`DROP TABLE "node"`) - await queryRunner.query(`DROP TYPE "node_state_enum"`) - await queryRunner.query(`DROP TYPE "node_region_enum"`) - await queryRunner.query(`DROP TYPE "node_class_enum"`) - await queryRunner.query(`DROP TABLE "workspace_usage_periods"`) - await queryRunner.query(`DROP TABLE "team"`) - await queryRunner.query(`DROP TABLE "user"`) - } -} diff --git a/apps/api/src/migrations/1741088165704-migration.ts b/apps/api/src/migrations/1741088165704-migration.ts deleted file mode 100644 index 9de051ebb..000000000 --- a/apps/api/src/migrations/1741088165704-migration.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1741088165704 implements MigrationInterface { - name = 'Migration1741088165704' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "image" DROP COLUMN "internalRegistryId"`) - await queryRunner.query( - `CREATE TYPE "public"."docker_registry_registrytype_enum" AS ENUM('internal', 'user', 'public', 'transient')`, - ) - await queryRunner.query( - `ALTER TABLE "docker_registry" ADD "registryType" "public"."docker_registry_registrytype_enum" NOT NULL DEFAULT 'internal'`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "docker_registry" DROP COLUMN "registryType"`) - await queryRunner.query(`DROP TYPE "public"."docker_registry_registrytype_enum"`) - await queryRunner.query(`ALTER TABLE "image" ADD "internalRegistryId" character varying`) - } -} diff --git a/apps/api/src/migrations/1741088883000-migration.ts b/apps/api/src/migrations/1741088883000-migration.ts deleted file mode 100644 index ddca09ca8..000000000 --- a/apps/api/src/migrations/1741088883000-migration.ts +++ /dev/null @@ -1,487 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1741088883000 implements MigrationInterface { - name = 'Migration1741088883000' - - public async up(queryRunner: QueryRunner): Promise { - // organizations - await queryRunner.query( - `CREATE TABLE "organization" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "createdBy" character varying NOT NULL, "personal" boolean NOT NULL DEFAULT false, "telemetryEnabled" boolean NOT NULL DEFAULT true, "total_cpu_quota" integer NOT NULL DEFAULT '10', "total_memory_quota" integer NOT NULL DEFAULT '40', "total_disk_quota" integer NOT NULL DEFAULT '100', "max_cpu_per_workspace" integer NOT NULL DEFAULT '2', "max_memory_per_workspace" integer NOT NULL DEFAULT '4', "max_disk_per_workspace" integer NOT NULL DEFAULT '10', "max_concurrent_workspaces" integer NOT NULL DEFAULT '10', "workspace_quota" integer NOT NULL DEFAULT '0', "image_quota" integer NOT NULL DEFAULT '5', "max_image_size" integer NOT NULL DEFAULT '2', "total_image_size" integer NOT NULL DEFAULT '5', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "organization_id_pk" PRIMARY KEY ("id"))`, - ) - - // organization users - await queryRunner.query(`CREATE TYPE "public"."organization_user_role_enum" AS ENUM('owner', 'member')`) - await queryRunner.query( - `CREATE TABLE "organization_user" ("organizationId" uuid NOT NULL, "userId" character varying NOT NULL, "role" "public"."organization_user_role_enum" NOT NULL DEFAULT 'member', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "organization_user_organizationId_userId_pk" PRIMARY KEY ("organizationId", "userId"))`, - ) - await queryRunner.query( - `ALTER TABLE "organization_user" ADD CONSTRAINT "organization_user_organizationId_fk" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ) - - // organization invitations - await queryRunner.query(`CREATE TYPE "public"."organization_invitation_role_enum" AS ENUM('owner', 'member')`) - await queryRunner.query( - `CREATE TYPE "public"."organization_invitation_status_enum" AS ENUM('pending', 'accepted', 'declined', 'cancelled')`, - ) - await queryRunner.query( - `CREATE TABLE "organization_invitation" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "organizationId" uuid NOT NULL, "email" character varying NOT NULL, "role" "public"."organization_invitation_role_enum" NOT NULL DEFAULT 'member', "expiresAt" TIMESTAMP NOT NULL, "status" "public"."organization_invitation_status_enum" NOT NULL DEFAULT 'pending', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "organization_invitation_id_pk" PRIMARY KEY ("id"))`, - ) - await queryRunner.query( - `ALTER TABLE "organization_invitation" ADD CONSTRAINT "organization_invitation_organizationId_fk" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ) - - // organization roles - await queryRunner.query( - `CREATE TYPE "public"."organization_role_permissions_enum" AS ENUM('write:registries', 'delete:registries', 'write:images', 'delete:images', 'write:sandboxes', 'delete:sandboxes')`, - ) - await queryRunner.query( - `CREATE TABLE "organization_role" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "description" character varying NOT NULL, "permissions" "public"."organization_role_permissions_enum" array NOT NULL, "isGlobal" boolean NOT NULL DEFAULT false, "organizationId" uuid, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "organization_role_id_pk" PRIMARY KEY ("id"))`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role" ADD CONSTRAINT "organization_role_organizationId_fk" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ) - - // organization role assignments for members - await queryRunner.query( - `CREATE TABLE "organization_role_assignment" ("organizationId" uuid NOT NULL, "userId" character varying NOT NULL, "roleId" uuid NOT NULL, CONSTRAINT "organization_role_assignment_organizationId_userId_roleId_pk" PRIMARY KEY ("organizationId", "userId", "roleId"))`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment" ADD CONSTRAINT "organization_role_assignment_organizationId_userId_fk" FOREIGN KEY ("organizationId", "userId") REFERENCES "organization_user"("organizationId","userId") ON DELETE CASCADE ON UPDATE CASCADE`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment" ADD CONSTRAINT "organization_role_assignment_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "organization_role"("id") ON DELETE CASCADE ON UPDATE CASCADE`, - ) - await queryRunner.query( - `CREATE INDEX "organization_role_assignment_organizationId_userId_index" ON "organization_role_assignment" ("organizationId", "userId") `, - ) - await queryRunner.query( - `CREATE INDEX "organization_role_assignment_roleId_index" ON "organization_role_assignment" ("roleId") `, - ) - - // organization role assignments for invitations - await queryRunner.query( - `CREATE TABLE "organization_role_assignment_invitation" ("invitationId" uuid NOT NULL, "roleId" uuid NOT NULL, CONSTRAINT "organization_role_assignment_invitation_invitationId_roleId_pk" PRIMARY KEY ("invitationId", "roleId"))`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment_invitation" ADD CONSTRAINT "organization_role_assignment_invitation_invitationId_fk" FOREIGN KEY ("invitationId") REFERENCES "organization_invitation"("id") ON DELETE CASCADE ON UPDATE CASCADE`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment_invitation" ADD CONSTRAINT "organization_role_assignment_invitation_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "organization_role"("id") ON DELETE CASCADE ON UPDATE CASCADE`, - ) - await queryRunner.query( - `CREATE INDEX "organization_role_assignment_invitation_invitationId_index" ON "organization_role_assignment_invitation" ("invitationId") `, - ) - await queryRunner.query( - `CREATE INDEX "organization_role_assignment_invitation_roleId_index" ON "organization_role_assignment_invitation" ("roleId") `, - ) - - // create personal organizations - await queryRunner.query(` - INSERT INTO "organization" ( - name, - personal, - "createdBy", - total_cpu_quota, - total_memory_quota, - total_disk_quota, - max_cpu_per_workspace, - max_memory_per_workspace, - max_disk_per_workspace, - max_concurrent_workspaces, - workspace_quota, - image_quota, - max_image_size, - total_image_size - ) - SELECT - 'Personal', - true, - u.id, - u.total_cpu_quota, - u.total_memory_quota, - u.total_disk_quota, - u.max_cpu_per_workspace, - u.max_memory_per_workspace, - u.max_disk_per_workspace, - u.max_concurrent_workspaces, - u.workspace_quota, - u.image_quota, - u.max_image_size, - u.total_image_size - FROM "user" u - `) - await queryRunner.query(` - INSERT INTO "organization_user" ("organizationId", "userId", role) - SELECT - o.id, - o."createdBy", - 'owner' - FROM "organization" o - WHERE o.personal = true - `) - - // drop user quotas - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "total_cpu_quota"`) - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "total_memory_quota"`) - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "total_disk_quota"`) - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "max_cpu_per_workspace"`) - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "max_memory_per_workspace"`) - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "max_disk_per_workspace"`) - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "max_concurrent_workspaces"`) - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "workspace_quota"`) - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "image_quota"`) - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "max_image_size"`) - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "total_image_size"`) - - // move existing api keys to corresponding personal organizations - await queryRunner.query(`ALTER TABLE "api_key" ADD "organizationId" uuid NULL`) - await queryRunner.query(` - UPDATE "api_key" ak - SET "organizationId" = ( - SELECT o.id - FROM "organization" o - WHERE o."createdBy" = ak."userId" - AND o.personal = true - LIMIT 1 - ) - `) - await queryRunner.query(`ALTER TABLE "api_key" ALTER COLUMN "organizationId" SET NOT NULL`) - - // update api key primary key - await queryRunner.query(` - DO $$ - DECLARE - constraint_name text; - BEGIN - SELECT tc.constraint_name INTO constraint_name - FROM information_schema.table_constraints tc - WHERE tc.table_name = 'api_key' - AND tc.constraint_type = 'PRIMARY KEY'; - IF constraint_name IS NOT NULL THEN - EXECUTE format('ALTER TABLE "api_key" DROP CONSTRAINT "%s"', constraint_name); - END IF; - END $$; - `) - await queryRunner.query( - `ALTER TABLE "api_key" ADD CONSTRAINT "api_key_userId_name_organizationId_pk" PRIMARY KEY ("userId", "name", "organizationId")`, - ) - - // api key permissions - await queryRunner.query( - `CREATE TYPE "public"."api_key_permissions_enum" AS ENUM('write:registries', 'delete:registries', 'write:images', 'delete:images', 'write:sandboxes', 'delete:sandboxes')`, - ) - await queryRunner.query(`ALTER TABLE "api_key" ADD "permissions" "public"."api_key_permissions_enum" array NULL`) - await queryRunner.query(` - UPDATE api_key - SET permissions = ARRAY[ - 'write:registries', - 'delete:registries', - 'write:images', - 'delete:images', - 'write:sandboxes', - 'delete:sandboxes' - ]::api_key_permissions_enum[] - `) - await queryRunner.query(`ALTER TABLE "api_key" ALTER COLUMN "permissions" SET NOT NULL`) - - // modify docker registry type enum - await queryRunner.query(`ALTER TABLE "docker_registry" ALTER COLUMN "registryType" DROP DEFAULT`) - await queryRunner.query( - `ALTER TYPE "public"."docker_registry_registrytype_enum" RENAME TO "docker_registry_registrytype_enum_old"`, - ) - await queryRunner.query( - `CREATE TYPE "public"."docker_registry_registrytype_enum" AS ENUM('internal', 'organization', 'public', 'transient')`, - ) - await queryRunner.query(` - CREATE OR REPLACE FUNCTION migrate_registry_type(old_type text) - RETURNS "public"."docker_registry_registrytype_enum" AS $$ - BEGIN - IF old_type = 'user' THEN - RETURN 'organization'::"public"."docker_registry_registrytype_enum"; - ELSE - RETURN old_type::"public"."docker_registry_registrytype_enum"; - END IF; - END; - $$ LANGUAGE plpgsql; - `) - await queryRunner.query(` - ALTER TABLE "docker_registry" - ALTER COLUMN "registryType" TYPE "public"."docker_registry_registrytype_enum" - USING migrate_registry_type("registryType"::text) - `) - await queryRunner.query(`ALTER TABLE "docker_registry" ALTER COLUMN "registryType" SET DEFAULT 'internal'`) - await queryRunner.query(`DROP TYPE "public"."docker_registry_registrytype_enum_old"`) - await queryRunner.query(`DROP FUNCTION migrate_registry_type`) - - // move existing docker registries to corresponding personal organizations - await queryRunner.query(`ALTER TABLE "docker_registry" ADD "organizationId" uuid NULL`) - await queryRunner.query(`UPDATE "docker_registry" SET "organizationId" = NULL WHERE "userId" = 'system'`) - await queryRunner.query(` - UPDATE "docker_registry" dr - SET "organizationId" = ( - SELECT o.id - FROM "organization" o - WHERE o."createdBy" = dr."userId" - AND o.personal = true - LIMIT 1 - ) - `) - await queryRunner.query(`ALTER TABLE "docker_registry" DROP COLUMN "userId"`) - - // move existing images to corresponding personal organizations - await queryRunner.query(`ALTER TABLE "image" ADD "organizationId" uuid NULL`) - await queryRunner.query(` - UPDATE "image" i - SET "organizationId" = ( - SELECT o.id - FROM "organization" o - WHERE o."createdBy" = i."userId" - AND o.personal = true - LIMIT 1 - ) - `) - - // update image unique constraint - await queryRunner.query(` - DO $$ - DECLARE - constraint_name text; - BEGIN - SELECT tc.constraint_name INTO constraint_name - FROM information_schema.table_constraints tc - WHERE tc.table_name = 'image' - AND tc.constraint_type = 'UNIQUE' - AND tc.constraint_name LIKE '%name%'; - - IF constraint_name IS NOT NULL THEN - EXECUTE format('ALTER TABLE "image" DROP CONSTRAINT "%s"', constraint_name); - END IF; - END $$; - `) - await queryRunner.query( - `ALTER TABLE "image" ADD CONSTRAINT "image_organizationId_name_unique" UNIQUE ("organizationId", "name")`, - ) - await queryRunner.query(`ALTER TABLE "image" DROP COLUMN "userId"`) - - // move existing workspaces to corresponding personal organizations - await queryRunner.query(`ALTER TABLE "workspace" ADD "organizationId" uuid NULL`) - await queryRunner.query(` - UPDATE "workspace" w - SET "organizationId" = ( - SELECT o.id - FROM "organization" o - WHERE o."createdBy" = w."userId" - AND o.personal = true - LIMIT 1 - ) - WHERE w."userId" != 'unassigned' - `) - await queryRunner.query(` - UPDATE "workspace" w - SET "organizationId" = '00000000-0000-0000-0000-000000000000' - WHERE w."userId" = 'unassigned' - `) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "organizationId" SET NOT NULL`) - await queryRunner.query(`ALTER TABLE "workspace" DROP COLUMN "userId"`) - } - - public async down(queryRunner: QueryRunner): Promise { - // workspaces - await queryRunner.query(`ALTER TABLE "workspace" ADD "userId" character varying NULL`) - await queryRunner.query(` - UPDATE "workspace" w - SET "userId" = 'unassigned' - WHERE w."organizationId" = '00000000-0000-0000-0000-000000000000' - `) - await queryRunner.query(` - UPDATE "workspace" w - SET "userId" = ( - SELECT o."createdBy" - FROM "organization" o - WHERE o.id = w."organizationId" - ) - WHERE w."organizationId" != '00000000-0000-0000-0000-000000000000' - `) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "userId" SET NOT NULL`) - await queryRunner.query(`ALTER TABLE "workspace" DROP COLUMN "organizationId"`) - - // images - await queryRunner.query(`ALTER TABLE "image" ADD "userId" character varying NULL`) - await queryRunner.query(` - UPDATE "image" i - SET "userId" = ( - SELECT o."createdBy" - FROM "organization" o - WHERE o.id = i."organizationId" - ) - `) - await queryRunner.query(`ALTER TABLE "image" ALTER COLUMN "userId" SET NOT NULL`) - await queryRunner.query(`ALTER TABLE "image" DROP CONSTRAINT "image_organizationId_name_unique"`) - await queryRunner.query(`ALTER TABLE "image" ADD CONSTRAINT "image_userId_name_unique" UNIQUE ("userId", "name")`) - await queryRunner.query(`ALTER TABLE "image" DROP COLUMN "organizationId"`) - - // docker registries - await queryRunner.query(`ALTER TABLE "docker_registry" ALTER COLUMN "registryType" DROP DEFAULT`) - await queryRunner.query( - `ALTER TYPE "public"."docker_registry_registrytype_enum" RENAME TO "docker_registry_registrytype_enum_old"`, - ) - await queryRunner.query( - `CREATE TYPE "public"."docker_registry_registrytype_enum" AS ENUM('internal', 'user', 'public', 'transient')`, - ) - await queryRunner.query(` - CREATE OR REPLACE FUNCTION rollback_registry_type(new_type text) - RETURNS "public"."docker_registry_registrytype_enum" AS $$ - BEGIN - IF new_type = 'organization' THEN - RETURN 'user'::"public"."docker_registry_registrytype_enum"; - ELSE - RETURN new_type::"public"."docker_registry_registrytype_enum"; - END IF; - END; - $$ LANGUAGE plpgsql; - `) - await queryRunner.query(` - ALTER TABLE "docker_registry" - ALTER COLUMN "registryType" TYPE "public"."docker_registry_registrytype_enum" - USING rollback_registry_type("registryType"::text) - `) - await queryRunner.query(`ALTER TABLE "docker_registry" ALTER COLUMN "registryType" SET DEFAULT 'internal'`) - await queryRunner.query(`DROP TYPE "public"."docker_registry_registrytype_enum_old"`) - await queryRunner.query(`DROP FUNCTION rollback_registry_type`) - await queryRunner.query(`ALTER TABLE "docker_registry" ADD "userId" character varying NULL`) - await queryRunner.query(` - UPDATE "docker_registry" dr - SET "userId" = ( - SELECT o."createdBy" - FROM "organization" o - WHERE o.id = dr."organizationId" - ) - `) - await queryRunner.query(`ALTER TABLE "docker_registry" DROP COLUMN "organizationId"`) - - // api keys - await queryRunner.query(`ALTER TABLE "api_key" DROP CONSTRAINT "api_key_userId_name_organizationId_pk"`) - await queryRunner.query( - `ALTER TABLE "api_key" ADD CONSTRAINT "api_key_userId_name_pk" PRIMARY KEY ("userId", "name")`, - ) - await queryRunner.query(`ALTER TABLE "api_key" DROP COLUMN "organizationId"`) - await queryRunner.query(`ALTER TABLE "api_key" DROP COLUMN "permissions"`) - await queryRunner.query(`DROP TYPE "public"."api_key_permissions_enum"`) - - // user quotas - await queryRunner.query(`ALTER TABLE "user" ADD "total_cpu_quota" integer NOT NULL DEFAULT '10'`) - await queryRunner.query(`ALTER TABLE "user" ADD "total_memory_quota" integer NOT NULL DEFAULT '40'`) - await queryRunner.query(`ALTER TABLE "user" ADD "total_disk_quota" integer NOT NULL DEFAULT '100'`) - await queryRunner.query(`ALTER TABLE "user" ADD "max_cpu_per_workspace" integer NOT NULL DEFAULT '2'`) - await queryRunner.query(`ALTER TABLE "user" ADD "max_memory_per_workspace" integer NOT NULL DEFAULT '4'`) - await queryRunner.query(`ALTER TABLE "user" ADD "max_disk_per_workspace" integer NOT NULL DEFAULT '10'`) - await queryRunner.query(`ALTER TABLE "user" ADD "max_concurrent_workspaces" integer NOT NULL DEFAULT '10'`) - await queryRunner.query(`ALTER TABLE "user" ADD "workspace_quota" integer NOT NULL DEFAULT '0'`) - await queryRunner.query(`ALTER TABLE "user" ADD "image_quota" integer NOT NULL DEFAULT '5'`) - await queryRunner.query(`ALTER TABLE "user" ADD "max_image_size" integer NOT NULL DEFAULT '2'`) - await queryRunner.query(`ALTER TABLE "user" ADD "total_image_size" integer NOT NULL DEFAULT '5'`) - await queryRunner.query(` - UPDATE "user" u - SET - total_cpu_quota = ( - SELECT o.total_cpu_quota - FROM "organization" o - WHERE o."createdBy" = u.id - AND o.personal = true - LIMIT 1 - ), - total_memory_quota = ( - SELECT o.total_memory_quota - FROM "organization" o - WHERE o."createdBy" = u.id - AND o.personal = true - LIMIT 1 - ), - total_disk_quota = ( - SELECT o.total_disk_quota - FROM "organization" o - WHERE o."createdBy" = u.id - AND o.personal = true - LIMIT 1 - ), - max_cpu_per_workspace = ( - SELECT o.max_cpu_per_workspace - FROM "organization" o - WHERE o."createdBy" = u.id - AND o.personal = true - LIMIT 1 - ), - max_memory_per_workspace = ( - SELECT o.max_memory_per_workspace - FROM "organization" o - WHERE o."createdBy" = u.id - AND o.personal = true - LIMIT 1 - ), - max_disk_per_workspace = ( - SELECT o.max_disk_per_workspace - FROM "organization" o - WHERE o."createdBy" = u.id - AND o.personal = true - LIMIT 1 - ), - max_concurrent_workspaces = ( - SELECT o.max_concurrent_workspaces - FROM "organization" o - WHERE o."createdBy" = u.id - AND o.personal = true - LIMIT 1 - ), - workspace_quota = ( - SELECT o.workspace_quota - FROM "organization" o - WHERE o."createdBy" = u.id - AND o.personal = true - LIMIT 1 - ), - image_quota = ( - SELECT o.image_quota - FROM "organization" o - WHERE o."createdBy" = u.id - AND o.personal = true - LIMIT 1 - ), - max_image_size = ( - SELECT o.max_image_size - FROM "organization" o - WHERE o."createdBy" = u.id - AND o.personal = true - LIMIT 1 - ), - total_image_size = ( - SELECT o.total_image_size - FROM "organization" o - WHERE o."createdBy" = u.id - AND o.personal = true - LIMIT 1 - ) - `) - - // drop organization tables and related constraints - await queryRunner.query(`DROP INDEX "organization_role_assignment_invitation_roleId_index"`) - await queryRunner.query(`DROP INDEX "organization_role_assignment_invitation_invitationId_index"`) - await queryRunner.query(`DROP TABLE "organization_role_assignment_invitation"`) - await queryRunner.query(`DROP INDEX "organization_role_assignment_roleId_index"`) - await queryRunner.query(`DROP INDEX "organization_role_assignment_organizationId_userId_index"`) - await queryRunner.query(`DROP TABLE "organization_role_assignment"`) - await queryRunner.query(`DROP TABLE "organization_role"`) - await queryRunner.query(`DROP TYPE "organization_role_permissions_enum"`) - await queryRunner.query(`DROP TABLE "organization_invitation"`) - await queryRunner.query(`DROP TYPE "organization_invitation_status_enum"`) - await queryRunner.query(`DROP TYPE "organization_invitation_role_enum"`) - await queryRunner.query(`DROP TABLE "organization_user"`) - await queryRunner.query(`DROP TYPE "organization_user_role_enum"`) - await queryRunner.query(`DROP TABLE "organization"`) - } -} diff --git a/apps/api/src/migrations/1741088883001-migration.ts b/apps/api/src/migrations/1741088883001-migration.ts deleted file mode 100644 index 214452085..000000000 --- a/apps/api/src/migrations/1741088883001-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1741088883001 implements MigrationInterface { - name = 'Migration1741088883001' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "user" ADD "email" character varying NOT NULL DEFAULT ''`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "email"`) - } -} diff --git a/apps/api/src/migrations/1741088883002-migration.ts b/apps/api/src/migrations/1741088883002-migration.ts deleted file mode 100644 index a38604ebf..000000000 --- a/apps/api/src/migrations/1741088883002-migration.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' -import { GlobalOrganizationRolesIds } from '../organization/constants/global-organization-roles.constant' -import { OrganizationResourcePermission } from '../organization/enums/organization-resource-permission.enum' - -export class Migration1741088883002 implements MigrationInterface { - name = 'Migration1741088883002' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - INSERT INTO "organization_role" - ("id", "name", "description", "permissions", "isGlobal") - VALUES - ( - '${GlobalOrganizationRolesIds.DEVELOPER}', - 'Developer', - 'Grants the ability to create sandboxes and keys in the organization', - ARRAY[ - '${OrganizationResourcePermission.WRITE_BOXES}' - ]::organization_role_permissions_enum[], - TRUE - ) - `) - - await queryRunner.query(` - INSERT INTO "organization_role" - ("id", "name", "description", "permissions", "isGlobal") - VALUES - ( - '${GlobalOrganizationRolesIds.BOXES_ADMIN}', - 'Sandboxes Admin', - 'Grants admin access to sandboxes in the organization', - ARRAY[ - '${OrganizationResourcePermission.WRITE_BOXES}', - '${OrganizationResourcePermission.DELETE_BOXES}' - ]::organization_role_permissions_enum[], - TRUE - ) - `) - - await queryRunner.query(` - INSERT INTO "organization_role" - ("id", "name", "description", "permissions", "isGlobal") - VALUES - ( - '${GlobalOrganizationRolesIds.TEMPLATES_ADMIN}', - 'Images Admin', - 'Grants admin access to images in the organization', - ARRAY[ - 'write:images', - 'delete:images' - ]::organization_role_permissions_enum[], - TRUE - ) - `) - - await queryRunner.query(` - INSERT INTO "organization_role" - ("id", "name", "description", "permissions", "isGlobal") - VALUES - ( - '${GlobalOrganizationRolesIds.REGISTRIES_ADMIN}', - 'Registries Admin', - 'Grants admin access to registries in the organization', - ARRAY[ - '${OrganizationResourcePermission.WRITE_REGISTRIES}', - '${OrganizationResourcePermission.DELETE_REGISTRIES}' - ]::organization_role_permissions_enum[], - TRUE - ) - `) - - await queryRunner.query(` - INSERT INTO "organization_role" - ("id", "name", "description", "permissions", "isGlobal") - VALUES - ( - '${GlobalOrganizationRolesIds.SUPER_ADMIN}', - 'Super Admin', - 'Grants full access to all resources in the organization', - ARRAY[ - '${OrganizationResourcePermission.WRITE_REGISTRIES}', - '${OrganizationResourcePermission.DELETE_REGISTRIES}', - 'write:images', - 'delete:images', - '${OrganizationResourcePermission.WRITE_BOXES}', - '${OrganizationResourcePermission.DELETE_BOXES}' - ]::organization_role_permissions_enum[], - TRUE - ) - `) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DELETE FROM "organization_role" WHERE "isGlobal" = TRUE`) - } -} diff --git a/apps/api/src/migrations/1741877019888-migration.ts b/apps/api/src/migrations/1741877019888-migration.ts deleted file mode 100644 index 3815094a5..000000000 --- a/apps/api/src/migrations/1741877019888-migration.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1741877019888 implements MigrationInterface { - name = 'Migration1741877019888' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TYPE "public"."warm_pool_target_enum" AS ENUM('eu', 'us', 'asia')`) - await queryRunner.query(`CREATE TYPE "public"."warm_pool_class_enum" AS ENUM('small', 'medium', 'large')`) - await queryRunner.query( - `CREATE TABLE "warm_pool" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "pool" integer NOT NULL, "image" character varying NOT NULL, "target" "public"."warm_pool_target_enum" NOT NULL DEFAULT 'eu', "cpu" integer NOT NULL, "mem" integer NOT NULL, "disk" integer NOT NULL, "gpu" integer NOT NULL, "gpuType" character varying NOT NULL, "class" "public"."warm_pool_class_enum" NOT NULL DEFAULT 'small', "osUser" character varying NOT NULL, "errorReason" character varying, "env" text NOT NULL DEFAULT '{}', "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_fb06a13baeb3ac0ced145345d90" PRIMARY KEY ("id"))`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "warm_pool"`) - await queryRunner.query(`DROP TYPE "public"."warm_pool_class_enum"`) - await queryRunner.query(`DROP TYPE "public"."warm_pool_target_enum"`) - } -} diff --git a/apps/api/src/migrations/1742215525714-migration.ts b/apps/api/src/migrations/1742215525714-migration.ts deleted file mode 100644 index 4c511ba07..000000000 --- a/apps/api/src/migrations/1742215525714-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1742215525714 implements MigrationInterface { - name = 'Migration1742215525714' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "image_quota" SET DEFAULT '0'`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "image_quota" SET DEFAULT '5'`) - } -} diff --git a/apps/api/src/migrations/1742475055353-migration.ts b/apps/api/src/migrations/1742475055353-migration.ts deleted file mode 100644 index 55232c40d..000000000 --- a/apps/api/src/migrations/1742475055353-migration.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1742475055353 implements MigrationInterface { - name = 'Migration1742475055353' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TYPE "public"."user_role_enum" AS ENUM('admin', 'user')`) - await queryRunner.query(`ALTER TABLE "user" ADD "role" "public"."user_role_enum" NOT NULL DEFAULT 'user'`) - - await queryRunner.query(`UPDATE "user" SET "role" = 'admin' WHERE "id" = 'boxlite-admin'`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "role"`) - await queryRunner.query(`DROP TYPE "public"."user_role_enum"`) - } -} diff --git a/apps/api/src/migrations/1742831092942-migration.ts b/apps/api/src/migrations/1742831092942-migration.ts deleted file mode 100644 index 4b4471037..000000000 --- a/apps/api/src/migrations/1742831092942-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1742831092942 implements MigrationInterface { - name = 'Migration1742831092942' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "workspace" ADD "pending" boolean NOT NULL DEFAULT false`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "workspace" DROP COLUMN "pending"`) - } -} diff --git a/apps/api/src/migrations/1743593463168-migration.ts b/apps/api/src/migrations/1743593463168-migration.ts deleted file mode 100644 index 4735198a8..000000000 --- a/apps/api/src/migrations/1743593463168-migration.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1743593463168 implements MigrationInterface { - name = 'Migration1743593463168' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "organization_invitation" ADD "invitedBy" character varying NOT NULL DEFAULT ''`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization_invitation" DROP COLUMN "invitedBy"`) - } -} diff --git a/apps/api/src/migrations/1743683015304-migration.ts b/apps/api/src/migrations/1743683015304-migration.ts deleted file mode 100644 index 694d88e8a..000000000 --- a/apps/api/src/migrations/1743683015304-migration.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1743683015304 implements MigrationInterface { - name = 'Migration1743683015304' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "workspace" DROP COLUMN "name"`) - await queryRunner.query(`ALTER TABLE "workspace" DROP CONSTRAINT "PK_ca86b6f9b3be5fe26d307d09b49"`) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "id" SET DEFAULT uuid_generate_v4()`) - await queryRunner.query(`ALTER TABLE "workspace" ADD CONSTRAINT "workspace_id_pk" PRIMARY KEY ("id")`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "workspace" DROP CONSTRAINT "workspace_id_pk"`) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "id" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "workspace" ADD CONSTRAINT "PK_ca86b6f9b3be5fe26d307d09b49" PRIMARY KEY ("id")`, - ) - await queryRunner.query(`ALTER TABLE "workspace" ADD "name" character varying NOT NULL DEFAULT ''`) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "name" DROP DEFAULT`) - } -} diff --git a/apps/api/src/migrations/1744028841133-migration.ts b/apps/api/src/migrations/1744028841133-migration.ts deleted file mode 100644 index 442da6180..000000000 --- a/apps/api/src/migrations/1744028841133-migration.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1744028841133 implements MigrationInterface { - name = 'Migration1744028841133' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "workspace_usage_periods" DROP COLUMN "storage"`) - await queryRunner.query(`ALTER TABLE "workspace_usage_periods" ADD "organizationId" character varying NOT NULL`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "workspace_usage_periods" DROP COLUMN "organizationId"`) - await queryRunner.query(`ALTER TABLE "workspace_usage_periods" ADD "storage" double precision NOT NULL`) - } -} diff --git a/apps/api/src/migrations/1744114341077-migration.ts b/apps/api/src/migrations/1744114341077-migration.ts deleted file mode 100644 index 4b4ce894f..000000000 --- a/apps/api/src/migrations/1744114341077-migration.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1744114341077 implements MigrationInterface { - name = 'Migration1744114341077' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "organization_role_assignment" DROP CONSTRAINT "organization_role_assignment_roleId_fk"`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment_invitation" DROP CONSTRAINT "organization_role_assignment_invitation_roleId_fk"`, - ) - await queryRunner.query( - `ALTER TABLE "workspace" ADD "authToken" character varying NOT NULL DEFAULT MD5(random()::text)`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment" ADD CONSTRAINT "organization_role_assignment_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "organization_role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment_invitation" ADD CONSTRAINT "organization_role_assignment_invitation_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "organization_role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "organization_role_assignment_invitation" DROP CONSTRAINT "organization_role_assignment_invitation_roleId_fk"`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment" DROP CONSTRAINT "organization_role_assignment_roleId_fk"`, - ) - await queryRunner.query(`ALTER TABLE "workspace" DROP COLUMN "authToken"`) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment_invitation" ADD CONSTRAINT "organization_role_assignment_invitation_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "organization_role"("id") ON DELETE CASCADE ON UPDATE CASCADE`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment" ADD CONSTRAINT "organization_role_assignment_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "organization_role"("id") ON DELETE CASCADE ON UPDATE CASCADE`, - ) - } -} diff --git a/apps/api/src/migrations/1744378115901-migration.ts b/apps/api/src/migrations/1744378115901-migration.ts deleted file mode 100644 index 8c07075df..000000000 --- a/apps/api/src/migrations/1744378115901-migration.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1744378115901 implements MigrationInterface { - name = 'Migration1744378115901' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" ADD "suspended" boolean NOT NULL DEFAULT false`) - await queryRunner.query(`ALTER TABLE "organization" ADD "suspensionReason" character varying`) - await queryRunner.query(`ALTER TABLE "organization" ADD "suspendedUntil" TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "organization" ADD "suspendedAt" TIMESTAMP`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "suspendedAt"`) - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "suspendedUntil"`) - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "suspensionReason"`) - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "suspended"`) - } -} diff --git a/apps/api/src/migrations/1744808444807-migration.ts b/apps/api/src/migrations/1744808444807-migration.ts deleted file mode 100644 index 286bd0893..000000000 --- a/apps/api/src/migrations/1744808444807-migration.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1744808444807 implements MigrationInterface { - name = 'Migration1744808444807' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "volume" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "organizationId" uuid, "name" character varying NOT NULL, "state" character varying NOT NULL, "errorReason" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "lastUsedAt" TIMESTAMP, CONSTRAINT "volume_organizationId_name_unique" UNIQUE ("organizationId", "name"), CONSTRAINT "volume_id_pk" PRIMARY KEY ("id"))`, - ) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "authToken" SET DEFAULT MD5(random()::text)`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "authToken" SET DEFAULT md5((random()))`) - await queryRunner.query(`DROP TABLE "volume"`) - } -} diff --git a/apps/api/src/migrations/1744868914148-migration.ts b/apps/api/src/migrations/1744868914148-migration.ts deleted file mode 100644 index bfa66d5a8..000000000 --- a/apps/api/src/migrations/1744868914148-migration.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' -import { GlobalOrganizationRolesIds } from '../organization/constants/global-organization-roles.constant' -import { OrganizationResourcePermission } from '../organization/enums/organization-resource-permission.enum' - -export class Migration1744868914148 implements MigrationInterface { - name = 'Migration1744868914148' - - public async up(queryRunner: QueryRunner): Promise { - // update enums - await queryRunner.query(`ALTER TYPE "public"."api_key_permissions_enum" RENAME TO "api_key_permissions_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."api_key_permissions_enum" AS ENUM('write:registries', 'delete:registries', 'write:images', 'delete:images', 'write:sandboxes', 'delete:sandboxes', 'read:volumes', 'write:volumes', 'delete:volumes')`, - ) - await queryRunner.query( - `ALTER TABLE "api_key" ALTER COLUMN "permissions" TYPE "public"."api_key_permissions_enum"[] USING "permissions"::"text"::"public"."api_key_permissions_enum"[]`, - ) - await queryRunner.query(`DROP TYPE "public"."api_key_permissions_enum_old"`) - await queryRunner.query( - `ALTER TYPE "public"."organization_role_permissions_enum" RENAME TO "organization_role_permissions_enum_old"`, - ) - await queryRunner.query( - `CREATE TYPE "public"."organization_role_permissions_enum" AS ENUM('write:registries', 'delete:registries', 'write:images', 'delete:images', 'write:sandboxes', 'delete:sandboxes', 'read:volumes', 'write:volumes', 'delete:volumes')`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role" ALTER COLUMN "permissions" TYPE "public"."organization_role_permissions_enum"[] USING "permissions"::"text"::"public"."organization_role_permissions_enum"[]`, - ) - await queryRunner.query(`DROP TYPE "public"."organization_role_permissions_enum_old"`) - - // add volumes admin role - await queryRunner.query(` - INSERT INTO "organization_role" - ("id", "name", "description", "permissions", "isGlobal") - VALUES - ( - '${GlobalOrganizationRolesIds.VOLUMES_ADMIN}', - 'Volumes Admin', - 'Grants admin access to volumes in the organization', - ARRAY[ - '${OrganizationResourcePermission.READ_VOLUMES}', - '${OrganizationResourcePermission.WRITE_VOLUMES}', - '${OrganizationResourcePermission.DELETE_VOLUMES}' - ]::organization_role_permissions_enum[], - TRUE - ) - `) - } - - public async down(queryRunner: QueryRunner): Promise { - // remove volumes admin role - await queryRunner.query( - `DELETE FROM "organization_role" WHERE "id" = '${GlobalOrganizationRolesIds.VOLUMES_ADMIN}'`, - ) - - // revert enums - await queryRunner.query( - `CREATE TYPE "public"."organization_role_permissions_enum_old" AS ENUM('write:registries', 'delete:registries', 'write:images', 'delete:images', 'write:sandboxes', 'delete:sandboxes')`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role" ALTER COLUMN "permissions" TYPE "public"."organization_role_permissions_enum_old"[] USING "permissions"::"text"::"public"."organization_role_permissions_enum_old"[]`, - ) - await queryRunner.query(`DROP TYPE "public"."organization_role_permissions_enum"`) - await queryRunner.query( - `ALTER TYPE "public"."organization_role_permissions_enum_old" RENAME TO "organization_role_permissions_enum"`, - ) - await queryRunner.query( - `CREATE TYPE "public"."api_key_permissions_enum_old" AS ENUM('write:registries', 'delete:registries', 'write:images', 'delete:images', 'write:sandboxes', 'delete:sandboxes')`, - ) - await queryRunner.query( - `ALTER TABLE "api_key" ALTER COLUMN "permissions" TYPE "public"."api_key_permissions_enum_old"[] USING "permissions"::"text"::"public"."api_key_permissions_enum_old"[]`, - ) - await queryRunner.query(`DROP TYPE "public"."api_key_permissions_enum"`) - await queryRunner.query(`ALTER TYPE "public"."api_key_permissions_enum_old" RENAME TO "api_key_permissions_enum"`) - } -} diff --git a/apps/api/src/migrations/1744971114480-migration.ts b/apps/api/src/migrations/1744971114480-migration.ts deleted file mode 100644 index b0f60351d..000000000 --- a/apps/api/src/migrations/1744971114480-migration.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1744971114480 implements MigrationInterface { - name = 'Migration1744971114480' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "workspace" ADD "volumes" jsonb NOT NULL DEFAULT '[]'`) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "authToken" SET DEFAULT MD5(random()::text)`) - await queryRunner.query(`ALTER TABLE "volume" DROP COLUMN "state"`) - await queryRunner.query( - `CREATE TYPE "public"."volume_state_enum" AS ENUM('creating', 'ready', 'pending_create', 'pending_delete', 'deleting', 'deleted', 'error')`, - ) - await queryRunner.query( - `ALTER TABLE "volume" ADD "state" "public"."volume_state_enum" NOT NULL DEFAULT 'pending_create'`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "volume" DROP COLUMN "state"`) - await queryRunner.query(`DROP TYPE "public"."volume_state_enum"`) - await queryRunner.query(`ALTER TABLE "volume" ADD "state" character varying NOT NULL`) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "authToken" SET DEFAULT md5((random()))`) - await queryRunner.query(`ALTER TABLE "workspace" DROP COLUMN "volumes"`) - } -} diff --git a/apps/api/src/migrations/1745393243334-migration.ts b/apps/api/src/migrations/1745393243334-migration.ts deleted file mode 100644 index bf3b3f59b..000000000 --- a/apps/api/src/migrations/1745393243334-migration.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1745393243334 implements MigrationInterface { - name = 'Migration1745393243334' - - public async up(queryRunner: QueryRunner): Promise { - // First, get all images with their current entrypoint values - const images = await queryRunner.query(`SELECT id, entrypoint FROM "image" WHERE entrypoint IS NOT NULL`) - - // Rename the column to avoid data loss - await queryRunner.query(`ALTER TABLE "image" RENAME COLUMN "entrypoint" TO "entrypoint_old"`) - - // Add the new jsonb column - await queryRunner.query(`ALTER TABLE "image" ADD "entrypoint" text[]`) - - // Update each image to convert the string entrypoint to a JSON array - for (const image of images) { - const entrypointValue = image.entrypoint - if (entrypointValue) { - // Convert the string to a JSON array with a single element - await queryRunner.query(`UPDATE "image" SET "entrypoint" = $1 WHERE id = $2`, [ - entrypointValue.split(' '), - image.id, - ]) - } - } - - // Drop the old column - await queryRunner.query(`ALTER TABLE "image" DROP COLUMN "entrypoint_old"`) - } - - public async down(queryRunner: QueryRunner): Promise { - // First, get all images with their current entrypoint values - const images = await queryRunner.query(`SELECT id, entrypoint FROM "image" WHERE entrypoint IS NOT NULL`) - - // Rename the column to avoid data loss - await queryRunner.query(`ALTER TABLE "image" RENAME COLUMN "entrypoint" TO "entrypoint_old"`) - - // Add the new character varying column - await queryRunner.query(`ALTER TABLE "image" ADD "entrypoint" character varying`) - - // Update each image to convert the JSON array to a string - for (const image of images) { - const entrypointArray = image.entrypoint_old - if (entrypointArray && Array.isArray(entrypointArray) && entrypointArray.length > 0) { - // Take the first element of the array as the string value - await queryRunner.query(`UPDATE "image" SET "entrypoint" = $1 WHERE id = $2`, [entrypointArray[0], image.id]) - } - } - - // Drop the old column - await queryRunner.query(`ALTER TABLE "image" DROP COLUMN "entrypoint_old"`) - } -} diff --git a/apps/api/src/migrations/1745494761360-migration.ts b/apps/api/src/migrations/1745494761360-migration.ts deleted file mode 100644 index 68a83e932..000000000 --- a/apps/api/src/migrations/1745494761360-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1745494761360 implements MigrationInterface { - name = 'Migration1745494761360' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "user" ADD "emailVerified" boolean NOT NULL DEFAULT false`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "emailVerified"`) - } -} diff --git a/apps/api/src/migrations/1745574377029-migration.ts b/apps/api/src/migrations/1745574377029-migration.ts deleted file mode 100644 index 4f2f85fd5..000000000 --- a/apps/api/src/migrations/1745574377029-migration.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1745574377029 implements MigrationInterface { - name = 'Migration1745574377029' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "build_info" ("imageRef" character varying NOT NULL, "dockerfileContent" text, "contextHashes" text, "lastUsedAt" TIMESTAMP NOT NULL DEFAULT now(), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "build_info_imageRef_pk" PRIMARY KEY ("imageRef"))`, - ) - await queryRunner.renameColumn('image_node', 'internalImageName', 'imageRef') - await queryRunner.query(`ALTER TABLE "image_node" DROP COLUMN "image"`) - await queryRunner.query(`ALTER TABLE "image" ADD "buildInfoImageRef" character varying`) - await queryRunner.query(`ALTER TABLE "workspace" ADD "buildInfoImageRef" character varying`) - await queryRunner.query(`ALTER TYPE "public"."image_node_state_enum" RENAME TO "image_node_state_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."image_node_state_enum" AS ENUM('pulling_image', 'building_image', 'ready', 'error', 'removing')`, - ) - await queryRunner.query(`ALTER TABLE "image_node" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "image_node" ALTER COLUMN "state" TYPE "public"."image_node_state_enum" USING "state"::"text"::"public"."image_node_state_enum"`, - ) - await queryRunner.query(`ALTER TABLE "image_node" ALTER COLUMN "state" SET DEFAULT 'pulling_image'`) - await queryRunner.query(`DROP TYPE "public"."image_node_state_enum_old"`) - await queryRunner.query(`ALTER TYPE "public"."image_state_enum" RENAME TO "image_state_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."image_state_enum" AS ENUM('build_pending', 'building', 'pending', 'pulling_image', 'pending_validation', 'validating', 'active', 'error', 'removing')`, - ) - await queryRunner.query(`ALTER TABLE "image" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "image" ALTER COLUMN "state" TYPE "public"."image_state_enum" USING "state"::"text"::"public"."image_state_enum"`, - ) - await queryRunner.query(`ALTER TABLE "image" ALTER COLUMN "state" SET DEFAULT 'pending'`) - await queryRunner.query(`DROP TYPE "public"."image_state_enum_old"`) - await queryRunner.query(`ALTER TYPE "public"."workspace_state_enum" RENAME TO "workspace_state_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."workspace_state_enum" AS ENUM('creating', 'restoring', 'destroyed', 'destroying', 'started', 'stopped', 'starting', 'stopping', 'resizing', 'error', 'pending_build', 'building_image', 'unknown', 'pulling_image', 'archiving', 'archived')`, - ) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "workspace" ALTER COLUMN "state" TYPE "public"."workspace_state_enum" USING "state"::"text"::"public"."workspace_state_enum"`, - ) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "state" SET DEFAULT 'unknown'`) - await queryRunner.query(`DROP TYPE "public"."workspace_state_enum_old"`) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "image" DROP NOT NULL`) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "authToken" SET DEFAULT MD5(random()::text)`) - await queryRunner.query( - `ALTER TABLE "image" ADD CONSTRAINT "image_buildInfoImageRef_fk" FOREIGN KEY ("buildInfoImageRef") REFERENCES "build_info"("imageRef") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ) - await queryRunner.query( - `ALTER TABLE "workspace" ADD CONSTRAINT "workspace_buildInfoImageRef_fk" FOREIGN KEY ("buildInfoImageRef") REFERENCES "build_info"("imageRef") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "workspace" DROP CONSTRAINT "workspace_buildInfoImageRef_fk"`) - await queryRunner.query(`ALTER TABLE "image" DROP CONSTRAINT "image_buildInfoImageRef_fk"`) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "authToken" SET DEFAULT md5((random()))`) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "image" SET NOT NULL`) - await queryRunner.query( - `CREATE TYPE "public"."workspace_state_enum_old" AS ENUM('archived', 'archiving', 'creating', 'destroyed', 'destroying', 'error', 'pulling_image', 'resizing', 'restoring', 'started', 'starting', 'stopped', 'stopping', 'unknown')`, - ) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "workspace" ALTER COLUMN "state" TYPE "public"."workspace_state_enum_old" USING "state"::"text"::"public"."workspace_state_enum_old"`, - ) - await queryRunner.query(`ALTER TABLE "workspace" ALTER COLUMN "state" SET DEFAULT 'unknown'`) - await queryRunner.query(`DROP TYPE "public"."workspace_state_enum"`) - await queryRunner.query(`ALTER TYPE "public"."workspace_state_enum_old" RENAME TO "workspace_state_enum"`) - await queryRunner.query( - `CREATE TYPE "public"."image_state_enum_old" AS ENUM('active', 'error', 'pending', 'pending_validation', 'pulling_image', 'removing', 'validating')`, - ) - await queryRunner.query(`ALTER TABLE "image" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "image" ALTER COLUMN "state" TYPE "public"."image_state_enum_old" USING "state"::"text"::"public"."image_state_enum_old"`, - ) - await queryRunner.query(`ALTER TABLE "image" ALTER COLUMN "state" SET DEFAULT 'pending'`) - await queryRunner.query(`DROP TYPE "public"."image_state_enum"`) - await queryRunner.query(`ALTER TYPE "public"."image_state_enum_old" RENAME TO "image_state_enum"`) - await queryRunner.query( - `CREATE TYPE "public"."image_node_state_enum_old" AS ENUM('error', 'pulling_image', 'ready', 'removing')`, - ) - await queryRunner.query(`ALTER TABLE "image_node" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "image_node" ALTER COLUMN "state" TYPE "public"."image_node_state_enum_old" USING "state"::"text"::"public"."image_node_state_enum_old"`, - ) - await queryRunner.query(`ALTER TABLE "image_node" ALTER COLUMN "state" SET DEFAULT 'pulling_image'`) - await queryRunner.query(`DROP TYPE "public"."image_node_state_enum"`) - await queryRunner.query(`ALTER TYPE "public"."image_node_state_enum_old" RENAME TO "image_node_state_enum"`) - await queryRunner.query(`ALTER TABLE "workspace" DROP COLUMN "buildInfoImageRef"`) - await queryRunner.query(`ALTER TABLE "image" DROP COLUMN "buildInfoImageRef"`) - await queryRunner.renameColumn('image_node', 'imageRef', 'internalImageName') - await queryRunner.query(`ALTER TABLE "image_node" ADD "image" character varying NOT NULL`) - await queryRunner.query(`DROP TABLE "build_info"`) - } -} diff --git a/apps/api/src/migrations/1745840296260-migration.ts b/apps/api/src/migrations/1745840296260-migration.ts deleted file mode 100644 index 688c3eab8..000000000 --- a/apps/api/src/migrations/1745840296260-migration.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' -import * as crypto from 'crypto' - -export class Migration1745840296260 implements MigrationInterface { - name = 'Migration1745840296260' - - public async up(queryRunner: QueryRunner): Promise { - // Add the new columns - await queryRunner.query(`ALTER TABLE "api_key" ADD "keyHash" character varying NOT NULL DEFAULT ''`) - await queryRunner.query(`ALTER TABLE "api_key" ADD "keyPrefix" character varying NOT NULL DEFAULT ''`) - await queryRunner.query(`ALTER TABLE "api_key" ADD "keySuffix" character varying NOT NULL DEFAULT ''`) - - // Get all existing API keys - const existingKeys = await queryRunner.query(`SELECT "value" FROM "api_key"`) - - // Update each key with its hash, prefix, and suffix - for (const key of existingKeys) { - const value = key.value - const keyHash = crypto.createHash('sha256').update(value).digest('hex') - const keyPrefix = value.substring(0, 3) - const keySuffix = value.slice(-3) - - await queryRunner.query( - `UPDATE "api_key" - SET "keyHash" = $1, - "keyPrefix" = $2, - "keySuffix" = $3 - WHERE "value" = $4`, - [keyHash, keyPrefix, keySuffix, value], - ) - } - - // Drop value column and its unique constraint - await queryRunner.query(`ALTER TABLE "api_key" DROP CONSTRAINT "UQ_4b0873b633484d5de20b2d8f852"`) - await queryRunner.query(`ALTER TABLE "api_key" DROP COLUMN "value"`) - - // Add unique constraint - await queryRunner.query(`ALTER TABLE "api_key" ADD CONSTRAINT "api_key_keyHash_unique" UNIQUE ("keyHash")`) - } - - public async down(queryRunner: QueryRunner): Promise { - // Revert the schema changes - await queryRunner.query(`ALTER TABLE "api_key" DROP CONSTRAINT "api_key_keyHash_unique"`) - await queryRunner.query(`ALTER TABLE "api_key" DROP COLUMN "keySuffix"`) - await queryRunner.query(`ALTER TABLE "api_key" DROP COLUMN "keyPrefix"`) - await queryRunner.query(`ALTER TABLE "api_key" DROP COLUMN "keyHash"`) - await queryRunner.query(`ALTER TABLE "api_key" ADD "value" character varying NOT NULL DEFAULT ''`) - } -} diff --git a/apps/api/src/migrations/1745864972652-migration.ts b/apps/api/src/migrations/1745864972652-migration.ts deleted file mode 100644 index ea92927f1..000000000 --- a/apps/api/src/migrations/1745864972652-migration.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1745864972652 implements MigrationInterface { - name = 'Migration1745864972652' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE "workspace" - ALTER COLUMN "env" DROP DEFAULT, - ALTER COLUMN "env" TYPE jsonb USING "env"::jsonb, - ALTER COLUMN "env" SET DEFAULT '{}'::jsonb, - ALTER COLUMN "env" SET NOT NULL; - `) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE "workspace" - ALTER COLUMN "env" DROP DEFAULT, - ALTER COLUMN "env" TYPE text USING "env"::text, - ALTER COLUMN "env" SET DEFAULT '{}'::text, - ALTER COLUMN "env" SET NOT NULL; - `) - } -} diff --git a/apps/api/src/migrations/1746354231722-migration.ts b/apps/api/src/migrations/1746354231722-migration.ts deleted file mode 100644 index 1e142e5d6..000000000 --- a/apps/api/src/migrations/1746354231722-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1746354231722 implements MigrationInterface { - name = 'Migration1746354231722' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "image" ADD "buildNodeId" character varying`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "image" DROP COLUMN "buildNodeId"`) - } -} diff --git a/apps/api/src/migrations/1746604150910-migration.ts b/apps/api/src/migrations/1746604150910-migration.ts deleted file mode 100644 index 2e6425f7b..000000000 --- a/apps/api/src/migrations/1746604150910-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1746604150910 implements MigrationInterface { - name = 'Migration1746604150910' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" ADD "volume_quota" integer NOT NULL DEFAULT '10'`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "volume_quota"`) - } -} diff --git a/apps/api/src/migrations/1747658203010-migration.ts b/apps/api/src/migrations/1747658203010-migration.ts deleted file mode 100644 index 5c54ed143..000000000 --- a/apps/api/src/migrations/1747658203010-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1747658203010 implements MigrationInterface { - name = 'Migration1747658203010' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "api_key" ADD "lastUsedAt" TIMESTAMP`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "api_key" DROP COLUMN "lastUsedAt"`) - } -} diff --git a/apps/api/src/migrations/1748006546552-migration.ts b/apps/api/src/migrations/1748006546552-migration.ts deleted file mode 100644 index 94699b60e..000000000 --- a/apps/api/src/migrations/1748006546552-migration.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1748006546552 implements MigrationInterface { - name = 'Migration1748006546552' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "max_concurrent_workspaces"`) - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "workspace_quota"`) - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "total_image_size"`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "total_memory_quota" SET DEFAULT '10'`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "total_disk_quota" SET DEFAULT '30'`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "max_cpu_per_workspace" SET DEFAULT '4'`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "max_memory_per_workspace" SET DEFAULT '8'`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "max_image_size" SET DEFAULT '20'`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "image_quota" SET DEFAULT '100'`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "volume_quota" SET DEFAULT '100'`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "volume_quota" SET DEFAULT '10'`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "image_quota" SET DEFAULT '0'`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "max_image_size" SET DEFAULT '2'`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "max_memory_per_workspace" SET DEFAULT '4'`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "max_cpu_per_workspace" SET DEFAULT '2'`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "total_disk_quota" SET DEFAULT '100'`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "total_memory_quota" SET DEFAULT '40'`) - await queryRunner.query(`ALTER TABLE "organization" ADD "total_image_size" integer NOT NULL DEFAULT '5'`) - await queryRunner.query(`ALTER TABLE "organization" ADD "workspace_quota" integer NOT NULL DEFAULT '0'`) - await queryRunner.query(`ALTER TABLE "organization" ADD "max_concurrent_workspaces" integer NOT NULL DEFAULT '10'`) - } -} diff --git a/apps/api/src/migrations/1748866194353-migration.ts b/apps/api/src/migrations/1748866194353-migration.ts deleted file mode 100644 index 793989493..000000000 --- a/apps/api/src/migrations/1748866194353-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1748866194353 implements MigrationInterface { - name = 'Migration1748866194353' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "workspace" ADD "autoArchiveInterval" integer NOT NULL DEFAULT '10080'`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "workspace" DROP COLUMN "autoArchiveInterval"`) - } -} diff --git a/apps/api/src/migrations/1749474791343-migration.ts b/apps/api/src/migrations/1749474791343-migration.ts deleted file mode 100644 index 4bd919693..000000000 --- a/apps/api/src/migrations/1749474791343-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1749474791343 implements MigrationInterface { - name = 'Migration1749474791343' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "api_key" ADD "expiresAt" TIMESTAMP`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "api_key" DROP COLUMN "expiresAt"`) - } -} diff --git a/apps/api/src/migrations/1749474791344-migration.ts b/apps/api/src/migrations/1749474791344-migration.ts deleted file mode 100644 index e7beb601e..000000000 --- a/apps/api/src/migrations/1749474791344-migration.ts +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1749474791344 implements MigrationInterface { - name = 'Migration1749474791344' - - public async up(queryRunner: QueryRunner): Promise { - // Snapshot to backup rename - await queryRunner.renameColumn('workspace', 'snapshotRegistryId', 'backupRegistryId') - await queryRunner.renameColumn('workspace', 'snapshotImage', 'backupImage') - await queryRunner.renameColumn('workspace', 'lastSnapshotAt', 'lastBackupAt') - await queryRunner.renameColumn('workspace', 'snapshotState', 'backupState') - await queryRunner.renameColumn('workspace', 'existingSnapshotImages', 'existingBackupImages') - - // Node to runner rename - await queryRunner.renameColumn('image', 'buildNodeId', 'buildRunnerId') - await queryRunner.renameTable('image_node', 'image_runner') - await queryRunner.renameColumn('image_runner', 'nodeId', 'runnerId') - await queryRunner.renameTable('node', 'runner') - await queryRunner.renameColumn('workspace', 'nodeId', 'runnerId') - await queryRunner.renameColumn('workspace', 'prevNodeId', 'prevRunnerId') - - // Image to snapshot rename - await queryRunner.renameColumn('warm_pool', 'image', 'snapshot') - await queryRunner.renameColumn('organization', 'image_quota', 'snapshot_quota') - await queryRunner.renameColumn('organization', 'max_image_size', 'max_snapshot_size') - await queryRunner.query( - `ALTER TYPE "public"."organization_role_permissions_enum" RENAME VALUE 'write:images' TO 'write:snapshots'`, - ) - await queryRunner.query( - `ALTER TYPE "public"."organization_role_permissions_enum" RENAME VALUE 'delete:images' TO 'delete:snapshots'`, - ) - await queryRunner.query( - `ALTER TYPE "public"."api_key_permissions_enum" RENAME VALUE 'write:images' TO 'write:snapshots'`, - ) - await queryRunner.query( - `ALTER TYPE "public"."api_key_permissions_enum" RENAME VALUE 'delete:images' TO 'delete:snapshots'`, - ) - await queryRunner.renameTable('image_runner', 'snapshot_runner') - await queryRunner.renameColumn('snapshot_runner', 'imageRef', 'snapshotRef') - await queryRunner.query( - `ALTER TYPE "public"."snapshot_runner_state_enum" RENAME VALUE 'pulling_image' TO 'pulling_snapshot'`, - ) - await queryRunner.query( - `ALTER TYPE "public"."snapshot_runner_state_enum" RENAME VALUE 'building_image' TO 'building_snapshot'`, - ) - await queryRunner.query(`ALTER TABLE "snapshot_runner" ALTER COLUMN "state" SET DEFAULT 'pulling_snapshot'`) - await queryRunner.renameColumn('build_info', 'imageRef', 'snapshotRef') - await queryRunner.renameTable('image', 'snapshot') - await queryRunner.renameColumn('snapshot', 'buildInfoImageRef', 'buildInfoSnapshotRef') - await queryRunner.query(`ALTER TYPE "public"."snapshot_state_enum" RENAME VALUE 'pulling_image' TO 'pulling'`) - await queryRunner.renameColumn('workspace', 'image', 'snapshot') - await queryRunner.renameColumn('workspace', 'buildInfoImageRef', 'buildInfoSnapshotRef') - await queryRunner.renameColumn('workspace', 'backupImage', 'backupSnapshot') - await queryRunner.renameColumn('workspace', 'existingBackupImages', 'existingBackupSnapshots') - await queryRunner.query( - `ALTER TYPE "public"."workspace_state_enum" RENAME VALUE 'pulling_image' TO 'pulling_snapshot'`, - ) - await queryRunner.query( - `ALTER TYPE "public"."workspace_state_enum" RENAME VALUE 'building_image' TO 'building_snapshot'`, - ) - - // Workspace to sandbox rename - await queryRunner.renameTable('workspace', 'sandbox') - await queryRunner.renameTable('workspace_usage_periods', 'sandbox_usage_periods') - await queryRunner.renameColumn('sandbox_usage_periods', 'workspaceId', 'sandboxId') - await queryRunner.renameColumn('organization', 'max_cpu_per_workspace', 'max_cpu_per_sandbox') - await queryRunner.renameColumn('organization', 'max_memory_per_workspace', 'max_memory_per_sandbox') - await queryRunner.renameColumn('organization', 'max_disk_per_workspace', 'max_disk_per_sandbox') - - // Snapshot fields - await queryRunner.query(`ALTER TABLE "snapshot" ADD "imageName" character varying NOT NULL DEFAULT ''`) - await queryRunner.query(`ALTER TABLE "snapshot" ADD "cpu" integer NOT NULL DEFAULT '1'`) - await queryRunner.query(`ALTER TABLE "snapshot" ADD "gpu" integer NOT NULL DEFAULT '0'`) - await queryRunner.query(`ALTER TABLE "snapshot" ADD "mem" integer NOT NULL DEFAULT '1'`) - await queryRunner.query(`ALTER TABLE "snapshot" ADD "disk" integer NOT NULL DEFAULT '3'`) - await queryRunner.query(`UPDATE "snapshot" SET "imageName" = "name"`) - - // Add hideFromUsers column - await queryRunner.query(`ALTER TABLE "snapshot" ADD "hideFromUsers" boolean NOT NULL DEFAULT false`) - - // Set hideFromUsers to true for general snapshots with names starting with "boxlite-ai/" - await queryRunner.query( - `UPDATE "snapshot" SET "hideFromUsers" = true WHERE "general" = true AND "name" LIKE 'boxlite-ai/%'`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - // Remove hideFromUsers column - await queryRunner.query(`ALTER TABLE "snapshot" DROP COLUMN "hideFromUsers"`) - - // Snapshot fields - await queryRunner.query(`ALTER TABLE "snapshot" DROP COLUMN "disk"`) - await queryRunner.query(`ALTER TABLE "snapshot" DROP COLUMN "mem"`) - await queryRunner.query(`ALTER TABLE "snapshot" DROP COLUMN "gpu"`) - await queryRunner.query(`ALTER TABLE "snapshot" DROP COLUMN "cpu"`) - await queryRunner.query(`ALTER TABLE "snapshot" DROP COLUMN "imageName"`) - - // Revert workspace to sandbox rename - await queryRunner.renameColumn('organization', 'max_disk_per_sandbox', 'max_disk_per_workspace') - await queryRunner.renameColumn('organization', 'max_memory_per_sandbox', 'max_memory_per_workspace') - await queryRunner.renameColumn('organization', 'max_cpu_per_sandbox', 'max_cpu_per_workspace') - await queryRunner.renameColumn('sandbox_usage_periods', 'sandboxId', 'workspaceId') - await queryRunner.renameTable('sandbox_usage_periods', 'workspace_usage_periods') - await queryRunner.renameTable('sandbox', 'workspace') - - // Revert image to snapshot rename - await queryRunner.query( - `ALTER TYPE "public"."workspace_state_enum" RENAME VALUE 'pulling_snapshot' TO 'pulling_image'`, - ) - await queryRunner.query( - `ALTER TYPE "public"."workspace_state_enum" RENAME VALUE 'building_snapshot' TO 'building_image'`, - ) - await queryRunner.renameColumn('workspace', 'existingBackupSnapshots', 'existingBackupImages') - await queryRunner.renameColumn('workspace', 'backupSnapshot', 'backupImage') - await queryRunner.renameColumn('workspace', 'buildInfoSnapshotRef', 'buildInfoImageRef') - await queryRunner.renameColumn('workspace', 'snapshot', 'image') - await queryRunner.query(`ALTER TYPE "public"."snapshot_state_enum" RENAME VALUE 'pulling' TO 'pulling_image'`) - await queryRunner.renameColumn('snapshot', 'buildInfoSnapshotRef', 'buildInfoImageRef') - await queryRunner.renameTable('snapshot', 'image') - await queryRunner.renameColumn('build_info', 'snapshotRef', 'imageRef') - await queryRunner.query( - `ALTER TYPE "public"."snapshot_runner_state_enum" RENAME VALUE 'pulling_snapshot' TO 'pulling_image'`, - ) - await queryRunner.query( - `ALTER TYPE "public"."snapshot_runner_state_enum" RENAME VALUE 'building_snapshot' TO 'building_image'`, - ) - await queryRunner.query(`ALTER TABLE "snapshot_runner" ALTER COLUMN "state" SET DEFAULT 'pulling_image'`) - await queryRunner.renameColumn('snapshot_runner', 'snapshotRef', 'imageRef') - await queryRunner.renameTable('snapshot_runner', 'image_runner') - await queryRunner.query( - `ALTER TYPE "public"."api_key_permissions_enum" RENAME VALUE 'write:snapshots' TO 'write:images'`, - ) - await queryRunner.query( - `ALTER TYPE "public"."api_key_permissions_enum" RENAME VALUE 'delete:snapshots' TO 'delete:images'`, - ) - await queryRunner.query( - `ALTER TYPE "public"."organization_role_permissions_enum" RENAME VALUE 'write:snapshots' TO 'write:images'`, - ) - await queryRunner.query( - `ALTER TYPE "public"."organization_role_permissions_enum" RENAME VALUE 'delete:snapshots' TO 'delete:images'`, - ) - await queryRunner.renameColumn('organization', 'max_snapshot_size', 'max_image_size') - await queryRunner.renameColumn('organization', 'snapshot_quota', 'image_quota') - await queryRunner.renameColumn('warm_pool', 'snapshot', 'image') - - // Revert node to runner rename - await queryRunner.renameColumn('workspace', 'prevRunnerId', 'prevNodeId') - await queryRunner.renameColumn('workspace', 'runnerId', 'nodeId') - await queryRunner.renameTable('runner', 'node') - await queryRunner.renameColumn('image_runner', 'runnerId', 'nodeId') - await queryRunner.renameTable('image_runner', 'image_node') - await queryRunner.renameColumn('image', 'buildRunnerId', 'buildNodeId') - - // Revert snapshot to backup rename - await queryRunner.renameColumn('workspace', 'existingBackupImages', 'existingSnapshotImages') - await queryRunner.renameColumn('workspace', 'backupState', 'snapshotState') - await queryRunner.renameColumn('workspace', 'lastBackupAt', 'lastSnapshotAt') - await queryRunner.renameColumn('workspace', 'backupImage', 'snapshotImage') - await queryRunner.renameColumn('workspace', 'backupRegistryId', 'snapshotRegistryId') - } -} diff --git a/apps/api/src/migrations/1749474791345-migration.ts b/apps/api/src/migrations/1749474791345-migration.ts deleted file mode 100644 index bb9e4a39f..000000000 --- a/apps/api/src/migrations/1749474791345-migration.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1749474791345 implements MigrationInterface { - name = 'Migration1749474791345' - - public async up(queryRunner: QueryRunner): Promise { - // For sandbox_state_enum - await queryRunner.query(`ALTER TYPE "public"."sandbox_state_enum" RENAME TO "sandbox_state_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."sandbox_state_enum" AS ENUM('creating', 'restoring', 'destroyed', 'destroying', 'started', 'stopped', 'starting', 'stopping', 'error', 'build_failed', 'pending_build', 'building_snapshot', 'unknown', 'pulling_snapshot', 'archiving', 'archived')`, - ) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "sandbox" ALTER COLUMN "state" TYPE "public"."sandbox_state_enum" USING "state"::"text"::"public"."sandbox_state_enum"`, - ) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "state" SET DEFAULT 'unknown'`) - await queryRunner.query(`DROP TYPE "public"."sandbox_state_enum_old"`) - - // For snapshot_state_enum - await queryRunner.query(`ALTER TYPE "public"."snapshot_state_enum" RENAME TO "snapshot_state_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."snapshot_state_enum" AS ENUM('build_pending', 'building', 'pending', 'pulling', 'pending_validation', 'validating', 'active', 'error', 'build_failed', 'removing')`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "snapshot" ALTER COLUMN "state" TYPE "public"."snapshot_state_enum" USING "state"::"text"::"public"."snapshot_state_enum"`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" SET DEFAULT 'pending'`) - await queryRunner.query(`DROP TYPE "public"."snapshot_state_enum_old"`) - } - - public async down(queryRunner: QueryRunner): Promise { - // For snapshot_state_enum - recreate without build_failed - await queryRunner.query(`UPDATE "snapshot" SET "state" = 'error' WHERE "state" = 'build_failed'`) - - await queryRunner.query(`ALTER TYPE "public"."snapshot_state_enum" RENAME TO "snapshot_state_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."snapshot_state_enum" AS ENUM('build_pending', 'building', 'pending', 'pulling', 'pending_validation', 'validating', 'active', 'error', 'removing')`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "snapshot" ALTER COLUMN "state" TYPE "public"."snapshot_state_enum" USING "state"::"text"::"public"."snapshot_state_enum"`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" SET DEFAULT 'pending'`) - await queryRunner.query(`DROP TYPE "public"."snapshot_state_enum_old"`) - - // For sandbox_state_enum - recreate without build_failed - await queryRunner.query(`UPDATE "sandbox" SET "state" = 'error' WHERE "state" = 'build_failed'`) - - await queryRunner.query(`ALTER TYPE "public"."sandbox_state_enum" RENAME TO "sandbox_state_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."sandbox_state_enum" AS ENUM('creating', 'restoring', 'destroyed', 'destroying', 'started', 'stopped', 'starting', 'stopping', 'error', 'pending_build', 'building_snapshot', 'unknown', 'pulling_snapshot', 'archiving', 'archived')`, - ) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "sandbox" ALTER COLUMN "state" TYPE "public"."sandbox_state_enum" USING "state"::"text"::"public"."sandbox_state_enum"`, - ) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "state" SET DEFAULT 'unknown'`) - await queryRunner.query(`DROP TYPE "public"."sandbox_state_enum_old"`) - } -} diff --git a/apps/api/src/migrations/1750077343089-migration.ts b/apps/api/src/migrations/1750077343089-migration.ts deleted file mode 100644 index 55a604d21..000000000 --- a/apps/api/src/migrations/1750077343089-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1750077343089 implements MigrationInterface { - name = 'Migration1750077343089' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "sandbox" ADD "daemonVersion" character varying`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN "daemonVersion"`) - } -} diff --git a/apps/api/src/migrations/1750436374899-migration.ts b/apps/api/src/migrations/1750436374899-migration.ts deleted file mode 100644 index a3c5c0fa3..000000000 --- a/apps/api/src/migrations/1750436374899-migration.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1750436374899 implements MigrationInterface { - name = 'Migration1750436374899' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "audit_log" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "actorId" character varying NOT NULL, "actorEmail" character varying NOT NULL DEFAULT '', "organizationId" character varying, "action" character varying NOT NULL, "targetType" character varying, "targetId" character varying, "statusCode" integer, "errorMessage" character varying, "ipAddress" character varying, "userAgent" text, "source" character varying, "metadata" jsonb, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "audit_log_id_pk" PRIMARY KEY ("id"))`, - ) - await queryRunner.query( - `CREATE INDEX "audit_log_organizationId_createdAt_index" ON "audit_log" ("organizationId", "createdAt") `, - ) - await queryRunner.query(`CREATE INDEX "audit_log_createdAt_index" ON "audit_log" ("createdAt") `) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."audit_log_createdAt_index"`) - await queryRunner.query(`DROP INDEX "public"."audit_log_organizationId_createdAt_index"`) - await queryRunner.query(`DROP TABLE "audit_log"`) - } -} diff --git a/apps/api/src/migrations/1750668569562-migration.ts b/apps/api/src/migrations/1750668569562-migration.ts deleted file mode 100644 index 04c9ccd4b..000000000 --- a/apps/api/src/migrations/1750668569562-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1750668569562 implements MigrationInterface { - name = 'Migration1750668569562' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "sandbox" ADD "autoDeleteInterval" integer NOT NULL DEFAULT '-1'`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN "autoDeleteInterval"`) - } -} diff --git a/apps/api/src/migrations/1750751712412-migration.ts b/apps/api/src/migrations/1750751712412-migration.ts deleted file mode 100644 index 6ff2f08aa..000000000 --- a/apps/api/src/migrations/1750751712412-migration.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1750751712412 implements MigrationInterface { - name = 'Migration1750751712412' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TYPE "public"."snapshot_state_enum" RENAME TO "snapshot_state_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."snapshot_state_enum" AS ENUM('build_pending', 'building', 'pending', 'pulling', 'pending_validation', 'validating', 'active', 'inactive', 'error', 'build_failed', 'removing')`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "snapshot" ALTER COLUMN "state" TYPE "public"."snapshot_state_enum" USING "state"::"text"::"public"."snapshot_state_enum"`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" SET DEFAULT 'pending'`) - await queryRunner.query(`DROP TYPE "public"."snapshot_state_enum_old"`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TYPE "public"."snapshot_state_enum_old" AS ENUM('active', 'build_failed', 'build_pending', 'building', 'error', 'pending', 'pending_validation', 'pulling', 'removing', 'validating')`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "snapshot" ALTER COLUMN "state" TYPE "public"."snapshot_state_enum_old" USING "state"::"text"::"public"."snapshot_state_enum_old"`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" SET DEFAULT 'pending'`) - await queryRunner.query(`DROP TYPE "public"."snapshot_state_enum"`) - await queryRunner.query(`ALTER TYPE "public"."snapshot_state_enum_old" RENAME TO "snapshot_state_enum"`) - } -} diff --git a/apps/api/src/migrations/1751456907334-migration.ts b/apps/api/src/migrations/1751456907334-migration.ts deleted file mode 100644 index 91a52d0c9..000000000 --- a/apps/api/src/migrations/1751456907334-migration.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' -import { GlobalOrganizationRolesIds } from '../organization/constants/global-organization-roles.constant' -import { OrganizationResourcePermission } from '../organization/enums/organization-resource-permission.enum' - -export class Migration1751456907334 implements MigrationInterface { - name = 'Migration1751456907334' - - public async up(queryRunner: QueryRunner): Promise { - // update enums - await queryRunner.query(`ALTER TYPE "public"."api_key_permissions_enum" RENAME TO "api_key_permissions_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."api_key_permissions_enum" AS ENUM('write:registries', 'delete:registries', 'write:snapshots', 'delete:snapshots', 'write:sandboxes', 'delete:sandboxes', 'read:volumes', 'write:volumes', 'delete:volumes', 'read:audit_logs')`, - ) - await queryRunner.query( - `ALTER TABLE "api_key" ALTER COLUMN "permissions" TYPE "public"."api_key_permissions_enum"[] USING "permissions"::"text"::"public"."api_key_permissions_enum"[]`, - ) - await queryRunner.query(`DROP TYPE "public"."api_key_permissions_enum_old"`) - await queryRunner.query( - `ALTER TYPE "public"."organization_role_permissions_enum" RENAME TO "organization_role_permissions_enum_old"`, - ) - await queryRunner.query( - `CREATE TYPE "public"."organization_role_permissions_enum" AS ENUM('write:registries', 'delete:registries', 'write:snapshots', 'delete:snapshots', 'write:sandboxes', 'delete:sandboxes', 'read:volumes', 'write:volumes', 'delete:volumes', 'read:audit_logs')`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role" ALTER COLUMN "permissions" TYPE "public"."organization_role_permissions_enum"[] USING "permissions"::"text"::"public"."organization_role_permissions_enum"[]`, - ) - await queryRunner.query(`DROP TYPE "public"."organization_role_permissions_enum_old"`) - - // add auditor role - await queryRunner.query(` - INSERT INTO "organization_role" - ("id", "name", "description", "permissions", "isGlobal") - VALUES - ( - '${GlobalOrganizationRolesIds.AUDITOR}', - 'Auditor', - 'Grants access to audit logs in the organization', - ARRAY[ - '${OrganizationResourcePermission.READ_AUDIT_LOGS}' - ]::organization_role_permissions_enum[], - TRUE - ) - `) - - // update organization role foreign keys - await queryRunner.query( - `ALTER TABLE "organization_role_assignment" DROP CONSTRAINT "organization_role_assignment_roleId_fk"`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment" ADD CONSTRAINT "organization_role_assignment_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "organization_role"("id") ON DELETE CASCADE ON UPDATE CASCADE`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment_invitation" DROP CONSTRAINT "organization_role_assignment_invitation_roleId_fk"`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment_invitation" ADD CONSTRAINT "organization_role_assignment_invitation_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "organization_role"("id") ON DELETE CASCADE ON UPDATE CASCADE`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - // delete auditor role - await queryRunner.query(`DELETE FROM "organization_role" WHERE "id" = '${GlobalOrganizationRolesIds.AUDITOR}'`) - - // remove read:audit_logs permission from api keys and organization roles - await queryRunner.query( - `UPDATE "api_key" SET "permissions" = array_remove("permissions", '${OrganizationResourcePermission.READ_AUDIT_LOGS}')`, - ) - await queryRunner.query( - `UPDATE "organization_role" SET "permissions" = array_remove("permissions", '${OrganizationResourcePermission.READ_AUDIT_LOGS}')`, - ) - - // revert enums - await queryRunner.query( - `CREATE TYPE "public"."organization_role_permissions_enum_old" AS ENUM('delete:registries', 'delete:sandboxes', 'delete:snapshots', 'delete:volumes', 'read:volumes', 'write:registries', 'write:sandboxes', 'write:snapshots', 'write:volumes')`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role" ALTER COLUMN "permissions" TYPE "public"."organization_role_permissions_enum_old"[] USING "permissions"::"text"::"public"."organization_role_permissions_enum_old"[]`, - ) - await queryRunner.query(`DROP TYPE "public"."organization_role_permissions_enum"`) - await queryRunner.query( - `ALTER TYPE "public"."organization_role_permissions_enum_old" RENAME TO "organization_role_permissions_enum"`, - ) - await queryRunner.query( - `CREATE TYPE "public"."api_key_permissions_enum_old" AS ENUM('delete:registries', 'delete:sandboxes', 'delete:snapshots', 'delete:volumes', 'read:volumes', 'write:registries', 'write:sandboxes', 'write:snapshots', 'write:volumes')`, - ) - await queryRunner.query( - `ALTER TABLE "api_key" ALTER COLUMN "permissions" TYPE "public"."api_key_permissions_enum_old"[] USING "permissions"::"text"::"public"."api_key_permissions_enum_old"[]`, - ) - await queryRunner.query(`DROP TYPE "public"."api_key_permissions_enum"`) - await queryRunner.query(`ALTER TYPE "public"."api_key_permissions_enum_old" RENAME TO "api_key_permissions_enum"`) - } -} diff --git a/apps/api/src/migrations/1752494676200-migration.ts b/apps/api/src/migrations/1752494676200-migration.ts deleted file mode 100644 index 22994cc2a..000000000 --- a/apps/api/src/migrations/1752494676200-migration.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1752494676200 implements MigrationInterface { - name = 'Migration1752494676200' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "team"`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "team" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, CONSTRAINT "PK_f57d8293406df4af348402e4b74" PRIMARY KEY ("id"))`, - ) - } -} diff --git a/apps/api/src/migrations/1752494676205-migration.ts b/apps/api/src/migrations/1752494676205-migration.ts deleted file mode 100644 index 28934e422..000000000 --- a/apps/api/src/migrations/1752494676205-migration.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1752494676205 implements MigrationInterface { - name = 'Migration1752494676205' - - public async up(queryRunner: QueryRunner): Promise { - // Convert runner.region from enum to varchar - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "region" TYPE varchar USING "region"::text`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "region" SET DEFAULT 'us'`) - - // Convert sandbox.region from enum to varchar - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "region" TYPE varchar USING "region"::text`) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "region" SET DEFAULT 'us'`) - - // Convert warm_pool.target from enum to varchar - await queryRunner.query(`ALTER TABLE "warm_pool" ALTER COLUMN "target" TYPE varchar USING "target"::text`) - await queryRunner.query(`ALTER TABLE "warm_pool" ALTER COLUMN "target" SET DEFAULT 'us'`) - - // Drop the enum type if it exists - await queryRunner.query(`DROP TYPE IF EXISTS "public"."runner_region_enum"`) - await queryRunner.query(`DROP TYPE IF EXISTS "public"."sandbox_region_enum"`) - await queryRunner.query(`DROP TYPE IF EXISTS "public"."warm_pool_target_enum"`) - } - - public async down(queryRunner: QueryRunner): Promise { - // Recreate the enum type - await queryRunner.query(`CREATE TYPE "public"."warm_pool_target_enum" AS ENUM('eu', 'us', 'asia')`) - await queryRunner.query(`CREATE TYPE "public"."sandbox_region_enum" AS ENUM('eu', 'us', 'asia')`) - await queryRunner.query(`CREATE TYPE "public"."runner_region_enum" AS ENUM('eu', 'us', 'asia')`) - - // Convert back to enum for runner.region - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "region" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "runner" ALTER COLUMN "region" TYPE "public"."runner_region_enum" USING "region"::"public"."runner_region_enum"`, - ) - - // Convert back to enum for sandbox.region - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "region" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "sandbox" ALTER COLUMN "region" TYPE "public"."sandbox_region_enum" USING "region"::"public"."sandbox_region_enum"`, - ) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "region" SET DEFAULT 'eu'`) - - // Convert back to enum for warm_pool.target - await queryRunner.query(`ALTER TABLE "warm_pool" ALTER COLUMN "target" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "warm_pool" ALTER COLUMN "target" TYPE "public"."warm_pool_target_enum" USING "target"::"public"."warm_pool_target_enum"`, - ) - await queryRunner.query(`ALTER TABLE "warm_pool" ALTER COLUMN "target" SET DEFAULT 'eu'`) - } -} diff --git a/apps/api/src/migrations/1752848014862-migration.ts b/apps/api/src/migrations/1752848014862-migration.ts deleted file mode 100644 index a4d230683..000000000 --- a/apps/api/src/migrations/1752848014862-migration.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1752848014862 implements MigrationInterface { - name = 'Migration1752848014862' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "warm_pool" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "warm_pool" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "lastChecked" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "sandbox_usage_periods" ALTER COLUMN "startAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "sandbox_usage_periods" ALTER COLUMN "endAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "snapshot_runner" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "snapshot_runner" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "lastActivityAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "lastBackupAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "build_info" ALTER COLUMN "lastUsedAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "build_info" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "build_info" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "docker_registry" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "docker_registry" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "volume" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "volume" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "organization_user" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "organization_user" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query( - `ALTER TABLE "organization_invitation" ALTER COLUMN "expiresAt" TYPE TIMESTAMP WITH TIME ZONE`, - ) - await queryRunner.query( - `ALTER TABLE "organization_invitation" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE`, - ) - await queryRunner.query( - `ALTER TABLE "organization_invitation" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE`, - ) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "suspendedAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "suspendedUntil" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "organization_role" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "organization_role" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization_role" ALTER COLUMN "updatedAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "organization_role" ALTER COLUMN "createdAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "updatedAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "createdAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "suspendedUntil" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "suspendedAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "organization_invitation" ALTER COLUMN "updatedAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "organization_invitation" ALTER COLUMN "createdAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "organization_invitation" ALTER COLUMN "expiresAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "organization_user" ALTER COLUMN "updatedAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "organization_user" ALTER COLUMN "createdAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "volume" ALTER COLUMN "updatedAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "volume" ALTER COLUMN "createdAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "docker_registry" ALTER COLUMN "updatedAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "docker_registry" ALTER COLUMN "createdAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "updatedAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "createdAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "imageName" SET DEFAULT ''`) - await queryRunner.query(`ALTER TABLE "build_info" ALTER COLUMN "updatedAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "build_info" ALTER COLUMN "createdAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "build_info" ALTER COLUMN "lastUsedAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "lastBackupAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "lastActivityAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "updatedAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "createdAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "snapshot_runner" ALTER COLUMN "updatedAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "snapshot_runner" ALTER COLUMN "createdAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "sandbox_usage_periods" ALTER COLUMN "endAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "sandbox_usage_periods" ALTER COLUMN "startAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "updatedAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "createdAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "lastChecked" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "warm_pool" ALTER COLUMN "updatedAt" TYPE TIMESTAMP`) - await queryRunner.query(`ALTER TABLE "warm_pool" ALTER COLUMN "createdAt" TYPE TIMESTAMP`) - } -} diff --git a/apps/api/src/migrations/1753099115783-migration.ts b/apps/api/src/migrations/1753099115783-migration.ts deleted file mode 100644 index 73d6c6f0b..000000000 --- a/apps/api/src/migrations/1753099115783-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1753099115783 implements MigrationInterface { - name = 'Migration1753099115783' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "snapshot" DROP COLUMN "enabled"`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "snapshot" ADD "enabled" boolean NOT NULL DEFAULT true`) - } -} diff --git a/apps/api/src/migrations/1753100751730-migration.ts b/apps/api/src/migrations/1753100751730-migration.ts deleted file mode 100644 index a56f51a1c..000000000 --- a/apps/api/src/migrations/1753100751730-migration.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1753100751730 implements MigrationInterface { - name = 'Migration1753100751730' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.renameColumn('runner', 'memory', 'memoryGiB') - await queryRunner.renameColumn('runner', 'disk', 'diskGiB') - await queryRunner.query( - `ALTER TABLE "runner" ADD "currentCpuUsagePercentage" double precision NOT NULL DEFAULT '0'`, - ) - await queryRunner.query( - `ALTER TABLE "runner" ADD "currentMemoryUsagePercentage" double precision NOT NULL DEFAULT '0'`, - ) - await queryRunner.query( - `ALTER TABLE "runner" ADD "currentDiskUsagePercentage" double precision NOT NULL DEFAULT '0'`, - ) - await queryRunner.query(`ALTER TABLE "runner" ADD "currentAllocatedCpu" integer NOT NULL DEFAULT '0'`) - await queryRunner.query(`ALTER TABLE "runner" ADD "currentAllocatedMemoryGiB" integer NOT NULL DEFAULT '0'`) - await queryRunner.query(`ALTER TABLE "runner" ADD "currentAllocatedDiskGiB" integer NOT NULL DEFAULT '0'`) - await queryRunner.query(`ALTER TABLE "runner" ADD "currentSnapshotCount" integer NOT NULL DEFAULT '0'`) - await queryRunner.query(`ALTER TABLE "runner" ADD "availabilityScore" integer NOT NULL DEFAULT '0'`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "availabilityScore"`) - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "currentSnapshotCount"`) - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "currentAllocatedDiskGiB"`) - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "currentAllocatedMemoryGiB"`) - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "currentAllocatedCpu"`) - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "currentDiskUsagePercentage"`) - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "currentMemoryUsagePercentage"`) - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "currentCpuUsagePercentage"`) - await queryRunner.renameColumn('runner', 'diskGiB', 'disk') - await queryRunner.renameColumn('runner', 'memoryGiB', 'memory') - } -} diff --git a/apps/api/src/migrations/1753100751731-migration.ts b/apps/api/src/migrations/1753100751731-migration.ts deleted file mode 100644 index 923132d6c..000000000 --- a/apps/api/src/migrations/1753100751731-migration.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' -import { GlobalOrganizationRolesIds } from '../organization/constants/global-organization-roles.constant' - -export class Migration1753100751731 implements MigrationInterface { - name = 'Migration1753100751731' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE "organization_role" - SET "name" = 'Snapshots Admin', "description" = 'Grants admin access to snapshots in the organization' - WHERE "id" = '${GlobalOrganizationRolesIds.TEMPLATES_ADMIN}' - `) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - UPDATE "organization_role" - SET "name" = 'Images Admin', "description" = 'Grants admin access to images in the organization' - WHERE "id" = '${GlobalOrganizationRolesIds.TEMPLATES_ADMIN}' - `) - } -} diff --git a/apps/api/src/migrations/1753185133351-migration.ts b/apps/api/src/migrations/1753185133351-migration.ts deleted file mode 100644 index 2b1d470ec..000000000 --- a/apps/api/src/migrations/1753185133351-migration.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1753185133351 implements MigrationInterface { - name = 'Migration1753185133351' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "runner" ADD "version" character varying NOT NULL DEFAULT '0'`) - await queryRunner.query(`ALTER TABLE "runner" ADD "proxyUrl" character varying NOT NULL DEFAULT ''`) - // Copy apiUrl to proxyUrl for all existing records - await queryRunner.query(`UPDATE "runner" SET "proxyUrl" = "apiUrl"`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "version"`) - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "proxyUrl"`) - } -} diff --git a/apps/api/src/migrations/1753274135567-migration.ts b/apps/api/src/migrations/1753274135567-migration.ts deleted file mode 100644 index 94058d2f1..000000000 --- a/apps/api/src/migrations/1753274135567-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1753274135567 implements MigrationInterface { - name = 'Migration1753274135567' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "docker_registry" ALTER COLUMN "project" SET DEFAULT ''`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "docker_registry" ALTER COLUMN "project" DROP DEFAULT`) - } -} diff --git a/apps/api/src/migrations/1753430929609-migration.ts b/apps/api/src/migrations/1753430929609-migration.ts deleted file mode 100644 index abeeec971..000000000 --- a/apps/api/src/migrations/1753430929609-migration.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1753430929609 implements MigrationInterface { - name = 'Migration1753430929609' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "user" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`) - - // For existing users, set createdAt to match their personal organization's createdAt - await queryRunner.query(` - UPDATE "user" u - SET "createdAt" = o."createdAt" - FROM "organization" o - WHERE o."createdBy" = u.id - AND o.personal = true; - `) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "createdAt"`) - } -} diff --git a/apps/api/src/migrations/1753717830378-migration.ts b/apps/api/src/migrations/1753717830378-migration.ts deleted file mode 100644 index 060af620c..000000000 --- a/apps/api/src/migrations/1753717830378-migration.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1753717830378 implements MigrationInterface { - name = 'Migration1753717830378' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "sandbox" ADD "networkBlockAll" boolean NOT NULL DEFAULT false`) - await queryRunner.query(`ALTER TABLE "sandbox" ADD "networkAllowList" character varying`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN "networkAllowList"`) - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN "networkBlockAll"`) - } -} diff --git a/apps/api/src/migrations/1754042247109-migration.ts b/apps/api/src/migrations/1754042247109-migration.ts deleted file mode 100644 index 9a13757ef..000000000 --- a/apps/api/src/migrations/1754042247109-migration.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1754042247109 implements MigrationInterface { - name = 'Migration1754042247109' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "organization" ADD "suspensionCleanupGracePeriodHours" integer NOT NULL DEFAULT '24'`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "suspensionCleanupGracePeriodHours"`) - } -} diff --git a/apps/api/src/migrations/1755003696741-migration.ts b/apps/api/src/migrations/1755003696741-migration.ts deleted file mode 100644 index 9f3bfa6c4..000000000 --- a/apps/api/src/migrations/1755003696741-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1755003696741 implements MigrationInterface { - name = 'Migration1755003696741' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "sandbox" ADD "backupErrorReason" text`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN "backupErrorReason"`) - } -} diff --git a/apps/api/src/migrations/1755356869493-migration.ts b/apps/api/src/migrations/1755356869493-migration.ts deleted file mode 100644 index ce4ea9704..000000000 --- a/apps/api/src/migrations/1755356869493-migration.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1755356869493 implements MigrationInterface { - name = 'Migration1755356869493' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "webhook_initialization" ("organizationId" character varying NOT NULL, "svixApplicationId" character varying, "lastError" text, "retryCount" integer NOT NULL DEFAULT '0', "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "webhook_initialization_organizationId_pk" PRIMARY KEY ("organizationId"))`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "webhook_initialization"`) - } -} diff --git a/apps/api/src/migrations/1755464957487-migration.ts b/apps/api/src/migrations/1755464957487-migration.ts deleted file mode 100644 index ad4896e0b..000000000 --- a/apps/api/src/migrations/1755464957487-migration.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1755464957487 implements MigrationInterface { - name = 'Migration1755464957487' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "ssh_access" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "sandboxId" character varying NOT NULL, "token" text NOT NULL, "expiresAt" TIMESTAMP NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "ssh_access_id_pk" PRIMARY KEY ("id"))`, - ) - await queryRunner.query( - `ALTER TABLE "ssh_access" ADD CONSTRAINT "ssh_access_sandboxId_fk" FOREIGN KEY ("sandboxId") REFERENCES "sandbox"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ) - await queryRunner.query( - `ALTER TABLE "sandbox" ADD "sshPass" character varying(32) NOT NULL DEFAULT REPLACE(uuid_generate_v4()::text, '-', '')`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN "sshPass"`) - await queryRunner.query(`ALTER TABLE "ssh_access" DROP CONSTRAINT "ssh_access_sandboxId_fk"`) - await queryRunner.query(`DROP TABLE "ssh_access"`) - } -} diff --git a/apps/api/src/migrations/1755521645207-migration.ts b/apps/api/src/migrations/1755521645207-migration.ts deleted file mode 100644 index e18e15c50..000000000 --- a/apps/api/src/migrations/1755521645207-migration.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1755521645207 implements MigrationInterface { - name = 'Migration1755521645207' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "sandbox_usage_periods_archive" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "sandboxId" character varying NOT NULL, "organizationId" character varying NOT NULL, "startAt" TIMESTAMP WITH TIME ZONE NOT NULL, "endAt" TIMESTAMP WITH TIME ZONE NOT NULL, "cpu" double precision NOT NULL, "gpu" double precision NOT NULL, "mem" double precision NOT NULL, "disk" double precision NOT NULL, "region" character varying NOT NULL, CONSTRAINT "sandbox_usage_periods_archive_id_pk" PRIMARY KEY ("id"))`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "sandbox_usage_periods_archive"`) - } -} diff --git a/apps/api/src/migrations/1755860619921-migration.ts b/apps/api/src/migrations/1755860619921-migration.ts deleted file mode 100644 index 911663f42..000000000 --- a/apps/api/src/migrations/1755860619921-migration.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1755860619921 implements MigrationInterface { - name = 'Migration1755860619921' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "organization" ADD "sandboxLimitedNetworkEgress" boolean NOT NULL DEFAULT false`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "sandboxLimitedNetworkEgress"`) - } -} diff --git a/apps/api/src/migrations/1757513754037-migration.ts b/apps/api/src/migrations/1757513754037-migration.ts deleted file mode 100644 index f19dfa2b8..000000000 --- a/apps/api/src/migrations/1757513754037-migration.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1757513754037 implements MigrationInterface { - name = 'Migration1757513754037' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "docker_registry" ADD "region" character varying`) - - await queryRunner.query( - `ALTER TYPE "public"."docker_registry_registrytype_enum" RENAME TO "docker_registry_registrytype_enum_old"`, - ) - await queryRunner.query( - `CREATE TYPE "public"."docker_registry_registrytype_enum" AS ENUM('internal', 'organization', 'transient', 'backup')`, - ) - await queryRunner.query(`ALTER TABLE "docker_registry" ALTER COLUMN "registryType" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "docker_registry" ALTER COLUMN "registryType" TYPE "public"."docker_registry_registrytype_enum" USING "registryType"::"text"::"public"."docker_registry_registrytype_enum"`, - ) - await queryRunner.query(`ALTER TABLE "docker_registry" ALTER COLUMN "registryType" SET DEFAULT 'internal'`) - await queryRunner.query(`DROP TYPE "public"."docker_registry_registrytype_enum_old"`) - - // Create the base default registry by copying from the default internal one - await queryRunner.query(` - INSERT INTO public.docker_registry ( - name, url, username, password, "isDefault", project, "createdAt", "updatedAt", "registryType", "organizationId", "region" - ) - SELECT - 'Backup Registry' AS name, - url, - username, - password, - "isDefault", - project, - now() AS "createdAt", - now() AS "updatedAt", - 'backup' AS "registryType", - "organizationId", - "region" - FROM public.docker_registry - WHERE "registryType" = 'internal' - AND "isDefault" = true - ORDER BY "createdAt" ASC - LIMIT 1 - `) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DELETE FROM public.docker_registry - WHERE "registryType" = 'backup' - `) - - await queryRunner.query( - `CREATE TYPE "public"."docker_registry_registrytype_enum_old" AS ENUM('internal', 'organization', 'transient', 'backup')`, - ) - await queryRunner.query(`ALTER TABLE "docker_registry" ALTER COLUMN "registryType" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "docker_registry" ALTER COLUMN "registryType" TYPE "public"."docker_registry_registrytype_enum_old" USING "registryType"::"text"::"public"."docker_registry_registrytype_enum_old"`, - ) - await queryRunner.query(`ALTER TABLE "docker_registry" ALTER COLUMN "registryType" SET DEFAULT 'internal'`) - await queryRunner.query(`DROP TYPE "public"."docker_registry_registrytype_enum"`) - await queryRunner.query( - `ALTER TYPE "public"."docker_registry_registrytype_enum_old" RENAME TO "docker_registry_registrytype_enum"`, - ) - - await queryRunner.query(`ALTER TABLE "docker_registry" DROP COLUMN "region"`) - } -} diff --git a/apps/api/src/migrations/1757513754038-migration.ts b/apps/api/src/migrations/1757513754038-migration.ts deleted file mode 100644 index 6c0fc3ec8..000000000 --- a/apps/api/src/migrations/1757513754038-migration.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1757513754038 implements MigrationInterface { - name = 'Migration1757513754038' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "docker_registry" ADD "isFallback" boolean NOT NULL DEFAULT false`) - - // Update existing registries that have isDefault = true and region = null to be fallback registries - await queryRunner.query(` - UPDATE "docker_registry" - SET "isFallback" = true - WHERE "isDefault" = true AND "region" IS NULL AND "registryType" = 'backup' - `) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "docker_registry" DROP COLUMN "isFallback"`) - } -} diff --git a/apps/api/src/migrations/1759241690773-migration.ts b/apps/api/src/migrations/1759241690773-migration.ts deleted file mode 100644 index 92604deef..000000000 --- a/apps/api/src/migrations/1759241690773-migration.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1759241690773 implements MigrationInterface { - name = 'Migration1759241690773' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "used"`) - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "capacity"`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "runner" ADD "capacity" integer NOT NULL DEFAULT '1000'`) - await queryRunner.query(`ALTER TABLE "runner" ADD "used" integer NOT NULL DEFAULT '0'`) - } -} diff --git a/apps/api/src/migrations/1759768058397-migration.ts b/apps/api/src/migrations/1759768058397-migration.ts deleted file mode 100644 index 0bd9239c3..000000000 --- a/apps/api/src/migrations/1759768058397-migration.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1759768058397 implements MigrationInterface { - name = 'Migration1759768058397' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "sandbox" ADD "name" character varying`) - await queryRunner.query(`UPDATE "sandbox" SET "name" = "id"`) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "name" SET NOT NULL`) - await queryRunner.query( - `ALTER TABLE "sandbox" ALTER COLUMN "name" SET DEFAULT 'sandbox-' || substring(gen_random_uuid()::text, 1, 10)`, - ) - await queryRunner.query( - `ALTER TABLE "sandbox" ADD CONSTRAINT "sandbox_organizationId_name_unique" UNIQUE ("organizationId", "name")`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "sandbox" DROP CONSTRAINT "sandbox_organizationId_name_unique"`) - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN "name"`) - } -} diff --git a/apps/api/src/migrations/1761912147638-migration.ts b/apps/api/src/migrations/1761912147638-migration.ts deleted file mode 100644 index f83a07410..000000000 --- a/apps/api/src/migrations/1761912147638-migration.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' -import { configuration } from '../config/configuration' - -export class Migration1761912147638 implements MigrationInterface { - name = 'Migration1761912147638' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" ADD "defaultRegion" character varying NULL`) - await queryRunner.query(`UPDATE "organization" SET "defaultRegion" = '${configuration.defaultRegion.id}'`) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "defaultRegion" SET NOT NULL`) - - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "region" DROP DEFAULT`) - await queryRunner.query(`ALTER TABLE "warm_pool" ALTER COLUMN "target" DROP DEFAULT`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "defaultRegion"`) - - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "region" SET DEFAULT 'us'`) - await queryRunner.query(`ALTER TABLE "warm_pool" ALTER COLUMN "target" SET DEFAULT 'us'`) - } -} diff --git a/apps/api/src/migrations/1761912147639-migration.ts b/apps/api/src/migrations/1761912147639-migration.ts deleted file mode 100644 index fd24cfb1e..000000000 --- a/apps/api/src/migrations/1761912147639-migration.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1761912147639 implements MigrationInterface { - name = 'Migration1761912147639' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.renameColumn('snapshot', 'internalName', 'ref') - await queryRunner.renameColumn('snapshot', 'buildRunnerId', 'initialRunnerId') - await queryRunner.query(`ALTER TABLE "snapshot" ADD "skipValidation" boolean NOT NULL DEFAULT false`) - - // Update snapshot states - await queryRunner.query(`ALTER TYPE "public"."snapshot_state_enum" RENAME TO "snapshot_state_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."snapshot_state_enum" AS ENUM('pending', 'pulling', 'pending_validation', 'validating', 'active', 'inactive', 'building', 'error', 'build_failed', 'removing')`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "snapshot" ALTER COLUMN "state" TYPE "public"."snapshot_state_enum" USING "state"::"text"::"public"."snapshot_state_enum"`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" SET DEFAULT 'pending'`) - await queryRunner.query(`DROP TYPE "public"."snapshot_state_enum_old"`) - } - - public async down(queryRunner: QueryRunner): Promise { - // Revert snapshot states - await queryRunner.query(`ALTER TYPE "public"."snapshot_state_enum" RENAME TO "snapshot_state_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."snapshot_state_enum" AS ENUM('build_pending', 'pending', 'pulling', 'pending_validation', 'validating', 'active', 'inactive', 'building', 'error', 'build_failed', 'removing')`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "snapshot" ALTER COLUMN "state" TYPE "public"."snapshot_state_enum" USING "state"::"text"::"public"."snapshot_state_enum"`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" SET DEFAULT 'pending'`) - await queryRunner.query(`DROP TYPE "public"."snapshot_state_enum_old"`) - - await queryRunner.query(`ALTER TABLE "snapshot" DROP COLUMN "skipValidation"`) - await queryRunner.renameColumn('snapshot', 'initialRunnerId', 'buildRunnerId') - await queryRunner.renameColumn('snapshot', 'ref', 'internalName') - } -} diff --git a/apps/api/src/migrations/1763561822000-migration.ts b/apps/api/src/migrations/1763561822000-migration.ts deleted file mode 100644 index cb510f8ea..000000000 --- a/apps/api/src/migrations/1763561822000-migration.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1763561822000 implements MigrationInterface { - name = 'Migration1763561822000' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" ADD "authenticated_rate_limit" integer`) - await queryRunner.query(`ALTER TABLE "organization" ADD "sandbox_create_rate_limit" integer`) - await queryRunner.query(`ALTER TABLE "organization" ADD "sandbox_lifecycle_rate_limit" integer`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "sandbox_lifecycle_rate_limit"`) - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "sandbox_create_rate_limit"`) - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "authenticated_rate_limit"`) - } -} diff --git a/apps/api/src/migrations/1764073472179-migration.ts b/apps/api/src/migrations/1764073472179-migration.ts deleted file mode 100644 index b63338597..000000000 --- a/apps/api/src/migrations/1764073472179-migration.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' -import { configuration } from '../config/configuration' - -export class Migration1764073472179 implements MigrationInterface { - name = 'Migration1764073472179' - - public async up(queryRunner: QueryRunner): Promise { - // Create region table - await queryRunner.query( - `CREATE TABLE "region" ("id" character varying NOT NULL, "name" character varying NOT NULL, "organizationId" uuid, "hidden" boolean NOT NULL DEFAULT false, "enforceQuotas" boolean NOT NULL DEFAULT true, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "region_id_pk" PRIMARY KEY ("id"))`, - ) - - // Add unique constraints for region name - await queryRunner.query( - `CREATE UNIQUE INDEX "region_organizationId_null_name_unique" ON "region" ("name") WHERE "organizationId" IS NULL`, - ) - await queryRunner.query( - `CREATE UNIQUE INDEX "region_organizationId_name_unique" ON "region" ("organizationId", "name") WHERE "organizationId" IS NOT NULL`, - ) - - // Expand organization table with defaultRegionId column (make it nullable) - await queryRunner.query(`ALTER TABLE "organization" ADD "defaultRegionId" character varying NULL`) - await queryRunner.query(`UPDATE "organization" SET "defaultRegionId" = "defaultRegion"`) - - // Add default value for required defaultRegion column before dropping it in the contract migration - await queryRunner.query( - `ALTER TABLE "organization" ALTER COLUMN "defaultRegion" SET DEFAULT '${configuration.defaultRegion.id}'`, - ) - - // Create region_quota table - await queryRunner.query( - `CREATE TABLE "region_quota" ("organizationId" uuid NOT NULL, "regionId" character varying NOT NULL, "total_cpu_quota" integer NOT NULL DEFAULT '10', "total_memory_quota" integer NOT NULL DEFAULT '10', "total_disk_quota" integer NOT NULL DEFAULT '30', "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "region_quota_organizationId_regionId_pk" PRIMARY KEY ("organizationId", "regionId"))`, - ) - await queryRunner.query( - `ALTER TABLE "region_quota" ADD CONSTRAINT "region_quota_organizationId_fk" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ) - - // For existing organizations, migrate their region-specific quotas to their default region - await queryRunner.query(` - INSERT INTO "region_quota" ("organizationId", "regionId", "total_cpu_quota", "total_memory_quota", "total_disk_quota") - SELECT - o."id" as "organizationId", - o."defaultRegionId" as "regionId", - o."total_cpu_quota", - o."total_memory_quota", - o."total_disk_quota" - FROM "organization" o - `) - } - - public async down(queryRunner: QueryRunner): Promise { - // Drop region table - await queryRunner.query(`DROP TABLE "region"`) - - // Drop defaultRegionId column from organization table - await queryRunner.dropColumn('organization', 'defaultRegionId') - - // Drop region_quota table - await queryRunner.query(`DROP TABLE "region_quota"`) - } -} diff --git a/apps/api/src/migrations/1764073472180-migration.ts b/apps/api/src/migrations/1764073472180-migration.ts deleted file mode 100644 index 295c3c6d2..000000000 --- a/apps/api/src/migrations/1764073472180-migration.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' -import { configuration } from '../config/configuration' - -export class Migration1764073472180 implements MigrationInterface { - name = 'Migration1764073472180' - - public async up(queryRunner: QueryRunner): Promise { - // Remove defaultRegion column from organization table - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "defaultRegion"`) - - // Remove region-specific quotas from organization table - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "total_cpu_quota"`) - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "total_memory_quota"`) - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "total_disk_quota"`) - } - - public async down(queryRunner: QueryRunner): Promise { - // Restore defaultRegion column to organization table - await queryRunner.query(`ALTER TABLE "organization" ADD "defaultRegion" character varying NULL`) - await queryRunner.query(`UPDATE "organization" SET "defaultRegion" = "defaultRegionId"`) - await queryRunner.query( - `ALTER TABLE "organization" ALTER COLUMN "defaultRegion" SET DEFAULT '${configuration.defaultRegion.id}'`, - ) - await queryRunner.query(`ALTER TABLE "organization" ALTER COLUMN "defaultRegion" SET NOT NULL`) - - // Restore region-specific quotas to organization table - await queryRunner.query(`ALTER TABLE "organization" ADD "total_disk_quota" integer NOT NULL DEFAULT '30'`) - await queryRunner.query(`ALTER TABLE "organization" ADD "total_memory_quota" integer NOT NULL DEFAULT '10'`) - await queryRunner.query(`ALTER TABLE "organization" ADD "total_cpu_quota" integer NOT NULL DEFAULT '10'`) - - // For each organization, restore region-specific quotas by taking the maximum values among all region quotas - await queryRunner.query(` - UPDATE "organization" o - SET - "total_cpu_quota" = COALESCE(q."total_cpu_quota", 10), - "total_memory_quota" = COALESCE(q."total_memory_quota", 10), - "total_disk_quota" = COALESCE(q."total_disk_quota", 30) - FROM ( - SELECT - "organizationId", - MAX("total_cpu_quota") as "total_cpu_quota", - MAX("total_memory_quota") as "total_memory_quota", - MAX("total_disk_quota") as "total_disk_quota" - FROM "region_quota" - GROUP BY "organizationId" - ) q - WHERE o."id" = q."organizationId" - `) - } -} diff --git a/apps/api/src/migrations/1764844895057-migration.ts b/apps/api/src/migrations/1764844895057-migration.ts deleted file mode 100644 index 1b84ef044..000000000 --- a/apps/api/src/migrations/1764844895057-migration.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' -import { GlobalOrganizationRolesIds } from '../organization/constants/global-organization-roles.constant' -import { OrganizationResourcePermission } from '../organization/enums/organization-resource-permission.enum' - -export class Migration1764844895057 implements MigrationInterface { - name = 'Migration1764844895057' - - public async up(queryRunner: QueryRunner): Promise { - // add region type field with its type and constraints - await queryRunner.query(`CREATE TYPE "public"."region_regiontype_enum" AS ENUM('shared', 'dedicated', 'custom')`) - await queryRunner.query(`ALTER TABLE "region" ADD "regionType" "public"."region_regiontype_enum"`) - await queryRunner.query( - `ALTER TABLE "region" ADD CONSTRAINT "region_not_custom" CHECK ("organizationId" IS NOT NULL OR "regionType" != 'custom')`, - ) - await queryRunner.query( - `ALTER TABLE "region" ADD CONSTRAINT "region_not_shared" CHECK ("organizationId" IS NULL OR "regionType" != 'shared')`, - ) - await queryRunner.query(`UPDATE "region" SET "regionType" = 'custom' WHERE "organizationId" IS NOT NULL`) - await queryRunner.query(`UPDATE "region" SET "regionType" = 'shared' WHERE "organizationId" IS NULL`) - await queryRunner.query(`ALTER TABLE "region" ALTER COLUMN "regionType" SET NOT NULL`) - - // update api key permission enum - await queryRunner.query(`ALTER TYPE "public"."api_key_permissions_enum" RENAME TO "api_key_permissions_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."api_key_permissions_enum" AS ENUM('write:registries', 'delete:registries', 'write:snapshots', 'delete:snapshots', 'write:sandboxes', 'delete:sandboxes', 'read:volumes', 'write:volumes', 'delete:volumes', 'write:regions', 'delete:regions', 'read:runners', 'write:runners', 'delete:runners', 'read:audit_logs')`, - ) - await queryRunner.query( - `ALTER TABLE "api_key" ALTER COLUMN "permissions" TYPE "public"."api_key_permissions_enum"[] USING "permissions"::"text"::"public"."api_key_permissions_enum"[]`, - ) - await queryRunner.query(`DROP TYPE "public"."api_key_permissions_enum_old"`) - - // update organization role permission enum - await queryRunner.query( - `ALTER TYPE "public"."organization_role_permissions_enum" RENAME TO "organization_role_permissions_enum_old"`, - ) - await queryRunner.query( - `CREATE TYPE "public"."organization_role_permissions_enum" AS ENUM('write:registries', 'delete:registries', 'write:snapshots', 'delete:snapshots', 'write:sandboxes', 'delete:sandboxes', 'read:volumes', 'write:volumes', 'delete:volumes', 'write:regions', 'delete:regions', 'read:runners', 'write:runners', 'delete:runners', 'read:audit_logs')`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role" ALTER COLUMN "permissions" TYPE "public"."organization_role_permissions_enum"[] USING "permissions"::"text"::"public"."organization_role_permissions_enum"[]`, - ) - await queryRunner.query(`DROP TYPE "public"."organization_role_permissions_enum_old"`) - - // add infrastructure admin role - await queryRunner.query(` - INSERT INTO "organization_role" - ("id", "name", "description", "permissions", "isGlobal") - VALUES - ( - '${GlobalOrganizationRolesIds.INFRASTRUCTURE_ADMIN}', - 'Infrastructure Admin', - 'Grants admin access to infrastructure in the organization', - ARRAY[ - '${OrganizationResourcePermission.WRITE_REGIONS}', - '${OrganizationResourcePermission.DELETE_REGIONS}', - '${OrganizationResourcePermission.READ_RUNNERS}', - '${OrganizationResourcePermission.WRITE_RUNNERS}', - '${OrganizationResourcePermission.DELETE_RUNNERS}' - ]::organization_role_permissions_enum[], - TRUE - ) - `) - - // add runner name field - await queryRunner.query(`ALTER TABLE "runner" ADD "name" character varying`) - await queryRunner.query(`UPDATE "runner" SET "name" = "id"`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "name" SET NOT NULL`) - await queryRunner.query(`ALTER TABLE "runner" ADD CONSTRAINT "runner_region_name_unique" UNIQUE ("region", "name")`) - - // create new index for runner - await queryRunner.query( - `CREATE INDEX "runner_state_unschedulable_region_index" ON "runner" ("state", "unschedulable", "region") `, - ) - - // add region proxy and ssh gateway fields - await queryRunner.query(`ALTER TABLE "region" ADD "proxyUrl" character varying`) - await queryRunner.query(`ALTER TABLE "region" ADD "toolboxProxyUrl" character varying`) - await queryRunner.query(`ALTER TABLE "region" ADD "proxyApiKeyHash" character varying`) - await queryRunner.query(`ALTER TABLE "region" ADD "sshGatewayUrl" character varying`) - await queryRunner.query(`ALTER TABLE "region" ADD "sshGatewayApiKeyHash" character varying`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "proxyUrl" DROP DEFAULT`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "region" DROP DEFAULT`) - } - - public async down(queryRunner: QueryRunner): Promise { - // remove region proxy and ssh gateway fields - await queryRunner.query(`ALTER TABLE "region" DROP COLUMN "sshGatewayApiKeyHash"`) - await queryRunner.query(`ALTER TABLE "region" DROP COLUMN "sshGatewayUrl"`) - await queryRunner.query(`ALTER TABLE "region" DROP COLUMN "proxyApiKeyHash"`) - await queryRunner.query(`ALTER TABLE "region" DROP COLUMN "toolboxProxyUrl"`) - await queryRunner.query(`ALTER TABLE "region" DROP COLUMN "proxyUrl"`) - - // drop region type field - await queryRunner.query(`ALTER TABLE "region" DROP COLUMN "regionType"`) - await queryRunner.query(`DROP TYPE "public"."region_regiontype_enum"`) - - // remove infrastructure admin role - await queryRunner.query( - `DELETE FROM "organization_role" WHERE "id" = '${GlobalOrganizationRolesIds.INFRASTRUCTURE_ADMIN}'`, - ) - - // revert api key permission enum - await queryRunner.query( - `CREATE TYPE "public"."api_key_permissions_enum_old" AS ENUM('delete:registries', 'delete:sandboxes', 'delete:snapshots', 'delete:volumes', 'read:audit_logs', 'read:volumes', 'write:registries', 'write:sandboxes', 'write:snapshots', 'write:volumes')`, - ) - await queryRunner.query( - `ALTER TABLE "api_key" ALTER COLUMN "permissions" TYPE "public"."api_key_permissions_enum_old"[] USING "permissions"::"text"::"public"."api_key_permissions_enum_old"[]`, - ) - await queryRunner.query(`DROP TYPE "public"."api_key_permissions_enum"`) - await queryRunner.query(`ALTER TYPE "public"."api_key_permissions_enum_old" RENAME TO "api_key_permissions_enum"`) - - // revert organization role permission enum - await queryRunner.query( - `CREATE TYPE "public"."organization_role_permissions_enum_old" AS ENUM('delete:registries', 'delete:sandboxes', 'delete:snapshots', 'delete:volumes', 'read:audit_logs', 'read:volumes', 'write:registries', 'write:sandboxes', 'write:snapshots', 'write:volumes')`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role" ALTER COLUMN "permissions" TYPE "public"."organization_role_permissions_enum_old"[] USING "permissions"::"text"::"public"."organization_role_permissions_enum_old"[]`, - ) - await queryRunner.query(`DROP TYPE "public"."organization_role_permissions_enum"`) - await queryRunner.query( - `ALTER TYPE "public"."organization_role_permissions_enum_old" RENAME TO "organization_role_permissions_enum"`, - ) - - // drop runner name field - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "name"`) - - // drop new index for runner - await queryRunner.query(`DROP INDEX "public"."runner_state_unschedulable_region_index"`) - } -} diff --git a/apps/api/src/migrations/1764844895058-migration.ts b/apps/api/src/migrations/1764844895058-migration.ts deleted file mode 100644 index dc9f0f844..000000000 --- a/apps/api/src/migrations/1764844895058-migration.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1764844895058 implements MigrationInterface { - name = 'Migration1764844895058' - - public async up(queryRunner: QueryRunner): Promise { - // drop region hidden field - await queryRunner.query(`ALTER TABLE "region" DROP COLUMN "hidden"`) - } - - public async down(queryRunner: QueryRunner): Promise { - // revert drop region hidden field - await queryRunner.query(`ALTER TABLE "region" ADD "hidden" boolean NOT NULL DEFAULT false`) - } -} diff --git a/apps/api/src/migrations/1765282546000-migration.ts b/apps/api/src/migrations/1765282546000-migration.ts deleted file mode 100644 index 3ffd25ea9..000000000 --- a/apps/api/src/migrations/1765282546000-migration.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1765282546000 implements MigrationInterface { - name = 'Migration1765282546000' - - public async up(queryRunner: QueryRunner): Promise { - // Remove skipValidation column - await queryRunner.query(`ALTER TABLE "snapshot" DROP COLUMN "skipValidation"`) - - // Update any snapshots in VALIDATING or PENDING_VALIDATION state to PENDING - await queryRunner.query(`UPDATE "snapshot" SET "state" = 'pending' WHERE "state" = 'validating'`) - await queryRunner.query(`UPDATE "snapshot" SET "state" = 'pending' WHERE "state" = 'pending_validation'`) - - // Update snapshot_state_enum to remove VALIDATING and PENDING_VALIDATION - await queryRunner.query(`ALTER TYPE "public"."snapshot_state_enum" RENAME TO "snapshot_state_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."snapshot_state_enum" AS ENUM('pending', 'pulling', 'active', 'inactive', 'building', 'error', 'build_failed', 'removing')`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "snapshot" ALTER COLUMN "state" TYPE "public"."snapshot_state_enum" USING "state"::"text"::"public"."snapshot_state_enum"`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" SET DEFAULT 'pending'`) - await queryRunner.query(`DROP TYPE "public"."snapshot_state_enum_old"`) - } - - public async down(queryRunner: QueryRunner): Promise { - // Revert snapshot_state_enum to include VALIDATING and PENDING_VALIDATION - await queryRunner.query(`ALTER TYPE "public"."snapshot_state_enum" RENAME TO "snapshot_state_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."snapshot_state_enum" AS ENUM('pending', 'pulling', 'pending_validation', 'validating', 'active', 'inactive', 'building', 'error', 'build_failed', 'removing')`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "snapshot" ALTER COLUMN "state" TYPE "public"."snapshot_state_enum" USING "state"::"text"::"public"."snapshot_state_enum"`, - ) - await queryRunner.query(`ALTER TABLE "snapshot" ALTER COLUMN "state" SET DEFAULT 'pending'`) - await queryRunner.query(`DROP TYPE "public"."snapshot_state_enum_old"`) - - // Re-add skipValidation column - await queryRunner.query(`ALTER TABLE "snapshot" ADD "skipValidation" boolean NOT NULL DEFAULT false`) - } -} diff --git a/apps/api/src/migrations/1765366773736-migration.ts b/apps/api/src/migrations/1765366773736-migration.ts deleted file mode 100644 index f9f91a4c2..000000000 --- a/apps/api/src/migrations/1765366773736-migration.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1765366773736 implements MigrationInterface { - name = 'Migration1765366773736' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "sandbox" ADD "recoverable" boolean NOT NULL DEFAULT false`) - - // Update existing sandboxes with recoverable error reasons to set recoverable = true - await queryRunner.query(` - UPDATE "sandbox" - SET "recoverable" = true - WHERE "state" = 'error' - AND ( - LOWER("errorReason") LIKE '%no space left on device%' - OR LOWER("errorReason") LIKE '%storage limit%' - OR LOWER("errorReason") LIKE '%disk quota exceeded%' - ) - `) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN "recoverable"`) - } -} diff --git a/apps/api/src/migrations/1765400000000-migration.ts b/apps/api/src/migrations/1765400000000-migration.ts deleted file mode 100644 index 651dd53b6..000000000 --- a/apps/api/src/migrations/1765400000000-migration.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1765400000000 implements MigrationInterface { - name = 'Migration1765400000000' - - public async up(queryRunner: QueryRunner): Promise { - // Normalize Docker Hub URLs to 'docker.io' for consistency - // The runner will convert to 'index.docker.io/v1/' for builds where needed - await queryRunner.query(` - UPDATE "docker_registry" - SET "url" = 'docker.io' - WHERE LOWER("url") LIKE '%docker.io%' - `) - } - - public async down(queryRunner: QueryRunner): Promise { - // Cannot reliably reverse this migration as we don't know the original URLs - // This is a one-way normalization - } -} diff --git a/apps/api/src/migrations/1765806205881-migration.ts b/apps/api/src/migrations/1765806205881-migration.ts deleted file mode 100644 index f2f37aadb..000000000 --- a/apps/api/src/migrations/1765806205881-migration.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1765806205881 implements MigrationInterface { - name = 'Migration1765806205881' - - public async up(queryRunner: QueryRunner): Promise { - // Create snapshot_region table - await queryRunner.query(` - CREATE TABLE "snapshot_region" ( - "snapshotId" uuid NOT NULL, - "regionId" character varying NOT NULL, - "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - CONSTRAINT "PK_snapshot_region" PRIMARY KEY ("snapshotId", "regionId") - ) - `) - - // Add foreign key constraints - await queryRunner.query(` - ALTER TABLE "snapshot_region" - ADD CONSTRAINT "FK_snapshot_region_snapshot" - FOREIGN KEY ("snapshotId") REFERENCES "snapshot"("id") ON DELETE CASCADE ON UPDATE NO ACTION - `) - - await queryRunner.query(` - ALTER TABLE "snapshot_region" - ADD CONSTRAINT "FK_snapshot_region_region" - FOREIGN KEY ("regionId") REFERENCES "region"("id") ON DELETE CASCADE ON UPDATE NO ACTION - `) - - // Migrate existing snapshots: add snapshot_region entries based on organization's default region - await queryRunner.query(` - INSERT INTO "snapshot_region" ("snapshotId", "regionId") - SELECT s.id, o."defaultRegionId" - FROM "snapshot" s - INNER JOIN "organization" o ON s."organizationId" = o.id - WHERE o."defaultRegionId" IS NOT NULL - `) - } - - public async down(queryRunner: QueryRunner): Promise { - // Drop foreign key constraints - await queryRunner.query(`ALTER TABLE "snapshot_region" DROP CONSTRAINT "FK_snapshot_region_region"`) - await queryRunner.query(`ALTER TABLE "snapshot_region" DROP CONSTRAINT "FK_snapshot_region_snapshot"`) - - // Drop snapshot_region table - await queryRunner.query(`DROP TABLE "snapshot_region"`) - } -} diff --git a/apps/api/src/migrations/1766415256696-migration.ts b/apps/api/src/migrations/1766415256696-migration.ts deleted file mode 100644 index 2cb32a26b..000000000 --- a/apps/api/src/migrations/1766415256696-migration.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1766415256696 implements MigrationInterface { - name = 'Migration1766415256696' - - public async up(queryRunner: QueryRunner): Promise { - // region snapshot manager field - await queryRunner.query(`ALTER TABLE "region" ADD "snapshotManagerUrl" character varying`) - - // docker registry indexes - await queryRunner.query( - `CREATE INDEX "docker_registry_registryType_isDefault_index" ON "docker_registry" ("registryType", "isDefault") `, - ) - await queryRunner.query( - `CREATE INDEX "docker_registry_region_registryType_index" ON "docker_registry" ("region", "registryType") `, - ) - await queryRunner.query( - `CREATE INDEX "docker_registry_organizationId_registryType_index" ON "docker_registry" ("organizationId", "registryType") `, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - // drop region snapshot manager field - await queryRunner.query(`ALTER TABLE "region" DROP COLUMN "snapshotManagerUrl"`) - - // drop docker registry indexes - await queryRunner.query(`DROP INDEX "public"."docker_registry_organizationId_registryType_index"`) - await queryRunner.query(`DROP INDEX "public"."docker_registry_region_registryType_index"`) - await queryRunner.query(`DROP INDEX "public"."docker_registry_registryType_isDefault_index"`) - } -} diff --git a/apps/api/src/migrations/1767830400000-migration.ts b/apps/api/src/migrations/1767830400000-migration.ts deleted file mode 100644 index 77849c825..000000000 --- a/apps/api/src/migrations/1767830400000-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1767830400000 implements MigrationInterface { - name = 'Migration1767830400000' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "runner" ADD "currentStartedSandboxes" integer NOT NULL DEFAULT 0`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "currentStartedSandboxes"`) - } -} diff --git a/apps/api/src/migrations/1768306129179-migration.ts b/apps/api/src/migrations/1768306129179-migration.ts deleted file mode 100644 index 45d74bbd4..000000000 --- a/apps/api/src/migrations/1768306129179-migration.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1768306129179 implements MigrationInterface { - name = 'Migration1768306129179' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TYPE "public"."job_status_enum" AS ENUM('PENDING', 'IN_PROGRESS', 'COMPLETED', 'FAILED')`, - ) - await queryRunner.query(`CREATE TYPE "public"."job_resourcetype_enum" AS ENUM('SANDBOX', 'SNAPSHOT', 'BACKUP')`) - await queryRunner.query( - `CREATE TABLE "job" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "version" integer NOT NULL, "type" character varying NOT NULL, "status" "public"."job_status_enum" NOT NULL DEFAULT 'PENDING', "runnerId" character varying NOT NULL, "resourceType" "public"."job_resourcetype_enum" NOT NULL, "resourceId" character varying NOT NULL, "payload" character varying, "resultMetadata" character varying, "traceContext" jsonb, "errorMessage" text, "startedAt" TIMESTAMP WITH TIME ZONE, "completedAt" TIMESTAMP WITH TIME ZONE, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "job_id_pk" PRIMARY KEY ("id"))`, - ) - await queryRunner.query( - `CREATE UNIQUE INDEX "IDX_UNIQUE_INCOMPLETE_JOB" ON "job" ("resourceType", "resourceId", "runnerId") WHERE "completedAt" IS NULL`, - ) - await queryRunner.query(`CREATE INDEX "job_resourceType_resourceId_index" ON "job" ("resourceType", "resourceId") `) - await queryRunner.query(`CREATE INDEX "job_status_createdAt_index" ON "job" ("status", "createdAt") `) - await queryRunner.query(`CREATE INDEX "job_runnerId_status_index" ON "job" ("runnerId", "status") `) - await queryRunner.query(`ALTER TABLE "runner" RENAME COLUMN "version" TO "apiVersion"`) - await queryRunner.query(`ALTER TABLE "runner" ADD "appVersion" character varying DEFAULT 'v0.0.0-dev'`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "domain" DROP NOT NULL`) - await queryRunner.query(`ALTER TABLE "runner" DROP CONSTRAINT "UQ_330d74ac3d0e349b4c73c62ad6d"`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "apiUrl" DROP NOT NULL`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "proxyUrl" DROP NOT NULL`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "cpu" TYPE double precision`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "memoryGiB" TYPE double precision`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "diskGiB" TYPE double precision`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "gpu" DROP NOT NULL`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "gpuType" DROP NOT NULL`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "gpuType" SET NOT NULL`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "gpu" SET NOT NULL`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "diskGiB" TYPE integer`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "memoryGiB" TYPE integer`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "cpu" TYPE integer`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "proxyUrl" SET NOT NULL`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "apiUrl" SET NOT NULL`) - await queryRunner.query(`ALTER TABLE "runner" ADD CONSTRAINT "UQ_330d74ac3d0e349b4c73c62ad6d" UNIQUE ("domain")`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "domain" SET NOT NULL`) - await queryRunner.query(`ALTER TABLE "runner" RENAME COLUMN "apiVersion" TO "version"`) - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "appVersion"`) - await queryRunner.query(`DROP INDEX "public"."job_runnerId_status_index"`) - await queryRunner.query(`DROP INDEX "public"."job_status_createdAt_index"`) - await queryRunner.query(`DROP INDEX "public"."job_resourceType_resourceId_index"`) - await queryRunner.query(`DROP INDEX "public"."IDX_UNIQUE_INCOMPLETE_JOB"`) - await queryRunner.query(`DROP TABLE "job"`) - await queryRunner.query(`DROP TYPE "public"."job_resourcetype_enum"`) - await queryRunner.query(`DROP TYPE "public"."job_status_enum"`) - } -} diff --git a/apps/api/src/migrations/1768461678804-migration.ts b/apps/api/src/migrations/1768461678804-migration.ts deleted file mode 100644 index b1266ad50..000000000 --- a/apps/api/src/migrations/1768461678804-migration.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1768461678804 implements MigrationInterface { - name = 'Migration1768461678804' - - // TODO: Add migrationsTransactionMode: 'each', to data-source.ts - // TypeORM currently does not support non-transactional reverts - // Needed because CREATE/DROP INDEX CONCURRENTLY cannot run inside a transaction - // public readonly transaction = false - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE INDEX IF NOT EXISTS "idx_sandbox_volumes_gin" - ON "sandbox" - USING GIN ("volumes" jsonb_path_ops) - WHERE "desiredState" <> 'destroyed'; - `) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DROP INDEX IF EXISTS "idx_sandbox_volumes_gin"; - `) - } -} diff --git a/apps/api/src/migrations/1768475454675-migration.ts b/apps/api/src/migrations/1768475454675-migration.ts deleted file mode 100644 index fb06955f1..000000000 --- a/apps/api/src/migrations/1768475454675-migration.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1768475454675 implements MigrationInterface { - name = 'Migration1768475454675' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE INDEX "idx_region_custom" ON "region" ("organizationId") WHERE "regionType" = 'custom'`, - ) - await queryRunner.query( - `CREATE UNIQUE INDEX "region_sshGatewayApiKeyHash_unique" ON "region" ("sshGatewayApiKeyHash") WHERE "sshGatewayApiKeyHash" IS NOT NULL`, - ) - await queryRunner.query( - `CREATE UNIQUE INDEX "region_proxyApiKeyHash_unique" ON "region" ("proxyApiKeyHash") WHERE "proxyApiKeyHash" IS NOT NULL`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."region_proxyApiKeyHash_unique"`) - await queryRunner.query(`DROP INDEX "public"."region_sshGatewayApiKeyHash_unique"`) - await queryRunner.query(`DROP INDEX "public"."idx_region_custom"`) - } -} diff --git a/apps/api/src/migrations/1768485728153-migration.ts b/apps/api/src/migrations/1768485728153-migration.ts deleted file mode 100644 index b17226cae..000000000 --- a/apps/api/src/migrations/1768485728153-migration.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1768485728153 implements MigrationInterface { - name = 'Migration1768485728153' - - public async up(queryRunner: QueryRunner): Promise { - // Note: not using CONCURRENTLY + skipping transactions because of reverting issue: https://github.com/typeorm/typeorm/issues/9981 - - await queryRunner.query(`CREATE INDEX "api_key_org_user_idx" ON "api_key" ("organizationId", "userId") `) - await queryRunner.query( - `CREATE INDEX "warm_pool_find_idx" ON "warm_pool" ("snapshot", "target", "class", "cpu", "mem", "disk", "gpu", "osUser", "env") `, - ) - await queryRunner.query(`CREATE INDEX "snapshot_runner_state_idx" ON "snapshot_runner" ("state") `) - await queryRunner.query(`CREATE INDEX "snapshot_runner_runnerid_idx" ON "snapshot_runner" ("runnerId") `) - await queryRunner.query( - `CREATE INDEX "snapshot_runner_runnerid_snapshotref_idx" ON "snapshot_runner" ("runnerId", "snapshotRef") `, - ) - await queryRunner.query(`CREATE INDEX "snapshot_runner_snapshotref_idx" ON "snapshot_runner" ("snapshotRef") `) - await queryRunner.query(`CREATE INDEX "sandbox_pending_idx" ON "sandbox" ("id") WHERE "pending" = true`) - await queryRunner.query( - `CREATE INDEX "sandbox_active_only_idx" ON "sandbox" ("id") WHERE "state" <> ALL (ARRAY['destroyed'::sandbox_state_enum, 'archived'::sandbox_state_enum])`, - ) - await queryRunner.query( - `CREATE INDEX "sandbox_runner_state_desired_idx" ON "sandbox" ("runnerId", "state", "desiredState") WHERE "pending" = false`, - ) - await queryRunner.query(`CREATE INDEX "sandbox_backupstate_idx" ON "sandbox" ("backupState") `) - await queryRunner.query(`CREATE INDEX "sandbox_resources_idx" ON "sandbox" ("cpu", "mem", "disk", "gpu") `) - await queryRunner.query(`CREATE INDEX "sandbox_region_idx" ON "sandbox" ("region") `) - await queryRunner.query(`CREATE INDEX "sandbox_organizationid_idx" ON "sandbox" ("organizationId") `) - await queryRunner.query(`CREATE INDEX "sandbox_runner_state_idx" ON "sandbox" ("runnerId", "state") `) - await queryRunner.query(`CREATE INDEX "sandbox_runnerid_idx" ON "sandbox" ("runnerId") `) - await queryRunner.query(`CREATE INDEX "sandbox_snapshot_idx" ON "sandbox" ("snapshot") `) - await queryRunner.query(`CREATE INDEX "sandbox_desiredstate_idx" ON "sandbox" ("desiredState") `) - await queryRunner.query(`CREATE INDEX "sandbox_state_idx" ON "sandbox" ("state") `) - await queryRunner.query(`CREATE INDEX "snapshot_state_idx" ON "snapshot" ("state") `) - await queryRunner.query(`CREATE INDEX "snapshot_name_idx" ON "snapshot" ("name") `) - await queryRunner.query( - `CREATE INDEX "sandbox_labels_gin_full_idx" ON "sandbox" USING gin ("labels" jsonb_path_ops)`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."sandbox_labels_gin_full_idx"`) - await queryRunner.query(`DROP INDEX "public"."snapshot_name_idx"`) - await queryRunner.query(`DROP INDEX "public"."snapshot_state_idx"`) - await queryRunner.query(`DROP INDEX "public"."sandbox_state_idx"`) - await queryRunner.query(`DROP INDEX "public"."sandbox_desiredstate_idx"`) - await queryRunner.query(`DROP INDEX "public"."sandbox_snapshot_idx"`) - await queryRunner.query(`DROP INDEX "public"."sandbox_runnerid_idx"`) - await queryRunner.query(`DROP INDEX "public"."sandbox_runner_state_idx"`) - await queryRunner.query(`DROP INDEX "public"."sandbox_organizationid_idx"`) - await queryRunner.query(`DROP INDEX "public"."sandbox_region_idx"`) - await queryRunner.query(`DROP INDEX "public"."sandbox_resources_idx"`) - await queryRunner.query(`DROP INDEX "public"."sandbox_backupstate_idx"`) - await queryRunner.query(`DROP INDEX "public"."sandbox_runner_state_desired_idx"`) - await queryRunner.query(`DROP INDEX "public"."sandbox_active_only_idx"`) - await queryRunner.query(`DROP INDEX "public"."sandbox_pending_idx"`) - await queryRunner.query(`DROP INDEX "public"."snapshot_runner_snapshotref_idx"`) - await queryRunner.query(`DROP INDEX "public"."snapshot_runner_runnerid_snapshotref_idx"`) - await queryRunner.query(`DROP INDEX "public"."snapshot_runner_runnerid_idx"`) - await queryRunner.query(`DROP INDEX "public"."snapshot_runner_state_idx"`) - await queryRunner.query(`DROP INDEX "public"."warm_pool_find_idx"`) - await queryRunner.query(`DROP INDEX "public"."api_key_org_user_idx"`) - } -} diff --git a/apps/api/src/migrations/1768583941244-migration.ts b/apps/api/src/migrations/1768583941244-migration.ts deleted file mode 100644 index 37f7e7aac..000000000 --- a/apps/api/src/migrations/1768583941244-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1768583941244 implements MigrationInterface { - name = 'Migration1768583941244' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "runner" ADD "currentCpuLoadAverage" double precision NOT NULL DEFAULT '0'`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "currentCpuLoadAverage"`) - } -} diff --git a/apps/api/src/migrations/1769516172576-migration.ts b/apps/api/src/migrations/1769516172576-migration.ts deleted file mode 100644 index d6814c57c..000000000 --- a/apps/api/src/migrations/1769516172576-migration.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1769516172576 implements MigrationInterface { - name = 'Migration1769516172576' - - public async up(queryRunner: QueryRunner): Promise { - // For sandbox_state_enum - add 'resizing' value - await queryRunner.query(`ALTER TYPE "public"."sandbox_state_enum" ADD VALUE IF NOT EXISTS 'resizing'`) - } - - public async down(queryRunner: QueryRunner): Promise { - // Drop any index with explicit desiredState enum type cast in WHERE clause (required for enum swap) - - // For sandbox_state_enum - remove 'resizing' value - await queryRunner.query(`UPDATE "sandbox" SET "state" = 'stopped' WHERE "state" = 'resizing'`) - await queryRunner.query(`ALTER TYPE "public"."sandbox_state_enum" RENAME TO "sandbox_state_enum_old"`) - await queryRunner.query( - `CREATE TYPE "public"."sandbox_state_enum" AS ENUM('creating', 'restoring', 'destroyed', 'destroying', 'started', 'stopped', 'starting', 'stopping', 'error', 'build_failed', 'pending_build', 'building_snapshot', 'unknown', 'pulling_snapshot', 'archiving', 'archived')`, - ) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "state" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "sandbox" ALTER COLUMN "state" TYPE "public"."sandbox_state_enum" USING "state"::"text"::"public"."sandbox_state_enum"`, - ) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "state" SET DEFAULT 'unknown'`) - await queryRunner.query(`DROP TYPE "public"."sandbox_state_enum_old"`) - - // Recreate the indices that were dropped - } -} diff --git a/apps/api/src/migrations/1769516172577-migration.ts b/apps/api/src/migrations/1769516172577-migration.ts deleted file mode 100644 index a3502011e..000000000 --- a/apps/api/src/migrations/1769516172577-migration.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1769516172577 implements MigrationInterface { - name = 'Migration1769516172577' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "runner" ADD "draining" boolean NOT NULL DEFAULT false`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "draining"`) - } -} diff --git a/apps/api/src/migrations/1770043707083-migration.ts b/apps/api/src/migrations/1770043707083-migration.ts deleted file mode 100644 index c55327d6e..000000000 --- a/apps/api/src/migrations/1770043707083-migration.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1770043707083 implements MigrationInterface { - name = 'Migration1770043707083' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" ADD "experimentalConfig" jsonb`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "experimentalConfig"`) - } -} diff --git a/apps/api/src/migrations/1770212429837-migration.ts b/apps/api/src/migrations/1770212429837-migration.ts deleted file mode 100644 index 2af396339..000000000 --- a/apps/api/src/migrations/1770212429837-migration.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1770212429837 implements MigrationInterface { - name = 'Migration1770212429837' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" ADD "authenticated_rate_limit_ttl_seconds" integer`) - await queryRunner.query(`ALTER TABLE "organization" ADD "sandbox_create_rate_limit_ttl_seconds" integer`) - await queryRunner.query(`ALTER TABLE "organization" ADD "sandbox_lifecycle_rate_limit_ttl_seconds" integer`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "sandbox_lifecycle_rate_limit_ttl_seconds"`) - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "sandbox_create_rate_limit_ttl_seconds"`) - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "authenticated_rate_limit_ttl_seconds"`) - } -} diff --git a/apps/api/src/migrations/1770823569571-migration.ts b/apps/api/src/migrations/1770823569571-migration.ts deleted file mode 100644 index 57c69d2d6..000000000 --- a/apps/api/src/migrations/1770823569571-migration.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1770823569571 implements MigrationInterface { - name = 'Migration1770823569571' - - public async up(queryRunner: QueryRunner): Promise { - // Note: not using CONCURRENTLY + skipping transactions because of reverting issue: https://github.com/typeorm/typeorm/issues/9981 - await queryRunner.query(`CREATE INDEX "idx_sandbox_authtoken" ON "sandbox" ("authToken") `) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."idx_sandbox_authtoken"`) - } -} diff --git a/apps/api/src/migrations/1770880371265-migration.ts b/apps/api/src/migrations/1770880371265-migration.ts deleted file mode 100644 index 708710f09..000000000 --- a/apps/api/src/migrations/1770880371265-migration.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1770880371265 implements MigrationInterface { - name = 'Migration1770880371265' - - public async up(queryRunner: QueryRunner): Promise { - // Note: not using CONCURRENTLY + skipping transactions because of reverting issue: https://github.com/typeorm/typeorm/issues/9981 - await queryRunner.query( - `CREATE INDEX "idx_sandbox_usage_periods_sandbox_end" ON "sandbox_usage_periods" ("sandboxId", "endAt") `, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."idx_sandbox_usage_periods_sandbox_end"`) - } -} diff --git a/apps/api/src/migrations/README.md b/apps/api/src/migrations/README.md index a96f808ed..f9f8d9628 100644 --- a/apps/api/src/migrations/README.md +++ b/apps/api/src/migrations/README.md @@ -2,6 +2,12 @@ This project uses the **Expand and Contract** pattern for database migrations to support zero-downtime deployments. +## Pre-launch Baseline + +Because BoxLite has not launched yet, the historical migration chain has been squashed into +`pre-deploy/1782000000000-migration.ts`. Fresh databases should run this single baseline first. +Future schema changes should use the expand-and-contract workflow below. + ## Overview The expand and contract pattern splits database changes into two phases: @@ -16,7 +22,8 @@ This allows the database and API to be updated independently while maintaining c - `pre-deploy/` - Migrations that run **before** the API is deployed - `post-deploy/` - Migrations that run **after** the API is deployed -Note: Root folder migrations (not in pre-deploy or post-deploy) are legacy migrations created before the expand-and-contract pattern was introduced. These run during `migration:run:init` only. +Note: historical root folder migrations were squashed into the pre-launch baseline. Do not add new root +migrations; use `pre-deploy/` or `post-deploy/` for new changes. ## Developer Workflow diff --git a/apps/api/src/migrations/default-organization-membership.migration.spec.ts b/apps/api/src/migrations/default-organization-membership.migration.spec.ts deleted file mode 100644 index 9c65e50eb..000000000 --- a/apps/api/src/migrations/default-organization-membership.migration.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { DefaultOrganizationMembership1780912800000 } from './pre-deploy/1780912800000-migration' -import { existsSync } from 'fs' -import { join } from 'path' - -function createQueryRunner() { - return { - query: jest.fn().mockResolvedValue(undefined), - } -} - -describe('default organization membership migrations', () => { - it('adds and backfills per-user default membership state before deploy', async () => { - const queryRunner = createQueryRunner() - - await new DefaultOrganizationMembership1780912800000().up(queryRunner as never) - - const sql = queryRunner.query.mock.calls.map(([statement]) => statement).join('\n') - expect(sql).toContain('ADD "isDefaultForUser" boolean NOT NULL DEFAULT false') - expect(sql).toContain('UPDATE "organization_user" "ou"') - expect(sql).toContain('"org"."personal" = true') - expect(sql).toContain('organization_user_default_user_unique') - expect(sql).toContain('WHERE "isDefaultForUser" = true') - }) - - it('does not drop the deprecated organization-level personal flag in the compatibility rollout', () => { - expect(existsSync(join(__dirname, 'post-deploy/1780912800001-migration.ts'))).toBe(false) - }) -}) diff --git a/apps/api/src/migrations/post-deploy/1770880371266-migration.ts b/apps/api/src/migrations/post-deploy/1770880371266-migration.ts deleted file mode 100644 index f3c89095e..000000000 --- a/apps/api/src/migrations/post-deploy/1770880371266-migration.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1770880371266 implements MigrationInterface { - name = 'Migration1770880371266' - - public async up(queryRunner: QueryRunner): Promise { - // Note: not using CONCURRENTLY + skipping transactions because of reverting issue: https://github.com/typeorm/typeorm/issues/9981 - await queryRunner.query(`DROP INDEX "public"."IDX_UNIQUE_INCOMPLETE_JOB"`) - await queryRunner.query( - `CREATE UNIQUE INDEX "IDX_UNIQUE_INCOMPLETE_JOB" ON "job" ("resourceType", "resourceId", "runnerId") WHERE "completedAt" IS NULL AND "type" != 'CREATE_BACKUP'`, - ) - await queryRunner.query( - `CREATE UNIQUE INDEX "IDX_UNIQUE_INCOMPLETE_BACKUP_JOB" ON "job" ("resourceType", "resourceId", "runnerId") WHERE "completedAt" IS NULL AND "type" = 'CREATE_BACKUP'`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_UNIQUE_INCOMPLETE_BACKUP_JOB"`) - await queryRunner.query(`DROP INDEX "public"."IDX_UNIQUE_INCOMPLETE_JOB"`) - await queryRunner.query( - `CREATE UNIQUE INDEX "IDX_UNIQUE_INCOMPLETE_JOB" ON "job" ("resourceType", "resourceId", "runnerId") WHERE "completedAt" IS NULL`, - ) - } -} diff --git a/apps/api/src/migrations/post-deploy/1774438866002-migration.ts b/apps/api/src/migrations/post-deploy/1774438866002-migration.ts deleted file mode 100644 index b8e72d70d..000000000 --- a/apps/api/src/migrations/post-deploy/1774438866002-migration.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1774438866002 implements MigrationInterface { - name = 'Migration1774438866002' - - public async up(queryRunner: QueryRunner): Promise { - /** - * Drop DB-level default for sandbox name. Now set exclusively in the entity constructor. - */ - await queryRunner.query(`ALTER TABLE "sandbox" DROP CONSTRAINT "sandbox_organizationId_name_unique"`) - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "name" DROP DEFAULT`) - await queryRunner.query( - `ALTER TABLE "sandbox" ADD CONSTRAINT "sandbox_organizationId_name_unique" UNIQUE ("organizationId", "name")`, - ) - - /** - * Drop DB-level default for sandbox authToken. Now set exclusively via class field initializer. - */ - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "authToken" DROP DEFAULT`) - - /** - * The initial migration incorrectly added the column, it was never actually added to the entity definition. - */ - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN "sshPass"`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "sandbox" ADD "sshPass" character varying(32) NOT NULL DEFAULT REPLACE(uuid_generate_v4()::text, '-', '')`, - ) - - await queryRunner.query(`ALTER TABLE "sandbox" DROP CONSTRAINT "sandbox_organizationId_name_unique"`) - await queryRunner.query( - `ALTER TABLE "sandbox" ALTER COLUMN "name" SET DEFAULT 'sandbox-' || substring(gen_random_uuid()::text, 1, 10)`, - ) - await queryRunner.query( - `ALTER TABLE "sandbox" ADD CONSTRAINT "sandbox_organizationId_name_unique" UNIQUE ("organizationId", "name")`, - ) - - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "authToken" SET DEFAULT MD5(random()::text)`) - } -} diff --git a/apps/api/src/migrations/post-deploy/1774454800508-migration.ts b/apps/api/src/migrations/post-deploy/1774454800508-migration.ts deleted file mode 100644 index 3d71fe11e..000000000 --- a/apps/api/src/migrations/post-deploy/1774454800508-migration.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1774454800508 implements MigrationInterface { - name = 'Migration1774454800508' - - public async up(queryRunner: QueryRunner): Promise { - // Drop dual-write triggers and function (no longer needed after deployment) - await queryRunner.query(`DROP TRIGGER IF EXISTS sandbox_activity_sync_to_old ON "sandbox_last_activity"`) - await queryRunner.query(`DROP TRIGGER IF EXISTS sandbox_activity_sync_to_new ON "sandbox"`) - await queryRunner.query(`DROP FUNCTION IF EXISTS sync_sandbox_last_activity()`) - - // Drop the old column - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN "lastActivityAt"`) - } - - public async down(queryRunner: QueryRunner): Promise { - // Restore the old column - await queryRunner.query(`ALTER TABLE "sandbox" ADD "lastActivityAt" TIMESTAMP WITH TIME ZONE`) - - // Backfill data from sandbox_last_activity to restored column - await queryRunner.query(` - UPDATE "sandbox" s - SET "lastActivityAt" = sla."lastActivityAt" - FROM "sandbox_last_activity" sla - WHERE s.id = sla."sandboxId" - `) - - // Recreate sync function for dual-write - await queryRunner.query(` - CREATE OR REPLACE FUNCTION sync_sandbox_last_activity() - RETURNS TRIGGER AS $$ - BEGIN - IF TG_TABLE_NAME = 'sandbox' THEN - INSERT INTO sandbox_last_activity ("sandboxId", "lastActivityAt") - VALUES (NEW.id, NEW."lastActivityAt") - ON CONFLICT ("sandboxId") DO UPDATE SET "lastActivityAt" = EXCLUDED."lastActivityAt" - WHERE sandbox_last_activity."lastActivityAt" IS DISTINCT FROM EXCLUDED."lastActivityAt"; - ELSIF TG_TABLE_NAME = 'sandbox_last_activity' THEN - UPDATE sandbox SET "lastActivityAt" = NEW."lastActivityAt" - WHERE id = NEW."sandboxId" AND "lastActivityAt" IS DISTINCT FROM NEW."lastActivityAt"; - END IF; - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - `) - - // Recreate trigger on sandbox table (old -> new) - await queryRunner.query(` - CREATE TRIGGER sandbox_activity_sync_to_new - AFTER INSERT OR UPDATE OF "lastActivityAt" ON "sandbox" - FOR EACH ROW EXECUTE FUNCTION sync_sandbox_last_activity(); - `) - - // Recreate trigger on sandbox_last_activity table (new -> old) - await queryRunner.query(` - CREATE TRIGGER sandbox_activity_sync_to_old - AFTER INSERT OR UPDATE OF "lastActivityAt" ON "sandbox_last_activity" - FOR EACH ROW EXECUTE FUNCTION sync_sandbox_last_activity(); - `) - - // Re-sync any rows that changed during the window between backfill and trigger creation - await queryRunner.query(` - UPDATE "sandbox" s - SET "lastActivityAt" = sla."lastActivityAt" - FROM "sandbox_last_activity" sla - WHERE s.id = sla."sandboxId" - AND s."lastActivityAt" IS DISTINCT FROM sla."lastActivityAt" - `) - } -} diff --git a/apps/api/src/migrations/post-deploy/1780200000000-migration.ts b/apps/api/src/migrations/post-deploy/1780200000000-migration.ts deleted file mode 100644 index 4bc23b8a8..000000000 --- a/apps/api/src/migrations/post-deploy/1780200000000-migration.ts +++ /dev/null @@ -1,379 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1780200000000 implements MigrationInterface { - name = 'Migration1780200000000' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS "public"."snapshot_name_idx"`) - await queryRunner.query(`DROP INDEX IF EXISTS "public"."snapshot_state_idx"`) - await queryRunner.query(`DROP INDEX IF EXISTS "public"."snapshot_runner_snapshotref_idx"`) - await queryRunner.query(`DROP INDEX IF EXISTS "public"."snapshot_runner_runnerid_snapshotref_idx"`) - await queryRunner.query(`DROP INDEX IF EXISTS "public"."snapshot_runner_runnerid_idx"`) - await queryRunner.query(`DROP INDEX IF EXISTS "public"."snapshot_runner_state_idx"`) - await queryRunner.query(`DROP INDEX IF EXISTS "public"."sandbox_snapshot_idx"`) - await queryRunner.query(`DROP INDEX IF EXISTS "public"."warm_pool_find_idx"`) - - await this.renameColumnIfExists(queryRunner, 'snapshot', 'ref', 'artifactRef') - await this.renameColumnIfExists(queryRunner, 'snapshot', 'buildInfoSnapshotRef', 'buildInfoArtifactRef') - await this.renameTableIfExists(queryRunner, 'snapshot', 'box_template') - await this.renameTableIfExists(queryRunner, 'snapshot_region', 'box_template_region') - await this.renameColumnIfExists(queryRunner, 'box_template_region', 'snapshotId', 'templateId') - await this.renameEnumTypeForColumn(queryRunner, 'box_template', 'state', 'box_template_state_enum') - - await this.renameColumnIfExists(queryRunner, 'build_info', 'snapshotRef', 'artifactRef') - await this.renameColumnIfExists(queryRunner, 'sandbox', 'buildInfoSnapshotRef', 'buildInfoArtifactRef') - await this.renameColumnIfExists(queryRunner, 'sandbox', 'snapshot', 'template') - await this.renameColumnIfExists(queryRunner, 'warm_pool', 'snapshot', 'template') - await this.renameColumnIfExists(queryRunner, 'organization', 'max_snapshot_size', 'max_template_size') - await this.renameColumnIfExists(queryRunner, 'organization', 'snapshot_quota', 'template_quota') - await this.renameColumnIfExists( - queryRunner, - 'organization', - 'snapshot_deactivation_timeout_minutes', - 'template_deactivation_timeout_minutes', - ) - await this.renameColumnIfExists(queryRunner, 'region', 'snapshotManagerUrl', 'artifactRegistryUrl') - await this.renameColumnIfExists(queryRunner, 'runner', 'currentSnapshotCount', 'currentArtifactCount') - - await this.renameTableIfExists(queryRunner, 'snapshot_runner', 'runner_artifact_cache') - await this.renameColumnIfExists(queryRunner, 'runner_artifact_cache', 'snapshotRef', 'artifactRef') - await this.renameEnumTypeForColumn( - queryRunner, - 'runner_artifact_cache', - 'state', - 'runner_artifact_cache_state_enum', - ) - await this.renameEnumValueIfExists( - queryRunner, - 'runner_artifact_cache_state_enum', - 'pulling_snapshot', - 'pulling_artifact', - ) - await this.renameEnumValueIfExists( - queryRunner, - 'runner_artifact_cache_state_enum', - 'building_snapshot', - 'building_artifact', - ) - await queryRunner.query(`ALTER TABLE "runner_artifact_cache" ALTER COLUMN "state" SET DEFAULT 'pulling_artifact'`) - await this.renameEnumValueIfExists(queryRunner, 'sandbox_state_enum', 'pulling_snapshot', 'pulling_artifact') - await this.renameEnumValueIfExists(queryRunner, 'sandbox_state_enum', 'building_snapshot', 'building_artifact') - await this.renameEnumValueIfExists(queryRunner, 'job_resourcetype_enum', 'SNAPSHOT', 'ARTIFACT') - await this.renameEnumValueIfExists( - queryRunner, - 'organization_role_permissions_enum', - 'write:snapshots', - 'write:templates', - ) - await this.renameEnumValueIfExists( - queryRunner, - 'organization_role_permissions_enum', - 'delete:snapshots', - 'delete:templates', - ) - await this.renameEnumValueIfExists(queryRunner, 'api_key_permissions_enum', 'write:snapshots', 'write:templates') - await this.renameEnumValueIfExists(queryRunner, 'api_key_permissions_enum', 'delete:snapshots', 'delete:templates') - await queryRunner.query(` - UPDATE "organization_role" - SET "name" = 'Templates Admin', "description" = 'Grants admin access to templates in the organization' - WHERE "name" = 'Snapshots Admin' - `) - await queryRunner.query(` - UPDATE "job" - SET "type" = CASE "type" - WHEN 'BUILD_SNAPSHOT' THEN 'BUILD_ARTIFACT' - WHEN 'PULL_SNAPSHOT' THEN 'PULL_ARTIFACT' - WHEN 'REMOVE_SNAPSHOT' THEN 'REMOVE_ARTIFACT' - WHEN 'INSPECT_SNAPSHOT_IN_REGISTRY' THEN 'INSPECT_ARTIFACT_IN_REGISTRY' - ELSE "type" - END - WHERE "type" IN ('BUILD_SNAPSHOT', 'PULL_SNAPSHOT', 'REMOVE_SNAPSHOT', 'INSPECT_SNAPSHOT_IN_REGISTRY') - `) - - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "runner_artifact_cache_state_idx" ON "runner_artifact_cache" ("state")`, - ) - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "runner_artifact_cache_runnerid_idx" ON "runner_artifact_cache" ("runnerId")`, - ) - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "runner_artifact_cache_runnerid_artifactref_idx" ON "runner_artifact_cache" ("runnerId", "artifactRef")`, - ) - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "runner_artifact_cache_artifactref_idx" ON "runner_artifact_cache" ("artifactRef")`, - ) - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "box_template_name_idx" ON "box_template" ("name")`) - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "box_template_state_idx" ON "box_template" ("state")`) - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "sandbox_template_idx" ON "sandbox" ("template")`) - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "warm_pool_find_idx" ON "warm_pool" ("template", "target", "class", "cpu", "mem", "disk", "gpu", "osUser", "env")`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS "public"."warm_pool_find_idx"`) - await queryRunner.query(`DROP INDEX IF EXISTS "public"."sandbox_template_idx"`) - await queryRunner.query(`DROP INDEX IF EXISTS "public"."box_template_state_idx"`) - await queryRunner.query(`DROP INDEX IF EXISTS "public"."box_template_name_idx"`) - await queryRunner.query(`DROP INDEX IF EXISTS "public"."runner_artifact_cache_artifactref_idx"`) - await queryRunner.query(`DROP INDEX IF EXISTS "public"."runner_artifact_cache_runnerid_artifactref_idx"`) - await queryRunner.query(`DROP INDEX IF EXISTS "public"."runner_artifact_cache_runnerid_idx"`) - await queryRunner.query(`DROP INDEX IF EXISTS "public"."runner_artifact_cache_state_idx"`) - - await queryRunner.query(` - UPDATE "job" - SET "type" = CASE "type" - WHEN 'BUILD_ARTIFACT' THEN 'BUILD_SNAPSHOT' - WHEN 'PULL_ARTIFACT' THEN 'PULL_SNAPSHOT' - WHEN 'REMOVE_ARTIFACT' THEN 'REMOVE_SNAPSHOT' - WHEN 'INSPECT_ARTIFACT_IN_REGISTRY' THEN 'INSPECT_SNAPSHOT_IN_REGISTRY' - ELSE "type" - END - WHERE "type" IN ('BUILD_ARTIFACT', 'PULL_ARTIFACT', 'REMOVE_ARTIFACT', 'INSPECT_ARTIFACT_IN_REGISTRY') - `) - await this.renameEnumValueIfExists(queryRunner, 'job_resourcetype_enum', 'ARTIFACT', 'SNAPSHOT') - await queryRunner.query(` - UPDATE "organization_role" - SET "name" = 'Snapshots Admin', "description" = 'Grants admin access to snapshots in the organization' - WHERE "name" = 'Templates Admin' - `) - await this.renameEnumValueIfExists(queryRunner, 'api_key_permissions_enum', 'delete:templates', 'delete:snapshots') - await this.renameEnumValueIfExists(queryRunner, 'api_key_permissions_enum', 'write:templates', 'write:snapshots') - await this.renameEnumValueIfExists( - queryRunner, - 'organization_role_permissions_enum', - 'delete:templates', - 'delete:snapshots', - ) - await this.renameEnumValueIfExists( - queryRunner, - 'organization_role_permissions_enum', - 'write:templates', - 'write:snapshots', - ) - await this.renameEnumValueIfExists(queryRunner, 'sandbox_state_enum', 'pulling_artifact', 'pulling_snapshot') - await this.renameEnumValueIfExists(queryRunner, 'sandbox_state_enum', 'building_artifact', 'building_snapshot') - await this.renameEnumValueIfExists( - queryRunner, - 'runner_artifact_cache_state_enum', - 'pulling_artifact', - 'pulling_snapshot', - ) - await this.renameEnumValueIfExists( - queryRunner, - 'runner_artifact_cache_state_enum', - 'building_artifact', - 'building_snapshot', - ) - await queryRunner.query(`ALTER TABLE "runner_artifact_cache" ALTER COLUMN "state" SET DEFAULT 'pulling_snapshot'`) - await this.renameEnumTypeForColumn(queryRunner, 'runner_artifact_cache', 'state', 'snapshot_runner_state_enum') - await this.renameColumnIfExists(queryRunner, 'runner_artifact_cache', 'artifactRef', 'snapshotRef') - await this.renameTableIfExists(queryRunner, 'runner_artifact_cache', 'snapshot_runner') - - await this.renameColumnIfExists(queryRunner, 'build_info', 'artifactRef', 'snapshotRef') - await this.renameColumnIfExists(queryRunner, 'sandbox', 'buildInfoArtifactRef', 'buildInfoSnapshotRef') - await this.renameColumnIfExists(queryRunner, 'sandbox', 'template', 'snapshot') - await this.renameColumnIfExists(queryRunner, 'warm_pool', 'template', 'snapshot') - await this.renameColumnIfExists(queryRunner, 'organization', 'max_template_size', 'max_snapshot_size') - await this.renameColumnIfExists(queryRunner, 'organization', 'template_quota', 'snapshot_quota') - await this.renameColumnIfExists( - queryRunner, - 'organization', - 'template_deactivation_timeout_minutes', - 'snapshot_deactivation_timeout_minutes', - ) - await this.renameColumnIfExists(queryRunner, 'region', 'artifactRegistryUrl', 'snapshotManagerUrl') - await this.renameColumnIfExists(queryRunner, 'runner', 'currentArtifactCount', 'currentSnapshotCount') - - await this.renameEnumTypeForColumn(queryRunner, 'box_template', 'state', 'snapshot_state_enum') - await this.renameColumnIfExists(queryRunner, 'box_template_region', 'templateId', 'snapshotId') - await this.renameTableIfExists(queryRunner, 'box_template_region', 'snapshot_region') - await this.renameColumnIfExists(queryRunner, 'box_template', 'buildInfoArtifactRef', 'buildInfoSnapshotRef') - await this.renameColumnIfExists(queryRunner, 'box_template', 'artifactRef', 'ref') - await this.renameTableIfExists(queryRunner, 'box_template', 'snapshot') - - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "snapshot_runner_state_idx" ON "snapshot_runner" ("state")`) - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "snapshot_runner_runnerid_idx" ON "snapshot_runner" ("runnerId")`, - ) - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "snapshot_runner_runnerid_snapshotref_idx" ON "snapshot_runner" ("runnerId", "snapshotRef")`, - ) - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "snapshot_runner_snapshotref_idx" ON "snapshot_runner" ("snapshotRef")`, - ) - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "snapshot_state_idx" ON "snapshot" ("state")`) - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "snapshot_name_idx" ON "snapshot" ("name")`) - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "sandbox_snapshot_idx" ON "sandbox" ("snapshot")`) - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "warm_pool_find_idx" ON "warm_pool" ("snapshot", "target", "class", "cpu", "mem", "disk", "gpu", "osUser", "env")`, - ) - } - - private async renameTableIfExists(queryRunner: QueryRunner, from: string, to: string): Promise { - if ((await queryRunner.hasTable(from)) && !(await queryRunner.hasTable(to))) { - await queryRunner.renameTable(from, to) - } - } - - private async renameColumnIfExists( - queryRunner: QueryRunner, - tableName: string, - from: string, - to: string, - ): Promise { - if (!(await this.tableExists(queryRunner, tableName))) { - return - } - - if ( - (await this.columnExists(queryRunner, tableName, from)) && - !(await this.columnExists(queryRunner, tableName, to)) - ) { - await queryRunner.query( - `ALTER TABLE ${this.quoteIdentifier(tableName)} RENAME COLUMN ${this.quoteIdentifier(from)} TO ${this.quoteIdentifier(to)}`, - ) - } - } - - private async tableExists(queryRunner: QueryRunner, tableName: string): Promise { - const result = await queryRunner.query( - ` - SELECT EXISTS ( - SELECT 1 - FROM information_schema.tables - WHERE table_schema = 'public' - AND table_name = $1 - ) AS "exists" - `, - [tableName], - ) - - return result[0]?.exists === true - } - - private async columnExists(queryRunner: QueryRunner, tableName: string, columnName: string): Promise { - const result = await queryRunner.query( - ` - SELECT EXISTS ( - SELECT 1 - FROM information_schema.columns - WHERE table_schema = 'public' - AND table_name = $1 - AND column_name = $2 - ) AS "exists" - `, - [tableName, columnName], - ) - - return result[0]?.exists === true - } - - private async renameEnumTypeForColumn( - queryRunner: QueryRunner, - tableName: string, - columnName: string, - targetTypeName: string, - ): Promise { - const currentTypeName = await this.getEnumTypeForColumn(queryRunner, tableName, columnName) - if (!currentTypeName || currentTypeName === targetTypeName) { - return - } - - if (await this.enumTypeExists(queryRunner, targetTypeName)) { - return - } - - await queryRunner.query( - `ALTER TYPE "public".${this.quoteIdentifier(currentTypeName)} RENAME TO ${this.quoteIdentifier(targetTypeName)}`, - ) - } - - private async getEnumTypeForColumn( - queryRunner: QueryRunner, - tableName: string, - columnName: string, - ): Promise { - const result = await queryRunner.query( - ` - SELECT t.typname - FROM pg_type t - JOIN pg_attribute a ON a.atttypid = t.oid - JOIN pg_class c ON c.oid = a.attrelid - JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE n.nspname = 'public' - AND c.relname = $1 - AND a.attname = $2 - AND t.typtype = 'e' - AND NOT a.attisdropped - LIMIT 1 - `, - [tableName, columnName], - ) - - return result[0]?.typname - } - - private async enumTypeExists(queryRunner: QueryRunner, typeName: string): Promise { - const result = await queryRunner.query( - ` - SELECT EXISTS ( - SELECT 1 - FROM pg_type t - JOIN pg_namespace n ON n.oid = t.typnamespace - WHERE n.nspname = 'public' - AND t.typname = $1 - ) AS "exists" - `, - [typeName], - ) - - return result[0]?.exists === true - } - - private async renameEnumValueIfExists( - queryRunner: QueryRunner, - typeName: string, - from: string, - to: string, - ): Promise { - const labels = await this.getEnumLabels(queryRunner, typeName) - if (!labels.includes(from) || labels.includes(to)) { - return - } - - await queryRunner.query( - `ALTER TYPE "public".${this.quoteIdentifier(typeName)} RENAME VALUE '${this.escapeLiteral(from)}' TO '${this.escapeLiteral(to)}'`, - ) - } - - private async getEnumLabels(queryRunner: QueryRunner, typeName: string): Promise { - const result = await queryRunner.query( - ` - SELECT e.enumlabel - FROM pg_enum e - JOIN pg_type t ON t.oid = e.enumtypid - JOIN pg_namespace n ON n.oid = t.typnamespace - WHERE n.nspname = 'public' - AND t.typname = $1 - `, - [typeName], - ) - - return result.map((row: { enumlabel: string }) => row.enumlabel) - } - - private quoteIdentifier(identifier: string): string { - return `"${identifier.replace(/"/g, '""')}"` - } - - private escapeLiteral(value: string): string { - return value.replace(/'/g, "''") - } -} diff --git a/apps/api/src/migrations/post-deploy/1780531200000-migration.ts b/apps/api/src/migrations/post-deploy/1780531200000-migration.ts deleted file mode 100644 index f9a671b9e..000000000 --- a/apps/api/src/migrations/post-deploy/1780531200000-migration.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1780531200000 implements MigrationInterface { - name = 'Migration1780531200000' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN IF EXISTS "autoArchiveInterval"`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "sandbox" ADD COLUMN IF NOT EXISTS "autoArchiveInterval" integer NOT NULL DEFAULT '10080'`, - ) - } -} diff --git a/apps/api/src/migrations/post-deploy/1780999000000-migration.ts b/apps/api/src/migrations/post-deploy/1780999000000-migration.ts deleted file mode 100644 index cd8aed08d..000000000 --- a/apps/api/src/migrations/post-deploy/1780999000000-migration.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -/** - * Removes the user-Dockerfile build capability from the schema. - * - * The MVP only pulls prebuilt images, so the runner-side build path and its - * persistence are dead weight. Dropping the FK columns first lets Postgres - * remove the dependent foreign-key constraints automatically before the - * referenced `build_info` table is dropped. - */ -export class Migration1780999000000 implements MigrationInterface { - name = 'Migration1780999000000' - - public async up(queryRunner: QueryRunner): Promise { - // Dropping a column also drops any FK constraint that references it. - await queryRunner.query(`ALTER TABLE "box_template" DROP COLUMN IF EXISTS "buildInfoArtifactRef"`) - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN IF EXISTS "buildInfoArtifactRef"`) - await queryRunner.query(`DROP TABLE IF EXISTS "build_info"`) - } - - public async down(queryRunner: QueryRunner): Promise { - // Best-effort restore: recreate the table and the nullable FK columns. - await queryRunner.query( - `CREATE TABLE IF NOT EXISTS "build_info" (` + - `"artifactRef" character varying NOT NULL, ` + - `"dockerfileContent" text, ` + - `"contextHashes" text, ` + - `"lastUsedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), ` + - `"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), ` + - `"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), ` + - `CONSTRAINT "build_info_artifactRef_pk" PRIMARY KEY ("artifactRef"))`, - ) - await queryRunner.query( - `ALTER TABLE "box_template" ADD COLUMN IF NOT EXISTS "buildInfoArtifactRef" character varying`, - ) - await queryRunner.query(`ALTER TABLE "sandbox" ADD COLUMN IF NOT EXISTS "buildInfoArtifactRef" character varying`) - await queryRunner.query( - `ALTER TABLE "box_template" ADD CONSTRAINT "box_template_buildInfoArtifactRef_fk" ` + - `FOREIGN KEY ("buildInfoArtifactRef") REFERENCES "build_info"("artifactRef") ` + - `ON DELETE NO ACTION ON UPDATE NO ACTION`, - ) - await queryRunner.query( - `ALTER TABLE "sandbox" ADD CONSTRAINT "sandbox_buildInfoArtifactRef_fk" ` + - `FOREIGN KEY ("buildInfoArtifactRef") REFERENCES "build_info"("artifactRef") ` + - `ON DELETE NO ACTION ON UPDATE NO ACTION`, - ) - } -} diff --git a/apps/api/src/migrations/post-deploy/1781000000000-migration.ts b/apps/api/src/migrations/post-deploy/1781000000000-migration.ts deleted file mode 100644 index f2b2468d1..000000000 --- a/apps/api/src/migrations/post-deploy/1781000000000-migration.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -/** - * Removes the sandbox backup / restore / cross-runner-migration capability from - * the schema. - * - * The runner-side backup path was an unimplemented stub, so `backupState` never - * advanced past its default and every backup-gated code path was dead. Dropping - * the index first, then the columns, then the enum type leaves the schema with - * no dangling references. - */ -export class Migration1781000000000 implements MigrationInterface { - name = 'Migration1781000000000' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX IF EXISTS "sandbox_backupstate_idx"`) - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN IF EXISTS "backupState"`) - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN IF EXISTS "backupSnapshot"`) - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN IF EXISTS "backupRegistryId"`) - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN IF EXISTS "lastBackupAt"`) - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN IF EXISTS "backupErrorReason"`) - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN IF EXISTS "existingBackupSnapshots"`) - // The enum type predates the workspace->sandbox / snapshotState->backupState - // renames, which do not rename the underlying Postgres type. Drop both the - // historical and the TypeORM-derived names so no orphan type is left behind. - await queryRunner.query(`DROP TYPE IF EXISTS "workspace_snapshotstate_enum"`) - await queryRunner.query(`DROP TYPE IF EXISTS "sandbox_backupstate_enum"`) - } - - public async down(queryRunner: QueryRunner): Promise { - // Best-effort restore of the dropped columns, index, and enum type. The - // historical type name is reused since `up` dropped it under that name. - await queryRunner.query( - `DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'workspace_snapshotstate_enum') THEN - CREATE TYPE "workspace_snapshotstate_enum" AS ENUM ('None', 'Pending', 'InProgress', 'Completed', 'Error'); - END IF; - END $$;`, - ) - await queryRunner.query( - `ALTER TABLE "sandbox" ADD COLUMN IF NOT EXISTS "backupState" "workspace_snapshotstate_enum" NOT NULL DEFAULT 'None'`, - ) - await queryRunner.query(`ALTER TABLE "sandbox" ADD COLUMN IF NOT EXISTS "backupSnapshot" character varying`) - await queryRunner.query(`ALTER TABLE "sandbox" ADD COLUMN IF NOT EXISTS "backupRegistryId" character varying`) - await queryRunner.query(`ALTER TABLE "sandbox" ADD COLUMN IF NOT EXISTS "lastBackupAt" TIMESTAMP WITH TIME ZONE`) - await queryRunner.query(`ALTER TABLE "sandbox" ADD COLUMN IF NOT EXISTS "backupErrorReason" text`) - await queryRunner.query( - `ALTER TABLE "sandbox" ADD COLUMN IF NOT EXISTS "existingBackupSnapshots" jsonb NOT NULL DEFAULT '[]'`, - ) - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "sandbox_backupstate_idx" ON "sandbox" ("backupState")`) - } -} diff --git a/apps/api/src/migrations/post-deploy/1781016743403-migration.ts b/apps/api/src/migrations/post-deploy/1781016743403-migration.ts deleted file mode 100644 index 4c4e72c05..000000000 --- a/apps/api/src/migrations/post-deploy/1781016743403-migration.ts +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -/** - * Renames the physical Postgres schema from the legacy `sandbox*` vocabulary to - * `box*` so the relations, columns, enum type, and indexes match the renamed - * TypeORM entities (Box, BoxLastActivity, BoxUsagePeriod, BoxUsagePeriodArchive). - * - * This is a contract (post-deploy) migration: it must run after every node is - * already serving the box-entity code, because the entity metadata pins the - * `box*` names and would otherwise diverge from the live schema. - * - * Guards: each statement uses an existence guard (ALTER ... IF EXISTS, or a - * catalog-checked DO block for objects that lack IF EXISTS) so re-running a - * partially-applied migration is safe. - * - * Enum note: the `box_active_only_idx` partial index predicate casts to the - * enum type (`::sandbox_state_enum`). Postgres stores that predicate by enum - * OID, not by name, so `ALTER TYPE ... RENAME` is transparent to the existing - * index. The enum is renamed first for logical coherence with the entity pins. - */ -export class Migration1781016743403 implements MigrationInterface { - name = 'Migration1781016743403' - - public async up(queryRunner: QueryRunner): Promise { - // 1. Enum type rename (must cohere with partial-index predicates). - await queryRunner.query(` - DO $$ - BEGIN - IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'sandbox_state_enum') - AND NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'box_state_enum') THEN - ALTER TYPE "sandbox_state_enum" RENAME TO "box_state_enum"; - END IF; - END $$; - `) - - // 2. Table renames. - await queryRunner.query(`ALTER TABLE IF EXISTS "sandbox" RENAME TO "box"`) - await queryRunner.query(`ALTER TABLE IF EXISTS "sandbox_last_activity" RENAME TO "box_last_activity"`) - await queryRunner.query(`ALTER TABLE IF EXISTS "sandbox_usage_periods" RENAME TO "box_usage_periods"`) - await queryRunner.query( - `ALTER TABLE IF EXISTS "sandbox_usage_periods_archive" RENAME TO "box_usage_periods_archive"`, - ) - - // 3. Column renames: "sandboxId" -> "boxId". - await queryRunner.query(`ALTER TABLE IF EXISTS "box_last_activity" RENAME COLUMN "sandboxId" TO "boxId"`) - await queryRunner.query(`ALTER TABLE IF EXISTS "ssh_access" RENAME COLUMN "sandboxId" TO "boxId"`) - await queryRunner.query(`ALTER TABLE IF EXISTS "box_usage_periods" RENAME COLUMN "sandboxId" TO "boxId"`) - await queryRunner.query(`ALTER TABLE IF EXISTS "box_usage_periods_archive" RENAME COLUMN "sandboxId" TO "boxId"`) - - // 4. Organization rate-limit columns. - await queryRunner.query( - `ALTER TABLE IF EXISTS "organization" RENAME COLUMN "sandbox_create_rate_limit" TO "box_create_rate_limit"`, - ) - await queryRunner.query( - `ALTER TABLE IF EXISTS "organization" RENAME COLUMN "sandbox_lifecycle_rate_limit" TO "box_lifecycle_rate_limit"`, - ) - await queryRunner.query( - `ALTER TABLE IF EXISTS "organization" RENAME COLUMN "sandbox_create_rate_limit_ttl_seconds" TO "box_create_rate_limit_ttl_seconds"`, - ) - await queryRunner.query( - `ALTER TABLE IF EXISTS "organization" RENAME COLUMN "sandbox_lifecycle_rate_limit_ttl_seconds" TO "box_lifecycle_rate_limit_ttl_seconds"`, - ) - - // 5. Organization quota columns (entity @Column names: max_cpu_per_box, max_memory_per_box, max_disk_per_box). - await queryRunner.query( - `ALTER TABLE IF EXISTS "organization" RENAME COLUMN "max_cpu_per_sandbox" TO "max_cpu_per_box"`, - ) - await queryRunner.query( - `ALTER TABLE IF EXISTS "organization" RENAME COLUMN "max_memory_per_sandbox" TO "max_memory_per_box"`, - ) - await queryRunner.query( - `ALTER TABLE IF EXISTS "organization" RENAME COLUMN "max_disk_per_sandbox" TO "max_disk_per_box"`, - ) - - // 6. Index renames: sandbox_*_idx -> box_*_idx (same suffix). - await queryRunner.query(`ALTER INDEX IF EXISTS "sandbox_state_idx" RENAME TO "box_state_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "sandbox_desiredstate_idx" RENAME TO "box_desiredstate_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "sandbox_snapshot_idx" RENAME TO "box_snapshot_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "sandbox_runnerid_idx" RENAME TO "box_runnerid_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "sandbox_runner_state_idx" RENAME TO "box_runner_state_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "sandbox_organizationid_idx" RENAME TO "box_organizationid_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "sandbox_region_idx" RENAME TO "box_region_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "sandbox_resources_idx" RENAME TO "box_resources_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "sandbox_backupstate_idx" RENAME TO "box_backupstate_idx"`) - await queryRunner.query( - `ALTER INDEX IF EXISTS "sandbox_runner_state_desired_idx" RENAME TO "box_runner_state_desired_idx"`, - ) - await queryRunner.query(`ALTER INDEX IF EXISTS "sandbox_active_only_idx" RENAME TO "box_active_only_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "sandbox_pending_idx" RENAME TO "box_pending_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "sandbox_labels_gin_full_idx" RENAME TO "box_labels_gin_full_idx"`) - - // 7. Indexes with the `idx_sandbox_*` prefix variant (entity pins: idx_box_*). - await queryRunner.query(`ALTER INDEX IF EXISTS "idx_sandbox_authtoken" RENAME TO "idx_box_authtoken"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "idx_sandbox_volumes_gin" RENAME TO "idx_box_volumes_gin"`) - await queryRunner.query( - `ALTER INDEX IF EXISTS "idx_sandbox_usage_periods_sandbox_end" RENAME TO "idx_box_usage_periods_box_end"`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - // Reverse order of up(): indexes, quota cols, rate-limit cols, columns, tables, enum. - - // 7. `idx_box_*` prefix variant back. - await queryRunner.query( - `ALTER INDEX IF EXISTS "idx_box_usage_periods_box_end" RENAME TO "idx_sandbox_usage_periods_sandbox_end"`, - ) - await queryRunner.query(`ALTER INDEX IF EXISTS "idx_box_volumes_gin" RENAME TO "idx_sandbox_volumes_gin"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "idx_box_authtoken" RENAME TO "idx_sandbox_authtoken"`) - - // 6. Index renames back. - await queryRunner.query(`ALTER INDEX IF EXISTS "box_labels_gin_full_idx" RENAME TO "sandbox_labels_gin_full_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "box_pending_idx" RENAME TO "sandbox_pending_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "box_active_only_idx" RENAME TO "sandbox_active_only_idx"`) - await queryRunner.query( - `ALTER INDEX IF EXISTS "box_runner_state_desired_idx" RENAME TO "sandbox_runner_state_desired_idx"`, - ) - await queryRunner.query(`ALTER INDEX IF EXISTS "box_backupstate_idx" RENAME TO "sandbox_backupstate_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "box_resources_idx" RENAME TO "sandbox_resources_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "box_region_idx" RENAME TO "sandbox_region_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "box_organizationid_idx" RENAME TO "sandbox_organizationid_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "box_runner_state_idx" RENAME TO "sandbox_runner_state_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "box_runnerid_idx" RENAME TO "sandbox_runnerid_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "box_snapshot_idx" RENAME TO "sandbox_snapshot_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "box_desiredstate_idx" RENAME TO "sandbox_desiredstate_idx"`) - await queryRunner.query(`ALTER INDEX IF EXISTS "box_state_idx" RENAME TO "sandbox_state_idx"`) - - // 5. Organization quota columns back. - await queryRunner.query( - `ALTER TABLE IF EXISTS "organization" RENAME COLUMN "max_disk_per_box" TO "max_disk_per_sandbox"`, - ) - await queryRunner.query( - `ALTER TABLE IF EXISTS "organization" RENAME COLUMN "max_memory_per_box" TO "max_memory_per_sandbox"`, - ) - await queryRunner.query( - `ALTER TABLE IF EXISTS "organization" RENAME COLUMN "max_cpu_per_box" TO "max_cpu_per_sandbox"`, - ) - - // 4. Organization rate-limit columns back. - await queryRunner.query( - `ALTER TABLE IF EXISTS "organization" RENAME COLUMN "box_lifecycle_rate_limit_ttl_seconds" TO "sandbox_lifecycle_rate_limit_ttl_seconds"`, - ) - await queryRunner.query( - `ALTER TABLE IF EXISTS "organization" RENAME COLUMN "box_create_rate_limit_ttl_seconds" TO "sandbox_create_rate_limit_ttl_seconds"`, - ) - await queryRunner.query( - `ALTER TABLE IF EXISTS "organization" RENAME COLUMN "box_lifecycle_rate_limit" TO "sandbox_lifecycle_rate_limit"`, - ) - await queryRunner.query( - `ALTER TABLE IF EXISTS "organization" RENAME COLUMN "box_create_rate_limit" TO "sandbox_create_rate_limit"`, - ) - - // 3. Column renames back: "boxId" -> "sandboxId". - await queryRunner.query(`ALTER TABLE IF EXISTS "box_usage_periods_archive" RENAME COLUMN "boxId" TO "sandboxId"`) - await queryRunner.query(`ALTER TABLE IF EXISTS "box_usage_periods" RENAME COLUMN "boxId" TO "sandboxId"`) - await queryRunner.query(`ALTER TABLE IF EXISTS "ssh_access" RENAME COLUMN "boxId" TO "sandboxId"`) - await queryRunner.query(`ALTER TABLE IF EXISTS "box_last_activity" RENAME COLUMN "boxId" TO "sandboxId"`) - - // 2. Table renames back. - await queryRunner.query( - `ALTER TABLE IF EXISTS "box_usage_periods_archive" RENAME TO "sandbox_usage_periods_archive"`, - ) - await queryRunner.query(`ALTER TABLE IF EXISTS "box_usage_periods" RENAME TO "sandbox_usage_periods"`) - await queryRunner.query(`ALTER TABLE IF EXISTS "box_last_activity" RENAME TO "sandbox_last_activity"`) - await queryRunner.query(`ALTER TABLE IF EXISTS "box" RENAME TO "sandbox"`) - - // 1. Enum type rename back. - await queryRunner.query(` - DO $$ - BEGIN - IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'box_state_enum') - AND NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'sandbox_state_enum') THEN - ALTER TYPE "box_state_enum" RENAME TO "sandbox_state_enum"; - END IF; - END $$; - `) - } -} diff --git a/apps/api/src/migrations/post-deploy/1781072797240-migration.ts b/apps/api/src/migrations/post-deploy/1781072797240-migration.ts deleted file mode 100644 index 440735c1c..000000000 --- a/apps/api/src/migrations/post-deploy/1781072797240-migration.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -/** - * Follow-up to Migration1781016743403 (Sandbox → Box rename): that migration - * renamed most sandbox-vocabulary objects to their box-vocabulary equivalents - * but missed three pieces, leaving the schema inconsistent with the renamed - * entities and enums: - * - * runner.currentStartedSandboxes → currentStartedBoxes - * organization.sandboxLimitedNetworkEgress → boxLimitedNetworkEgress - * job_resourcetype_enum SANDBOX value → BOX - * - * Symptoms before this migration: - * - Any query that loads the Runner entity (ResourceManager, - * SnapshotManager.propagateSnapshotToRunners) errors with - * "column Runner.currentStartedBoxes does not exist". - * - Box create through the Box service errors with - * "column Organization.boxLimitedNetworkEgress does not exist" - * (box.service.ts reads organization.boxLimitedNetworkEgress). - * - The runner-job CREATE_BOX path errors with - * 'invalid input value for enum job_resourcetype_enum: "BOX"' because - * the TS ResourceType enum was renamed (apps/api/src/box/enums/ - * resource-type.enum.ts: BOX = 'BOX') but the Postgres enum type - * still only knows 'SANDBOX'. - * - * Guards: every step is wrapped in a DO block that checks current state, so - * partial-state environments are safe to re-run and idempotent if anyone has - * already hand-applied any of the three renames. - */ -export class Migration1781072797240 implements MigrationInterface { - name = 'Migration1781072797240' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DO $$ - BEGIN - IF EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'runner' AND column_name = 'currentStartedSandboxes' - ) AND NOT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'runner' AND column_name = 'currentStartedBoxes' - ) THEN - ALTER TABLE "runner" RENAME COLUMN "currentStartedSandboxes" TO "currentStartedBoxes"; - END IF; - END $$; - `) - - await queryRunner.query(` - DO $$ - BEGIN - IF EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'organization' AND column_name = 'sandboxLimitedNetworkEgress' - ) AND NOT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'organization' AND column_name = 'boxLimitedNetworkEgress' - ) THEN - ALTER TABLE "organization" - RENAME COLUMN "sandboxLimitedNetworkEgress" TO "boxLimitedNetworkEgress"; - END IF; - END $$; - `) - - // ALTER TYPE ... RENAME VALUE is in-place and propagates to every row - // referencing the enum (storage is by OID, label is what's renamed). - // Requires Postgres 10+. - await queryRunner.query(` - DO $$ - BEGIN - IF EXISTS ( - SELECT 1 FROM pg_enum - WHERE enumtypid = (SELECT oid FROM pg_type WHERE typname = 'job_resourcetype_enum') - AND enumlabel = 'SANDBOX' - ) AND NOT EXISTS ( - SELECT 1 FROM pg_enum - WHERE enumtypid = (SELECT oid FROM pg_type WHERE typname = 'job_resourcetype_enum') - AND enumlabel = 'BOX' - ) THEN - ALTER TYPE "job_resourcetype_enum" RENAME VALUE 'SANDBOX' TO 'BOX'; - END IF; - END $$; - `) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - DO $$ - BEGIN - IF EXISTS ( - SELECT 1 FROM pg_enum - WHERE enumtypid = (SELECT oid FROM pg_type WHERE typname = 'job_resourcetype_enum') - AND enumlabel = 'BOX' - ) AND NOT EXISTS ( - SELECT 1 FROM pg_enum - WHERE enumtypid = (SELECT oid FROM pg_type WHERE typname = 'job_resourcetype_enum') - AND enumlabel = 'SANDBOX' - ) THEN - ALTER TYPE "job_resourcetype_enum" RENAME VALUE 'BOX' TO 'SANDBOX'; - END IF; - END $$; - `) - await queryRunner.query( - `ALTER TABLE IF EXISTS "organization" RENAME COLUMN "boxLimitedNetworkEgress" TO "sandboxLimitedNetworkEgress"`, - ) - await queryRunner.query( - `ALTER TABLE IF EXISTS "runner" RENAME COLUMN "currentStartedBoxes" TO "currentStartedSandboxes"`, - ) - } -} diff --git a/apps/api/src/migrations/post-deploy/1781100000000-migration.ts b/apps/api/src/migrations/post-deploy/1781100000000-migration.ts deleted file mode 100644 index 5b0da6441..000000000 --- a/apps/api/src/migrations/post-deploy/1781100000000-migration.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -/** - * Drops the self-hosted internal docker registry and the per-template multi-region - * mapping from the schema. - * - * The runner now pulls private ghcr images directly with its runtime-scoped ghcr - * auth, so the control plane keeps no per-pull registry rows. Templates are - * region-agnostic, so the `box_template_region` join table is gone too. Dropping - * the tables with CASCADE removes their FK constraints (e.g. the historical - * `snapshot_region` FKs to `box_template`/`region`); the registry enum type is - * then orphaned and dropped. - * - * Guards: every statement is `IF EXISTS` so the migration is idempotent and safe - * on databases where a table/type was already absent. - */ -export class Migration1781100000000 implements MigrationInterface { - name = 'Migration1781100000000' - - public async up(queryRunner: QueryRunner): Promise { - // CASCADE drops any FK constraints referencing these tables. - await queryRunner.query(`DROP TABLE IF EXISTS "box_template_region" CASCADE`) - await queryRunner.query(`DROP TABLE IF EXISTS "docker_registry" CASCADE`) - // Orphaned enum type backing docker_registry."registryType". - await queryRunner.query(`DROP TYPE IF EXISTS "public"."docker_registry_registrytype_enum"`) - } - - public async down(_queryRunner: QueryRunner): Promise { - // Best-effort, intentionally a no-op. The self-hosted registry and per-template - // region mapping are removed from the application; recreating empty tables - // would not restore the data or the application code that populated them. - } -} diff --git a/apps/api/src/migrations/post-deploy/1781200000000-migration.ts b/apps/api/src/migrations/post-deploy/1781200000000-migration.ts deleted file mode 100644 index 61e46a606..000000000 --- a/apps/api/src/migrations/post-deploy/1781200000000-migration.ts +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -/** - * Drift-independent rebuild of the two surviving artifact tables: `box_template` - * and `runner_artifact_cache`. - * - * Background: dev RDS sits on a divergent "saved_image" naming lineage while this - * branch is on the "box_template" lineage. Earlier box_template migrations shared - * names with the dev lineage, so they were recorded as already-run and skipped on - * dev, leaving the schema drifted. A brand-new migration name always runs exactly - * once, so this migration force-converges any prior state (saved_image lineage, - * box_template lineage, or empty) to the identical clean final shape. - * - * Strategy: drop every legacy naming variant of the two tables and their join / - * child tables (CASCADE removes any surviving FK, e.g. the historical - * `snapshot_region` -> `snapshot` constraint), drop the orphaned enum types, then - * recreate the two tables, enums, indexes, and the unique constraint exactly as - * the current entity definitions require. - * - * Every statement is guarded (IF EXISTS / IF NOT EXISTS / pg_catalog checks) so the - * migration is idempotent and safe regardless of which tables/types pre-exist. - */ -export class Migration1781200000000 implements MigrationInterface { - name = 'Migration1781200000000' - - public async up(queryRunner: QueryRunner): Promise { - // uuid_generate_v4() default below depends on this extension. - await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`) - - // 1. Drop every legacy naming variant. Child / join + cache tables first so the - // parent drops do not trip over dependents; CASCADE clears any surviving FK. - await queryRunner.query(`DROP TABLE IF EXISTS "box_template_region" CASCADE`) - await queryRunner.query(`DROP TABLE IF EXISTS "snapshot_region" CASCADE`) - await queryRunner.query(`DROP TABLE IF EXISTS "saved_image_region" CASCADE`) - await queryRunner.query(`DROP TABLE IF EXISTS "docker_registry" CASCADE`) - await queryRunner.query(`DROP TABLE IF EXISTS "build_info" CASCADE`) - await queryRunner.query(`DROP TABLE IF EXISTS "runner_artifact_cache" CASCADE`) - await queryRunner.query(`DROP TABLE IF EXISTS "snapshot_runner" CASCADE`) - await queryRunner.query(`DROP TABLE IF EXISTS "saved_image_runner" CASCADE`) - await queryRunner.query(`DROP TABLE IF EXISTS "image_node" CASCADE`) - await queryRunner.query(`DROP TABLE IF EXISTS "box_template" CASCADE`) - await queryRunner.query(`DROP TABLE IF EXISTS "snapshot" CASCADE`) - await queryRunner.query(`DROP TABLE IF EXISTS "saved_image" CASCADE`) - await queryRunner.query(`DROP TABLE IF EXISTS "image" CASCADE`) - - // 2. Drop every enum variant once its backing tables are gone. - await queryRunner.query(`DROP TYPE IF EXISTS "public"."box_template_state_enum"`) - await queryRunner.query(`DROP TYPE IF EXISTS "public"."snapshot_state_enum"`) - await queryRunner.query(`DROP TYPE IF EXISTS "public"."saved_image_state_enum"`) - await queryRunner.query(`DROP TYPE IF EXISTS "public"."image_state_enum"`) - await queryRunner.query(`DROP TYPE IF EXISTS "public"."runner_artifact_cache_state_enum"`) - await queryRunner.query(`DROP TYPE IF EXISTS "public"."snapshot_runner_state_enum"`) - await queryRunner.query(`DROP TYPE IF EXISTS "public"."saved_image_runner_state_enum"`) - await queryRunner.query(`DROP TYPE IF EXISTS "public"."image_node_state_enum"`) - await queryRunner.query(`DROP TYPE IF EXISTS "public"."docker_registry_registrytype_enum"`) - await queryRunner.query(`DROP TYPE IF EXISTS "public"."docker_registry_registrytype_enum_old"`) - - // 3. Recreate the two enum types (CREATE TYPE has no IF NOT EXISTS). - await queryRunner.query(` - DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'box_template_state_enum') THEN - CREATE TYPE "public"."box_template_state_enum" AS ENUM ( - 'pending', 'pulling', 'active', 'inactive', 'error', 'removing' - ); - END IF; - END $$; - `) - await queryRunner.query(` - DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'runner_artifact_cache_state_enum') THEN - CREATE TYPE "public"."runner_artifact_cache_state_enum" AS ENUM ( - 'pulling_artifact', 'ready', 'error', 'removing' - ); - END IF; - END $$; - `) - - // 4. Recreate the two tables to match the current entity definitions. - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS "box_template" ( - "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "organizationId" uuid, - "general" boolean NOT NULL DEFAULT false, - "name" character varying NOT NULL, - "imageName" character varying NOT NULL DEFAULT '', - "artifactRef" character varying, - "state" "public"."box_template_state_enum" NOT NULL DEFAULT 'pending', - "errorReason" character varying, - "size" double precision, - "cpu" integer NOT NULL DEFAULT 1, - "gpu" integer NOT NULL DEFAULT 0, - "mem" integer NOT NULL DEFAULT 1, - "disk" integer NOT NULL DEFAULT 3, - "hideFromUsers" boolean NOT NULL DEFAULT false, - "entrypoint" text[], - "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "lastUsedAt" TIMESTAMP, - "initialRunnerId" character varying, - CONSTRAINT "box_template_pkey" PRIMARY KEY ("id") - ) - `) - await queryRunner.query(` - CREATE TABLE IF NOT EXISTS "runner_artifact_cache" ( - "id" uuid NOT NULL DEFAULT uuid_generate_v4(), - "state" "public"."runner_artifact_cache_state_enum" NOT NULL DEFAULT 'pulling_artifact', - "errorReason" character varying, - "artifactRef" character varying NOT NULL DEFAULT '', - "runnerId" character varying NOT NULL, - "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - CONSTRAINT "runner_artifact_cache_pkey" PRIMARY KEY ("id") - ) - `) - - // 5. Recreate indexes and the unique constraint. - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "box_template_name_idx" ON "box_template" ("name")`) - await queryRunner.query(`CREATE INDEX IF NOT EXISTS "box_template_state_idx" ON "box_template" ("state")`) - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "runner_artifact_cache_artifactref_idx" ON "runner_artifact_cache" ("artifactRef")`, - ) - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "runner_artifact_cache_runnerid_artifactref_idx" ON "runner_artifact_cache" ("runnerId", "artifactRef")`, - ) - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "runner_artifact_cache_runnerid_idx" ON "runner_artifact_cache" ("runnerId")`, - ) - await queryRunner.query( - `CREATE INDEX IF NOT EXISTS "runner_artifact_cache_state_idx" ON "runner_artifact_cache" ("state")`, - ) - - // @Unique(['organizationId', 'name']) on BoxTemplate. ADD CONSTRAINT has no - // IF NOT EXISTS, so guard against re-adding when the constraint already exists. - await queryRunner.query(` - DO $$ BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint WHERE conname = 'box_template_organizationId_name_unique' - ) THEN - ALTER TABLE "box_template" - ADD CONSTRAINT "box_template_organizationId_name_unique" UNIQUE ("organizationId", "name"); - END IF; - END $$; - `) - } - - public async down(_queryRunner: QueryRunner): Promise { - // Best-effort, intentionally a no-op. This migration force-rebuilds the surviving - // tables to converge a drifted database; the prior multi-table legacy state - // (saved_image / snapshot / image lineages with their region, registry, and - // build_info dependents) is not reconstructable and should not be recreated. - } -} diff --git a/apps/api/src/migrations/pre-deploy/1770900000000-migration.ts b/apps/api/src/migrations/pre-deploy/1770900000000-migration.ts deleted file mode 100644 index fada64c76..000000000 --- a/apps/api/src/migrations/pre-deploy/1770900000000-migration.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1770900000000 implements MigrationInterface { - name = 'Migration1770900000000' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "organization" ADD "snapshot_deactivation_timeout_minutes" integer NOT NULL DEFAULT 20160`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "organization" DROP COLUMN "snapshot_deactivation_timeout_minutes"`) - } -} diff --git a/apps/api/src/migrations/pre-deploy/1773744656413-migration.ts b/apps/api/src/migrations/pre-deploy/1773744656413-migration.ts deleted file mode 100644 index 0fb8f835e..000000000 --- a/apps/api/src/migrations/pre-deploy/1773744656413-migration.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1773744656413 implements MigrationInterface { - name = 'Migration1773744656413' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "currentAllocatedCpu" TYPE double precision`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "currentAllocatedMemoryGiB" TYPE double precision`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "currentAllocatedDiskGiB" TYPE double precision`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "runner" ALTER COLUMN "currentAllocatedDiskGiB" TYPE integer USING ROUND("currentAllocatedDiskGiB")::integer`, - ) - await queryRunner.query( - `ALTER TABLE "runner" ALTER COLUMN "currentAllocatedMemoryGiB" TYPE integer USING ROUND("currentAllocatedMemoryGiB")::integer`, - ) - await queryRunner.query( - `ALTER TABLE "runner" ALTER COLUMN "currentAllocatedCpu" TYPE integer USING ROUND("currentAllocatedCpu")::integer`, - ) - } -} diff --git a/apps/api/src/migrations/pre-deploy/1773916204375-migration.ts b/apps/api/src/migrations/pre-deploy/1773916204375-migration.ts deleted file mode 100644 index ce1561051..000000000 --- a/apps/api/src/migrations/pre-deploy/1773916204375-migration.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1773916204375 implements MigrationInterface { - name = 'Migration1773916204375' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "runner" ADD "serviceHealth" jsonb`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "runner" DROP COLUMN "serviceHealth"`) - } -} diff --git a/apps/api/src/migrations/pre-deploy/1774438866001-migration.ts b/apps/api/src/migrations/pre-deploy/1774438866001-migration.ts deleted file mode 100644 index 144460e34..000000000 --- a/apps/api/src/migrations/pre-deploy/1774438866001-migration.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -/** - * Reconciliation migration that resolves drift between the database schema and the current entity definitions in the codebase. - */ -export class Migration1774438866001 implements MigrationInterface { - name = 'Migration1774438866001' - - public async up(queryRunner: QueryRunner): Promise { - /** - * Constraint renames due to conflict with custom naming strategy. - */ - await queryRunner.query( - `ALTER TABLE "snapshot_region" RENAME CONSTRAINT "FK_snapshot_region_snapshot" TO "snapshot_region_snapshotId_fk"`, - ) - await queryRunner.query( - `ALTER TABLE "snapshot_region" RENAME CONSTRAINT "FK_snapshot_region_region" TO "snapshot_region_regionId_fk"`, - ) - await queryRunner.query( - `ALTER TABLE "snapshot" RENAME CONSTRAINT "public.snapshot_buildInfoImageRef_fk" TO "snapshot_buildInfoSnapshotRef_fk"`, - ) - await queryRunner.query( - `ALTER TABLE "sandbox" RENAME CONSTRAINT "public.sandbox_buildInfoSnapshotRef_fk" TO "sandbox_buildInfoSnapshotRef_fk"`, - ) - await queryRunner.query( - `ALTER TABLE "snapshot" RENAME CONSTRAINT "image_organizationId_name_unique" TO "snapshot_organizationId_name_unique"`, - ) - await queryRunner.query(`ALTER TABLE "sandbox" RENAME CONSTRAINT "public.sandbox_id_pk" TO "sandbox_id_pk"`) - - /** Add missing column defaults for runner that the entity defines but the original migration omitted. */ - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "cpu" SET DEFAULT '0'`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "memoryGiB" SET DEFAULT '0'`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "diskGiB" SET DEFAULT '0'`) - - /** - * Recreate roleId FKs on junction tables with NO ACTION instead of CASCADE. - * TypeORM's onDelete: CASCADE on ManyToMany only applies to the owning side FK, - * the inverse side (roleId) defaults to NO ACTION. The original migrations incorrectly - * created both FKs with CASCADE. This aligns the database with TypeORM's expected schema. - */ - await queryRunner.query( - `ALTER TABLE "organization_role_assignment_invitation" DROP CONSTRAINT "organization_role_assignment_invitation_roleId_fk"`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment" DROP CONSTRAINT "organization_role_assignment_roleId_fk"`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment_invitation" ADD CONSTRAINT "organization_role_assignment_invitation_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "organization_role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment" ADD CONSTRAINT "organization_role_assignment_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "organization_role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "organization_role_assignment" DROP CONSTRAINT "organization_role_assignment_roleId_fk"`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment_invitation" DROP CONSTRAINT "organization_role_assignment_invitation_roleId_fk"`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment" ADD CONSTRAINT "organization_role_assignment_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "organization_role"("id") ON DELETE CASCADE ON UPDATE CASCADE`, - ) - await queryRunner.query( - `ALTER TABLE "organization_role_assignment_invitation" ADD CONSTRAINT "organization_role_assignment_invitation_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "organization_role"("id") ON DELETE CASCADE ON UPDATE CASCADE`, - ) - - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "diskGiB" DROP DEFAULT`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "memoryGiB" DROP DEFAULT`) - await queryRunner.query(`ALTER TABLE "runner" ALTER COLUMN "cpu" DROP DEFAULT`) - - await queryRunner.query(`ALTER TABLE "sandbox" RENAME CONSTRAINT "sandbox_id_pk" TO "public.sandbox_id_pk"`) - - await queryRunner.query( - `ALTER TABLE "snapshot" RENAME CONSTRAINT "snapshot_organizationId_name_unique" TO "image_organizationId_name_unique"`, - ) - - await queryRunner.query( - `ALTER TABLE "sandbox" RENAME CONSTRAINT "sandbox_buildInfoSnapshotRef_fk" TO "public.sandbox_buildInfoSnapshotRef_fk"`, - ) - await queryRunner.query( - `ALTER TABLE "snapshot" RENAME CONSTRAINT "snapshot_buildInfoSnapshotRef_fk" TO "public.snapshot_buildInfoImageRef_fk"`, - ) - await queryRunner.query( - `ALTER TABLE "snapshot_region" RENAME CONSTRAINT "snapshot_region_regionId_fk" TO "FK_snapshot_region_region"`, - ) - await queryRunner.query( - `ALTER TABLE "snapshot_region" RENAME CONSTRAINT "snapshot_region_snapshotId_fk" TO "FK_snapshot_region_snapshot"`, - ) - } -} diff --git a/apps/api/src/migrations/pre-deploy/1774454790153-migration.ts b/apps/api/src/migrations/pre-deploy/1774454790153-migration.ts deleted file mode 100644 index 6b32c41a3..000000000 --- a/apps/api/src/migrations/pre-deploy/1774454790153-migration.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Daytona Platforms Inc. - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class Migration1774454790153 implements MigrationInterface { - name = 'Migration1774454790153' - - public async up(queryRunner: QueryRunner): Promise { - // Create new table - await queryRunner.query( - `CREATE TABLE "sandbox_last_activity" ("sandboxId" character varying NOT NULL, "lastActivityAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "sandbox_last_activity_sandboxId_pk" PRIMARY KEY ("sandboxId"))`, - ) - await queryRunner.query( - `ALTER TABLE "sandbox_last_activity" ADD CONSTRAINT "sandbox_last_activity_sandboxId_fk" FOREIGN KEY ("sandboxId") REFERENCES "sandbox"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, - ) - - // Copy existing data from sandbox.lastActivityAt to new table - await queryRunner.query(` - INSERT INTO "sandbox_last_activity" ("sandboxId", "lastActivityAt") - SELECT "id", COALESCE("lastActivityAt", "createdAt", NOW()) - FROM "sandbox" - WHERE "state" != 'destroyed' - `) - - // Create sync function for dual-write - await queryRunner.query(` - CREATE OR REPLACE FUNCTION sync_sandbox_last_activity() - RETURNS TRIGGER AS $$ - BEGIN - IF TG_TABLE_NAME = 'sandbox' THEN - INSERT INTO sandbox_last_activity ("sandboxId", "lastActivityAt") - VALUES (NEW.id, NEW."lastActivityAt") - ON CONFLICT ("sandboxId") DO UPDATE SET "lastActivityAt" = EXCLUDED."lastActivityAt" - WHERE sandbox_last_activity."lastActivityAt" IS DISTINCT FROM EXCLUDED."lastActivityAt"; - ELSIF TG_TABLE_NAME = 'sandbox_last_activity' THEN - UPDATE sandbox SET "lastActivityAt" = NEW."lastActivityAt" - WHERE id = NEW."sandboxId" AND "lastActivityAt" IS DISTINCT FROM NEW."lastActivityAt"; - END IF; - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - `) - - // Create trigger on sandbox table (old -> new) - await queryRunner.query(` - CREATE TRIGGER sandbox_activity_sync_to_new - AFTER INSERT OR UPDATE OF "lastActivityAt" ON "sandbox" - FOR EACH ROW EXECUTE FUNCTION sync_sandbox_last_activity(); - `) - - // Create trigger on sandbox_last_activity table (new -> old) - await queryRunner.query(` - CREATE TRIGGER sandbox_activity_sync_to_old - AFTER INSERT OR UPDATE OF "lastActivityAt" ON "sandbox_last_activity" - FOR EACH ROW EXECUTE FUNCTION sync_sandbox_last_activity(); - `) - - // Re-sync any rows that were created or changed during the window between initial copy and trigger creation - await queryRunner.query(` - INSERT INTO "sandbox_last_activity" ("sandboxId", "lastActivityAt") - SELECT "id", COALESCE("lastActivityAt", "createdAt", NOW()) - FROM "sandbox" - WHERE "state" != 'destroyed' - ON CONFLICT ("sandboxId") DO UPDATE - SET "lastActivityAt" = EXCLUDED."lastActivityAt" - WHERE "sandbox_last_activity"."lastActivityAt" IS DISTINCT FROM EXCLUDED."lastActivityAt" - `) - } - - public async down(queryRunner: QueryRunner): Promise { - // Drop dual-write triggers and function - await queryRunner.query(`DROP TRIGGER IF EXISTS sandbox_activity_sync_to_old ON "sandbox_last_activity"`) - await queryRunner.query(`DROP TRIGGER IF EXISTS sandbox_activity_sync_to_new ON "sandbox"`) - await queryRunner.query(`DROP FUNCTION IF EXISTS sync_sandbox_last_activity()`) - - // Drop table - await queryRunner.query(`ALTER TABLE "sandbox_last_activity" DROP CONSTRAINT "sandbox_last_activity_sandboxId_fk"`) - await queryRunner.query(`DROP TABLE "sandbox_last_activity"`) - } -} diff --git a/apps/api/src/migrations/pre-deploy/1780600000000-migration.ts b/apps/api/src/migrations/pre-deploy/1780600000000-migration.ts deleted file mode 100644 index dbd706fa4..000000000 --- a/apps/api/src/migrations/pre-deploy/1780600000000-migration.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { randomInt } from 'crypto' -import { MigrationInterface, QueryRunner } from 'typeorm' - -const BOX_ID_LENGTH = 12 -const BOX_ID_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' - -function generateBoxId(usedBoxIds: Set): string { - for (let attempt = 0; attempt < 20; attempt += 1) { - let boxId = '' - for (let i = 0; i < BOX_ID_LENGTH; i += 1) { - boxId += BOX_ID_ALPHABET[randomInt(BOX_ID_ALPHABET.length)] - } - - if (!usedBoxIds.has(boxId)) { - usedBoxIds.add(boxId) - return boxId - } - } - - throw new Error('Failed to generate a unique sandbox boxId') -} - -export class Migration1780600000000 implements MigrationInterface { - name = 'Migration1780600000000' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "sandbox" ADD "boxId" character varying(12)`) - - const rows = (await queryRunner.query(`SELECT "id", "boxId" FROM "sandbox"`)) as Array<{ - id: string - boxId: string | null - }> - - const usedBoxIds = new Set(rows.map((row) => row.boxId).filter((boxId): boxId is string => Boolean(boxId))) - - for (const row of rows) { - if (row.boxId) { - continue - } - await queryRunner.query(`UPDATE "sandbox" SET "boxId" = $1 WHERE "id" = $2`, [generateBoxId(usedBoxIds), row.id]) - } - - await queryRunner.query(`ALTER TABLE "sandbox" ALTER COLUMN "boxId" SET NOT NULL`) - await queryRunner.query(`CREATE UNIQUE INDEX "sandbox_boxid_unique_idx" ON "sandbox" ("boxId")`) - await queryRunner.query(`CREATE INDEX "sandbox_organizationid_boxid_idx" ON "sandbox" ("organizationId", "boxId")`) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."sandbox_organizationid_boxid_idx"`) - await queryRunner.query(`DROP INDEX "public"."sandbox_boxid_unique_idx"`) - await queryRunner.query(`ALTER TABLE "sandbox" DROP COLUMN "boxId"`) - } -} diff --git a/apps/api/src/migrations/pre-deploy/1780912800000-migration.ts b/apps/api/src/migrations/pre-deploy/1780912800000-migration.ts deleted file mode 100644 index 8418bffaa..000000000 --- a/apps/api/src/migrations/pre-deploy/1780912800000-migration.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2025 Daytona Platforms Inc. - * Modified by BoxLite AI, 2025-2026 - * SPDX-License-Identifier: AGPL-3.0 - */ - -import { MigrationInterface, QueryRunner } from 'typeorm' - -export class DefaultOrganizationMembership1780912800000 implements MigrationInterface { - name = 'DefaultOrganizationMembership1780912800000' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query('ALTER TABLE "organization_user" ADD "isDefaultForUser" boolean NOT NULL DEFAULT false') - await queryRunner.query(` - UPDATE "organization_user" "ou" - SET "isDefaultForUser" = true - FROM "organization" "org" - WHERE "ou"."organizationId" = "org"."id" - AND "ou"."userId" = "org"."createdBy" - AND "org"."personal" = true - `) - await queryRunner.query(` - WITH ranked_memberships AS ( - SELECT - "organizationId", - "userId", - ROW_NUMBER() OVER ( - PARTITION BY "userId" - ORDER BY "createdAt" ASC, "organizationId" ASC - ) AS rank - FROM "organization_user" "ou" - WHERE NOT EXISTS ( - SELECT 1 - FROM "organization_user" "default_ou" - WHERE "default_ou"."userId" = "ou"."userId" - AND "default_ou"."isDefaultForUser" = true - ) - ) - UPDATE "organization_user" "ou" - SET "isDefaultForUser" = true - FROM ranked_memberships "ranked" - WHERE "ou"."organizationId" = "ranked"."organizationId" - AND "ou"."userId" = "ranked"."userId" - AND "ranked"."rank" = 1 - `) - await queryRunner.query(` - UPDATE "organization" - SET "name" = 'Default Organization' - WHERE "personal" = true - AND "name" = 'Personal' - `) - await queryRunner.query( - 'CREATE UNIQUE INDEX "organization_user_default_user_unique" ON "organization_user" ("userId") WHERE "isDefaultForUser" = true', - ) - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query('DROP INDEX "organization_user_default_user_unique"') - await queryRunner.query('ALTER TABLE "organization_user" DROP COLUMN "isDefaultForUser"') - } -} diff --git a/apps/api/src/migrations/pre-deploy/1782000000000-migration.ts b/apps/api/src/migrations/pre-deploy/1782000000000-migration.ts new file mode 100644 index 000000000..e5a962330 --- /dev/null +++ b/apps/api/src/migrations/pre-deploy/1782000000000-migration.ts @@ -0,0 +1,371 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class Migration1782000000000 implements MigrationInterface { + name = 'Migration1782000000000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`) + await queryRunner.query(`CREATE TYPE "public"."user_role_enum" AS ENUM('admin', 'user')`) + await queryRunner.query( + `CREATE TABLE "user" ("id" character varying NOT NULL, "name" character varying NOT NULL, "email" character varying NOT NULL DEFAULT '', "emailVerified" boolean NOT NULL DEFAULT false, "keyPair" text, "publicKeys" text NOT NULL, "role" "public"."user_role_enum" NOT NULL DEFAULT 'user', "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "user_id_pk" PRIMARY KEY ("id"))`, + ) + await queryRunner.query( + `CREATE TYPE "public"."api_key_permissions_enum" AS ENUM('write:registries', 'delete:registries', 'write:templates', 'delete:templates', 'write:boxes', 'delete:boxes', 'read:volumes', 'write:volumes', 'delete:volumes', 'write:regions', 'delete:regions', 'read:runners', 'write:runners', 'delete:runners', 'read:audit_logs')`, + ) + await queryRunner.query( + `CREATE TABLE "api_key" ("organizationId" uuid NOT NULL, "userId" character varying NOT NULL, "name" character varying NOT NULL, "keyHash" character varying NOT NULL DEFAULT '', "keyPrefix" character varying NOT NULL DEFAULT '', "keySuffix" character varying NOT NULL DEFAULT '', "permissions" "public"."api_key_permissions_enum" array NOT NULL, "createdAt" TIMESTAMP NOT NULL, "lastUsedAt" TIMESTAMP, "expiresAt" TIMESTAMP, CONSTRAINT "api_key_keyHash_unique" UNIQUE ("keyHash"), CONSTRAINT "api_key_organizationId_userId_name_pk" PRIMARY KEY ("organizationId", "userId", "name"))`, + ) + await queryRunner.query(`CREATE INDEX "api_key_org_user_idx" ON "api_key" ("organizationId", "userId") `) + await queryRunner.query( + `CREATE TABLE "webhook_initialization" ("organizationId" character varying NOT NULL, "svixApplicationId" character varying, "lastError" text, "retryCount" integer NOT NULL DEFAULT '0', "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "webhook_initialization_organizationId_pk" PRIMARY KEY ("organizationId"))`, + ) + await queryRunner.query(`CREATE TYPE "public"."region_regiontype_enum" AS ENUM('shared', 'dedicated', 'custom')`) + await queryRunner.query( + `CREATE TABLE "region" ("id" character varying NOT NULL, "name" character varying NOT NULL, "organizationId" uuid, "regionType" "public"."region_regiontype_enum" NOT NULL, "enforceQuotas" boolean NOT NULL DEFAULT true, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "proxyUrl" character varying, "toolboxProxyUrl" character varying, "proxyApiKeyHash" character varying, "sshGatewayUrl" character varying, "sshGatewayApiKeyHash" character varying, CONSTRAINT "region_not_custom" CHECK ("organizationId" IS NOT NULL OR "regionType" != 'custom'), CONSTRAINT "region_not_shared" CHECK ("organizationId" IS NULL OR "regionType" != 'shared'), CONSTRAINT "region_id_pk" PRIMARY KEY ("id"))`, + ) + await queryRunner.query( + `CREATE INDEX "idx_region_custom" ON "region" ("organizationId") WHERE "regionType" = 'custom'`, + ) + await queryRunner.query( + `CREATE UNIQUE INDEX "region_sshGatewayApiKeyHash_unique" ON "region" ("sshGatewayApiKeyHash") WHERE "sshGatewayApiKeyHash" IS NOT NULL`, + ) + await queryRunner.query( + `CREATE UNIQUE INDEX "region_proxyApiKeyHash_unique" ON "region" ("proxyApiKeyHash") WHERE "proxyApiKeyHash" IS NOT NULL`, + ) + await queryRunner.query( + `CREATE UNIQUE INDEX "region_organizationId_null_name_unique" ON "region" ("name") WHERE "organizationId" IS NULL`, + ) + await queryRunner.query( + `CREATE UNIQUE INDEX "region_organizationId_name_unique" ON "region" ("organizationId", "name") WHERE "organizationId" IS NOT NULL`, + ) + await queryRunner.query(`CREATE TYPE "public"."organization_invitation_role_enum" AS ENUM('owner', 'member')`) + await queryRunner.query( + `CREATE TYPE "public"."organization_invitation_status_enum" AS ENUM('pending', 'accepted', 'declined', 'cancelled')`, + ) + await queryRunner.query( + `CREATE TABLE "organization_invitation" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "organizationId" uuid NOT NULL, "email" character varying NOT NULL, "invitedBy" character varying NOT NULL DEFAULT '', "role" "public"."organization_invitation_role_enum" NOT NULL DEFAULT 'member', "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "status" "public"."organization_invitation_status_enum" NOT NULL DEFAULT 'pending', "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "organization_invitation_id_pk" PRIMARY KEY ("id"))`, + ) + await queryRunner.query( + `CREATE TYPE "public"."organization_role_permissions_enum" AS ENUM('write:registries', 'delete:registries', 'write:templates', 'delete:templates', 'write:boxes', 'delete:boxes', 'read:volumes', 'write:volumes', 'delete:volumes', 'write:regions', 'delete:regions', 'read:runners', 'write:runners', 'delete:runners', 'read:audit_logs')`, + ) + await queryRunner.query( + `CREATE TABLE "organization_role" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "description" character varying NOT NULL, "permissions" "public"."organization_role_permissions_enum" array NOT NULL, "isGlobal" boolean NOT NULL DEFAULT false, "organizationId" uuid, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "organization_role_id_pk" PRIMARY KEY ("id"))`, + ) + await queryRunner.query(` + INSERT INTO "organization_role" + ("id", "name", "description", "permissions", "isGlobal") + VALUES + ( + '00000000-0000-0000-0000-000000000001', + 'Developer', + 'Grants the ability to create boxes in the organization', + ARRAY['write:boxes']::organization_role_permissions_enum[], + TRUE + ), + ( + '00000000-0000-0000-0000-000000000002', + 'Boxes Admin', + 'Grants admin access to boxes in the organization', + ARRAY['write:boxes', 'delete:boxes']::organization_role_permissions_enum[], + TRUE + ), + ( + '00000000-0000-0000-0000-000000000003', + 'Templates Admin', + 'Grants admin access to templates in the organization', + ARRAY['write:templates', 'delete:templates']::organization_role_permissions_enum[], + TRUE + ), + ( + '00000000-0000-0000-0000-000000000004', + 'Registries Admin', + 'Grants admin access to registries in the organization', + ARRAY['write:registries', 'delete:registries']::organization_role_permissions_enum[], + TRUE + ), + ( + '00000000-0000-0000-0000-000000000005', + 'Super Admin', + 'Grants full access to all resources in the organization', + ARRAY[ + 'write:registries', + 'delete:registries', + 'write:templates', + 'delete:templates', + 'write:boxes', + 'delete:boxes', + 'read:volumes', + 'write:volumes', + 'delete:volumes', + 'write:regions', + 'delete:regions', + 'read:runners', + 'write:runners', + 'delete:runners', + 'read:audit_logs' + ]::organization_role_permissions_enum[], + TRUE + ), + ( + '00000000-0000-0000-0000-000000000006', + 'Volumes Admin', + 'Grants admin access to volumes in the organization', + ARRAY['read:volumes', 'write:volumes', 'delete:volumes']::organization_role_permissions_enum[], + TRUE + ), + ( + '00000000-0000-0000-0000-000000000007', + 'Auditor', + 'Grants access to audit logs in the organization', + ARRAY['read:audit_logs']::organization_role_permissions_enum[], + TRUE + ), + ( + '00000000-0000-0000-0000-000000000008', + 'Infrastructure Admin', + 'Grants admin access to infrastructure in the organization', + ARRAY[ + 'write:regions', + 'delete:regions', + 'read:runners', + 'write:runners', + 'delete:runners' + ]::organization_role_permissions_enum[], + TRUE + ) + ON CONFLICT ("id") DO UPDATE + SET + "name" = EXCLUDED."name", + "description" = EXCLUDED."description", + "permissions" = EXCLUDED."permissions", + "isGlobal" = EXCLUDED."isGlobal" + `) + await queryRunner.query(`CREATE TYPE "public"."organization_user_role_enum" AS ENUM('owner', 'member')`) + await queryRunner.query( + `CREATE TABLE "organization_user" ("organizationId" uuid NOT NULL, "userId" character varying NOT NULL, "role" "public"."organization_user_role_enum" NOT NULL DEFAULT 'member', "isDefaultForUser" boolean NOT NULL DEFAULT false, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "organization_user_organizationId_userId_pk" PRIMARY KEY ("organizationId", "userId"))`, + ) + await queryRunner.query( + `CREATE UNIQUE INDEX "organization_user_default_user_unique" ON "organization_user" ("userId") WHERE "isDefaultForUser" = true`, + ) + await queryRunner.query( + `CREATE TABLE "organization" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "createdBy" character varying NOT NULL, "telemetryEnabled" boolean NOT NULL DEFAULT true, "defaultRegionId" character varying, "max_cpu_per_box" integer NOT NULL DEFAULT '4', "max_memory_per_box" integer NOT NULL DEFAULT '8', "max_disk_per_box" integer NOT NULL DEFAULT '10', "authenticated_rate_limit" integer, "box_create_rate_limit" integer, "box_lifecycle_rate_limit" integer, "authenticated_rate_limit_ttl_seconds" integer, "box_create_rate_limit_ttl_seconds" integer, "box_lifecycle_rate_limit_ttl_seconds" integer, "suspended" boolean NOT NULL DEFAULT false, "suspendedAt" TIMESTAMP WITH TIME ZONE, "suspensionReason" character varying, "suspensionCleanupGracePeriodHours" integer NOT NULL DEFAULT '24', "suspendedUntil" TIMESTAMP WITH TIME ZONE, "template_deactivation_timeout_minutes" integer NOT NULL DEFAULT '20160', "boxLimitedNetworkEgress" boolean NOT NULL DEFAULT false, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "experimentalConfig" jsonb, CONSTRAINT "organization_id_pk" PRIMARY KEY ("id"))`, + ) + await queryRunner.query( + `CREATE TYPE "public"."volume_state_enum" AS ENUM('creating', 'ready', 'pending_create', 'pending_delete', 'deleting', 'deleted', 'error')`, + ) + await queryRunner.query( + `CREATE TABLE "volume" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "organizationId" uuid, "name" character varying NOT NULL, "state" "public"."volume_state_enum" NOT NULL DEFAULT 'pending_create', "errorReason" character varying, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "lastUsedAt" TIMESTAMP, CONSTRAINT "volume_organizationId_name_unique" UNIQUE ("organizationId", "name"), CONSTRAINT "volume_id_pk" PRIMARY KEY ("id"))`, + ) + await queryRunner.query(`CREATE TYPE "public"."warm_pool_class_enum" AS ENUM('small', 'medium', 'large')`) + await queryRunner.query( + `CREATE TABLE "warm_pool" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "pool" integer NOT NULL, "savedImage" character varying NOT NULL, "target" character varying NOT NULL, "cpu" integer NOT NULL, "mem" integer NOT NULL, "disk" integer NOT NULL, "gpu" integer NOT NULL, "gpuType" character varying NOT NULL, "class" "public"."warm_pool_class_enum" NOT NULL DEFAULT 'small', "osUser" character varying NOT NULL, "errorReason" character varying, "env" text NOT NULL DEFAULT '{}', "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "warm_pool_id_pk" PRIMARY KEY ("id"))`, + ) + await queryRunner.query( + `CREATE INDEX "warm_pool_find_idx" ON "warm_pool" ("savedImage", "target", "class", "cpu", "mem", "disk", "gpu", "osUser", "env") `, + ) + await queryRunner.query( + `CREATE TABLE "box_last_activity" ("boxId" character varying NOT NULL, "lastActivityAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "box_last_activity_boxId_pk" PRIMARY KEY ("boxId"))`, + ) + await queryRunner.query(`CREATE TYPE "public"."box_class_enum" AS ENUM('small', 'medium', 'large')`) + await queryRunner.query( + `CREATE TYPE "public"."box_state_enum" AS ENUM('creating', 'restoring', 'destroyed', 'destroying', 'started', 'stopped', 'starting', 'stopping', 'error', 'unknown', 'archived', 'archiving', 'resizing')`, + ) + await queryRunner.query( + `CREATE TYPE "public"."box_desiredstate_enum" AS ENUM('destroyed', 'started', 'stopped', 'resized')`, + ) + await queryRunner.query( + `CREATE TABLE "box" ("id" character varying NOT NULL DEFAULT uuid_generate_v4(), "boxId" character varying(12) NOT NULL, "organizationId" uuid NOT NULL, "name" character varying NOT NULL, "region" character varying NOT NULL, "runnerId" uuid, "prevRunnerId" uuid, "class" "public"."box_class_enum" NOT NULL DEFAULT 'small', "state" "public"."box_state_enum" NOT NULL DEFAULT 'unknown', "desiredState" "public"."box_desiredstate_enum" NOT NULL DEFAULT 'started', "osUser" character varying NOT NULL, "errorReason" character varying, "recoverable" boolean NOT NULL DEFAULT false, "env" jsonb NOT NULL DEFAULT '{}', "public" boolean NOT NULL DEFAULT false, "networkBlockAll" boolean NOT NULL DEFAULT false, "networkAllowList" character varying, "labels" jsonb, "cpu" integer NOT NULL DEFAULT '2', "gpu" integer NOT NULL DEFAULT '0', "mem" integer NOT NULL DEFAULT '4', "disk" integer NOT NULL DEFAULT '10', "volumes" jsonb NOT NULL DEFAULT '[]', "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "autoStopInterval" integer NOT NULL DEFAULT '15', "autoDeleteInterval" integer NOT NULL DEFAULT '-1', "pending" boolean NOT NULL DEFAULT false, "authToken" character varying NOT NULL, "daemonVersion" character varying, CONSTRAINT "box_organizationId_name_unique" UNIQUE ("organizationId", "name"), CONSTRAINT "box_id_pk" PRIMARY KEY ("id"))`, + ) + await queryRunner.query(`CREATE INDEX "idx_box_authtoken" ON "box" ("authToken") `) + await queryRunner.query(`CREATE INDEX "box_pending_idx" ON "box" ("id") WHERE "pending" = true`) + await queryRunner.query( + `CREATE INDEX "box_active_only_idx" ON "box" ("id") WHERE "state" <> ALL (ARRAY['destroyed'::box_state_enum, 'archived'::box_state_enum])`, + ) + await queryRunner.query( + `CREATE INDEX "box_runner_state_desired_idx" ON "box" ("runnerId", "state", "desiredState") WHERE "pending" = false`, + ) + await queryRunner.query(`CREATE INDEX "box_resources_idx" ON "box" ("cpu", "mem", "disk", "gpu") `) + await queryRunner.query(`CREATE INDEX "box_region_idx" ON "box" ("region") `) + await queryRunner.query(`CREATE INDEX "box_organizationid_boxid_idx" ON "box" ("organizationId", "boxId") `) + await queryRunner.query(`CREATE INDEX "box_organizationid_idx" ON "box" ("organizationId") `) + await queryRunner.query(`CREATE INDEX "box_runner_state_idx" ON "box" ("runnerId", "state") `) + await queryRunner.query(`CREATE INDEX "box_runnerid_idx" ON "box" ("runnerId") `) + await queryRunner.query(`CREATE INDEX "box_desiredstate_idx" ON "box" ("desiredState") `) + await queryRunner.query(`CREATE INDEX "box_state_idx" ON "box" ("state") `) + await queryRunner.query(`CREATE UNIQUE INDEX "box_boxid_unique_idx" ON "box" ("boxId") `) + await queryRunner.query(`CREATE INDEX "box_labels_gin_full_idx" ON "box" USING gin ("labels" jsonb_path_ops)`) + await queryRunner.query(`CREATE INDEX "idx_box_volumes_gin" ON "box" USING gin ("volumes" jsonb_path_ops)`) + await queryRunner.query( + `CREATE TABLE "ssh_access" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "boxId" character varying NOT NULL, "token" text NOT NULL, "expiresAt" TIMESTAMP NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "ssh_access_id_pk" PRIMARY KEY ("id"))`, + ) + await queryRunner.query(`CREATE TYPE "public"."runner_class_enum" AS ENUM('small', 'medium', 'large')`) + await queryRunner.query( + `CREATE TYPE "public"."runner_state_enum" AS ENUM('initializing', 'ready', 'disabled', 'decommissioned', 'unresponsive')`, + ) + await queryRunner.query( + `CREATE TABLE "runner" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "domain" character varying, "apiUrl" character varying, "proxyUrl" character varying, "apiKey" character varying NOT NULL, "cpu" double precision NOT NULL DEFAULT '0', "memoryGiB" double precision NOT NULL DEFAULT '0', "diskGiB" double precision NOT NULL DEFAULT '0', "gpu" integer, "gpuType" character varying, "class" "public"."runner_class_enum" NOT NULL DEFAULT 'small', "currentCpuLoadAverage" double precision NOT NULL DEFAULT '0', "currentCpuUsagePercentage" double precision NOT NULL DEFAULT '0', "currentMemoryUsagePercentage" double precision NOT NULL DEFAULT '0', "currentDiskUsagePercentage" double precision NOT NULL DEFAULT '0', "currentAllocatedCpu" double precision NOT NULL DEFAULT '0', "currentAllocatedMemoryGiB" double precision NOT NULL DEFAULT '0', "currentAllocatedDiskGiB" double precision NOT NULL DEFAULT '0', "currentStartedBoxes" integer NOT NULL DEFAULT '0', "availabilityScore" integer NOT NULL DEFAULT '0', "region" character varying NOT NULL, "name" character varying NOT NULL, "state" "public"."runner_state_enum" NOT NULL DEFAULT 'initializing', "appVersion" character varying DEFAULT 'v0.0.0-dev', "apiVersion" character varying NOT NULL DEFAULT '0', "lastChecked" TIMESTAMP WITH TIME ZONE, "unschedulable" boolean NOT NULL DEFAULT false, "draining" boolean NOT NULL DEFAULT false, "serviceHealth" jsonb, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "runner_region_name_unique" UNIQUE ("region", "name"), CONSTRAINT "runner_id_pk" PRIMARY KEY ("id"))`, + ) + await queryRunner.query( + `CREATE INDEX "runner_state_unschedulable_region_index" ON "runner" ("state", "unschedulable", "region") `, + ) + await queryRunner.query( + `CREATE TYPE "public"."job_status_enum" AS ENUM('PENDING', 'IN_PROGRESS', 'COMPLETED', 'FAILED')`, + ) + await queryRunner.query(`CREATE TYPE "public"."job_resourcetype_enum" AS ENUM('BOX', 'ARTIFACT', 'BACKUP')`) + await queryRunner.query( + `CREATE TABLE "job" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "version" integer NOT NULL, "type" character varying NOT NULL, "status" "public"."job_status_enum" NOT NULL DEFAULT 'PENDING', "runnerId" character varying NOT NULL, "resourceType" "public"."job_resourcetype_enum" NOT NULL, "resourceId" character varying NOT NULL, "payload" character varying, "resultMetadata" character varying, "traceContext" jsonb, "errorMessage" text, "startedAt" TIMESTAMP WITH TIME ZONE, "completedAt" TIMESTAMP WITH TIME ZONE, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "job_id_pk" PRIMARY KEY ("id"))`, + ) + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_UNIQUE_INCOMPLETE_BACKUP_JOB" ON "job" ("resourceType", "resourceId", "runnerId") WHERE "completedAt" IS NULL AND "type" = 'CREATE_BACKUP'`, + ) + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_UNIQUE_INCOMPLETE_JOB" ON "job" ("resourceType", "resourceId", "runnerId") WHERE "completedAt" IS NULL AND "type" != 'CREATE_BACKUP'`, + ) + await queryRunner.query(`CREATE INDEX "job_resourceType_resourceId_index" ON "job" ("resourceType", "resourceId") `) + await queryRunner.query(`CREATE INDEX "job_status_createdAt_index" ON "job" ("status", "createdAt") `) + await queryRunner.query(`CREATE INDEX "job_runnerId_status_index" ON "job" ("runnerId", "status") `) + await queryRunner.query( + `CREATE TABLE "audit_log" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "actorId" character varying NOT NULL, "actorEmail" character varying NOT NULL DEFAULT '', "organizationId" character varying, "action" character varying NOT NULL, "targetType" character varying, "targetId" character varying, "statusCode" integer, "errorMessage" character varying, "ipAddress" character varying, "userAgent" text, "source" character varying, "metadata" jsonb, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "audit_log_id_pk" PRIMARY KEY ("id"))`, + ) + await queryRunner.query( + `CREATE INDEX "audit_log_organizationId_createdAt_index" ON "audit_log" ("organizationId", "createdAt") `, + ) + await queryRunner.query(`CREATE INDEX "audit_log_createdAt_index" ON "audit_log" ("createdAt") `) + await queryRunner.query( + `CREATE TABLE "organization_role_assignment_invitation" ("invitationId" uuid NOT NULL, "roleId" uuid NOT NULL, CONSTRAINT "organization_role_assignment_invitation_invitationId_roleId_pk" PRIMARY KEY ("invitationId", "roleId"))`, + ) + await queryRunner.query( + `CREATE INDEX "organization_role_assignment_invitation_invitationId_index" ON "organization_role_assignment_invitation" ("invitationId") `, + ) + await queryRunner.query( + `CREATE INDEX "organization_role_assignment_invitation_roleId_index" ON "organization_role_assignment_invitation" ("roleId") `, + ) + await queryRunner.query( + `CREATE TABLE "organization_role_assignment" ("organizationId" uuid NOT NULL, "userId" character varying NOT NULL, "roleId" uuid NOT NULL, CONSTRAINT "organization_role_assignment_organizationId_userId_roleId_pk" PRIMARY KEY ("organizationId", "userId", "roleId"))`, + ) + await queryRunner.query( + `CREATE INDEX "organization_role_assignment_organizationId_userId_index" ON "organization_role_assignment" ("organizationId", "userId") `, + ) + await queryRunner.query( + `CREATE INDEX "organization_role_assignment_roleId_index" ON "organization_role_assignment" ("roleId") `, + ) + await queryRunner.query( + `ALTER TABLE "organization_invitation" ADD CONSTRAINT "organization_invitation_organizationId_fk" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ) + await queryRunner.query( + `ALTER TABLE "organization_role" ADD CONSTRAINT "organization_role_organizationId_fk" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ) + await queryRunner.query( + `ALTER TABLE "organization_user" ADD CONSTRAINT "organization_user_organizationId_fk" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ) + await queryRunner.query( + `ALTER TABLE "box_last_activity" ADD CONSTRAINT "box_last_activity_boxId_fk" FOREIGN KEY ("boxId") REFERENCES "box"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ) + await queryRunner.query( + `ALTER TABLE "ssh_access" ADD CONSTRAINT "ssh_access_boxId_fk" FOREIGN KEY ("boxId") REFERENCES "box"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ) + await queryRunner.query( + `ALTER TABLE "organization_role_assignment_invitation" ADD CONSTRAINT "organization_role_assignment_invitation_invitationId_fk" FOREIGN KEY ("invitationId") REFERENCES "organization_invitation"("id") ON DELETE CASCADE ON UPDATE CASCADE`, + ) + await queryRunner.query( + `ALTER TABLE "organization_role_assignment_invitation" ADD CONSTRAINT "organization_role_assignment_invitation_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "organization_role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ) + await queryRunner.query( + `ALTER TABLE "organization_role_assignment" ADD CONSTRAINT "organization_role_assignment_organizationId_userId_fk" FOREIGN KEY ("organizationId", "userId") REFERENCES "organization_user"("organizationId","userId") ON DELETE CASCADE ON UPDATE CASCADE`, + ) + await queryRunner.query( + `ALTER TABLE "organization_role_assignment" ADD CONSTRAINT "organization_role_assignment_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "organization_role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "organization_role_assignment" DROP CONSTRAINT "organization_role_assignment_roleId_fk"`, + ) + await queryRunner.query( + `ALTER TABLE "organization_role_assignment" DROP CONSTRAINT "organization_role_assignment_organizationId_userId_fk"`, + ) + await queryRunner.query( + `ALTER TABLE "organization_role_assignment_invitation" DROP CONSTRAINT "organization_role_assignment_invitation_roleId_fk"`, + ) + await queryRunner.query( + `ALTER TABLE "organization_role_assignment_invitation" DROP CONSTRAINT "organization_role_assignment_invitation_invitationId_fk"`, + ) + await queryRunner.query(`ALTER TABLE "ssh_access" DROP CONSTRAINT "ssh_access_boxId_fk"`) + await queryRunner.query(`ALTER TABLE "box_last_activity" DROP CONSTRAINT "box_last_activity_boxId_fk"`) + await queryRunner.query(`ALTER TABLE "organization_user" DROP CONSTRAINT "organization_user_organizationId_fk"`) + await queryRunner.query(`ALTER TABLE "organization_role" DROP CONSTRAINT "organization_role_organizationId_fk"`) + await queryRunner.query( + `ALTER TABLE "organization_invitation" DROP CONSTRAINT "organization_invitation_organizationId_fk"`, + ) + await queryRunner.query(`DROP INDEX "public"."organization_role_assignment_roleId_index"`) + await queryRunner.query(`DROP INDEX "public"."organization_role_assignment_organizationId_userId_index"`) + await queryRunner.query(`DROP TABLE "organization_role_assignment"`) + await queryRunner.query(`DROP INDEX "public"."organization_role_assignment_invitation_roleId_index"`) + await queryRunner.query(`DROP INDEX "public"."organization_role_assignment_invitation_invitationId_index"`) + await queryRunner.query(`DROP TABLE "organization_role_assignment_invitation"`) + await queryRunner.query(`DROP INDEX "public"."audit_log_createdAt_index"`) + await queryRunner.query(`DROP INDEX "public"."audit_log_organizationId_createdAt_index"`) + await queryRunner.query(`DROP TABLE "audit_log"`) + await queryRunner.query(`DROP INDEX "public"."job_runnerId_status_index"`) + await queryRunner.query(`DROP INDEX "public"."job_status_createdAt_index"`) + await queryRunner.query(`DROP INDEX "public"."job_resourceType_resourceId_index"`) + await queryRunner.query(`DROP INDEX "public"."IDX_UNIQUE_INCOMPLETE_JOB"`) + await queryRunner.query(`DROP INDEX "public"."IDX_UNIQUE_INCOMPLETE_BACKUP_JOB"`) + await queryRunner.query(`DROP TABLE "job"`) + await queryRunner.query(`DROP TYPE "public"."job_resourcetype_enum"`) + await queryRunner.query(`DROP TYPE "public"."job_status_enum"`) + await queryRunner.query(`DROP INDEX "public"."runner_state_unschedulable_region_index"`) + await queryRunner.query(`DROP TABLE "runner"`) + await queryRunner.query(`DROP TYPE "public"."runner_state_enum"`) + await queryRunner.query(`DROP TYPE "public"."runner_class_enum"`) + await queryRunner.query(`DROP TABLE "ssh_access"`) + await queryRunner.query(`DROP INDEX "public"."idx_box_volumes_gin"`) + await queryRunner.query(`DROP INDEX "public"."box_labels_gin_full_idx"`) + await queryRunner.query(`DROP INDEX "public"."box_boxid_unique_idx"`) + await queryRunner.query(`DROP INDEX "public"."box_state_idx"`) + await queryRunner.query(`DROP INDEX "public"."box_desiredstate_idx"`) + await queryRunner.query(`DROP INDEX "public"."box_runnerid_idx"`) + await queryRunner.query(`DROP INDEX "public"."box_runner_state_idx"`) + await queryRunner.query(`DROP INDEX "public"."box_organizationid_idx"`) + await queryRunner.query(`DROP INDEX "public"."box_organizationid_boxid_idx"`) + await queryRunner.query(`DROP INDEX "public"."box_region_idx"`) + await queryRunner.query(`DROP INDEX "public"."box_resources_idx"`) + await queryRunner.query(`DROP INDEX "public"."box_runner_state_desired_idx"`) + await queryRunner.query(`DROP INDEX "public"."box_active_only_idx"`) + await queryRunner.query(`DROP INDEX "public"."box_pending_idx"`) + await queryRunner.query(`DROP INDEX "public"."idx_box_authtoken"`) + await queryRunner.query(`DROP TABLE "box"`) + await queryRunner.query(`DROP TYPE "public"."box_desiredstate_enum"`) + await queryRunner.query(`DROP TYPE "public"."box_state_enum"`) + await queryRunner.query(`DROP TYPE "public"."box_class_enum"`) + await queryRunner.query(`DROP TABLE "box_last_activity"`) + await queryRunner.query(`DROP INDEX "public"."warm_pool_find_idx"`) + await queryRunner.query(`DROP TABLE "warm_pool"`) + await queryRunner.query(`DROP TYPE "public"."warm_pool_class_enum"`) + await queryRunner.query(`DROP TABLE "volume"`) + await queryRunner.query(`DROP TYPE "public"."volume_state_enum"`) + await queryRunner.query(`DROP TABLE "organization"`) + await queryRunner.query(`DROP INDEX "public"."organization_user_default_user_unique"`) + await queryRunner.query(`DROP TABLE "organization_user"`) + await queryRunner.query(`DROP TYPE "public"."organization_user_role_enum"`) + await queryRunner.query(`DROP TABLE "organization_role"`) + await queryRunner.query(`DROP TYPE "public"."organization_role_permissions_enum"`) + await queryRunner.query(`DROP TABLE "organization_invitation"`) + await queryRunner.query(`DROP TYPE "public"."organization_invitation_status_enum"`) + await queryRunner.query(`DROP TYPE "public"."organization_invitation_role_enum"`) + await queryRunner.query(`DROP INDEX "public"."region_organizationId_name_unique"`) + await queryRunner.query(`DROP INDEX "public"."region_organizationId_null_name_unique"`) + await queryRunner.query(`DROP INDEX "public"."region_proxyApiKeyHash_unique"`) + await queryRunner.query(`DROP INDEX "public"."region_sshGatewayApiKeyHash_unique"`) + await queryRunner.query(`DROP INDEX "public"."idx_region_custom"`) + await queryRunner.query(`DROP TABLE "region"`) + await queryRunner.query(`DROP TYPE "public"."region_regiontype_enum"`) + await queryRunner.query(`DROP TABLE "webhook_initialization"`) + await queryRunner.query(`DROP INDEX "public"."api_key_org_user_idx"`) + await queryRunner.query(`DROP TABLE "api_key"`) + await queryRunner.query(`DROP TYPE "public"."api_key_permissions_enum"`) + await queryRunner.query(`DROP TABLE "user"`) + await queryRunner.query(`DROP TYPE "public"."user_role_enum"`) + } +} diff --git a/apps/api/src/organization/organization.module.ts b/apps/api/src/organization/organization.module.ts index c59218ba4..bf668250b 100644 --- a/apps/api/src/organization/organization.module.ts +++ b/apps/api/src/organization/organization.module.ts @@ -70,11 +70,6 @@ import { EncryptionModule } from '../encryption/encryption.module' ) => new BoxRepository(dataSource, eventEmitter, boxLookupCacheInvalidationService), }, ], - exports: [ - OrganizationService, - OrganizationRoleService, - OrganizationUserService, - OrganizationInvitationService, - ], + exports: [OrganizationService, OrganizationRoleService, OrganizationUserService, OrganizationInvitationService], }) export class OrganizationModule {} diff --git a/apps/api/src/organization/services/default-organization-membership.spec.ts b/apps/api/src/organization/services/default-organization-membership.spec.ts index 8e4990ef2..61327a89a 100644 --- a/apps/api/src/organization/services/default-organization-membership.spec.ts +++ b/apps/api/src/organization/services/default-organization-membership.spec.ts @@ -42,15 +42,18 @@ function createOrganizationService() { }), } + // Argument positions must match OrganizationService's constructor signature + // (organization.service.ts): orgRepo, boxRepo, eventEmitter, configService, + // redisLockProvider, regionRepo, regionService, encryptionService. return new OrganizationService( { manager: {} } as never, {} as never, - {} as never, { emitAsync: jest.fn() } as never, configService as never, {} as never, {} as never, {} as never, + {} as never, ) } diff --git a/apps/dashboard/src/components/Box/CreateBoxSheet.tsx b/apps/dashboard/src/components/Box/CreateBoxSheet.tsx index 2cb3d9d99..31f2ea289 100644 --- a/apps/dashboard/src/components/Box/CreateBoxSheet.tsx +++ b/apps/dashboard/src/components/Box/CreateBoxSheet.tsx @@ -8,6 +8,7 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/ import { Field, FieldError, FieldLabel } from '@/components/ui/field' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { Sheet, SheetContent, @@ -36,6 +37,14 @@ import { ScrollArea } from '../ui/scroll-area' const NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/ const MAX_INTERVAL_MINUTES = 2_147_483_647 +const CURATED_IMAGE_KEYS = ['base', 'python', 'node'] as const +type CuratedImageKey = (typeof CURATED_IMAGE_KEYS)[number] + +const CURATED_IMAGE_OPTIONS: Array<{ key: CuratedImageKey; label: string; description: string }> = [ + { key: 'base', label: 'Base', description: 'Core runtime' }, + { key: 'python', label: 'Python', description: 'Python tooling' }, + { key: 'node', label: 'Node', description: 'Node.js tooling' }, +] const isOptionalIntegerInRange = (value: string | undefined, min: number) => { const trimmedValue = value?.trim() @@ -65,6 +74,7 @@ const formSchema = z.object({ .string() .optional() .refine((val) => !val || NAME_REGEX.test(val), 'Only letters, digits, dots, underscores and dashes are allowed'), + image: z.enum(CURATED_IMAGE_KEYS), autoStopInterval: z .string() .optional() @@ -82,6 +92,7 @@ type FormValues = z.input const defaultValues: FormValues = { name: '', + image: 'base', autoStopInterval: '', autoDeleteInterval: '', cpu: '', @@ -155,11 +166,9 @@ export const CreateBoxSheet = ({ } const hasResourceOverrides = Object.values(resources).some((resource) => resource !== undefined) - // TODO(image-rewrite): the image/template picker was removed with the image/template - // subsystem; box creation no longer selects an image. Rebuild image selection here once - // the new model lands. const box = await createBoxMutation.mutateAsync({ name: value.name?.trim() || undefined, + image: value.image, public: false, networkBlockAll: false, autoStopInterval: parseOptionalInteger(value.autoStopInterval), @@ -248,7 +257,36 @@ export const CreateBoxSheet = ({ }} - {/* TODO(image-rewrite): image/template picker removed with the image/template subsystem; rebuild here. */} + + {(field) => ( + + Image + field.handleChange(value as CuratedImageKey)} + className="grid gap-2 sm:grid-cols-3" + > + {CURATED_IMAGE_OPTIONS.map((option) => ( +
+ + +
+ ))} +
+
+ )} +
Result<(), Box> { )?; let options = BoxOptions { - rootfs: RootfsSpec::Image("boxlite/base".into()), + rootfs: RootfsSpec::Image("base".into()), ..Default::default() }; let box_handle = rt.create(options, Some("sdk-quickstart".into())).await?; diff --git a/apps/dashboard/src/providers/PlaygroundProvider.tsx b/apps/dashboard/src/providers/PlaygroundProvider.tsx index e8b4bc0a4..e5d8316c7 100644 --- a/apps/dashboard/src/providers/PlaygroundProvider.tsx +++ b/apps/dashboard/src/providers/PlaygroundProvider.tsx @@ -28,7 +28,7 @@ import { } from '@/contexts/PlaygroundContext' import { MouseButton, MouseScrollDirection, BoxParametersSections, ScreenshotFormatOption } from '@/enums/Playground' import { getLanguageCodeToRun, objectHasAnyValue } from '@/lib/playground' -import { CreateBoxBaseParams, CreateBoxFromImageParams, CreateBoxFromTemplateParams, Image } from '@boxlite-ai/sdk' +import { CreateBoxBaseParams, CreateBoxFromImageParams, CreateBoxFromTemplateParams } from '@boxlite-ai/sdk' import { useCallback, useState } from 'react' const PARAM_SECTION_MAP: Partial> = { @@ -297,7 +297,7 @@ export const PlaygroundProvider: React.FC<{ children: React.ReactNode }> = ({ ch const useAutoDeleteInterval = createBoxParamsExist && boxParametersState['createBoxBaseParams']['autoDeleteInterval'] !== undefined - const createBoxFromImageParams: CreateBoxFromImageParams = { image: Image.debianSlim('3.13') } // Default and fixed image if CreateBoxFromImageParams are used + const createBoxFromImageParams: CreateBoxFromImageParams = { image: 'base' } const templateName = boxParametersState['templateName'] const useCustomImageName = templateName !== undefined && templateName !== BOX_TEMPLATE_DEFAULT_VALUE // TODO(image-rewrite): templateId param was removed with the image/template subsystem. diff --git a/apps/libs/sdk-typescript/src/BoxLite.ts b/apps/libs/sdk-typescript/src/BoxLite.ts index 51cefeb7f..8ccb4a85a 100644 --- a/apps/libs/sdk-typescript/src/BoxLite.ts +++ b/apps/libs/sdk-typescript/src/BoxLite.ts @@ -17,7 +17,6 @@ import { BoxPythonCodeToolbox } from './code-toolbox/BoxPythonCodeToolbox' import { BoxTsCodeToolbox } from './code-toolbox/BoxTsCodeToolbox' import { BoxJsCodeToolbox } from './code-toolbox/BoxJsCodeToolbox' import { BoxliteError, BoxLiteNotFoundError, BoxLiteRateLimitError } from './errors/BoxliteError' -import { Image } from './Image' import { Box, PaginatedBoxes } from './Box' import { VolumeService } from './Volume' import * as packageJson from '../package.json' @@ -167,16 +166,19 @@ export type CreateBoxBaseParams = { * Parameters for creating a new Box. * * @interface - * @property {string | Image} [image] - Custom Docker image to use for the Box. If an Image object is provided, - * the image will be dynamically built. + * @property {string} [image] - Curated image key to use for the Box. * @property {Resources} [resources] - Resource allocation for the Box. If not provided, box will * have default resources. */ export type CreateBoxFromImageParams = CreateBoxBaseParams & { - image: string | Image + image: string resources?: Resources } +type CreateBoxWithCuratedImage = Parameters[0] & { + image?: string +} + /** * Parameters for creating a new Box. * @@ -376,12 +378,9 @@ export class BoxLite implements AsyncDisposable { */ public async create(params?: CreateBoxFromTemplateParams, options?: { timeout?: number }): Promise /** - * Creates Boxes from specified image available on some registry or declarative BoxLite Image. + * Creates Boxes from a curated image key. * - * @deprecated The API no longer supports image-based box creation (dynamic builds were - * removed). Calling create() with an `image` param throws a BoxliteError. - * - * @param {CreateBoxFromImageParams} [params] - Parameters for Box creation from image + * @param {CreateBoxFromImageParams} [params] - Parameters for Box creation from curated image * @param {object} [options] - Options for the create operation * @param {number} [options.timeout] - Timeout in seconds (0 means no timeout, default is 60) * @returns {Promise} The created Box instance @@ -430,10 +429,10 @@ export class BoxLite implements AsyncDisposable { const codeToolbox = this.getCodeToolbox(params.language as CodeLanguage) - // The API removed image- and template-based creation (boxes use the - // standard runtime). Fail loudly instead of silently ignoring the params. - if ('image' in params) { - throw new BoxliteError('Image-based box creation is no longer supported by the API.') + // The API accepts curated image keys. Declarative Image objects require the + // removed dynamic-build path, so fail before the request instead of dropping them. + if ('image' in params && typeof params.image !== 'string') { + throw new BoxliteError('Declarative Image objects are no longer supported by the API; pass a curated image key.') } if ('templateId' in params && params.templateId !== undefined) { throw new BoxliteError('Box templates were removed from the API; remove the templateId parameter.') @@ -446,23 +445,26 @@ export class BoxLite implements AsyncDisposable { resources = params.resources as Resources | undefined } + const createBoxBody: CreateBoxWithCuratedImage = { + name: params.name, + user: params.user, + env: params.envVars || {}, + labels, + public: params.public, + ...('image' in params ? { image: params.image } : {}), + target: this.target, + cpu: resources?.cpu, + memory: resources?.memory, + disk: resources?.disk, + autoStopInterval: params.autoStopInterval, + autoDeleteInterval: params.autoDeleteInterval, + volumes: params.volumes, + networkBlockAll: params.networkBlockAll, + networkAllowList: params.networkAllowList, + } + const response = await this.boxApi.createBox( - { - name: params.name, - user: params.user, - env: params.envVars || {}, - labels: labels, - public: params.public, - target: this.target, - cpu: resources?.cpu, - memory: resources?.memory, - disk: resources?.disk, - autoStopInterval: params.autoStopInterval, - autoDeleteInterval: params.autoDeleteInterval, - volumes: params.volumes, - networkBlockAll: params.networkBlockAll, - networkAllowList: params.networkAllowList, - }, + createBoxBody, undefined, { timeout: options.timeout * 1000, diff --git a/apps/libs/sdk-typescript/src/__tests__/BoxLite.create-guards.test.ts b/apps/libs/sdk-typescript/src/__tests__/BoxLite.create-guards.test.ts index 37db3a445..9e23ea40c 100644 --- a/apps/libs/sdk-typescript/src/__tests__/BoxLite.create-guards.test.ts +++ b/apps/libs/sdk-typescript/src/__tests__/BoxLite.create-guards.test.ts @@ -5,20 +5,47 @@ import { BoxLite } from '../BoxLite' import { BoxliteError } from '../errors/BoxliteError' -// The API removed image- and template-based box creation; create() must fail -// loudly on those params instead of silently dropping them. Both guards throw -// before any network call, so a dummy config is safe here. -describe('BoxLite.create removed-parameter guards', () => { - const boxlite = new BoxLite({ apiKey: 'test-key', apiUrl: 'http://127.0.0.1:1', target: 'test' }) - - it('rejects image-based creation', async () => { - await expect(boxlite.create({ image: 'debian:12.9' })).rejects.toThrow(BoxliteError) - await expect(boxlite.create({ image: 'debian:12.9' })).rejects.toThrow( - 'Image-based box creation is no longer supported', - ) +const createStartedBoxDto = () => ({ + id: 'box-uuid', + boxId: 'box_public', + name: 'box-name', + organizationId: 'org-uuid', + user: 'boxlite', + env: {}, + labels: {}, + public: false, + target: 'test', + cpu: 2, + gpu: 0, + memory: 4, + disk: 10, + state: 'started', + recoverable: false, + networkBlockAll: false, + toolboxProxyUrl: 'http://127.0.0.1:9999', +}) + +describe('BoxLite.create image and template guards', () => { + it('passes curated image keys to the API', async () => { + const boxlite = new BoxLite({ apiKey: 'test-key', apiUrl: 'http://127.0.0.1:1', target: 'test' }) + const createBox = jest.fn().mockResolvedValue({ data: createStartedBoxDto() }) + ;(boxlite as unknown as { boxApi: { createBox: typeof createBox } }).boxApi.createBox = createBox + + await boxlite.create({ image: 'base' }) + + expect(createBox).toHaveBeenCalledWith(expect.objectContaining({ image: 'base' }), undefined, expect.any(Object)) + }) + + it('rejects declarative Image object creation', async () => { + const boxlite = new BoxLite({ apiKey: 'test-key', apiUrl: 'http://127.0.0.1:1', target: 'test' }) + + await expect(boxlite.create({ image: {} } as any)).rejects.toThrow(BoxliteError) + await expect(boxlite.create({ image: {} } as any)).rejects.toThrow('Declarative Image objects are no longer supported') }) it('rejects templateId-based creation', async () => { + const boxlite = new BoxLite({ apiKey: 'test-key', apiUrl: 'http://127.0.0.1:1', target: 'test' }) + await expect(boxlite.create({ templateId: 'my-template' })).rejects.toThrow(BoxliteError) await expect(boxlite.create({ templateId: 'my-template' })).rejects.toThrow('Box templates were removed') }) diff --git a/apps/runner/pkg/boxlite/daemon_env_test.go b/apps/runner/pkg/boxlite/daemon_env_test.go index f37ff2f92..2f673dd34 100644 --- a/apps/runner/pkg/boxlite/daemon_env_test.go +++ b/apps/runner/pkg/boxlite/daemon_env_test.go @@ -24,7 +24,7 @@ func TestDaemonBoxEnvIncludesRequiredBoxIdentity(t *testing.T) { }) want := map[string]string{ - "BOXLITE_BOX_ID": "box-1", + "BOXLITE_BOX_ID": "box-1", "BOXLITE_ORGANIZATION_ID": "org-1", "BOXLITE_REGION_ID": "region-1", "BOXLITE_OTEL_ENDPOINT": "http://otel.local:4318", diff --git a/apps/runner/pkg/boxlite/toolbox_ports.go b/apps/runner/pkg/boxlite/toolbox_ports.go index a41a322b6..f9634bddb 100644 --- a/apps/runner/pkg/boxlite/toolbox_ports.go +++ b/apps/runner/pkg/boxlite/toolbox_ports.go @@ -23,7 +23,7 @@ const ( ) type toolboxPortRecord struct { - BoxID string `json:"boxId"` + BoxID string `json:"boxId"` GuestPort int `json:"guestPort"` HostPort int `json:"hostPort"` } @@ -42,7 +42,7 @@ func (c *Client) reserveToolboxHostPort(ctx context.Context, boxID string) (int, } record := toolboxPortRecord{ - BoxID: boxID, + BoxID: boxID, GuestPort: ToolboxGuestPort, HostPort: port, } diff --git a/apps/runner/pkg/boxlite/toolbox_ports_test.go b/apps/runner/pkg/boxlite/toolbox_ports_test.go index 2f3418374..8db4f9ccf 100644 --- a/apps/runner/pkg/boxlite/toolbox_ports_test.go +++ b/apps/runner/pkg/boxlite/toolbox_ports_test.go @@ -78,7 +78,7 @@ func TestWaitForToolboxReadyReturnsAfterVersionEndpointResponds(t *testing.T) { toolboxReadyTimeout: time.Second, } if err := client.writeToolboxPortRecord(toolboxPortRecord{ - BoxID: "box-1", + BoxID: "box-1", GuestPort: ToolboxGuestPort, HostPort: port, }); err != nil { @@ -101,7 +101,7 @@ func TestWaitForToolboxReadyTimesOutWhenEndpointDoesNotRespond(t *testing.T) { toolboxReadyTimeout: 20 * time.Millisecond, } if err := client.writeToolboxPortRecord(toolboxPortRecord{ - BoxID: "box-1", + BoxID: "box-1", GuestPort: ToolboxGuestPort, HostPort: port, }); err != nil { diff --git a/apps/runner/pkg/models/enums/box_state.go b/apps/runner/pkg/models/enums/box_state.go index 2bcca344d..f4cd8b1c7 100644 --- a/apps/runner/pkg/models/enums/box_state.go +++ b/apps/runner/pkg/models/enums/box_state.go @@ -7,17 +7,17 @@ package enums type BoxState string const ( - BoxStateCreating BoxState = "creating" - BoxStateRestoring BoxState = "restoring" - BoxStateDestroyed BoxState = "destroyed" - BoxStateDestroying BoxState = "destroying" - BoxStateStarted BoxState = "started" - BoxStateStopped BoxState = "stopped" - BoxStateStarting BoxState = "starting" - BoxStateStopping BoxState = "stopping" - BoxStateResizing BoxState = "resizing" - BoxStateError BoxState = "error" - BoxStateUnknown BoxState = "unknown" + BoxStateCreating BoxState = "creating" + BoxStateRestoring BoxState = "restoring" + BoxStateDestroyed BoxState = "destroyed" + BoxStateDestroying BoxState = "destroying" + BoxStateStarted BoxState = "started" + BoxStateStopped BoxState = "stopped" + BoxStateStarting BoxState = "starting" + BoxStateStopping BoxState = "stopping" + BoxStateResizing BoxState = "resizing" + BoxStateError BoxState = "error" + BoxStateUnknown BoxState = "unknown" ) func (s BoxState) String() string { diff --git a/openapi/box.openapi.yaml b/openapi/box.openapi.yaml index 60f8cd554..aabb0a79a 100644 --- a/openapi/box.openapi.yaml +++ b/openapi/box.openapi.yaml @@ -1524,7 +1524,11 @@ components: example: dev-box image: type: string - description: OCI image reference + description: | + OCI image reference. + Hosted multi-tenant deployments restrict this to a curated set of + image keys (`base`, `python`, `node`; default `base`) and reject + other values with 400. default: "alpine:latest" example: python:3.11-slim rootfs_path: diff --git a/scripts/test/e2e/README.md b/scripts/test/e2e/README.md index 115b65936..67a5d437e 100644 --- a/scripts/test/e2e/README.md +++ b/scripts/test/e2e/README.md @@ -64,11 +64,13 @@ python3 scripts/test/e2e/fixture_setup.py This: -- Registers `alpine:3.23` snapshot via the API admin endpoint -- Waits for the snapshot to reach `active` state (runner pulls + pushes to local registry) - Sets reasonable per-box quotas on the admin org - Adds a `[profiles.p1]` entry in `~/.boxlite/credentials.toml` pointing at the local API +Box images need no registration: tests pass a curated image key (`base` | +`python` | `node`), which the API resolves through `BOXLITE_SYSTEM_*_IMAGE` +env vars — bootstrap.sh points them at public refs for the local stack. + ## Running ```bash diff --git a/scripts/test/e2e/bootstrap.sh b/scripts/test/e2e/bootstrap.sh index 1b9ac21ba..d64b53258 100755 --- a/scripts/test/e2e/bootstrap.sh +++ b/scripts/test/e2e/bootstrap.sh @@ -143,6 +143,11 @@ RUN_MIGRATIONS=true VERSION=0.1.0 DEFAULT_REGION_ENFORCE_QUOTAS=false DEFAULT_SNAPSHOT=ubuntu:22.04 +# Curated image keys resolve to public refs on the local stack so the runner +# can pull without private-registry credentials (prod resolves to pinned ghcr). +BOXLITE_SYSTEM_BASE_IMAGE=alpine:3.23 +BOXLITE_SYSTEM_PYTHON_IMAGE=python:3.11-alpine +BOXLITE_SYSTEM_NODE_IMAGE=node:18-alpine DB_HOST=localhost DB_PORT=5432 DB_USERNAME=boxlite diff --git a/scripts/test/e2e/cases/conftest.py b/scripts/test/e2e/cases/conftest.py index c96f041d0..e145c10ed 100644 --- a/scripts/test/e2e/cases/conftest.py +++ b/scripts/test/e2e/cases/conftest.py @@ -27,7 +27,8 @@ from path_verification import runner_journal_seek, runner_hits_for_box DEFAULT_PROFILE = os.environ.get("BOXLITE_E2E_PROFILE", "p1") -DEFAULT_IMAGE = os.environ.get("BOXLITE_E2E_IMAGE", "alpine:3.23") +DEFAULT_IMAGE = os.environ.get("BOXLITE_E2E_IMAGE", "base") +SKIP_PATH_VERIFY = os.environ.get("BOXLITE_E2E_SKIP_PATH_VERIFY") == "1" CRED_PATH = Path.home() / ".boxlite" / "credentials.toml" @@ -67,10 +68,33 @@ async def create(self, *args, **kwargs): pass # never mask the real return return box + async def remove(self, box_id, *args, **kwargs): + """Retry transient lifecycle races from teardown cleanup paths.""" + last_error = None + for attempt in range(1, 9): + try: + return await self._inner.remove(box_id, *args, **kwargs) + except Exception as exc: + last_error = exc + if not _is_transient_remove_error(exc) or attempt == 8: + raise + await asyncio.sleep(min(0.5 * attempt, 2.0)) + raise last_error + def __getattr__(self, name): return getattr(self._inner, name) +def _is_transient_remove_error(exc: Exception) -> bool: + msg = str(exc).lower() + return ( + "state change in progress" in msg + or "transition" in msg + or "stopping" in msg + or "starting" in msg + ) + + @pytest_asyncio.fixture(scope="session") async def rt(): """REST-mode Boxlite runtime against the local API, wrapped in a @@ -106,8 +130,12 @@ async def verify_runner_saw_all_boxes(rt): chain (e.g. degraded to local FFI, or the runner-side journal write broke). Tests that don't create any boxes are unaffected. """ - since = runner_journal_seek() object.__setattr__(rt, "_created", []) + if SKIP_PATH_VERIFY: + yield + return + + since = runner_journal_seek() yield diff --git a/scripts/test/e2e/cases/test_box_management.py b/scripts/test/e2e/cases/test_box_management.py index da49bb7c3..3a540ea12 100644 --- a/scripts/test/e2e/cases/test_box_management.py +++ b/scripts/test/e2e/cases/test_box_management.py @@ -7,15 +7,23 @@ """ from __future__ import annotations +import os +import uuid + import boxlite import pytest +def _unique_name() -> str: + run_id = os.environ.get("GITHUB_RUN_ID", "local") + return f"e2e-test-box-{run_id}-{uuid.uuid4().hex[:8]}" + + @pytest.mark.asyncio async def test_create_named_box(rt, image): """Box created with an explicit name carries it through to get_info.""" - name = "e2e-test-box" + name = _unique_name() box = await rt.create( boxlite.BoxOptions(image=image, auto_remove=True), name=name, ) diff --git a/scripts/test/e2e/cases/test_c_entry.py b/scripts/test/e2e/cases/test_c_entry.py index 4216cdf20..e51a73110 100644 --- a/scripts/test/e2e/cases/test_c_entry.py +++ b/scripts/test/e2e/cases/test_c_entry.py @@ -73,7 +73,7 @@ def test_c_sdk_create_remove(c_binary): "BOXLITE_E2E_URL": p["url"], "BOXLITE_E2E_API_KEY": p["api_key"], "BOXLITE_E2E_PREFIX": p.get("path_prefix") or "", - "BOXLITE_E2E_IMAGE": "alpine:3.23", + "BOXLITE_E2E_IMAGE": "base", "LD_LIBRARY_PATH": str(LIB_DIR), } r = subprocess.run( diff --git a/scripts/test/e2e/cases/test_cli_detach_recovery.py b/scripts/test/e2e/cases/test_cli_detach_recovery.py index 8dc71a684..dae6c9927 100644 --- a/scripts/test/e2e/cases/test_cli_detach_recovery.py +++ b/scripts/test/e2e/cases/test_cli_detach_recovery.py @@ -37,7 +37,7 @@ from path_verification import runner_journal_seek, runner_hits_for_box BOXLITE_BIN = os.environ.get("BOXLITE_E2E_CLI", shutil.which("boxlite")) -IMAGE = os.environ.get("BOXLITE_E2E_IMAGE", "alpine:3.23") +IMAGE = os.environ.get("BOXLITE_E2E_IMAGE", "base") UUID_RE = re.compile( r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" ) diff --git a/scripts/test/e2e/cases/test_cli_entry.py b/scripts/test/e2e/cases/test_cli_entry.py index 7763e859b..0173ddd6a 100644 --- a/scripts/test/e2e/cases/test_cli_entry.py +++ b/scripts/test/e2e/cases/test_cli_entry.py @@ -29,7 +29,7 @@ from path_verification import runner_journal_seek, runner_hits_for_box BOXLITE_BIN = os.environ.get("BOXLITE_E2E_CLI", shutil.which("boxlite")) -IMAGE = os.environ.get("BOXLITE_E2E_IMAGE", "alpine:3.23") +IMAGE = os.environ.get("BOXLITE_E2E_IMAGE", "base") UUID_RE = re.compile( r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" ) diff --git a/scripts/test/e2e/cases/test_error_code_mapping.py b/scripts/test/e2e/cases/test_error_code_mapping.py index 43ed30717..ce9636dc6 100644 --- a/scripts/test/e2e/cases/test_error_code_mapping.py +++ b/scripts/test/e2e/cases/test_error_code_mapping.py @@ -113,7 +113,7 @@ async def test_invalid_argument_zero_cpu_returns_400(rt): status, body = _api_call( "POST", f"/v1/{p['path_prefix']}/boxes", - {"image": "alpine:3.23", "cpus": 0, "memory_mib": 256, "disk_size_gb": 4}, + {"image": "base", "cpus": 0, "memory_mib": 256, "disk_size_gb": 4}, ) _assert_http_code( status, @@ -141,7 +141,7 @@ async def test_invalid_argument_negative_memory_returns_400(rt): status, body = _api_call( "POST", f"/v1/{p['path_prefix']}/boxes", - {"image": "alpine:3.23", "cpus": 1, "memory_mib": -1, "disk_size_gb": 4}, + {"image": "base", "cpus": 1, "memory_mib": -1, "disk_size_gb": 4}, ) _assert_http_code( status, @@ -254,7 +254,7 @@ async def test_resource_exhausted_over_cpu_quota_returns_429(rt): status, body = _api_call( "POST", f"/v1/{p['path_prefix']}/boxes", - {"image": "alpine:3.23", "cpus": 999, "memory_mib": 256, "disk_size_gb": 4}, + {"image": "base", "cpus": 999, "memory_mib": 256, "disk_size_gb": 4}, ) # The mapping says 429 ResourceExhausted; some implementations may also # 400 InvalidArgument (treating it as a parse-time validation failure). diff --git a/scripts/test/e2e/cases/test_go_entry.py b/scripts/test/e2e/cases/test_go_entry.py index e3fc933ab..935395923 100644 --- a/scripts/test/e2e/cases/test_go_entry.py +++ b/scripts/test/e2e/cases/test_go_entry.py @@ -61,7 +61,7 @@ def test_go_sdk_create_exec_remove(go_binary): "BOXLITE_E2E_URL": p["url"], "BOXLITE_E2E_API_KEY": p["api_key"], "BOXLITE_E2E_PREFIX": p.get("path_prefix") or "", - "BOXLITE_E2E_IMAGE": "alpine:3.23", + "BOXLITE_E2E_IMAGE": "base", # CGO dev tag — uses libboxlite.so from the workspace target/release, # not a vendored prebuilt one. "LD_LIBRARY_PATH": str(REPO / "target/release"), diff --git a/scripts/test/e2e/cases/test_node_entry.py b/scripts/test/e2e/cases/test_node_entry.py index c00e6270b..3e34134c4 100644 --- a/scripts/test/e2e/cases/test_node_entry.py +++ b/scripts/test/e2e/cases/test_node_entry.py @@ -68,7 +68,7 @@ def test_node_sdk_create_exec_remove(node_runner): "BOXLITE_E2E_URL": p["url"], "BOXLITE_E2E_API_KEY": p["api_key"], "BOXLITE_E2E_PREFIX": p.get("path_prefix") or "", - "BOXLITE_E2E_IMAGE": "alpine:3.23", + "BOXLITE_E2E_IMAGE": "base", } # Use npx tsx to run the .ts directly without a separate compile step. # tsx is bundled with the apps workspace. diff --git a/scripts/test/e2e/cases/test_path_verification.py b/scripts/test/e2e/cases/test_path_verification.py index 9ffc10d3d..65e85c435 100644 --- a/scripts/test/e2e/cases/test_path_verification.py +++ b/scripts/test/e2e/cases/test_path_verification.py @@ -18,6 +18,7 @@ """ from __future__ import annotations +import os import sys from pathlib import Path @@ -28,6 +29,11 @@ from path_verification import runner_journal_seek, runner_hits_for_box from conftest import drain +pytestmark = pytest.mark.skipif( + os.environ.get("BOXLITE_E2E_SKIP_PATH_VERIFY") == "1", + reason="cloud e2e can run against a remote API/runner without local journalctl or :3000", +) + @pytest.mark.asyncio async def test_sdk_runtime_is_rest_against_local_api(rt): diff --git a/scripts/test/e2e/cases/test_quota_enforcement.py b/scripts/test/e2e/cases/test_quota_enforcement.py index 1a1dbaefe..d9ba18a95 100644 --- a/scripts/test/e2e/cases/test_quota_enforcement.py +++ b/scripts/test/e2e/cases/test_quota_enforcement.py @@ -13,10 +13,12 @@ treats it as a validation error). 500 means the runner accepted a doomed job and crashed it later; that's the bug class this case covers. -ALL cases in this file currently XFAIL — see module-level pytestmark. +Over-quota cases currently XFAIL while the API lacks max quota enforcement. +Lower-bound validation such as cpus=0 is active because CreateBoxDto now +rejects undersized resources before a box is created. """ -# Production bug pinned by every case in this file: API silently clamps +# Production bug pinned by the over-quota cases in this file: API silently clamps # out-of-range / over-quota resource values to org defaults instead of # rejecting at the boundary. Root cause at # apps/api/src/boxlite-rest/dto/create-box.dto.ts:24 (@Min present, no @Max, @@ -24,7 +26,8 @@ # (createFromSnapshot doesn't consult max_*_per_box columns even though # fixture_setup.py:107-126 sets them). # -# Two-sided requires API-side fix; tests pin the bug, NOT the test code. +# Two-sided requires API-side fix for the over-quota paths; the lower-bound +# path is already fixed by DTO validation and should stay active. from __future__ import annotations @@ -37,7 +40,7 @@ import pytest -pytestmark = pytest.mark.xfail( +over_quota_xfail = pytest.mark.xfail( strict=True, reason=( "Production bug: API silently clamps out-of-range / over-quota " @@ -90,22 +93,24 @@ def _delete_box(box_id: str) -> None: pass +@over_quota_xfail @pytest.mark.asyncio async def test_cpus_above_per_box_limit_returns_4xx(): """cpus far above max_cpu_per_box (4) → 429 or 400, not 5xx.""" status, body = _post_box( - {"image": "alpine:3.23", "cpus": 999, "memory_mib": 256, "disk_size_gb": 4} + {"image": "base", "cpus": 999, "memory_mib": 256, "disk_size_gb": 4} ) body_str = json.dumps(body) if body else "" assert 400 <= status < 500, f"cpus=999 leaked HTTP {status}: {body_str}" +@over_quota_xfail @pytest.mark.asyncio async def test_memory_above_per_box_limit_returns_4xx(): """memory far above max_memory_per_box (8 GiB) → 4xx, not 5xx.""" status, body = _post_box( { - "image": "alpine:3.23", + "image": "base", "cpus": 1, "memory_mib": 8_192_000_000, "disk_size_gb": 4, @@ -115,12 +120,13 @@ async def test_memory_above_per_box_limit_returns_4xx(): assert 400 <= status < 500, f"memory=99999 leaked HTTP {status}: {body_str}" +@over_quota_xfail @pytest.mark.asyncio async def test_disk_above_per_box_limit_returns_4xx(): """disk far above max_disk_per_box (20 GiB) → 4xx, not 5xx.""" status, body = _post_box( { - "image": "alpine:3.23", + "image": "base", "cpus": 1, "memory_mib": 256, "disk_size_gb": 99_999_999, @@ -130,13 +136,14 @@ async def test_disk_above_per_box_limit_returns_4xx(): assert 400 <= status < 500, f"disk=99999999 leaked HTTP {status}: {body_str}" +@over_quota_xfail @pytest.mark.asyncio async def test_quota_violation_does_not_silently_create_box(rt): """A 4xx quota response must NOT have created a box. If we list immediately and find an orphan with cpus=999, the runner accepted the doomed request and the quota check is decorative.""" status, body = _post_box( - {"image": "alpine:3.23", "cpus": 999, "memory_mib": 256, "disk_size_gb": 4} + {"image": "base", "cpus": 999, "memory_mib": 256, "disk_size_gb": 4} ) if 200 <= status < 300: pytest.fail(f"cpus=999 unexpectedly succeeded: HTTP {status}, body={body}") @@ -158,7 +165,7 @@ async def test_quota_zero_cpus_returns_4xx(): """cpus=0 — boundary at the other end. Must be 4xx, not 500 or a box that immediately crashes.""" status, body = _post_box( - {"image": "alpine:3.23", "cpus": 0, "memory_mib": 256, "disk_size_gb": 4} + {"image": "base", "cpus": 0, "memory_mib": 256, "disk_size_gb": 4} ) body_str = json.dumps(body) if body else "" assert 400 <= status < 500, f"cpus=0 leaked HTTP {status}: {body_str}" diff --git a/scripts/test/e2e/fixture_setup.py b/scripts/test/e2e/fixture_setup.py index e477d69ae..b2faf64d6 100755 --- a/scripts/test/e2e/fixture_setup.py +++ b/scripts/test/e2e/fixture_setup.py @@ -4,16 +4,19 @@ Idempotent. Safe to re-run after bootstrap.sh. Configures: - 1. Required snapshots active (alpine:3.23, ubuntu:22.04) - 2. Admin org has non-zero per-box quotas - 3. `[profiles.p1]` in ~/.boxlite/credentials.toml points at the local API + 1. Admin org has non-zero per-box quotas + 2. `[profiles.p1]` in ~/.boxlite/credentials.toml points at the local API + +Box images need no registration: create requests carry a curated image key +(base | python | node) that the API resolves via BOXLITE_SYSTEM_*_IMAGE env +(bootstrap.sh points them at public refs for the local stack). """ from __future__ import annotations import json import os import sys -import time +import uuid import urllib.request import urllib.error from pathlib import Path @@ -43,8 +46,6 @@ def _read_admin_key_from_secrets() -> str | None: or _read_admin_key_from_secrets() or "devkey" # only used when bootstrap hasn't run yet ) -SNAPSHOTS_TO_REGISTER = ["alpine:3.23", "ubuntu:22.04", "ubuntu:24.04"] -SNAPSHOT_WAIT_SECONDS = 180 CRED_PATH = Path.home() / ".boxlite" / "credentials.toml" @@ -72,71 +73,31 @@ def me() -> dict: return body -def _snapshot_state(name: str) -> tuple[str | None, str | None]: - """Read snapshot state directly from Postgres — the API's snapshot GET - routes are scoped behind a different controller surface, but the - fixture lives next to the DB anyway, so use the canonical source.""" - import subprocess - sql = f"SELECT state, \"errorReason\" FROM snapshot WHERE name = '{name}' LIMIT 1;" - r = subprocess.run( - ["psql", "-h", "localhost", "-U", "boxlite", "-d", "boxlite_dev", - "-tAF", "|", "-c", sql], - env={**os.environ, "PGPASSWORD": "boxlite"}, - capture_output=True, text=True, - ) - line = r.stdout.strip() - if not line: - return None, None - parts = line.split("|", 1) - return parts[0], (parts[1] if len(parts) > 1 else None) - - -def register_snapshot(name: str): - """POST /snapshots if missing; waits (polling DB) until state == active.""" - state, _ = _snapshot_state(name) - if state is None: - status, body = http("POST", "/snapshots", { - "name": name, "imageName": name, - "cpu": 1, "memory": 1, "disk": 2, - }) - if status not in (200, 201): - sys.exit(f" POST /snapshots → {status} {body}") - print(f" created {name} — waiting for runner pull") - elif state == "error": - # Wipe + recreate so the runner retries (e.g. registry was down before). - import subprocess - subprocess.run( - ["psql", "-h", "localhost", "-U", "boxlite", "-d", "boxlite_dev", - "-c", f"DELETE FROM snapshot WHERE name = '{name}';"], - env={**os.environ, "PGPASSWORD": "boxlite"}, check=True, - ) - return register_snapshot(name) - else: - print(f" {name}: state = {state} (existing)") - - deadline = time.time() + SNAPSHOT_WAIT_SECONDS - while time.time() < deadline: - st, err = _snapshot_state(name) - if st == "active": - print(f" {name}: active ✓") - return - if st == "error": - sys.exit(f" {name}: {err}") - time.sleep(3) - sys.exit(f" {name} did not reach active within {SNAPSHOT_WAIT_SECONDS}s") - - -def patch_admin_quota(): +def _require_org_id(path_prefix: str | None) -> str: + if not path_prefix: + sys.exit("GET /v1/me returned no path_prefix; cannot patch org quota") + try: + return str(uuid.UUID(path_prefix)) + except ValueError: + sys.exit(f"GET /v1/me returned non-UUID path_prefix: {path_prefix!r}") + + +def patch_admin_quota(path_prefix: str | None): """The admin user is created on first API boot with org quotas at 0 (config defaults are 0 unless ADMIN_* env vars override). Bump them so the box CREATE path doesn't 403 in tests.""" import subprocess - sql = """ -UPDATE organization SET + org_id = _require_org_id(path_prefix) + sql = f""" +WITH updated AS ( + UPDATE organization SET max_cpu_per_box = 4, max_memory_per_box = 8, max_disk_per_box = 20 -WHERE personal = true; + WHERE id = '{org_id}'::uuid + RETURNING id +) +SELECT count(*) FROM updated; """ r = subprocess.run( ["psql", "-h", "localhost", "-U", "boxlite", "-d", "boxlite_dev", @@ -146,7 +107,9 @@ def patch_admin_quota(): ) if r.returncode != 0: sys.exit(f"quota patch failed: {r.stderr}") - print(" admin org quota: ok") + if r.stdout.strip() != "1": + sys.exit(f"quota patch failed: org {org_id} was not updated") + print(f" admin org quota: ok ({org_id})") def ensure_p1_profile(prefix: str): @@ -194,19 +157,15 @@ def ensure_p1_profile(prefix: str): def main(): print(f"API_URL={API_URL}") print() - print("1. Bumping admin org quota...") - patch_admin_quota() - print() - print("2. Querying /v1/me for prefix...") + print("1. Querying /v1/me for prefix...") info = me() prefix = info["path_prefix"] print(f" prefix = {prefix}") print() - print("3. Registering snapshots...") - for snap in SNAPSHOTS_TO_REGISTER: - register_snapshot(snap) + print("2. Bumping admin org quota...") + patch_admin_quota(prefix) print() - print("4. Writing ~/.boxlite/credentials.toml profile p1...") + print("3. Writing ~/.boxlite/credentials.toml profile p1...") ensure_p1_profile(prefix) print() print("fixture_setup: done.") diff --git a/scripts/test/e2e/sdks/c/e2e_basic.c b/scripts/test/e2e/sdks/c/e2e_basic.c index c8257797a..3bb49156f 100644 --- a/scripts/test/e2e/sdks/c/e2e_basic.c +++ b/scripts/test/e2e/sdks/c/e2e_basic.c @@ -79,7 +79,7 @@ int main(void) { const char* url = env_or("BOXLITE_E2E_URL", "http://localhost:3000/api"); const char* api_key = env_or("BOXLITE_E2E_API_KEY", "devkey"); const char* prefix = env_or("BOXLITE_E2E_PREFIX", ""); - const char* image = env_or("BOXLITE_E2E_IMAGE", "alpine:3.23"); + const char* image = env_or("BOXLITE_E2E_IMAGE", "base"); CBoxliteError err = {0}; diff --git a/scripts/test/e2e/sdks/go/e2e_basic.go b/scripts/test/e2e/sdks/go/e2e_basic.go index 429bf3424..6e415b872 100644 --- a/scripts/test/e2e/sdks/go/e2e_basic.go +++ b/scripts/test/e2e/sdks/go/e2e_basic.go @@ -32,7 +32,7 @@ func main() { url := env("BOXLITE_E2E_URL", "http://localhost:3000/api") apiKey := env("BOXLITE_E2E_API_KEY", "devkey") prefix := env("BOXLITE_E2E_PREFIX", "") - image := env("BOXLITE_E2E_IMAGE", "alpine:3.23") + image := env("BOXLITE_E2E_IMAGE", "base") rt, err := boxlite.NewRest(boxlite.BoxliteRestOptions{ URL: url, diff --git a/scripts/test/e2e/sdks/node/e2e_basic.ts b/scripts/test/e2e/sdks/node/e2e_basic.ts index 03e5db9b0..7930bdf9b 100644 --- a/scripts/test/e2e/sdks/node/e2e_basic.ts +++ b/scripts/test/e2e/sdks/node/e2e_basic.ts @@ -26,7 +26,7 @@ function die(msg: string): never { const url = env('BOXLITE_E2E_URL', 'http://localhost:3000/api'); const apiKey = env('BOXLITE_E2E_API_KEY', 'devkey'); const prefix = env('BOXLITE_E2E_PREFIX', ''); - const image = env('BOXLITE_E2E_IMAGE', 'alpine:3.23'); + const image = env('BOXLITE_E2E_IMAGE', 'base'); const rt = JsBoxlite.rest(new BoxliteRestOptions({ url, diff --git a/sdks/go/options.go b/sdks/go/options.go index 59e26667f..a7d2eef56 100644 --- a/sdks/go/options.go +++ b/sdks/go/options.go @@ -90,6 +90,7 @@ type boxConfig struct { rootfsPath string env [][2]string volumes []volumeEntry + ports []portEntry workDir string entrypoint []string cmd []string @@ -105,6 +106,11 @@ type volumeEntry struct { readOnly bool } +type portEntry struct { + guestPort int + hostPort int +} + // WithName sets a human-readable name for the box. func WithName(name string) BoxOption { return func(c *boxConfig) { c.name = name } @@ -161,6 +167,14 @@ func WithVolumeReadOnly(hostPath, containerPath string) BoxOption { } } +// WithPort forwards a guest port to a host port. A hostPort of 0 lets the +// runtime assign one dynamically. +func WithPort(guestPort, hostPort int) BoxOption { + return func(c *boxConfig) { + c.ports = append(c.ports, portEntry{guestPort, hostPort}) + } +} + // WithWorkDir sets the working directory inside the container. func WithWorkDir(dir string) BoxOption { return func(c *boxConfig) { c.workDir = dir } @@ -270,6 +284,9 @@ func buildCOptions(image string, cfg *boxConfig) (*C.CBoxliteOptions, error) { C.free(unsafe.Pointer(cHost)) C.free(unsafe.Pointer(cGuest)) } + for _, port := range cfg.ports { + C.boxlite_options_add_port(cOpts, C.int(port.guestPort), C.int(port.hostPort)) + } if cfg.network != nil { switch cfg.network.Mode { case "", NetworkModeEnabled: diff --git a/src/boxlite/tests/health_check.rs b/src/boxlite/tests/health_check.rs index 9414494cb..5720586aa 100644 --- a/src/boxlite/tests/health_check.rs +++ b/src/boxlite/tests/health_check.rs @@ -149,4 +149,6 @@ async fn health_check_becomes_unhealthy_when_shim_killed() { health_status.state, health_status.failures ); + + let _ = t.runtime.remove(box_id.as_str(), true).await; } diff --git a/src/boxlite/tests/jailer.rs b/src/boxlite/tests/jailer.rs index 2a8adf389..6fe84cfdb 100644 --- a/src/boxlite/tests/jailer.rs +++ b/src/boxlite/tests/jailer.rs @@ -26,6 +26,17 @@ fn jailer_test_home_base_dir() -> PathBuf { .join(".boxlite-it") } +#[cfg(target_os = "linux")] +fn skip_linux_jailer_vm_test_in_ci() -> bool { + std::env::var_os("CI").is_some() + && std::env::var_os("BOXLITE_CI_ENABLE_LINUX_JAILER_TESTS").is_none() +} + +#[cfg(not(target_os = "linux"))] +fn skip_linux_jailer_vm_test_in_ci() -> bool { + false +} + #[cfg(target_os = "macos")] fn assert_macos_socket_path_budget(home_dir: &std::path::Path) { let probe = home_dir @@ -215,6 +226,13 @@ fn standard_mode_enables_jailer() { /// Box with jailer enabled starts and executes commands successfully. #[tokio::test] async fn jailer_enabled_box_starts_and_executes() { + if skip_linux_jailer_vm_test_in_ci() { + eprintln!( + "Skipping Linux jailer VM test in CI; set BOXLITE_CI_ENABLE_LINUX_JAILER_TESTS=1 to run it." + ); + return; + } + let jh = JailerHome::new(); let t = BoxTestBase::with_home(jh.home, jailer_enabled_options()).await; t.bx.start().await.unwrap(); @@ -335,6 +353,13 @@ async fn jailer_disabled_with_same_profile_still_starts() { #[cfg(target_os = "linux")] #[tokio::test] async fn jailer_creates_isolated_mount_namespace() { + if skip_linux_jailer_vm_test_in_ci() { + eprintln!( + "Skipping Linux jailer VM test in CI; set BOXLITE_CI_ENABLE_LINUX_JAILER_TESTS=1 to run it." + ); + return; + } + let jh = JailerHome::new(); let t = BoxTestBase::with_home(jh.home, jailer_enabled_options()).await; t.bx.start().await.unwrap(); diff --git a/src/cli/tests/create.rs b/src/cli/tests/create.rs index c7ef9346d..79eb4c0e3 100644 --- a/src/cli/tests/create.rs +++ b/src/cli/tests/create.rs @@ -1,7 +1,17 @@ use predicates::prelude::*; +use std::net::TcpListener; mod common; +fn reserve_host_port() -> (TcpListener, u16) { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind ephemeral host port"); + let port = listener + .local_addr() + .expect("read ephemeral host port") + .port(); + (listener, port) +} + #[test] fn test_create_basic() { let mut ctx = common::boxlite(); @@ -70,16 +80,19 @@ fn test_create_resources() { fn test_create_with_publish_success() { let mut ctx = common::boxlite(); let name = "create-publish"; - + let (port_guard, host_port) = reserve_host_port(); + let publish = format!("{host_port}:9000"); + + ctx.cmd.args([ + "create", + "--name", + name, + "-p", + publish.as_str(), + "alpine:latest", + ]); + drop(port_guard); ctx.cmd - .args([ - "create", - "--name", - name, - "-p", - "19000:9000", - "alpine:latest", - ]) .assert() .success() .stdout(predicate::str::is_match(r"^[0-9A-Za-z]{12}\n$").unwrap()); diff --git a/src/cli/tests/run.rs b/src/cli/tests/run.rs index fe59a07f0..c102b1cef 100644 --- a/src/cli/tests/run.rs +++ b/src/cli/tests/run.rs @@ -1,7 +1,17 @@ use predicates::prelude::*; +use std::net::TcpListener; mod common; +fn reserve_host_port() -> (TcpListener, u16) { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind ephemeral host port"); + let port = listener + .local_addr() + .expect("read ephemeral host port") + .port(); + (listener, port) +} + // ============================================================================ // Exit Code Tests // ============================================================================ @@ -465,46 +475,55 @@ fn test_run_rm_cleanup() { #[test] fn test_run_with_publish_success() { let mut ctx = common::boxlite(); + let (port_guard, host_port) = reserve_host_port(); + let publish = format!("{host_port}:18789"); ctx.cmd.args([ "run", "--rm", "-p", - "18789:18789", + publish.as_str(), "alpine:latest", "echo", "ok", ]); + drop(port_guard); ctx.cmd.assert().success().stdout("ok\n"); } #[test] fn test_run_with_publish_short_flag() { let mut ctx = common::boxlite(); + let (port_guard, host_port) = reserve_host_port(); + let publish = format!("{host_port}:80"); ctx.cmd.args([ "run", "--rm", "-p", - "8080:80", + publish.as_str(), "alpine:latest", "sh", "-c", "echo done", ]); + drop(port_guard); ctx.cmd.assert().success().stdout("done\n"); } #[test] fn test_run_with_publish_tcp_suffix() { let mut ctx = common::boxlite(); + let (port_guard, host_port) = reserve_host_port(); + let publish = format!("{host_port}:9000/tcp"); ctx.cmd.args([ "run", "--rm", "--publish", - "9000:9000/tcp", + publish.as_str(), "alpine:latest", "echo", "tcp", ]); + drop(port_guard); ctx.cmd.assert().success().stdout("tcp\n"); }