From 2414deb0697612c7c302d9fa4b478cebf6721723 Mon Sep 17 00:00:00 2001 From: Cheese Date: Mon, 10 Aug 2026 20:53:06 +0800 Subject: [PATCH] refactor: rename tdc command line to ti --- .github/ISSUE_TEMPLATE/bug_report.yml | 10 +- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yml | 4 +- .github/workflows/live-e2e.yml | 46 +- .gitignore | 2 +- .goreleaser.yaml | 26 +- AGENTS.md | 917 +++++++++--------- Makefile | 24 +- README.md | 268 ++--- cmd/tdc/main.go | 24 - .../main.go | 2 +- .../main.go | 2 +- cmd/ti/main.go | 45 + deploy/telemetry/Dockerfile | 14 +- deploy/telemetry/docker-compose.yml | 2 +- docs/present.md | 216 ++--- docs/priciples.md | 122 +-- .../mount-file-system-config-free-mount.md | 36 +- ...> 0027-homebrew-and-scoop-distribution.md} | 87 +- ...=> 0028-serverless-function-deployment.md} | 134 +-- .../0012-install-and-update-distribution.md | 4 +- .../done/0026-ti-cli-rename-and-migration.md | 621 ++++++++++++ docs/telemetry-backend-design.md | 52 +- e2e/cli_test.go | 542 ++++++----- e2e/installer_test.go | 197 +++- e2e/live_test.go | 600 ++++++------ e2e/telemetry_test.go | 36 +- e2e/testdata/fake-drive9.go | 40 +- go.mod | 4 +- internal/api/client.go | 20 +- internal/api/client_test.go | 12 +- internal/api/endpoints/endpoints.go | 46 +- internal/api/endpoints/endpoints_test.go | 18 +- internal/api/error.go | 2 +- internal/api/fs/client.go | 14 +- internal/api/fs/client_test.go | 14 +- internal/api/fs/git_workspace.go | 2 +- internal/api/fs/journal.go | 2 +- internal/api/fs/journal_test.go | 4 +- internal/api/fs/layer.go | 4 +- internal/api/fs/upload.go | 24 +- internal/api/fs/upload_test.go | 14 +- internal/api/fs/vault.go | 6 +- internal/api/fs/vault_test.go | 6 +- internal/api/iam/client.go | 2 +- internal/api/iam/client_test.go | 34 +- internal/api/oplog_transport.go | 8 +- internal/api/starter/branch_test.go | 2 +- internal/api/starter/client.go | 2 +- internal/api/starter/client_test.go | 8 +- internal/api/transport/transport.go | 6 +- internal/api/transport/transport_test.go | 2 +- internal/auth/auth.go | 40 +- internal/auth/auth_test.go | 16 +- internal/authz/authz.go | 128 +-- internal/authz/authz_test.go | 18 +- internal/cli/commands.go | 485 ++++----- internal/cli/root.go | 128 ++- internal/cli/root_test.go | 295 +++--- internal/config/configure/wizard.go | 87 +- internal/config/configure/wizard_test.go | 58 +- internal/config/envcompat/envcompat.go | 91 ++ internal/config/envcompat/envcompat_test.go | 49 + internal/config/homemigration/migration.go | 422 ++++++++ .../config/homemigration/migration_test.go | 222 +++++ internal/config/homemigration/mount_unix.go | 32 + .../config/homemigration/mount_windows.go | 31 + internal/config/profile.go | 74 +- internal/config/profile_test.go | 74 +- internal/config/store/store.go | 111 ++- internal/config/store/store_test.go | 101 +- internal/db/branch.go | 16 +- internal/db/branch_test.go | 8 +- internal/db/cluster.go | 26 +- internal/db/cluster_plan.go | 12 +- internal/db/cluster_plan_test.go | 4 +- internal/db/cluster_region.go | 8 +- internal/db/cluster_region_test.go | 6 +- internal/db/cluster_test.go | 32 +- .../db/connectionstring/connectionstring.go | 4 +- .../connectionstring/connectionstring_test.go | 18 +- internal/db/sql.go | 40 +- internal/db/sql_test.go | 32 +- internal/db/sqlaccess/sqlaccess.go | 14 +- internal/db/sqlaccess/sqlaccess_test.go | 26 +- internal/db/sqlcred/store.go | 6 +- internal/db/sqlcred/store_test.go | 12 +- internal/db/sqlhttp/sqlhttp.go | 12 +- internal/db/sqlhttp/sqlhttp_test.go | 6 +- internal/db/sqlmysql/sqlmysql.go | 6 +- internal/db/sqlmysql/sqlmysql_test.go | 4 +- internal/db/sqlresult/sqlresult.go | 2 +- internal/db/sqlsingle/sqlsingle.go | 2 +- internal/db/validate/validate.go | 2 +- internal/fs/control.go | 30 +- internal/fs/data.go | 10 +- internal/fs/drive9_companion.go | 88 +- internal/fs/drive9_companion_test.go | 105 +- internal/fs/fscred/fscred.go | 71 +- internal/fs/fscred/fscred_test.go | 46 +- internal/fs/fuse_git.go | 2 +- internal/fs/fuse_mount.go | 18 +- internal/fs/fuse_mount_control.go | 4 +- internal/fs/fuse_mount_unsupported.go | 6 +- internal/fs/fuse_version.go | 2 +- internal/fs/git.go | 52 +- internal/fs/journal.go | 6 +- internal/fs/layer.go | 28 +- internal/fs/metastore.go | 24 +- internal/fs/mount.go | 48 +- internal/fs/mountdriver/driver.go | 20 +- internal/fs/mountlocator/locator.go | 4 +- internal/fs/mountlocator/locator_test.go | 2 +- internal/fs/mountstate/state.go | 10 +- internal/fs/pack.go | 46 +- internal/fs/vault.go | 32 +- internal/fs/vault_mount.go | 12 +- internal/fs/vault_mount_unsupported.go | 4 +- internal/fs/webdavfs.go | 4 +- internal/fswrap/runner.go | 46 +- internal/fswrap/runner_test.go | 29 +- internal/oplog/oplog.go | 37 +- internal/oplog/oplog_test.go | 8 +- internal/organization/organization.go | 14 +- internal/organization/organization_test.go | 16 +- internal/output/output.go | 4 +- internal/output/output_test.go | 2 +- internal/settings/settings.go | 30 +- internal/settings/settings_test.go | 16 +- internal/telemetry/telemetry.go | 45 +- internal/telemetry/telemetry_test.go | 28 +- .../telemetrybackend/client_contract_test.go | 6 +- internal/telemetrybackend/event.go | 6 +- internal/telemetrybackend/event_test.go | 17 +- .../telemetrybackend/migrations/migrations.go | 2 +- internal/telemetrybackend/posthog.go | 4 +- internal/telemetrybackend/server.go | 4 +- internal/telemetrybackend/server_test.go | 8 +- .../telemetrybackend/test_helpers_test.go | 12 +- internal/update/update.go | 114 +-- internal/update/update_test.go | 60 +- internal/version/version.go | 2 +- scripts/install.ps1 | 97 +- scripts/install.sh | 104 +- 144 files changed, 5331 insertions(+), 3145 deletions(-) delete mode 100644 cmd/tdc/main.go rename cmd/{tdc-telemetry-backend => ti-telemetry-backend}/main.go (97%) rename cmd/{tdc-telemetry-migrate => ti-telemetry-migrate}/main.go (93%) create mode 100644 cmd/ti/main.go rename docs/spec/{0026-homebrew-and-scoop-distribution.md => 0027-homebrew-and-scoop-distribution.md} (57%) rename docs/spec/{0027-serverless-function-deployment.md => 0028-serverless-function-deployment.md} (76%) create mode 100644 docs/spec/done/0026-ti-cli-rename-and-migration.md create mode 100644 internal/config/envcompat/envcompat.go create mode 100644 internal/config/envcompat/envcompat_test.go create mode 100644 internal/config/homemigration/migration.go create mode 100644 internal/config/homemigration/migration_test.go create mode 100644 internal/config/homemigration/mount_unix.go create mode 100644 internal/config/homemigration/mount_windows.go diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 705bc02..71e3b6a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ name: Bug Report -description: Report a reproducible problem in tdc. +description: Report a reproducible problem in ti. title: "[Bug] " labels: - bug @@ -7,12 +7,12 @@ body: - type: markdown attributes: value: | - Thank you for reporting a tdc issue. Remove credentials, tokens, connection strings, SQL containing sensitive data, and customer data before submitting. + Thank you for reporting a ti issue. Remove credentials, tokens, connection strings, SQL containing sensitive data, and customer data before submitting. - type: input id: version attributes: - label: tdc version - description: Run `tdc --version`. + label: ti version + description: Run `ti --version`. placeholder: v0.1.2 validations: required: true @@ -77,7 +77,7 @@ body: id: logs attributes: label: Sanitized logs - description: Add relevant `~/.tdc/logs/tdc.jsonl` or mount-log entries after removing all sensitive data. + description: Add relevant `~/.ti/logs/ti.jsonl` or mount-log entries after removing all sensitive data. render: text - type: checkboxes id: confirmation diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 21699c7..556646e 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: Security vulnerability - url: https://github.com/tidbcloud/tdc/security/advisories/new + url: https://github.com/tidbcloud/ti-cli/security/advisories/new about: Report security vulnerabilities privately. Do not open a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 7c5fcde..100cac3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,5 +1,5 @@ name: Feature Request -description: Propose a tdc capability or command improvement. +description: Propose a ti capability or command improvement. title: "[Feature] " labels: - enhancement @@ -19,7 +19,7 @@ body: id: current_workflow attributes: label: Current workflow - description: Show how users or agents handle this today, including relevant tdc commands. + description: Show how users or agents handle this today, including relevant ti commands. validations: required: true - type: textarea diff --git a/.github/workflows/live-e2e.yml b/.github/workflows/live-e2e.yml index 0f08be5..e47d652 100644 --- a/.github/workflows/live-e2e.yml +++ b/.github/workflows/live-e2e.yml @@ -20,11 +20,11 @@ jobs: live-e2e: runs-on: ubuntu-latest env: - TDC_REGION_CODE: ${{ vars.TDC_REGION_CODE }} - TDC_PUBLIC_KEY: ${{ secrets.TDC_PUBLIC_KEY }} - TDC_PRIVATE_KEY: ${{ secrets.TDC_PRIVATE_KEY }} - TDC_LIVE_HOME: /tmp/tdc-live-home-${{ github.run_id }}-${{ github.run_attempt }} - TDC_LIVE_FS_NAME: tdc-live-e2e-${{ github.run_id }}-${{ github.run_attempt }} + TI_REGION_CODE: ${{ vars.TI_REGION_CODE }} + TIDB_CLOUD_PUBLIC_KEY: ${{ secrets.TIDB_CLOUD_PUBLIC_KEY }} + TIDB_CLOUD_PRIVATE_KEY: ${{ secrets.TIDB_CLOUD_PRIVATE_KEY }} + TI_LIVE_HOME: /tmp/ti-live-home-${{ github.run_id }}-${{ github.run_attempt }} + TI_LIVE_FS_NAME: ti-live-e2e-${{ github.run_id }}-${{ github.run_attempt }} steps: - name: Checkout uses: actions/checkout@v6 @@ -37,18 +37,18 @@ jobs: - name: Mask live secrets run: | set -euo pipefail - if [ -n "${TDC_PUBLIC_KEY:-}" ]; then - echo "::add-mask::$TDC_PUBLIC_KEY" + if [ -n "${TIDB_CLOUD_PUBLIC_KEY:-}" ]; then + echo "::add-mask::$TIDB_CLOUD_PUBLIC_KEY" fi - if [ -n "${TDC_PRIVATE_KEY:-}" ]; then - echo "::add-mask::$TDC_PRIVATE_KEY" + if [ -n "${TIDB_CLOUD_PRIVATE_KEY:-}" ]; then + echo "::add-mask::$TIDB_CLOUD_PRIVATE_KEY" fi - name: Validate live configuration run: | set -euo pipefail missing=0 - for name in TDC_REGION_CODE TDC_PUBLIC_KEY TDC_PRIVATE_KEY; do + for name in TI_REGION_CODE TIDB_CLOUD_PUBLIC_KEY TIDB_CLOUD_PRIVATE_KEY; do if [ -z "${!name:-}" ]; then echo "::error::$name is required for live-e2e" missing=1 @@ -75,36 +75,36 @@ jobs: - name: Configure live profile env: - HOME: ${{ env.TDC_LIVE_HOME }} + HOME: ${{ env.TI_LIVE_HOME }} run: | set -euo pipefail mkdir -p "$HOME" - bin/tdc configure --profile live-e2e --non-interactive + bin/ti configure --profile live-e2e --non-interactive - - name: Create temporary tdc fs resource + - name: Create temporary ti fs resource env: - HOME: ${{ env.TDC_LIVE_HOME }} + HOME: ${{ env.TI_LIVE_HOME }} run: | set -euo pipefail - bin/tdc fs create-file-system \ + bin/ti fs create-file-system \ --profile live-e2e \ - --file-system-name "$TDC_LIVE_FS_NAME" + --file-system-name "$TI_LIVE_FS_NAME" - name: Run live e2e env: - HOME: ${{ env.TDC_LIVE_HOME }} + HOME: ${{ env.TI_LIVE_HOME }} run: make live-e2e - - name: Delete temporary tdc fs resource + - name: Delete temporary ti fs resource if: always() env: - HOME: ${{ env.TDC_LIVE_HOME }} + HOME: ${{ env.TI_LIVE_HOME }} run: | set -euo pipefail - if [ ! -f "$HOME/.tdc/config" ] || [ ! -f "$HOME/.tdc/credentials" ]; then - echo "Skipping tdc fs cleanup because live profile files were not created." + if [ ! -f "$HOME/.ti/config" ] || [ ! -f "$HOME/.ti/credentials" ]; then + echo "Skipping ti fs cleanup because live profile files were not created." exit 0 fi - bin/tdc fs delete-file-system \ + bin/ti fs delete-file-system \ --profile live-e2e \ - --file-system-name "$TDC_LIVE_FS_NAME" + --file-system-name "$TI_LIVE_FS_NAME" diff --git a/.gitignore b/.gitignore index 5b2c6d3..f8cca96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ /bin/ /dist/ -/tdc +/ti .env .env.local .env.telemetry diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 0338921..5abcf92 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,15 +1,15 @@ version: 2 -project_name: tdc +project_name: ti before: hooks: - go mod download builds: - - id: tdc - main: ./cmd/tdc - binary: tdc + - id: ti + main: ./cmd/ti + binary: ti env: - CGO_ENABLED=0 goos: @@ -24,17 +24,17 @@ builds: goarch: arm64 ldflags: - -s -w - - -X github.com/tidbcloud/tdc/internal/version.version={{ .Version }} - - -X github.com/tidbcloud/tdc/internal/version.commit={{ .Commit }} - - -X github.com/tidbcloud/tdc/internal/version.date={{ .Date }} - - -X github.com/tidbcloud/tdc/internal/version.installSource=archive - - -X github.com/tidbcloud/tdc/internal/version.releaseChannel=stable - - -X github.com/tidbcloud/tdc/internal/version.telemetryEndpoint=https://tdc-telemetry.tidbcloud.com/v1/telemetry/batch + - -X github.com/tidbcloud/ti-cli/internal/version.version={{ .Version }} + - -X github.com/tidbcloud/ti-cli/internal/version.commit={{ .Commit }} + - -X github.com/tidbcloud/ti-cli/internal/version.date={{ .Date }} + - -X github.com/tidbcloud/ti-cli/internal/version.installSource=archive + - -X github.com/tidbcloud/ti-cli/internal/version.releaseChannel=stable + - -X github.com/tidbcloud/ti-cli/internal/version.telemetryEndpoint=https://tdc-telemetry.tidbcloud.com/v1/telemetry/batch archives: - - id: tdc + - id: ti ids: - - tdc + - ti name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" formats: - tar.gz @@ -54,7 +54,7 @@ snapshot: release: github: owner: tidbcloud - name: tdc + name: ti-cli prerelease: auto make_latest: true mode: replace diff --git a/AGENTS.md b/AGENTS.md index 275ba4d..29ce608 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,23 +1,30 @@ --- -title: AGENTS.md - tdc development guide for AI coding agents +title: AGENTS.md - ti development guide for AI coding agents --- # Repository Overview -tdc is a Go command-line product for TiDB Cloud Starter. It is designed to be +ti is a Go command-line product for TiDB Cloud Starter. It is designed to be agent-friendly, predictable, scriptable, and safe for automation. -Module: `github.com/tidbcloud/tdc` +Module: `github.com/tidbcloud/ti-cli` Go version: 1.26.5 (see `go.mod`) The most important product document is `docs/priciples.md`. Treat that file as the source of truth for product principles. Requirement specs live in `docs/spec/`; completed specs are moved to `docs/spec/done/`. +Completed specs that mention `tdc` describe the pre-v0.2 product name. Do not +mechanically rename those historical records. Current code, documentation, +commands, paths, and environment variables use `ti`. + ## Current Implementation Status Implemented: +- Ti CLI repository, executable, local-state, environment-variable, installer, + updater, release, telemetry, and documentation rename with safe v0.1 state + migration from `docs/spec/done/0026-ti-cli-rename-and-migration.md` - CLI foundation from `docs/spec/done/0001-cli-foundation.md` - Local config and credentials from `docs/spec/done/0002-local-config-and-credentials.md` @@ -45,19 +52,19 @@ Implemented: `docs/spec/done/0023-starter-only-db-resource-guardrails.md` - Region-scoped Starter DB cluster listing from `docs/spec/done/0024-region-scoped-db-cluster-listing.md` -- tdc fs Unix-style command aliases from +- ti fs Unix-style command aliases from `docs/spec/done/0014-tdc-fs-unix-command-aliases.md` -- tdc fs control plane from +- ti fs control plane from `docs/spec/done/0009-tdc-fs-control-plane.md` -- tdc fs data plane from +- ti fs data plane from `docs/spec/done/0010-tdc-fs-data-plane.md` -- tdc fs mount runtime from +- ti fs mount runtime from `docs/spec/done/0011-tdc-fs-mount-runtime.md` -- tdc fs FUSE correctness and Drive9 parity extension from +- ti fs FUSE correctness and Drive9 parity extension from `docs/spec/done/0011-ext01-fuse-cache-and-open-handle-correctness.md` -- profile-scoped 1:N tdc fs resource registry from +- profile-scoped 1:N ti fs resource registry from `docs/spec/done/0016-profile-fs-resource-registry.md` -- explicit tdc fs resource selection from +- explicit ti fs resource selection from `docs/spec/done/0020-explicit-file-system-selection.md` - FS token authentication and configuration-free access from `docs/spec/done/0018-fs-token-auth-and-config-free-access.md` @@ -65,77 +72,77 @@ Implemented: `docs/spec/done/0012-install-and-update-distribution.md` - English PingCAP Preview documentation from `docs/spec/done/0019-pingcap-tdc-documentation.md` -- `tdc configure` -- `tdc update --check` -- `tdc update` -- `tdc organization list-projects` -- `tdc db create-db-cluster` -- `tdc db list-db-clusters` -- `tdc db describe-db-cluster` -- `tdc db update-db-cluster` -- `tdc db delete-db-cluster` -- `tdc db create-db-cluster-branch` -- `tdc db list-db-cluster-branches` -- `tdc db describe-db-cluster-branch` -- `tdc db delete-db-cluster-branch` -- `tdc db create-db-sql-users` -- `tdc db format-db-connection-string` -- `tdc db execute-sql-statement` -- `tdc fs create-file-system` -- `tdc fs delete-file-system` -- `tdc fs list-file-systems` -- `tdc fs describe-file-system` -- `tdc fs check-file-system` -- `tdc fs copy-file` -- `tdc fs read-file` -- `tdc fs list-files` -- `tdc fs describe-file` -- `tdc fs move-file` -- `tdc fs delete-file` -- `tdc fs create-directory` -- `tdc fs chmod-file` -- `tdc fs create-symlink` -- `tdc fs create-hardlink` -- `tdc fs search-file-content` -- `tdc fs find-files` -- `tdc fs create-layer` -- `tdc fs list-layers` -- `tdc fs describe-layer` -- `tdc fs diff-layer` -- `tdc fs create-layer-checkpoint` -- `tdc fs rollback-layer` -- `tdc fs commit-layer` -- `tdc fs mount-file-system` -- `tdc fs drain-file-system` -- `tdc fs unmount-file-system` -- Unix-style `tdc fs` command aliases: `cp`, `cat`, `ls`, `stat`, `mv`, `rm`, +- `ti configure` +- `ti update --check` +- `ti update` +- `ti organization list-projects` +- `ti db create-db-cluster` +- `ti db list-db-clusters` +- `ti db describe-db-cluster` +- `ti db update-db-cluster` +- `ti db delete-db-cluster` +- `ti db create-db-cluster-branch` +- `ti db list-db-cluster-branches` +- `ti db describe-db-cluster-branch` +- `ti db delete-db-cluster-branch` +- `ti db create-db-sql-users` +- `ti db format-db-connection-string` +- `ti db execute-sql-statement` +- `ti fs create-file-system` +- `ti fs delete-file-system` +- `ti fs list-file-systems` +- `ti fs describe-file-system` +- `ti fs check-file-system` +- `ti fs copy-file` +- `ti fs read-file` +- `ti fs list-files` +- `ti fs describe-file` +- `ti fs move-file` +- `ti fs delete-file` +- `ti fs create-directory` +- `ti fs chmod-file` +- `ti fs create-symlink` +- `ti fs create-hardlink` +- `ti fs search-file-content` +- `ti fs find-files` +- `ti fs create-layer` +- `ti fs list-layers` +- `ti fs describe-layer` +- `ti fs diff-layer` +- `ti fs create-layer-checkpoint` +- `ti fs rollback-layer` +- `ti fs commit-layer` +- `ti fs mount-file-system` +- `ti fs drain-file-system` +- `ti fs unmount-file-system` +- Unix-style `ti fs` command aliases: `cp`, `cat`, `ls`, `stat`, `mv`, `rm`, `mkdir`, `chmod`, `symlink`, `hardlink`, `grep`, `find`, `mount`, `drain`, and `umount` -- `tdc fs-vault create-secret` -- `tdc fs-vault replace-secret` -- `tdc fs-vault read-secret` -- `tdc fs-vault list-secrets` -- `tdc fs-vault delete-secret` -- `tdc fs-vault create-grant` -- `tdc fs-vault delete-grant` -- `tdc fs-vault list-audit-events` -- `tdc fs-vault run-with-secret` -- `tdc fs-vault mount-vault` -- `tdc fs-vault unmount-vault` -- `tdc fs-journal create-journal` -- `tdc fs-journal append-journal-entries` -- `tdc fs-journal read-journal-entries` -- `tdc fs-journal search-journal-entries` -- `tdc fs-journal verify-journal` +- `ti fs-vault create-secret` +- `ti fs-vault replace-secret` +- `ti fs-vault read-secret` +- `ti fs-vault list-secrets` +- `ti fs-vault delete-secret` +- `ti fs-vault create-grant` +- `ti fs-vault delete-grant` +- `ti fs-vault list-audit-events` +- `ti fs-vault run-with-secret` +- `ti fs-vault mount-vault` +- `ti fs-vault unmount-vault` +- `ti fs-journal create-journal` +- `ti fs-journal append-journal-entries` +- `ti fs-journal read-journal-entries` +- `ti fs-journal search-journal-entries` +- `ti fs-journal verify-journal` - help and version behavior at every command level - structured JSON/text rendering and JMESPath `--query` - `--dry-run` on mutating control-plane commands - TiDB Cloud Digest-auth API client foundation and auth/authz error mapping -- profile-scoped 1:N tdc fs resource registry with per-resource credentials -- tdc fs/fs-git/fs-journal/fs-vault commands routed through the bundled - `tdc-drive9` companion, with tdc-owned profile loading, credential storage, +- profile-scoped 1:N ti fs resource registry with per-resource credentials +- ti fs/fs-git/fs-journal/fs-vault commands routed through the bundled + `ti-drive9` companion, with ti-owned profile loading, credential storage, region resolution, and output/error handling -- Drive9 public CLI coverage for tdc fs data-plane operations, FUSE/WebDAV +- Drive9 public CLI coverage for ti fs data-plane operations, FUSE/WebDAV mount, mount drain, layers, pack/unpack, vault, journal, and Git clone, hydrate, add-worktree, and remove-worktree workflows - GoReleaser/GitHub Releases install and update workflow @@ -153,11 +160,13 @@ dry-run support. a reference for TiDB Cloud concepts, profile handling, output helpers, telemetry, and API client patterns. - `ref/drive9/` is the filesystem reference implementation. Use it as context - for filesystem commands, mount behavior, and data-plane semantics. In tdc - user-facing output, this domain is always called `tdc fs`. + for filesystem commands, mount behavior, and data-plane semantics. In ti + user-facing output, this domain is always called `ti fs`. - `ref/serverless-js/` is a reference for the HTTPS SQL API call shape. +- `ref/fs/` is the TiDB Cloud deployment wrapper around Drive9. Use it only to + understand the hosted filesystem API and authorization boundary. -Reference directories are not product source for tdc. They exist only to give +Reference directories are not product source for ti. They exist only to give agents context and implementation examples. In main project code, behave as if `ref/` does not exist: @@ -193,23 +202,23 @@ make release-snapshot make clean ``` -`make build` writes the binary to `bin/tdc`. +`make build` writes the binary to `bin/ti`. `make build-telemetry-backend` writes the independent ingestion service to -`bin/tdc-telemetry-backend`. +`bin/ti-telemetry-backend`. `make build-telemetry-migrator` writes the one-shot Goose migration runner to -`bin/tdc-telemetry-migrate`. +`bin/ti-telemetry-migrate`. `make test` runs ordinary Go tests and must not require live cloud credentials. -`make e2e` builds `bin/tdc` and runs black-box tests against the real binary via -`TDC_E2E_BIN`. +`make e2e` builds `bin/ti` and runs black-box tests against the real binary via +`TI_E2E_BIN`. `make telemetry-e2e` is separately opt-in. It loads the ignored -`e2e/.env.telemetry` file, requires a test-only `TDC_TEST_TELEMETRY_TIDB_DSN` +`e2e/.env.telemetry` file, requires a test-only `TI_TEST_TELEMETRY_TIDB_DSN` whose user can create and drop databases, and creates a unique temporary TiDB database. It verifies Goose initialization from empty state, an additive upgrade preserving a legacy event, and the real local CLI-to-backend-to-TiDB delivery path before dropping only that temporary database. It must not run as part of `make test`, `make e2e`, or any live-e2e target. -The `make live-e2e-` targets build `bin/tdc` and run only the selected +The `make live-e2e-` targets build `bin/ti` and run only the selected top-level command family against the `live-e2e` profile by default. Keep configure, organization, db, fs, fs-git, fs-journal, and fs-vault tests independently selectable. Do not make a focused family target run tests from a @@ -220,18 +229,18 @@ the profile for both focused and complete live targets. Live e2e must strictly cover every implemented interface and command for the current project stage, including real create/update/delete flows when those commands are implemented. For Starter DB clusters, the live suite creates a -uniquely named `tdc-e2e-*` cluster with `--wait`, without a +uniquely named `ti-e2e-*` cluster with `--wait`, without a spending limit or explicit/configured project ID, verifies the returned state is `ACTIVE` and has a non-empty server-selected project label, and deletes only that cluster. The server-selected account default is not required to equal the -`tidbx_virtual` project discovered by `tdc configure`. For Starter DB branches, -the live suite creates, reads, lists, and deletes only a `tdc-e2e-branch-*` +`tidbx_virtual` project discovered by `ti configure`. For Starter DB branches, +the live suite creates, reads, lists, and deletes only a `ti-e2e-branch-*` branch on the cluster created by the same test run. Branch creation must use -`--wait`; cluster deletion must use `--wait`. For Starter DB SQL access, the live suite prepares tdc-managed +`--wait`; cluster deletion must use `--wait`. For Starter DB SQL access, the live suite prepares ti-managed read-only, read-write, and admin SQL users on the temporary cluster, verifies connection string output, and executes the HTTPS SQL API with all three access modes. -For tdc fs data-plane and mount runtime, the live suite creates uniquely named +For ti fs data-plane and mount runtime, the live suite creates uniquely named remote paths, exercises real file create/read/list/copy/move/delete flows, range reads, append, resume, recursive local/remote copy, stdin/stdout copy, tags/descriptions, chmod, symlink, hardlink, pack/unpack, real public layer @@ -241,13 +250,13 @@ vault grant reads, vault mount read on macOS/Linux hosts when available, journal create/append/read/search/verify, public Git clone/hydrate/worktree flows, mount and drain through the companion runtime, and explicit WebDAV fallback when the platform supports it. -If the live profile has no registry resource named by `TDC_LIVE_FS_NAME` or -`workspace`, the suite creates that temporary tdc fs resource, stores its +If the live profile has no registry resource named by `TI_LIVE_FS_NAME` or +`workspace`, the suite creates that temporary ti fs resource, stores its metadata and API key in the profile-scoped resource registry, and deletes only that auto-created resource before the DB lifecycle needs the Starter slot, or when the test process exits if execution stops earlier. The live suite also attempts a separate 1:N registry lifecycle with two unique -`tdc-e2e-fs-*` resources, covering create, list, default selection, explicit +`ti-e2e-fs-*` resources, covering create, list, default selection, explicit selection, isolated deletion, and cleanup. If the second resource is rejected specifically because Starter quota is full, complete the single-resource live flow and rely on `make e2e` for fake-companion multi-resource routing coverage. @@ -261,7 +270,7 @@ For focused work, direct Go commands are also fine: ```bash go test ./... go test ./internal/config -run TestName -go build ./cmd/tdc +go build ./cmd/ti ``` Build and release artifacts are ignored through `.gitignore`. Do not commit @@ -275,9 +284,9 @@ that rewrite unrelated files. Current layout: ```text -cmd/tdc/ CLI entrypoint -cmd/tdc-telemetry-backend/ independent telemetry ingestion service entrypoint -cmd/tdc-telemetry-migrate/ one-shot embedded Goose migration runner +cmd/ti/ CLI entrypoint +cmd/ti-telemetry-backend/ independent telemetry ingestion service entrypoint +cmd/ti-telemetry-migrate/ one-shot embedded Goose migration runner internal/api/ shared HTTP API client and service clients internal/api/endpoints/ provider/region endpoint resolver internal/api/transport/ Digest/Bearer/debug HTTP transports @@ -287,7 +296,9 @@ internal/authz/ permission constants and permission errors internal/cli/ command wiring internal/config/ profile loading and precedence rules internal/config/configure/ interactive configure wizard -internal/config/fsresource/ legacy flat tdc fs migration key names +internal/config/envcompat/ canonical and legacy environment compatibility +internal/config/fsresource/ legacy flat ti fs migration key names +internal/config/homemigration/ atomic pre-v0.2 state migration internal/config/region/ provider and region validation internal/config/store/ TOML read/write, file modes, atomic writes internal/db/ Starter DB cluster, branch, and SQL use cases @@ -300,8 +311,8 @@ internal/db/sqlresult/ SQL result model and decoding internal/db/sqlsingle/ one-statement validation internal/db/validate/ DB flag and request validation helpers internal/dryrun/ shared dry-run result envelope -internal/fs/ tdc fs control-plane, data-plane, and mount use cases -internal/fs/fscred/ profile-scoped tdc fs registry, selection, and migration +internal/fs/ ti fs control-plane, data-plane, and mount use cases +internal/fs/fscred/ profile-scoped ti fs registry, selection, and migration internal/fs/mountlocator/ non-secret Drive9 background mount routing state internal/oplog/ local JSONL operation log writer internal/output/ structured JSON/text/raw rendering @@ -330,17 +341,17 @@ without underscores. Follow these rules unless `docs/priciples.md` is updated: -- The command tree is at most two levels: `tdc [subcommand]`. -- `tdc configure` and `tdc update` are the only intentional top-level verb - exceptions. `tdc configure` is the only interactive command. +- The command tree is at most two levels: `ti [subcommand]`. +- `ti configure` and `ti update` are the only intentional top-level verb + exceptions. `ti configure` is the only interactive command. - Other top-level commands are nouns such as `db`, `fs`, and `organization`. - Use long flags only, for example `--profile` and `--db-cluster-name`. - Do not add short flags or one-letter aliases. The current CLI rejects short flags before invoking Cobra. -- `tdc fs` Unix-style aliases are command-name aliases only. They must keep the +- `ti fs` Unix-style aliases are command-name aliases only. They must keep the same long flags, output modes, auth, permissions, dry-run behavior, and command handlers as their canonical commands. -- Do not prompt for input except inside `tdc configure`. +- Do not prompt for input except inside `ti configure`. - Successful structured control-plane commands output JSON by default. - Implement DB, organization, and fs control-plane commands through `controlPlaneCommandSpec` in `internal/cli`, so normal execution, dry-run, @@ -350,36 +361,36 @@ Follow these rules unless `docs/priciples.md` is updated: - Mutating control-plane commands support `--dry-run`. - `--dry-run` must validate local config, credentials, provider, and region before reporting a planned mutation. -- `tdc db create-db-cluster --wait` waits up to 12 minutes for the +- `ti db create-db-cluster --wait` waits up to 12 minutes for the created cluster to reach `ACTIVE`. It must never delete the cluster on timeout, cancellation, a polling failure, or a terminal state; errors must retain the created cluster ID and provide an inspection command. -- `tdc db create-db-cluster-branch --wait` waits up to 5 minutes +- `ti db create-db-cluster-branch --wait` waits up to 5 minutes for the created branch to reach `ACTIVE`. A failed wait must not delete or recreate the accepted branch. -- `tdc db delete-db-cluster --wait` waits up to 12 minutes and +- `ti db delete-db-cluster --wait` waits up to 12 minutes and succeeds when the API reports `DELETED` or the deleted cluster is no longer accessible. A failed wait must state that deletion may still be in progress. -- `tdc fs create-file-system --wait` waits up to 10 minutes for the +- `ti fs create-file-system --wait` waits up to 10 minutes for the root to become readable through the public Drive9 CLI. It must retain the resource and local credentials when waiting fails. -- `tdc fs delete-file-system` is asynchronous. After Drive9 accepts deletion, +- `ti fs delete-file-system` is asynchronous. After Drive9 accepts deletion, output status is `deleting`, not `deleted`. - Read-only commands reject `--dry-run`. - Apply `--query` after command execution and before rendering. - Users provide cloud placement as one canonical `region_code`, never as separate provider/region fields or server URLs. -- Every `tdc db` command must enforce the Starter-only product boundary from +- Every `ti db` command must enforce the Starter-only product boundary from API resource metadata. Treat `servicePlan` as canonical and `clusterPlan` as a legacy fallback. Reject non-Starter, missing, or conflicting plans before any cluster, branch, IAM SQL-user, local SQL-credential, or SQL mutation. -- `tdc db list-db-clusters` adds an immutable API filter for the effective +- `ti db list-db-clusters` adds an immutable API filter for the effective provider and region, then defensively filters each API page to verified Starter clusters in that same region. It preserves the API next-page token and omits the API total size because that total can include resources outside - tdc's verified result. Missing or conflicting region metadata is excluded. + ti's verified result. Missing or conflicting region metadata is excluded. - The global `--region ` flag overrides placement for - the current command only. It has higher priority than `TDC_REGION_CODE` and + the current command only. It has higher priority than `TI_REGION_CODE` and profile `region_code`, but it must not change the selected profile or credential source. - Every command should be usable by scripts and agents without @@ -388,276 +399,298 @@ Follow these rules unless `docs/priciples.md` is updated: request template. If it does, follow that template when writing the pull request description. - Help must work as: - - `tdc help` - - `tdc help` - - `tdc help` + - `ti help` + - `ti help` + - `ti help` - In generated Flags and Global Flags sections, render value types as `` and append `(required)` to flags marked with - `tdc_usage_required`. + `ti_usage_required`. - Keep the global `--version` behavior intact at every command level. Do not add command-specific `--version ` flags; use names such as `--target-version` when a command needs a version input. -- `tdc update --check` and `tdc update` use GitHub Releases metadata and - must not read or mutate `~/.tdc/`. -- `tdc update` may replace only tdc-owned archive/script installs. It must +- `ti update --check` and `ti update` use GitHub Releases metadata and + must not read or mutate `~/.ti/` or inspect/migrate `~/.tdc/`. +- `ti update` may replace only ti-owned archive/script installs. It must refuse local, unknown, Homebrew, Scoop, Winget, or other package-manager installs with actionable guidance. -- `tdc update` is itself explicit update intent and must not require `--yes`. +- `ti update` is itself explicit update intent and must not require `--yes`. It downloads, extracts, verifies, stages, and replaces artifacts as the current user and must never invoke sudo or another privilege escalation mechanism. -- Installer scripts default to the stable user-owned `~/.tdc/bin` directory on +- Installer scripts default to the stable user-owned `~/.ti/bin` directory on macOS, Linux, and Windows unless `--install-dir`/`-InstallDir` or - `TDC_INSTALL_DIR` overrides it. They must not prefer or overwrite an active - system-level tdc found on PATH, invoke sudo, create system-directory + `TI_INSTALL_DIR` overrides it. They must not prefer or overwrite an active + system-level ti found on PATH, invoke sudo, create system-directory symlinks, or modify shell profile files automatically. -- Installer scripts must detect PATH shadowing, bootstrap `~/.tdc/config` only - when missing, print the exact command that prepends `~/.tdc/bin` to PATH, - print DB and tdc fs region lists, and show clear next steps. They must never - write `~/.tdc/credentials`. +- Installer scripts must detect PATH shadowing, bootstrap `~/.ti/config` only + when missing, print the exact command that prepends `~/.ti/bin` to PATH, + print DB and ti fs region lists, and show clear next steps. They must never + write `~/.ti/credentials`. +- Installer scripts must invoke the staged `ti` binary's shared home migration + before creating the default `~/.ti/bin` destination. Do not duplicate state + copying in shell or PowerShell. ## Commands Implemented command behavior: -- `tdc` without a command returns `cli.missing_command` with exit code `2` and +- `ti` without a command returns `cli.missing_command` with exit code `2` and an AWS-style compact two-level usage synopsis on stderr -- `tdc configure` -- `tdc configure --non-interactive` -- `tdc help` -- `tdc --version` -- `tdc help` -- `tdc help` -- `tdc --version` -- `tdc --version` -- `tdc update --check` -- `tdc update --check --fail-if-update-available` -- `tdc update --dry-run` -- `tdc update` -- `tdc update --target-version v0.1.1` -- `tdc organization list-projects` -- `tdc organization list-projects --query 'projects[0].id'` -- `tdc organization list-projects --output text` -- `tdc db create-db-cluster --db-cluster-name demo` -- `tdc db create-db-cluster --db-cluster-name demo --wait` -- `tdc db create-db-cluster --db-cluster-name demo --dry-run` -- `tdc db create-db-cluster --db-cluster-name demo --project-id ` -- `tdc db list-db-clusters` -- `tdc db list-db-clusters --query 'clusters[].id'` -- `tdc db describe-db-cluster --db-cluster-id ` -- `tdc db update-db-cluster --db-cluster-id --db-cluster-name new-name` -- `tdc db update-db-cluster --db-cluster-id --monthly-spending-limit-usd-cents 1000 --dry-run` -- `tdc db delete-db-cluster --db-cluster-id ` -- `tdc db delete-db-cluster --db-cluster-id --wait` -- `tdc db delete-db-cluster --db-cluster-id --dry-run` -- `tdc db create-db-cluster-branch --db-cluster-id --db-cluster-branch-name dev` -- `tdc db create-db-cluster-branch --db-cluster-id --db-cluster-branch-name dev --wait` -- `tdc db create-db-cluster-branch --db-cluster-id --db-cluster-branch-name dev --dry-run` -- `tdc db list-db-cluster-branches --db-cluster-id ` -- `tdc db list-db-cluster-branches --db-cluster-id --query 'branches[].id'` -- `tdc db list-db-cluster-branches --db-cluster-id --output text` -- `tdc db describe-db-cluster-branch --db-cluster-id --db-cluster-branch-id ` -- `tdc db delete-db-cluster-branch --db-cluster-id --db-cluster-branch-id ` -- `tdc db delete-db-cluster-branch --db-cluster-id --db-cluster-branch-id --dry-run` -- `tdc db create-db-sql-users --db-cluster-id ` -- `tdc db create-db-sql-users --db-cluster-id --dry-run` -- `tdc db format-db-connection-string --db-cluster-id ` -- `tdc db format-db-connection-string --db-cluster-id --read-write --format mysql-uri` -- `tdc db format-db-connection-string --db-cluster-id --read-only --format env` -- `tdc db format-db-connection-string --db-cluster-id --admin --format jdbc` -- `tdc db execute-sql-statement --db-cluster-id --sql "select 1"` -- `tdc db execute-sql-statement --db-cluster-id --read-write --sql "select 1"` -- `tdc db execute-sql-statement --db-cluster-id --read-only --sql "select 1"` -- `tdc db execute-sql-statement --db-cluster-id --admin --sql "select 1"` -- `tdc db execute-sql-statement --db-cluster-id --transport https --sql "select 1"` -- `tdc db execute-sql-statement --db-cluster-id --transport mysql --sql "select 1"` -- `tdc fs create-file-system --file-system-name workspace` -- `tdc fs create-file-system --file-system-name workspace --wait` -- `tdc fs create-file-system --file-system-name workspace --dry-run` -- `tdc fs create-file-system --file-system-name scratch` -- `tdc fs delete-file-system --file-system-name workspace` -- `tdc fs delete-file-system --file-system-name workspace --dry-run` -- `tdc fs list-file-systems` -- `tdc fs describe-file-system --file-system-name workspace` -- `tdc fs check-file-system` -- `tdc fs check-file-system --file-system-name workspace` -- `tdc fs copy-file --from-local ./README.md --to-remote /workspace/README.md` -- `tdc fs copy-file --from-remote /workspace/README.md --to-local ./README.copy.md --create-parents` -- `tdc fs copy-file --from-remote /workspace/README.md --to-remote /workspace/README.copy.md` -- `tdc fs read-file --path /workspace/README.md` -- `tdc fs read-file --path /workspace/README.md --offset 0 --length 1024` -- `tdc fs copy-file --from-local ./tail.log --to-remote /workspace/app.log --append` -- `tdc fs copy-file --from-remote /workspace/large.bin --to-local ./large.bin --resume` -- `tdc fs copy-file --from-local ./large.bin --to-remote /workspace/large.bin --resume` -- `tdc fs copy-file --from-local ./src-dir --to-remote /workspace/src-dir --recursive` -- `tdc fs copy-file --from-remote /workspace/src-dir --to-local ./src-dir.copy --recursive` -- `tdc fs copy-file --from-remote /workspace/src-dir --to-remote /workspace/src-dir.copy --recursive` -- `tdc fs copy-file --from-stdin --to-remote /workspace/stdin.txt --tag source=stdin --description "stdin upload"` -- `tdc fs copy-file --from-remote /workspace/stdin.txt --to-stdout` -- `tdc fs list-files --path /workspace` -- `tdc fs list-files --path /workspace --output text` -- `tdc fs describe-file --path /workspace/README.md` -- `tdc fs move-file --from-remote /workspace/README.copy.md --to-remote /workspace/archive/README.md` -- `tdc fs delete-file --path /workspace/archive/README.md` -- `tdc fs delete-file --path /workspace --recursive` -- `tdc fs create-directory --path /workspace/archive --mode 0755` -- `tdc fs chmod-file --path /workspace/README.md --mode 0600` -- `tdc fs create-symlink --target README.md --link-path /workspace/README.link` -- `tdc fs create-hardlink --source-path /workspace/README.md --link-path /workspace/README.hard` -- `tdc fs search-file-content --path /workspace --pattern "hello"` -- `tdc fs search-file-content --path /workspace --pattern "hello" --layer-id layer-1` -- `tdc fs find-files --path /workspace --file-name-pattern "*.md"` -- `tdc fs find-files --path /workspace --file-name-pattern "*.md" --layer-id layer-1` -- `tdc fs create-layer --layer-id layer-1 --base-root-path /workspace --layer-name task --durability-mode restore-safe --tag task=auth` -- `tdc fs list-layers` -- `tdc fs list-layers --output text` -- `tdc fs describe-layer --layer-id layer-1` -- `tdc fs diff-layer --layer-id layer-1` -- `tdc fs copy-file --from-local ./README.md --to-remote /workspace/layered.md --layer-id layer-1` -- `tdc fs create-layer-checkpoint --layer-id layer-1 --checkpoint-id cp-1 --label before-commit` -- `tdc fs rollback-layer --layer-id layer-1` -- `tdc fs commit-layer --layer-id layer-1` -- `tdc fs pack-file-system --local-root ~/.tdc/local/fs/demo --remote-root /workspace --mount-profile portable` -- `tdc fs pack-file-system --mount-path ./workspace` -- `tdc fs unpack-file-system --local-root ~/.tdc/local/fs/demo --remote-root /workspace --mount-profile portable` -- `tdc fs mount-file-system --file-system-name workspace --mount-path ./workspace` -- `tdc fs mount-file-system --file-system-name workspace --mount-path ./workspace --driver fuse` -- `tdc fs mount-file-system --file-system-name workspace --mount-path ./workspace --driver webdav` -- `tdc fs mount-file-system --file-system-name workspace --mount-path ./workspace --mount-profile coding-agent` -- `tdc fs mount-file-system --file-system-name workspace --mount-path ./workspace --mount-profile portable --pack-path /` -- `tdc fs mount-file-system --file-system-name workspace --mount-path ./workspace --driver fuse --read-cache-size-mb 256 --read-cache-max-file-mb 16` -- `tdc fs mount-file-system --file-system-name workspace --mount-path ./workspace --driver fuse --cache-dir ~/.tdc/cache/workspace --write-back-cache=false` -- `tdc fs drain-file-system --mount-path ./workspace` -- `tdc fs drain-file-system --mount-path ./workspace --timeout 30s` -- `tdc fs unmount-file-system --mount-path ./workspace` -- `tdc fs unmount-file-system --mount-path ./workspace --ignore-absent` -- `tdc fs-vault create-secret --secret-name db-prod --field DB_URL=mysql://example --field PASSWORD=@./password.txt` -- `tdc fs-vault replace-secret --secret-path /n/vault/db-prod --from-directory ./secret-fields` -- `tdc fs-vault read-secret --secret-name db-prod` -- `tdc fs-vault read-secret --secret-name db-prod --field PASSWORD --format raw` -- `tdc fs-vault read-secret --secret-name db-prod --field DB_URL --format env` -- `tdc fs-vault list-secrets` -- `tdc fs-vault delete-secret --secret-name db-prod` -- `tdc fs-vault create-grant --agent-id deploy-agent --scope db-prod/DB_URL --permission read --ttl 10m` -- `tdc fs-vault delete-grant --grant-id --reason rotated` -- `tdc fs-vault list-audit-events --secret-name db-prod --limit 20` -- `tdc fs-vault run-with-secret --secret-path /n/vault/db-prod -- env` -- `tdc fs-vault mount-vault --mount-path ./vault --vault-token "$TDC_VAULT_TOKEN"` -- `tdc fs-vault unmount-vault --mount-path ./vault` -- `tdc fs-journal create-journal --journal-id jrn-demo --journal-kind agent --title "demo task" --actor agent:tdc --label env=dev` -- `tdc fs-journal append-journal-entries --journal-id jrn-demo --entry-json '{"type":"task.started"}'` -- `tdc fs-journal read-journal-entries --journal-id jrn-demo --after-seq 0 --limit 100` -- `tdc fs-journal search-journal-entries --entry-type task.started --label env=dev --include-entries` -- `tdc fs-journal verify-journal --journal-id jrn-demo --output text` -- `tdc fs-git clone-git-workspace --repo-url https://github.com/pingcap/tidb.git --target-path ./workspace/tidb` -- `tdc fs-git clone-git-workspace --repo-url https://github.com/pingcap/tidb.git --target-path ./workspace/tidb --blobless --hydrate sync` -- `tdc fs-git hydrate-git-workspace --target-path ./workspace/tidb --timeout 30m` -- `tdc fs-git add-git-worktree --base-path ./workspace/tidb --worktree-path ./workspace/tidb-feature --branch-name feature-x` -- `tdc fs-git remove-git-worktree --worktree-path ./workspace/tidb-feature --force` +- `ti configure` +- `ti configure --non-interactive` +- `ti help` +- `ti --version` +- `ti help` +- `ti help` +- `ti --version` +- `ti --version` +- `ti update --check` +- `ti update --check --fail-if-update-available` +- `ti update --dry-run` +- `ti update` +- `ti update --target-version v0.1.1` +- `ti organization list-projects` +- `ti organization list-projects --query 'projects[0].id'` +- `ti organization list-projects --output text` +- `ti db create-db-cluster --db-cluster-name demo` +- `ti db create-db-cluster --db-cluster-name demo --wait` +- `ti db create-db-cluster --db-cluster-name demo --dry-run` +- `ti db create-db-cluster --db-cluster-name demo --project-id ` +- `ti db list-db-clusters` +- `ti db list-db-clusters --query 'clusters[].id'` +- `ti db describe-db-cluster --db-cluster-id ` +- `ti db update-db-cluster --db-cluster-id --db-cluster-name new-name` +- `ti db update-db-cluster --db-cluster-id --monthly-spending-limit-usd-cents 1000 --dry-run` +- `ti db delete-db-cluster --db-cluster-id ` +- `ti db delete-db-cluster --db-cluster-id --wait` +- `ti db delete-db-cluster --db-cluster-id --dry-run` +- `ti db create-db-cluster-branch --db-cluster-id --db-cluster-branch-name dev` +- `ti db create-db-cluster-branch --db-cluster-id --db-cluster-branch-name dev --wait` +- `ti db create-db-cluster-branch --db-cluster-id --db-cluster-branch-name dev --dry-run` +- `ti db list-db-cluster-branches --db-cluster-id ` +- `ti db list-db-cluster-branches --db-cluster-id --query 'branches[].id'` +- `ti db list-db-cluster-branches --db-cluster-id --output text` +- `ti db describe-db-cluster-branch --db-cluster-id --db-cluster-branch-id ` +- `ti db delete-db-cluster-branch --db-cluster-id --db-cluster-branch-id ` +- `ti db delete-db-cluster-branch --db-cluster-id --db-cluster-branch-id --dry-run` +- `ti db create-db-sql-users --db-cluster-id ` +- `ti db create-db-sql-users --db-cluster-id --dry-run` +- `ti db format-db-connection-string --db-cluster-id ` +- `ti db format-db-connection-string --db-cluster-id --read-write --format mysql-uri` +- `ti db format-db-connection-string --db-cluster-id --read-only --format env` +- `ti db format-db-connection-string --db-cluster-id --admin --format jdbc` +- `ti db execute-sql-statement --db-cluster-id --sql "select 1"` +- `ti db execute-sql-statement --db-cluster-id --read-write --sql "select 1"` +- `ti db execute-sql-statement --db-cluster-id --read-only --sql "select 1"` +- `ti db execute-sql-statement --db-cluster-id --admin --sql "select 1"` +- `ti db execute-sql-statement --db-cluster-id --transport https --sql "select 1"` +- `ti db execute-sql-statement --db-cluster-id --transport mysql --sql "select 1"` +- `ti fs create-file-system --file-system-name workspace` +- `ti fs create-file-system --file-system-name workspace --wait` +- `ti fs create-file-system --file-system-name workspace --dry-run` +- `ti fs create-file-system --file-system-name scratch` +- `ti fs delete-file-system --file-system-name workspace` +- `ti fs delete-file-system --file-system-name workspace --dry-run` +- `ti fs list-file-systems` +- `ti fs describe-file-system --file-system-name workspace` +- `ti fs check-file-system` +- `ti fs check-file-system --file-system-name workspace` +- `ti fs copy-file --from-local ./README.md --to-remote /workspace/README.md` +- `ti fs copy-file --from-remote /workspace/README.md --to-local ./README.copy.md --create-parents` +- `ti fs copy-file --from-remote /workspace/README.md --to-remote /workspace/README.copy.md` +- `ti fs read-file --path /workspace/README.md` +- `ti fs read-file --path /workspace/README.md --offset 0 --length 1024` +- `ti fs copy-file --from-local ./tail.log --to-remote /workspace/app.log --append` +- `ti fs copy-file --from-remote /workspace/large.bin --to-local ./large.bin --resume` +- `ti fs copy-file --from-local ./large.bin --to-remote /workspace/large.bin --resume` +- `ti fs copy-file --from-local ./src-dir --to-remote /workspace/src-dir --recursive` +- `ti fs copy-file --from-remote /workspace/src-dir --to-local ./src-dir.copy --recursive` +- `ti fs copy-file --from-remote /workspace/src-dir --to-remote /workspace/src-dir.copy --recursive` +- `ti fs copy-file --from-stdin --to-remote /workspace/stdin.txt --tag source=stdin --description "stdin upload"` +- `ti fs copy-file --from-remote /workspace/stdin.txt --to-stdout` +- `ti fs list-files --path /workspace` +- `ti fs list-files --path /workspace --output text` +- `ti fs describe-file --path /workspace/README.md` +- `ti fs move-file --from-remote /workspace/README.copy.md --to-remote /workspace/archive/README.md` +- `ti fs delete-file --path /workspace/archive/README.md` +- `ti fs delete-file --path /workspace --recursive` +- `ti fs create-directory --path /workspace/archive --mode 0755` +- `ti fs chmod-file --path /workspace/README.md --mode 0600` +- `ti fs create-symlink --target README.md --link-path /workspace/README.link` +- `ti fs create-hardlink --source-path /workspace/README.md --link-path /workspace/README.hard` +- `ti fs search-file-content --path /workspace --pattern "hello"` +- `ti fs search-file-content --path /workspace --pattern "hello" --layer-id layer-1` +- `ti fs find-files --path /workspace --file-name-pattern "*.md"` +- `ti fs find-files --path /workspace --file-name-pattern "*.md" --layer-id layer-1` +- `ti fs create-layer --layer-id layer-1 --base-root-path /workspace --layer-name task --durability-mode restore-safe --tag task=auth` +- `ti fs list-layers` +- `ti fs list-layers --output text` +- `ti fs describe-layer --layer-id layer-1` +- `ti fs diff-layer --layer-id layer-1` +- `ti fs copy-file --from-local ./README.md --to-remote /workspace/layered.md --layer-id layer-1` +- `ti fs create-layer-checkpoint --layer-id layer-1 --checkpoint-id cp-1 --label before-commit` +- `ti fs rollback-layer --layer-id layer-1` +- `ti fs commit-layer --layer-id layer-1` +- `ti fs pack-file-system --local-root ~/.ti/local/fs/demo --remote-root /workspace --mount-profile portable` +- `ti fs pack-file-system --mount-path ./workspace` +- `ti fs unpack-file-system --local-root ~/.ti/local/fs/demo --remote-root /workspace --mount-profile portable` +- `ti fs mount-file-system --file-system-name workspace --mount-path ./workspace` +- `ti fs mount-file-system --file-system-name workspace --mount-path ./workspace --driver fuse` +- `ti fs mount-file-system --file-system-name workspace --mount-path ./workspace --driver webdav` +- `ti fs mount-file-system --file-system-name workspace --mount-path ./workspace --mount-profile coding-agent` +- `ti fs mount-file-system --file-system-name workspace --mount-path ./workspace --mount-profile portable --pack-path /` +- `ti fs mount-file-system --file-system-name workspace --mount-path ./workspace --driver fuse --read-cache-size-mb 256 --read-cache-max-file-mb 16` +- `ti fs mount-file-system --file-system-name workspace --mount-path ./workspace --driver fuse --cache-dir ~/.ti/cache/workspace --write-back-cache=false` +- `ti fs drain-file-system --mount-path ./workspace` +- `ti fs drain-file-system --mount-path ./workspace --timeout 30s` +- `ti fs unmount-file-system --mount-path ./workspace` +- `ti fs unmount-file-system --mount-path ./workspace --ignore-absent` +- `ti fs-vault create-secret --secret-name db-prod --field DB_URL=mysql://example --field PASSWORD=@./password.txt` +- `ti fs-vault replace-secret --secret-path /n/vault/db-prod --from-directory ./secret-fields` +- `ti fs-vault read-secret --secret-name db-prod` +- `ti fs-vault read-secret --secret-name db-prod --field PASSWORD --format raw` +- `ti fs-vault read-secret --secret-name db-prod --field DB_URL --format env` +- `ti fs-vault list-secrets` +- `ti fs-vault delete-secret --secret-name db-prod` +- `ti fs-vault create-grant --agent-id deploy-agent --scope db-prod/DB_URL --permission read --ttl 10m` +- `ti fs-vault delete-grant --grant-id --reason rotated` +- `ti fs-vault list-audit-events --secret-name db-prod --limit 20` +- `ti fs-vault run-with-secret --secret-path /n/vault/db-prod -- env` +- `ti fs-vault mount-vault --mount-path ./vault --vault-token "$TI_VAULT_TOKEN"` +- `ti fs-vault unmount-vault --mount-path ./vault` +- `ti fs-journal create-journal --journal-id jrn-demo --journal-kind agent --title "demo task" --actor agent:ti --label env=dev` +- `ti fs-journal append-journal-entries --journal-id jrn-demo --entry-json '{"type":"task.started"}'` +- `ti fs-journal read-journal-entries --journal-id jrn-demo --after-seq 0 --limit 100` +- `ti fs-journal search-journal-entries --entry-type task.started --label env=dev --include-entries` +- `ti fs-journal verify-journal --journal-id jrn-demo --output text` +- `ti fs-git clone-git-workspace --repo-url https://github.com/pingcap/tidb.git --target-path ./workspace/tidb` +- `ti fs-git clone-git-workspace --repo-url https://github.com/pingcap/tidb.git --target-path ./workspace/tidb --blobless --hydrate sync` +- `ti fs-git hydrate-git-workspace --target-path ./workspace/tidb --timeout 30m` +- `ti fs-git add-git-worktree --base-path ./workspace/tidb --worktree-path ./workspace/tidb-feature --branch-name feature-x` +- `ti fs-git remove-git-worktree --worktree-path ./workspace/tidb-feature --force` Registered command surface: -- `tdc update --check` -- `tdc update` -- `tdc organization list-projects` -- `tdc db create-db-cluster` -- `tdc db list-db-clusters` -- `tdc db describe-db-cluster` -- `tdc db update-db-cluster` -- `tdc db delete-db-cluster` -- `tdc db create-db-cluster-branch` -- `tdc db list-db-cluster-branches` -- `tdc db describe-db-cluster-branch` -- `tdc db delete-db-cluster-branch` -- `tdc db create-db-sql-users` -- `tdc db format-db-connection-string` -- `tdc db execute-sql-statement` -- `tdc fs create-file-system` -- `tdc fs delete-file-system` -- `tdc fs list-file-systems` -- `tdc fs describe-file-system` -- `tdc fs check-file-system` -- `tdc fs copy-file` -- `tdc fs read-file` -- `tdc fs list-files` -- `tdc fs describe-file` -- `tdc fs move-file` -- `tdc fs delete-file` -- `tdc fs create-directory` -- `tdc fs chmod-file` -- `tdc fs create-symlink` -- `tdc fs create-hardlink` -- `tdc fs search-file-content` -- `tdc fs find-files` -- `tdc fs create-layer` -- `tdc fs list-layers` -- `tdc fs describe-layer` -- `tdc fs diff-layer` -- `tdc fs create-layer-checkpoint` -- `tdc fs rollback-layer` -- `tdc fs commit-layer` -- `tdc fs pack-file-system` -- `tdc fs unpack-file-system` -- `tdc fs mount-file-system` -- `tdc fs drain-file-system` -- `tdc fs unmount-file-system` -- `tdc fs cp` aliases `tdc fs copy-file` -- `tdc fs cat` aliases `tdc fs read-file` -- `tdc fs ls` aliases `tdc fs list-files` -- `tdc fs stat` aliases `tdc fs describe-file` -- `tdc fs mv` aliases `tdc fs move-file` -- `tdc fs rm` aliases `tdc fs delete-file` -- `tdc fs mkdir` aliases `tdc fs create-directory` -- `tdc fs chmod` aliases `tdc fs chmod-file` -- `tdc fs symlink` aliases `tdc fs create-symlink` -- `tdc fs hardlink` aliases `tdc fs create-hardlink` -- `tdc fs grep` aliases `tdc fs search-file-content` -- `tdc fs find` aliases `tdc fs find-files` -- `tdc fs mount` aliases `tdc fs mount-file-system` -- `tdc fs drain` aliases `tdc fs drain-file-system` -- `tdc fs umount` aliases `tdc fs unmount-file-system` -- `tdc fs-vault create-secret` -- `tdc fs-vault replace-secret` -- `tdc fs-vault read-secret` -- `tdc fs-vault list-secrets` -- `tdc fs-vault delete-secret` -- `tdc fs-vault create-grant` -- `tdc fs-vault delete-grant` -- `tdc fs-vault list-audit-events` -- `tdc fs-vault run-with-secret` -- `tdc fs-vault mount-vault` -- `tdc fs-vault unmount-vault` -- `tdc fs-journal create-journal` -- `tdc fs-journal append-journal-entries` -- `tdc fs-journal read-journal-entries` -- `tdc fs-journal search-journal-entries` -- `tdc fs-journal verify-journal` -- `tdc fs-git clone-git-workspace` -- `tdc fs-git hydrate-git-workspace` -- `tdc fs-git add-git-worktree` -- `tdc fs-git remove-git-worktree` +- `ti update --check` +- `ti update` +- `ti organization list-projects` +- `ti db create-db-cluster` +- `ti db list-db-clusters` +- `ti db describe-db-cluster` +- `ti db update-db-cluster` +- `ti db delete-db-cluster` +- `ti db create-db-cluster-branch` +- `ti db list-db-cluster-branches` +- `ti db describe-db-cluster-branch` +- `ti db delete-db-cluster-branch` +- `ti db create-db-sql-users` +- `ti db format-db-connection-string` +- `ti db execute-sql-statement` +- `ti fs create-file-system` +- `ti fs delete-file-system` +- `ti fs list-file-systems` +- `ti fs describe-file-system` +- `ti fs check-file-system` +- `ti fs copy-file` +- `ti fs read-file` +- `ti fs list-files` +- `ti fs describe-file` +- `ti fs move-file` +- `ti fs delete-file` +- `ti fs create-directory` +- `ti fs chmod-file` +- `ti fs create-symlink` +- `ti fs create-hardlink` +- `ti fs search-file-content` +- `ti fs find-files` +- `ti fs create-layer` +- `ti fs list-layers` +- `ti fs describe-layer` +- `ti fs diff-layer` +- `ti fs create-layer-checkpoint` +- `ti fs rollback-layer` +- `ti fs commit-layer` +- `ti fs pack-file-system` +- `ti fs unpack-file-system` +- `ti fs mount-file-system` +- `ti fs drain-file-system` +- `ti fs unmount-file-system` +- `ti fs cp` aliases `ti fs copy-file` +- `ti fs cat` aliases `ti fs read-file` +- `ti fs ls` aliases `ti fs list-files` +- `ti fs stat` aliases `ti fs describe-file` +- `ti fs mv` aliases `ti fs move-file` +- `ti fs rm` aliases `ti fs delete-file` +- `ti fs mkdir` aliases `ti fs create-directory` +- `ti fs chmod` aliases `ti fs chmod-file` +- `ti fs symlink` aliases `ti fs create-symlink` +- `ti fs hardlink` aliases `ti fs create-hardlink` +- `ti fs grep` aliases `ti fs search-file-content` +- `ti fs find` aliases `ti fs find-files` +- `ti fs mount` aliases `ti fs mount-file-system` +- `ti fs drain` aliases `ti fs drain-file-system` +- `ti fs umount` aliases `ti fs unmount-file-system` +- `ti fs-vault create-secret` +- `ti fs-vault replace-secret` +- `ti fs-vault read-secret` +- `ti fs-vault list-secrets` +- `ti fs-vault delete-secret` +- `ti fs-vault create-grant` +- `ti fs-vault delete-grant` +- `ti fs-vault list-audit-events` +- `ti fs-vault run-with-secret` +- `ti fs-vault mount-vault` +- `ti fs-vault unmount-vault` +- `ti fs-journal create-journal` +- `ti fs-journal append-journal-entries` +- `ti fs-journal read-journal-entries` +- `ti fs-journal search-journal-entries` +- `ti fs-journal verify-journal` +- `ti fs-git clone-git-workspace` +- `ti fs-git hydrate-git-workspace` +- `ti fs-git add-git-worktree` +- `ti fs-git remove-git-worktree` Do not rename commands without updating specs, README, e2e tests, and AGENTS. Any code change that changes user-visible behavior must keep README.md in sync. -It must also update the matching English tdc pages under +It must also update the matching English ti pages under `docs/pingcap-docs/docs/` when the published behavior changes. Use globally -unique `tdc-` basenames, preserve the standard Preview note on every page, and +unique `ti-` basenames, preserve the standard Preview note on every page, and update `TOC-ai.md` and `ai/_index.md` when adding or removing a document. Validate command names and flags against the compiled CLI help, not historical specs or demos. ## Configuration And Credentials -All tdc local state belongs under `~/.tdc/`. - -- `~/.tdc/config` stores profile-scoped non-sensitive TOML values. -- `~/.tdc/credentials` stores profile-scoped sensitive TOML values. +All ti local state belongs under `~/.ti/`. + +For v0.2.x, `internal/config/homemigration` performs a one-way migration when +only `~/.tdc` exists. It copies only durable config, credential, preferences, +telemetry identity, DB-user, and filesystem-registry state through a sibling +staging directory, validates it, then atomically publishes `~/.ti`. It never +modifies or deletes `~/.tdc`. Both homes, unsafe source files, or an active old +mount are hard errors unless the new home contains the valid owner-only +`.migrated-from-tdc` marker produced by the atomic migration. Runtime logs, +caches, local data, mount locators, and the old Drive9 home are not migrated. +Normal non-update commands and both installers use the same Go implementation; +every update mode bypasses it. + +Canonical environment variables use `TI_*`, except TiDB Cloud API credentials +which use `TIDB_CLOUD_PUBLIC_KEY` and `TIDB_CLOUD_PRIVATE_KEY`. During v0.2.x, +the corresponding `TDC_*` variables are accepted only as deterministic legacy +fallbacks. If both names are present with different values, fail with +`config.environment_conflict` before local or remote mutation. Never print a +legacy-variable warning into command output; operation logs may record only +the deprecated variable name. + +- `~/.ti/config` stores profile-scoped non-sensitive TOML values. +- `~/.ti/credentials` stores profile-scoped sensitive TOML values. - Both files use profile sections such as `[default]` and `[stage]`. -- `~/.tdc/.preferences` is optional hidden global TOML configuration and is - never selected by profile. Fresh installs and `tdc configure` do not create - it. Do not create or migrate the unshipped intermediate `~/.tdc/settings` +- `~/.ti/.preferences` is optional hidden global TOML configuration and is + never selected by profile. Fresh installs and `ti configure` do not create + it. Do not create or migrate the unshipped intermediate `~/.ti/settings` path. -- `~/.tdc/.telemetry-installation-id` is a machine-generated pseudonymous +- `~/.ti/.telemetry-installation-id` is a machine-generated pseudonymous telemetry identity, not TOML configuration. It is created lazily with mode `0600` only for an eligible, effectively enabled invocation with a build-configured endpoint. Users can delete it to reset the identity. @@ -667,15 +700,15 @@ All tdc local state belongs under `~/.tdc/`. - The global `--profile` flag selects a profile when explicitly provided. - The global `--region` flag selects command-scope placement when explicitly provided and must reject an explicit empty value. -- `tdc configure` writes canonical `region_code`, discovers the unique - `tidbx_virtual` project as `project_id`, and writes `tdc_public_key` and - `tdc_private_key`. -- `tdc configure --non-interactive` must not prompt. It reads values from flags - first, then `TDC_REGION_CODE`, `TDC_PUBLIC_KEY`, and `TDC_PRIVATE_KEY`. +- `ti configure` writes canonical `region_code`, discovers the unique + `tidbx_virtual` project as `project_id`, and writes + `tidb_cloud_public_key` and `tidb_cloud_private_key`. +- `ti configure --non-interactive` must not prompt. It reads values from flags + first, then `TI_REGION_CODE`, `TIDB_CLOUD_PUBLIC_KEY`, and `TIDB_CLOUD_PRIVATE_KEY`. Missing values fail with an actionable error. - For CI/CD, prefer environment variables for private keys over command-line secret flags. -- Interactive `tdc configure` must respond to Ctrl+C and surface an +- Interactive `ti configure` must respond to Ctrl+C and surface an `interrupted` error with exit code 130. - The credentials file is restricted to owner read/write permissions where POSIX mode bits are meaningful. @@ -683,38 +716,38 @@ All tdc local state belongs under `~/.tdc/`. Typical configured profile keys: ```toml -# ~/.tdc/config +# ~/.ti/config [default] region_code = "aws-us-east-1" project_id = "..." -# ~/.tdc/credentials +# ~/.ti/credentials [default] -tdc_public_key = "..." -tdc_private_key = "..." +tidb_cloud_public_key = "..." +tidb_cloud_private_key = "..." ``` -`project_id` is written by `tdc configure` but is not required to create a +`project_id` is written by `ti configure` but is not required to create a Starter cluster. If it is absent and `--project-id` is not provided, the create request omits the project label and TiDB Cloud selects the account default. -One profile can own multiple tdc fs resources. The main config stores neither a +One profile can own multiple ti fs resources. The main config stores neither a default resource name nor resource credentials. Each resource stores metadata and credentials separately: ```text -~/.tdc/fs_resources///config -~/.tdc/fs_resources///credentials +~/.ti/fs_resources///config +~/.ti/fs_resources///credentials ``` Resource config files contain `file_system_name`, `tenant_id`, `cloud_provider`, `region_code`, and `created_at`. Resource credentials files contain only `api_key`, use mode `0600`, and must never be written to the main -`~/.tdc/credentials` file. Profile and resource path segments are safely +`~/.ti/credentials` file. Profile and resource path segments are safely encoded; always use the stored `file_system_name` for user-facing output. -`tdc fs create-file-system` returns the stored owner credential as `fs_token`; +`ti fs create-file-system` returns the stored owner credential as `fs_token`; this is the only ordinary command result that may reveal it. Treat `fs_token` as a secret and never include it in logs, telemetry, debug output, errors, mount locators, non-secret config, or test diagnostics. @@ -728,22 +761,22 @@ flat fields. Incomplete legacy state fails with DB SQL user credentials live outside the main credentials file: ```text -~/.tdc/db_users//credentials +~/.ti/db_users//credentials ``` That file uses role sections: ```toml [read_only] -username = "prefix.tdc_ro" +username = "prefix.ti_ro" password = "..." [read_write] -username = "prefix.tdc_rw" +username = "prefix.ti_rw" password = "..." [admin] -username = "prefix.tdc_admin" +username = "prefix.ti_admin" password = "..." ``` @@ -754,8 +787,8 @@ overrides, if added later, must be hidden from ordinary user workflows and must not be required by MVP usage. TiDB Cloud control-plane API calls use HTTP Digest auth through -`internal/api/transport`; never send `tdc_private_key` as Basic Auth for those -APIs. SQL HTTPS API execution and tdc fs data-plane auth are separate +`internal/api/transport`; never send `tidb_cloud_private_key` as Basic Auth for those +APIs. SQL HTTPS API execution and ti fs data-plane auth are separate authentication schemes. SQL HTTPS API execution uses the prepared DB SQL username/password as Basic Auth against `https://http-/v1beta/sql`; TiDB Cloud API keys must not be used @@ -764,41 +797,41 @@ for SQL execution Basic Auth. Use `internal/api/endpoints` for Starter, IAM/account, and fs endpoint selection. Do not add service URLs to user config. The default Starter host is `https://serverless.tidbapi.com`; the default IAM host is -`https://iam.tidbapi.com`. The tdc fs host is resolved from the hosted tdc fs +`https://iam.tidbapi.com`. The ti fs host is resolved from the hosted ti fs region manifest at `https://drive9.ai/manifest/regions/drive9-regions.json`, matching the active profile's cloud provider and region against `tidb_cloud_native` entries. If the manifest does not contain the profile placement, return a clear unsupported endpoint error; do not add a user-facing raw server URL flag or config key. -Tests may override the IAM base URL with `TDC_TEST_IAM_BASE_URL` and the fs -manifest URL with `TDC_TEST_FS_MANIFEST_URL`, only when -`TDC_ALLOW_TEST_ENDPOINTS=1`; these are hidden test controls, not supported +Tests may override the IAM base URL with `TI_TEST_IAM_BASE_URL` and the fs +manifest URL with `TI_TEST_FS_MANIFEST_URL`, only when +`TI_ALLOW_TEST_ENDPOINTS=1`; these are hidden test controls, not supported user configuration. Local profile namespace lookup order for authenticated commands: 1. If `--profile ` is explicitly provided, use that profile name. -2. If `TDC_PROFILE` is set, use that profile name. +2. If `TI_PROFILE` is set, use that profile name. 3. Otherwise use `default`. TiDB Cloud API key lookup order: -1. If either `TDC_PUBLIC_KEY` or `TDC_PRIVATE_KEY` is set, read the API key pair +1. If either `TIDB_CLOUD_PUBLIC_KEY` or `TIDB_CLOUD_PRIVATE_KEY` is set, read the API key pair from environment variables. Both are required in this mode. -2. Otherwise read `tdc_public_key` and `tdc_private_key` from the selected - local profile in `~/.tdc/credentials`. +2. Otherwise read `tidb_cloud_public_key` and `tidb_cloud_private_key` from the + selected local profile in `~/.ti/credentials`. Placement lookup order for authenticated commands: 1. If `--region ` is explicitly provided, use it for this command only. -2. If `TDC_REGION_CODE` is set, use it for this command only. +2. If `TI_REGION_CODE` is set, use it for this command only. 3. Otherwise use the selected profile's `region_code`. Starter DB cluster creation project lookup order is: 1. Explicit non-empty `--project-id`. -2. The selected profile's `project_id`, discovered by `tdc configure` from the +2. The selected profile's `project_id`, discovered by `ti configure` from the unique accessible project whose type is `tidbx_virtual`. 3. Otherwise omit the `tidb.cloud/project` label and let the Starter API select the account's default project. @@ -807,40 +840,40 @@ An explicitly empty `--project-id` is an error and must not use the profile or server fallback. When no project ID resolves, omit the label entirely; do not send `tidb.cloud/project` with an empty value. Other DB commands identify existing resources by cluster or branch ID and do not send `project_id`. -Drive9-backed tdc fs commands do not consume this DB project default. +Drive9-backed ti fs commands do not consume this DB project default. Environment credentials are a credential source only; they must not change the -local profile namespace and must not cause tdc to write local `[env]` sections. -Generated tdc fs state is always stored under the selected local profile: -`--profile`, `TDC_PROFILE`, or `default`. +local profile namespace and must not cause ti to write local `[env]` sections. +Generated ti fs state is always stored under the selected local profile: +`--profile`, `TI_PROFILE`, or `default`. -tdc fs resource selection order is: +ti fs resource selection order is: 1. Explicit `--file-system-name`. -2. `TDC_FS_FILE_SYSTEM_NAME`. +2. `TI_FS_FILE_SYSTEM_NAME`. 3. Otherwise fail with `fs.missing_file_system_name` before credential loading, endpoint resolution, companion startup, or a remote request. -Never infer a tdc fs resource from profile state, registry cardinality, -creation order, or deletion side effects. `TDC_FS_FILE_SYSTEM_NAME` is an +Never infer a ti fs resource from profile state, registry cardinality, +creation order, or deletion side effects. `TI_FS_FILE_SYSTEM_NAME` is an explicit process-scoped selector and must not be persisted. -Remote tdc fs, fs-git, fs-journal, and owner fs-vault commands use this FS +Remote ti fs, fs-git, fs-journal, and owner fs-vault commands use this FS credential lookup order: 1. Explicit command-local `--fs-token`. -2. `TDC_FS_TOKEN`. +2. `TI_FS_TOKEN`. 3. The selected resource's `api_key` in its resource-scoped credentials file. Those commands do not require TiDB Cloud public/private keys. A clean machine can use an existing resource with a file-system name, canonical region, and FS token supplied independently through flags or environment variables. Do not persist ephemeral flag/environment credentials or create a synthetic `[env]` -profile. `tdc fs create-file-system` and `tdc fs delete-file-system` remain +profile. `ti fs create-file-system` and `ti fs delete-file-system` remain TiDB Cloud-authenticated; deletion also requires the selected locally registered resource and its owner token. -The selector is available on tdc fs data-plane/runtime commands and all +The selector is available on ti fs data-plane/runtime commands and all `fs-git`, `fs-journal`, and `fs-vault` subcommands. Creation, deletion, and description require an explicit resource name where their command contract declares it. Drain and unmount resolve an existing mount through its mount path @@ -848,7 +881,7 @@ and locator instead of selecting a resource again. When implementing command handlers, detect whether `--profile` was explicitly set before calling `config.Load`; the root flag has a default value, but that -default must not suppress `TDC_PROFILE`. Also pass the explicit `--region` +default must not suppress `TI_PROFILE`. Also pass the explicit `--region` value into profile loading so endpoint selection sees the override. Supported MVP placement values: @@ -870,12 +903,12 @@ Do not store secrets in logs, telemetry, generated docs examples, or test fixtures. Local operation logs are enabled by default and live at -`~/.tdc/logs/tdc.jsonl`. They are local audit/debug summaries, not telemetry. -`TDC_LOGGING=off` disables them for the current process, and global settings +`~/.ti/logs/ti.jsonl`. They are local audit/debug summaries, not telemetry. +`TI_LOGGING=off` disables them for the current process, and global settings can disable them with: ```toml -# ~/.tdc/.preferences +# ~/.ti/.preferences schema_version = 1 [logging] @@ -884,72 +917,72 @@ enabled = false Environment values `off`, `false`, `0`, and `no` disable logging; `on`, `true`, `1`, and `yes` enable it. The environment variable takes precedence -over settings. Do not add a `tdc logging status` command. Invalid settings or +over settings. Do not add a `ti logging status` command. Invalid settings or environment values fail closed for logging without failing the requested -command. Legacy `[logging]` in `~/.tdc/config` is migrated atomically into -`~/.tdc/.preferences`; config and credentials remain profile-only afterward. -Every `tdc update` form must bypass settings, migration, profiles, credentials, -operation logs, and all other `~/.tdc/` state. The operation log may +command. Legacy `[logging]` in `~/.ti/config` is migrated atomically into +`~/.ti/.preferences`; config and credentials remain profile-only afterward. +Every `ti update` form must bypass settings, migration, profiles, credentials, +operation logs, and all other `~/.ti/` state. The operation log may record command paths, flag names, profile names, region codes, duration, exit code, app error code/category, service name, HTTP method/status, operation, and request id. It must never record flag values, SQL text, SQL results, file -contents, raw request/response bodies, connection strings, local paths, tdc fs -raw paths, API keys, DB passwords, or tdc fs API keys. +contents, raw request/response bodies, connection strings, local paths, ti fs +raw paths, API keys, DB passwords, or ti fs API keys. Generated DB SQL usernames and passwords live in -`~/.tdc/db_users//credentials`, not in the main -`~/.tdc/credentials` file. Do not add nested +`~/.ti/db_users//credentials`, not in the main +`~/.ti/credentials` file. Do not add nested `[profile.db_users."".role]` TOML sections to -`~/.tdc/credentials`. TiDB Cloud cluster IDs are globally unique, so DB SQL -credentials are cluster-scoped rather than profile-scoped. `tdc db +`~/.ti/credentials`. TiDB Cloud cluster IDs are globally unique, so DB SQL +credentials are cluster-scoped rather than profile-scoped. `ti db create-db-sql-users` owns those credentials and must be idempotent: it -creates or repairs the stable tdc-managed read-only, read-write, and admin +creates or repairs the stable ti-managed read-only, read-write, and admin users for a cluster instead of creating a new group every time. -Generated `tdc fs` resource API keys live only in the per-resource credentials -files under `~/.tdc/fs_resources/`. User-facing docs and commands must call -these `tdc fs` API keys or resource credentials, never reference implementation +Generated `ti fs` resource API keys live only in the per-resource credentials +files under `~/.ti/fs_resources/`. User-facing docs and commands must call +these `ti fs` API keys or resource credentials, never reference implementation API keys. Filesystem data-plane commands route through the installer-managed Drive9 companion binary named -`tdc-drive9`. tdc owns profile loading, region resolution, credential storage, +`ti-drive9`. ti owns profile loading, region resolution, credential storage, preflight errors, output/query handling, and command naming; Drive9 owns the filesystem runtime semantics for data-plane file operations, FUSE/WebDAV mount, FUSE mount drain, layer behavior, pack/unpack, Git workflows, journal, and vault. -Do not reintroduce a runtime fallback to tdc-native fs behavior. Public fs +Do not reintroduce a runtime fallback to ti-native fs behavior. Public fs service methods must route through the Drive9 companion path unconditionally; do not add switches such as `UseDrive9Companion` or hidden environment flags -that select old tdc HTTP/FUSE/WebDAV implementations. +that select old ti HTTP/FUSE/WebDAV implementations. The companion runs with resource-scoped isolated state under -`~/.tdc/drive9-home//`; do not write or require user +`~/.ti/drive9-home//`; do not write or require user edits to `~/.drive9`. Never use a shared Drive9 `current_context` as the source -of truth for tdc resource selection. +of truth for ti resource selection. Background FS and vault mounts write only a non-secret locator under -`~/.tdc/mounts/`. Drain and unmount must route through that locator to the +`~/.ti/mounts/`. Drain and unmount must route through that locator to the original resource-scoped companion HOME without requiring the FS token again. Successful unmount removes the locator; failed unmount preserves it for retry. -Do not implement or expose tdc commands for Drive9 internal APIs that are not +Do not implement or expose ti commands for Drive9 internal APIs that are not part of Drive9's public CLI. In particular, do not reintroduce low-level layer entry/object/event commands, low-level Git workspace/tree/state/object-pack/ overlay commands, or legacy vault token commands unless Drive9 exposes a -matching public command and the tdc command surface is intentionally updated in -README, specs, tests, and AGENTS. Use `TDC_DRIVE9_BIN` only as a developer/test +matching public command and the ti command surface is intentionally updated in +README, specs, tests, and AGENTS. Use `TI_DRIVE9_BIN` only as a developer/test override for a compatible companion; normal installs should rely on the -installer-managed `tdc-drive9`. +installer-managed `ti-drive9`. -`tdc fs create-directory --mode` is a compatibility flag only in the Drive9 +`ti fs create-directory --mode` is a compatibility flag only in the Drive9 companion path: validate the octal value, but do not emulate directory chmod -with a non-public backend call. `tdc fs chmod-file` remains the explicit chmod +with a non-public backend call. `ti fs chmod-file` remains the explicit chmod command and should follow Drive9 public CLI behavior. -`tdc fs-vault mount-vault` requires a delegated vault token from -`tdc fs-vault create-grant`; the selected resource's owner API key is used for +`ti fs-vault mount-vault` requires a delegated vault token from +`ti fs-vault create-grant`; the selected resource's owner API key is used for `create-secret`, `read-secret`, `list-secrets`, `replace-secret`, `delete-secret`, grants, audit, and `run-with-secret`, but not for the vault mount consumption path. -`tdc fs drain-file-system` is meaningful only for FUSE mounts where the +`ti fs drain-file-system` is meaningful only for FUSE mounts where the companion records a drain control socket. WebDAV mounts flush through normal file close semantics and should not be expected to support drain. @@ -958,11 +991,11 @@ sanitized environment: `HOME` from that resource's scoped companion directory, `DRIVE9_SERVER` from its resolved endpoint, `DRIVE9_REGION_CODE` from its canonical resource region, `DRIVE9_API_KEY` from its per-resource credentials, and TiDB Cloud public/private keys only for provision/delete flows. Strip -inherited `DRIVE9_*` values so user shell state cannot override tdc selection. -Debug and error output must redact TiDB Cloud keys, tdc fs API keys, vault tokens, SQL +inherited `DRIVE9_*` values so user shell state cannot override ti selection. +Debug and error output must redact TiDB Cloud keys, ti fs API keys, vault tokens, SQL credentials, file contents, and secret values. -`tdc db format-db-connection-string` and `tdc db execute-sql-statement` use +`ti db format-db-connection-string` and `ti db execute-sql-statement` use read-write credentials by default. `--read-write`, `--read-only`, and `--admin` must be mutually exclusive explicit selections. Do not add SQL-text classification or an automatic access mode. @@ -985,7 +1018,7 @@ Use structured output contracts from the start. - Errors follow this shape: ```text -tdc [ERROR]: +ti [ERROR]: ``` Library code returns errors instead of printing or exiting. Only the CLI @@ -994,12 +1027,12 @@ boundary writes to stdout/stderr and maps errors to exit codes. ## Telemetry Rules The product-owned telemetry backend is implemented as the independent -`tdc-telemetry-backend` process. The CLI collection and delivery path is +`ti-telemetry-backend` process. The CLI collection and delivery path is implemented by `internal/telemetry` under `docs/spec/done/0022-telemetry.md`. Release archives contain a build-configured -product endpoint; local builds and CI default to disabled. `TDC_TELEMETRY` -overrides `[telemetry].enabled` in `~/.tdc/.preferences`. Help, version, -commandless usage, and all `tdc update` forms must be excluded before telemetry +product endpoint; local builds and CI default to disabled. `TI_TELEMETRY` +overrides `[telemetry].enabled` in `~/.ti/.preferences`. Help, version, +commandless usage, and all `ti update` forms must be excluded before telemetry reads preferences or installation state. Allowed fields: - command and subcommand invoked @@ -1008,8 +1041,8 @@ reads preferences or installation state. Allowed fields: - TiDB Cloud provider and canonical region - CLI version, OS, architecture, and install source - anonymous installation ID and profile source category, never profile name -- an explicit process-scoped `TDC_TELEMETRY_TAG` value, bounded to 128 UTF-8 bytes -- an explicit process-scoped `TDC_TELEMETRY_EXTRA` JSON value, accepted only when complete, bounded to 2 KiB, depth-limited, and free of prohibited field names +- an explicit process-scoped `TI_TELEMETRY_TAG` value, bounded to 128 UTF-8 bytes +- an explicit process-scoped `TI_TELEMETRY_EXTRA` JSON value, accepted only when complete, bounded to 2 KiB, depth-limited, and free of prohibited field names Never collect credentials, tokens, resource IDs, file contents, SQL text, query output, flag values, raw errors, profile names, local or remote paths, host @@ -1018,8 +1051,8 @@ best-effort HTTPS POST with a three-second hard timeout, no foreground retry, no redirect following, and no local durable queue. Any failure must preserve the command's stdout, stderr, output format, and exit status. -`TDC_TELEMETRY_TAG` and `TDC_TELEMETRY_EXTRA` are read only after telemetry is -eligible and enabled, are never persisted under `~/.tdc/`, and must never appear +`TI_TELEMETRY_TAG` and `TI_TELEMETRY_EXTRA` are read only after telemetry is +eligible and enabled, are never persisted under `~/.ti/`, and must never appear in normal output, errors, debug diagnostics, or operation logs. The CLI emits schema version 2; the backend must continue to accept schema version 1 without metadata during rollout. @@ -1050,8 +1083,8 @@ Current expectations: packages directly. - Unit tests should use temp home directories for config and credentials. - E2E tests should use temp `HOME` values and must not touch the user's real - `~/.tdc/`. -- Unit/e2e helpers should set `TDC_LOGGING=off` by default unless the test is + `~/.ti/`. +- Unit/e2e helpers should set `TI_LOGGING=off` by default unless the test is explicitly verifying operation logging. - API client tests should use mock HTTP servers once API specs are implemented. - Live cloud tests are opt-in, skipped by default, and run through diff --git a/Makefile b/Makefile index 7823533..11cc02c 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,14 @@ GO ?= go GORELEASER ?= goreleaser -MODULE := github.com/tidbcloud/tdc -BINARY_NAME := tdc +MODULE := github.com/tidbcloud/ti-cli +BINARY_NAME := ti BIN_DIR := bin -TDC_BIN := $(BIN_DIR)/$(BINARY_NAME) -TELEMETRY_BACKEND_BIN := $(BIN_DIR)/tdc-telemetry-backend -TELEMETRY_MIGRATOR_BIN := $(BIN_DIR)/tdc-telemetry-migrate +TI_BIN := $(BIN_DIR)/$(BINARY_NAME) +TELEMETRY_BACKEND_BIN := $(BIN_DIR)/ti-telemetry-backend +TELEMETRY_MIGRATOR_BIN := $(BIN_DIR)/ti-telemetry-migrate TELEMETRY_E2E_ENV := e2e/.env.telemetry LIVE_E2E_PROFILE ?= live-e2e -LIVE_E2E_RUN = TDC_E2E_BIN="$(abspath $(TDC_BIN))" TDC_LIVE=1 TDC_PROFILE="$(LIVE_E2E_PROFILE)" $(GO) test ./e2e -count=1 -v -timeout 30m +LIVE_E2E_RUN = TI_E2E_BIN="$(abspath $(TI_BIN))" TI_LIVE=1 TI_PROFILE="$(LIVE_E2E_PROFILE)" $(GO) test ./e2e -count=1 -v -timeout 30m VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo none) @@ -31,26 +31,26 @@ all: build build: @mkdir -p $(BIN_DIR) - $(GO) build -trimpath -ldflags "$(LDFLAGS)" -o $(TDC_BIN) ./cmd/tdc + $(GO) build -trimpath -ldflags "$(LDFLAGS)" -o $(TI_BIN) ./cmd/ti build-telemetry-backend: @mkdir -p $(BIN_DIR) - $(GO) build -trimpath -o $(TELEMETRY_BACKEND_BIN) ./cmd/tdc-telemetry-backend + $(GO) build -trimpath -o $(TELEMETRY_BACKEND_BIN) ./cmd/ti-telemetry-backend build-telemetry-migrator: @mkdir -p $(BIN_DIR) - $(GO) build -trimpath -o $(TELEMETRY_MIGRATOR_BIN) ./cmd/tdc-telemetry-migrate + $(GO) build -trimpath -o $(TELEMETRY_MIGRATOR_BIN) ./cmd/ti-telemetry-migrate test: $(GO) test ./... e2e: build - TDC_E2E_BIN="$(abspath $(TDC_BIN))" $(GO) test ./e2e -count=1 -v + TI_E2E_BIN="$(abspath $(TI_BIN))" $(GO) test ./e2e -count=1 -v telemetry-e2e: build build-telemetry-backend build-telemetry-migrator - @test -f "$(TELEMETRY_E2E_ENV)" || { echo "missing $(TELEMETRY_E2E_ENV); set TDC_TEST_TELEMETRY_TIDB_DSN in that ignored file" >&2; exit 2; } + @test -f "$(TELEMETRY_E2E_ENV)" || { echo "missing $(TELEMETRY_E2E_ENV); set TI_TEST_TELEMETRY_TIDB_DSN in that ignored file" >&2; exit 2; } @set -a; . "$(TELEMETRY_E2E_ENV)"; set +a; \ - TDC_E2E_BIN="$(abspath $(TDC_BIN))" TDC_TELEMETRY_BACKEND_E2E_BIN="$(abspath $(TELEMETRY_BACKEND_BIN))" TDC_TELEMETRY_MIGRATOR_E2E_BIN="$(abspath $(TELEMETRY_MIGRATOR_BIN))" TDC_TELEMETRY_E2E=1 $(GO) test ./e2e -count=1 -v -run '^TestTelemetryDeliveryToTiDB$$' + TI_E2E_BIN="$(abspath $(TI_BIN))" TI_TELEMETRY_BACKEND_E2E_BIN="$(abspath $(TELEMETRY_BACKEND_BIN))" TI_TELEMETRY_MIGRATOR_E2E_BIN="$(abspath $(TELEMETRY_MIGRATOR_BIN))" TI_TELEMETRY_E2E=1 $(GO) test ./e2e -count=1 -v -run '^TestTelemetryDeliveryToTiDB$$' live-e2e: build $(LIVE_E2E_RUN) -run '^TestLive' diff --git a/README.md b/README.md index 45ba2f5..8091ce4 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,58 @@ -# tdc +# ti -tdc ([TiDB Cloud](https://tidbcloud.com) CLI) is a unified tool to manage your TiDB Cloud Filesystem (FS) and Starter services. +ti ([TiDB Cloud](https://tidbcloud.com) CLI) is a unified tool to manage your TiDB Cloud Filesystem (FS) and Starter services. - TiDB Cloud Filesystem is a serverless distributed file system designed specifically for AI coding agent workloads. - TiDB Cloud Starter provides serverless distributed database clusters that are fully compatible with MySQL. -> `tdc` is currently in preview. Subcommands labeled as preview are subject to change without prior notice. +> `ti` is currently in preview. Subcommands labeled as preview are subject to change without prior notice. ## 3-Command Superpower for Your Agent ### Always-On File System for Sandboxes — Zero Infrastructure Required -With `tdc`, an agent can persist state between sessions, share files across sandboxes, snapshot its workspace before attempting a risky operation, and roll back on failure — all through a CLI with POSIX compatibility. +With `ti`, an agent can persist state between sessions, share files across sandboxes, snapshot its workspace before attempting a risky operation, and roll back on failure — all through a CLI with POSIX compatibility. 1. Create a file system and obtain the file system token (performed once, outside the sandbox). ```shell -export TDC_FS_TOKEN="$(tdc fs create-file-system --file-system-name agent-workspace --region --wait --query fs_token --output text)" +export TI_FS_TOKEN="$(ti fs create-file-system --file-system-name agent-workspace --region --wait --query fs_token --output text)" ``` 2. Mount the filesystem to a local path and use it as a normal POSIX-compliant filesystem (performed within the sandbox) ```shell -export TDC_FS_TOKEN="" -tdc fs mount-file-system --file-system-name agent-workspace --mount-path /path-to-workspace --region +export TI_FS_TOKEN="" +ti fs mount-file-system --file-system-name agent-workspace --mount-path /path-to-workspace --region echo "Hello Sandbox Workspace!" >> /path-to-workspace/hello.txt ``` 3. Unmount the file system to release the workspace before passing it to another sandbox (performed within the sandbox). ```shell -tdc fs unmount-file-system --mount-path /path-to-workspace --region +ti fs unmount-file-system --mount-path /path-to-workspace --region ``` ### Always-On MySQL — Zero Infrastructure Required -With `tdc`, an agent can go from zero to live HTAP SQL (Hybrid Transaction / Analytical Processing) in three commands: +With `ti`, an agent can go from zero to live HTAP SQL (Hybrid Transaction / Analytical Processing) in three commands: 1. Provision a serverless MySQL-compatible cluster, wait until it is active, and capture its ID ```shell -export CLUSTER_ID="$(tdc db create-db-cluster --db-cluster-name my-app-db --wait --query id --output text)" +export CLUSTER_ID="$(ti db create-db-cluster --db-cluster-name my-app-db --wait --query id --output text)" ``` 2. Create the SQL users it needs to connect ```shell -tdc db create-db-sql-users --db-cluster-id "$CLUSTER_ID" +ti db create-db-sql-users --db-cluster-id "$CLUSTER_ID" ``` 3. Retrieve the database connection string for your agent and share it across sandboxes as needed ```shell -export DATABASE_URL="$(tdc db format-db-connection-string --db-cluster-id "$CLUSTER_ID" --read-write --query connection_string --output text)" +export DATABASE_URL="$(ti db format-db-connection-string --db-cluster-id "$CLUSTER_ID" --read-write --query connection_string --output text)" ``` ## Install @@ -60,34 +60,42 @@ export DATABASE_URL="$(tdc db format-db-connection-string --db-cluster-id "$CLUS macOS and Linux users: ```bash -curl -fsSL https://github.com/tidbcloud/tdc/releases/latest/download/install.sh | sh -s -- --yes +curl -fsSL https://github.com/tidbcloud/ti-cli/releases/latest/download/install.sh | sh -s -- --yes ``` -After installation, add tdc to the current shell and verify it: +After installation, add ti to the current shell and verify it: ```bash -export PATH="$HOME/.tdc/bin:$PATH" -tdc --version +export PATH="$HOME/.ti/bin:$PATH" +ti --version ``` -The installer writes `tdc` and `tdc-drive9` to `~/.tdc/bin` without sudo. Add the `export PATH=...` line to your shell profile to make it persistent. +The installer writes `ti` and `ti-drive9` to `~/.ti/bin` without sudo. Add the `export PATH=...` line to your shell profile to make it persistent. Windows users: ```powershell -$script = "$env:TEMP\install-tdc.ps1" -iwr https://github.com/tidbcloud/tdc/releases/latest/download/install.ps1 -OutFile $script +$script = "$env:TEMP\install-ti.ps1" +iwr https://github.com/tidbcloud/ti-cli/releases/latest/download/install.ps1 -OutFile $script powershell -ExecutionPolicy Bypass -File $script -Yes ``` -After installation, add tdc to the current PowerShell session and verify it: +After installation, add ti to the current PowerShell session and verify it: ```powershell -$env:Path = "$HOME\.tdc\bin;$env:Path" -tdc --version +$env:Path = "$HOME\.ti\bin;$env:Path" +ti --version ``` -Add `$HOME\.tdc\bin` to your user `PATH` to keep tdc available in new PowerShell sessions. +Add `$HOME\.ti\bin` to your user `PATH` to keep ti available in new PowerShell sessions. + +### Upgrade from tdc v0.1.x + +The first `ti` installer or non-update command migrates durable state from `~/.tdc` to `~/.ti` when `~/.ti` does not already exist. It copies profiles, credentials, preferences, telemetry identity, DB SQL credentials, and filesystem registrations; it leaves logs, caches, mount state, and the old Drive9 home behind. The migration never modifies or deletes `~/.tdc`. A hidden owner-only marker under `~/.ti` records that the two directories coexist because migration completed successfully. + +Drain and unmount every mount started by `tdc` before installing `ti`. If both directories were created independently, or the migration marker is absent or invalid, migration fails instead of merging or overwriting either directory. Verify `ti` before manually removing the old binaries or state. + +Automation should move to `TI_*` and `TIDB_CLOUD_*` environment variables. The v0.2.x release line accepts the corresponding legacy `TDC_*` names when only one form is set; conflicting old and new values fail before any mutation. ## Quick Start Guide @@ -95,28 +103,28 @@ Add `$HOME\.tdc\bin` to your user `PATH` to keep tdc available in new PowerShell - Authentication: a TiDB Cloud Public Key and a Private Key from the [TiDB Cloud API Keys](https://tidbcloud.com/org-settings/api-keys) console. - Default region: one of aws-us-east-1, aws-us-west-2, aws-eu-central-1, aws-ap-northeast-1, aws-ap-southeast-1, or ali-ap-southeast-1. - - Regions support TiDB Cloud Filesystem: aws-us-east-1, aws-ap-southeast-1. + - Regions support TiDB Cloud Filesystem: aws-us-east-1, aws-us-west-2, aws-ap-southeast-1, and ali-ap-southeast-1. - Regions support TiDB Cloud Starter: aws-us-east-1, aws-us-west-2, aws-eu-central-1, aws-ap-northeast-1, aws-ap-southeast-1, or ali-ap-southeast-1. Set up a default profile with one command: ```shell -tdc configure --non-interactive --region-code --tdc-public-key --tdc-private-key +ti configure --non-interactive --region-code --tidb-cloud-public-key --tidb-cloud-private-key ``` Alternatively, set up a default profile interactively by running the command below. You will be prompted to enter your TiDB Cloud Public Key, Private Key, and the default region: ```shell -tdc configure +ti configure ``` -`tdc configure` stores non-sensitive profile configuration in `~/.tdc/config` and API credentials in `~/.tdc/credentials`. +`ti configure` stores non-sensitive profile configuration in `~/.ti/config` and API credentials in `~/.ti/credentials`. ### Global Settings and Operation Logs -Process-wide preferences are separate from profiles. The optional, hidden `~/.tdc/.preferences` file applies to every profile and is not created on fresh installs or by `tdc configure`. Local operation logs are enabled by default at `~/.tdc/logs/tdc.jsonl`; they contain redacted command and API summaries, not command values or user data. +Process-wide preferences are separate from profiles. The optional, hidden `~/.ti/.preferences` file applies to every profile and is not created on fresh installs or by `ti configure`. Local operation logs are enabled by default at `~/.ti/logs/ti.jsonl`; they contain redacted command and API summaries, not command values or user data. -To disable operation logging persistently, create `~/.tdc/.preferences`: +To disable operation logging persistently, create `~/.ti/.preferences`: ```toml schema_version = 1 @@ -127,13 +135,13 @@ max_file_mb = 10 max_files = 5 ``` -Use `TDC_LOGGING=off` to disable logging for one process. Accepted values are `on`, `true`, `1`, `yes`, `off`, `false`, `0`, and `no`. An existing `[logging]` section in `~/.tdc/config` is migrated automatically to `~/.tdc/.preferences`; profiles and credentials are preserved. `tdc update` does not read or write settings, profiles, credentials, operation logs, or other `~/.tdc/` state. +Use `TI_LOGGING=off` to disable logging for one process. Accepted values are `on`, `true`, `1`, `yes`, `off`, `false`, `0`, and `no`. An existing `[logging]` section in `~/.ti/config` is migrated automatically to `~/.ti/.preferences`; profiles and credentials are preserved. `ti update` does not read or write settings, profiles, credentials, operation logs, or other `~/.ti/` state. ### Anonymous Telemetry -Release builds collect minimal command usage and reliability telemetry through the tdc-owned ingestion service. Events contain canonical command and explicitly supplied flag names, stable exit and error codes, duration, region, tdc version, OS, architecture, install source, and a random installation ID. They never contain flag values, credentials, tokens, SQL text, paths, file contents, command output, API payloads, profile names, or cloud resource IDs. +Release builds collect minimal command usage and reliability telemetry through the ti-owned ingestion service. Events contain canonical command and explicitly supplied flag names, stable exit and error codes, duration, region, ti version, OS, architecture, install source, and a random installation ID. They never contain flag values, credentials, tokens, SQL text, paths, file contents, command output, API payloads, profile names, or cloud resource IDs. -Telemetry is disabled by default for development builds and recognized CI environments. To disable it persistently for release builds, create or edit `~/.tdc/.preferences`: +Telemetry is disabled by default for development builds and recognized CI environments. To disable it persistently for release builds, create or edit `~/.ti/.preferences`: ```toml schema_version = 1 @@ -142,133 +150,133 @@ schema_version = 1 enabled = false ``` -Use `TDC_TELEMETRY=off` to disable telemetry for one process, or `TDC_TELEMETRY=on` to explicitly enable it for an eligible release or development invocation whose build contains the product endpoint. Accepted values are `on`, `true`, `1`, `off`, `false`, `0`. Help, version, commandless usage, and every `tdc update` mode never send telemetry. The pseudonymous ID is stored with owner-only permissions at `~/.tdc/.telemetry-installation-id`; deleting that file resets the identity without changing the preference. +Use `TI_TELEMETRY=off` to disable telemetry for one process, or `TI_TELEMETRY=on` to explicitly enable it for an eligible release or development invocation whose build contains the product endpoint. Accepted values are `on`, `true`, `1`, `off`, `false`, `0`. Help, version, commandless usage, and every `ti update` mode never send telemetry. The pseudonymous ID is stored with owner-only permissions at `~/.ti/.telemetry-installation-id`; deleting that file resets the identity without changing the preference. -An integration can add optional process-scoped attribution without changing a profile or command: `TDC_TELEMETRY_TAG` is a UTF-8 string limited to 128 bytes, and `TDC_TELEMETRY_EXTRA` is one complete JSON value limited to 2 KiB after compaction. Invalid, prohibited, or oversized extra metadata is omitted; the command still runs normally. Never include credentials, tokens, SQL, paths, personal data, profile names, or cloud resource IDs in either value. +An integration can add optional process-scoped attribution without changing a profile or command: `TI_TELEMETRY_TAG` is a UTF-8 string limited to 128 bytes, and `TI_TELEMETRY_EXTRA` is one complete JSON value limited to 2 KiB after compaction. Invalid, prohibited, or oversized extra metadata is omitted; the command still runs normally. Never include credentials, tokens, SQL, paths, personal data, profile names, or cloud resource IDs in either value. ### TiDB Cloud Filesystem ```shell mkdir ~/my-workspace -tdc fs create-file-system --file-system-name my-workspace --wait -tdc fs mount-file-system --file-system-name my-workspace --mount-path ~/my-workspace +ti fs create-file-system --file-system-name my-workspace --wait +ti fs mount-file-system --file-system-name my-workspace --mount-path ~/my-workspace ``` Automatic mounting uses FUSE on Linux and WebDAV on macOS and Windows. macOS users can install macFUSE and explicitly add `--driver fuse` for the full FUSE experience. -One profile can manage multiple file systems. tdc never infers which resource a command targets, so provide `--file-system-name` for one-off commands or set `TDC_FS_FILE_SYSTEM_NAME` for repeated commands: +One profile can manage multiple file systems. ti never infers which resource a command targets, so provide `--file-system-name` for one-off commands or set `TI_FS_FILE_SYSTEM_NAME` for repeated commands: ```shell -tdc fs create-file-system --file-system-name scratch -tdc fs list-file-systems -tdc fs describe-file-system --file-system-name scratch -export TDC_FS_FILE_SYSTEM_NAME=scratch -tdc fs list-files +ti fs create-file-system --file-system-name scratch +ti fs list-file-systems +ti fs describe-file-system --file-system-name scratch +export TI_FS_FILE_SYSTEM_NAME=scratch +ti fs list-files ``` `create-file-system` returns an file system token (`fs_token`) in its JSON result. This is the file system owner credential and should be handled as a secret. A configured machine can provision a file system and capture the token without printing the full result: ```shell -export TDC_FS_TOKEN="$(tdc fs create-file-system --file-system-name agent-workspace --wait --query fs_token --output text)" +export TI_FS_TOKEN="$(ti fs create-file-system --file-system-name agent-workspace --wait --query fs_token --output text)" ``` -An agent sandbox can then use that existing file system without running `tdc configure` or providing TiDB Cloud API keys: +An agent sandbox can then use that existing file system without running `ti configure` or providing TiDB Cloud API keys: ```shell -export TDC_FS_TOKEN="" -tdc fs mount-file-system --file-system-name agent-workspace --mount-path /path_to_workspace --region aws-us-east-1 +export TI_FS_TOKEN="" +ti fs mount-file-system --file-system-name agent-workspace --mount-path /path_to_workspace --region aws-us-east-1 ``` ### TiDB Cloud Starter -`tdc db` manages TiDB Cloud Starter clusters only. Cluster lists include only verified Starter clusters in the effective region and omit Essential, other service plans, cross-region resources, and resources whose region cannot be verified. Use global `--region`, for example `tdc --region aws-us-west-2 db list-db-clusters`, to inspect another region without changing the stored profile. Every cluster, branch, SQL-user, connection-string, and SQL command verifies the cluster service plan before continuing. If TiDB Cloud does not return enough plan metadata to prove that a cluster is Starter, `tdc` fails without issuing the requested mutation. +`ti db` manages TiDB Cloud Starter clusters only. Cluster lists include only verified Starter clusters in the effective region and omit Essential, other service plans, cross-region resources, and resources whose region cannot be verified. Use global `--region`, for example `ti --region aws-us-west-2 db list-db-clusters`, to inspect another region without changing the stored profile. Every cluster, branch, SQL-user, connection-string, and SQL command verifies the cluster service plan before continuing. If TiDB Cloud does not return enough plan metadata to prove that a cluster is Starter, `ti` fails without issuing the requested mutation. -`tdc configure` discovers the account's virtual project and saves its ID in the selected profile. Cluster creation uses an explicit `--project-id` first, then that saved project ID. If neither is available, `tdc` omits the project label and lets TiDB Cloud select the account's default project. +`ti configure` discovers the account's virtual project and saves its ID in the selected profile. Cluster creation uses an explicit `--project-id` first, then that saved project ID. If neither is available, `ti` omits the project label and lets TiDB Cloud select the account's default project. ```shell -tdc db create-db-cluster --db-cluster-name my-distributed-mysql --wait +ti db create-db-cluster --db-cluster-name my-distributed-mysql --wait ``` ## Get Help -- `tdc` -- `tdc help` -- `tdc help` -- `tdc help` +- `ti` +- `ti help` +- `ti help` +- `ti help`
All commands ```text -tdc configure -tdc update -tdc organization list-projects - -tdc db create-db-cluster -tdc db list-db-clusters -tdc db describe-db-cluster -tdc db update-db-cluster -tdc db delete-db-cluster -tdc db create-db-cluster-branch -tdc db list-db-cluster-branches -tdc db describe-db-cluster-branch -tdc db delete-db-cluster-branch -tdc db create-db-sql-users -tdc db format-db-connection-string -tdc db execute-sql-statement - -tdc fs create-file-system -tdc fs delete-file-system -tdc fs list-file-systems -tdc fs describe-file-system -tdc fs check-file-system -tdc fs copy-file -tdc fs read-file -tdc fs list-files -tdc fs describe-file -tdc fs move-file -tdc fs delete-file -tdc fs create-directory -tdc fs chmod-file -tdc fs create-symlink -tdc fs create-hardlink -tdc fs search-file-content -tdc fs find-files -tdc fs create-layer -tdc fs list-layers -tdc fs describe-layer -tdc fs diff-layer -tdc fs create-layer-checkpoint -tdc fs rollback-layer -tdc fs commit-layer -tdc fs pack-file-system -tdc fs unpack-file-system -tdc fs mount-file-system -tdc fs drain-file-system -tdc fs unmount-file-system - -tdc fs-git clone-git-workspace -tdc fs-git hydrate-git-workspace -tdc fs-git add-git-worktree -tdc fs-git remove-git-worktree - -tdc fs-journal create-journal -tdc fs-journal append-journal-entries -tdc fs-journal read-journal-entries -tdc fs-journal search-journal-entries -tdc fs-journal verify-journal - -tdc fs-vault create-secret -tdc fs-vault replace-secret -tdc fs-vault read-secret -tdc fs-vault list-secrets -tdc fs-vault delete-secret -tdc fs-vault create-grant -tdc fs-vault delete-grant -tdc fs-vault list-audit-events -tdc fs-vault run-with-secret -tdc fs-vault mount-vault -tdc fs-vault unmount-vault +ti configure +ti update +ti organization list-projects + +ti db create-db-cluster +ti db list-db-clusters +ti db describe-db-cluster +ti db update-db-cluster +ti db delete-db-cluster +ti db create-db-cluster-branch +ti db list-db-cluster-branches +ti db describe-db-cluster-branch +ti db delete-db-cluster-branch +ti db create-db-sql-users +ti db format-db-connection-string +ti db execute-sql-statement + +ti fs create-file-system +ti fs delete-file-system +ti fs list-file-systems +ti fs describe-file-system +ti fs check-file-system +ti fs copy-file +ti fs read-file +ti fs list-files +ti fs describe-file +ti fs move-file +ti fs delete-file +ti fs create-directory +ti fs chmod-file +ti fs create-symlink +ti fs create-hardlink +ti fs search-file-content +ti fs find-files +ti fs create-layer +ti fs list-layers +ti fs describe-layer +ti fs diff-layer +ti fs create-layer-checkpoint +ti fs rollback-layer +ti fs commit-layer +ti fs pack-file-system +ti fs unpack-file-system +ti fs mount-file-system +ti fs drain-file-system +ti fs unmount-file-system + +ti fs-git clone-git-workspace +ti fs-git hydrate-git-workspace +ti fs-git add-git-worktree +ti fs-git remove-git-worktree + +ti fs-journal create-journal +ti fs-journal append-journal-entries +ti fs-journal read-journal-entries +ti fs-journal search-journal-entries +ti fs-journal verify-journal + +ti fs-vault create-secret +ti fs-vault replace-secret +ti fs-vault read-secret +ti fs-vault list-secrets +ti fs-vault delete-secret +ti fs-vault create-grant +ti fs-vault delete-grant +ti fs-vault list-audit-events +ti fs-vault run-with-secret +ti fs-vault mount-vault +ti fs-vault unmount-vault ``` Filesystem aliases are `cp`, `cat`, `ls`, `stat`, `mv`, `rm`, `mkdir`, `chmod`, `symlink`, `hardlink`, `grep`, `find`, `mount`, `drain`, and `umount`. Aliases keep the canonical command's long flags. @@ -278,17 +286,17 @@ Filesystem aliases are `cp`, `cat`, `ls`, `stat`, `mv`, `rm`, `mkdir`, `chmod`, ## Update ```bash -tdc update --check -tdc update --dry-run -tdc update -tdc update --target-version v0.1.1 +ti update --check +ti update --dry-run +ti update +ti update --target-version v0.1.1 ``` -`tdc update` downloads and verifies both `tdc` and its `tdc-drive9` companion before replacing either binary in the user-writable install directory. It never requests sudo. Legacy installations under `/usr/local/bin` must run the installer once to migrate to `~/.tdc/bin`. +`ti update` downloads and verifies both `ti` and its `ti-drive9` companion before replacing either binary in the user-writable install directory. It never requests sudo. The old `tdc update` command cannot migrate to a differently named executable; install `ti` once with the new installer instead. ## Documentation -- [Preview Documentation](docs/pingcap-docs/docs/ai/tdc/tdc-overview.md) +- [Preview Documentation](docs/pingcap-docs/docs/ai/ti/ti-overview.md) ## Build From Source @@ -307,7 +315,7 @@ make build The binary is written to: ```text -bin/tdc +bin/ti ``` Build the independently deployed telemetry ingestion service: @@ -316,7 +324,7 @@ Build the independently deployed telemetry ingestion service: make build-telemetry-backend ``` -The backend binary is written to `bin/tdc-telemetry-backend`. Its API, privacy contract, TiDB/PostHog batching behavior, and Docker deployment are documented in [Telemetry Backend Design](docs/telemetry-backend-design.md). +The backend binary is written to `bin/ti-telemetry-backend`. Its API, privacy contract, TiDB/PostHog batching behavior, and Docker deployment are documented in [Telemetry Backend Design](docs/telemetry-backend-design.md). ## Test diff --git a/cmd/tdc/main.go b/cmd/tdc/main.go deleted file mode 100644 index f612471..0000000 --- a/cmd/tdc/main.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "os/signal" - - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/cli" - "github.com/tidbcloud/tdc/internal/version" -) - -func main() { - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) - defer stop() - - info := version.Current() - root := cli.NewRootCommand(info) - if err := cli.Execute(ctx, root, info, os.Args[1:], os.Stdout, os.Stderr); err != nil { - fmt.Fprintf(os.Stderr, "\ntdc [ERROR]: %s\n", apperr.MessageFor(err)) - os.Exit(apperr.ExitCodeFor(err)) - } -} diff --git a/cmd/tdc-telemetry-backend/main.go b/cmd/ti-telemetry-backend/main.go similarity index 97% rename from cmd/tdc-telemetry-backend/main.go rename to cmd/ti-telemetry-backend/main.go index bb9b8be..92b3636 100644 --- a/cmd/tdc-telemetry-backend/main.go +++ b/cmd/ti-telemetry-backend/main.go @@ -12,7 +12,7 @@ import ( "time" _ "github.com/go-sql-driver/mysql" - "github.com/tidbcloud/tdc/internal/telemetrybackend" + "github.com/tidbcloud/ti-cli/internal/telemetrybackend" ) func main() { diff --git a/cmd/tdc-telemetry-migrate/main.go b/cmd/ti-telemetry-migrate/main.go similarity index 93% rename from cmd/tdc-telemetry-migrate/main.go rename to cmd/ti-telemetry-migrate/main.go index 48d3d2e..06ee943 100644 --- a/cmd/tdc-telemetry-migrate/main.go +++ b/cmd/ti-telemetry-migrate/main.go @@ -10,7 +10,7 @@ import ( "time" _ "github.com/go-sql-driver/mysql" - "github.com/tidbcloud/tdc/internal/telemetrybackend/migrations" + "github.com/tidbcloud/ti-cli/internal/telemetrybackend/migrations" ) func main() { diff --git a/cmd/ti/main.go b/cmd/ti/main.go new file mode 100644 index 0000000..5f332b1 --- /dev/null +++ b/cmd/ti/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/signal" + + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/cli" + "github.com/tidbcloud/ti-cli/internal/config/homemigration" + "github.com/tidbcloud/ti-cli/internal/version" +) + +func main() { + if len(os.Args) == 2 && os.Args[1] == "__migrate-home" { + runHomeMigration() + return + } + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + info := version.Current() + root := cli.NewRootCommand(info) + if err := cli.Execute(ctx, root, info, os.Args[1:], os.Stdout, os.Stderr, cli.WithHomeMigration()); err != nil { + fmt.Fprintf(os.Stderr, "\nti [ERROR]: %s\n", apperr.MessageFor(err)) + os.Exit(apperr.ExitCodeFor(err)) + } +} + +func runHomeMigration() { + home, err := os.UserHomeDir() + if err == nil { + var result homemigration.Result + result, err = homemigration.Ensure(home) + if err == nil { + err = json.NewEncoder(os.Stdout).Encode(result) + } + } + if err != nil { + fmt.Fprintf(os.Stderr, "\nti [ERROR]: %s\n", apperr.MessageFor(err)) + os.Exit(apperr.ExitCodeFor(err)) + } +} diff --git a/deploy/telemetry/Dockerfile b/deploy/telemetry/Dockerfile index 5de71b5..406fb80 100644 --- a/deploy/telemetry/Dockerfile +++ b/deploy/telemetry/Dockerfile @@ -3,22 +3,22 @@ FROM golang:1.26.5-alpine AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download -COPY cmd/tdc-telemetry-backend ./cmd/tdc-telemetry-backend -COPY cmd/tdc-telemetry-migrate ./cmd/tdc-telemetry-migrate +COPY cmd/ti-telemetry-backend ./cmd/ti-telemetry-backend +COPY cmd/ti-telemetry-migrate ./cmd/ti-telemetry-migrate COPY internal/telemetrybackend ./internal/telemetrybackend RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" \ - -o /out/tdc-telemetry-backend ./cmd/tdc-telemetry-backend + -o /out/ti-telemetry-backend ./cmd/ti-telemetry-backend RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" \ - -o /out/tdc-telemetry-migrate ./cmd/tdc-telemetry-migrate + -o /out/ti-telemetry-migrate ./cmd/ti-telemetry-migrate FROM alpine:3.23 RUN apk add --no-cache ca-certificates wget \ && addgroup -S -g 10001 telemetry \ && adduser -S -D -H -u 10001 -G telemetry telemetry -COPY --from=build /out/tdc-telemetry-backend /usr/local/bin/tdc-telemetry-backend -COPY --from=build /out/tdc-telemetry-migrate /usr/local/bin/tdc-telemetry-migrate +COPY --from=build /out/ti-telemetry-backend /usr/local/bin/ti-telemetry-backend +COPY --from=build /out/ti-telemetry-migrate /usr/local/bin/ti-telemetry-migrate USER telemetry EXPOSE 8080 -ENTRYPOINT ["/usr/local/bin/tdc-telemetry-backend"] +ENTRYPOINT ["/usr/local/bin/ti-telemetry-backend"] diff --git a/deploy/telemetry/docker-compose.yml b/deploy/telemetry/docker-compose.yml index 198e5e1..e05fcd3 100644 --- a/deploy/telemetry/docker-compose.yml +++ b/deploy/telemetry/docker-compose.yml @@ -5,7 +5,7 @@ services: build: context: ../.. dockerfile: deploy/telemetry/Dockerfile - entrypoint: ["/usr/local/bin/tdc-telemetry-migrate"] + entrypoint: ["/usr/local/bin/ti-telemetry-migrate"] env_file: - ${TELEMETRY_ENV_FILE:-.env} restart: "no" diff --git a/docs/present.md b/docs/present.md index 31308f7..af40217 100644 --- a/docs/present.md +++ b/docs/present.md @@ -1,29 +1,29 @@ -# tdc 中文演示脚本 +# ti 中文演示脚本 -这份脚本使用本地二进制 `bin/tdc` 展示当前 Preview 版本。故事线是:配置默认 profile,创建 Starter 数据库并按角色执行 SQL,创建一个 TiDB Cloud Filesystem workspace,通过 data plane 和 mount 双向操作,再用 Git、Journal 和 Vault 完成 agent 工作流。 +这份脚本使用本地二进制 `bin/ti` 展示当前 Preview 版本。故事线是:配置默认 profile,创建 Starter 数据库并按角色执行 SQL,创建一个 TiDB Cloud Filesystem workspace,通过 data plane 和 mount 双向操作,再用 Git、Journal 和 Vault 完成 agent 工作流。 ## 演示前准备 确认版本和帮助: ```bash -bin/tdc --version -bin/tdc help +bin/ti --version +bin/ti help ``` 交互式配置默认 profile: ```bash -bin/tdc configure +bin/ti configure ``` 自动化环境也可以使用: ```bash -TDC_REGION_CODE=aws-us-east-1 \ -TDC_PUBLIC_KEY="$TDC_PUBLIC_KEY" \ -TDC_PRIVATE_KEY="$TDC_PRIVATE_KEY" \ -bin/tdc configure --non-interactive +TI_REGION_CODE=aws-us-east-1 \ +TIDB_CLOUD_PUBLIC_KEY="$TIDB_CLOUD_PUBLIC_KEY" \ +TIDB_CLOUD_PRIVATE_KEY="$TIDB_CLOUD_PRIVATE_KEY" \ +bin/ti configure --non-interactive ``` `configure` 会验证 TiDB Cloud API key,找到唯一的 `tidbx_virtual` project,并把其 ID 保存为默认 `project_id`。后续创建 Starter cluster 不需要重复传 `--project-id`。 @@ -31,8 +31,8 @@ bin/tdc configure --non-interactive ## 1. 查看 Organization Project ```bash -bin/tdc organization list-projects --output text -bin/tdc organization list-projects --query 'projects[].{id:id,name:name,type:type}' +bin/ti organization list-projects --output text +bin/ti organization list-projects --query 'projects[].{id:id,name:name,type:type}' ``` `organization` 当前是只读 control plane,可供人、脚本和 agent 检查 API key 能访问的项目。 @@ -43,18 +43,18 @@ bin/tdc organization list-projects --query 'projects[].{id:id,name:name,type:typ ```bash export DEMO_ID="$(date +%Y%m%d%H%M%S)" -export CLUSTER_NAME="tdc-demo-${DEMO_ID}" +export CLUSTER_NAME="ti-demo-${DEMO_ID}" ``` 先 dry run,再实际创建: ```bash -bin/tdc db create-db-cluster \ +bin/ti db create-db-cluster \ --db-cluster-name "$CLUSTER_NAME" \ --db-cluster-type starter \ --dry-run -bin/tdc db create-db-cluster \ +bin/ti db create-db-cluster \ --db-cluster-name "$CLUSTER_NAME" \ --db-cluster-type starter \ --wait @@ -63,9 +63,9 @@ bin/tdc db create-db-cluster \ 从 JSON 结果中找到 cluster ID: ```bash -bin/tdc db list-db-clusters --output text -export CLUSTER_ID="$(bin/tdc db list-db-clusters | jq -r --arg name "$CLUSTER_NAME" '.clusters[] | select(.display_name == $name) | .id' | head -n 1)" -bin/tdc db describe-db-cluster --db-cluster-id "$CLUSTER_ID" --output text +bin/ti db list-db-clusters --output text +export CLUSTER_ID="$(bin/ti db list-db-clusters | jq -r --arg name "$CLUSTER_NAME" '.clusters[] | select(.display_name == $name) | .id' | head -n 1)" +bin/ti db describe-db-cluster --db-cluster-id "$CLUSTER_ID" --output text ``` 创建命令返回 `ACTIVE` 后,更新名称并重新读取: @@ -73,46 +73,46 @@ bin/tdc db describe-db-cluster --db-cluster-id "$CLUSTER_ID" --output text ```bash export CLUSTER_RENAMED="${CLUSTER_NAME}-renamed" -bin/tdc db update-db-cluster \ +bin/ti db update-db-cluster \ --db-cluster-id "$CLUSTER_ID" \ --db-cluster-name "$CLUSTER_RENAMED" -bin/tdc db describe-db-cluster --db-cluster-id "$CLUSTER_ID" --output text +bin/ti db describe-db-cluster --db-cluster-id "$CLUSTER_ID" --output text ``` 分支生命周期: ```bash -bin/tdc db create-db-cluster-branch \ +bin/ti db create-db-cluster-branch \ --db-cluster-id "$CLUSTER_ID" \ --db-cluster-branch-name demo-branch \ --wait -bin/tdc db list-db-cluster-branches --db-cluster-id "$CLUSTER_ID" --output text +bin/ti db list-db-cluster-branches --db-cluster-id "$CLUSTER_ID" --output text ``` ## 3. 创建 SQL 用户并按角色执行 SQL -创建或修复 tdc 管理的三种稳定 SQL 用户。该操作可重入,不会在每次运行时创建新的一组: +创建或修复 ti 管理的三种稳定 SQL 用户。该操作可重入,不会在每次运行时创建新的一组: ```bash -bin/tdc db create-db-sql-users --db-cluster-id "$CLUSTER_ID" +bin/ti db create-db-sql-users --db-cluster-id "$CLUSTER_ID" ``` 格式化三种连接信息。默认角色是 read-write,也可以显式指定: ```bash -bin/tdc db format-db-connection-string \ +bin/ti db format-db-connection-string \ --db-cluster-id "$CLUSTER_ID" \ --read-write \ --format mysql-uri -bin/tdc db format-db-connection-string \ +bin/ti db format-db-connection-string \ --db-cluster-id "$CLUSTER_ID" \ --read-only \ --format env -bin/tdc db format-db-connection-string \ +bin/ti db format-db-connection-string \ --db-cluster-id "$CLUSTER_ID" \ --admin \ --format jdbc @@ -121,25 +121,25 @@ bin/tdc db format-db-connection-string \ 使用 admin 建库建表,read-write 写入,read-only 查询: ```bash -bin/tdc db execute-sql-statement \ +bin/ti db execute-sql-statement \ --db-cluster-id "$CLUSTER_ID" \ --admin \ - --sql "create database if not exists tdc_demo" + --sql "create database if not exists ti_demo" -bin/tdc db execute-sql-statement \ +bin/ti db execute-sql-statement \ --db-cluster-id "$CLUSTER_ID" \ --admin \ - --sql "create table if not exists tdc_demo.messages (id int primary key, body varchar(128))" + --sql "create table if not exists ti_demo.messages (id int primary key, body varchar(128))" -bin/tdc db execute-sql-statement \ +bin/ti db execute-sql-statement \ --db-cluster-id "$CLUSTER_ID" \ --read-write \ - --sql "insert into tdc_demo.messages values (1, 'hello from tdc') on duplicate key update body = values(body)" + --sql "insert into ti_demo.messages values (1, 'hello from ti') on duplicate key update body = values(body)" -bin/tdc db execute-sql-statement \ +bin/ti db execute-sql-statement \ --db-cluster-id "$CLUSTER_ID" \ --read-only \ - --sql "select id, body from tdc_demo.messages" \ + --sql "select id, body from ti_demo.messages" \ --output text ``` @@ -147,54 +147,54 @@ SQL 默认通过 HTTPS SQL API 执行,一次命令只执行一个 statement。 ## 4. 创建并管理 Filesystem -创建名为 `tdc-demo-workspace` 的资源。一个 profile 可以注册多个 Filesystem;首次创建的资源会成为默认资源: +创建名为 `ti-demo-workspace` 的资源。一个 profile 可以注册多个 Filesystem;每个资源都必须通过名称显式选择: ```bash -bin/tdc fs create-file-system \ - --file-system-name tdc-demo-workspace \ +bin/ti fs create-file-system \ + --file-system-name ti-demo-workspace \ --dry-run -bin/tdc fs create-file-system \ - --file-system-name tdc-demo-workspace \ +bin/ti fs create-file-system \ + --file-system-name ti-demo-workspace \ --wait -bin/tdc fs list-file-systems --output text -bin/tdc fs describe-file-system \ - --file-system-name tdc-demo-workspace \ +bin/ti fs list-file-systems --output text +bin/ti fs describe-file-system \ + --file-system-name ti-demo-workspace \ --output text -bin/tdc fs check-file-system \ - --file-system-name tdc-demo-workspace \ +bin/ti fs check-file-system \ + --file-system-name ti-demo-workspace \ --output text ``` -创建命令的 JSON 结果包含一次性的 `fs_token`。它是资源 owner credential,不能写入日志或公开传递。资源元数据和凭证分别存储在 `~/.tdc/fs_resources///` 下,不写入主 `~/.tdc/credentials`。 +创建命令的 JSON 结果包含一次性的 `fs_token`。它是资源 owner credential,不能写入日志或公开传递。资源元数据和凭证分别存储在 `~/.ti/fs_resources///` 下,不写入主 `~/.ti/credentials`。 ## 5. 使用 Data Plane 操作文件 ```bash -bin/tdc fs create-directory \ - --file-system-name tdc-demo-workspace \ +bin/ti fs create-directory \ + --file-system-name ti-demo-workspace \ --path /demo -printf 'hello from data plane\n' | bin/tdc fs copy-file \ - --file-system-name tdc-demo-workspace \ +printf 'hello from data plane\n' | bin/ti fs copy-file \ + --file-system-name ti-demo-workspace \ --from-stdin \ --to-remote /demo/from-data-plane.txt \ --tag source=data-plane \ - --description "created through tdc fs data plane" + --description "created through ti fs data plane" -bin/tdc fs list-files \ - --file-system-name tdc-demo-workspace \ +bin/ti fs list-files \ + --file-system-name ti-demo-workspace \ --path /demo \ --output text -bin/tdc fs read-file \ - --file-system-name tdc-demo-workspace \ +bin/ti fs read-file \ + --file-system-name ti-demo-workspace \ --path /demo/from-data-plane.txt -bin/tdc fs describe-file \ - --file-system-name tdc-demo-workspace \ +bin/ti fs describe-file \ + --file-system-name ti-demo-workspace \ --path /demo/from-data-plane.txt \ --output text ``` @@ -202,18 +202,18 @@ bin/tdc fs describe-file \ Unix-style alias 只缩短命令名,flags 仍使用完整名称: ```bash -bin/tdc fs ls --file-system-name tdc-demo-workspace --path /demo --output text -bin/tdc fs cat --file-system-name tdc-demo-workspace --path /demo/from-data-plane.txt +bin/ti fs ls --file-system-name ti-demo-workspace --path /demo --output text +bin/ti fs cat --file-system-name ti-demo-workspace --path /demo/from-data-plane.txt ``` ## 6. 挂载并验证双向可见性 ```bash -export MOUNT_PATH="/tmp/tdc-demo-${DEMO_ID}" +export MOUNT_PATH="/tmp/ti-demo-${DEMO_ID}" mkdir -p "$MOUNT_PATH" -bin/tdc fs mount-file-system \ - --file-system-name tdc-demo-workspace \ +bin/ti fs mount-file-system \ + --file-system-name ti-demo-workspace \ --mount-path "$MOUNT_PATH" ``` @@ -230,15 +230,15 @@ cat "$MOUNT_PATH/demo/from-data-plane.txt" ```bash printf 'hello from mount\n' > "$MOUNT_PATH/demo/from-mount.txt" -bin/tdc fs read-file \ - --file-system-name tdc-demo-workspace \ +bin/ti fs read-file \ + --file-system-name ti-demo-workspace \ --path /demo/from-mount.txt ``` FUSE mount 可以在卸载前 drain dirty state。WebDAV 通过正常的 file close 刷新,不支持 drain: ```bash -bin/tdc fs drain-file-system --mount-path "$MOUNT_PATH" +bin/ti fs drain-file-system --mount-path "$MOUNT_PATH" ``` ## 7. 使用 Filesystem Git Workspace @@ -248,15 +248,15 @@ bin/tdc fs drain-file-system --mount-path "$MOUNT_PATH" ```bash mkdir -p "$MOUNT_PATH/repos" -bin/tdc fs-git clone-git-workspace \ - --file-system-name tdc-demo-workspace \ +bin/ti fs-git clone-git-workspace \ + --file-system-name ti-demo-workspace \ --repo-url https://github.com/octocat/Hello-World.git \ --target-path "$MOUNT_PATH/repos/hello" \ --blobless \ --hydrate background -bin/tdc fs-git hydrate-git-workspace \ - --file-system-name tdc-demo-workspace \ +bin/ti fs-git hydrate-git-workspace \ + --file-system-name ti-demo-workspace \ --target-path "$MOUNT_PATH/repos/hello" git -C "$MOUNT_PATH/repos/hello" status --short @@ -265,53 +265,53 @@ git -C "$MOUNT_PATH/repos/hello" status --short 创建隔离的 linked worktree: ```bash -bin/tdc fs-git add-git-worktree \ - --file-system-name tdc-demo-workspace \ +bin/ti fs-git add-git-worktree \ + --file-system-name ti-demo-workspace \ --base-path "$MOUNT_PATH/repos/hello" \ --worktree-path "$MOUNT_PATH/repos/hello-feature" \ --branch-name demo-feature git -C "$MOUNT_PATH/repos/hello-feature" status --short -bin/tdc fs-git remove-git-worktree \ - --file-system-name tdc-demo-workspace \ +bin/ti fs-git remove-git-worktree \ + --file-system-name ti-demo-workspace \ --worktree-path "$MOUNT_PATH/repos/hello-feature" \ --force ``` -`tdc fs-git` 不替代 Git。它负责为 Filesystem mount 准备 clone、hydrate 和 worktree 工作流;普通提交和分支操作仍使用 `git`。 +`ti fs-git` 不替代 Git。它负责为 Filesystem mount 准备 clone、hydrate 和 worktree 工作流;普通提交和分支操作仍使用 `git`。 ## 8. 使用 Journal 记录 Agent 工作流 ```bash export JOURNAL_ID="jrn-demo-${DEMO_ID}" -bin/tdc fs-journal create-journal \ - --file-system-name tdc-demo-workspace \ +bin/ti fs-journal create-journal \ + --file-system-name ti-demo-workspace \ --journal-id "$JOURNAL_ID" \ --journal-kind agent \ - --title "tdc demo ${DEMO_ID}" \ + --title "ti demo ${DEMO_ID}" \ --actor agent:demo \ --label demo=present -bin/tdc fs-journal append-journal-entries \ - --file-system-name tdc-demo-workspace \ +bin/ti fs-journal append-journal-entries \ + --file-system-name ti-demo-workspace \ --journal-id "$JOURNAL_ID" \ --entry-json '{"type":"demo.started"}' \ --entry-json '{"type":"demo.completed"}' -bin/tdc fs-journal read-journal-entries \ - --file-system-name tdc-demo-workspace \ +bin/ti fs-journal read-journal-entries \ + --file-system-name ti-demo-workspace \ --journal-id "$JOURNAL_ID" \ --output text -bin/tdc fs-journal search-journal-entries \ - --file-system-name tdc-demo-workspace \ +bin/ti fs-journal search-journal-entries \ + --file-system-name ti-demo-workspace \ --entry-type demo.completed \ --include-entries -bin/tdc fs-journal verify-journal \ - --file-system-name tdc-demo-workspace \ +bin/ti fs-journal verify-journal \ + --file-system-name ti-demo-workspace \ --journal-id "$JOURNAL_ID" \ --output text ``` @@ -321,16 +321,16 @@ Journal 是 append-only、可验证的 workflow ledger,不是普通文本日 ## 9. 使用 Vault 管理和委派 Secret ```bash -printf 'demo-token\n' > /tmp/tdc-demo-token.txt +printf 'demo-token\n' > /tmp/ti-demo-token.txt -bin/tdc fs-vault create-secret \ - --file-system-name tdc-demo-workspace \ +bin/ti fs-vault create-secret \ + --file-system-name ti-demo-workspace \ --secret-name demo-service \ --field ENDPOINT=https://example.invalid \ - --field API_TOKEN=@/tmp/tdc-demo-token.txt + --field API_TOKEN=@/tmp/ti-demo-token.txt -bin/tdc fs-vault read-secret \ - --file-system-name tdc-demo-workspace \ +bin/ti fs-vault read-secret \ + --file-system-name ti-demo-workspace \ --secret-name demo-service \ --field ENDPOINT \ --format raw @@ -339,23 +339,23 @@ bin/tdc fs-vault read-secret \ 向 agent 委派一个字段的临时只读访问: ```bash -export TDC_VAULT_TOKEN="$(bin/tdc fs-vault create-grant \ - --file-system-name tdc-demo-workspace \ +export TI_VAULT_TOKEN="$(bin/ti fs-vault create-grant \ + --file-system-name ti-demo-workspace \ --agent-id demo-agent \ --scope demo-service/ENDPOINT \ --permission read \ --ttl 10m \ --token-only)" -bin/tdc fs-vault read-secret \ - --file-system-name tdc-demo-workspace \ +bin/ti fs-vault read-secret \ + --file-system-name ti-demo-workspace \ --secret-name demo-service \ --field ENDPOINT \ --format raw \ - --vault-token "$TDC_VAULT_TOKEN" + --vault-token "$TI_VAULT_TOKEN" -bin/tdc fs-vault list-audit-events \ - --file-system-name tdc-demo-workspace \ +bin/ti fs-vault list-audit-events \ + --file-system-name ti-demo-workspace \ --secret-name demo-service \ --limit 20 \ --output text @@ -368,40 +368,40 @@ Vault mount 是只读 FUSE view,需要 delegated Vault token;Windows 不支 FUSE mount 先 drain,再卸载。WebDAV mount 跳过 drain: ```bash -bin/tdc fs drain-file-system --mount-path "$MOUNT_PATH" -bin/tdc fs unmount-file-system --mount-path "$MOUNT_PATH" --ignore-absent +bin/ti fs drain-file-system --mount-path "$MOUNT_PATH" +bin/ti fs unmount-file-system --mount-path "$MOUNT_PATH" --ignore-absent rm -rf "$MOUNT_PATH" ``` 删除演示 secret 和临时文件: ```bash -bin/tdc fs-vault delete-secret \ - --file-system-name tdc-demo-workspace \ +bin/ti fs-vault delete-secret \ + --file-system-name ti-demo-workspace \ --secret-name demo-service -rm -f /tmp/tdc-demo-token.txt +rm -f /tmp/ti-demo-token.txt ``` 删除 Filesystem 资源及其本地 registry entry: ```bash -bin/tdc fs delete-file-system \ - --file-system-name tdc-demo-workspace +bin/ti fs delete-file-system \ + --file-system-name ti-demo-workspace ``` 删除演示 cluster: ```bash -bin/tdc db delete-db-cluster \ +bin/ti db delete-db-cluster \ --db-cluster-id "$CLUSTER_ID" \ --wait ``` ## 讲解重点 -- tdc 使用完整命令名、长 flags、JSON 默认输出、JMESPath `--query`、`--output text` 和 control-plane `--dry-run`,同时适合人和 agent。 +- ti 使用完整命令名、长 flags、JSON 默认输出、JMESPath `--query`、`--output text` 和 control-plane `--dry-run`,同时适合人和 agent。 - TiDB Cloud control-plane API key、SQL 用户凭证和 FS owner token 是三个不同的认证边界。 - 一个 profile 可以注册多个 Filesystem;data plane 和 mount 对同一个远端 namespace 双向可见。 -- tdc 负责命令、配置、资源选择、输出和安装;随附的 `tdc-drive9` 负责 Filesystem runtime 正确性。 -- `tdc fs-git`、`tdc fs-journal` 和 `tdc fs-vault` 是 Filesystem 的附属一级域,分别服务于 Git workspace、可验证 workflow ledger 和 secret delegation。 +- ti 负责命令、配置、资源选择、输出和安装;随附的 `ti-drive9` 负责 Filesystem runtime 正确性。 +- `ti fs-git`、`ti fs-journal` 和 `ti fs-vault` 是 Filesystem 的附属一级域,分别服务于 Git workspace、可验证 workflow ledger 和 secret delegation。 diff --git a/docs/priciples.md b/docs/priciples.md index 122eae7..9111c51 100644 --- a/docs/priciples.md +++ b/docs/priciples.md @@ -1,26 +1,26 @@ -# Agentic CLI - tdc +# Agentic CLI - ti -tdc is the command-line interface for TiDB Cloud Starter and TiDB Cloud Filesystem. It is designed for people, scripts, and AI agents that need deterministic resource management without terminal-specific assumptions. +ti is the command-line interface for TiDB Cloud Starter and TiDB Cloud Filesystem. It is designed for people, scripts, and AI agents that need deterministic resource management without terminal-specific assumptions. -tdc is currently in Preview. Its feature and command contracts can change before GA. +ti is currently in Preview. Its feature and command contracts can change before GA. ## Product Scope -- `tdc organization` reads TiDB Cloud organization and project context. -- `tdc db` manages TiDB Cloud Starter clusters and branches, prepares SQL users, formats connection strings, and executes one SQL statement per invocation. -- `tdc fs` manages TiDB Cloud Filesystem resources, files, layers, packs, and mounts. -- `tdc fs-git`, `tdc fs-journal`, and `tdc fs-vault` expose Filesystem-backed Git workspace, append-only journal, and secret-management workflows. -- `tdc configure` initializes a local profile. -- `tdc update` explicitly checks for or installs a release update. +- `ti organization` reads TiDB Cloud organization and project context. +- `ti db` manages TiDB Cloud Starter clusters and branches, prepares SQL users, formats connection strings, and executes one SQL statement per invocation. +- `ti fs` manages TiDB Cloud Filesystem resources, files, layers, packs, and mounts. +- `ti fs-git`, `ti fs-journal`, and `ti fs-vault` expose Filesystem-backed Git workspace, append-only journal, and secret-management workflows. +- `ti configure` initializes a local profile. +- `ti update` explicitly checks for or installs a release update. -tdc is Starter-only in the current Preview. `--db-cluster-type starter` remains explicit so the command contract can accommodate other TiDB Cloud plans later. +ti is Starter-only in the current Preview. `--db-cluster-type starter` remains explicit so the command contract can accommodate other TiDB Cloud plans later. ## Command Design The command tree has at most two command levels: ```text -tdc [subcommand] +ti [subcommand] ``` `configure` and `update` are intentional top-level verb exceptions. Other top-level commands identify product domains: `organization`, `db`, `fs`, `fs-git`, `fs-journal`, and `fs-vault`. @@ -28,9 +28,9 @@ tdc [subcommand] - Commands and flags use complete, self-explanatory names. - Flags are long-only. Do not add one-letter flags. - Required flags appear before optional flags in help output. -- `tdc fs` Unix-style aliases shorten only the command name. Their flags stay long and match the canonical command. -- Only `tdc configure` may prompt. -- Help works through `tdc help`, `tdc help`, and `tdc help`. +- `ti fs` Unix-style aliases shorten only the command name. Their flags stay long and match the canonical command. +- Only `ti configure` may prompt. +- Help works through `ti help`, `ti help`, and `ti help`. - `--version` remains available at every command level. - A command does not infer access mode, resource selection, or mutation intent from SQL or other user content. @@ -42,17 +42,17 @@ tdc [subcommand] - Commands that stream raw bytes reject `--query`. - Mutating control-plane commands support `--dry-run`; read-only commands reject it. - Dry run validates local input, credentials, placement, and request construction without sending a mutation. -- Errors use `tdc [ERROR]: ` and stable exit categories. +- Errors use `ti [ERROR]: ` and stable exit categories. - Commands do not silently retry through a different SQL role, transport, resource, or filesystem implementation. ## Profiles And Placement -All persistent tdc state belongs under `~/.tdc/`. +All persistent ti state belongs under `~/.ti/`. The local profile namespace is selected in this order: 1. Explicit `--profile`. -2. `TDC_PROFILE`. +2. `TI_PROFILE`. 3. `default`. An explicit empty `--profile ""` is invalid. Omitting `--profile` selects `default`. @@ -62,7 +62,7 @@ Users select placement with one canonical region code. They never provide separa Placement is selected in this order: 1. Explicit global `--region`. -2. `TDC_REGION_CODE`. +2. `TI_REGION_CODE`. 3. The selected profile's `region_code`. The command-scoped override does not change the profile, credential source, or persisted configuration. @@ -84,16 +84,16 @@ Supported TiDB Cloud Starter placement values are: TiDB Cloud public/private API keys are selected independently from the profile namespace: -1. If either `TDC_PUBLIC_KEY` or `TDC_PRIVATE_KEY` is set, both must be set and the pair is used. -2. Otherwise use `tdc_public_key` and `tdc_private_key` from the selected profile in `~/.tdc/credentials`. +1. If either `TIDB_CLOUD_PUBLIC_KEY` or `TIDB_CLOUD_PRIVATE_KEY` is set, both must be set and the pair is used. +2. Otherwise use `tidb_cloud_public_key` and `tidb_cloud_private_key` from the selected profile in `~/.ti/credentials`. -Environment credentials must not create or select a synthetic `[env]` profile. Any generated persistent state remains under the profile selected by `--profile`, `TDC_PROFILE`, or `default`. +Environment credentials must not create or select a synthetic `[env]` profile. Any generated persistent state remains under the profile selected by `--profile`, `TI_PROFILE`, or `default`. TiDB Cloud control-plane requests use HTTP Digest authentication. API keys must not be used as SQL Basic Auth credentials or Filesystem data-plane credentials. ## Configure And Default Project -`tdc configure` collects: +`ti configure` collects: - a canonical `region_code`; - a TiDB Cloud public API key; @@ -101,13 +101,13 @@ TiDB Cloud control-plane requests use HTTP Digest authentication. API keys must After validating the keys, configure lists accessible projects, requires exactly one project whose type is `tidbx_virtual`, and stores its ID as the profile's `project_id`. It commits the profile only after discovery succeeds. -`tdc configure --non-interactive` reads flags first, then `TDC_REGION_CODE`, `TDC_PUBLIC_KEY`, and `TDC_PRIVATE_KEY`, and fails instead of prompting for missing input. Interactive configure must handle Ctrl+C and exit with code 130. +`ti configure --non-interactive` reads flags first, then `TI_REGION_CODE`, `TIDB_CLOUD_PUBLIC_KEY`, and `TIDB_CLOUD_PRIVATE_KEY`, and fails instead of prompting for missing input. Interactive configure must handle Ctrl+C and exit with code 130. Starter cluster creation resolves its project in this order: 1. Explicit non-empty `--project-id`. 2. The selected profile's discovered `project_id`. -3. Otherwise fail before making the create request. +3. Otherwise omit the project label and let TiDB Cloud select the account default. Other DB operations identify existing resources by cluster or branch ID. Filesystem provisioning does not use the DB `project_id`. @@ -116,8 +116,8 @@ Other DB operations identify existing resources by cluster or branch ID. Filesys Main profile files are: ```text -~/.tdc/config -~/.tdc/credentials +~/.ti/config +~/.ti/credentials ``` `config` contains non-sensitive profile values. `credentials` contains only profile-scoped TiDB Cloud API keys. Sensitive files use owner-only permissions where the platform supports POSIX modes. @@ -125,22 +125,26 @@ Main profile files are: Example: ```toml -# ~/.tdc/config +# ~/.ti/config [default] region_code = "aws-us-east-1" project_id = "..." -# ~/.tdc/credentials +# ~/.ti/credentials [default] -tdc_public_key = "..." -tdc_private_key = "..." +tidb_cloud_public_key = "..." +tidb_cloud_private_key = "..." ``` +The v0.2.x release line accepts the corresponding legacy `TDC_*` environment variables and legacy `tdc_public_key`/`tdc_private_key` credential fields. New names win only when both values are equal; conflicting old and new values fail before mutation. New writes use canonical names only. + +When only `~/.tdc` exists, the first non-update `ti` invocation atomically copies durable state into `~/.ti`, records an owner-only migration marker, and preserves the old directory. When both directories were created independently or the marker is invalid, `ti` fails instead of merging them. Active mounts started by the old executable must be drained and unmounted with `tdc` before migration. Every `ti update` form bypasses this migration and all local product state. + One profile can own multiple Filesystem resources. Each resource has isolated metadata and credentials: ```text -~/.tdc/fs_resources///config -~/.tdc/fs_resources///credentials +~/.ti/fs_resources///config +~/.ti/fs_resources///credentials ``` The resource config stores `file_system_name`, `tenant_id`, `cloud_provider`, `region_code`, and `created_at`. Its credentials file stores only the owner `api_key`. The main profile never stores a default resource name or resource API keys. @@ -150,16 +154,16 @@ Legacy flat `fs_*` fields are migration input only. A complete legacy resource i DB SQL credentials are cluster-scoped because TiDB Cloud cluster IDs are globally unique: ```text -~/.tdc/db_users//credentials +~/.ti/db_users//credentials ``` The file contains `[read_only]`, `[read_write]`, and `[admin]` sections with generated username/password pairs. SQL credentials do not belong in the main profile credentials file. -Background Filesystem and Vault mounts store only non-secret routing state under `~/.tdc/mounts/`. Operation logs live under `~/.tdc/logs/`. +Background Filesystem and Vault mounts store only non-secret routing state under `~/.ti/mounts/`. Operation logs live under `~/.ti/logs/`. ## Starter SQL Access -`tdc db create-db-sql-users` creates or repairs three stable managed users for a cluster: +`ti db create-db-sql-users` creates or repairs three stable managed users for a cluster: - read-only; - read-write; @@ -167,7 +171,7 @@ Background Filesystem and Vault mounts store only non-secret routing state under The command is idempotent and must not create a new set on every invocation. -`tdc db format-db-connection-string` formats existing credentials; it does not create a remote resource. It supports common connection-string formats and `.env` components. `tdc db execute-sql-statement` executes exactly one statement. +`ti db format-db-connection-string` formats existing credentials; it does not create a remote resource. It supports common connection-string formats and `.env` components. `ti db execute-sql-statement` executes exactly one statement. Both commands default to read-write. `--read-write`, `--read-only`, and `--admin` are mutually exclusive explicit choices. There is no automatic role classification. @@ -175,42 +179,42 @@ SQL execution prefers the HTTPS SQL API and uses the selected SQL username/passw ## Filesystem Ownership Boundary -tdc does not implement filesystem runtime semantics itself. Installed `tdc fs`, `tdc fs-git`, `tdc fs-journal`, and `tdc fs-vault` commands route through the bundled `tdc-drive9` companion. +ti does not implement filesystem runtime semantics itself. Installed `ti fs`, `ti fs-git`, `ti fs-journal`, and `ti fs-vault` commands route through the bundled `ti-drive9` companion. -- tdc owns command naming, profile and region resolution, resource selection, credential storage, preflight validation, output/query behavior, errors, installation, and updates. +- ti owns command naming, profile and region resolution, resource selection, credential storage, preflight validation, output/query behavior, errors, installation, and updates. - Drive9 owns file data-plane semantics, layers, pack/unpack, FUSE and WebDAV mounts, drain, Git workspace behavior, journal behavior, and Vault behavior. -- There is no native tdc filesystem fallback. +- There is no native ti filesystem fallback. - `ref/drive9` is context only and is never imported, built, packaged, or used by tests. -- tdc exposes only operations present in the Drive9 public CLI. It does not expose Drive9 internal APIs. +- ti exposes only operations present in the Drive9 public CLI. It does not expose Drive9 internal APIs. Each resource runs the companion with isolated state under: ```text -~/.tdc/drive9-home// +~/.ti/drive9-home// ``` -tdc supplies a sanitized companion environment containing the resolved server, canonical region, and resource owner token. Inherited `DRIVE9_*` values must not override tdc's selection. Users do not edit `~/.drive9` for tdc workflows. +ti supplies a sanitized companion environment containing the resolved server, canonical region, and resource owner token. Inherited `DRIVE9_*` values must not override ti's selection. Users do not edit `~/.drive9` for ti workflows. Filesystem resource selection is: 1. Explicit `--file-system-name`. -2. `TDC_FS_FILE_SYSTEM_NAME`. +2. `TI_FS_FILE_SYSTEM_NAME`. 3. Otherwise fail with `fs.missing_file_system_name` before endpoint resolution, companion startup, or a remote call. -tdc never infers a target from profile state, the number of registered resources, creation order, or deletion side effects. Creating the first resource does not select it for later commands, and deleting a resource does not promote another resource. `TDC_FS_FILE_SYSTEM_NAME` is an explicit process-scoped selector, not a persisted default. +ti never infers a target from profile state, the number of registered resources, creation order, or deletion side effects. Creating the first resource does not select it for later commands, and deleting a resource does not promote another resource. `TI_FS_FILE_SYSTEM_NAME` is an explicit process-scoped selector, not a persisted default. Remote data-plane, mount, Git, journal, and owner Vault commands select their FS token in this order: 1. Explicit command-local token flag. -2. `TDC_FS_TOKEN`. +2. `TI_FS_TOKEN`. 3. The selected resource's credentials file. A clean agent sandbox can access an existing Filesystem using only: ```text -TDC_FS_TOKEN -TDC_REGION_CODE -TDC_FS_FILE_SYSTEM_NAME +TI_FS_TOKEN +TI_REGION_CODE +TI_FS_FILE_SYSTEM_NAME ``` These environment values form an in-memory command context and are not persisted. TiDB Cloud API keys remain required for `create-file-system` and `delete-file-system`; deletion also requires the resource to be registered locally. @@ -221,35 +225,35 @@ On macOS and Windows, automatic mounting selects WebDAV. On Linux, automatic mou ## Install And Update -GitHub Releases and GoReleaser produce release archives and checksums. Supported shell and PowerShell installers place both `tdc` and `tdc-drive9` in the user-owned `~/.tdc/bin` directory by default. +GitHub Releases and GoReleaser produce release archives and checksums. Supported shell and PowerShell installers place both `ti` and `ti-drive9` in the user-owned `~/.ti/bin` directory by default. - Installation and update do not require sudo. -- Installers do not edit shell profiles automatically; they print the command that prepends `~/.tdc/bin` to `PATH`. -- Installers support tdc release version pinning and checksum verification. The companion is currently downloaded and checksum-verified from Drive9's unversioned release endpoint; tdc does not yet negotiate a companion version range. -- `tdc update --check` checks explicitly; there is no background update. -- `tdc update` is itself explicit consent and does not require `--yes`. -- The updater stages and verifies tdc and its companion before replacement. -- Self-update is allowed only for tdc-owned installer/archive installations. +- Installers do not edit shell profiles automatically; they print the command that prepends `~/.ti/bin` to `PATH`. +- Installers support ti release version pinning and checksum verification. The companion is currently downloaded and checksum-verified from Drive9's unversioned release endpoint; ti does not yet negotiate a companion version range. +- `ti update --check` checks explicitly; there is no background update. +- `ti update` is itself explicit consent and does not require `--yes`. +- The updater stages and verifies ti and its companion before replacement. +- Self-update is allowed only for ti-owned installer/archive installations. - Package-manager, local-build, and unknown installations fail with actionable guidance. - Active mounts must be drained and unmounted before updating the companion. ## Logs, Telemetry, And Secret Safety -Local operation logs are enabled by default at `~/.tdc/logs/tdc.jsonl`. They may include command path, flag names, profile, region, duration, exit code, application error category, service, HTTP method/status, operation, and request ID. +Local operation logs are enabled by default at `~/.ti/logs/ti.jsonl`. They may include command path, flag names, profile, region, duration, exit code, application error category, service, HTTP method/status, operation, and request ID. Logs must never include flag values, SQL text or results, file contents, remote or local paths, request/response bodies, connection strings, API keys, FS tokens, DB passwords, Vault tokens, or secret values. -Disable local logging for one process with `TDC_LOGGING=off`, or globally: +Disable local logging for one process with `TI_LOGGING=off`, or globally: ```toml -# ~/.tdc/.preferences +# ~/.ti/.preferences schema_version = 1 [logging] enabled = false ``` -`~/.tdc/.preferences` is optional, hidden from ordinary directory listings, global across profiles, and separate from the profile-only `~/.tdc/config` and `~/.tdc/credentials` files. Missing settings use in-memory defaults without creating the file. Existing legacy `[logging]` configuration in `~/.tdc/config` is migrated atomically. `tdc update` does not read or write any state under `~/.tdc/`, including settings and operation logs. +`~/.ti/.preferences` is optional, hidden from ordinary directory listings, global across profiles, and separate from the profile-only `~/.ti/config` and `~/.ti/credentials` files. Missing settings use in-memory defaults without creating the file. Existing legacy `[logging]` configuration in `~/.ti/config` is migrated atomically. `ti update` does not read or write any state under `~/.ti/`, including settings and operation logs. Telemetry follows the same data-minimization rule and must be explicitly disclosed after installation. It can collect command/subcommand names, flag names, error codes, duration, region, CLI version, and OS type, but never credentials or user content. Telemetry must have documented settings-file and process-scoped environment opt-outs before collection is enabled. diff --git a/docs/requirements/mount-file-system-config-free-mount.md b/docs/requirements/mount-file-system-config-free-mount.md index 112c020..8c3e154 100644 --- a/docs/requirements/mount-file-system-config-free-mount.md +++ b/docs/requirements/mount-file-system-config-free-mount.md @@ -1,18 +1,18 @@ -# `tdc fs mount-file-system` Configuration\-Free Mount +# `ti fs mount-file-system` Configuration\-Free Mount ## Problem -Today `tdc fs mount-file-system` requires an initialized `~/.tdc/credentials` file, which in turn requires `tdc configure` with TiDB Cloud API public/private keys\. This makes the mount command unusable in ephemeral environments \(CI/CD pipelines, E2B sandboxes, Docker containers\) where provisioning long\-lived API credentials is undesirable\. Users want a "mount and go" experience with only the information intrinsic to the filesystem itself\. +Today `ti fs mount-file-system` requires an initialized `~/.ti/credentials` file, which in turn requires `ti configure` with TiDB Cloud API public/private keys\. This makes the mount command unusable in ephemeral environments \(CI/CD pipelines, E2B sandboxes, Docker containers\) where provisioning long\-lived API credentials is undesirable\. Users want a "mount and go" experience with only the information intrinsic to the filesystem itself\. ## Solution -Make `tdc fs mount-file-system --file-system-name my-workspace --mount-path ~/my-workspace` work without any prior `tdc configure` by accepting three pieces of information directly: +Make `ti fs mount-file-system --file-system-name my-workspace --mount-path ~/my-workspace` work without any prior `ti configure` by accepting three pieces of information directly: 1. **file\-system\-name** — identifies the target File System -2. **\-\-region\-code** flag or **TDC\_REGION\_CODE** environment variable — specifies the deployment region +2. **\-\-region\-code** flag or **TI\_REGION\_CODE** environment variable — specifies the deployment region -3. **\-\-fs\-token** flag or **TDC\_FS\_TOKEN** environment variable — the filesystem\-scoped authentication token +3. **\-\-fs\-token** flag or **TI\_FS\_TOKEN** environment variable — the filesystem\-scoped authentication token With these three variables, the CLI has everything it needs to mount the filesystem — region for the API endpoint, FS identity, and a scoped credential — without a credentials file or API key pair\. @@ -20,35 +20,35 @@ With these three variables, the CLI has everything it needs to mount the filesys ```bash # Minimal mount — everything via flags -tdc fs mount-file-system \ +ti fs mount-file-system \ --file-system-name my-workspace \ --mount-path ~/my-workspace \ - --region-code aws-us-east-1 \ - --fs-token tdc_fs_v1_abc123...xyz + --region aws-us-east-1 \ + --fs-token drive9_abc123...xyz # Mount using env vars for region and token -export TDC_REGION_CODE=aws-us-east-1 -export TDC_FS_TOKEN=tdc_fs_v1_abc123...xyz -tdc fs mount-file-system \ +export TI_REGION_CODE=aws-us-east-1 +export TI_FS_TOKEN=drive9_abc123...xyz +ti fs mount-file-system \ --file-system-name my-workspace \ --mount-path ~/my-workspace # Mount with mixed sources — pick what suits the environment -tdc fs mount-file-system \ +ti fs mount-file-system \ --file-system-name my-workspace \ --mount-path ~/my-workspace \ - --region-code aws-us-east-1 + --region aws-us-east-1 ``` ## Variable Precedence When multiple sources provide the same variable, the priority is \(highest wins\): -1. CLI flag \(`--region-code`, `--fs-token`\) +1. CLI flag \(`--region`, `--fs-token`\) -2. Environment variable \(`TDC_REGION_CODE`, `TDC_FS_TOKEN`\) +2. Environment variable \(`TI_REGION_CODE`, `TI_FS_TOKEN`\) -3. `~/.tdc/credentials` \(existing config, if any\) +3. `~/.ti/credentials` \(existing config, if any\) This means existing configured setups continue to work unchanged — the new flags and env vars are purely additive\. @@ -62,11 +62,11 @@ This means existing configured setups continue to work unchanged — the new fla 4. Mount proceeds as normal — FUSE mount \(Linux/macOS\) or WebDAV \(fallback\) -5. No `~/.tdc/credentials` file is **pre\-required** before this mount command +5. No `~/.ti/credentials` file is **pre\-required** before this mount command ## Use Cases -- **E2B sandboxes:** pass `TDC_FS_TOKEN` and `TDC_REGION_CODE` as sandbox environment variables — no config step needed +- **E2B sandboxes:** pass `TI_FS_TOKEN` and `TI_REGION_CODE` as sandbox environment variables — no config step needed - **CI/CD pipelines:** inject token and region via CI secrets, mount in one command diff --git a/docs/spec/0026-homebrew-and-scoop-distribution.md b/docs/spec/0027-homebrew-and-scoop-distribution.md similarity index 57% rename from docs/spec/0026-homebrew-and-scoop-distribution.md rename to docs/spec/0027-homebrew-and-scoop-distribution.md index 6354a55..607ed8b 100644 --- a/docs/spec/0026-homebrew-and-scoop-distribution.md +++ b/docs/spec/0027-homebrew-and-scoop-distribution.md @@ -9,38 +9,38 @@ Add optional package-manager distribution after the GitHub Releases installer wo Homebrew: ```bash -brew install tidbcloud/tap/tdc -brew upgrade tidbcloud/tap/tdc +brew install tidbcloud/tap/ti-cli +brew upgrade tidbcloud/tap/ti-cli ``` Scoop: ```powershell -scoop bucket add icemap https://github.com/tidbcloud/scoop-bucket -scoop install tdc -scoop update tdc +scoop bucket add tidbcloud https://github.com/tidbcloud/scoop-bucket +scoop install ti-cli +scoop update ti-cli ``` -Existing `tdc` commands keep the same behavior: +Existing `ti` commands keep the same behavior: ```bash -tdc update --check -tdc update --dry-run -tdc update +ti update --check +ti update --dry-run +ti update ``` -For Homebrew and Scoop installs, `tdc update` must not replace the binary. It returns `update.managed_install` with the correct package-manager command. +For Homebrew and Scoop installs, `ti update` must not replace the binary. It returns `update.managed_install` with the correct package-manager command. ## Behavior - Homebrew distribution uses a separate tap repository, expected name `github.com/tidbcloud/homebrew-tap`. - Scoop distribution uses a separate bucket repository, expected name `github.com/tidbcloud/scoop-bucket`. -- The main `github.com/tidbcloud/tdc` release remains the source of binary artifacts and checksums. +- The main `github.com/tidbcloud/ti-cli` release remains the source of binary artifacts and checksums. - GoReleaser updates the Homebrew formula and Scoop manifest as part of release publishing after this spec is implemented. - The Homebrew formula and Scoop manifest consume GitHub Releases assets produced by `0012-install-and-update-distribution.md`. - Package-manager installs embed `install_source=homebrew` or `install_source=scoop` through package build/wrapper metadata when practical. -- `tdc update` also detects common Homebrew and Scoop install paths as a fallback, even if build metadata is missing. -- Users can still run `tdc update --check` from Homebrew/Scoop installs; it reports release availability but does not mutate package-managed files. +- `ti update` also detects common Homebrew and Scoop install paths as a fallback, even if build metadata is missing. +- Users can still run `ti update --check` from Homebrew/Scoop installs; it reports release availability but does not mutate package-managed files. ## Inputs And Config @@ -55,20 +55,20 @@ GoReleaser config additions: - Formula/manifest URLs pointing to GitHub Releases assets. - SHA-256 values generated from the release artifacts. -No user `~/.tdc/` config or credentials are required for package-manager installation. Package-manager metadata must not store TiDB Cloud API keys, DB credentials, fs API keys, SQL text, file paths, or telemetry identifiers. +No user `~/.ti/` config or credentials are required for package-manager installation. Package-manager metadata must not store TiDB Cloud API keys, DB credentials, fs API keys, SQL text, file paths, or telemetry identifiers. ## Output And Errors -`tdc update` from a Homebrew install: +`ti update` from a Homebrew install: ```text -tdc [ERROR]: tdc is managed by homebrew; update it with `brew upgrade tidbcloud/tap/tdc` +ti [ERROR]: ti is managed by homebrew; update it with `brew upgrade tidbcloud/tap/ti-cli` ``` -`tdc update` from a Scoop install: +`ti update` from a Scoop install: ```text -tdc [ERROR]: tdc is managed by scoop; update it with `scoop update tdc` +ti [ERROR]: ti is managed by scoop; update it with `scoop update ti-cli` ``` The structured error code remains `update.managed_install`. @@ -78,22 +78,22 @@ The structured error code remains `update.managed_install`. Users can choose between direct GitHub Releases installers and package-manager installs: ```bash -curl -fsSL https://github.com/tidbcloud/tdc/releases/latest/download/install.sh | sh -s -- --yes -export PATH="$HOME/.tdc/bin:$PATH" -brew install tidbcloud/tap/tdc +curl -fsSL https://github.com/tidbcloud/ti-cli/releases/latest/download/install.sh | sh -s -- --yes +export PATH="$HOME/.ti/bin:$PATH" +brew install tidbcloud/tap/ti-cli ``` Windows users can choose between the PowerShell installer and Scoop: ```powershell -iwr https://github.com/tidbcloud/tdc/releases/latest/download/install.ps1 -OutFile $env:TEMP\install-tdc.ps1 -powershell -ExecutionPolicy Bypass -File $env:TEMP\install-tdc.ps1 -Yes -$env:Path = "$HOME\.tdc\bin;$env:Path" -scoop bucket add icemap https://github.com/tidbcloud/scoop-bucket -scoop install tdc +iwr https://github.com/tidbcloud/ti-cli/releases/latest/download/install.ps1 -OutFile $env:TEMP\install-ti.ps1 +powershell -ExecutionPolicy Bypass -File $env:TEMP\install-ti.ps1 -Yes +$env:Path = "$HOME\.ti\bin;$env:Path" +scoop bucket add tidbcloud https://github.com/tidbcloud/scoop-bucket +scoop install ti-cli ``` -Package-manager users update through the package manager, not `tdc update`. +Package-manager users update through the package manager, not `ti update`. ## Implementation Design @@ -102,20 +102,20 @@ Package-manager users update through the package manager, not `tdc update`. - Add release workflow secret usage for the cross-repository publishing token. - Add README installation sections for Homebrew and Scoop. - Add e2e/unit coverage for `install_source=homebrew`, `install_source=scoop`, and known path detection. -- Keep `tdc update` refusal logic in `internal/update`; do not add package-manager-specific update code outside that package. +- Keep `ti update` refusal logic in `internal/update`; do not add package-manager-specific update code outside that package. Homebrew tap repository work: - Create `github.com/tidbcloud/homebrew-tap`. -- Let GoReleaser write a formula such as `Formula/tdc.rb`. -- Formula installs `tdc` from GitHub Releases. -- Formula test should run `tdc --version`. +- Let GoReleaser write a formula such as `Formula/ti-cli.rb`. +- Formula installs `ti` from GitHub Releases. +- Formula test should run `ti --version`. Scoop bucket repository work: - Create `github.com/tidbcloud/scoop-bucket`. -- Let GoReleaser write `bucket/tdc.json`. -- Manifest installs `tdc.exe` from GitHub Releases. +- Let GoReleaser write `bucket/ti-cli.json`. +- Manifest installs `ti.exe` from GitHub Releases. - Manifest checkver/autoupdate should track GitHub Releases tags. ## API Call Chain @@ -131,9 +131,9 @@ Release publishing call chain: Runtime update call chain from package-managed installs: -1. `tdc update --check` reads GitHub Releases metadata normally. -2. `tdc update` reads local install-source metadata and known install path patterns. -3. `tdc update` returns `update.managed_install` before downloading or replacing anything. +1. `ti update --check` reads GitHub Releases metadata normally. +2. `ti update` reads local install-source metadata and known install path patterns. +3. `ti update` returns `update.managed_install` before downloading or replacing anything. ## Dependencies And Platform @@ -146,16 +146,17 @@ Runtime update call chain from package-managed installs: ## Dependencies - `0012-install-and-update-distribution.md` +- `0026-ti-cli-rename-and-migration.md` - `0013-github-actions-ci-cd.md` if package-manager publishing is automated through the same release workflow hardening. ## Acceptance Criteria -- `brew install tidbcloud/tap/tdc` installs a released `tdc` binary on macOS. -- `brew upgrade tidbcloud/tap/tdc` upgrades to a newer release. -- `scoop bucket add icemap https://github.com/tidbcloud/scoop-bucket` and `scoop install tdc` install `tdc.exe` on Windows. -- `scoop update tdc` upgrades to a newer release. -- `tdc update --check` works from Homebrew and Scoop installs. -- `tdc update` refuses Homebrew and Scoop installs with `update.managed_install`. +- `brew install tidbcloud/tap/ti-cli` installs a released `ti` binary on macOS. +- `brew upgrade tidbcloud/tap/ti-cli` upgrades to a newer release. +- `scoop bucket add tidbcloud https://github.com/tidbcloud/scoop-bucket` and `scoop install ti-cli` install `ti.exe` on Windows. +- `scoop update ti-cli` upgrades to a newer release. +- `ti update --check` works from Homebrew and Scoop installs. +- `ti update` refuses Homebrew and Scoop installs with `update.managed_install`. - README documents Homebrew and Scoop as optional package-manager channels. ## Out Of Scope @@ -163,4 +164,4 @@ Runtime update call chain from package-managed installs: - apt, yum, dnf, apk, pacman, winget, Chocolatey, Mac App Store, Microsoft Store, Snap, Flatpak, and other package registries. - Package-manager install telemetry. - Auto-update daemons or background update checks. -- Changing the `~/.tdc/` config and credentials model. +- Changing the `~/.ti/` config and credentials model. diff --git a/docs/spec/0027-serverless-function-deployment.md b/docs/spec/0028-serverless-function-deployment.md similarity index 76% rename from docs/spec/0027-serverless-function-deployment.md rename to docs/spec/0028-serverless-function-deployment.md index e4a7469..0c9651f 100644 --- a/docs/spec/0027-serverless-function-deployment.md +++ b/docs/spec/0028-serverless-function-deployment.md @@ -6,7 +6,7 @@ Add an agent-friendly serverless function workflow that can package a local HTTP function and deploy it to supported external runtimes such as Vercel Functions and AWS Lambda. -tdc should not try to become a serverless platform in this spec. It should be a +`ti` should not try to become a serverless platform in this spec. It should be a predictable packaging and deployment adapter: local source plus an explicit manifest becomes a provider-specific deployment, and the CLI reports the resulting function URL and metadata in structured output. @@ -16,16 +16,16 @@ resulting function URL and metadata in structured output. Add a new top-level command namespace: ```bash -tdc function init-function -tdc function validate-function -tdc function package-function -tdc function deploy-function -tdc function describe-function -tdc function list-functions -tdc function delete-function -tdc function invoke-function -tdc function get-function-url -tdc function logs-function +ti function init-function +ti function validate-function +ti function package-function +ti function deploy-function +ti function describe-function +ti function list-functions +ti function delete-function +ti function invoke-function +ti function get-function-url +ti function logs-function ``` Initial MVP commands may implement only `init-function`, `validate-function`, @@ -36,12 +36,12 @@ until provider state management is implemented. Example: ```bash -tdc function init-function --function-name hello --runtime nodejs --target vercel-node -tdc function validate-function --function-file tdc.function.toml -tdc function package-function --function-file tdc.function.toml --output-dir .tdc/functions/hello -tdc function deploy-function --function-file tdc.function.toml --target vercel-node -tdc function get-function-url --function-name hello --target vercel-node -tdc function invoke-function --function-url https://example.vercel.app/api/hello +ti function init-function --function-name hello --runtime nodejs --target vercel-node +ti function validate-function --function-file ti.function.toml +ti function package-function --function-file ti.function.toml --output-dir .ti/functions/hello +ti function deploy-function --function-file ti.function.toml --target vercel-node +ti function get-function-url --function-name hello --target vercel-node +ti function invoke-function --function-url https://example.vercel.app/api/hello ``` ## Behavior @@ -74,9 +74,9 @@ export default { without calling provider mutation APIs. - Read-only commands reject `--dry-run`. - Successful structured commands support `--output json|text` and `--query`. -- Do not prompt except inside existing `tdc configure`. Missing provider CLI +- Do not prompt except inside existing `ti configure`. Missing provider CLI installation or authentication must fail with actionable errors. -- `tdc function help` and `tdc function deploy-function help` must list each +- `ti function help` and `ti function deploy-function help` must list each supported target and its required external tools, authentication expectation, and major cloud resources. @@ -85,7 +85,7 @@ export default { Each function is described by a local manifest: ```toml -# tdc.function.toml +# ti.function.toml name = "hello" runtime = "nodejs" entrypoint = "src/hello.ts" @@ -110,11 +110,11 @@ Supported MVP manifest fields: - `[http].path`: HTTP path when the target supports routing. - `[http].methods`: allowed methods for generated adapter code when supported. - `[env]`: environment variable bindings. Values starting with `secret:` refer - to provider-side secrets or tdc-managed secret references and must not be + to provider-side secrets or `ti`-managed secret references and must not be inlined into package artifacts. Provider credentials are not stored in the function manifest, local deployment -metadata, or `~/.tdc/credentials`. The MVP delegates provider authentication to +metadata, or `~/.ti/credentials`. The MVP delegates provider authentication to official provider CLIs and their standard auth mechanisms. Provider authentication inputs: @@ -123,7 +123,7 @@ Provider authentication inputs: - `vercel` CLI must be installed. - Local interactive users authenticate with `vercel login`. - CI users set `VERCEL_TOKEN` through the CI secret store. - - tdc verifies non-mutating availability/authentication through `vercel + - `ti` verifies non-mutating availability/authentication through `vercel whoami` or an equivalent Vercel CLI command. - AWS: - AWS CLI v2 must be installed. @@ -131,13 +131,13 @@ Provider authentication inputs: `aws configure`, `aws sso login`, or `AWS_PROFILE`. - CI users use standard AWS environment variables, OIDC role assumption, or another AWS CLI-supported credential source. - - tdc verifies non-mutating availability/authentication through `aws sts + - `ti` verifies non-mutating availability/authentication through `aws sts get-caller-identity`. - `aws-lambda-container` additionally requires Docker or a compatible OCI builder because it builds and pushes a container image. -Provider deployment defaults are non-sensitive and live in `~/.tdc/config` -under the active profile. Keep tdc-wide, Vercel-specific, and AWS-specific +Provider deployment defaults are non-sensitive and live in `~/.ti/config` +under the active profile. Keep `ti`-wide, Vercel-specific, and AWS-specific settings visually separated in examples: ```toml @@ -149,26 +149,26 @@ function_vercel_org_id = "team_..." function_aws_profile = "default" function_aws_region_code = "us-east-1" -function_aws_lambda_role_arn = "arn:aws:iam::123456789012:role/tdc-lambda-role" -function_aws_ecr_repository = "tdc-functions" +function_aws_lambda_role_arn = "arn:aws:iam::123456789012:role/ti-lambda-role" +function_aws_ecr_repository = "ti-functions" ``` `function_default_target` is the default deployment target, not a provider credential. Target selection precedence is: 1. `--target` -2. `target` in `tdc.function.toml` -3. `function_default_target` in `~/.tdc/config` +2. `target` in `ti.function.toml` +3. `function_default_target` in `~/.ti/config` If none of those values is present, deployment and packaging commands fail and ask the user to choose a target explicitly. Do not store both `function_provider` and `function_default_target`; the provider is derived from the target prefix. -Local deployment metadata lives under `~/.tdc/functions/`: +Local deployment metadata lives under `~/.ti/functions/`: ```text -~/.tdc/functions////deployment.json +~/.ti/functions////deployment.json ``` This metadata may store provider deployment IDs, function URLs, target, region, @@ -195,7 +195,7 @@ Future targets: Do not claim one source file is fully portable across all targets. The manifest and validation must make runtime/target constraints explicit. -Help text for `tdc function` must include a compact target table equivalent to: +Help text for `ti function` must include a compact target table equivalent to: ```text Targets: @@ -234,19 +234,19 @@ URL: https://example.vercel.app/api/hello Example validation error: ```text -tdc [ERROR]: target vercel-edge does not support runtime nodejs with Node built-in module "fs"; use vercel-node or remove the dependency +ti [ERROR]: target vercel-edge does not support runtime nodejs with Node built-in module "fs"; use vercel-node or remove the dependency ``` Example missing credentials error: ```text -tdc [ERROR]: authentication required: Vercel CLI is not authenticated; run `vercel login` or set VERCEL_TOKEN +ti [ERROR]: authentication required: Vercel CLI is not authenticated; run `vercel login` or set VERCEL_TOKEN ``` Example missing external dependency error: ```text -tdc [ERROR]: dependency required: target aws-lambda-container requires Docker; install Docker or choose --target aws-lambda-zip +ti [ERROR]: dependency required: target aws-lambda-container requires Docker; install Docker or choose --target aws-lambda-zip ``` ## After This Spec @@ -254,22 +254,22 @@ tdc [ERROR]: dependency required: target aws-lambda-container requires Docker; i Users can create and deploy a function from a local project: ```bash -tdc function init-function --function-name hello --runtime nodejs --target vercel-node -tdc function validate-function -tdc function deploy-function -tdc function get-function-url --function-name hello -tdc function invoke-function --function-name hello --method GET +ti function init-function --function-name hello --runtime nodejs --target vercel-node +ti function validate-function +ti function deploy-function +ti function get-function-url --function-name hello +ti function invoke-function --function-name hello --method GET ``` CI can package and deploy non-interactively: ```bash -tdc function validate-function --function-file tdc.function.toml -tdc function package-function --function-file tdc.function.toml --output-dir .tdc/build/function -tdc function deploy-function --function-file tdc.function.toml --target aws-lambda-zip --dry-run -tdc function deploy-function --function-file tdc.function.toml --target aws-lambda-zip -tdc function deploy-function --function-file tdc.function.toml --target aws-lambda-container --dry-run -tdc function deploy-function --function-file tdc.function.toml --target aws-lambda-container +ti function validate-function --function-file ti.function.toml +ti function package-function --function-file ti.function.toml --output-dir .ti/build/function +ti function deploy-function --function-file ti.function.toml --target aws-lambda-zip --dry-run +ti function deploy-function --function-file ti.function.toml --target aws-lambda-zip +ti function deploy-function --function-file ti.function.toml --target aws-lambda-container --dry-run +ti function deploy-function --function-file ti.function.toml --target aws-lambda-container ``` ## Implementation Design @@ -294,10 +294,10 @@ type Provider interface { - `internal/function/provider/aws` shells out to AWS CLI v2 and parses stable machine-readable JSON output. - `internal/function/state` stores local deployment metadata under - `~/.tdc/functions/`. -- `internal/cli` registers `tdc function ...` commands and keeps handlers thin. + `~/.ti/functions/`. +- `internal/cli` registers `ti function ...` commands and keeps handlers thin. - The first implementation must use official provider CLIs for authentication - and provider operations. Do not implement provider login flows in tdc. Avoid + and provider operations. Do not implement provider login flows in `ti`. Avoid scraping human output; request JSON output from provider CLIs whenever supported. @@ -310,7 +310,7 @@ Packaging strategy: Function URL. - `aws-lambda-container` builds an OCI image, pushes it to ECR, creates or updates a Lambda function, and optionally creates a Lambda Function URL. -- All packaging must happen in a deterministic output directory under `.tdc/` +- All packaging must happen in a deterministic output directory under `.ti/` or a user-specified `--output-dir`. - Generated adapters should be small and explicit. Do not rewrite user source files in place. @@ -319,19 +319,19 @@ Packaging strategy: Vercel deploy flow, high level: -1. Read `tdc.function.toml`. +1. Read `ti.function.toml`. 2. Validate target/runtime compatibility. 3. Build provider output for Vercel. 4. Verify `vercel` is installed. 5. Verify `vercel` is authenticated through `vercel whoami` or equivalent. CI - may provide `VERCEL_TOKEN`; tdc does not store it. + may provide `VERCEL_TOKEN`; `ti` does not store it. 6. Run `vercel deploy` with non-interactive flags and machine-readable output where available. 7. Record deployment URL and metadata. AWS Lambda ZIP deploy flow, high level: -1. Read `tdc.function.toml`. +1. Read `ti.function.toml`. 2. Validate AWS CLI, AWS credentials, AWS region, Lambda role ARN, runtime, and handler settings. 3. Build runtime-specific ZIP artifact. @@ -342,7 +342,7 @@ AWS Lambda ZIP deploy flow, high level: AWS Lambda container deploy flow, high level: -1. Read `tdc.function.toml`. +1. Read `ti.function.toml`. 2. Validate AWS CLI, Docker/OCI builder, AWS credentials, AWS region, Lambda role ARN, and ECR repository settings. 3. Build OCI image. @@ -380,37 +380,39 @@ Platform notes: - Packaging should work on macOS and Linux first. - Windows support is desired but may be limited by Docker and shell behavior in the first implementation. -- Do not introduce cgo requirements into the tdc binary. +- Do not introduce cgo requirements into the `ti` binary. ## Dependencies - `0001-cli-foundation.md` for command patterns. - `0002-local-config-and-credentials.md` for profile and local state rules. - `0003-output-error-query-dry-run.md` for output, query, and dry-run behavior. +- `0026-ti-cli-rename-and-migration.md` for executable, state path, and + environment naming. - `0014-tdc-fs-unix-command-aliases.md` is unrelated but should remain unaffected. ## Acceptance Criteria -- `tdc function init-function` creates a minimal `tdc.function.toml` and sample +- `ti function init-function` creates a minimal `ti.function.toml` and sample source file without overwriting existing files unless `--overwrite` is set. -- `tdc function validate-function` rejects invalid target/runtime combinations +- `ti function validate-function` rejects invalid target/runtime combinations before any provider mutation. -- `tdc function help` and `tdc function deploy-function help` show the +- `ti function help` and `ti function deploy-function help` show the supported target table with required external tools and authentication notes. -- `tdc function package-function` writes deterministic provider output and +- `ti function package-function` writes deterministic provider output and reports a source digest. -- `tdc function deploy-function --dry-run` validates inputs and planned provider +- `ti function deploy-function --dry-run` validates inputs and planned provider operations without mutating remote state. -- `tdc function deploy-function --target vercel-node` deploys a simple HTTP +- `ti function deploy-function --target vercel-node` deploys a simple HTTP function and returns a URL. -- `tdc function deploy-function --target aws-lambda-zip` deploys a simple HTTP +- `ti function deploy-function --target aws-lambda-zip` deploys a simple HTTP function to Lambda ZIP and returns a Function URL when configured. -- `tdc function deploy-function --target aws-lambda-container` deploys a simple +- `ti function deploy-function --target aws-lambda-container` deploys a simple HTTP function to Lambda container and returns a Function URL when configured. -- `tdc function invoke-function` can call the returned URL and print status, +- `ti function invoke-function` can call the returned URL and print status, headers, and body according to output mode. -- Local deployment metadata is stored under `~/.tdc/functions/` without secrets. +- Local deployment metadata is stored under `~/.ti/functions/` without secrets. - `make test` covers manifest validation, packaging decisions, state storage, and dry-run behavior without live provider credentials. - Live provider tests, if added, are opt-in and isolated behind explicit diff --git a/docs/spec/done/0012-install-and-update-distribution.md b/docs/spec/done/0012-install-and-update-distribution.md index 8e32158..77ef3aa 100644 --- a/docs/spec/done/0012-install-and-update-distribution.md +++ b/docs/spec/done/0012-install-and-update-distribution.md @@ -2,7 +2,7 @@ ## Goal -Make `tdc` installable and updatable through deterministic GitHub Releases artifacts. The MVP channel is GoReleaser plus GitHub Releases, with shell and PowerShell installers. Homebrew and Scoop are intentionally deferred to `docs/spec/0026-homebrew-and-scoop-distribution.md`. +Make `tdc` installable and updatable through deterministic GitHub Releases artifacts. The MVP channel is GoReleaser plus GitHub Releases, with shell and PowerShell installers. Homebrew and Scoop are intentionally deferred to `docs/spec/0027-homebrew-and-scoop-distribution.md`. ## User-facing Commands @@ -266,7 +266,7 @@ Installer scripts: - Silent auto-update. - Updating TiDB Cloud credentials or DB SQL credentials. - Config migrations that modify user config during update. -- Homebrew tap and Scoop bucket publishing. See `0026-homebrew-and-scoop-distribution.md`. +- Homebrew tap and Scoop bucket publishing. See `0027-homebrew-and-scoop-distribution.md`. - Linux apt/yum repositories. - Winget publishing. - Notarization or binary signing beyond SHA-256 checksums for MVP. diff --git a/docs/spec/done/0026-ti-cli-rename-and-migration.md b/docs/spec/done/0026-ti-cli-rename-and-migration.md new file mode 100644 index 0000000..a8e90e8 --- /dev/null +++ b/docs/spec/done/0026-ti-cli-rename-and-migration.md @@ -0,0 +1,621 @@ +# Ti CLI Rename and Migration + +## Goal + +Rename the repository and user-facing command-line product from `tdc` to `ti` +without silently losing existing local configuration, credentials, filesystem +registrations, SQL credentials, or telemetry identity. + +This is an explicit breaking change released as `v0.2.0`. The new release does +not provide a `tdc` executable alias, compatibility symlink, wrapper command, or +bridge release. Existing users install `ti` with the new installer and validate +the migrated state before manually removing the old installation. + +The formal product names remain: + +- TiDB Cloud Command Line Interface +- TiDB Cloud CLI +- `ti` when referring to the executable, commands, or implementation + +## Canonical Names + +After this spec, new code, build artifacts, installation paths, and active +documentation use the following names: + +| Concern | Canonical value | +| --- | --- | +| GitHub repository | `github.com/tidbcloud/ti-cli` | +| Go module | `github.com/tidbcloud/ti-cli` | +| Primary executable | `ti` or `ti.exe` | +| Installed filesystem companion | `ti-drive9` or `ti-drive9.exe` | +| User state directory | `~/.ti/` | +| Profile config | `~/.ti/config` | +| Profile credentials | `~/.ti/credentials` | +| Global preferences | `~/.ti/.preferences` | +| Telemetry installation identity | `~/.ti/.telemetry-installation-id` | +| Operation log | `~/.ti/logs/ti.jsonl` | +| Telemetry backend executable | `ti-telemetry-backend` | +| Telemetry migration executable | `ti-telemetry-migrate` | +| Default install directory | `~/.ti/bin` | + +Historical completed specs and release records may continue to use `tdc` when +describing behavior before `v0.2.0`. They are historical records, not current +product naming. + +## User-facing Command Changes + +Every current command changes only its executable prefix unless another active +spec explicitly changes that command's contract. For example: + +```bash +ti configure +ti update --check +ti organization list-projects +ti db create-db-cluster --db-cluster-type starter --db-cluster-name demo +ti fs create-file-system --file-system-name workspace +ti fs mount-file-system --file-system-name workspace --mount-path ./workspace +ti fs-git clone-git-workspace --repo-url https://github.com/pingcap/tidb.git --target-path ./workspace/tidb +ti fs-journal create-journal --journal-id task-log --journal-kind agent --title "task log" +ti fs-vault list-secrets +``` + +The old executable must fail as absent after users remove it. `ti` must not +register `tdc` as a top-level command, alias, hidden compatibility command, or +subcommand. + +Help and errors use `ti` consistently: + +```text +usage: ti [parameters] +``` + +```text +ti [ERROR]: authentication required: ... +``` + +## Repository And Source Layout + +Implementation changes include: + +- Change `go.mod` to `module github.com/tidbcloud/ti-cli`. +- Rewrite imports owned by this repository to the new module path. +- Rename `cmd/tdc` to `cmd/ti`. +- Rename `cmd/tdc-telemetry-backend` to `cmd/ti-telemetry-backend`. +- Rename `cmd/tdc-telemetry-migrate` to `cmd/ti-telemetry-migrate`. +- Build `bin/ti`, `bin/ti-telemetry-backend`, and + `bin/ti-telemetry-migrate` locally. +- Package `ti` in release archives and install the checksum-verified Drive9 + companion as `ti-drive9` through the existing installer/update flow. +- Rename build variables, test-only environment variables, Makefile variables, + workflow artifact names, installer variables, and GoReleaser artifact names + where they describe this product rather than an external API. +- Update the Git remote examples and release URLs to + `github.com/tidbcloud/ti-cli`. + +The installed companion remains an implementation detail. User-facing output +continues to describe TiDB Cloud Filesystem rather than presenting Drive9 as a +separate product. + +## Canonical Environment Variables + +New documentation and output use only these canonical variables: + +| Purpose | Canonical variable | Legacy v0.2.x fallback | +| --- | --- | --- | +| Selected profile | `TI_PROFILE` | `TDC_PROFILE` | +| Effective region | `TI_REGION_CODE` | `TDC_REGION_CODE` | +| TiDB Cloud API public key | `TIDB_CLOUD_PUBLIC_KEY` | `TDC_PUBLIC_KEY` | +| TiDB Cloud API private key | `TIDB_CLOUD_PRIVATE_KEY` | `TDC_PRIVATE_KEY` | +| Filesystem-scoped token | `TI_FS_TOKEN` | `TDC_FS_TOKEN` | +| Filesystem name | `TI_FS_FILE_SYSTEM_NAME` | `TDC_FS_FILE_SYSTEM_NAME` | +| Local operation logging override | `TI_LOGGING` | `TDC_LOGGING` | +| Telemetry enablement override | `TI_TELEMETRY` | `TDC_TELEMETRY` | +| Telemetry tag | `TI_TELEMETRY_TAG` | `TDC_TELEMETRY_TAG` | +| Telemetry JSON metadata | `TI_TELEMETRY_EXTRA` | `TDC_TELEMETRY_EXTRA` | +| Filesystem vault token | `TI_VAULT_TOKEN` | `TDC_VAULT_TOKEN` | +| Installer destination | `TI_INSTALL_DIR` | `TDC_INSTALL_DIR` | + +The v0.2 release line temporarily accepts the corresponding legacy `TDC_*` +variables. Compatibility behavior is deterministic: + +1. If only the canonical variable exists, use it. +2. If only the legacy variable exists, use it. +3. If both exist with the same value, use the canonical variable. +4. If both exist with different values, fail before any remote or local + mutation with `config.environment_conflict`. + +Legacy environment variable use must not add warnings to stdout or stderr, +because that could corrupt structured command output. The operation log may +record the deprecated variable name, but never its value. Legacy `TDC_*` +support is removed in `v0.3.0`. + +Configuration flags become: + +```text +--tidb-cloud-public-key +--tidb-cloud-private-key +``` + +The old `--tdc-public-key` and `--tdc-private-key` flags are removed and are not +registered as aliases. This keeps the public help surface unambiguous. + +All internal CI and test variables owned by this repository also move to +`TI_*` or `TIDB_CLOUD_*` names. Internal-only compatibility aliases are not +kept. + +## New Local State Model + +The new state directory is independent from the old installation: + +```text +~/.ti/ + config + credentials + .preferences + .telemetry-installation-id + db_users/ + fs_resources/ + fs_credentials/ + bin/ + logs/ + cache/ + local/ + mounts/ + drive9-home/ +``` + +The credentials TOML keys become: + +```toml +[default] +tidb_cloud_public_key = "..." +tidb_cloud_private_key = "..." +``` + +New writes use only `tidb_cloud_public_key` and +`tidb_cloud_private_key`. During the v0.2 compatibility window, readers accept +legacy `tdc_public_key` and `tdc_private_key`. If both forms exist and values +differ, loading fails instead of selecting one silently. + +The profile, filesystem registry, SQL credential, preferences, and telemetry +formats otherwise retain their existing semantics. This rename must not turn +secret files into non-secret files or weaken existing file modes. + +## State Directory Selection + +Before loading a profile, `ti` resolves the local state directory using this +table: + +| `~/.ti` | `~/.tdc` | Behavior | +| --- | --- | --- | +| absent | absent | Start with a fresh `~/.ti` state directory. | +| present | absent | Use `~/.ti` normally. | +| absent | present | Validate and perform the one-way migration. | +| present | present | Use `~/.ti` only when it contains a valid migration marker for this exact `~/.tdc`; otherwise fail and require explicit manual resolution. | + +Successful migration writes `~/.ti/.migrated-from-tdc` inside the staged tree +before atomic publication. The owner-only marker contains a schema version and +the exact legacy source path. It lets later commands distinguish the expected +post-migration state, where the preserved old directory coexists with the new +one, from two independently created state trees. An absent, malformed, unsafe, +or mismatched marker does not bypass the conflict. `ti` must never merge the +two trees, choose the newest one, or overwrite either directory. + +`ti update`, `ti update --check`, and `ti update --dry-run` are exceptions. +They preserve the updater contract and must not inspect, create, or migrate +either state directory. + +## Migration Triggers + +Migration is available through one shared Go implementation, not duplicated in +shell and PowerShell scripts: + +- The new installer invokes an internal, undocumented migration execution mode + from the verified staged executable before creating the final install + directory. +- A normal non-update `ti` command lazily invokes the same migration path before + loading configuration. +- Installer and lazy migration produce the same files, validation errors, and + rollback behavior. + +This lets an archive user run `ti` directly while still receiving the same safe +migration as an installer user. + +The migration implementation lives in `internal/config/homemigration`. CLI +wiring and installer integration remain thin callers. + +## Durable State Migration + +Only durable state is copied from `~/.tdc`: + +- `config` +- `credentials` +- `.preferences` +- `.telemetry-installation-id` +- `db_users/` +- `fs_resources/` +- `fs_credentials/` + +The following runtime, cache, and historical data is not migrated: + +- `logs/` +- `cache/` +- `local/` +- `mounts/` +- `drive9-home/` + +Migration follows these invariants: + +1. Never modify or delete `~/.tdc`. +2. Reject symbolic links and special files in the migration source. +3. Reject credential files with unsafe permissions rather than copying them. +4. Create a temporary sibling directory under `~/.ti`'s parent. +5. Copy only the allowlisted durable paths with their required permissions. +6. Parse and validate copied TOML and credential structures. +7. Rewrite legacy TiDB Cloud credential field names to canonical names. +8. Write and validate the owner-only migration source marker. +9. Verify the complete destination before atomically renaming it to `~/.ti`. +10. On any failure, remove only the temporary destination and leave `~/.tdc` + unchanged. + +There must never be a partially initialized `~/.ti` accepted as a successful +migration. + +## Mount Migration Boundary + +An active mount started by the old executable is owned by the old companion +home and process. `ti` must not adopt, terminate, drain, or unmount that process. + +Before migration, inspect the old mount routing state and reject migration when +an old mount is still active. The error tells the user to use the old binary: + +```bash +tdc fs drain-file-system --mount-path +tdc fs unmount-file-system --mount-path +``` + +After every old mount is unmounted, migration can proceed. New mounts use only +the new `~/.ti/drive9-home/` namespaces and the bundled `ti-drive9` executable. + +## Installation And PATH Behavior + +The release installer URL becomes: + +```bash +curl -fsSL https://github.com/tidbcloud/ti-cli/releases/latest/download/install.sh | sh -s -- --yes +``` + +The installer defaults to `~/.ti/bin` and tells users to prepend it to PATH: + +```bash +export PATH="$HOME/.ti/bin:$PATH" +``` + +PowerShell uses the equivalent `$HOME\.ti\bin` path. + +Installer behavior: + +- Install `ti` and `ti-drive9` together. +- Replace only artifacts previously installed by the `ti` installer in the + selected destination. +- Never replace an unrelated executable named `ti` found elsewhere on PATH. +- When another `ti` shadows the installed executable, complete installation but + report both the installed path and the path currently resolved by the shell. +- Never invoke `sudo`, create a system-directory symlink, or modify shell + profile files automatically. +- Never delete `tdc`, `tdc-drive9`, or `~/.tdc`. +- Show explicit migration status and manual old-install cleanup guidance. + +An unrelated `ti` executable is not treated as an updatable installation. +`ti update` accepts only archive/script installations carrying the expected +`ti` ownership metadata. + +## Release And Update Contract + +The first renamed release is `v0.2.0`. + +The old `tdc update` command looks at the old repository and cannot safely +replace itself with a differently named executable. It is not a supported +migration path. Users must run the new `ti` installer. + +The release contains: + +- `ti` archives for supported operating systems and architectures +- checksums and signatures required by the current release process +- `install.sh` and `install.ps1` pointing at `tidbcloud/ti-cli` + +The installers and updater continue to download the companion from Drive9's +release endpoint, verify its separate checksum manifest, and install it beside +`ti` as `ti-drive9`. The primary GitHub release archive does not duplicate the +external companion artifact. + +`ti update`, `ti update --check`, and `ti update --target-version` use releases +from `github.com/tidbcloud/ti-cli`. Update output, install-source metadata, and +artifact ownership markers use the new name. + +The release and deployment order is: + +1. Deploy telemetry backend compatibility for both old and new event names and + installation ID prefixes. +2. Merge repository, module, executable, installer, workflow, and test renames. +3. Publish matching product documentation. +4. Release `v0.2.0`. +5. Test installation and state migration from a real `v0.1.7` installation. +6. Announce that the `tdc` release line is no longer updated. + +## Package-manager Naming + +Future package-manager distribution uses a package name distinct from the +short executable name: + +- Homebrew formula: `ti-cli` +- Homebrew commands: `brew install tidbcloud/tap/ti-cli` and + `brew upgrade tidbcloud/tap/ti-cli` +- Scoop package: `ti-cli` +- Winget package ID, if added later: `TiDBCloud.TiCLI` + +The installed executable remains `ti`. This follows the same package-versus- +executable distinction as packages such as `aws-cli` installing `aws`. + +## Telemetry Compatibility + +New CLI command telemetry uses: + +- event name `ti.command.finished` +- newly generated installation IDs prefixed with `ti_` + +When migration copies an existing `.telemetry-installation-id`, preserve its +value exactly, including a legacy `tdc_` prefix. Generating a new ID would split +one installation into two identities and invalidate upgrade analysis. + +The telemetry backend must accept during the migration window: + +- `tdc.command.finished` +- `ti.command.finished` +- legacy `tdc_` installation IDs +- new `ti_` installation IDs + +The telemetry public domain, TiDB database, Docker Compose service names, and +server-side deployment directory do not need a coordinated rename. They are +operational internals and retaining them reduces deployment risk. Dashboards +must group old and new event names when measuring usage across the v0.2.0 +boundary. + +## GitHub Actions Migration + +Repository Actions configuration changes to the new repository and canonical +variables. + +Canonical live-test configuration: + +- Actions variable `TI_REGION_CODE` +- Actions secret `TIDB_CLOUD_PUBLIC_KEY` +- Actions secret `TIDB_CLOUD_PRIVATE_KEY` + +Use the existing local legacy `live-e2e` profile as the source when migrating +those repository settings. Update secrets with `gh secret set` through stdin +and never print values to terminal output or logs. Keep old repository values +until a workflow using the renamed values passes, then remove them. + +Workflow names, artifact paths, executable paths, cache keys owned by this +project, release repository references, test binary variables, and Makefile +targets must use `ti`. Generic external variables and provider-owned variables +keep their established names. + +## Documentation Migration + +Current product documentation changes as one coordinated update: + +- Rename commands and examples from `tdc` to `ti`. +- Rename active configuration paths from `~/.tdc` to `~/.ti`. +- Rename current environment variables to canonical `TI_*` and + `TIDB_CLOUD_*` names. +- Point GitHub links, installer commands, issue links, badges, and source links + at `github.com/tidbcloud/ti-cli`. +- Rename PingCAP documentation directory `ai/tdc/` to `ai/ti/`. +- Rename current PingCAP documentation filenames prefixed with `tdc-` to + `ti-`, and update TOC and internal links. +- Update README, AGENTS, `docs/priciples.md`, active specs, examples, installer + output, and generated command reference. +- Explain in release and migration documentation that `tdc` is the name of the + pre-v0.2 CLI and `ti` is the new executable. + +Do not mechanically rewrite historical completed spec prose. Preserve it as a +record of the behavior that was implemented under the old name. Update only +forward references that must resolve to renamed active files. AGENTS must state +that completed specs mentioning `tdc` refer to the pre-v0.2 product name. + +The WIP branch `cheese/wip-remote-fs-wait-backend` remains untouched while this +rename is implemented on `main`. After the rename lands, merge the renamed main +branch into that WIP branch and resolve command, path, environment variable, and +spec-number conflicts there. + +## After This Spec + +New users install and invoke the renamed product directly: + +```bash +curl -fsSL https://github.com/tidbcloud/ti-cli/releases/latest/download/install.sh | sh -s -- --yes +export PATH="$HOME/.ti/bin:$PATH" +ti configure +ti db list-db-clusters --db-cluster-type starter +``` + +Existing users run the same installer after draining old mounts. The installer +copies eligible durable state into `~/.ti`, and the user verifies `ti` before +manually deleting the old executable or `~/.tdc` directory. + +Automation migrates to canonical environment variable names immediately. A +v0.2.x compatibility window lets existing jobs keep legacy variable names while +their owners update them without changing command output. + +## Implementation Design + +Keep migration and compatibility decisions at existing ownership boundaries: + +- `internal/config/homemigration` validates and atomically copies durable old + state. +- `internal/config` resolves canonical and legacy environment variables and + rejects conflicting values. +- `internal/config/store` continues to own TOML parsing, file modes, and atomic + file writes. +- `internal/settings` reads only the selected home and keeps legacy preference + migrations within that home. +- `internal/telemetry` generates new identity prefixes and preserves migrated + identities. +- `internal/update` recognizes only `ti` release ownership and never triggers + home migration. +- `internal/cli` owns renamed commands, flags, help, and errors; business + packages must not special-case the executable name. +- installer scripts call the Go migration implementation instead of copying + configuration themselves. + +Do not introduce cgo. The migration uses standard Go filesystem APIs and must +work on macOS, Linux, and Windows. Platform-specific permission checks may be +implemented behind small build-tagged helpers where Windows semantics differ. + +## Dependencies And Platform + +- Use Go standard-library filesystem and process APIs for home migration. +- Continue using the repository's existing TOML parser, Cobra command tree, + output renderer, operation logger, and installer ownership metadata. +- Do not add a migration framework or a shell-only state copier. +- Do not add cgo or a platform-specific runtime dependency. +- macOS, Linux, and Windows installers must implement the same state-selection + and conflict contract, with permission checks adapted to each platform. +- The installed filesystem companion remains required for filesystem commands and + is installed and updated with the primary executable. + +## Dependencies + +- `0001-cli-foundation.md` for command, help, and binary behavior. +- `0002-local-config-and-credentials.md` for profile and credential ownership. +- `0012-install-and-update-distribution.md` for archive ownership and updater + safety. +- `0013-github-actions-ci-cd.md` for release and live e2e workflows. +- `0015-drive9-companion-wrapper-for-tdc-fs.md` for companion installation and + mount ownership. +- `0021-global-settings.md` and `0022-telemetry.md` for durable global state and + telemetry identity. + +## Call Chains + +Normal command with existing new state: + +1. Resolve the user home directory. +2. Observe `~/.ti` and no `~/.tdc` migration requirement. +3. Load canonical environment variables and `~/.ti` profile state. +4. Execute the existing command-specific API chain. + +Normal command requiring migration: + +1. Resolve the user home directory. +2. Observe `~/.tdc` and no `~/.ti`. +3. Validate source file types, permissions, and inactive mount state. +4. Copy and rewrite allowlisted durable state into a temporary directory. +5. Validate the temporary destination. +6. Atomically rename it to `~/.ti`. +7. Load the migrated profile and continue command execution. + +Installer migration: + +1. Download and verify `ti` from the new GitHub release and `ti-drive9` from + the Drive9 release endpoint. +2. Extract `ti` into a temporary staging directory. +3. Invoke the shared migration execution mode from the staged executable. +4. Install both artifacts into the selected user-owned directory. +5. Print installation path, migration result, PATH guidance, and next steps. + +This spec changes local product behavior and release endpoints. It introduces +no new TiDB Cloud control-plane or data-plane API calls. + +## Output And Errors + +Stable migration errors include: + +- `config.home_migration_conflict`: both state directories exist without a + valid marker proving that `~/.ti` was atomically migrated from this exact + `~/.tdc` path. +- `config.home_migration_active_mount`: an old mount must be drained and + unmounted with the old executable. +- `config.home_migration_unsafe_source`: a source file is a symlink, special + file, or has unsafe secret permissions. +- `config.home_migration_failed`: copying, rewriting, validation, or atomic + publication failed. +- `config.environment_conflict`: canonical and legacy environment variables or + credential fields contain different values. + +Errors must name the affected paths or variable names but never include API +keys, filesystem tokens, SQL passwords, vault tokens, telemetry extra values, +or complete credential file contents. + +## Tests + +Unit tests cover: + +- every state-directory selection row +- complete migration of the durable allowlist +- exclusion of logs, caches, local runtime data, mounts, and companion home +- old source preservation after success and failure +- atomic cleanup after injected copy, parse, permission, and rename failures +- symlink and special-file rejection +- credential permission validation +- active old mount rejection +- canonical-only, legacy-only, equal dual, and conflicting dual environment + variable resolution +- canonical and legacy credential field resolution +- update commands bypassing migration +- migrated telemetry identity preservation and new identity generation +- renamed help, errors, executable paths, and release metadata + +Black-box e2e tests use temporary HOME directories and cover: + +- a fresh `ti` installation +- installer migration from a representative `v0.1.7` home +- lazy migration when running an archive directly +- rejection when both homes exist +- continued command execution after successful migration +- structured JSON output remaining unpolluted when a legacy variable is used +- installer PATH-shadow reporting without replacing the shadowing executable + +Release verification builds and tests every supported archive, confirms the +archive contains `ti`, confirms installers/updaters plan and verify `ti-drive9`, +and performs a real migration rehearsal +from an installed `v0.1.7` release before `v0.2.0` is announced. + +## Acceptance Criteria + +- `go.mod`, repository-owned imports, binaries, commands, help, errors, + installers, archives, workflows, and current documentation use `ti` and + `github.com/tidbcloud/ti-cli`. +- No `tdc` executable alias, wrapper, hidden command, symlink, or release bridge + is shipped. +- A user with only a valid `~/.tdc` gets a complete, atomic, one-way migration + to `~/.ti` without changes to the old directory. +- A user with two independently created state directories receives a + deterministic error and no mutation; a valid completed-migration marker + permits the expected preserved old directory to coexist with `~/.ti`. +- Active old mounts block migration with actionable old-command instructions. +- New state writes use canonical credential fields and environment variable + names. +- Legacy environment variables and credential fields work through v0.2.x, + conflict deterministically, and produce no output warnings. +- `ti update` uses the new repository and never reads or migrates local product + state. +- New telemetry uses the new event and identity prefix, while migrated identity + remains stable and the backend accepts both generations. +- Homebrew and Scoop planning uses package name `ti-cli` and executable `ti`. +- README, AGENTS, principles, active specs, PingCAP docs, and command reference + agree with the implemented command and path contracts. + +## Out Of Scope + +- Removing old user files or old binaries automatically. +- Supporting `tdc` as an alias after `v0.2.0`. +- Merging independently modified `~/.tdc` and `~/.ti` trees. +- Adopting or terminating mounts started by the old companion. +- Renaming the telemetry database, production host, Docker Compose services, or + server deployment directory solely for cosmetic consistency. +- Implementing Homebrew or Scoop distribution in this spec. +- Renaming or rebasing the remote-filesystem WIP branch as part of the main + rename change. diff --git a/docs/telemetry-backend-design.md b/docs/telemetry-backend-design.md index 9a4819c..2571528 100644 --- a/docs/telemetry-backend-design.md +++ b/docs/telemetry-backend-design.md @@ -2,17 +2,17 @@ ## Purpose -The tdc telemetry backend is a small product-owned HTTPS ingestion service between the tdc CLI, TiDB, and PostHog. +The ti telemetry backend is a small product-owned HTTPS ingestion service between the ti CLI, TiDB, and PostHog. ```text -tdc CLI -> telemetry backend -> in-memory batcher +ti CLI -> telemetry backend -> in-memory batcher |-> TiDB |-> PostHog /batch/ ``` The backend exists so the CLI never sends directly to PostHog, never embeds a PostHog project token, and never relies on PostHog as the only telemetry data store. The backend enforces the privacy schema, rate limits abuse, rejects unknown fields, batches valid events in memory, then best-effort writes the same sanitized batch to TiDB and PostHog. -TiDB is the tdc-owned telemetry store and future migration/analysis base. PostHog is an analytics destination. TiDB is not an outbox queue in MVP, and the backend must not consume events from TiDB to forward them to PostHog. +TiDB is the ti-owned telemetry store and future migration/analysis base. PostHog is an analytics destination. TiDB is not an outbox queue in MVP, and the backend must not consume events from TiDB to forward them to PostHog. ## Non-goals @@ -83,7 +83,7 @@ Prometheus text-format process counters for accepted, rejected, rate-limited, bu ### `POST /v1/telemetry/batch` -Accepts one small request batch of sanitized tdc CLI telemetry events, validates it, enqueues valid events into the bounded in-memory batcher, and returns immediately. The response means the backend accepted the events into memory; it does not mean TiDB and PostHog have already flushed the batch. +Accepts one small request batch of sanitized ti CLI telemetry events, validates it, enqueues valid events into the bounded in-memory batcher, and returns immediately. The response means the backend accepted the events into memory; it does not mean TiDB and PostHog have already flushed the batch. The only success status for this endpoint is `202 Accepted`. Do not return `200 OK` for an accepted batch because the asynchronous TiDB and PostHog sink writes are not complete. @@ -91,7 +91,7 @@ Required request headers: ```http Content-Type: application/json -User-Agent: tdc/ +User-Agent: ti/ ``` Request limits: @@ -113,17 +113,17 @@ Request body: "events": [ { "event_id": "018f7e67-8fe4-7cc2-9ca5-2d3536c7fb44", - "event_name": "tdc.command.finished", + "event_name": "ti.command.finished", "occurred_at": "2026-07-08T12:00:00Z", - "anonymous_installation_id": "tdc_01j0a0n8m9f4q2x6cn0b9q3k3z", - "command_path": "tdc fs create-file-system", + "anonymous_installation_id": "ti_01j0a0n8m9f4q2x6cn0b9q3k3z", + "command_path": "ti fs create-file-system", "flag_names": ["file-system-name", "output"], "exit_code": 0, "error_code": "", "duration_ms": 182, "cloud_provider": "aws", "region_code": "aws-us-east-1", - "cli_version": "0.1.0", + "cli_version": "0.2.0", "os": "darwin", "arch": "arm64", "install_source": "github-release", @@ -185,16 +185,16 @@ Allowed event fields: | Field | Type | Required | Validation | | --- | --- | --- | --- | | `event_id` | string | yes | UUID/ULID-like opaque ID, max 64 bytes. | -| `event_name` | string | yes | MVP allows only `tdc.command.finished`. | +| `event_name` | string | yes | New clients send `ti.command.finished`; the backend also accepts legacy `tdc.command.finished` events during the v0.2 migration window. | | `occurred_at` | RFC3339 string | yes | Command completion time. | -| `anonymous_installation_id` | string | yes | Random local ID, regex `^tdc_[a-zA-Z0-9_-]{16,96}$`. | -| `command_path` | string | yes | `tdc` plus at most two lowercase kebab-case command levels; max 128 bytes. | +| `anonymous_installation_id` | string | yes | New random IDs use the `ti_` prefix; migrated v0.1 identities retain `tdc_`. The backend accepts `^(ti|tdc)_[a-zA-Z0-9_-]{16,96}$`. | +| `command_path` | string | yes | `ti` plus at most two lowercase kebab-case command levels; the backend also accepts legacy `tdc` paths during the v0.2 migration window; max 128 bytes. | | `flag_names` | string array | yes | Each entry regex `^[a-z][a-z0-9-]{0,63}$`. | | `exit_code` | integer | yes | 0 to 255. | | `error_code` | string | no | Stable code only, max 64 bytes; empty string allowed. | | `duration_ms` | integer | yes | 0 to 86,400,000. | | `cloud_provider` | string | no | `aws`, `alibaba_cloud`, `unknown`, or empty. | -| `region_code` | string | no | Known tdc region code, `unknown`, or empty. | +| `region_code` | string | no | Known ti region code, `unknown`, or empty. | | `cli_version` | string | yes | Version-safe characters only, max 64 bytes. | | `os` | string | yes | Known Go `runtime.GOOS` value. | | `arch` | string | yes | Known Go `runtime.GOARCH` value. | @@ -220,7 +220,7 @@ The batcher may do a small in-memory retry for sink failures, but it must not wr ## TiDB Storage -TiDB stores sanitized telemetry events as tdc-owned telemetry data. It is not a queue for PostHog forwarding in MVP. +TiDB stores sanitized telemetry events as ti-owned telemetry data. It is not a queue for PostHog forwarding in MVP. Recommended schema: @@ -255,7 +255,7 @@ CREATE TABLE IF NOT EXISTS telemetry_events ( ); ``` -Schema changes are managed only by embedded [Goose](https://github.com/pressly/goose) SQL migrations under `internal/telemetrybackend/migrations/sql/`. The one-shot `tdc-telemetry-migrate` process owns the `tdc_telemetry_schema_migrations` history table and runs before the API starts. The API process never runs DDL. Migration `00001` creates the historical v1 table with `CREATE TABLE IF NOT EXISTS`; migration `00002` only adds `tag`, `extra_json`, and `idx_tag_received` with TiDB's `IF NOT EXISTS` syntax. These migrations are additive and contain no `DROP`, `TRUNCATE`, `DELETE`, rename, or data rewrite operation. Existing telemetry rows remain intact. `extra_json` is intentionally not indexed. +Schema changes are managed only by embedded [Goose](https://github.com/pressly/goose) SQL migrations under `internal/telemetrybackend/migrations/sql/`. The one-shot `ti-telemetry-migrate` process owns the `tdc_telemetry_schema_migrations` history table and runs before the API starts. The API process never runs DDL. Migration `00001` creates the historical v1 table with `CREATE TABLE IF NOT EXISTS`; migration `00002` only adds `tag`, `extra_json`, and `idx_tag_received` with TiDB's `IF NOT EXISTS` syntax. These migrations are additive and contain no `DROP`, `TRUNCATE`, `DELETE`, rename, or data rewrite operation. Existing telemetry rows remain intact. `extra_json` is intentionally not indexed. TiDB write behavior: @@ -295,14 +295,14 @@ PostHog request body: "historical_migration": false, "batch": [ { - "event": "tdc.command.finished", + "event": "ti.command.finished", "timestamp": "2026-07-08T12:00:00Z", "properties": { - "distinct_id": "tdc_01j0a0n8m9f4q2x6cn0b9q3k3z", + "distinct_id": "ti_01j0a0n8m9f4q2x6cn0b9q3k3z", "$process_person_profile": false, "schema_version": 2, "event_id": "018f7e67-8fe4-7cc2-9ca5-2d3536c7fb44", - "command_path": "tdc fs create-file-system", + "command_path": "ti fs create-file-system", "flag_names": ["file-system-name", "output"], "exit_code": 0, "error_code": "", @@ -314,7 +314,7 @@ PostHog request body: "arch": "arm64", "install_source": "github-release", "profile_source": "default", - "tdc_environment": "production", + "ti_environment": "production", "tag": "e2b-preview", "extra": {"campaign":"launch","runtime":"e2b"} } @@ -389,8 +389,8 @@ Exported backend metrics: Implemented production layout: ```text -cmd/tdc-telemetry-backend/ -cmd/tdc-telemetry-migrate/ +cmd/ti-telemetry-backend/ +cmd/ti-telemetry-migrate/ internal/telemetrybackend/ deploy/telemetry/ .env @@ -509,17 +509,17 @@ curl -fsS https://telemetry.example.com/healthz curl -fsS https://telemetry.example.com/readyz curl -fsS -X POST https://telemetry.example.com/v1/telemetry/batch \ -H 'Content-Type: application/json' \ - -H 'User-Agent: tdc/0.2.0' \ + -H 'User-Agent: ti/0.2.0' \ --data '{ "schema_version": 2, "sent_at": "2026-07-24T12:00:00Z", "events": [ { "event_id": "018f7e67-8fe4-7cc2-9ca5-2d3536c7fb44", - "event_name": "tdc.command.finished", + "event_name": "ti.command.finished", "occurred_at": "2026-07-24T12:00:00Z", - "anonymous_installation_id": "tdc_01j0a0n8m9f4q2x6cn0b9q3k3z", - "command_path": "tdc db list-db-clusters", + "anonymous_installation_id": "ti_01j0a0n8m9f4q2x6cn0b9q3k3z", + "command_path": "ti db list-db-clusters", "flag_names": [], "exit_code": 0, "error_code": "", @@ -548,7 +548,7 @@ Expected response: } ``` -Then verify the event appears in TiDB after the next flush and appears in PostHog as `tdc.command.finished` without creating a person profile. +Then verify the event appears in TiDB after the next flush and appears in PostHog as `ti.command.finished` without creating a person profile. ## When To Add MQ Or Durable Queues diff --git a/e2e/cli_test.go b/e2e/cli_test.go index e369c9f..5788a52 100644 --- a/e2e/cli_test.go +++ b/e2e/cli_test.go @@ -16,40 +16,40 @@ import ( "testing" "time" - "github.com/tidbcloud/tdc/internal/fs/fscred" + "github.com/tidbcloud/ti-cli/internal/fs/fscred" ) func TestHelpAndVersion(t *testing.T) { - bin := tdcBinary(t) + bin := tiBinary(t) - missingCommand := runTDC(t, bin) + missingCommand := runTI(t, bin) missingCommand.wantExitCode(2) if missingCommand.stdout != "" { missingCommand.fail("stdout should be empty") } - missingCommand.wantStderrContains("tdc [ERROR]: the following arguments are required: command") + missingCommand.wantStderrContains("ti [ERROR]: the following arguments are required: command") missingCommand.wantStderrContains("The TiDB Cloud Command Line Interface is a unified tool") - missingCommand.wantStderrContains("usage: tdc [] [parameters]") - missingCommand.wantStderrContains("tdc help") + missingCommand.wantStderrContains("usage: ti [] [parameters]") + missingCommand.wantStderrContains("ti help") missingCommand.wantStderrNotContains("Commands:") - if !strings.HasPrefix(missingCommand.stderr, "\ntdc [ERROR]:") { + if !strings.HasPrefix(missingCommand.stderr, "\nti [ERROR]:") { missingCommand.fail("stderr should start with a blank line followed by the error prefix") } - root := runTDC(t, bin, "help") + root := runTI(t, bin, "help") root.wantExitCode(0) root.wantStdoutContains("Commands:") root.wantStdoutContains("db") root.wantStdoutNotContains("-h,") root.wantStdoutContains("--region ") - db := runTDC(t, bin, "db", "help") + db := runTI(t, bin, "db", "help") db.wantExitCode(0) db.wantStdoutContains("create-db-cluster") db.wantStdoutContains("create-db-sql-users") db.wantStdoutContains("format-db-connection-string") - subcommand := runTDC(t, bin, "fs", "mount-file-system", "help") + subcommand := runTI(t, bin, "fs", "mount-file-system", "help") subcommand.wantExitCode(0) subcommand.wantStdoutContains("Mount a file system to a local path.") subcommand.wantStdoutContains("--mount-path") @@ -58,7 +58,7 @@ func TestHelpAndVersion(t *testing.T) { subcommand.wantStdoutContains("--local-root") subcommand.wantStdoutContains("--pack-path") - copyFile := runTDC(t, bin, "fs", "copy-file", "help") + copyFile := runTI(t, bin, "fs", "copy-file", "help") copyFile.wantExitCode(0) copyFile.wantStdoutContains("--from-local") copyFile.wantStdoutContains("--to-remote") @@ -66,16 +66,16 @@ func TestHelpAndVersion(t *testing.T) { copyFile.wantStdoutContains("--to-stdout") copyFile.wantStdoutContains("--description") - chmodFile := runTDC(t, bin, "fs", "chmod-file", "help") + chmodFile := runTI(t, bin, "fs", "chmod-file", "help") chmodFile.wantExitCode(0) chmodFile.wantStdoutContains("--mode") - deleteFileSystem := runTDC(t, bin, "fs", "delete-file-system", "help") + deleteFileSystem := runTI(t, bin, "fs", "delete-file-system", "help") deleteFileSystem.wantExitCode(0) deleteFileSystem.wantStdoutContains("--file-system-name") deleteFileSystem.wantStdoutNotContains("--confirm-file-system-name") - createDBCluster := runTDC(t, bin, "db", "create-db-cluster", "help") + createDBCluster := runTI(t, bin, "db", "create-db-cluster", "help") createDBCluster.wantExitCode(0) createDBCluster.wantStdoutContains("--db-cluster-name (required)") createDBCluster.wantStdoutContains("[--db-cluster-type ]") @@ -83,29 +83,29 @@ func TestHelpAndVersion(t *testing.T) { createDBCluster.wantStdoutContains("--project-id ") createDBCluster.wantStdoutNotContains("--project-id (required)") - configure := runTDC(t, bin, "configure", "help") + configure := runTI(t, bin, "configure", "help") configure.wantExitCode(0) configure.wantStdoutContains("--region-code ") - packFileSystem := runTDC(t, bin, "fs", "pack-file-system", "help") + packFileSystem := runTI(t, bin, "fs", "pack-file-system", "help") packFileSystem.wantExitCode(0) packFileSystem.wantStdoutContains("--archive-path") packFileSystem.wantStdoutContains("--mount-profile") packFileSystem.wantStdoutContains("--path") - gitClone := runTDC(t, bin, "fs-git", "clone-git-workspace", "help") + gitClone := runTI(t, bin, "fs-git", "clone-git-workspace", "help") gitClone.wantExitCode(0) gitClone.wantStdoutContains("--repo-url") gitClone.wantStdoutContains("--target-path") gitClone.wantStdoutContains("--hydrate") - version := runTDC(t, bin, "fs", "mount-file-system", "--version") + version := runTI(t, bin, "fs", "mount-file-system", "--version") version.wantExitCode(0) - version.wantStdoutContains("tdc ") + version.wantStdoutContains("ti ") } func TestFSUnixAliasHelp(t *testing.T) { - bin := tdcBinary(t) + bin := tiBinary(t) tests := []struct { alias string @@ -131,9 +131,9 @@ func TestFSUnixAliasHelp(t *testing.T) { for _, tt := range tests { t.Run(tt.alias, func(t *testing.T) { - result := runTDC(t, bin, "fs", tt.alias, "help") + result := runTI(t, bin, "fs", tt.alias, "help") result.wantExitCode(0) - result.wantStdoutContains("tdc fs " + tt.canonical) + result.wantStdoutContains("ti fs " + tt.canonical) result.wantStdoutContains("Aliases:") result.wantStdoutContains(" " + tt.alias) result.wantStdoutContains(tt.flag) @@ -142,17 +142,17 @@ func TestFSUnixAliasHelp(t *testing.T) { } func TestErrorsAreRenderedAtCLIBoundary(t *testing.T) { - bin := tdcBinary(t) + bin := tiBinary(t) - shortFlag := runTDC(t, bin, "-h") + shortFlag := runTI(t, bin, "-h") shortFlag.wantExitCode(2) - shortFlag.wantStderrContains("tdc [ERROR]: short flags are not supported") + shortFlag.wantStderrContains("ti [ERROR]: short flags are not supported") - unknown := runTDC(t, bin, "db", "missing-command") + unknown := runTI(t, bin, "db", "missing-command") unknown.wantExitCode(2) - unknown.wantStderrContains(`tdc [ERROR]: unknown command "missing-command" for "tdc db"`) + unknown.wantStderrContains(`ti [ERROR]: unknown command "missing-command" for "ti db"`) - removedConfirmation := runTDC(t, bin, "fs", "delete-file-system", "--file-system-name", "workspace", "--confirm-file-system-name", "workspace") + removedConfirmation := runTI(t, bin, "fs", "delete-file-system", "--file-system-name", "workspace", "--confirm-file-system-name", "workspace") removedConfirmation.wantExitCode(2) removedConfirmation.wantStderrContains(`unknown flag: --confirm-file-system-name`) @@ -164,37 +164,120 @@ func TestErrorsAreRenderedAtCLIBoundary(t *testing.T) { artifact := artifactNameForRuntime(t) fmt.Fprintf(w, `{ "tag_name": "v99.0.0", - "html_url": "https://github.com/tidbcloud/tdc/releases/tag/v99.0.0", + "html_url": "https://github.com/tidbcloud/ti-cli/releases/tag/v99.0.0", "assets": [ { "name": %q, - "browser_download_url": "https://github.com/tidbcloud/tdc/releases/download/v99.0.0/%s" + "browser_download_url": "https://github.com/tidbcloud/ti-cli/releases/download/v99.0.0/%s" } ] }`, artifact, artifact) })) defer releaseServer.Close() - checkUpdate := runTDCWithInput(t, bin, "", []string{"TDC_RELEASE_API_BASE_URL=" + releaseServer.URL}, "update", "--check", "--query", "latest_version") + checkUpdate := runTIWithInput(t, bin, "", []string{"TI_RELEASE_API_BASE_URL=" + releaseServer.URL}, "update", "--check", "--query", "latest_version") checkUpdate.wantExitCode(0) checkUpdate.wantStdoutContains(`"99.0.0"`) - update := runTDC(t, bin, "update", "--dry-run") + update := runTI(t, bin, "update", "--dry-run") update.wantExitCode(1) - update.wantStderrContains("tdc [ERROR]: tdc install source") + update.wantStderrContains("ti [ERROR]: ti install source") - directUpdate := runTDC(t, bin, "update") + directUpdate := runTI(t, bin, "update") directUpdate.wantExitCode(1) - directUpdate.wantStderrContains("tdc [ERROR]: tdc install source") + directUpdate.wantStderrContains("ti [ERROR]: ti install source") directUpdate.wantStderrNotContains("requires --yes") - invalidQuery := runTDCWithInput(t, bin, "", tdcConfigEnv(), append(createClusterDryRunArgs(), "--query", "command[")...) + invalidQuery := runTIWithInput(t, bin, "", tiConfigEnv(), append(createClusterDryRunArgs(), "--query", "command[")...) invalidQuery.wantExitCode(2) - invalidQuery.wantStderrContains("tdc [ERROR]: invalid --query expression") + invalidQuery.wantStderrContains("ti [ERROR]: invalid --query expression") +} + +func TestHomeMigrationThroughRealBinary(t *testing.T) { + bin := tiBinary(t) + + t.Run("lazy migration", func(t *testing.T) { + home := t.TempDir() + legacy := filepath.Join(home, ".tdc") + writeE2EFile(t, filepath.Join(legacy, "config"), "[default]\nregion_code = 'aws-us-east-1'\n", 0o644) + writeE2EFile(t, filepath.Join(legacy, "credentials"), "[default]\ntdc_public_key = 'public'\ntdc_private_key = 'private'\n", 0o600) + writeE2EFile(t, filepath.Join(legacy, "logs", "tdc.jsonl"), "not migrated\n", 0o600) + + result := runTIWithInput(t, bin, "", []string{"HOME=" + home}, "help") + result.wantExitCode(0) + result.wantStdoutContains("The TiDB Cloud Command Line Interface") + + credentials, err := os.ReadFile(filepath.Join(home, ".ti", "credentials")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(credentials), "tidb_cloud_public_key") || strings.Contains(string(credentials), "tdc_public_key") { + t.Fatalf("migrated credentials are not canonical:\n%s", credentials) + } + if _, err := os.Stat(filepath.Join(home, ".tdc", "credentials")); err != nil { + t.Fatalf("legacy credentials changed: %v", err) + } + if _, err := os.Stat(filepath.Join(home, ".ti", "logs")); !os.IsNotExist(err) { + t.Fatalf("runtime logs were migrated: %v", err) + } + }) + + t.Run("both homes conflict", func(t *testing.T) { + home := t.TempDir() + if err := os.Mkdir(filepath.Join(home, ".tdc"), 0o700); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(filepath.Join(home, ".ti"), 0o700); err != nil { + t.Fatal(err) + } + result := runTIWithInput(t, bin, "", []string{"HOME=" + home}, "help") + result.wantExitCode(2) + result.wantStderrContains("both " + filepath.Join(home, ".tdc") + " and " + filepath.Join(home, ".ti") + " exist") + }) + + t.Run("update bypasses migration", func(t *testing.T) { + home := t.TempDir() + for _, name := range []string{".tdc", ".ti"} { + if err := os.Mkdir(filepath.Join(home, name), 0o700); err != nil { + t.Fatal(err) + } + } + result := runTIWithInput(t, bin, "", []string{"HOME=" + home}, "update", "help") + result.wantExitCode(0) + result.wantStdoutContains("Update this tool.") + }) +} + +func TestLegacyEnvironmentCompatibilityDoesNotPolluteOutput(t *testing.T) { + bin := tiBinary(t) + home := t.TempDir() + result := runTIWithInput(t, bin, "", []string{ + "HOME=" + home, + "TDC_PROFILE=legacy", + "TDC_REGION_CODE=aws-us-east-1", + }, "help") + result.wantExitCode(0) + result.wantStderrNotContains("deprecated") + result.wantStdoutNotContains("deprecated") + if _, err := os.Stat(filepath.Join(home, ".ti")); !os.IsNotExist(err) { + t.Fatalf("help in a fresh home unexpectedly created state: %v", err) + } + + conflictHome := t.TempDir() + conflict := runTIWithInput(t, bin, "", []string{ + "HOME=" + conflictHome, + "TI_REGION_CODE=aws-us-east-1", + "TDC_REGION_CODE=aws-us-west-2", + }, "help") + conflict.wantExitCode(2) + conflict.wantStderrContains("environment variables TI_REGION_CODE and TDC_REGION_CODE contain different values") + if _, err := os.Stat(filepath.Join(conflictHome, ".ti")); !os.IsNotExist(err) { + t.Fatalf("environment conflict mutated state: %v", err) + } } func TestTelemetryUsesFakeIngestionServer(t *testing.T) { - bin := tdcBinary(t) + bin := tiBinary(t) home := t.TempDir() requests := make(chan []byte, 1) server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { @@ -208,17 +291,17 @@ func TestTelemetryUsesFakeIngestionServer(t *testing.T) { defer server.Close() secretName := "must-not-appear-in-telemetry" - result := runTDCWithInput(t, bin, "", []string{ + result := runTIWithInput(t, bin, "", []string{ "HOME=" + home, - "TDC_ALLOW_TEST_ENDPOINTS=1", - "TDC_TEST_TELEMETRY_ENDPOINT=" + server.URL + "/v1/telemetry/batch", - "TDC_TELEMETRY=on", - "TDC_TELEMETRY_TAG=e2b-preview", - `TDC_TELEMETRY_EXTRA={"campaign":"launch","runtime":"e2b"}`, - "TDC_LOGGING=off", - "TDC_REGION_CODE=aws-us-east-1", - "TDC_PUBLIC_KEY=must-not-appear-public-key", - "TDC_PRIVATE_KEY=must-not-appear-private-key", + "TI_ALLOW_TEST_ENDPOINTS=1", + "TI_TEST_TELEMETRY_ENDPOINT=" + server.URL + "/v1/telemetry/batch", + "TI_TELEMETRY=on", + "TI_TELEMETRY_TAG=e2b-preview", + `TI_TELEMETRY_EXTRA={"campaign":"launch","runtime":"e2b"}`, + "TI_LOGGING=off", + "TI_REGION_CODE=aws-us-east-1", + "TIDB_CLOUD_PUBLIC_KEY=must-not-appear-public-key", + "TIDB_CLOUD_PRIVATE_KEY=must-not-appear-private-key", }, "db", "create-db-cluster", "--db-cluster-name", secretName, "--project-id", "must-not-appear-project", "--dry-run") result.wantExitCode(0) result.wantStdoutContains(`"dry_run": true`) @@ -227,7 +310,7 @@ func TestTelemetryUsesFakeIngestionServer(t *testing.T) { select { case body = <-requests: case <-time.After(2 * time.Second): - t.Fatal("tdc did not send telemetry to the fake ingestion server") + t.Fatal("ti did not send telemetry to the fake ingestion server") } for _, prohibited := range []string{secretName, "must-not-appear-public-key", "must-not-appear-private-key", "must-not-appear-project"} { if strings.Contains(string(body), prohibited) { @@ -235,19 +318,19 @@ func TestTelemetryUsesFakeIngestionServer(t *testing.T) { } } if !strings.Contains(string(body), `"schema_version":2`) || - !strings.Contains(string(body), `"command_path":"tdc db create-db-cluster"`) || + !strings.Contains(string(body), `"command_path":"ti db create-db-cluster"`) || !strings.Contains(string(body), `"db-cluster-name"`) || !strings.Contains(string(body), `"project-id"`) || !strings.Contains(string(body), `"tag":"e2b-preview"`) || !strings.Contains(string(body), `"extra":{"campaign":"launch","runtime":"e2b"}`) { t.Fatalf("unexpected telemetry payload: %s", body) } - idPath := filepath.Join(home, ".tdc", ".telemetry-installation-id") + idPath := filepath.Join(home, ".ti", ".telemetry-installation-id") id, err := os.ReadFile(idPath) if err != nil { t.Fatalf("read telemetry installation ID: %v", err) } - if !strings.HasPrefix(strings.TrimSpace(string(id)), "tdc_") { + if !strings.HasPrefix(strings.TrimSpace(string(id)), "ti_") { t.Fatalf("unexpected telemetry installation ID %q", id) } if runtime.GOOS != "windows" { @@ -262,37 +345,37 @@ func TestTelemetryUsesFakeIngestionServer(t *testing.T) { } func TestOutputQueryAndDryRun(t *testing.T) { - bin := tdcBinary(t) - env := tdcConfigEnv() + bin := tiBinary(t) + env := tiConfigEnv() - dryRun := runTDCWithInput(t, bin, "", env, createClusterDryRunArgs()...) + dryRun := runTIWithInput(t, bin, "", env, createClusterDryRunArgs()...) dryRun.wantExitCode(0) dryRun.wantStdoutContains(`"dry_run": true`) dryRun.wantStdoutContains(`"would_send_request": true`) dryRun.wantStdoutContains(`"post_create_wait"`) - regionOverride := runTDCWithInput(t, bin, "", []string{ - "TDC_PUBLIC_KEY=e2e-public", - "TDC_PRIVATE_KEY=e2e-private", + regionOverride := runTIWithInput(t, bin, "", []string{ + "TIDB_CLOUD_PUBLIC_KEY=e2e-public", + "TIDB_CLOUD_PRIVATE_KEY=e2e-private", }, append([]string{"--region", "aws-ap-southeast-1"}, createClusterDryRunArgs()...)...) regionOverride.wantExitCode(0) regionOverride.wantStdoutContains("aws ap-southeast-1") - text := runTDCWithInput(t, bin, "", env, append(createClusterDryRunArgs(), "--output", "text")...) + text := runTIWithInput(t, bin, "", env, append(createClusterDryRunArgs(), "--output", "text")...) text.wantExitCode(0) - text.wantStdoutContains("Dry run: tdc db create-db-cluster") + text.wantStdoutContains("Dry run: ti db create-db-cluster") - query := runTDCWithInput(t, bin, "", env, append(createClusterDryRunArgs(), "--query", "command")...) + query := runTIWithInput(t, bin, "", env, append(createClusterDryRunArgs(), "--query", "command")...) query.wantExitCode(0) - query.wantStdoutContains(`"tdc db create-db-cluster"`) + query.wantStdoutContains(`"ti db create-db-cluster"`) - readOnlyDryRun := runTDC(t, bin, "db", "list-db-clusters", "--dry-run") + readOnlyDryRun := runTI(t, bin, "db", "list-db-clusters", "--dry-run") readOnlyDryRun.wantExitCode(2) - readOnlyDryRun.wantStderrContains("tdc [ERROR]: invalid flag for tdc db list-db-clusters: unknown flag: --dry-run") + readOnlyDryRun.wantStderrContains("ti [ERROR]: invalid flag for ti db list-db-clusters: unknown flag: --dry-run") } func TestStarterOnlyDBGuardrailsThroughBinary(t *testing.T) { - bin := tdcBinary(t) + bin := tiBinary(t) home := t.TempDir() mutations := 0 var listFilters []string @@ -321,16 +404,16 @@ func TestStarterOnlyDBGuardrailsThroughBinary(t *testing.T) { })) defer server.Close() - writeE2EFile(t, filepath.Join(home, ".tdc", "config"), "[default]\nregion_code = 'aws-us-east-1'\nproject_id = 'project-1'\n", 0o600) - writeE2EFile(t, filepath.Join(home, ".tdc", "credentials"), "[default]\ntdc_public_key = 'public'\ntdc_private_key = 'private'\n", 0o600) + writeE2EFile(t, filepath.Join(home, ".ti", "config"), "[default]\nregion_code = 'aws-us-east-1'\nproject_id = 'project-1'\n", 0o600) + writeE2EFile(t, filepath.Join(home, ".ti", "credentials"), "[default]\ntidb_cloud_public_key = 'public'\ntidb_cloud_private_key = 'private'\n", 0o600) env := []string{ "HOME=" + home, - "TDC_ALLOW_TEST_ENDPOINTS=1", - "TDC_TEST_STARTER_BASE_URL=" + server.URL, - "TDC_LOGGING=on", + "TI_ALLOW_TEST_ENDPOINTS=1", + "TI_TEST_STARTER_BASE_URL=" + server.URL, + "TI_LOGGING=on", } - list := runTDCWithInput(t, bin, "", env, "db", "list-db-clusters") + list := runTIWithInput(t, bin, "", env, "db", "list-db-clusters") list.wantExitCode(0) list.wantStdoutContains(`"id": "starter-east"`) list.wantStdoutContains(`"next_page_token": "token-2"`) @@ -340,7 +423,7 @@ func TestStarterOnlyDBGuardrailsThroughBinary(t *testing.T) { list.wantStdoutNotContains("essential-1") list.wantStdoutNotContains("total_size") - westList := runTDCWithInput(t, bin, "", env, "--region", "aws-us-west-2", "db", "list-db-clusters") + westList := runTIWithInput(t, bin, "", env, "--region", "aws-us-west-2", "db", "list-db-clusters") westList.wantExitCode(0) westList.wantStdoutContains(`"id": "starter-west"`) westList.wantStdoutNotContains("starter-east") @@ -355,18 +438,18 @@ func TestStarterOnlyDBGuardrailsThroughBinary(t *testing.T) { t.Fatalf("list filters = %#v, want %#v", listFilters, wantFilters) } - describe := runTDCWithInput(t, bin, "", env, "db", "describe-db-cluster", "--db-cluster-id", "essential-1") + describe := runTIWithInput(t, bin, "", env, "db", "describe-db-cluster", "--db-cluster-id", "essential-1") describe.wantExitCode(2) describe.wantStderrContains(`cluster "essential-1" uses service plan "Essential"`) - update := runTDCWithInput(t, bin, "", env, "db", "update-db-cluster", "--db-cluster-id", "essential-1", "--db-cluster-name", "renamed") + update := runTIWithInput(t, bin, "", env, "db", "update-db-cluster", "--db-cluster-id", "essential-1", "--db-cluster-name", "renamed") update.wantExitCode(2) - update.wantStderrContains(`tdc db only manages Starter clusters`) + update.wantStderrContains(`ti db only manages Starter clusters`) if mutations != 0 { t.Fatalf("non-Starter commands sent %d mutation requests", mutations) } - logData, err := os.ReadFile(filepath.Join(home, ".tdc", "logs", "tdc.jsonl")) + logData, err := os.ReadFile(filepath.Join(home, ".ti", "logs", "ti.jsonl")) if err != nil { t.Fatalf("read operation log: %v", err) } @@ -376,7 +459,7 @@ func TestStarterOnlyDBGuardrailsThroughBinary(t *testing.T) { } func TestCreateDBClusterUsesServerDefaultProjectThroughBinary(t *testing.T) { - bin := tdcBinary(t) + bin := tiBinary(t) home := t.TempDir() created := false server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -401,15 +484,15 @@ func TestCreateDBClusterUsesServerDefaultProjectThroughBinary(t *testing.T) { })) defer server.Close() - writeE2EFile(t, filepath.Join(home, ".tdc", "config"), "[default]\nregion_code = 'aws-us-east-1'\n", 0o600) - writeE2EFile(t, filepath.Join(home, ".tdc", "credentials"), "[default]\ntdc_public_key = 'public'\ntdc_private_key = 'private'\n", 0o600) + writeE2EFile(t, filepath.Join(home, ".ti", "config"), "[default]\nregion_code = 'aws-us-east-1'\n", 0o600) + writeE2EFile(t, filepath.Join(home, ".ti", "credentials"), "[default]\ntidb_cloud_public_key = 'public'\ntidb_cloud_private_key = 'private'\n", 0o600) env := []string{ "HOME=" + home, - "TDC_ALLOW_TEST_ENDPOINTS=1", - "TDC_TEST_STARTER_BASE_URL=" + server.URL, + "TI_ALLOW_TEST_ENDPOINTS=1", + "TI_TEST_STARTER_BASE_URL=" + server.URL, } - result := runTDCWithInput(t, bin, "", env, "db", "create-db-cluster", "--db-cluster-name", "server-default-project") + result := runTIWithInput(t, bin, "", env, "db", "create-db-cluster", "--db-cluster-name", "server-default-project") result.wantExitCode(0) result.wantStdoutContains(`"id": "starter-1"`) if !created { @@ -417,11 +500,11 @@ func TestCreateDBClusterUsesServerDefaultProjectThroughBinary(t *testing.T) { } } -func tdcConfigEnv() []string { +func tiConfigEnv() []string { return []string{ - "TDC_REGION_CODE=aws-us-east-1", - "TDC_PUBLIC_KEY=e2e-public", - "TDC_PRIVATE_KEY=e2e-private", + "TI_REGION_CODE=aws-us-east-1", + "TIDB_CLOUD_PUBLIC_KEY=e2e-public", + "TIDB_CLOUD_PRIVATE_KEY=e2e-private", } } @@ -442,12 +525,12 @@ func artifactNameForRuntime(t *testing.T) string { if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { t.Skipf("unsupported release target %s/%s", runtime.GOOS, runtime.GOARCH) } - return fmt.Sprintf("tdc_%s_%s.tar.gz", runtime.GOOS, runtime.GOARCH) + return fmt.Sprintf("ti_%s_%s.tar.gz", runtime.GOOS, runtime.GOARCH) case "windows": if runtime.GOARCH != "amd64" { t.Skipf("unsupported release target %s/%s", runtime.GOOS, runtime.GOARCH) } - return "tdc_windows_amd64.zip" + return "ti_windows_amd64.zip" default: t.Skipf("unsupported release target %s/%s", runtime.GOOS, runtime.GOARCH) return "" @@ -455,20 +538,20 @@ func artifactNameForRuntime(t *testing.T) string { } func TestConfigureWritesLocalProfile(t *testing.T) { - bin := tdcBinary(t) + bin := tiBinary(t) home := t.TempDir() env := append([]string{"HOME=" + home}, configureIAMEnv(t)...) - result := runTDCWithInput(t, bin, "aws-us-east-1\npublic-key\nprivate-key\n", env, "configure", "--profile", "stage") + result := runTIWithInput(t, bin, "aws-us-east-1\npublic-key\nprivate-key\n", env, "configure", "--profile", "stage") result.wantExitCode(0) result.wantStdoutContains(`"project_id": "virtual-e2e"`) result.wantStdoutNotContains("private-key") - configBytes, err := os.ReadFile(filepath.Join(home, ".tdc", "config")) + configBytes, err := os.ReadFile(filepath.Join(home, ".ti", "config")) if err != nil { t.Fatalf("read config: %v", err) } - credentialsPath := filepath.Join(home, ".tdc", "credentials") + credentialsPath := filepath.Join(home, ".ti", "credentials") credentialsBytes, err := os.ReadFile(credentialsPath) if err != nil { t.Fatalf("read credentials: %v", err) @@ -480,8 +563,8 @@ func TestConfigureWritesLocalProfile(t *testing.T) { !strings.Contains(string(configBytes), `project_id = 'virtual-e2e'`) { t.Fatalf("config did not contain expected stage profile:\n%s", string(configBytes)) } - if !strings.Contains(string(credentialsBytes), `tdc_public_key = 'public-key'`) || - !strings.Contains(string(credentialsBytes), `tdc_private_key = 'private-key'`) { + if !strings.Contains(string(credentialsBytes), `tidb_cloud_public_key = 'public-key'`) || + !strings.Contains(string(credentialsBytes), `tidb_cloud_private_key = 'private-key'`) { t.Fatalf("credentials did not contain expected keys:\n%s", string(credentialsBytes)) } @@ -494,32 +577,32 @@ func TestConfigureWritesLocalProfile(t *testing.T) { t.Fatalf("credentials mode: want 0600, got %o", info.Mode().Perm()) } } - if _, err := os.Stat(filepath.Join(home, ".tdc", ".preferences")); !os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(home, ".ti", ".preferences")); !os.IsNotExist(err) { t.Fatalf("configure created global settings: %v", err) } } func TestConfigureNonInteractiveFromEnvironment(t *testing.T) { - bin := tdcBinary(t) + bin := tiBinary(t) home := t.TempDir() env := []string{ "HOME=" + home, - "TDC_REGION_CODE=aws-us-east-1", - "TDC_PUBLIC_KEY=ci-public", - "TDC_PRIVATE_KEY=ci-private", + "TI_REGION_CODE=aws-us-east-1", + "TIDB_CLOUD_PUBLIC_KEY=ci-public", + "TIDB_CLOUD_PRIVATE_KEY=ci-private", } env = append(env, configureIAMEnv(t)...) - result := runTDCWithInput(t, bin, "", env, "configure", "--profile", "ci", "--non-interactive") + result := runTIWithInput(t, bin, "", env, "configure", "--profile", "ci", "--non-interactive") result.wantExitCode(0) result.wantStdoutContains(`"project_id": "virtual-e2e"`) result.wantStdoutNotContains("ci-private") - configBytes, err := os.ReadFile(filepath.Join(home, ".tdc", "config")) + configBytes, err := os.ReadFile(filepath.Join(home, ".ti", "config")) if err != nil { t.Fatalf("read config: %v", err) } - credentialsBytes, err := os.ReadFile(filepath.Join(home, ".tdc", "credentials")) + credentialsBytes, err := os.ReadFile(filepath.Join(home, ".ti", "credentials")) if err != nil { t.Fatalf("read credentials: %v", err) } @@ -529,8 +612,8 @@ func TestConfigureNonInteractiveFromEnvironment(t *testing.T) { strings.Contains(string(configBytes), `cloud_provider`) { t.Fatalf("config did not contain expected ci profile:\n%s", string(configBytes)) } - if !strings.Contains(string(credentialsBytes), `tdc_public_key = 'ci-public'`) || - !strings.Contains(string(credentialsBytes), `tdc_private_key = 'ci-private'`) { + if !strings.Contains(string(credentialsBytes), `tidb_cloud_public_key = 'ci-public'`) || + !strings.Contains(string(credentialsBytes), `tidb_cloud_private_key = 'ci-private'`) { t.Fatalf("credentials did not contain expected ci keys:\n%s", string(credentialsBytes)) } } @@ -539,9 +622,9 @@ func TestFSResourceRegistrySelectionAcrossCommandFamilies(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("fake companion build path is covered by unit tests on Windows") } - bin := tdcBinary(t) + bin := tiBinary(t) home := t.TempDir() - companion := filepath.Join(t.TempDir(), "tdc-drive9") + companion := filepath.Join(t.TempDir(), "ti-drive9") build := exec.Command("go", "build", "-o", companion, "./testdata/fake-drive9.go") build.Dir = "." if output, err := build.CombinedOutput(); err != nil { @@ -554,36 +637,36 @@ func TestFSResourceRegistrySelectionAcrossCommandFamilies(t *testing.T) { defer manifestServer.Close() baseEnv := []string{ "HOME=" + home, - "TDC_DRIVE9_BIN=" + companion, + "TI_DRIVE9_BIN=" + companion, "FAKE_DRIVE9_RECORD=" + recordPath, - "TDC_ALLOW_TEST_ENDPOINTS=1", - "TDC_TEST_FS_MANIFEST_URL=" + manifestServer.URL, + "TI_ALLOW_TEST_ENDPOINTS=1", + "TI_TEST_FS_MANIFEST_URL=" + manifestServer.URL, } baseEnv = append(baseEnv, configureIAMEnv(t)...) - configured := runTDCWithInput(t, bin, "", append(baseEnv, - "TDC_REGION_CODE=aws-us-east-1", - "TDC_PUBLIC_KEY=e2e-public", - "TDC_PRIVATE_KEY=e2e-private", + configured := runTIWithInput(t, bin, "", append(baseEnv, + "TI_REGION_CODE=aws-us-east-1", + "TIDB_CLOUD_PUBLIC_KEY=e2e-public", + "TIDB_CLOUD_PRIVATE_KEY=e2e-private", ), "configure", "--profile", "stage", "--non-interactive") configured.wantExitCode(0) - missingWithZeroResources := runTDCWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "list-files", "--path", "/") + missingWithZeroResources := runTIWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "list-files", "--path", "/") missingWithZeroResources.wantExitCode(2) - missingWithZeroResources.wantStderrContains("file system name is required; pass --file-system-name or set TDC_FS_FILE_SYSTEM_NAME") + missingWithZeroResources.wantStderrContains("file system name is required; pass --file-system-name or set TI_FS_FILE_SYSTEM_NAME") - createWorkspace := runTDCWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "create-file-system", "--file-system-name", "workspace", "--wait") + createWorkspace := runTIWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "create-file-system", "--file-system-name", "workspace", "--wait") createWorkspace.wantExitCode(0) createWorkspace.wantStdoutContains(`"status": "ready"`) createWorkspace.wantStdoutContains(`"credentials_stored": true`) createWorkspace.wantStdoutContains(`"fs_token": "key-workspace"`) - missingWithOneResource := runTDCWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "list-files", "--path", "/") + missingWithOneResource := runTIWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "list-files", "--path", "/") missingWithOneResource.wantExitCode(2) - missingWithOneResource.wantStderrContains("file system name is required; pass --file-system-name or set TDC_FS_FILE_SYSTEM_NAME") - createScratch := runTDCWithInput(t, bin, "", baseEnv, "--profile", "stage", "--region", "aws-us-west-2", "fs", "create-file-system", "--file-system-name", "scratch", "--wait") + missingWithOneResource.wantStderrContains("file system name is required; pass --file-system-name or set TI_FS_FILE_SYSTEM_NAME") + createScratch := runTIWithInput(t, bin, "", baseEnv, "--profile", "stage", "--region", "aws-us-west-2", "fs", "create-file-system", "--file-system-name", "scratch", "--wait") createScratch.wantExitCode(0) createScratch.wantStdoutContains(`"status": "ready"`) createScratch.wantStdoutContains(`"credentials_stored": true`) - list := runTDCWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "list-file-systems") + list := runTIWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "list-file-systems") list.wantExitCode(0) list.wantStdoutContains(`"file_system_name": "workspace"`) list.wantStdoutContains(`"file_system_name": "scratch"`) @@ -591,7 +674,7 @@ func TestFSResourceRegistrySelectionAcrossCommandFamilies(t *testing.T) { list.wantStdoutNotContains("key-scratch") list.wantStdoutNotContains("default_file_system_name") list.wantStdoutNotContains("is_default") - describe := runTDCWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "describe-file-system", "--file-system-name", "scratch") + describe := runTIWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "describe-file-system", "--file-system-name", "scratch") describe.wantExitCode(0) describe.wantStdoutContains(`"tenant_id": "tenant-scratch"`) describe.wantStdoutContains(`"region_code": "aws-us-west-2"`) @@ -603,26 +686,26 @@ func TestFSResourceRegistrySelectionAcrossCommandFamilies(t *testing.T) { {"fs-journal", "read-journal-entries", "--journal-id", "jrn-e2e"}, {"fs-git", "hydrate-git-workspace", "--target-path", filepath.Join(home, "ambiguous-workspace")}, } { - missing := runTDCWithInput(t, bin, "", baseEnv, append([]string{"--profile", "stage"}, args...)...) + missing := runTIWithInput(t, bin, "", baseEnv, append([]string{"--profile", "stage"}, args...)...) missing.wantExitCode(2) - missing.wantStderrContains("file system name is required; pass --file-system-name or set TDC_FS_FILE_SYSTEM_NAME") + missing.wantStderrContains("file system name is required; pass --file-system-name or set TI_FS_FILE_SYSTEM_NAME") } if calls := readFakeDrive9Calls(t, recordPath); len(calls) != callsBeforeMissingSelectorCommands { t.Fatalf("missing resource selection must fail before invoking Drive9: calls before=%d after=%d", callsBeforeMissingSelectorCommands, len(calls)) } - missingDryRun := runTDCWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "create-directory", "--path", "/tmp", "--dry-run") + missingDryRun := runTIWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "create-directory", "--path", "/tmp", "--dry-run") missingDryRun.wantExitCode(2) - missingDryRun.wantStderrContains("file system name is required; pass --file-system-name or set TDC_FS_FILE_SYSTEM_NAME") + missingDryRun.wantStderrContains("file system name is required; pass --file-system-name or set TI_FS_FILE_SYSTEM_NAME") - dataPlane := runTDCWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "list-files", "--file-system-name", "scratch", "--path", "/") + dataPlane := runTIWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "list-files", "--file-system-name", "scratch", "--path", "/") dataPlane.wantExitCode(0) - vault := runTDCWithInput(t, bin, "", append(baseEnv, "TDC_FS_FILE_SYSTEM_NAME=workspace"), "--profile", "stage", "fs-vault", "list-secrets") + vault := runTIWithInput(t, bin, "", append(baseEnv, "TI_FS_FILE_SYSTEM_NAME=workspace"), "--profile", "stage", "fs-vault", "list-secrets") vault.wantExitCode(0) - journal := runTDCWithInput(t, bin, "", append(baseEnv, "TDC_FS_FILE_SYSTEM_NAME=workspace"), "--profile", "stage", "fs-journal", "create-journal", "--file-system-name", "scratch", "--journal-id", "jrn-e2e") + journal := runTIWithInput(t, bin, "", append(baseEnv, "TI_FS_FILE_SYSTEM_NAME=workspace"), "--profile", "stage", "fs-journal", "create-journal", "--file-system-name", "scratch", "--journal-id", "jrn-e2e") journal.wantExitCode(0) - git := runTDCWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs-git", "hydrate-git-workspace", "--file-system-name", "scratch", "--target-path", filepath.Join(home, "workspace")) + git := runTIWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs-git", "hydrate-git-workspace", "--file-system-name", "scratch", "--target-path", filepath.Join(home, "workspace")) git.wantExitCode(0) - mount := runTDCWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "mount-file-system", "--file-system-name", "scratch", "--mount-path", filepath.Join(home, "mount"), "--foreground") + mount := runTIWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "mount-file-system", "--file-system-name", "scratch", "--mount-path", filepath.Join(home, "mount"), "--foreground") mount.wantExitCode(0) calls := readFakeDrive9Calls(t, recordPath) @@ -634,16 +717,16 @@ func TestFSResourceRegistrySelectionAcrossCommandFamilies(t *testing.T) { assertFakeDrive9Call(t, calls, []string{"git", "hydrate"}, "key-scratch", home, "stage", "scratch", "https://fs-west.test", "aws-us-west-2") assertFakeDrive9Call(t, calls, []string{"mount"}, "key-scratch", home, "stage", "scratch", "https://fs-west.test", "aws-us-west-2") - deleteScratch := runTDCWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "delete-file-system", "--file-system-name", "scratch") + deleteScratch := runTIWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "delete-file-system", "--file-system-name", "scratch") deleteScratch.wantExitCode(0) deleteScratch.wantStdoutContains(`"status": "deleting"`) - afterDelete := runTDCWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "list-file-systems") + afterDelete := runTIWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "list-file-systems") afterDelete.wantExitCode(0) afterDelete.wantStdoutContains(`"file_system_name": "workspace"`) afterDelete.wantStdoutNotContains(`"file_system_name": "scratch"`) - stillMissingAfterDelete := runTDCWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "list-files", "--path", "/") + stillMissingAfterDelete := runTIWithInput(t, bin, "", baseEnv, "--profile", "stage", "fs", "list-files", "--path", "/") stillMissingAfterDelete.wantExitCode(2) - stillMissingAfterDelete.wantStderrContains("file system name is required; pass --file-system-name or set TDC_FS_FILE_SYSTEM_NAME") + stillMissingAfterDelete.wantStderrContains("file system name is required; pass --file-system-name or set TI_FS_FILE_SYSTEM_NAME") assertFakeDrive9Call(t, readFakeDrive9Calls(t, recordPath), []string{"delete", "--json", "--yes"}, "key-scratch", home, "stage", "scratch", "https://fs-west.test", "aws-us-west-2") for _, args := range [][]string{ @@ -651,7 +734,7 @@ func TestFSResourceRegistrySelectionAcrossCommandFamilies(t *testing.T) { {"--profile", "stage", "fs", "unset-default-file-system"}, {"--profile", "stage", "fs", "create-file-system", "--file-system-name", "removed-flag", "--set-default"}, } { - removed := runTDCWithInput(t, bin, "", baseEnv, args...) + removed := runTIWithInput(t, bin, "", baseEnv, args...) removed.wantExitCode(2) } } @@ -660,9 +743,9 @@ func TestFSConfigurationFreeAccess(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("fake companion build path is covered by unit tests on Windows") } - bin := tdcBinary(t) + bin := tiBinary(t) home := t.TempDir() - companion := filepath.Join(t.TempDir(), "tdc-drive9") + companion := filepath.Join(t.TempDir(), "ti-drive9") build := exec.Command("go", "build", "-o", companion, "./testdata/fake-drive9.go") build.Dir = "." if output, err := build.CombinedOutput(); err != nil { @@ -675,20 +758,20 @@ func TestFSConfigurationFreeAccess(t *testing.T) { defer manifestServer.Close() baseEnv := []string{ "HOME=" + home, - "TDC_LOGGING=on", - "TDC_DRIVE9_BIN=" + companion, + "TI_LOGGING=on", + "TI_DRIVE9_BIN=" + companion, "FAKE_DRIVE9_RECORD=" + recordPath, - "TDC_ALLOW_TEST_ENDPOINTS=1", - "TDC_TEST_FS_MANIFEST_URL=" + manifestServer.URL, + "TI_ALLOW_TEST_ENDPOINTS=1", + "TI_TEST_FS_MANIFEST_URL=" + manifestServer.URL, } authEnv := append(append([]string{}, baseEnv...), - "TDC_FS_FILE_SYSTEM_NAME=workspace", - "TDC_FS_TOKEN=configuration-free-token", - "TDC_REGION_CODE=aws-us-east-1", - "TDC_PUBLIC_KEY=must-not-reach-data-plane", + "TI_FS_FILE_SYSTEM_NAME=workspace", + "TI_FS_TOKEN=configuration-free-token", + "TI_REGION_CODE=aws-us-east-1", + "TIDB_CLOUD_PUBLIC_KEY=must-not-reach-data-plane", ) - localList := runTDCWithInput(t, bin, "", baseEnv, "fs", "list-file-systems") + localList := runTIWithInput(t, bin, "", baseEnv, "fs", "list-file-systems") localList.wantExitCode(0) localList.wantStdoutContains(`"file_systems": []`) @@ -699,11 +782,11 @@ func TestFSConfigurationFreeAccess(t *testing.T) { {"fs-vault", "list-secrets"}, {"fs-git", "hydrate-git-workspace", "--target-path", filepath.Join(home, "workspace")}, } { - result := runTDCWithInput(t, bin, "", authEnv, args...) + result := runTIWithInput(t, bin, "", authEnv, args...) result.wantExitCode(0) } - flagsOnly := runTDCWithInput(t, bin, "", baseEnv, + flagsOnly := runTIWithInput(t, bin, "", baseEnv, "--region", "aws-us-east-1", "fs", "list-files", "--file-system-name", "workspace", @@ -712,7 +795,7 @@ func TestFSConfigurationFreeAccess(t *testing.T) { ) flagsOnly.wantExitCode(0) - mixed := runTDCWithInput(t, bin, "", append(baseEnv, "TDC_FS_TOKEN=mixed-token"), + mixed := runTIWithInput(t, bin, "", append(baseEnv, "TI_FS_TOKEN=mixed-token"), "--region", "aws-us-east-1", "fs", "list-files", "--file-system-name", "workspace", @@ -721,32 +804,32 @@ func TestFSConfigurationFreeAccess(t *testing.T) { mixed.wantExitCode(0) mountPath := filepath.Join(home, "mount") - mount := runTDCWithInput(t, bin, "", authEnv, + mount := runTIWithInput(t, bin, "", authEnv, "fs", "mount-file-system", "--mount-path", mountPath, ) mount.wantExitCode(0) - drain := runTDCWithInput(t, bin, "", baseEnv, + drain := runTIWithInput(t, bin, "", baseEnv, "fs", "drain-file-system", "--mount-path", mountPath, ) drain.wantExitCode(0) - unmount := runTDCWithInput(t, bin, "", baseEnv, + unmount := runTIWithInput(t, bin, "", baseEnv, "fs", "unmount-file-system", "--mount-path", mountPath, ) unmount.wantExitCode(0) for _, path := range []string{ - filepath.Join(home, ".tdc", "config"), - filepath.Join(home, ".tdc", "credentials"), - filepath.Join(home, ".tdc", "fs_resources"), + filepath.Join(home, ".ti", "config"), + filepath.Join(home, ".ti", "credentials"), + filepath.Join(home, ".ti", "fs_resources"), } { if _, err := os.Stat(path); !os.IsNotExist(err) { - t.Fatalf("configuration-free command persisted tdc configuration at %s: %v", path, err) + t.Fatalf("configuration-free command persisted ti configuration at %s: %v", path, err) } } - logData, err := os.ReadFile(filepath.Join(home, ".tdc", "logs", "tdc.jsonl")) + logData, err := os.ReadFile(filepath.Join(home, ".ti", "logs", "ti.jsonl")) if err != nil { t.Fatalf("read configuration-free operation log: %v", err) } @@ -760,8 +843,8 @@ func TestFSConfigurationFreeAccess(t *testing.T) { t.Fatal("configuration-free commands did not invoke companion") } for _, call := range calls { - if call.TDCPublicKey != "" || call.TDCPrivateKey != "" || call.TDCFSToken != "" || call.Drive9Public != "" || call.Drive9Private != "" { - t.Fatalf("data-plane companion inherited TiDB Cloud or raw tdc secrets: %#v", call) + if call.TiDBCloudPublicKey != "" || call.TiDBCloudPrivateKey != "" || call.TIFSToken != "" || call.Drive9Public != "" || call.Drive9Private != "" { + t.Fatalf("data-plane companion inherited TiDB Cloud or raw ti secrets: %#v", call) } } assertFakeDrive9Call(t, calls, []string{"fs", "ls"}, "configuration-free-token", home, "default", "workspace", "https://fs-east.test", "aws-us-east-1") @@ -770,16 +853,16 @@ func TestFSConfigurationFreeAccess(t *testing.T) { } type fakeDrive9Call struct { - Args []string `json:"args"` - Home string `json:"home"` - APIKey string `json:"api_key"` - Server string `json:"server"` - RegionCode string `json:"region_code"` - TDCPublicKey string `json:"tdc_public_key,omitempty"` - TDCPrivateKey string `json:"tdc_private_key,omitempty"` - TDCFSToken string `json:"tdc_fs_token,omitempty"` - Drive9Public string `json:"drive9_public_key,omitempty"` - Drive9Private string `json:"drive9_private_key,omitempty"` + Args []string `json:"args"` + Home string `json:"home"` + APIKey string `json:"api_key"` + Server string `json:"server"` + RegionCode string `json:"region_code"` + TiDBCloudPublicKey string `json:"tidb_cloud_public_key,omitempty"` + TiDBCloudPrivateKey string `json:"tidb_cloud_private_key,omitempty"` + TIFSToken string `json:"ti_fs_token,omitempty"` + Drive9Public string `json:"drive9_public_key,omitempty"` + Drive9Private string `json:"drive9_private_key,omitempty"` } func readFakeDrive9Calls(t *testing.T, path string) []fakeDrive9Call { @@ -827,31 +910,31 @@ func assertFakeDrive9Call(t *testing.T, calls []fakeDrive9Call, prefix []string, t.Fatalf("missing fake Drive9 call with prefix %v in %#v", prefix, calls) } -func tdcBinary(t *testing.T) string { +func tiBinary(t *testing.T) string { t.Helper() - bin := os.Getenv("TDC_E2E_BIN") + bin := os.Getenv("TI_E2E_BIN") if bin == "" { - t.Skip("TDC_E2E_BIN is not set; run make e2e") + t.Skip("TI_E2E_BIN is not set; run make e2e") } return bin } func TestOperationLogWritesSafeJSONL(t *testing.T) { - bin := tdcBinary(t) + bin := tiBinary(t) home := t.TempDir() env := []string{ "HOME=" + home, - "TDC_LOGGING=on", - "TDC_REGION_CODE=aws-us-east-1", - "TDC_PUBLIC_KEY=ci-public-secret", - "TDC_PRIVATE_KEY=ci-private-secret", + "TI_LOGGING=on", + "TI_REGION_CODE=aws-us-east-1", + "TIDB_CLOUD_PUBLIC_KEY=ci-public-secret", + "TIDB_CLOUD_PRIVATE_KEY=ci-private-secret", } env = append(env, configureIAMEnv(t)...) - result := runTDCWithInput(t, bin, "", env, "configure", "--profile", "ci", "--non-interactive") + result := runTIWithInput(t, bin, "", env, "configure", "--profile", "ci", "--non-interactive") result.wantExitCode(0) - logPath := filepath.Join(home, ".tdc", "logs", "tdc.jsonl") + logPath := filepath.Join(home, ".ti", "logs", "ti.jsonl") data, err := os.ReadFile(logPath) if err != nil { t.Fatalf("read operation log: %v", err) @@ -869,7 +952,7 @@ func TestOperationLogWritesSafeJSONL(t *testing.T) { event = candidate } } - if event["type"] != "command" || event["command"] != "tdc configure" || event["profile"] != "ci" { + if event["type"] != "command" || event["command"] != "ti configure" || event["profile"] != "ci" { t.Fatalf("unexpected operation log event: %#v", event) } } @@ -885,36 +968,36 @@ func configureIAMEnv(t *testing.T) []string { })) t.Cleanup(server.Close) return []string{ - "TDC_ALLOW_TEST_ENDPOINTS=1", - "TDC_TEST_IAM_BASE_URL=" + server.URL, + "TI_ALLOW_TEST_ENDPOINTS=1", + "TI_TEST_IAM_BASE_URL=" + server.URL, } } func TestOperationLogCanBeDisabled(t *testing.T) { - bin := tdcBinary(t) + bin := tiBinary(t) home := t.TempDir() - result := runTDCWithInput(t, bin, "", []string{ + result := runTIWithInput(t, bin, "", []string{ "HOME=" + home, - "TDC_LOGGING=off", + "TI_LOGGING=off", }, "help") result.wantExitCode(0) - if _, err := os.Stat(filepath.Join(home, ".tdc", "logs", "tdc.jsonl")); !os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(home, ".ti", "logs", "ti.jsonl")); !os.IsNotExist(err) { t.Fatalf("expected no operation log file, got err=%v", err) } } func TestGlobalSettingsControlOperationLogging(t *testing.T) { - bin := tdcBinary(t) + bin := tiBinary(t) t.Run("enabled", func(t *testing.T) { home := t.TempDir() - writeE2EFile(t, filepath.Join(home, ".tdc", ".preferences"), "schema_version = 1\n\n[logging]\nenabled = true\nmax_file_mb = 1\nmax_files = 2\n", 0o600) + writeE2EFile(t, filepath.Join(home, ".ti", ".preferences"), "schema_version = 1\n\n[logging]\nenabled = true\nmax_file_mb = 1\nmax_files = 2\n", 0o600) for _, profileName := range []string{"default", "stage"} { - result := runTDCUsingLoggingSettings(t, bin, []string{"HOME=" + home}, "--profile", profileName, "help") + result := runTIUsingLoggingSettings(t, bin, []string{"HOME=" + home}, "--profile", profileName, "help") result.wantExitCode(0) } - data, err := os.ReadFile(filepath.Join(home, ".tdc", "logs", "tdc.jsonl")) + data, err := os.ReadFile(filepath.Join(home, ".ti", "logs", "ti.jsonl")) if err != nil { t.Fatalf("enabled settings did not create operation log: %v", err) } @@ -935,19 +1018,19 @@ func TestGlobalSettingsControlOperationLogging(t *testing.T) { t.Run("disabled", func(t *testing.T) { home := t.TempDir() - writeE2EFile(t, filepath.Join(home, ".tdc", ".preferences"), "[logging]\nenabled = false\n", 0o600) - result := runTDCUsingLoggingSettings(t, bin, []string{"HOME=" + home}, "help") + writeE2EFile(t, filepath.Join(home, ".ti", ".preferences"), "[logging]\nenabled = false\n", 0o600) + result := runTIUsingLoggingSettings(t, bin, []string{"HOME=" + home}, "help") result.wantExitCode(0) - if _, err := os.Stat(filepath.Join(home, ".tdc", "logs", "tdc.jsonl")); !os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(home, ".ti", "logs", "ti.jsonl")); !os.IsNotExist(err) { t.Fatalf("disabled settings created operation log: %v", err) } }) } func TestLegacyLoggingConfigMigratesThroughBinary(t *testing.T) { - bin := tdcBinary(t) + bin := tiBinary(t) home := t.TempDir() - configPath := filepath.Join(home, ".tdc", "config") + configPath := filepath.Join(home, ".ti", "config") writeE2EFile(t, configPath, `[default] region_code = "aws-us-east-1" project_id = "virtual-e2e" @@ -957,14 +1040,14 @@ enabled = false max_file_mb = 3 max_files = 2 `, 0o644) - credentialsPath := filepath.Join(home, ".tdc", "credentials") - credentials := "[default]\ntdc_public_key = \"public\"\ntdc_private_key = \"private\"\n" + credentialsPath := filepath.Join(home, ".ti", "credentials") + credentials := "[default]\ntidb_cloud_public_key = \"public\"\ntidb_cloud_private_key = \"private\"\n" writeE2EFile(t, credentialsPath, credentials, 0o600) - result := runTDCUsingLoggingSettings(t, bin, []string{"HOME=" + home}, "help") + result := runTIUsingLoggingSettings(t, bin, []string{"HOME=" + home}, "help") result.wantExitCode(0) - settingsPath := filepath.Join(home, ".tdc", ".preferences") + settingsPath := filepath.Join(home, ".ti", ".preferences") settingsData, err := os.ReadFile(settingsPath) if err != nil { t.Fatalf("read migrated settings: %v", err) @@ -998,13 +1081,13 @@ max_files = 2 } func TestMalformedGlobalSettingsFailClosedThroughBinary(t *testing.T) { - bin := tdcBinary(t) + bin := tiBinary(t) home := t.TempDir() - settingsPath := filepath.Join(home, ".tdc", ".preferences") + settingsPath := filepath.Join(home, ".ti", ".preferences") before := "[logging]\nenabled = \"not-a-boolean\"\n" writeE2EFile(t, settingsPath, before, 0o644) - result := runTDCUsingLoggingSettings(t, bin, []string{"HOME=" + home}, "--debug", "help") + result := runTIUsingLoggingSettings(t, bin, []string{"HOME=" + home}, "--debug", "help") result.wantExitCode(0) result.wantStderrContains("operation logging disabled because global settings could not be loaded") if strings.Contains(result.stderr, "not-a-boolean") { @@ -1017,17 +1100,17 @@ func TestMalformedGlobalSettingsFailClosedThroughBinary(t *testing.T) { if string(after) != before { t.Fatalf("malformed settings were rewritten: %q", after) } - if _, err := os.Stat(filepath.Join(home, ".tdc", "logs", "tdc.jsonl")); !os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(home, ".ti", "logs", "ti.jsonl")); !os.IsNotExist(err) { t.Fatalf("malformed settings did not fail closed: %v", err) } } func TestUpdateAndReservedProfileGlobalSettingsBoundaries(t *testing.T) { - bin := tdcBinary(t) + bin := tiBinary(t) - t.Run("update does not access tdc home", func(t *testing.T) { + t.Run("update does not access ti home", func(t *testing.T) { home := t.TempDir() - settingsPath := filepath.Join(home, ".tdc", ".preferences") + settingsPath := filepath.Join(home, ".ti", ".preferences") before := "not valid toml = [" writeE2EFile(t, settingsPath, before, 0o600) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { @@ -1035,14 +1118,14 @@ func TestUpdateAndReservedProfileGlobalSettingsBoundaries(t *testing.T) { if runtime.GOOS == "windows" { extension = "zip" } - assetName := fmt.Sprintf("tdc_%s_%s.%s", runtime.GOOS, runtime.GOARCH, extension) + assetName := fmt.Sprintf("ti_%s_%s.%s", runtime.GOOS, runtime.GOARCH, extension) _, _ = fmt.Fprintf(w, `{"tag_name":"v0.1.5","html_url":"https://example.test/v0.1.5","assets":[{"name":%q,"browser_download_url":"https://example.test/%s"}]}`, assetName, assetName) })) defer server.Close() - result := runTDCUsingLoggingSettings(t, bin, []string{ + result := runTIUsingLoggingSettings(t, bin, []string{ "HOME=" + home, - "TDC_LOGGING=on", - "TDC_RELEASE_API_BASE_URL=" + server.URL, + "TI_LOGGING=on", + "TI_RELEASE_API_BASE_URL=" + server.URL, }, "--debug", "update", "--check") result.wantExitCode(0) result.wantStderrNotContains("operation logging disabled") @@ -1053,21 +1136,21 @@ func TestUpdateAndReservedProfileGlobalSettingsBoundaries(t *testing.T) { if string(after) != before { t.Fatalf("update changed settings: %q", after) } - if _, err := os.Stat(filepath.Join(home, ".tdc", "logs", "tdc.jsonl")); !os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(home, ".ti", "logs", "ti.jsonl")); !os.IsNotExist(err) { t.Fatalf("update wrote operation log: %v", err) } }) t.Run("logging profile is reserved", func(t *testing.T) { home := t.TempDir() - result := runTDCWithInput(t, bin, "", []string{ + result := runTIWithInput(t, bin, "", []string{ "HOME=" + home, - "TDC_LOGGING=off", - }, "configure", "--profile", "logging", "--region-code", "aws-us-east-1", "--tdc-public-key", "public", "--tdc-private-key", "private", "--non-interactive") + "TI_LOGGING=off", + }, "configure", "--profile", "logging", "--region-code", "aws-us-east-1", "--tidb-cloud-public-key", "public", "--tidb-cloud-private-key", "private", "--non-interactive") result.wantExitCode(2) result.wantStderrContains(`profile name "logging" is reserved`) for _, name := range []string{"config", "credentials", ".preferences"} { - if _, err := os.Stat(filepath.Join(home, ".tdc", name)); !os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(home, ".ti", name)); !os.IsNotExist(err) { t.Fatalf("reserved profile wrote %s: %v", name, err) } } @@ -1084,9 +1167,9 @@ func writeE2EFile(t *testing.T, path, content string, mode os.FileMode) { } } -func runTDC(t *testing.T, bin string, args ...string) commandResult { +func runTI(t *testing.T, bin string, args ...string) commandResult { t.Helper() - return runTDCWithInput(t, bin, "", nil, args...) + return runTIWithInput(t, bin, "", nil, args...) } func hasLiveFSCommandFamily(args []string) bool { @@ -1099,23 +1182,26 @@ func hasLiveFSCommandFamily(args []string) bool { return false } -func runTDCWithInput(t *testing.T, bin, stdin string, env []string, args ...string) commandResult { - return runTDCProcess(t, bin, stdin, env, true, args...) +func runTIWithInput(t *testing.T, bin, stdin string, env []string, args ...string) commandResult { + return runTIProcess(t, bin, stdin, env, true, args...) } -func runTDCUsingLoggingSettings(t *testing.T, bin string, env []string, args ...string) commandResult { +func runTIUsingLoggingSettings(t *testing.T, bin string, env []string, args ...string) commandResult { t.Helper() - return runTDCProcess(t, bin, "", env, false, args...) + return runTIProcess(t, bin, "", env, false, args...) } -func runTDCProcess(t *testing.T, bin, stdin string, env []string, disableLoggingByDefault bool, args ...string) commandResult { +func runTIProcess(t *testing.T, bin, stdin string, env []string, disableLoggingByDefault bool, args ...string) commandResult { t.Helper() - if os.Getenv("TDC_LIVE") == "1" && hasLiveFSCommandFamily(args) && !envContains(env, "TDC_FS_FILE_SYSTEM_NAME") { - name := strings.TrimSpace(os.Getenv("TDC_LIVE_FS_NAME")) + if os.Getenv("TI_LIVE") != "1" && !envContains(env, "HOME") { + env = append(env, "HOME="+t.TempDir()) + } + if os.Getenv("TI_LIVE") == "1" && hasLiveFSCommandFamily(args) && !envContains(env, "TI_FS_FILE_SYSTEM_NAME") { + name := strings.TrimSpace(os.Getenv("TI_LIVE_FS_NAME")) if name == "" { name = "workspace" } - env = append(env, "TDC_FS_FILE_SYSTEM_NAME="+name) + env = append(env, "TI_FS_FILE_SYSTEM_NAME="+name) } cmd := exec.Command(bin, args...) @@ -1127,8 +1213,8 @@ func runTDCProcess(t *testing.T, bin, stdin string, env []string, disableLogging cmd.Stdin = strings.NewReader(stdin) } cmd.Env = append([]string{}, os.Environ()...) - if disableLoggingByDefault && !envContains(env, "TDC_LOGGING") { - cmd.Env = append(cmd.Env, "TDC_LOGGING=off") + if disableLoggingByDefault && !envContains(env, "TI_LOGGING") { + cmd.Env = append(cmd.Env, "TI_LOGGING=off") } if len(env) > 0 { cmd.Env = append(cmd.Env, env...) @@ -1210,7 +1296,7 @@ func (r commandResult) fail(format string, args ...any) { r.t.Helper() message := fmt.Sprintf(format, args...) r.t.Fatalf("%s", strings.Join([]string{ - "command: tdc " + strings.Join(r.args, " "), + "command: ti " + strings.Join(r.args, " "), message, "stdout:\n" + r.stdout, "stderr:\n" + r.stderr, diff --git a/e2e/installer_test.go b/e2e/installer_test.go index 228f3a2..f518acb 100644 --- a/e2e/installer_test.go +++ b/e2e/installer_test.go @@ -1,6 +1,8 @@ package e2e import ( + "crypto/sha256" + "fmt" "os" "os/exec" "path/filepath" @@ -9,7 +11,7 @@ import ( "testing" ) -func TestUnixInstallerDefaultsToTDCBin(t *testing.T) { +func TestUnixInstallerDefaultsToTIBin(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("shell installer is not supported on Windows") } @@ -30,10 +32,10 @@ func TestUnixInstallerDefaultsToTDCBin(t *testing.T) { t.Fatalf("install.sh --dry-run failed: %v\n%s", err, output) } got := string(output) - installDir := filepath.Join(home, ".tdc", "bin") + installDir := filepath.Join(home, ".ti", "bin") for _, want := range []string{ - "target: " + filepath.Join(installDir, "tdc"), - "companion_target: " + filepath.Join(installDir, "tdc-drive9"), + "target: " + filepath.Join(installDir, "ti"), + "companion_target: " + filepath.Join(installDir, "ti-drive9"), `path_export: export PATH="` + installDir + `:$PATH"`, } { if !strings.Contains(got, want) { @@ -45,6 +47,185 @@ func TestUnixInstallerDefaultsToTDCBin(t *testing.T) { } } +func TestUnixInstallerInstallDirectoryCompatibility(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("shell installer is not supported on Windows") + } + script := filepath.Join("..", "scripts", "install.sh") + home := t.TempDir() + legacyDir := filepath.Join(home, "legacy-bin") + legacy := exec.Command("sh", script, "--dry-run") + legacy.Env = []string{"HOME=" + home, "PATH=" + os.Getenv("PATH"), "TDC_INSTALL_DIR=" + legacyDir} + legacyOutput, err := legacy.CombinedOutput() + if err != nil { + t.Fatalf("legacy install variable failed: %v\n%s", err, legacyOutput) + } + if !strings.Contains(string(legacyOutput), "target: "+filepath.Join(legacyDir, "ti")) { + t.Fatalf("legacy install variable was not used:\n%s", legacyOutput) + } + + conflict := exec.Command("sh", script, "--dry-run", "--install-dir", filepath.Join(home, "explicit")) + conflict.Env = []string{ + "HOME=" + home, + "PATH=" + os.Getenv("PATH"), + "TI_INSTALL_DIR=" + filepath.Join(home, "canonical"), + "TDC_INSTALL_DIR=" + filepath.Join(home, "legacy"), + } + conflictOutput, err := conflict.CombinedOutput() + if err == nil { + t.Fatalf("conflicting install variables should fail:\n%s", conflictOutput) + } + if !strings.Contains(string(conflictOutput), "TI_INSTALL_DIR and deprecated TDC_INSTALL_DIR contain different values") { + t.Fatalf("unexpected conflict output:\n%s", conflictOutput) + } +} + +func TestInstallersMigrateBeforeCreatingNewHome(t *testing.T) { + shellBytes, err := os.ReadFile(filepath.Join("..", "scripts", "install.sh")) + if err != nil { + t.Fatal(err) + } + shell := string(shellBytes) + migrateIndex := strings.LastIndex(shell, "run_home_migration \"$FOUND\"") + installIndex := strings.LastIndex(shell, "install_file \"$FOUND\" \"$TARGET\"") + if migrateIndex < 0 || installIndex < 0 || migrateIndex >= installIndex { + t.Fatal("install.sh must invoke migration from the staged binary before creating the install destination") + } + if !strings.Contains(shell, `REPO="tidbcloud/ti-cli"`) { + t.Fatal("install.sh must download releases from tidbcloud/ti-cli") + } + + powerShellBytes, err := os.ReadFile(filepath.Join("..", "scripts", "install.ps1")) + if err != nil { + t.Fatal(err) + } + powerShell := string(powerShellBytes) + migrateIndex = strings.LastIndex(powerShell, "Invoke-HomeMigration $Extracted.FullName") + installIndex = strings.LastIndex(powerShell, "New-Item -ItemType Directory -Force -Path $InstallDir") + if migrateIndex < 0 || installIndex < 0 || migrateIndex >= installIndex { + t.Fatal("install.ps1 must invoke migration from the staged binary before creating the install destination") + } + if !strings.Contains(powerShell, `$Repo = "tidbcloud/ti-cli"`) { + t.Fatal("install.ps1 must download releases from tidbcloud/ti-cli") + } +} + +func TestUnixInstallerMigratesLegacyStateAndPreservesPathShadow(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("shell installer is not supported on Windows") + } + bin := tiBinary(t) + root := t.TempDir() + home := filepath.Join(root, "home") + legacyHome := filepath.Join(home, ".tdc") + writeE2EFile(t, filepath.Join(legacyHome, "config"), "[default]\nregion_code = 'aws-us-east-1'\n", 0o644) + writeE2EFile(t, filepath.Join(legacyHome, "credentials"), "[default]\ntdc_public_key = 'public'\ntdc_private_key = 'private'\n", 0o600) + + assetDir := filepath.Join(root, "assets") + packageDir := filepath.Join(root, "package") + if err := os.MkdirAll(packageDir, 0o700); err != nil { + t.Fatal(err) + } + packagedTI := filepath.Join(packageDir, "ti") + copyTestFile(t, bin, packagedTI, 0o755) + artifact := fmt.Sprintf("ti_%s_%s.tar.gz", runtime.GOOS, runtime.GOARCH) + archive := filepath.Join(assetDir, artifact) + if err := os.MkdirAll(assetDir, 0o700); err != nil { + t.Fatal(err) + } + tarCommand := exec.Command("tar", "-czf", archive, "-C", packageDir, "ti") + if output, err := tarCommand.CombinedOutput(); err != nil { + t.Fatalf("build installer fixture archive: %v\n%s", err, output) + } + archiveData, err := os.ReadFile(archive) + if err != nil { + t.Fatal(err) + } + archiveChecksum := sha256.Sum256(archiveData) + writeE2EFile(t, filepath.Join(assetDir, "ti_checksums.txt"), fmt.Sprintf("%x %s\n", archiveChecksum, artifact), 0o600) + + companionArtifact := fmt.Sprintf("drive9-%s-%s", runtime.GOOS, runtime.GOARCH) + companion := filepath.Join(assetDir, companionArtifact) + writeE2EFile(t, companion, "#!/bin/sh\nexit 0\n", 0o755) + companionData, err := os.ReadFile(companion) + if err != nil { + t.Fatal(err) + } + companionChecksum := sha256.Sum256(companionData) + writeE2EFile(t, filepath.Join(assetDir, "drive9_checksums.txt"), fmt.Sprintf("%x %s\n", companionChecksum, companionArtifact), 0o600) + + pathDir := filepath.Join(root, "path") + shadow := filepath.Join(pathDir, "ti") + writeE2EFile(t, shadow, "#!/bin/sh\nprintf 'unrelated ti\\n'\n", 0o755) + fakeCurl := filepath.Join(pathDir, "curl") + writeE2EFile(t, fakeCurl, fmt.Sprintf(`#!/bin/sh +out='' +url='' +while [ "$#" -gt 0 ]; do + case "$1" in + -o) out="$2"; shift 2 ;; + -*) shift ;; + *) url="$1"; shift ;; + esac +done +case "$url" in + */ti_checksums.txt) source=%q ;; + */%s) source=%q ;; + */checksums.txt) source=%q ;; + */%s) source=%q ;; + */drive9-regions.json) printf '{"regions":[]}'; exit 0 ;; + *) printf 'unexpected URL: %%s\n' "$url" >&2; exit 1 ;; +esac +if [ -n "$out" ]; then + cp "$source" "$out" +else + cat "$source" +fi +`, filepath.Join(assetDir, "ti_checksums.txt"), artifact, archive, filepath.Join(assetDir, "drive9_checksums.txt"), companionArtifact, companion), 0o755) + + script := filepath.Join("..", "scripts", "install.sh") + command := exec.Command("sh", script, "--yes") + command.Env = []string{ + "HOME=" + home, + "PATH=" + pathDir + string(os.PathListSeparator) + os.Getenv("PATH"), + } + output, err := command.CombinedOutput() + if err != nil { + t.Fatalf("install fixture release: %v\n%s", err, output) + } + if !strings.Contains(string(output), "PATH shadowing detected") || !strings.Contains(string(output), shadow) { + t.Fatalf("installer did not report the unrelated ti on PATH:\n%s", output) + } + if data, err := os.ReadFile(shadow); err != nil || !strings.Contains(string(data), "unrelated ti") { + t.Fatalf("installer replaced PATH shadow: %q, %v", data, err) + } + for _, installed := range []string{ + filepath.Join(home, ".ti", "bin", "ti"), + filepath.Join(home, ".ti", "bin", "ti-drive9"), + } { + if info, err := os.Stat(installed); err != nil || info.Mode()&0o111 == 0 { + t.Fatalf("missing installed executable %s: %v", installed, err) + } + } + migratedCredentials, err := os.ReadFile(filepath.Join(home, ".ti", "credentials")) + if err != nil || !strings.Contains(string(migratedCredentials), "tidb_cloud_public_key") || strings.Contains(string(migratedCredentials), "tdc_public_key") { + t.Fatalf("legacy credentials were not migrated canonically: %q, %v", migratedCredentials, err) + } + legacyCredentials, err := os.ReadFile(filepath.Join(legacyHome, "credentials")) + if err != nil || !strings.Contains(string(legacyCredentials), "tdc_public_key") { + t.Fatalf("installer modified legacy credentials: %q, %v", legacyCredentials, err) + } +} + +func copyTestFile(t *testing.T, source, destination string, mode os.FileMode) { + t.Helper() + data, err := os.ReadFile(source) + if err != nil { + t.Fatal(err) + } + writeE2EFile(t, destination, string(data), mode) +} + func TestInstallersDoNotEscalatePrivileges(t *testing.T) { shellBytes, err := os.ReadFile(filepath.Join("..", "scripts", "install.sh")) if err != nil { @@ -60,8 +241,8 @@ func TestInstallersDoNotEscalatePrivileges(t *testing.T) { t.Fatal(err) } powerShell := string(powerShellBytes) - if !strings.Contains(powerShell, `$DefaultInstallDir = Join-Path (Join-Path $HOME ".tdc") "bin"`) { - t.Fatalf("install.ps1 should default to $HOME\\.tdc\\bin") + if !strings.Contains(powerShell, `$DefaultInstallDir = Join-Path (Join-Path $HOME ".ti") "bin"`) { + t.Fatalf("install.ps1 should default to $HOME\\.ti\\bin") } } @@ -77,10 +258,10 @@ func TestInstallersExplainTelemetryControls(t *testing.T) { content := string(data) for _, want := range []string{ "anonymous command usage and reliability telemetry", - "~/.tdc/.preferences", + "~/.ti/.preferences", "[telemetry]", "enabled = false", - "TDC_TELEMETRY=off tdc ...", + "TI_TELEMETRY=off ti ...", } { if !strings.Contains(strings.ToLower(content), strings.ToLower(want)) { t.Fatalf("%s does not contain telemetry notice text %q", path, want) diff --git a/e2e/live_test.go b/e2e/live_test.go index 0a86ab9..94943d6 100644 --- a/e2e/live_test.go +++ b/e2e/live_test.go @@ -13,14 +13,14 @@ import ( "testing" "time" - "github.com/tidbcloud/tdc/internal/api" - "github.com/tidbcloud/tdc/internal/api/endpoints" - apifs "github.com/tidbcloud/tdc/internal/api/fs" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/auth" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/fs/fscred" + "github.com/tidbcloud/ti-cli/internal/api" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + apifs "github.com/tidbcloud/ti-cli/internal/api/fs" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/auth" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/fs/fscred" ) const defaultLiveProfile = "live-e2e" @@ -42,10 +42,10 @@ func TestMain(m *testing.M) { func TestLiveProfileConfigured(t *testing.T) { requireLive(t) - bin := tdcBinary(t) - version := runTDC(t, bin, "--version") + bin := tiBinary(t) + version := runTI(t, bin, "--version") version.wantExitCode(0) - version.wantStdoutContains("tdc ") + version.wantStdoutContains("ti ") profile := liveProfile(t) if profile.CloudProvider == "" || profile.RegionCode == "" { @@ -85,10 +85,10 @@ func TestLiveOrganizationAPIReadOnlyProbes(t *testing.T) { func TestLiveFSResourceRegistryLifecycle(t *testing.T) { requireLive(t) - bin := tdcBinary(t) + bin := tiBinary(t) profileName := liveProfileName(t) suffix := fmt.Sprintf("%s-%d", time.Now().UTC().Format("20060102150405"), os.Getpid()) - names := []string{"tdc-e2e-fs-" + suffix + "-a", "tdc-e2e-fs-" + suffix + "-b"} + names := []string{"ti-e2e-fs-" + suffix + "-a", "ti-e2e-fs-" + suffix + "-b"} created := make(map[string]bool, len(names)) defer func() { for i := len(names) - 1; i >= 0; i-- { @@ -96,37 +96,37 @@ func TestLiveFSResourceRegistryLifecycle(t *testing.T) { if !created[name] { continue } - result := runTDC(t, bin, "--profile", profileName, "fs", "delete-file-system", "--file-system-name", name) + result := runTI(t, bin, "--profile", profileName, "fs", "delete-file-system", "--file-system-name", name) if result.exitCode != 0 { - t.Logf("cleanup delete failed for tdc fs resource %q: exit=%d stdout=%s stderr=%s", name, result.exitCode, result.stdout, result.stderr) + t.Logf("cleanup delete failed for ti fs resource %q: exit=%d stdout=%s stderr=%s", name, result.exitCode, result.stdout, result.stderr) } } }() for i, name := range names { - create := runTDC(t, bin, "--profile", profileName, "fs", "create-file-system", "--file-system-name", name, "--wait") + create := runTI(t, bin, "--profile", profileName, "fs", "create-file-system", "--file-system-name", name, "--wait") if create.exitCode != 0 { if isLiveFSQuotaError(create.stderr) { if i == 0 { - t.Skipf("tdc fs live registry lifecycle requires one free Starter slot: %s", strings.TrimSpace(create.stderr)) + t.Skipf("ti fs live registry lifecycle requires one free Starter slot: %s", strings.TrimSpace(create.stderr)) } - t.Logf("second tdc fs resource could not be created because Starter quota is full; single-resource live flow completed and multi-resource selection remains covered by the fake-companion e2e: %s", strings.TrimSpace(create.stderr)) - check := runTDC(t, bin, "--profile", profileName, "fs", "check-file-system", "--file-system-name", names[0]) + t.Logf("second ti fs resource could not be created because Starter quota is full; single-resource live flow completed and multi-resource selection remains covered by the fake-companion e2e: %s", strings.TrimSpace(create.stderr)) + check := runTI(t, bin, "--profile", profileName, "fs", "check-file-system", "--file-system-name", names[0]) check.wantExitCode(0) check.wantStdoutContains(`"status": "passed"`) return } - create.fail("create live tdc fs registry resource") + create.fail("create live ti fs registry resource") } if strings.Contains(create.stdout, `"status": "exists"`) { - create.fail("generated live tdc fs resource name already existed; refusing to delete a resource not created by this test") + create.fail("generated live ti fs resource name already existed; refusing to delete a resource not created by this test") } created[name] = true create.wantStdoutContains(`"credentials_stored": true`) create.wantStdoutContains(`"status": "ready"`) } - list := runTDC(t, bin, "--profile", profileName, "fs", "list-file-systems") + list := runTI(t, bin, "--profile", profileName, "fs", "list-file-systems") list.wantExitCode(0) for _, name := range names { list.wantStdoutContains(`"file_system_name": "` + name + `"`) @@ -134,26 +134,26 @@ func TestLiveFSResourceRegistryLifecycle(t *testing.T) { list.wantStdoutNotContains("default_file_system_name") list.wantStdoutNotContains("is_default") - missingSelector := runTDCWithInput(t, bin, "", []string{"TDC_FS_FILE_SYSTEM_NAME="}, "--profile", profileName, "fs", "check-file-system") + missingSelector := runTIWithInput(t, bin, "", []string{"TI_FS_FILE_SYSTEM_NAME="}, "--profile", profileName, "fs", "check-file-system") missingSelector.wantExitCode(2) - missingSelector.wantStderrContains("file system name is required; pass --file-system-name or set TDC_FS_FILE_SYSTEM_NAME") - environmentCheck := runTDCWithInput(t, bin, "", []string{"TDC_FS_FILE_SYSTEM_NAME=" + names[0]}, "--profile", profileName, "fs", "check-file-system") + missingSelector.wantStderrContains("file system name is required; pass --file-system-name or set TI_FS_FILE_SYSTEM_NAME") + environmentCheck := runTIWithInput(t, bin, "", []string{"TI_FS_FILE_SYSTEM_NAME=" + names[0]}, "--profile", profileName, "fs", "check-file-system") environmentCheck.wantExitCode(0) environmentCheck.wantStdoutContains(`"file_system_name": "` + names[0] + `"`) - explicitCheck := runTDC(t, bin, "--profile", profileName, "fs", "check-file-system", "--file-system-name", names[1]) + explicitCheck := runTI(t, bin, "--profile", profileName, "fs", "check-file-system", "--file-system-name", names[1]) explicitCheck.wantExitCode(0) explicitCheck.wantStdoutContains(`"file_system_name": "` + names[1] + `"`) - deleteFirst := runTDC(t, bin, "--profile", profileName, "fs", "delete-file-system", "--file-system-name", names[0]) + deleteFirst := runTI(t, bin, "--profile", profileName, "fs", "delete-file-system", "--file-system-name", names[0]) deleteFirst.wantExitCode(0) deleteFirst.wantStdoutContains(`"status": "deleting"`) deleteFirst.wantStdoutContains(`"remote_deletion_state": "deleting"`) created[names[0]] = false - remaining := runTDC(t, bin, "--profile", profileName, "fs", "describe-file-system", "--file-system-name", names[1]) + remaining := runTI(t, bin, "--profile", profileName, "fs", "describe-file-system", "--file-system-name", names[1]) remaining.wantExitCode(0) remaining.wantStdoutContains(`"file_system_name": "` + names[1] + `"`) - deleteSecond := runTDC(t, bin, "--profile", profileName, "fs", "delete-file-system", "--file-system-name", names[1]) + deleteSecond := runTI(t, bin, "--profile", profileName, "fs", "delete-file-system", "--file-system-name", names[1]) deleteSecond.wantExitCode(0) deleteSecond.wantStdoutContains(`"status": "deleting"`) deleteSecond.wantStdoutContains(`"remote_deletion_state": "deleting"`) @@ -162,10 +162,10 @@ func TestLiveFSResourceRegistryLifecycle(t *testing.T) { func TestLiveCLICommandSurface(t *testing.T) { requireLive(t) - bin := tdcBinary(t) + bin := tiBinary(t) testLiveHelpCommands(t, bin, [][]string{{"help"}, {"update", "help"}}) - checkUpdateHelp := runTDC(t, bin, "update", "help") + checkUpdateHelp := runTI(t, bin, "update", "help") checkUpdateHelp.wantExitCode(0) checkUpdateHelp.wantStdoutContains("--check") checkUpdateHelp.wantStdoutContains("--fail-if-update-available") @@ -174,12 +174,12 @@ func TestLiveCLICommandSurface(t *testing.T) { func TestLiveOrganizationCommandSurface(t *testing.T) { requireLive(t) - bin := tdcBinary(t) + bin := tiBinary(t) profileName := liveProfileName(t) testLiveHelpCommands(t, bin, [][]string{{"organization", "help"}}) testLiveReadOnlyDryRunRejections(t, bin, profileName, [][]string{{"organization", "list-projects"}}) - projects := runTDC(t, bin, "--profile", profileName, "organization", "list-projects", "--page-size", "1") + projects := runTI(t, bin, "--profile", profileName, "organization", "list-projects", "--page-size", "1") projects.wantExitCode(0) projects.wantStdoutContains(`"projects"`) var projectList struct { @@ -197,11 +197,11 @@ func TestLiveOrganizationCommandSurface(t *testing.T) { t.Fatalf("expected live profile %q to see at least one project with an id and type:\n%s", profileName, projects.stdout) } - query := runTDC(t, bin, "--profile", profileName, "organization", "list-projects", "--page-size", "1", "--query", "projects[0].id") + query := runTI(t, bin, "--profile", profileName, "organization", "list-projects", "--page-size", "1", "--query", "projects[0].id") query.wantExitCode(0) query.wantStdoutContains(projectList.Projects[0].ID) - text := runTDC(t, bin, "--profile", profileName, "organization", "list-projects", "--page-size", "1", "--output", "text") + text := runTI(t, bin, "--profile", profileName, "organization", "list-projects", "--page-size", "1", "--output", "text") text.wantExitCode(0) text.wantStdoutContains("ID") text.wantStdoutContains("TYPE") @@ -211,7 +211,7 @@ func TestLiveOrganizationCommandSurface(t *testing.T) { func TestLiveDBCommandSurface(t *testing.T) { requireLive(t) - bin := tdcBinary(t) + bin := tiBinary(t) profileName := liveProfileName(t) profile := liveProfile(t) expectedRegionName := "regions/" + endpoints.APIProvider(profile.CloudProvider) + "-" + profile.RegionCode @@ -235,7 +235,7 @@ func TestLiveDBCommandSurface(t *testing.T) { {"db", "execute-sql-statement"}, }) - clusters := runTDC(t, bin, "--profile", profileName, "db", "list-db-clusters", "--page-size", "1") + clusters := runTI(t, bin, "--profile", profileName, "db", "list-db-clusters", "--page-size", "1") clusters.wantExitCode(0) clusters.wantStdoutContains(`"clusters"`) var clusterList struct { @@ -256,15 +256,15 @@ func TestLiveDBCommandSurface(t *testing.T) { } } - clusterQuery := runTDC(t, bin, "--profile", profileName, "db", "list-db-clusters", "--page-size", "1", "--query", "clusters[].id") + clusterQuery := runTI(t, bin, "--profile", profileName, "db", "list-db-clusters", "--page-size", "1", "--query", "clusters[].id") clusterQuery.wantExitCode(0) - clusterText := runTDC(t, bin, "--profile", profileName, "db", "list-db-clusters", "--page-size", "1", "--output", "text") + clusterText := runTI(t, bin, "--profile", profileName, "db", "list-db-clusters", "--page-size", "1", "--output", "text") clusterText.wantExitCode(0) clusterText.wantStdoutContains("ID") if len(clusterList.Clusters) > 0 && clusterList.Clusters[0].ID != "" { - describe := runTDC(t, bin, "--profile", profileName, "db", "describe-db-cluster", "--db-cluster-id", clusterList.Clusters[0].ID) + describe := runTI(t, bin, "--profile", profileName, "db", "describe-db-cluster", "--db-cluster-id", clusterList.Clusters[0].ID) describe.wantExitCode(0) describe.wantStdoutContains(`"id"`) describe.wantStdoutContains(clusterList.Clusters[0].ID) @@ -273,7 +273,7 @@ func TestLiveDBCommandSurface(t *testing.T) { func TestLiveFSCommandSurface(t *testing.T) { requireLive(t) - bin := tdcBinary(t) + bin := tiBinary(t) profileName := liveProfileName(t) fileSystemName := liveFileSystemName(t) ensureLiveFSResource(t, bin, profileName) @@ -300,11 +300,11 @@ func TestLiveFSCommandSurface(t *testing.T) { {"fs", "create-layer", "--layer-id", "layer-1", "--base-root-path", "/workspace", "--layer-name", "dev"}, {"fs", "create-layer-checkpoint", "--layer-id", "layer-1", "--checkpoint-id", "cp-1"}, {"fs", "rollback-layer", "--layer-id", "layer-1"}, {"fs", "commit-layer", "--layer-id", "layer-1"}, - {"fs", "pack-file-system", "--local-root", "/tmp/tdc-e2e-pack", "--remote-root", "/workspace", "--mount-profile", "portable"}, - {"fs", "unpack-file-system", "--local-root", "/tmp/tdc-e2e-pack", "--remote-root", "/workspace", "--mount-profile", "portable"}, - {"fs", "mount-file-system", "--mount-path", "/tmp/tdc-e2e-mount", "--driver", "webdav"}, + {"fs", "pack-file-system", "--local-root", "/tmp/ti-e2e-pack", "--remote-root", "/workspace", "--mount-profile", "portable"}, + {"fs", "unpack-file-system", "--local-root", "/tmp/ti-e2e-pack", "--remote-root", "/workspace", "--mount-profile", "portable"}, + {"fs", "mount-file-system", "--mount-path", "/tmp/ti-e2e-mount", "--driver", "webdav"}, }, "remote_mutation") - unmountDryRun := runTDC(t, bin, "--profile", profileName, "fs", "unmount-file-system", "--mount-path", "/tmp/tdc-e2e-mount", "--ignore-absent", "--dry-run", "--query", "checks[].name") + unmountDryRun := runTI(t, bin, "--profile", profileName, "fs", "unmount-file-system", "--mount-path", "/tmp/ti-e2e-mount", "--ignore-absent", "--dry-run", "--query", "checks[].name") unmountDryRun.wantExitCode(0) for _, check := range []string{"input_validation", "mount_locator", "remote_mutation"} { unmountDryRun.wantStdoutContains(check) @@ -323,7 +323,7 @@ func TestLiveFSCommandSurface(t *testing.T) { {"fs", "unset-default-file-system"}, {"fs", "create-file-system", "--file-system-name", "removed-flag", "--set-default"}, } { - result := runTDC(t, bin, append([]string{"--profile", profileName}, args...)...) + result := runTI(t, bin, append([]string{"--profile", profileName}, args...)...) result.wantExitCode(2) } testLiveReadOnlyDryRunRejections(t, bin, profileName, [][]string{ @@ -336,7 +336,7 @@ func TestLiveFSCommandSurface(t *testing.T) { func TestLiveFSVaultCommandSurface(t *testing.T) { requireLive(t) - bin := tdcBinary(t) + bin := tiBinary(t) profileName := liveProfileName(t) ensureLiveFSResource(t, bin, profileName) testLiveHelpCommands(t, bin, [][]string{ @@ -348,14 +348,14 @@ func TestLiveFSVaultCommandSurface(t *testing.T) { {"fs-vault", "mount-vault", "help"}, {"fs-vault", "unmount-vault", "help"}, }) testLiveMutatingDryRuns(t, bin, profileName, [][]string{ - {"fs-vault", "create-secret", "--secret-name", "tdc-e2e-secret", "--field", "DB_URL=mysql://example"}, - {"fs-vault", "replace-secret", "--secret-path", "/n/vault/tdc-e2e-secret", "--from-directory", "/tmp"}, - {"fs-vault", "delete-secret", "--secret-name", "tdc-e2e-secret"}, - {"fs-vault", "create-grant", "--agent-id", "tdc-live-e2e", "--scope", "tdc-e2e-secret/DB_URL", "--permission", "read", "--ttl", "10m"}, + {"fs-vault", "create-secret", "--secret-name", "ti-e2e-secret", "--field", "DB_URL=mysql://example"}, + {"fs-vault", "replace-secret", "--secret-path", "/n/vault/ti-e2e-secret", "--from-directory", "/tmp"}, + {"fs-vault", "delete-secret", "--secret-name", "ti-e2e-secret"}, + {"fs-vault", "create-grant", "--agent-id", "ti-live-e2e", "--scope", "ti-e2e-secret/DB_URL", "--permission", "read", "--ttl", "10m"}, {"fs-vault", "delete-grant", "--grant-id", "grant-1"}, - {"fs-vault", "mount-vault", "--mount-path", "/tmp/tdc-e2e-vault"}, + {"fs-vault", "mount-vault", "--mount-path", "/tmp/ti-e2e-vault"}, }, "remote_mutation") - unmountDryRun := runTDC(t, bin, "--profile", profileName, "fs-vault", "unmount-vault", "--mount-path", "/tmp/tdc-e2e-vault", "--ignore-absent", "--dry-run", "--query", "checks[].name") + unmountDryRun := runTI(t, bin, "--profile", profileName, "fs-vault", "unmount-vault", "--mount-path", "/tmp/ti-e2e-vault", "--ignore-absent", "--dry-run", "--query", "checks[].name") unmountDryRun.wantExitCode(0) for _, check := range []string{"input_validation", "mount_locator", "remote_mutation"} { unmountDryRun.wantStdoutContains(check) @@ -368,7 +368,7 @@ func TestLiveFSVaultCommandSurface(t *testing.T) { func TestLiveFSJournalCommandSurface(t *testing.T) { requireLive(t) - bin := tdcBinary(t) + bin := tiBinary(t) profileName := liveProfileName(t) ensureLiveFSResource(t, bin, profileName) testLiveHelpCommands(t, bin, [][]string{ @@ -377,8 +377,8 @@ func TestLiveFSJournalCommandSurface(t *testing.T) { {"fs-journal", "search-journal-entries", "help"}, {"fs-journal", "verify-journal", "help"}, }) testLiveMutatingDryRuns(t, bin, profileName, [][]string{ - {"fs-journal", "create-journal", "--journal-id", "jrn-tdc-e2e", "--journal-kind", "agent"}, - {"fs-journal", "append-journal-entries", "--journal-id", "jrn-tdc-e2e", "--entry-json", `{"type":"task.started"}`}, + {"fs-journal", "create-journal", "--journal-id", "jrn-ti-e2e", "--journal-kind", "agent"}, + {"fs-journal", "append-journal-entries", "--journal-id", "jrn-ti-e2e", "--entry-json", `{"type":"task.started"}`}, }, "remote_mutation") testLiveReadOnlyDryRunRejections(t, bin, profileName, [][]string{ {"fs-journal", "read-journal-entries"}, {"fs-journal", "search-journal-entries"}, @@ -388,7 +388,7 @@ func TestLiveFSJournalCommandSurface(t *testing.T) { func TestLiveFSGitCommandSurface(t *testing.T) { requireLive(t) - testLiveHelpCommands(t, tdcBinary(t), [][]string{ + testLiveHelpCommands(t, tiBinary(t), [][]string{ {"fs-git", "help"}, {"fs-git", "clone-git-workspace", "help"}, {"fs-git", "hydrate-git-workspace", "help"}, {"fs-git", "add-git-worktree", "help"}, {"fs-git", "remove-git-worktree", "help"}, @@ -398,17 +398,17 @@ func TestLiveFSGitCommandSurface(t *testing.T) { func TestLiveFSGitLifecycle(t *testing.T) { requireLive(t) if runtime.GOOS != "darwin" && runtime.GOOS != "linux" { - t.Skip("tdc fs-git live e2e requires a FUSE-capable macOS or Linux host") + t.Skip("ti fs-git live e2e requires a FUSE-capable macOS or Linux host") } if _, err := exec.LookPath("git"); err != nil { - t.Skip("tdc fs-git live e2e requires git") + t.Skip("ti fs-git live e2e requires git") } - bin := tdcBinary(t) + bin := tiBinary(t) profileName := liveProfileName(t) selected := ensureLiveFSResource(t, bin, profileName) suffix := fmt.Sprintf("%s-%d", time.Now().UTC().Format("20060102150405"), os.Getpid()) - remoteRoot := "/tdc-e2e-git-" + suffix + remoteRoot := "/ti-e2e-git-" + suffix mountPath := filepath.Join(t.TempDir(), "mount") if err := os.MkdirAll(mountPath, 0o755); err != nil { t.Fatalf("create fs-git mount path: %v", err) @@ -417,53 +417,53 @@ func TestLiveFSGitLifecycle(t *testing.T) { remoteDeleted := false defer func() { if !unmounted { - cleanupUnmount := runTDC(t, bin, "--profile", profileName, "fs", "unmount-file-system", "--mount-path", mountPath, "--ignore-absent", "--force") + cleanupUnmount := runTI(t, bin, "--profile", profileName, "fs", "unmount-file-system", "--mount-path", mountPath, "--ignore-absent", "--force") if cleanupUnmount.exitCode != 0 { t.Logf("cleanup fs-git unmount failed for %s: exit=%d stdout=%s stderr=%s", mountPath, cleanupUnmount.exitCode, cleanupUnmount.stdout, cleanupUnmount.stderr) } } if !remoteDeleted { - cleanupRemote := runTDC(t, bin, "--profile", profileName, "fs", "delete-file", "--path", remoteRoot, "--recursive") + cleanupRemote := runTI(t, bin, "--profile", profileName, "fs", "delete-file", "--path", remoteRoot, "--recursive") if cleanupRemote.exitCode != 0 && cleanupRemote.exitCode != 5 { t.Logf("cleanup fs-git remote path failed for %s: exit=%d stdout=%s stderr=%s", remoteRoot, cleanupRemote.exitCode, cleanupRemote.stdout, cleanupRemote.stderr) } } }() - createDir := runTDC(t, bin, "--profile", profileName, "fs", "create-directory", "--path", remoteRoot, "--mode", "0755") + createDir := runTI(t, bin, "--profile", profileName, "fs", "create-directory", "--path", remoteRoot, "--mode", "0755") createDir.wantExitCode(0) - mount := runTDC(t, bin, "--profile", profileName, "fs", "mount-file-system", "--mount-path", mountPath, "--remote-path", remoteRoot, "--driver", "fuse", "--ready-timeout", "30s") + mount := runTI(t, bin, "--profile", profileName, "fs", "mount-file-system", "--mount-path", mountPath, "--remote-path", remoteRoot, "--driver", "fuse", "--ready-timeout", "30s") mount.wantExitCode(0) repoPath := filepath.Join(mountPath, "hello-world") - clone := runTDC(t, bin, "--profile", profileName, "fs-git", "clone-git-workspace", "--repo-url", "https://github.com/octocat/Hello-World.git", "--target-path", repoPath, "--blobless", "--hydrate", "sync") + clone := runTI(t, bin, "--profile", profileName, "fs-git", "clone-git-workspace", "--repo-url", "https://github.com/octocat/Hello-World.git", "--target-path", repoPath, "--blobless", "--hydrate", "sync") clone.wantExitCode(0) clone.wantStdoutContains(`"operation": "clone_git_workspace"`) if _, err := os.Stat(filepath.Join(repoPath, ".git")); err != nil { t.Fatalf("cloned fs-git workspace is missing .git: %v", err) } - hydrate := runTDCWithInput(t, bin, "", liveFSTokenEnv(selected, t.TempDir()), "fs-git", "hydrate-git-workspace", "--target-path", repoPath, "--timeout", "5m") + hydrate := runTIWithInput(t, bin, "", liveFSTokenEnv(selected, t.TempDir()), "fs-git", "hydrate-git-workspace", "--target-path", repoPath, "--timeout", "5m") hydrate.wantExitCode(0) hydrate.wantStdoutContains(`"operation": "hydrate_git_workspace"`) worktreePath := filepath.Join(mountPath, "hello-world-worktree") - branchName := "tdc-e2e-" + suffix - addWorktree := runTDC(t, bin, "--profile", profileName, "fs-git", "add-git-worktree", "--base-path", repoPath, "--worktree-path", worktreePath, "--branch-name", branchName, "--hydrate", "sync") + branchName := "ti-e2e-" + suffix + addWorktree := runTI(t, bin, "--profile", profileName, "fs-git", "add-git-worktree", "--base-path", repoPath, "--worktree-path", worktreePath, "--branch-name", branchName, "--hydrate", "sync") addWorktree.wantExitCode(0) addWorktree.wantStdoutContains(`"operation": "add_git_worktree"`) if _, err := os.Stat(filepath.Join(worktreePath, ".git")); err != nil { t.Fatalf("fs-git linked worktree is missing .git: %v", err) } - removeWorktree := runTDC(t, bin, "--profile", profileName, "fs-git", "remove-git-worktree", "--worktree-path", worktreePath, "--force") + removeWorktree := runTI(t, bin, "--profile", profileName, "fs-git", "remove-git-worktree", "--worktree-path", worktreePath, "--force") removeWorktree.wantExitCode(0) removeWorktree.wantStdoutContains(`"status": "removed"`) - unmount := runTDC(t, bin, "--profile", profileName, "fs", "unmount-file-system", "--mount-path", mountPath) + unmount := runTI(t, bin, "--profile", profileName, "fs", "unmount-file-system", "--mount-path", mountPath) unmount.wantExitCode(0) unmounted = true - deleteRoot := runTDC(t, bin, "--profile", profileName, "fs", "delete-file", "--path", remoteRoot, "--recursive") + deleteRoot := runTI(t, bin, "--profile", profileName, "fs", "delete-file", "--path", remoteRoot, "--recursive") deleteRoot.wantExitCode(0) remoteDeleted = true } @@ -471,7 +471,7 @@ func TestLiveFSGitLifecycle(t *testing.T) { func testLiveHelpCommands(t *testing.T, bin string, commands [][]string) { t.Helper() for _, args := range commands { - result := runTDC(t, bin, args...) + result := runTI(t, bin, args...) result.wantExitCode(0) result.wantStdoutContains("Usage:") } @@ -482,7 +482,7 @@ func testLiveMutatingDryRuns(t *testing.T, bin, profileName string, commands [][ for _, args := range commands { fullArgs := append([]string{"--profile", profileName}, args...) fullArgs = append(fullArgs, "--dry-run", "--query", "checks[].name") - result := runTDC(t, bin, fullArgs...) + result := runTI(t, bin, fullArgs...) result.wantExitCode(0) result.wantStdoutContains("config_and_credentials") result.wantStdoutContains("endpoint_selection") @@ -496,7 +496,7 @@ func testLiveReadOnlyDryRunRejections(t *testing.T, bin, profileName string, com for _, args := range commands { fullArgs := append([]string{"--profile", profileName}, args...) fullArgs = append(fullArgs, "--dry-run") - result := runTDC(t, bin, fullArgs...) + result := runTI(t, bin, fullArgs...) result.wantExitCode(2) result.wantStderrContains("unknown flag: --dry-run") } @@ -510,7 +510,7 @@ func resolveLiveFSResource(t *testing.T, profile *config.Profile, name string) * } selected, _, err := fscred.Resolve(home, profile, name, true, nil) if err != nil { - t.Fatalf("resolve live tdc fs resource %q: %v", name, err) + t.Fatalf("resolve live ti fs resource %q: %v", name, err) } return selected } @@ -524,11 +524,11 @@ func isLiveFSQuotaError(message string) bool { func TestLiveFSDataPlaneLifecycle(t *testing.T) { requireLive(t) - bin := tdcBinary(t) + bin := tiBinary(t) profileName := liveProfileName(t) profile := ensureLiveFSResource(t, bin, profileName) suffix := time.Now().UTC().Format("20060102150405") - rootPath := "/tdc-e2e-" + suffix + rootPath := "/ti-e2e-" + suffix sourcePath := rootPath + "/README.md" copyPath := rootPath + "/README.copy.md" movedPath := rootPath + "/README.moved.md" @@ -537,48 +537,48 @@ func TestLiveFSDataPlaneLifecycle(t *testing.T) { if deleted { return } - cleanup := runTDC(t, bin, "--profile", profileName, "fs", "delete-file", "--path", rootPath, "--recursive") + cleanup := runTI(t, bin, "--profile", profileName, "fs", "delete-file", "--path", rootPath, "--recursive") if cleanup.exitCode != 0 && cleanup.exitCode != 5 { t.Logf("cleanup delete failed for %s: exit=%d stdout=%s stderr=%s", rootPath, cleanup.exitCode, cleanup.stdout, cleanup.stderr) } }() - createDir := runTDC(t, bin, "--profile", profileName, "fs", "create-directory", "--path", rootPath, "--mode", "0755") + createDir := runTI(t, bin, "--profile", profileName, "fs", "create-directory", "--path", rootPath, "--mode", "0755") createDir.wantExitCode(0) createDir.wantStdoutContains(`"status": "created"`) - content := "hello tdc fs live e2e " + suffix + "\n" + content := "hello ti fs live e2e " + suffix + "\n" localFile := filepath.Join(t.TempDir(), "README.md") if err := os.WriteFile(localFile, []byte(content), 0o644); err != nil { t.Fatalf("write local test file: %v", err) } - upload := runTDC(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", localFile, "--to-remote", sourcePath) + upload := runTI(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", localFile, "--to-remote", sourcePath) upload.wantExitCode(0) upload.wantStdoutContains(`"status": "copied"`) - list := runTDC(t, bin, "--profile", profileName, "fs", "list-files", "--path", rootPath) + list := runTI(t, bin, "--profile", profileName, "fs", "list-files", "--path", rootPath) list.wantExitCode(0) list.wantStdoutContains("README.md") - listText := runTDC(t, bin, "--profile", profileName, "fs", "list-files", "--path", rootPath, "--output", "text") + listText := runTI(t, bin, "--profile", profileName, "fs", "list-files", "--path", rootPath, "--output", "text") listText.wantExitCode(0) listText.wantStdoutContains("NAME") listText.wantStdoutContains("README.md") - describe := runTDC(t, bin, "--profile", profileName, "fs", "describe-file", "--path", sourcePath) + describe := runTI(t, bin, "--profile", profileName, "fs", "describe-file", "--path", sourcePath) describe.wantExitCode(0) describe.wantStdoutContains(`"size_bytes"`) - read := runTDC(t, bin, "--profile", profileName, "fs", "read-file", "--path", sourcePath) + read := runTI(t, bin, "--profile", profileName, "fs", "read-file", "--path", sourcePath) read.wantExitCode(0) if read.stdout != content { read.fail("read-file should return raw file bytes exactly") } - rangeRead := runTDC(t, bin, "--profile", profileName, "fs", "read-file", "--path", sourcePath, "--offset", "6", "--length", "3") + rangeRead := runTI(t, bin, "--profile", profileName, "fs", "read-file", "--path", sourcePath, "--offset", "6", "--length", "3") rangeRead.wantExitCode(0) - if rangeRead.stdout != "tdc" { + if rangeRead.stdout != "ti" { rangeRead.fail("read-file --offset/--length should return the requested byte range") } @@ -587,11 +587,11 @@ func TestLiveFSDataPlaneLifecycle(t *testing.T) { if err := os.WriteFile(appendFile, []byte(appendText), 0o644); err != nil { t.Fatalf("write append file: %v", err) } - appendRemote := runTDC(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", appendFile, "--to-remote", sourcePath, "--append") + appendRemote := runTI(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", appendFile, "--to-remote", sourcePath, "--append") appendRemote.wantExitCode(0) appendRemote.wantStdoutContains(`"status": "appended"`) fullContent := content + appendText - readAppended := runTDC(t, bin, "--profile", profileName, "fs", "read-file", "--path", sourcePath) + readAppended := runTI(t, bin, "--profile", profileName, "fs", "read-file", "--path", sourcePath) readAppended.wantExitCode(0) if readAppended.stdout != fullContent { readAppended.fail("read-file should include appended bytes") @@ -599,51 +599,51 @@ func TestLiveFSDataPlaneLifecycle(t *testing.T) { stdinPath := rootPath + "/stdin.txt" stdinContent := "stdin live e2e " + suffix + "\n" - stdinUpload := runTDCWithInput(t, bin, stdinContent, nil, "--profile", profileName, "fs", "copy-file", "--from-stdin", "--to-remote", stdinPath, "--tag", "source=stdin", "--tag", "suite=live-e2e", "--description", "tdc live e2e stdin") + stdinUpload := runTIWithInput(t, bin, stdinContent, nil, "--profile", profileName, "fs", "copy-file", "--from-stdin", "--to-remote", stdinPath, "--tag", "source=stdin", "--tag", "suite=live-e2e", "--description", "ti live e2e stdin") stdinUpload.wantExitCode(0) stdinUpload.wantStdoutContains(`"status": "copied"`) - stdinDownload := runTDC(t, bin, "--profile", profileName, "fs", "copy-file", "--from-remote", stdinPath, "--to-stdout") + stdinDownload := runTI(t, bin, "--profile", profileName, "fs", "copy-file", "--from-remote", stdinPath, "--to-stdout") stdinDownload.wantExitCode(0) if stdinDownload.stdout != stdinContent { stdinDownload.fail("copy-file --to-stdout should return raw file bytes exactly") } taggedPath := rootPath + "/tagged.md" - taggedUpload := runTDC(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", localFile, "--to-remote", taggedPath, "--tag", "source=local", "--tag", "suite=live-e2e", "--description", "tdc live e2e tagged") + taggedUpload := runTI(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", localFile, "--to-remote", taggedPath, "--tag", "source=local", "--tag", "suite=live-e2e", "--description", "ti live e2e tagged") taggedUpload.wantExitCode(0) taggedUpload.wantStdoutContains(`"status": "copied"`) - taggedDescribe := runTDC(t, bin, "--profile", profileName, "fs", "describe-file", "--path", taggedPath) + taggedDescribe := runTI(t, bin, "--profile", profileName, "fs", "describe-file", "--path", taggedPath) taggedDescribe.wantExitCode(0) taggedDescribe.wantStdoutContains(`"source": "local"`) taggedDescribe.wantStdoutContains(`"suite": "live-e2e"`) - chmod := runTDC(t, bin, "--profile", profileName, "fs", "chmod-file", "--path", sourcePath, "--mode", "0600") + chmod := runTI(t, bin, "--profile", profileName, "fs", "chmod-file", "--path", sourcePath, "--mode", "0600") chmod.wantExitCode(0) chmod.wantStdoutContains(`"status": "updated"`) - describeMode := runTDC(t, bin, "--profile", profileName, "fs", "describe-file", "--path", sourcePath) + describeMode := runTI(t, bin, "--profile", profileName, "fs", "describe-file", "--path", sourcePath) describeMode.wantExitCode(0) describeMode.wantStdoutContains(sourcePath) symlinkPath := rootPath + "/README.link.md" - symlink := runTDC(t, bin, "--profile", profileName, "fs", "create-symlink", "--target", "README.md", "--link-path", symlinkPath) + symlink := runTI(t, bin, "--profile", profileName, "fs", "create-symlink", "--target", "README.md", "--link-path", symlinkPath) symlink.wantExitCode(0) symlink.wantStdoutContains(`"status": "created"`) - listSymlink := runTDC(t, bin, "--profile", profileName, "fs", "list-files", "--path", rootPath) + listSymlink := runTI(t, bin, "--profile", profileName, "fs", "list-files", "--path", rootPath) listSymlink.wantExitCode(0) listSymlink.wantStdoutContains("README.link.md") hardlinkPath := rootPath + "/README.hard.md" - hardlink := runTDC(t, bin, "--profile", profileName, "fs", "create-hardlink", "--source-path", sourcePath, "--link-path", hardlinkPath) + hardlink := runTI(t, bin, "--profile", profileName, "fs", "create-hardlink", "--source-path", sourcePath, "--link-path", hardlinkPath) hardlink.wantExitCode(0) hardlink.wantStdoutContains(`"status": "created"`) - readHardlink := runTDC(t, bin, "--profile", profileName, "fs", "read-file", "--path", hardlinkPath) + readHardlink := runTI(t, bin, "--profile", profileName, "fs", "read-file", "--path", hardlinkPath) readHardlink.wantExitCode(0) if readHardlink.stdout != fullContent { readHardlink.fail("hardlink should read the source file contents") } aliasDir := rootPath + "/alias" - aliasMkdir := runTDC(t, bin, "--profile", profileName, "fs", "mkdir", "--path", aliasDir, "--mode", "0755") + aliasMkdir := runTI(t, bin, "--profile", profileName, "fs", "mkdir", "--path", aliasDir, "--mode", "0755") aliasMkdir.wantExitCode(0) aliasMkdir.wantStdoutContains(`"status": "created"`) @@ -653,35 +653,35 @@ func TestLiveFSDataPlaneLifecycle(t *testing.T) { t.Fatalf("write alias local file: %v", err) } aliasPath := aliasDir + "/alias.txt" - aliasUpload := runTDC(t, bin, "--profile", profileName, "fs", "cp", "--from-local", aliasLocalFile, "--to-remote", aliasPath) + aliasUpload := runTI(t, bin, "--profile", profileName, "fs", "cp", "--from-local", aliasLocalFile, "--to-remote", aliasPath) aliasUpload.wantExitCode(0) aliasUpload.wantStdoutContains(`"status": "copied"`) - aliasList := runTDC(t, bin, "--profile", profileName, "fs", "ls", "--path", aliasDir) + aliasList := runTI(t, bin, "--profile", profileName, "fs", "ls", "--path", aliasDir) aliasList.wantExitCode(0) aliasList.wantStdoutContains("alias.txt") - aliasStat := runTDC(t, bin, "--profile", profileName, "fs", "stat", "--path", aliasPath) + aliasStat := runTI(t, bin, "--profile", profileName, "fs", "stat", "--path", aliasPath) aliasStat.wantExitCode(0) aliasStat.wantStdoutContains(`"size_bytes"`) - aliasRead := runTDC(t, bin, "--profile", profileName, "fs", "cat", "--path", aliasPath) + aliasRead := runTI(t, bin, "--profile", profileName, "fs", "cat", "--path", aliasPath) aliasRead.wantExitCode(0) if aliasRead.stdout != aliasContent { aliasRead.fail("cat alias should return raw file bytes exactly") } - aliasChmod := runTDC(t, bin, "--profile", profileName, "fs", "chmod", "--path", aliasPath, "--mode", "0600") + aliasChmod := runTI(t, bin, "--profile", profileName, "fs", "chmod", "--path", aliasPath, "--mode", "0600") aliasChmod.wantExitCode(0) aliasChmod.wantStdoutContains(`"status": "updated"`) aliasSymlinkPath := aliasDir + "/alias.link" - aliasSymlink := runTDC(t, bin, "--profile", profileName, "fs", "symlink", "--target", "alias.txt", "--link-path", aliasSymlinkPath) + aliasSymlink := runTI(t, bin, "--profile", profileName, "fs", "symlink", "--target", "alias.txt", "--link-path", aliasSymlinkPath) aliasSymlink.wantExitCode(0) aliasSymlink.wantStdoutContains(`"status": "created"`) aliasHardlinkPath := aliasDir + "/alias.hard" - aliasHardlink := runTDC(t, bin, "--profile", profileName, "fs", "hardlink", "--source-path", aliasPath, "--link-path", aliasHardlinkPath) + aliasHardlink := runTI(t, bin, "--profile", profileName, "fs", "hardlink", "--source-path", aliasPath, "--link-path", aliasHardlinkPath) aliasHardlink.wantExitCode(0) aliasHardlink.wantStdoutContains(`"status": "created"`) @@ -689,16 +689,16 @@ func TestLiveFSDataPlaneLifecycle(t *testing.T) { waitLiveFSResult(t, bin, []string{"--profile", profileName, "fs", "find", "--path", aliasDir, "--file-name-pattern", "alias.txt", "--limit", "5"}, aliasPath, 2*time.Minute, "find alias file by name") aliasCopyPath := aliasDir + "/alias.copy.txt" - aliasCopy := runTDC(t, bin, "--profile", profileName, "fs", "cp", "--from-remote", aliasPath, "--to-remote", aliasCopyPath) + aliasCopy := runTI(t, bin, "--profile", profileName, "fs", "cp", "--from-remote", aliasPath, "--to-remote", aliasCopyPath) aliasCopy.wantExitCode(0) aliasCopy.wantStdoutContains(`"status": "copied"`) aliasMovedPath := aliasDir + "/alias.moved.txt" - aliasMove := runTDC(t, bin, "--profile", profileName, "fs", "mv", "--from-remote", aliasCopyPath, "--to-remote", aliasMovedPath) + aliasMove := runTI(t, bin, "--profile", profileName, "fs", "mv", "--from-remote", aliasCopyPath, "--to-remote", aliasMovedPath) aliasMove.wantExitCode(0) aliasMove.wantStdoutContains(`"status": "moved"`) - aliasDelete := runTDC(t, bin, "--profile", profileName, "fs", "rm", "--path", aliasMovedPath) + aliasDelete := runTI(t, bin, "--profile", profileName, "fs", "rm", "--path", aliasMovedPath) aliasDelete.wantExitCode(0) aliasDelete.wantStdoutContains(`"status": "deleted"`) @@ -708,10 +708,10 @@ func TestLiveFSDataPlaneLifecycle(t *testing.T) { if err := os.WriteFile(largeFile, []byte(largeContent), 0o644); err != nil { t.Fatalf("write large local file: %v", err) } - largeUpload := runTDC(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", largeFile, "--to-remote", largePath) + largeUpload := runTI(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", largeFile, "--to-remote", largePath) largeUpload.wantExitCode(0) largeUpload.wantStdoutContains(`"status": "copied"`) - readLarge := runTDC(t, bin, "--profile", profileName, "fs", "read-file", "--path", largePath) + readLarge := runTI(t, bin, "--profile", profileName, "fs", "read-file", "--path", largePath) readLarge.wantExitCode(0) if readLarge.stdout != largeContent { readLarge.fail("large multipart upload should preserve file contents") @@ -722,10 +722,10 @@ func TestLiveFSDataPlaneLifecycle(t *testing.T) { if err := os.WriteFile(largeAppendFile, []byte(largeAppendText), 0o644); err != nil { t.Fatalf("write large append file: %v", err) } - largeAppend := runTDC(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", largeAppendFile, "--to-remote", largePath, "--append") + largeAppend := runTI(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", largeAppendFile, "--to-remote", largePath, "--append") largeAppend.wantExitCode(0) largeAppend.wantStdoutContains(`"status": "appended"`) - readLargeAppended := runTDC(t, bin, "--profile", profileName, "fs", "read-file", "--path", largePath) + readLargeAppended := runTI(t, bin, "--profile", profileName, "fs", "read-file", "--path", largePath) readLargeAppended.wantExitCode(0) if readLargeAppended.stdout != largeContent+largeAppendText { readLargeAppended.fail("efficient append should preserve existing and appended bytes") @@ -744,28 +744,28 @@ func TestLiveFSDataPlaneLifecycle(t *testing.T) { defer cancel() _ = fsClient.AbortUpload(ctx, resumeUploadID) }() - resumeUpload := runTDC(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", resumeUploadFile, "--to-remote", resumeUploadPath, "--resume") + resumeUpload := runTI(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", resumeUploadFile, "--to-remote", resumeUploadPath, "--resume") resumeUpload.wantExitCode(0) resumeUpload.wantStdoutContains(`"status": "resumed"`) - readResumeUpload := runTDC(t, bin, "--profile", profileName, "fs", "read-file", "--path", resumeUploadPath) + readResumeUpload := runTI(t, bin, "--profile", profileName, "fs", "read-file", "--path", resumeUploadPath) readResumeUpload.wantExitCode(0) if readResumeUpload.stdout != resumeUploadContent { readResumeUpload.fail("upload resume should preserve uploaded contents") } - layerID := "tdc-e2e-layer-" + suffix + layerID := "ti-e2e-layer-" + suffix checkpointID := layerID + "-cp" layerClosed := false defer func() { if layerClosed { return } - rollback := runTDC(t, bin, "--profile", profileName, "fs", "rollback-layer", "--layer-id", layerID) + rollback := runTI(t, bin, "--profile", profileName, "fs", "rollback-layer", "--layer-id", layerID) if rollback.exitCode != 0 && rollback.exitCode != 5 { t.Logf("cleanup rollback failed for layer %s: exit=%d stdout=%s stderr=%s", layerID, rollback.exitCode, rollback.stdout, rollback.stderr) } }() - createLayer := runTDC( + createLayer := runTI( t, bin, "--profile", profileName, @@ -774,18 +774,18 @@ func TestLiveFSDataPlaneLifecycle(t *testing.T) { "--base-root-path", rootPath, "--layer-name", "live-e2e-"+suffix, "--durability-mode", "restore-safe", - "--actor-id", "tdc-live-e2e", - "--tag", "test=tdc-e2e", + "--actor-id", "ti-live-e2e", + "--tag", "test=ti-e2e", "--tag", "suffix="+suffix, ) createLayer.wantExitCode(0) createLayer.wantStdoutContains(layerID) - listLayers := runTDC(t, bin, "--profile", profileName, "fs", "list-layers", "--query", fmt.Sprintf("layers[?layer_id=='%s'].layer_id | [0]", layerID)) + listLayers := runTI(t, bin, "--profile", profileName, "fs", "list-layers", "--query", fmt.Sprintf("layers[?layer_id=='%s'].layer_id | [0]", layerID)) listLayers.wantExitCode(0) listLayers.wantStdoutContains(layerID) - describeLayer := runTDC(t, bin, "--profile", profileName, "fs", "describe-layer", "--layer-id", layerID) + describeLayer := runTI(t, bin, "--profile", profileName, "fs", "describe-layer", "--layer-id", layerID) describeLayer.wantExitCode(0) describeLayer.wantStdoutContains(layerID) @@ -795,21 +795,21 @@ func TestLiveFSDataPlaneLifecycle(t *testing.T) { if err := os.WriteFile(layerCopyFile, []byte(layerCopyContent), 0o644); err != nil { t.Fatalf("write layer copy file: %v", err) } - copyToLayer := runTDC(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", layerCopyFile, "--to-remote", layerCopyPath, "--layer-id", layerID) + copyToLayer := runTI(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", layerCopyFile, "--to-remote", layerCopyPath, "--layer-id", layerID) copyToLayer.wantExitCode(0) copyToLayer.wantStdoutContains(`"status"`) - diffLayer := runTDC(t, bin, "--profile", profileName, "fs", "diff-layer", "--layer-id", layerID, "--output", "text") + diffLayer := runTI(t, bin, "--profile", profileName, "fs", "diff-layer", "--layer-id", layerID, "--output", "text") diffLayer.wantExitCode(0) diffLayer.wantStdoutContains(layerCopyPath) - createCheckpoint := runTDC(t, bin, "--profile", profileName, "fs", "create-layer-checkpoint", "--layer-id", layerID, "--checkpoint-id", checkpointID, "--label", "live-e2e") + createCheckpoint := runTI(t, bin, "--profile", profileName, "fs", "create-layer-checkpoint", "--layer-id", layerID, "--checkpoint-id", checkpointID, "--label", "live-e2e") createCheckpoint.wantExitCode(0) createCheckpoint.wantStdoutContains(checkpointID) waitLiveFSResult(t, bin, []string{"--profile", profileName, "fs", "find-files", "--path", rootPath, "--file-name-pattern", "copy-layer.txt", "--layer-id", layerID, "--limit", "5"}, layerCopyPath, 2*time.Minute, "find file inside layer") - commitLayer := runTDC(t, bin, "--profile", profileName, "fs", "commit-layer", "--layer-id", layerID) + commitLayer := runTI(t, bin, "--profile", profileName, "fs", "commit-layer", "--layer-id", layerID) commitLayer.wantExitCode(0) commitLayer.wantStdoutContains(`"status"`) layerClosed = true @@ -819,22 +819,22 @@ func TestLiveFSDataPlaneLifecycle(t *testing.T) { func TestLiveFSVaultLifecycle(t *testing.T) { requireLive(t) - bin := tdcBinary(t) + bin := tiBinary(t) profileName := liveProfileName(t) selected := ensureLiveFSResource(t, bin, profileName) suffix := time.Now().UTC().Format("20060102150405") - vaultSecretName := "tdc-e2e-vault-" + suffix + vaultSecretName := "ti-e2e-vault-" + suffix vaultDeleted := false defer func() { if vaultDeleted { return } - cleanup := runTDC(t, bin, "--profile", profileName, "fs-vault", "delete-secret", "--secret-name", vaultSecretName) + cleanup := runTI(t, bin, "--profile", profileName, "fs-vault", "delete-secret", "--secret-name", vaultSecretName) if cleanup.exitCode != 0 && cleanup.exitCode != 5 { t.Logf("cleanup vault secret failed for %s: exit=%d stdout=%s stderr=%s", vaultSecretName, cleanup.exitCode, cleanup.stdout, cleanup.stderr) } }() - createVaultSecret := runTDC( + createVaultSecret := runTI( t, bin, "--profile", profileName, @@ -846,17 +846,17 @@ func TestLiveFSVaultLifecycle(t *testing.T) { createVaultSecret.wantExitCode(0) createVaultSecret.wantStdoutContains(vaultSecretName) - listVaultSecrets := runTDCWithInput(t, bin, "", liveFSTokenEnv(selected, t.TempDir()), "fs-vault", "list-secrets") + listVaultSecrets := runTIWithInput(t, bin, "", liveFSTokenEnv(selected, t.TempDir()), "fs-vault", "list-secrets") listVaultSecrets.wantExitCode(0) listVaultSecrets.wantStdoutContains(vaultSecretName) - readVaultSecret := runTDC(t, bin, "--profile", profileName, "fs-vault", "read-secret", "--secret-name", vaultSecretName, "--field", "PASSWORD", "--format", "raw") + readVaultSecret := runTI(t, bin, "--profile", profileName, "fs-vault", "read-secret", "--secret-name", vaultSecretName, "--field", "PASSWORD", "--format", "raw") readVaultSecret.wantExitCode(0) if readVaultSecret.stdout != "secret-"+suffix { readVaultSecret.fail("vault read-secret --format raw should return exact field bytes") } - readVaultEnv := runTDC(t, bin, "--profile", profileName, "fs-vault", "read-secret", "--secret-name", vaultSecretName, "--format", "env") + readVaultEnv := runTI(t, bin, "--profile", profileName, "fs-vault", "read-secret", "--secret-name", vaultSecretName, "--format", "env") readVaultEnv.wantExitCode(0) readVaultEnv.wantStdoutContains("DB_URL=mysql://" + suffix) readVaultEnv.wantStdoutContains("PASSWORD=secret-" + suffix) @@ -871,26 +871,26 @@ func TestLiveFSVaultLifecycle(t *testing.T) { if err := os.WriteFile(filepath.Join(replaceVaultDir, "PASSWORD"), []byte("replaced-"+suffix), 0o600); err != nil { t.Fatalf("write replacement PASSWORD: %v", err) } - replaceVaultSecret := runTDC(t, bin, "--profile", profileName, "fs-vault", "replace-secret", "--secret-path", "/n/vault/"+vaultSecretName, "--from-directory", replaceVaultDir) + replaceVaultSecret := runTI(t, bin, "--profile", profileName, "fs-vault", "replace-secret", "--secret-path", "/n/vault/"+vaultSecretName, "--from-directory", replaceVaultDir) replaceVaultSecret.wantExitCode(0) replaceVaultSecret.wantStdoutContains(vaultSecretName) - readReplacedVaultSecret := runTDC(t, bin, "--profile", profileName, "fs-vault", "read-secret", "--secret-name", vaultSecretName, "--field", "PASSWORD", "--format", "raw") + readReplacedVaultSecret := runTI(t, bin, "--profile", profileName, "fs-vault", "read-secret", "--secret-name", vaultSecretName, "--field", "PASSWORD", "--format", "raw") readReplacedVaultSecret.wantExitCode(0) if readReplacedVaultSecret.stdout != "replaced-"+suffix { readReplacedVaultSecret.fail("vault replace-secret should replace stored field bytes") } - createVaultGrant := runTDC( + createVaultGrant := runTI( t, bin, "--profile", profileName, "fs-vault", "create-grant", - "--agent-id", "tdc-live-e2e", + "--agent-id", "ti-live-e2e", "--scope", vaultSecretName, "--permission", "read", "--ttl", "10m", - "--label-hint", "tdc-live-e2e-"+suffix, + "--label-hint", "ti-live-e2e-"+suffix, ) createVaultGrant.wantExitCode(0) vaultGrant := decodeLiveVaultToken(t, createVaultGrant) @@ -902,7 +902,7 @@ func TestLiveFSVaultLifecycle(t *testing.T) { if grantDeleted { return } - cleanup := runTDC(t, bin, "--profile", profileName, "fs-vault", "delete-grant", "--grant-id", vaultGrant.GrantID, "--reason", "cleanup") + cleanup := runTI(t, bin, "--profile", profileName, "fs-vault", "delete-grant", "--grant-id", vaultGrant.GrantID, "--reason", "cleanup") if cleanup.exitCode != 0 && cleanup.exitCode != 5 { t.Logf("cleanup vault grant failed for %s: exit=%d stdout=%s stderr=%s", vaultGrant.GrantID, cleanup.exitCode, cleanup.stdout, cleanup.stderr) } @@ -917,44 +917,44 @@ func TestLiveFSVaultLifecycle(t *testing.T) { if vaultUnmounted { return } - cleanupUnmount := runTDC(t, bin, "--profile", profileName, "fs-vault", "unmount-vault", "--mount-path", vaultMountPath, "--ignore-absent", "--force") + cleanupUnmount := runTI(t, bin, "--profile", profileName, "fs-vault", "unmount-vault", "--mount-path", vaultMountPath, "--ignore-absent", "--force") if cleanupUnmount.exitCode != 0 { t.Logf("cleanup vault unmount failed for %s: exit=%d stdout=%s stderr=%s", vaultMountPath, cleanupUnmount.exitCode, cleanupUnmount.stdout, cleanupUnmount.stderr) } }() - mountVault := runTDC(t, bin, "--profile", profileName, "fs-vault", "mount-vault", "--mount-path", vaultMountPath, "--vault-token", vaultGrant.Token, "--ready-timeout", "30s") + mountVault := runTI(t, bin, "--profile", profileName, "fs-vault", "mount-vault", "--mount-path", vaultMountPath, "--vault-token", vaultGrant.Token, "--ready-timeout", "30s") mountVault.wantExitCode(0) mountVault.wantStdoutContains(`"status": "mounted"`) waitLiveLocalFile(t, filepath.Join(vaultMountPath, vaultSecretName, "PASSWORD"), "replaced-"+suffix, 30*time.Second) - unmountVault := runTDC(t, bin, "--profile", profileName, "fs-vault", "unmount-vault", "--mount-path", vaultMountPath) + unmountVault := runTI(t, bin, "--profile", profileName, "fs-vault", "unmount-vault", "--mount-path", vaultMountPath) unmountVault.wantExitCode(0) unmountVault.wantStdoutContains(`"status": "unmounted"`) vaultUnmounted = true } - readVaultWithGrant := runTDC(t, bin, "--profile", profileName, "fs-vault", "read-secret", "--secret-name", vaultSecretName, "--field", "DB_URL", "--format", "raw", "--vault-token", vaultGrant.Token) + readVaultWithGrant := runTI(t, bin, "--profile", profileName, "fs-vault", "read-secret", "--secret-name", vaultSecretName, "--field", "DB_URL", "--format", "raw", "--vault-token", vaultGrant.Token) readVaultWithGrant.wantExitCode(0) if readVaultWithGrant.stdout != "mysql://replaced-"+suffix { readVaultWithGrant.fail("delegated vault grant should read scoped field") } - deleteVaultGrant := runTDC(t, bin, "--profile", profileName, "fs-vault", "delete-grant", "--grant-id", vaultGrant.GrantID, "--reason", "live-e2e-complete") + deleteVaultGrant := runTI(t, bin, "--profile", profileName, "fs-vault", "delete-grant", "--grant-id", vaultGrant.GrantID, "--reason", "live-e2e-complete") deleteVaultGrant.wantExitCode(0) grantDeleted = true if runtime.GOOS != "windows" { if _, err := exec.LookPath("env"); err == nil { - runWithVault := runTDC(t, bin, "--profile", profileName, "fs-vault", "run-with-secret", "--secret-path", "/n/vault/"+vaultSecretName, "--", "env") + runWithVault := runTI(t, bin, "--profile", profileName, "fs-vault", "run-with-secret", "--secret-path", "/n/vault/"+vaultSecretName, "--", "env") runWithVault.wantExitCode(0) runWithVault.wantStdoutContains("DB_URL=mysql://replaced-" + suffix) runWithVault.wantStdoutContains("PASSWORD=replaced-" + suffix) - runWithVault.wantStdoutNotContains("TDC_PRIVATE_KEY=") + runWithVault.wantStdoutNotContains("TIDB_CLOUD_PRIVATE_KEY=") } } - listVaultAuditEvents := runTDC(t, bin, "--profile", profileName, "fs-vault", "list-audit-events", "--secret-name", vaultSecretName, "--limit", "20") + listVaultAuditEvents := runTI(t, bin, "--profile", profileName, "fs-vault", "list-audit-events", "--secret-name", vaultSecretName, "--limit", "20") listVaultAuditEvents.wantExitCode(0) listVaultAuditEvents.wantStdoutContains(`"events"`) - deleteVaultSecret := runTDC(t, bin, "--profile", profileName, "fs-vault", "delete-secret", "--secret-name", vaultSecretName) + deleteVaultSecret := runTI(t, bin, "--profile", profileName, "fs-vault", "delete-secret", "--secret-name", vaultSecretName) deleteVaultSecret.wantExitCode(0) deleteVaultSecret.wantStdoutContains(`"status": "deleted"`) vaultDeleted = true @@ -962,43 +962,43 @@ func TestLiveFSVaultLifecycle(t *testing.T) { func TestLiveFSJournalLifecycle(t *testing.T) { requireLive(t) - bin := tdcBinary(t) + bin := tiBinary(t) profileName := liveProfileName(t) selected := ensureLiveFSResource(t, bin, profileName) suffix := time.Now().UTC().Format("20060102150405") - rootPath := "/tdc-e2e-journal-" + suffix - journalID := "jrn-tdc-e2e-" + suffix - appendID := "app-tdc-e2e-" + suffix - createJournal := runTDC( + rootPath := "/ti-e2e-journal-" + suffix + journalID := "jrn-ti-e2e-" + suffix + appendID := "app-ti-e2e-" + suffix + createJournal := runTI( t, bin, "--profile", profileName, "fs-journal", "create-journal", "--journal-id", journalID, "--journal-kind", "agent", - "--title", "tdc live e2e "+suffix, - "--actor", "agent:tdc-live-e2e", - "--label", "test=tdc-e2e", + "--title", "ti live e2e "+suffix, + "--actor", "agent:ti-live-e2e", + "--label", "test=ti-e2e", "--label", "suffix="+suffix, ) createJournal.wantExitCode(0) createJournal.wantStdoutContains(journalID) - appendJournal := runTDC( + appendJournal := runTI( t, bin, "--profile", profileName, "fs-journal", "append-journal-entries", "--journal-id", journalID, "--idempotency-key", appendID, - "--entry-json", `{"type":"task.started","summary":{"message":"tdc live e2e `+suffix+`"}}`, + "--entry-json", `{"type":"task.started","summary":{"message":"ti live e2e `+suffix+`"}}`, "--subject", "path:"+rootPath, ) appendJournal.wantExitCode(0) appendJournal.wantStdoutContains(`"count": 1`) appendJournal.wantStdoutContains(appendID) - readJournal := runTDCWithInput(t, bin, "", liveFSTokenEnv(selected, t.TempDir()), "fs-journal", "read-journal-entries", "--journal-id", journalID, "--limit", "10") + readJournal := runTIWithInput(t, bin, "", liveFSTokenEnv(selected, t.TempDir()), "fs-journal", "read-journal-entries", "--journal-id", journalID, "--limit", "10") readJournal.wantExitCode(0) readJournal.wantStdoutContains(journalID) readJournal.wantStdoutContains("task.started") @@ -1010,7 +1010,7 @@ func TestLiveFSJournalLifecycle(t *testing.T) { "--profile", profileName, "fs-journal", "search-journal-entries", "--entry-type", "task.started", - "--label", "test=tdc-e2e", + "--label", "test=ti-e2e", "--limit", "10", }, journalID, @@ -1018,26 +1018,26 @@ func TestLiveFSJournalLifecycle(t *testing.T) { "index journal entry", ) - verifyJournal := runTDC(t, bin, "--profile", profileName, "fs-journal", "verify-journal", "--journal-id", journalID, "--output", "text") + verifyJournal := runTI(t, bin, "--profile", profileName, "fs-journal", "verify-journal", "--journal-id", journalID, "--output", "text") verifyJournal.wantExitCode(0) verifyJournal.wantStdoutContains("ok journal=" + journalID) } func testLiveFSDataPlaneContinuation(t *testing.T, bin, profileName, rootPath, sourcePath, copyPath, movedPath, suffix, fullContent string, deleted *bool) { t.Helper() - waitLiveFSResult(t, bin, []string{"--profile", profileName, "fs", "search-file-content", "--path", rootPath, "--pattern", "tdc fs live e2e", "--limit", "5"}, "README.md", 5*time.Minute, "find uploaded file content") + waitLiveFSResult(t, bin, []string{"--profile", profileName, "fs", "search-file-content", "--path", rootPath, "--pattern", "ti fs live e2e", "--limit", "5"}, "README.md", 5*time.Minute, "find uploaded file content") waitLiveFSResult(t, bin, []string{"--profile", profileName, "fs", "find-files", "--path", rootPath, "--file-name-pattern", "*.md", "--limit", "5"}, "README.md", 2*time.Minute, "find uploaded file by name") - remoteCopy := runTDC(t, bin, "--profile", profileName, "fs", "copy-file", "--from-remote", sourcePath, "--to-remote", copyPath) + remoteCopy := runTI(t, bin, "--profile", profileName, "fs", "copy-file", "--from-remote", sourcePath, "--to-remote", copyPath) remoteCopy.wantExitCode(0) remoteCopy.wantStdoutContains(`"status": "copied"`) - move := runTDC(t, bin, "--profile", profileName, "fs", "move-file", "--from-remote", copyPath, "--to-remote", movedPath) + move := runTI(t, bin, "--profile", profileName, "fs", "move-file", "--from-remote", copyPath, "--to-remote", movedPath) move.wantExitCode(0) move.wantStdoutContains(`"status": "moved"`) downloadPath := filepath.Join(t.TempDir(), "nested", "downloaded.md") - download := runTDC(t, bin, "--profile", profileName, "fs", "copy-file", "--from-remote", movedPath, "--to-local", downloadPath, "--create-parents") + download := runTI(t, bin, "--profile", profileName, "fs", "copy-file", "--from-remote", movedPath, "--to-local", downloadPath, "--create-parents") download.wantExitCode(0) download.wantStdoutContains(`"status": "copied"`) downloaded, err := os.ReadFile(downloadPath) @@ -1052,7 +1052,7 @@ func testLiveFSDataPlaneContinuation(t *testing.T, bin, profileName, rootPath, s if err := os.WriteFile(resumePath, []byte(fullContent[:5]), 0o644); err != nil { t.Fatalf("write partial resume file: %v", err) } - resume := runTDC(t, bin, "--profile", profileName, "fs", "copy-file", "--from-remote", movedPath, "--to-local", resumePath, "--resume") + resume := runTI(t, bin, "--profile", profileName, "fs", "copy-file", "--from-remote", movedPath, "--to-local", resumePath, "--resume") resume.wantExitCode(0) resume.wantStdoutContains(`"status": "resumed"`) resumed, err := os.ReadFile(resumePath) @@ -1074,27 +1074,27 @@ func testLiveFSDataPlaneContinuation(t *testing.T, bin, profileName, rootPath, s t.Fatalf("write nested local tree file: %v", err) } treeRoot := rootPath + "/tree" - recursiveUpload := runTDC(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", localTree, "--to-remote", treeRoot, "--recursive") + recursiveUpload := runTI(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", localTree, "--to-remote", treeRoot, "--recursive") recursiveUpload.wantExitCode(0) recursiveUpload.wantStdoutContains(`"status": "copied"`) - readTreeFile := runTDC(t, bin, "--profile", profileName, "fs", "read-file", "--path", treeRoot+"/nested/beta.txt") + readTreeFile := runTI(t, bin, "--profile", profileName, "fs", "read-file", "--path", treeRoot+"/nested/beta.txt") readTreeFile.wantExitCode(0) if readTreeFile.stdout != "beta "+suffix { readTreeFile.fail("recursive local-to-remote copy should preserve nested file contents") } treeCopyRoot := rootPath + "/tree-copy" - recursiveRemoteCopy := runTDC(t, bin, "--profile", profileName, "fs", "copy-file", "--from-remote", treeRoot, "--to-remote", treeCopyRoot, "--recursive") + recursiveRemoteCopy := runTI(t, bin, "--profile", profileName, "fs", "copy-file", "--from-remote", treeRoot, "--to-remote", treeCopyRoot, "--recursive") recursiveRemoteCopy.wantExitCode(0) recursiveRemoteCopy.wantStdoutContains(`"status": "copied"`) - readCopiedTreeFile := runTDC(t, bin, "--profile", profileName, "fs", "read-file", "--path", treeCopyRoot+"/nested/beta.txt") + readCopiedTreeFile := runTI(t, bin, "--profile", profileName, "fs", "read-file", "--path", treeCopyRoot+"/nested/beta.txt") readCopiedTreeFile.wantExitCode(0) if readCopiedTreeFile.stdout != "beta "+suffix { readCopiedTreeFile.fail("recursive remote-to-remote copy should preserve nested file contents") } downloadTree := filepath.Join(t.TempDir(), "download-tree") - recursiveDownload := runTDC(t, bin, "--profile", profileName, "fs", "copy-file", "--from-remote", treeRoot, "--to-local", downloadTree, "--recursive") + recursiveDownload := runTI(t, bin, "--profile", profileName, "fs", "copy-file", "--from-remote", treeRoot, "--to-local", downloadTree, "--recursive") recursiveDownload.wantExitCode(0) recursiveDownload.wantStdoutContains(`"status": "copied"`) downloadedTreeFile, err := os.ReadFile(filepath.Join(downloadTree, "nested", "beta.txt")) @@ -1115,12 +1115,12 @@ func testLiveFSDataPlaneContinuation(t *testing.T, bin, profileName, rootPath, s t.Fatalf("write pack overlay file: %v", err) } packArchivePath := rootPath + "/packs/portable.tar.gz" - pack := runTDC(t, bin, "--profile", profileName, "fs", "pack-file-system", "--local-root", packLocalRoot, "--remote-root", rootPath, "--mount-profile", "portable", "--archive-path", packArchivePath) + pack := runTI(t, bin, "--profile", profileName, "fs", "pack-file-system", "--local-root", packLocalRoot, "--remote-root", rootPath, "--mount-profile", "portable", "--archive-path", packArchivePath) pack.wantExitCode(0) pack.wantStdoutContains(`"status": "packed"`) pack.wantStdoutContains(`"archive_path": "` + packArchivePath + `"`) unpackLocalRoot := t.TempDir() - unpack := runTDC(t, bin, "--profile", profileName, "fs", "unpack-file-system", "--local-root", unpackLocalRoot, "--remote-root", rootPath, "--mount-profile", "portable", "--archive-path", packArchivePath) + unpack := runTI(t, bin, "--profile", profileName, "fs", "unpack-file-system", "--local-root", unpackLocalRoot, "--remote-root", rootPath, "--mount-profile", "portable", "--archive-path", packArchivePath) unpack.wantExitCode(0) unpack.wantStdoutContains(`"status": "unpacked"`) unpackedPackFile, err := os.ReadFile(filepath.Join(unpackLocalRoot, "overlay", "repo", "cache", "item.txt")) @@ -1131,11 +1131,11 @@ func testLiveFSDataPlaneContinuation(t *testing.T, bin, profileName, rootPath, s t.Fatalf("unpacked pack content mismatch: got %q want %q", unpackedPackFile, packContent) } - deleteMoved := runTDC(t, bin, "--profile", profileName, "fs", "delete-file", "--path", movedPath) + deleteMoved := runTI(t, bin, "--profile", profileName, "fs", "delete-file", "--path", movedPath) deleteMoved.wantExitCode(0) deleteMoved.wantStdoutContains(`"status": "deleted"`) - deleteRoot := runTDC(t, bin, "--profile", profileName, "fs", "delete-file", "--path", rootPath, "--recursive") + deleteRoot := runTI(t, bin, "--profile", profileName, "fs", "delete-file", "--path", rootPath, "--recursive") deleteRoot.wantExitCode(0) deleteRoot.wantStdoutContains(`"status": "deleted"`) *deleted = true @@ -1144,14 +1144,14 @@ func testLiveFSDataPlaneContinuation(t *testing.T, bin, profileName, rootPath, s func TestLiveFSMountRuntime(t *testing.T) { requireLive(t) if runtime.GOOS != "darwin" && runtime.GOOS != "linux" { - t.Skip("tdc fs FUSE mount live e2e currently runs on macOS or Linux") + t.Skip("ti fs FUSE mount live e2e currently runs on macOS or Linux") } - bin := tdcBinary(t) + bin := tiBinary(t) profileName := liveProfileName(t) ensureLiveFSResource(t, bin, profileName) suffix := time.Now().UTC().Format("20060102150405") - remoteRoot := "/tdc-e2e-mount-" + suffix + remoteRoot := "/ti-e2e-mount-" + suffix mountPath := filepath.Join(t.TempDir(), "mount") if err := os.MkdirAll(mountPath, 0o755); err != nil { t.Fatalf("create mount path: %v", err) @@ -1160,57 +1160,57 @@ func TestLiveFSMountRuntime(t *testing.T) { remoteDeleted := false defer func() { if !unmounted { - cleanupUnmount := runTDC(t, bin, "--profile", profileName, "fs", "unmount-file-system", "--mount-path", mountPath, "--ignore-absent", "--force") + cleanupUnmount := runTI(t, bin, "--profile", profileName, "fs", "unmount-file-system", "--mount-path", mountPath, "--ignore-absent", "--force") if cleanupUnmount.exitCode != 0 { t.Logf("cleanup unmount failed for %s: exit=%d stdout=%s stderr=%s", mountPath, cleanupUnmount.exitCode, cleanupUnmount.stdout, cleanupUnmount.stderr) } } if !remoteDeleted { - cleanupRemote := runTDC(t, bin, "--profile", profileName, "fs", "delete-file", "--path", remoteRoot, "--recursive") + cleanupRemote := runTI(t, bin, "--profile", profileName, "fs", "delete-file", "--path", remoteRoot, "--recursive") if cleanupRemote.exitCode != 0 && cleanupRemote.exitCode != 5 { t.Logf("cleanup remote failed for %s: exit=%d stdout=%s stderr=%s", remoteRoot, cleanupRemote.exitCode, cleanupRemote.stdout, cleanupRemote.stderr) } } }() - createDir := runTDC(t, bin, "--profile", profileName, "fs", "create-directory", "--path", remoteRoot, "--mode", "0755") + createDir := runTI(t, bin, "--profile", profileName, "fs", "create-directory", "--path", remoteRoot, "--mode", "0755") createDir.wantExitCode(0) localSeed := filepath.Join(t.TempDir(), "README.md") - seedContent := "hello mounted tdc fs " + suffix + "\n" + seedContent := "hello mounted ti fs " + suffix + "\n" if err := os.WriteFile(localSeed, []byte(seedContent), 0o644); err != nil { t.Fatalf("write local seed: %v", err) } upload := runLiveFSSetupCommand(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", localSeed, "--to-remote", remoteRoot+"/README.md") upload.wantExitCode(0) - mount := runTDC(t, bin, "--profile", profileName, "fs", "mount", "--mount-path", mountPath, "--remote-path", remoteRoot, "--ready-timeout", "30s") + mount := runTI(t, bin, "--profile", profileName, "fs", "mount", "--mount-path", mountPath, "--remote-path", remoteRoot, "--ready-timeout", "30s") mount.wantExitCode(0) mount.wantStdoutContains(`"status": "mounted"`) mount.wantStdoutContains(`"driver":`) waitLiveLocalFile(t, filepath.Join(mountPath, "README.md"), seedContent, 30*time.Second) - overwriteContent := "overwritten through mounted tdc fs " + suffix + "\n" + overwriteContent := "overwritten through mounted ti fs " + suffix + "\n" if err := os.WriteFile(filepath.Join(mountPath, "README.md"), []byte(overwriteContent), 0o644); err != nil { t.Fatalf("overwrite existing remote file through mount failed: %v", err) } - localWrite := "written through mounted tdc fs " + suffix + "\n" + localWrite := "written through mounted ti fs " + suffix + "\n" if err := os.WriteFile(filepath.Join(mountPath, "local-write.txt"), []byte(localWrite), 0o644); err != nil { t.Fatalf("write through mount failed: %v", err) } if strings.Contains(mount.stdout, `"driver": "fuse"`) { - drain := runTDC(t, bin, "--profile", profileName, "fs", "drain", "--mount-path", mountPath, "--timeout", "30s") + drain := runTI(t, bin, "--profile", profileName, "fs", "drain", "--mount-path", mountPath, "--timeout", "30s") drain.wantExitCode(0) drain.wantStdoutContains(`"status": "drained"`) } waitLiveRemoteRead(t, bin, profileName, remoteRoot+"/README.md", overwriteContent, 30*time.Second) waitLiveRemoteRead(t, bin, profileName, remoteRoot+"/local-write.txt", localWrite, 30*time.Second) - unmount := runTDC(t, bin, "--profile", profileName, "fs", "umount", "--mount-path", mountPath) + unmount := runTI(t, bin, "--profile", profileName, "fs", "umount", "--mount-path", mountPath) unmount.wantExitCode(0) unmount.wantStdoutContains(`"status": "unmounted"`) unmounted = true - deleteRoot := runTDC(t, bin, "--profile", profileName, "fs", "delete-file", "--path", remoteRoot, "--recursive") + deleteRoot := runTI(t, bin, "--profile", profileName, "fs", "delete-file", "--path", remoteRoot, "--recursive") deleteRoot.wantExitCode(0) remoteDeleted = true } @@ -1218,14 +1218,14 @@ func TestLiveFSMountRuntime(t *testing.T) { func TestLiveFSConfigurationFreeAccess(t *testing.T) { requireLive(t) if runtime.GOOS != "darwin" && runtime.GOOS != "linux" { - t.Skip("tdc fs configuration-free mount live e2e currently runs on macOS or Linux") + t.Skip("ti fs configuration-free mount live e2e currently runs on macOS or Linux") } - bin := tdcBinary(t) + bin := tiBinary(t) profileName := liveProfileName(t) suffix := fmt.Sprintf("%s-%d", time.Now().UTC().Format("20060102150405"), os.Getpid()) - fileSystemName := "tdc-e2e-token-" + suffix - create := runTDC(t, bin, "--profile", profileName, "fs", "create-file-system", "--file-system-name", fileSystemName, "--wait") + fileSystemName := "ti-e2e-token-" + suffix + create := runTI(t, bin, "--profile", profileName, "fs", "create-file-system", "--file-system-name", fileSystemName, "--wait") create.wantExitCode(0) var created struct { FileSystemName string `json:"file_system_name"` @@ -1244,7 +1244,7 @@ func TestLiveFSConfigurationFreeAccess(t *testing.T) { t.Fatalf("generated configuration-free FS resource name unexpectedly existed") } if created.Status != "ready" { - t.Fatalf("--wait returned tdc fs resource in status %q", created.Status) + t.Fatalf("--wait returned ti fs resource in status %q", created.Status) } deletedResource := false @@ -1252,7 +1252,7 @@ func TestLiveFSConfigurationFreeAccess(t *testing.T) { if deletedResource { return } - cleanup := runTDC(t, bin, "--profile", profileName, "fs", "delete-file-system", "--file-system-name", fileSystemName) + cleanup := runTI(t, bin, "--profile", profileName, "fs", "delete-file-system", "--file-system-name", fileSystemName) if cleanup.exitCode != 0 { t.Logf("cleanup configuration-free FS resource failed for %q: exit=%d stderr=%s", fileSystemName, cleanup.exitCode, strings.TrimSpace(cleanup.stderr)) } @@ -1265,28 +1265,28 @@ func TestLiveFSConfigurationFreeAccess(t *testing.T) { } cleanHome := t.TempDir() authEnv := liveFSTokenEnv(selected, cleanHome) - remoteRoot := "/tdc-e2e-token-" + suffix + remoteRoot := "/ti-e2e-token-" + suffix remoteDeleted := false defer func() { if remoteDeleted { return } - cleanup := runTDC(t, bin, "--profile", profileName, "fs", "delete-file", "--file-system-name", fileSystemName, "--path", remoteRoot, "--recursive") + cleanup := runTI(t, bin, "--profile", profileName, "fs", "delete-file", "--file-system-name", fileSystemName, "--path", remoteRoot, "--recursive") if cleanup.exitCode != 0 && cleanup.exitCode != 5 { t.Logf("cleanup configuration-free remote path failed for %s: exit=%d stderr=%s", remoteRoot, cleanup.exitCode, strings.TrimSpace(cleanup.stderr)) } }() - createDir := runTDCWithInput(t, bin, "", authEnv, "fs", "create-directory", "--path", remoteRoot) + createDir := runTIWithInput(t, bin, "", authEnv, "fs", "create-directory", "--path", remoteRoot) createDir.wantExitCode(0) seedContent := "configuration-free seed " + suffix + "\n" seedPath := filepath.Join(t.TempDir(), "seed.txt") if err := os.WriteFile(seedPath, []byte(seedContent), 0o600); err != nil { t.Fatalf("write configuration-free seed: %v", err) } - upload := runTDCWithInput(t, bin, "", authEnv, "fs", "copy-file", "--from-local", seedPath, "--to-remote", remoteRoot+"/seed.txt") + upload := runTIWithInput(t, bin, "", authEnv, "fs", "copy-file", "--from-local", seedPath, "--to-remote", remoteRoot+"/seed.txt") upload.wantExitCode(0) - read := runTDCWithInput(t, bin, "", authEnv, "fs", "read-file", "--path", remoteRoot+"/seed.txt") + read := runTIWithInput(t, bin, "", authEnv, "fs", "read-file", "--path", remoteRoot+"/seed.txt") read.wantExitCode(0) if read.stdout != seedContent { read.fail("configuration-free data-plane read should match uploaded bytes") @@ -1302,13 +1302,13 @@ func TestLiveFSConfigurationFreeAccess(t *testing.T) { if unmounted { return } - cleanup := runTDCWithInput(t, bin, "", locatorEnv, "fs", "unmount-file-system", "--mount-path", mountPath, "--ignore-absent", "--force") + cleanup := runTIWithInput(t, bin, "", locatorEnv, "fs", "unmount-file-system", "--mount-path", mountPath, "--ignore-absent", "--force") if cleanup.exitCode != 0 { t.Logf("cleanup configuration-free mount failed for %s: exit=%d stderr=%s", mountPath, cleanup.exitCode, strings.TrimSpace(cleanup.stderr)) } }() - mount := runTDCWithInput(t, bin, "", authEnv, "fs", "mount-file-system", "--mount-path", mountPath, "--remote-path", remoteRoot, "--ready-timeout", "30s") + mount := runTIWithInput(t, bin, "", authEnv, "fs", "mount-file-system", "--mount-path", mountPath, "--remote-path", remoteRoot, "--ready-timeout", "30s") mount.wantExitCode(0) waitLiveLocalFile(t, filepath.Join(mountPath, "seed.txt"), seedContent, 30*time.Second) @@ -1323,28 +1323,28 @@ func TestLiveFSConfigurationFreeAccess(t *testing.T) { if err := os.WriteFile(directPath, []byte(directContent), 0o600); err != nil { t.Fatalf("write configuration-free direct source: %v", err) } - directUpload := runTDCWithInput(t, bin, "", authEnv, "fs", "copy-file", "--from-local", directPath, "--to-remote", remoteRoot+"/direct.txt") + directUpload := runTIWithInput(t, bin, "", authEnv, "fs", "copy-file", "--from-local", directPath, "--to-remote", remoteRoot+"/direct.txt") directUpload.wantExitCode(0) waitLiveLocalFile(t, filepath.Join(mountPath, "direct.txt"), directContent, 30*time.Second) if strings.Contains(mount.stdout, `"driver": "fuse"`) { - drain := runTDCWithInput(t, bin, "", locatorEnv, "fs", "drain-file-system", "--mount-path", mountPath, "--timeout", "30s") + drain := runTIWithInput(t, bin, "", locatorEnv, "fs", "drain-file-system", "--mount-path", mountPath, "--timeout", "30s") drain.wantExitCode(0) } - unmount := runTDCWithInput(t, bin, "", locatorEnv, "fs", "unmount-file-system", "--mount-path", mountPath) + unmount := runTIWithInput(t, bin, "", locatorEnv, "fs", "unmount-file-system", "--mount-path", mountPath) unmount.wantExitCode(0) unmounted = true for _, path := range []string{ - filepath.Join(cleanHome, ".tdc", "config"), - filepath.Join(cleanHome, ".tdc", "credentials"), - filepath.Join(cleanHome, ".tdc", "fs_resources"), + filepath.Join(cleanHome, ".ti", "config"), + filepath.Join(cleanHome, ".ti", "credentials"), + filepath.Join(cleanHome, ".ti", "fs_resources"), } { if _, err := os.Stat(path); !os.IsNotExist(err) { - t.Fatalf("configuration-free live command persisted tdc configuration at %s: %v", path, err) + t.Fatalf("configuration-free live command persisted ti configuration at %s: %v", path, err) } } - locators, err := filepath.Glob(filepath.Join(cleanHome, ".tdc", "mounts", "*.locator.json")) + locators, err := filepath.Glob(filepath.Join(cleanHome, ".ti", "mounts", "*.locator.json")) if err != nil { t.Fatalf("inspect configuration-free mount locators: %v", err) } @@ -1352,10 +1352,10 @@ func TestLiveFSConfigurationFreeAccess(t *testing.T) { t.Fatalf("successful configuration-free unmount left %d mount locator(s)", len(locators)) } - deleteRemote := runTDC(t, bin, "--profile", profileName, "fs", "delete-file", "--file-system-name", fileSystemName, "--path", remoteRoot, "--recursive") + deleteRemote := runTI(t, bin, "--profile", profileName, "fs", "delete-file", "--file-system-name", fileSystemName, "--path", remoteRoot, "--recursive") deleteRemote.wantExitCode(0) remoteDeleted = true - deleteResource := runTDC(t, bin, "--profile", profileName, "fs", "delete-file-system", "--file-system-name", fileSystemName) + deleteResource := runTI(t, bin, "--profile", profileName, "fs", "delete-file-system", "--file-system-name", fileSystemName) deleteResource.wantExitCode(0) deleteResource.wantStdoutContains(`"status": "deleting"`) deleteResource.wantStdoutContains(`"remote_deletion_state": "deleting"`) @@ -1365,7 +1365,7 @@ func TestLiveFSConfigurationFreeAccess(t *testing.T) { func TestLiveFSWebDAVMountRuntime(t *testing.T) { requireLive(t) if runtime.GOOS != "darwin" { - t.Skip("tdc fs WebDAV mount live e2e currently runs on macOS") + t.Skip("ti fs WebDAV mount live e2e currently runs on macOS") } if _, err := exec.LookPath("mount_webdav"); err != nil { t.Skip("mount_webdav is not available") @@ -1374,11 +1374,11 @@ func TestLiveFSWebDAVMountRuntime(t *testing.T) { t.Skip("umount is not available") } - bin := tdcBinary(t) + bin := tiBinary(t) profileName := liveProfileName(t) ensureLiveFSResource(t, bin, profileName) suffix := time.Now().UTC().Format("20060102150405") - remoteRoot := "/tdc-e2e-webdav-mount-" + suffix + remoteRoot := "/ti-e2e-webdav-mount-" + suffix mountPath := filepath.Join(t.TempDir(), "mount") if err := os.MkdirAll(mountPath, 0o755); err != nil { t.Fatalf("create mount path: %v", err) @@ -1387,42 +1387,42 @@ func TestLiveFSWebDAVMountRuntime(t *testing.T) { remoteDeleted := false defer func() { if !unmounted { - cleanupUnmount := runTDC(t, bin, "--profile", profileName, "fs", "unmount-file-system", "--mount-path", mountPath, "--ignore-absent", "--force") + cleanupUnmount := runTI(t, bin, "--profile", profileName, "fs", "unmount-file-system", "--mount-path", mountPath, "--ignore-absent", "--force") if cleanupUnmount.exitCode != 0 { t.Logf("cleanup unmount failed for %s: exit=%d stdout=%s stderr=%s", mountPath, cleanupUnmount.exitCode, cleanupUnmount.stdout, cleanupUnmount.stderr) } } if !remoteDeleted { - cleanupRemote := runTDC(t, bin, "--profile", profileName, "fs", "delete-file", "--path", remoteRoot, "--recursive") + cleanupRemote := runTI(t, bin, "--profile", profileName, "fs", "delete-file", "--path", remoteRoot, "--recursive") if cleanupRemote.exitCode != 0 && cleanupRemote.exitCode != 5 { t.Logf("cleanup remote failed for %s: exit=%d stdout=%s stderr=%s", remoteRoot, cleanupRemote.exitCode, cleanupRemote.stdout, cleanupRemote.stderr) } } }() - createDir := runTDC(t, bin, "--profile", profileName, "fs", "create-directory", "--path", remoteRoot, "--mode", "0755") + createDir := runTI(t, bin, "--profile", profileName, "fs", "create-directory", "--path", remoteRoot, "--mode", "0755") createDir.wantExitCode(0) localSeed := filepath.Join(t.TempDir(), "README.md") - seedContent := "hello webdav mounted tdc fs " + suffix + "\n" + seedContent := "hello webdav mounted ti fs " + suffix + "\n" if err := os.WriteFile(localSeed, []byte(seedContent), 0o644); err != nil { t.Fatalf("write local seed: %v", err) } upload := runLiveFSSetupCommand(t, bin, "--profile", profileName, "fs", "copy-file", "--from-local", localSeed, "--to-remote", remoteRoot+"/README.md") upload.wantExitCode(0) - mount := runTDC(t, bin, "--profile", profileName, "fs", "mount-file-system", "--mount-path", mountPath, "--remote-path", remoteRoot, "--driver", "webdav", "--ready-timeout", "30s") + mount := runTI(t, bin, "--profile", profileName, "fs", "mount-file-system", "--mount-path", mountPath, "--remote-path", remoteRoot, "--driver", "webdav", "--ready-timeout", "30s") mount.wantExitCode(0) mount.wantStdoutContains(`"status": "mounted"`) mount.wantStdoutContains(`"driver": "webdav"`) waitLiveLocalFile(t, filepath.Join(mountPath, "README.md"), seedContent, 30*time.Second) - unmount := runTDC(t, bin, "--profile", profileName, "fs", "unmount-file-system", "--mount-path", mountPath) + unmount := runTI(t, bin, "--profile", profileName, "fs", "unmount-file-system", "--mount-path", mountPath) unmount.wantExitCode(0) unmount.wantStdoutContains(`"status": "unmounted"`) unmounted = true - deleteRoot := runTDC(t, bin, "--profile", profileName, "fs", "delete-file", "--path", remoteRoot, "--recursive") + deleteRoot := runTI(t, bin, "--profile", profileName, "fs", "delete-file", "--path", remoteRoot, "--recursive") deleteRoot.wantExitCode(0) remoteDeleted = true } @@ -1430,13 +1430,13 @@ func TestLiveFSWebDAVMountRuntime(t *testing.T) { func TestLiveDBClusterLifecycle(t *testing.T) { requireLive(t) - bin := tdcBinary(t) + bin := tiBinary(t) profileName := liveProfileName(t) releaseAutoCreatedLiveFSResource(t, bin, profileName) profile := liveProfile(t) suffix := time.Now().UTC().Format("20060102150405") - clusterName := "tdc-e2e-" + suffix + clusterName := "ti-e2e-" + suffix updatedName := clusterName + "-u" var clusterID string deleted := false @@ -1444,21 +1444,21 @@ func TestLiveDBClusterLifecycle(t *testing.T) { if clusterID == "" || deleted { return } - cleanup := runTDC(t, bin, "--profile", profileName, "db", "delete-db-cluster", "--db-cluster-id", clusterID, "--wait") + cleanup := runTI(t, bin, "--profile", profileName, "db", "delete-db-cluster", "--db-cluster-id", clusterID, "--wait") if cleanup.exitCode != 0 && cleanup.exitCode != 5 { t.Logf("cleanup delete failed for cluster %s: exit=%d stdout=%s stderr=%s", clusterID, cleanup.exitCode, cleanup.stdout, cleanup.stderr) } }() - create := runTDCWithInput( + create := runTIWithInput( t, bin, "", []string{ "HOME=" + t.TempDir(), - "TDC_REGION_CODE=" + profile.PlacementRegionCode, - "TDC_PUBLIC_KEY=" + profile.TDCPublicKey, - "TDC_PRIVATE_KEY=" + profile.TDCPrivateKey, + "TI_REGION_CODE=" + profile.PlacementRegionCode, + "TIDB_CLOUD_PUBLIC_KEY=" + profile.TiDBCloudPublicKey, + "TIDB_CLOUD_PRIVATE_KEY=" + profile.TiDBCloudPrivateKey, }, "db", "create-db-cluster", "--db-cluster-name", clusterName, @@ -1475,7 +1475,7 @@ func TestLiveDBClusterLifecycle(t *testing.T) { if created.State != "ACTIVE" { t.Fatalf("--wait returned cluster in state %q: %#v", created.State, created) } - describe := runTDC(t, bin, "--profile", profileName, "db", "describe-db-cluster", "--db-cluster-id", clusterID, "--view", "FULL") + describe := runTI(t, bin, "--profile", profileName, "db", "describe-db-cluster", "--db-cluster-id", clusterID, "--view", "FULL") describe.wantExitCode(0) described := decodeLiveCluster(t, describe) if described.ID != clusterID || described.DisplayName != clusterName || described.State != "ACTIVE" { @@ -1488,23 +1488,23 @@ func TestLiveDBClusterLifecycle(t *testing.T) { t.Fatalf("server-selected project label is empty: %#v", described) } - prepare := runTDC(t, bin, "--profile", profileName, "db", "create-db-sql-users", "--db-cluster-id", clusterID) + prepare := runTI(t, bin, "--profile", profileName, "db", "create-db-sql-users", "--db-cluster-id", clusterID) prepare.wantExitCode(0) prepare.wantStdoutContains(`"read_only"`) prepare.wantStdoutContains(`"read_write"`) prepare.wantStdoutContains(`"admin"`) - prepareAgain := runTDC(t, bin, "--profile", profileName, "db", "create-db-sql-users", "--db-cluster-id", clusterID) + prepareAgain := runTI(t, bin, "--profile", profileName, "db", "create-db-sql-users", "--db-cluster-id", clusterID) prepareAgain.wantExitCode(0) prepareAgain.wantStdoutContains(`"exists"`) - connectionString := runTDC(t, bin, "--profile", profileName, "db", "format-db-connection-string", "--db-cluster-id", clusterID, "--read-write", "--database", "test") + connectionString := runTI(t, bin, "--profile", profileName, "db", "format-db-connection-string", "--db-cluster-id", clusterID, "--read-write", "--database", "test") connectionString.wantExitCode(0) connectionString.wantStdoutContains(`"format": "mysql-uri"`) connectionString.wantStdoutContains(`"access_mode": "read_write"`) connectionString.wantStdoutContains(`"connection_string"`) - connectionEnv := runTDC(t, bin, "--profile", profileName, "db", "format-db-connection-string", "--db-cluster-id", clusterID, "--read-only", "--format", "env") + connectionEnv := runTI(t, bin, "--profile", profileName, "db", "format-db-connection-string", "--db-cluster-id", clusterID, "--read-only", "--format", "env") connectionEnv.wantExitCode(0) connectionEnv.wantStdoutContains("TIDB_HOST=") connectionEnv.wantStdoutContains("TIDB_ACCESS_MODE=read_only") @@ -1514,14 +1514,14 @@ func TestLiveDBClusterLifecycle(t *testing.T) { waitLiveSQL(t, bin, profileName, clusterID, []string{"--read-only"}, "read-only SQL execution") waitLiveSQL(t, bin, profileName, clusterID, []string{"--admin"}, "admin SQL execution") - branchName := "tdc-e2e-branch-" + suffix + branchName := "ti-e2e-branch-" + suffix branchID := "" branchDeleted := false defer func() { if branchID == "" || branchDeleted { return } - cleanup := runTDC( + cleanup := runTI( t, bin, "--profile", profileName, @@ -1534,7 +1534,7 @@ func TestLiveDBClusterLifecycle(t *testing.T) { } }() - branchCreate := runTDC( + branchCreate := runTI( t, bin, "--profile", profileName, @@ -1553,28 +1553,28 @@ func TestLiveDBClusterLifecycle(t *testing.T) { t.Fatalf("--wait returned branch in state %q: %#v", createdBranch.State, createdBranch) } - branches := runTDC(t, bin, "--profile", profileName, "db", "list-db-cluster-branches", "--db-cluster-id", clusterID, "--page-size", "100") + branches := runTI(t, bin, "--profile", profileName, "db", "list-db-cluster-branches", "--db-cluster-id", clusterID, "--page-size", "100") branches.wantExitCode(0) branches.wantStdoutContains(`"branches"`) branches.wantStdoutContains(branchID) - branchQuery := runTDC(t, bin, "--profile", profileName, "db", "list-db-cluster-branches", "--db-cluster-id", clusterID, "--query", "branches[].id") + branchQuery := runTI(t, bin, "--profile", profileName, "db", "list-db-cluster-branches", "--db-cluster-id", clusterID, "--query", "branches[].id") branchQuery.wantExitCode(0) branchQuery.wantStdoutContains(branchID) - branchText := runTDC(t, bin, "--profile", profileName, "db", "list-db-cluster-branches", "--db-cluster-id", clusterID, "--output", "text") + branchText := runTI(t, bin, "--profile", profileName, "db", "list-db-cluster-branches", "--db-cluster-id", clusterID, "--output", "text") branchText.wantExitCode(0) branchText.wantStdoutContains("ID") branchText.wantStdoutContains(branchName) - branchDescribe := runTDC(t, bin, "--profile", profileName, "db", "describe-db-cluster-branch", "--db-cluster-id", clusterID, "--db-cluster-branch-id", branchID, "--view", "FULL") + branchDescribe := runTI(t, bin, "--profile", profileName, "db", "describe-db-cluster-branch", "--db-cluster-id", clusterID, "--db-cluster-branch-id", branchID, "--view", "FULL") branchDescribe.wantExitCode(0) describedBranch := decodeLiveBranch(t, branchDescribe) if describedBranch.ID != branchID || describedBranch.DisplayName != branchName { t.Fatalf("unexpected described branch: %#v\n%s", describedBranch, branchDescribe.stdout) } - branchDelete := runTDC( + branchDelete := runTI( t, bin, "--profile", profileName, @@ -1591,7 +1591,7 @@ func TestLiveDBClusterLifecycle(t *testing.T) { waitLiveBranchDeleted(t, bin, profileName, clusterID, branchID, 5*time.Minute) - update := runTDC( + update := runTI( t, bin, "--profile", profileName, @@ -1608,7 +1608,7 @@ func TestLiveDBClusterLifecycle(t *testing.T) { return cluster.ID == clusterID && cluster.DisplayName == updatedName }, 3*time.Minute, "show updated display name") - remove := runTDC( + remove := runTI( t, bin, "--profile", profileName, @@ -1629,8 +1629,8 @@ func TestLiveDBClusterLifecycle(t *testing.T) { func requireLive(t *testing.T) { t.Helper() - if os.Getenv("TDC_LIVE") != "1" { - t.Skip("TDC_LIVE=1 is required; run make live-e2e") + if os.Getenv("TI_LIVE") != "1" { + t.Skip("TI_LIVE=1 is required; run make live-e2e") } } @@ -1714,7 +1714,7 @@ func waitLiveCluster(t *testing.T, bin, profileName, clusterID string, ready fun deadline := time.Now().Add(timeout) var last liveCluster for { - describe := runTDC(t, bin, "--profile", profileName, "db", "describe-db-cluster", "--db-cluster-id", clusterID, "--view", "FULL") + describe := runTI(t, bin, "--profile", profileName, "db", "describe-db-cluster", "--db-cluster-id", clusterID, "--view", "FULL") describe.wantExitCode(0) last = decodeLiveCluster(t, describe) if ready(last) { @@ -1735,7 +1735,7 @@ func waitLiveSQL(t *testing.T, bin, profileName, clusterID string, modeArgs []st args := []string{"--profile", profileName, "db", "execute-sql-statement", "--db-cluster-id", clusterID} args = append(args, modeArgs...) args = append(args, "--sql", "select 1") - last = runTDC(t, bin, args...) + last = runTI(t, bin, args...) if last.exitCode == 0 { last.wantStdoutContains(`"transport": "https"`) last.wantStdoutContains(`"row_count": 1`) @@ -1753,12 +1753,12 @@ func waitLiveFSResult(t *testing.T, bin string, args []string, want string, time deadline := time.Now().Add(timeout) var last commandResult for { - last = runTDC(t, bin, args...) + last = runTI(t, bin, args...) if last.exitCode == 0 && strings.Contains(last.stdout, want) { return last } if time.Now().After(deadline) { - last.fail("timed out waiting for tdc fs to %s", description) + last.fail("timed out waiting for ti fs to %s", description) } time.Sleep(5 * time.Second) } @@ -1790,7 +1790,7 @@ func waitLiveRemoteRead(t *testing.T, bin, profileName, remotePath, want string, deadline := time.Now().Add(timeout) var last commandResult for { - last = runTDC(t, bin, "--profile", profileName, "fs", "read-file", "--path", remotePath) + last = runTI(t, bin, "--profile", profileName, "fs", "read-file", "--path", remotePath) if last.exitCode == 0 && last.stdout == want { return } @@ -1806,7 +1806,7 @@ func waitLiveRemoteReadWithEnv(t *testing.T, bin string, env []string, remotePat deadline := time.Now().Add(timeout) var last commandResult for { - last = runTDCWithInput(t, bin, "", env, "fs", "read-file", "--path", remotePath) + last = runTIWithInput(t, bin, "", env, "fs", "read-file", "--path", remotePath) if last.exitCode == 0 && last.stdout == want { return } @@ -1820,26 +1820,26 @@ func waitLiveRemoteReadWithEnv(t *testing.T, bin string, env []string, remotePat func liveFSTokenEnv(profile *config.Profile, home string) []string { return []string{ "HOME=" + home, - "TDC_LOGGING=off", - "TDC_PROFILE=", - "TDC_PUBLIC_KEY=", - "TDC_PRIVATE_KEY=", - "TDC_FS_FILE_SYSTEM_NAME=" + profile.FSResourceName, - "TDC_FS_TOKEN=" + profile.FSAPIKey, - "TDC_REGION_CODE=" + profile.FSPlacementRegionCode, + "TI_LOGGING=off", + "TI_PROFILE=", + "TIDB_CLOUD_PUBLIC_KEY=", + "TIDB_CLOUD_PRIVATE_KEY=", + "TI_FS_FILE_SYSTEM_NAME=" + profile.FSResourceName, + "TI_FS_TOKEN=" + profile.FSAPIKey, + "TI_REGION_CODE=" + profile.FSPlacementRegionCode, } } func liveFSLocatorEnv(home string) []string { return []string{ "HOME=" + home, - "TDC_LOGGING=off", - "TDC_PROFILE=", - "TDC_PUBLIC_KEY=", - "TDC_PRIVATE_KEY=", - "TDC_FS_FILE_SYSTEM_NAME=", - "TDC_FS_TOKEN=", - "TDC_REGION_CODE=", + "TI_LOGGING=off", + "TI_PROFILE=", + "TIDB_CLOUD_PUBLIC_KEY=", + "TIDB_CLOUD_PRIVATE_KEY=", + "TI_FS_FILE_SYSTEM_NAME=", + "TI_FS_TOKEN=", + "TI_REGION_CODE=", } } @@ -1847,7 +1847,7 @@ func waitLiveBranchDeleted(t *testing.T, bin, profileName, clusterID, branchID s t.Helper() deadline := time.Now().Add(timeout) for { - describe := runTDC(t, bin, "--profile", profileName, "db", "describe-db-cluster-branch", "--db-cluster-id", clusterID, "--db-cluster-branch-id", branchID) + describe := runTI(t, bin, "--profile", profileName, "db", "describe-db-cluster-branch", "--db-cluster-id", clusterID, "--db-cluster-branch-id", branchID) switch describe.exitCode { case 0: branch := decodeLiveBranch(t, describe) @@ -1881,7 +1881,7 @@ func cleanupLiveSQLCredentials(t *testing.T, clusterID string) { t.Logf("cannot determine home directory for SQL credential cleanup: %v", err) return } - path := filepath.Join(home, ".tdc", "db_users", clusterID) + path := filepath.Join(home, ".ti", "db_users", clusterID) if err := os.RemoveAll(path); err != nil { t.Logf("cleanup SQL credentials failed for %s: %v", path, err) } @@ -1908,7 +1908,7 @@ func ensureLiveFSResource(t *testing.T, bin, profileName string) *config.Profile t.Fatalf("resolve live fs resource %q: %v", name, err) } - create := runTDC(t, bin, "--profile", profileName, "fs", "create-file-system", "--file-system-name", name, "--wait") + create := runTI(t, bin, "--profile", profileName, "fs", "create-file-system", "--file-system-name", name, "--wait") create.wantExitCode(0) create.wantStdoutContains(`"credentials_stored": true`) create.wantStdoutContains(`"status": "ready"`) @@ -1917,7 +1917,7 @@ func ensureLiveFSResource(t *testing.T, bin, profileName string) *config.Profile profile = liveProfile(t) selected, _, err := fscred.Resolve(home, profile, name, true, nil) if err != nil { - t.Fatalf("tdc fs resource %q was created but is not in profile %q registry: %v", name, profileName, err) + t.Fatalf("ti fs resource %q was created but is not in profile %q registry: %v", name, profileName, err) } return selected } @@ -1926,14 +1926,14 @@ func waitLiveFSReady(t *testing.T, bin, profileName string, profile *config.Prof t.Helper() client := liveFSClient(t, profile, authz.FSVolumeRead) probeLocalPath := filepath.Join(t.TempDir(), "ready.txt") - if err := os.WriteFile(probeLocalPath, []byte("tdc fs live readiness probe\n"), 0o600); err != nil { - t.Fatalf("write tdc fs readiness probe: %v", err) + if err := os.WriteFile(probeLocalPath, []byte("ti fs live readiness probe\n"), 0o600); err != nil { + t.Fatalf("write ti fs readiness probe: %v", err) } - probeRemotePath := fmt.Sprintf("/tdc-e2e-readiness-%d-%d.txt", os.Getpid(), time.Now().UnixNano()) + probeRemotePath := fmt.Sprintf("/ti-e2e-readiness-%d-%d.txt", os.Getpid(), time.Now().UnixNano()) defer func() { cleanup := runLiveFSSetupCommand(t, bin, "--profile", profileName, "fs", "delete-file", "--file-system-name", profile.FSResourceName, "--path", probeRemotePath) if cleanup.exitCode != 0 && !isLiveFSNotFound(cleanup.stderr) { - t.Logf("cleanup tdc fs readiness probe failed: exit=%d stderr=%s", cleanup.exitCode, strings.TrimSpace(cleanup.stderr)) + t.Logf("cleanup ti fs readiness probe failed: exit=%d stderr=%s", cleanup.exitCode, strings.TrimSpace(cleanup.stderr)) } }() deadline := time.Now().Add(timeout) @@ -1947,11 +1947,11 @@ func waitLiveFSReady(t *testing.T, bin, profileName string, profile *config.Prof lastStatus = status state := strings.ToLower(strings.TrimSpace(status.Status)) if state == "" || (!strings.Contains(state, "provision") && !strings.Contains(state, "delet")) { - lastProbe = runTDC(t, bin, "--profile", profileName, "fs", "copy-file", "--file-system-name", profile.FSResourceName, "--from-local", probeLocalPath, "--to-remote", probeRemotePath, "--overwrite") + lastProbe = runTI(t, bin, "--profile", profileName, "fs", "copy-file", "--file-system-name", profile.FSResourceName, "--from-local", probeLocalPath, "--to-remote", probeRemotePath, "--overwrite") if lastProbe.exitCode == 0 { cleanup := runLiveFSSetupCommand(t, bin, "--profile", profileName, "fs", "delete-file", "--file-system-name", profile.FSResourceName, "--path", probeRemotePath) if cleanup.exitCode != 0 && !isLiveFSNotFound(cleanup.stderr) { - cleanup.fail("delete tdc fs readiness probe") + cleanup.fail("delete ti fs readiness probe") } consecutiveWriteProbes++ if consecutiveWriteProbes >= 5 { @@ -1960,18 +1960,18 @@ func waitLiveFSReady(t *testing.T, bin, profileName string, profile *config.Prof } else { consecutiveWriteProbes = 0 if !isLiveFSReadinessError(lastProbe.stderr) { - lastProbe.fail("probe tdc fs data-plane readiness") + lastProbe.fail("probe ti fs data-plane readiness") } } } } else { lastErr = err if !isLiveFSReadinessError(err.Error()) { - t.Fatalf("check tdc fs readiness for profile %q failed: %v", profile.Name, err) + t.Fatalf("check ti fs readiness for profile %q failed: %v", profile.Name, err) } } if time.Now().After(deadline) { - t.Fatalf("timed out waiting for tdc fs resource %q in profile %q to become data-plane ready; last_status=%#v last_error=%v last_probe_stderr=%q", profile.FSResourceName, profile.Name, lastStatus, lastErr, strings.TrimSpace(lastProbe.stderr)) + t.Fatalf("timed out waiting for ti fs resource %q in profile %q to become data-plane ready; last_status=%#v last_error=%v last_probe_stderr=%q", profile.FSResourceName, profile.Name, lastStatus, lastErr, strings.TrimSpace(lastProbe.stderr)) } time.Sleep(5 * time.Second) } @@ -1996,7 +1996,7 @@ func runLiveFSSetupCommand(t *testing.T, bin string, args ...string) commandResu t.Helper() deadline := time.Now().Add(2 * time.Minute) for { - result := runTDC(t, bin, args...) + result := runTI(t, bin, args...) if result.exitCode == 0 || !isLiveFSReadinessError(result.stderr) || time.Now().After(deadline) { return result } @@ -2011,8 +2011,8 @@ func isLiveFSNotFound(stderr string) bool { func TestIsLiveFSReadinessError(t *testing.T) { t.Parallel() for _, message := range []string{ - "tdc [ERROR]: fs ls: storage backend unavailable; contact support", - "tdc [ERROR]: fs cp: HTTP 503:", + "ti [ERROR]: fs ls: storage backend unavailable; contact support", + "ti [ERROR]: fs cp: HTTP 503:", "resource is still provisioning", "503 Service Unavailable", "connection reset by peer", @@ -2025,18 +2025,18 @@ func TestIsLiveFSReadinessError(t *testing.T) { t.Fatalf("expected readiness error for %q", message) } } - if isLiveFSReadinessError("tdc [ERROR]: authentication required") { + if isLiveFSReadinessError("ti [ERROR]: authentication required") { t.Fatal("authentication errors must fail readiness immediately") } } func cleanupAutoCreatedLiveFSResource() { - if os.Getenv("TDC_LIVE") != "1" { + if os.Getenv("TI_LIVE") != "1" { return } - bin := os.Getenv("TDC_E2E_BIN") + bin := os.Getenv("TI_E2E_BIN") if bin == "" { - _, _ = fmt.Fprintln(os.Stderr, "tdc live e2e cleanup warning: TDC_E2E_BIN is not set; cannot delete auto-created tdc fs resource") + _, _ = fmt.Fprintln(os.Stderr, "ti live e2e cleanup warning: TI_E2E_BIN is not set; cannot delete auto-created ti fs resource") return } profileName := liveProfileNameFromEnv() @@ -2049,7 +2049,7 @@ func cleanupAutoCreatedLiveFSResource() { ) output, err := cmd.CombinedOutput() if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "tdc live e2e cleanup warning: delete tdc fs resource %q failed: %v\n%s", name, err, string(output)) + _, _ = fmt.Fprintf(os.Stderr, "ti live e2e cleanup warning: delete ti fs resource %q failed: %v\n%s", name, err, string(output)) } } @@ -2061,7 +2061,7 @@ func releaseAutoCreatedLiveFSResource(t *testing.T, bin, profileName string) { return } name := liveFileSystemName(t) - result := runTDC( + result := runTI( t, bin, "--profile", profileName, @@ -2082,7 +2082,7 @@ func liveProfileName(t *testing.T) string { } func liveProfileNameFromEnv() string { - profileName := os.Getenv("TDC_PROFILE") + profileName := os.Getenv("TI_PROFILE") if profileName == "" { profileName = defaultLiveProfile } @@ -2095,7 +2095,7 @@ func liveFileSystemName(t *testing.T) string { } func liveFileSystemNameFromEnv() string { - name := strings.TrimSpace(os.Getenv("TDC_LIVE_FS_NAME")) + name := strings.TrimSpace(os.Getenv("TI_LIVE_FS_NAME")) if name == "" { name = "workspace" } @@ -2115,16 +2115,16 @@ func liveProfile(t *testing.T) *config.Profile { } profile, err := load() if err != nil { - t.Fatalf("load live e2e profile %q: %v\nconfigure it with: bin/tdc configure --profile %s", profileName, err, profileName) + t.Fatalf("load live e2e profile %q: %v\nconfigure it with: bin/ti configure --profile %s", profileName, err, profileName) } if profile.ProjectID != "" { return profile } - configured := runTDCWithInput(t, tdcBinary(t), "", []string{ - "TDC_REGION_CODE=" + profile.PlacementRegionCode, - "TDC_PUBLIC_KEY=" + profile.TDCPublicKey, - "TDC_PRIVATE_KEY=" + profile.TDCPrivateKey, + configured := runTIWithInput(t, tiBinary(t), "", []string{ + "TI_REGION_CODE=" + profile.PlacementRegionCode, + "TIDB_CLOUD_PUBLIC_KEY=" + profile.TiDBCloudPublicKey, + "TIDB_CLOUD_PRIVATE_KEY=" + profile.TiDBCloudPrivateKey, }, "configure", "--profile", profileName, "--non-interactive") configured.wantExitCode(0) configured.wantStdoutContains(`"project_type": "tidbx_virtual"`) @@ -2143,7 +2143,7 @@ func liveDigestClient(t *testing.T, profile *config.Profile, endpoint endpoints. client, err := api.NewDigestClient(profile, endpoint, permission, api.Options{ Timeout: 30 * time.Second, MaxRetries: 1, - UserAgent: "tdc-live-e2e", + UserAgent: "ti-live-e2e", }) if err != nil { t.Fatalf("create live API client for %s: %v", endpoint.Service, err) @@ -2163,15 +2163,15 @@ func liveFSClient(t *testing.T, profile *config.Profile, permission authz.Permis } endpoint, err := endpoints.NewResolver().ResolveFS(provider, regionCode) if err != nil { - t.Fatalf("resolve live tdc fs endpoint: %v", err) + t.Fatalf("resolve live ti fs endpoint: %v", err) } client, err := api.NewBearerClient(profile.Name, profile.FSAPIKey, endpoint, permission, api.Options{ Timeout: 45 * time.Second, MaxRetries: 1, - UserAgent: "tdc-live-e2e", + UserAgent: "ti-live-e2e", }) if err != nil { - t.Fatalf("create live tdc fs client: %v", err) + t.Fatalf("create live ti fs client: %v", err) } return apifs.New(client) } diff --git a/e2e/telemetry_test.go b/e2e/telemetry_test.go index c756fac..aebc68f 100644 --- a/e2e/telemetry_test.go +++ b/e2e/telemetry_test.go @@ -20,12 +20,12 @@ import ( ) func TestTelemetryDeliveryToTiDB(t *testing.T) { - if os.Getenv("TDC_TELEMETRY_E2E") != "1" { + if os.Getenv("TI_TELEMETRY_E2E") != "1" { t.Skip("run make telemetry-e2e") } - baseDSN := strings.TrimSpace(os.Getenv("TDC_TEST_TELEMETRY_TIDB_DSN")) + baseDSN := strings.TrimSpace(os.Getenv("TI_TEST_TELEMETRY_TIDB_DSN")) if baseDSN == "" { - t.Fatal("TDC_TEST_TELEMETRY_TIDB_DSN is required") + t.Fatal("TI_TEST_TELEMETRY_TIDB_DSN is required") } ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) @@ -43,7 +43,7 @@ func TestTelemetryDeliveryToTiDB(t *testing.T) { event_id, occurred_at, anonymous_installation_id, event_name, command_path, flag_names_json, exit_code, duration_ms, cli_version, os, arch, schema_version ) VALUES (?, CURRENT_TIMESTAMP(6), ?, ?, ?, JSON_ARRAY(), 0, 0, ?, ?, ?, 1)`, - "legacy-event", "tdc_legacy_installation", "tdc.command.finished", "tdc legacy", "0.0.0", "linux", "amd64"); err != nil { + "legacy-event", "ti_legacy_installation", "ti.command.finished", "ti legacy", "0.0.0", "linux", "amd64"); err != nil { t.Fatalf("insert legacy telemetry event: %v", err) } runTelemetryMigrator(t, ctx, migrator, databaseDSN) @@ -66,13 +66,13 @@ flag_names_json, exit_code, duration_ms, cli_version, os, arch, schema_version endpoint := startTelemetryBackend(t, ctx, databaseDSN, postHog.URL) tag := "telemetry-e2e-" + databaseName runID := "run-" + tag - result := runTDCWithInput(t, tdcBinary(t), "", append(tdcConfigEnv(), + result := runTIWithInput(t, tiBinary(t), "", append(tiConfigEnv(), "HOME="+t.TempDir(), - "TDC_ALLOW_TEST_ENDPOINTS=1", - "TDC_TEST_TELEMETRY_ENDPOINT="+endpoint+"/v1/telemetry/batch", - "TDC_TELEMETRY=on", - "TDC_TELEMETRY_TAG="+tag, - `TDC_TELEMETRY_EXTRA={"telemetry_e2e_run":"`+runID+`"}`, + "TI_ALLOW_TEST_ENDPOINTS=1", + "TI_TEST_TELEMETRY_ENDPOINT="+endpoint+"/v1/telemetry/batch", + "TI_TELEMETRY=on", + "TI_TELEMETRY_TAG="+tag, + `TI_TELEMETRY_EXTRA={"telemetry_e2e_run":"`+runID+`"}`, ), createClusterDryRunArgs()...) result.wantExitCode(0) @@ -88,7 +88,7 @@ func createTelemetryE2EDatabase(t *testing.T, ctx context.Context, baseDSN strin t.Helper() config, err := mysql.ParseDSN(baseDSN) if err != nil { - t.Fatal("parse TDC_TEST_TELEMETRY_TIDB_DSN") + t.Fatal("parse TI_TEST_TELEMETRY_TIDB_DSN") } adminConfig := *config adminConfig.DBName = "" @@ -100,7 +100,7 @@ func createTelemetryE2EDatabase(t *testing.T, ctx context.Context, baseDSN strin if err := adminDB.PingContext(ctx); err != nil { t.Fatalf("connect to TiDB server: %v", err) } - name := "tdc_telemetry_e2e_" + strings.ReplaceAll(time.Now().UTC().Format("20060102150405000000000"), "-", "") + name := "ti_telemetry_e2e_" + strings.ReplaceAll(time.Now().UTC().Format("20060102150405000000000"), "-", "") if _, err := adminDB.ExecContext(ctx, "CREATE DATABASE `"+name+"`"); err != nil { t.Fatalf("create isolated telemetry e2e database: %v", err) } @@ -118,9 +118,9 @@ func createTelemetryE2EDatabase(t *testing.T, ctx context.Context, baseDSN strin func telemetryMigratorBinary(t *testing.T) string { t.Helper() - path := strings.TrimSpace(os.Getenv("TDC_TELEMETRY_MIGRATOR_E2E_BIN")) + path := strings.TrimSpace(os.Getenv("TI_TELEMETRY_MIGRATOR_E2E_BIN")) if path == "" { - t.Fatal("TDC_TELEMETRY_MIGRATOR_E2E_BIN is required; run make telemetry-e2e") + t.Fatal("TI_TELEMETRY_MIGRATOR_E2E_BIN is required; run make telemetry-e2e") } return path } @@ -145,16 +145,16 @@ func assertLegacyEventSurvivesMigration(t *testing.T, ctx context.Context, db *s if err := db.QueryRowContext(ctx, "SELECT command_path, tag, extra_json FROM telemetry_events WHERE event_id = 'legacy-event'").Scan(&commandPath, &tag, &extra); err != nil { t.Fatalf("read legacy event after migration: %v", err) } - if commandPath != "tdc legacy" || tag != "" || extra != nil { + if commandPath != "ti legacy" || tag != "" || extra != nil { t.Fatalf("legacy event changed during migration: command=%q tag=%q extra=%#v", commandPath, tag, extra) } } func startTelemetryBackend(t *testing.T, ctx context.Context, dsn, postHogURL string) string { t.Helper() - binary := strings.TrimSpace(os.Getenv("TDC_TELEMETRY_BACKEND_E2E_BIN")) + binary := strings.TrimSpace(os.Getenv("TI_TELEMETRY_BACKEND_E2E_BIN")) if binary == "" { - t.Fatal("TDC_TELEMETRY_BACKEND_E2E_BIN is required; run make telemetry-e2e") + t.Fatal("TI_TELEMETRY_BACKEND_E2E_BIN is required; run make telemetry-e2e") } listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { @@ -245,7 +245,7 @@ FROM telemetry_events WHERE tag = ? ORDER BY received_at DESC LIMIT 1`, tag).Sca if err := json.Unmarshal(extra, &metadata); err != nil { t.Fatalf("decode stored telemetry extra metadata: %v", err) } - if commandPath != "tdc db create-db-cluster" || exitCode != 0 || schema != 2 || metadata["telemetry_e2e_run"] != runID { + if commandPath != "ti db create-db-cluster" || exitCode != 0 || schema != 2 || metadata["telemetry_e2e_run"] != runID { t.Fatalf("unexpected stored telemetry event: command=%q exit_code=%d schema=%d extra=%s", commandPath, exitCode, schema, extra) } } diff --git a/e2e/testdata/fake-drive9.go b/e2e/testdata/fake-drive9.go index 7450bab..8596bfb 100644 --- a/e2e/testdata/fake-drive9.go +++ b/e2e/testdata/fake-drive9.go @@ -7,16 +7,16 @@ import ( ) type call struct { - Args []string `json:"args"` - Home string `json:"home"` - APIKey string `json:"api_key"` - Server string `json:"server"` - RegionCode string `json:"region_code"` - TDCPublicKey string `json:"tdc_public_key,omitempty"` - TDCPrivateKey string `json:"tdc_private_key,omitempty"` - TDCFSToken string `json:"tdc_fs_token,omitempty"` - Drive9Public string `json:"drive9_public_key,omitempty"` - Drive9Private string `json:"drive9_private_key,omitempty"` + Args []string `json:"args"` + Home string `json:"home"` + APIKey string `json:"api_key"` + Server string `json:"server"` + RegionCode string `json:"region_code"` + TiDBCloudPublicKey string `json:"tidb_cloud_public_key,omitempty"` + TiDBCloudPrivateKey string `json:"tidb_cloud_private_key,omitempty"` + TIFSToken string `json:"ti_fs_token,omitempty"` + Drive9Public string `json:"drive9_public_key,omitempty"` + Drive9Private string `json:"drive9_private_key,omitempty"` } func main() { @@ -27,16 +27,16 @@ func main() { panic(err) } _ = json.NewEncoder(file).Encode(call{ - Args: os.Args[1:], - Home: os.Getenv("HOME"), - APIKey: os.Getenv("DRIVE9_API_KEY"), - Server: os.Getenv("DRIVE9_SERVER"), - RegionCode: os.Getenv("DRIVE9_REGION_CODE"), - TDCPublicKey: os.Getenv("TDC_PUBLIC_KEY"), - TDCPrivateKey: os.Getenv("TDC_PRIVATE_KEY"), - TDCFSToken: os.Getenv("TDC_FS_TOKEN"), - Drive9Public: os.Getenv("DRIVE9_PUBLIC_KEY"), - Drive9Private: os.Getenv("DRIVE9_PRIVATE_KEY"), + Args: os.Args[1:], + Home: os.Getenv("HOME"), + APIKey: os.Getenv("DRIVE9_API_KEY"), + Server: os.Getenv("DRIVE9_SERVER"), + RegionCode: os.Getenv("DRIVE9_REGION_CODE"), + TiDBCloudPublicKey: os.Getenv("TIDB_CLOUD_PUBLIC_KEY"), + TiDBCloudPrivateKey: os.Getenv("TIDB_CLOUD_PRIVATE_KEY"), + TIFSToken: os.Getenv("TI_FS_TOKEN"), + Drive9Public: os.Getenv("DRIVE9_PUBLIC_KEY"), + Drive9Private: os.Getenv("DRIVE9_PRIVATE_KEY"), }) _ = file.Close() } diff --git a/go.mod b/go.mod index 9dacca6..3c4f4d9 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/tidbcloud/tdc +module github.com/tidbcloud/ti-cli go 1.26.5 @@ -22,5 +22,5 @@ require ( github.com/sethvargo/go-retry v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect + golang.org/x/sys v0.47.0 ) diff --git a/internal/api/client.go b/internal/api/client.go index 2221713..1866eba 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -11,15 +11,15 @@ import ( "strings" "time" - "github.com/tidbcloud/tdc/internal/api/endpoints" - apitransport "github.com/tidbcloud/tdc/internal/api/transport" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/auth" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + apitransport "github.com/tidbcloud/ti-cli/internal/api/transport" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/auth" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" ) -const defaultUserAgent = "tdc" +const defaultUserAgent = "ti" const defaultMaxRetries = 2 @@ -118,7 +118,7 @@ func NewBearerClient(profileName, apiKey string, endpoint endpoints.Endpoint, pe "auth.missing_fs_api_key", "authentication", 3, - fmt.Sprintf("authentication required: missing fs_api_key for profile %q. Create or configure a tdc fs resource first.", profileName), + fmt.Sprintf("authentication required: missing fs_api_key for profile %q. Create or configure a ti fs resource first.", profileName), ) } opts.Endpoint = endpoint @@ -312,9 +312,9 @@ func (c *Client) statusError(req *http.Request, res *http.Response) error { Body: string(body), } case http.StatusUnauthorized: - message := fmt.Sprintf("authentication failed: TiDB Cloud rejected the API key pair for profile %q. Check ~/.tdc/credentials or create a new API key.", profileName(c.ProfileName)) + message := fmt.Sprintf("authentication failed: TiDB Cloud rejected the API key pair for profile %q. Check ~/.ti/credentials or create a new API key.", profileName(c.ProfileName)) if c.Service == endpoints.ServiceFS { - message = fmt.Sprintf("authentication failed: tdc fs rejected fs_api_key for profile %q. Run `tdc fs create-file-system` or recreate the tdc fs resource.", profileName(c.ProfileName)) + message = fmt.Sprintf("authentication failed: ti fs rejected fs_api_key for profile %q. Run `ti fs create-file-system` or recreate the ti fs resource.", profileName(c.ProfileName)) } return &Error{ Code: "auth.invalid_credentials", diff --git a/internal/api/client_test.go b/internal/api/client_test.go index d7d2177..afb7f38 100644 --- a/internal/api/client_test.go +++ b/internal/api/client_test.go @@ -9,10 +9,10 @@ import ( "sync/atomic" "testing" - "github.com/tidbcloud/tdc/internal/api/endpoints" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/oplog" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/oplog" ) func TestClientDoJSON(t *testing.T) { @@ -116,7 +116,7 @@ func TestClientRecordsSafeAPIEvent(t *testing.T) { }, ProfileName: "stage", Permission: authz.FSFileRead, - Action: "read tdc fs file", + Action: "read ti fs file", }) if err != nil { t.Fatalf("New failed: %v", err) @@ -136,7 +136,7 @@ func TestClientRecordsSafeAPIEvent(t *testing.T) { t.Fatalf("expected one event, got %#v", recorder.events) } event := recorder.events[0] - if event.Service != "tdc_fs" || event.Operation != "read tdc fs file" || event.StatusCode != http.StatusOK || event.RequestID != "request-1" { + if event.Service != "ti_fs" || event.Operation != "read ti fs file" || event.StatusCode != http.StatusOK || event.RequestID != "request-1" { t.Fatalf("unexpected event: %#v", event) } if strings.Contains(event.Operation, "secret/path") || strings.Contains(event.Operation, "read=1") { diff --git a/internal/api/endpoints/endpoints.go b/internal/api/endpoints/endpoints.go index 533bbb0..e23a344 100644 --- a/internal/api/endpoints/endpoints.go +++ b/internal/api/endpoints/endpoints.go @@ -14,8 +14,8 @@ import ( "strings" "time" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config/region" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config/region" ) type Service string @@ -33,7 +33,7 @@ const ( DefaultFSMode = "tidb_cloud_native" ) -var errFSManifestUnavailable = errors.New("tdc fs region manifest unavailable") +var errFSManifestUnavailable = errors.New("ti fs region manifest unavailable") const ( fsManifestFetchAttempts = 3 @@ -78,14 +78,14 @@ func NewResolver() Resolver { FSManifestURL: DefaultFSManifestURL, FSMode: DefaultFSMode, } - if os.Getenv("TDC_ALLOW_TEST_ENDPOINTS") == "1" { - if baseURL := strings.TrimSpace(os.Getenv("TDC_TEST_STARTER_BASE_URL")); baseURL != "" { + if os.Getenv("TI_ALLOW_TEST_ENDPOINTS") == "1" { + if baseURL := strings.TrimSpace(os.Getenv("TI_TEST_STARTER_BASE_URL")); baseURL != "" { resolver.StarterBaseURL = baseURL } - if baseURL := strings.TrimSpace(os.Getenv("TDC_TEST_IAM_BASE_URL")); baseURL != "" { + if baseURL := strings.TrimSpace(os.Getenv("TI_TEST_IAM_BASE_URL")); baseURL != "" { resolver.IAMBaseURL = baseURL } - if manifestURL := strings.TrimSpace(os.Getenv("TDC_TEST_FS_MANIFEST_URL")); manifestURL != "" { + if manifestURL := strings.TrimSpace(os.Getenv("TI_TEST_FS_MANIFEST_URL")); manifestURL != "" { resolver.FSManifestURL = manifestURL } } @@ -262,7 +262,7 @@ func fetchFSManifest(ctx context.Context, manifestURL string, client *http.Clien func fetchFSManifestOnce(ctx context.Context, manifestURL string, client *http.Client) (*FSRegionManifest, bool, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, manifestURL, nil) if err != nil { - return nil, false, apperr.Wrap("api.fs_manifest_request", "api", 1, "build tdc fs region manifest request", err) + return nil, false, apperr.Wrap("api.fs_manifest_request", "api", 1, "build ti fs region manifest request", err) } res, err := client.Do(req) if err != nil { @@ -274,7 +274,7 @@ func fetchFSManifestOnce(ctx context.Context, manifestURL string, client *http.C } var manifest FSRegionManifest if err := json.NewDecoder(res.Body).Decode(&manifest); err != nil { - return nil, true, apperr.Wrap("api.fs_manifest_decode", "api", 1, "decode tdc fs region manifest", err) + return nil, true, apperr.Wrap("api.fs_manifest_decode", "api", 1, "decode ti fs region manifest", err) } if err := validateFSManifest(&manifest); err != nil { return nil, false, err @@ -300,10 +300,10 @@ func loadCachedFSManifest(manifestURL string) (*FSRegionManifest, error) { return nil, err } if cached.ManifestURL != manifestURL { - return nil, fmt.Errorf("cached tdc fs manifest belongs to a different URL") + return nil, fmt.Errorf("cached ti fs manifest belongs to a different URL") } if cached.FetchedAt.IsZero() || time.Since(cached.FetchedAt) > fsManifestCacheMaxAge { - return nil, fmt.Errorf("cached tdc fs manifest is expired") + return nil, fmt.Errorf("cached ti fs manifest is expired") } manifest := cached.Manifest if err := validateFSManifest(&manifest); err != nil { @@ -338,19 +338,19 @@ func storeCachedFSManifest(manifestURL string, manifest *FSRegionManifest) { func fsManifestCachePath(manifestURL string) (string, error) { home, err := os.UserHomeDir() if err != nil || strings.TrimSpace(home) == "" { - return "", fmt.Errorf("resolve tdc fs manifest cache home: %w", err) + return "", fmt.Errorf("resolve ti fs manifest cache home: %w", err) } sum := sha256.Sum256([]byte(manifestURL)) name := fmt.Sprintf("fs-region-manifest-%x.json", sum[:8]) - return filepath.Join(home, ".tdc", "cache", name), nil + return filepath.Join(home, ".ti", "cache", name), nil } func validateFSManifest(manifest *FSRegionManifest) error { if manifest == nil { - return apperr.New("api.fs_manifest_invalid", "api", 1, "tdc fs region manifest is required") + return apperr.New("api.fs_manifest_invalid", "api", 1, "ti fs region manifest is required") } if len(manifest.Regions) == 0 { - return apperr.New("api.fs_manifest_invalid", "api", 1, "tdc fs region manifest has no regions") + return apperr.New("api.fs_manifest_invalid", "api", 1, "ti fs region manifest has no regions") } seen := map[string]int{} for i := range manifest.Regions { @@ -361,17 +361,17 @@ func validateFSManifest(manifest *FSRegionManifest) error { entry.CloudProvider = strings.TrimSpace(entry.CloudProvider) entry.TiDBRegion = strings.TrimSpace(entry.TiDBRegion) if entry.RegionCode == "" { - return apperr.New("api.fs_manifest_invalid", "api", 1, fmt.Sprintf("tdc fs region manifest entry %d missing region_code", i)) + return apperr.New("api.fs_manifest_invalid", "api", 1, fmt.Sprintf("ti fs region manifest entry %d missing region_code", i)) } if entry.Mode == "" { - return apperr.New("api.fs_manifest_invalid", "api", 1, fmt.Sprintf("tdc fs region manifest entry %d missing mode", i)) + return apperr.New("api.fs_manifest_invalid", "api", 1, fmt.Sprintf("ti fs region manifest entry %d missing mode", i)) } if entry.ServerURL == "" { - return apperr.New("api.fs_manifest_invalid", "api", 1, fmt.Sprintf("tdc fs region manifest entry %d missing server_url", i)) + return apperr.New("api.fs_manifest_invalid", "api", 1, fmt.Sprintf("ti fs region manifest entry %d missing server_url", i)) } key := entry.RegionCode + "\x00" + entry.Mode if first, ok := seen[key]; ok { - return apperr.New("api.fs_manifest_invalid", "api", 1, fmt.Sprintf("tdc fs region manifest entries %d and %d duplicate region_code %q mode %q", first, i, entry.RegionCode, entry.Mode)) + return apperr.New("api.fs_manifest_invalid", "api", 1, fmt.Sprintf("ti fs region manifest entries %d and %d duplicate region_code %q mode %q", first, i, entry.RegionCode, entry.Mode)) } seen[key] = i } @@ -379,10 +379,10 @@ func validateFSManifest(manifest *FSRegionManifest) error { manifest.Default.RegionCode = strings.TrimSpace(manifest.Default.RegionCode) manifest.Default.Mode = strings.TrimSpace(manifest.Default.Mode) if manifest.Default.RegionCode == "" || manifest.Default.Mode == "" { - return apperr.New("api.fs_manifest_invalid", "api", 1, "tdc fs region manifest default must include region_code and mode") + return apperr.New("api.fs_manifest_invalid", "api", 1, "ti fs region manifest default must include region_code and mode") } if _, ok := seen[manifest.Default.RegionCode+"\x00"+manifest.Default.Mode]; !ok { - return apperr.New("api.fs_manifest_invalid", "api", 1, fmt.Sprintf("tdc fs region manifest default %q/%q not found in regions", manifest.Default.RegionCode, manifest.Default.Mode)) + return apperr.New("api.fs_manifest_invalid", "api", 1, fmt.Sprintf("ti fs region manifest default %q/%q not found in regions", manifest.Default.RegionCode, manifest.Default.Mode)) } } return nil @@ -408,14 +408,14 @@ func selectFSManifestEntry(entries []FSRegionManifestEntry, provider, regionCode "api.fs_endpoint_unsupported", "config", 2, - fmt.Sprintf("tdc fs is not available for %s/%s in mode %s; supported tdc fs regions: %s", provider, regionCode, mode, supportedFSRegions(entries, mode)), + fmt.Sprintf("ti fs is not available for %s/%s in mode %s; supported ti fs regions: %s", provider, regionCode, mode, supportedFSRegions(entries, mode)), ) default: return FSRegionManifestEntry{}, apperr.New( "api.fs_endpoint_ambiguous", "api", 1, - fmt.Sprintf("tdc fs region manifest has multiple endpoints for %s/%s in mode %s", provider, regionCode, mode), + fmt.Sprintf("ti fs region manifest has multiple endpoints for %s/%s in mode %s", provider, regionCode, mode), ) } } diff --git a/internal/api/endpoints/endpoints_test.go b/internal/api/endpoints/endpoints_test.go index 794d41a..0da32ed 100644 --- a/internal/api/endpoints/endpoints_test.go +++ b/internal/api/endpoints/endpoints_test.go @@ -7,8 +7,8 @@ import ( "sync/atomic" "testing" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config/region" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config/region" ) func TestResolveStarterEndpoint(t *testing.T) { @@ -39,8 +39,8 @@ func TestResolveStarterAlibabaRegion(t *testing.T) { } func TestResolveStarterTestOverrideRequiresOptIn(t *testing.T) { - t.Setenv("TDC_TEST_STARTER_BASE_URL", "https://starter.test") - t.Setenv("TDC_ALLOW_TEST_ENDPOINTS", "") + t.Setenv("TI_TEST_STARTER_BASE_URL", "https://starter.test") + t.Setenv("TI_ALLOW_TEST_ENDPOINTS", "") endpoint, err := NewResolver().ResolveStarter(region.ProviderAWS, "us-east-1") if err != nil { t.Fatal(err) @@ -49,7 +49,7 @@ func TestResolveStarterTestOverrideRequiresOptIn(t *testing.T) { t.Fatalf("test override used without opt-in: %q", endpoint.BaseURL) } - t.Setenv("TDC_ALLOW_TEST_ENDPOINTS", "1") + t.Setenv("TI_ALLOW_TEST_ENDPOINTS", "1") endpoint, err = NewResolver().ResolveStarter(region.ProviderAWS, "us-east-1") if err != nil { t.Fatal(err) @@ -70,8 +70,8 @@ func TestResolveIAMEndpoint(t *testing.T) { } func TestResolveIAMTestOverrideRequiresOptIn(t *testing.T) { - t.Setenv("TDC_TEST_IAM_BASE_URL", "https://iam.test") - t.Setenv("TDC_ALLOW_TEST_ENDPOINTS", "") + t.Setenv("TI_TEST_IAM_BASE_URL", "https://iam.test") + t.Setenv("TI_ALLOW_TEST_ENDPOINTS", "") endpoint, err := NewResolver().ResolveIAM() if err != nil { t.Fatal(err) @@ -80,7 +80,7 @@ func TestResolveIAMTestOverrideRequiresOptIn(t *testing.T) { t.Fatalf("test override used without opt-in: %q", endpoint.BaseURL) } - t.Setenv("TDC_ALLOW_TEST_ENDPOINTS", "1") + t.Setenv("TI_ALLOW_TEST_ENDPOINTS", "1") endpoint, err = NewResolver().ResolveIAM() if err != nil { t.Fatal(err) @@ -171,7 +171,7 @@ func TestResolveFSUnsupportedManifestRegion(t *testing.T) { t.Fatalf("expected exit 2, got %d", got) } message := apperr.MessageFor(err) - if !strings.Contains(message, "tdc fs is not available") || !strings.Contains(message, "aws/us-east-1") { + if !strings.Contains(message, "ti fs is not available") || !strings.Contains(message, "aws/us-east-1") { t.Fatalf("unexpected message: %q", message) } } diff --git a/internal/api/error.go b/internal/api/error.go index 234b3d2..05aebcf 100644 --- a/internal/api/error.go +++ b/internal/api/error.go @@ -3,7 +3,7 @@ package api import ( "fmt" - "github.com/tidbcloud/tdc/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/apperr" ) type Error struct { diff --git a/internal/api/fs/client.go b/internal/api/fs/client.go index 314fd5f..cbca912 100644 --- a/internal/api/fs/client.go +++ b/internal/api/fs/client.go @@ -13,8 +13,8 @@ import ( "strings" "time" - "github.com/tidbcloud/tdc/internal/api" - "github.com/tidbcloud/tdc/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/api" + "github.com/tidbcloud/ti-cli/internal/apperr" ) type Client struct { @@ -213,7 +213,7 @@ func (c *Client) readFile(ctx context.Context, remotePath string, offset, length defer res.Body.Close() data, err := io.ReadAll(res.Body) if err != nil { - return nil, apperr.Wrap("api.read_response", "runtime", 1, "read tdc fs response body", err) + return nil, apperr.Wrap("api.read_response", "runtime", 1, "read ti fs response body", err) } if ranged && res.StatusCode == http.StatusOK { if offset >= int64(len(data)) { @@ -232,11 +232,11 @@ func (c *Client) readRedirectedFile(ctx context.Context, res *http.Response, ran defer res.Body.Close() location, err := res.Location() if err != nil { - return nil, apperr.Wrap("api.invalid_redirect", "api", 1, "tdc fs returned an invalid download redirect", err) + return nil, apperr.Wrap("api.invalid_redirect", "api", 1, "ti fs returned an invalid download redirect", err) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, location.String(), nil) if err != nil { - return nil, apperr.Wrap("api.build_request", "runtime", 1, "build redirected tdc fs download request", err) + return nil, apperr.Wrap("api.build_request", "runtime", 1, "build redirected ti fs download request", err) } req.Header.Set("User-Agent", c.api.UserAgent) if rangeHeader != "" { @@ -258,7 +258,7 @@ func (c *Client) readRedirectedFile(ctx context.Context, res *http.Response, ran } if redirected.StatusCode < 200 || redirected.StatusCode >= 300 { body, _ := io.ReadAll(io.LimitReader(redirected.Body, 8*1024)) - message := "redirected tdc fs download failed with HTTP " + strconv.Itoa(redirected.StatusCode) + message := "redirected ti fs download failed with HTTP " + strconv.Itoa(redirected.StatusCode) if trimmed := strings.TrimSpace(string(body)); trimmed != "" { message += ": " + trimmed } @@ -266,7 +266,7 @@ func (c *Client) readRedirectedFile(ctx context.Context, res *http.Response, ran } data, err := io.ReadAll(redirected.Body) if err != nil { - return nil, apperr.Wrap("api.read_response", "runtime", 1, "read redirected tdc fs response body", err) + return nil, apperr.Wrap("api.read_response", "runtime", 1, "read redirected ti fs response body", err) } if ranged && redirected.StatusCode == http.StatusOK { if offset >= int64(len(data)) { diff --git a/internal/api/fs/client_test.go b/internal/api/fs/client_test.go index ae2af87..7a805b7 100644 --- a/internal/api/fs/client_test.go +++ b/internal/api/fs/client_test.go @@ -10,9 +10,9 @@ import ( "strings" "testing" - "github.com/tidbcloud/tdc/internal/api" - "github.com/tidbcloud/tdc/internal/api/endpoints" - "github.com/tidbcloud/tdc/internal/authz" + "github.com/tidbcloud/ti-cli/internal/api" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + "github.com/tidbcloud/ti-cli/internal/authz" ) func TestProvisionAndDeleteTenant(t *testing.T) { @@ -134,7 +134,7 @@ func TestDataPlaneMethods(t *testing.T) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(WriteResponse{Revision: 14}) case r.Method == http.MethodPut && r.URL.Path == "/v1/fs/workspace/tagged.txt": - if got := r.Header.Values("X-Dat9-Tag"); strings.Join(got, ",") != "owner=alice,topic=tdc" { + if got := r.Header.Values("X-Dat9-Tag"); strings.Join(got, ",") != "owner=alice,topic=ti" { t.Fatalf("X-Dat9-Tag = %v", got) } if got := r.Header.Get("X-Dat9-Description"); got != "demo file" { @@ -248,7 +248,7 @@ func TestDataPlaneMethods(t *testing.T) { t.Fatalf("unexpected conditional write response: %#v", conditional) } tagged, err := client.WriteFileWithOptions(ctx, "/workspace/tagged.txt", []byte("tagged"), WriteFileOptions{ - Tags: map[string]string{"topic": "tdc", "owner": "alice"}, + Tags: map[string]string{"topic": "ti", "owner": "alice"}, Description: "demo file", }) if err != nil { @@ -354,7 +354,7 @@ func TestReadFileRedirectsToPresignedURLWithoutBearer(t *testing.T) { http.Redirect(w, r, server.URL+"/signed/large.bin", http.StatusFound) case r.Method == http.MethodGet && r.URL.Path == "/signed/large.bin": if got := r.Header.Get("Authorization"); got != "" { - t.Fatalf("presigned download should not receive tdc auth, got %q", got) + t.Fatalf("presigned download should not receive ti auth, got %q", got) } _, _ = w.Write([]byte("large bytes")) default: @@ -387,7 +387,7 @@ func TestReadFileRangeRedirectPreservesRangeWithoutBearer(t *testing.T) { http.Redirect(w, r, server.URL+"/signed/large.bin", http.StatusFound) case r.Method == http.MethodGet && r.URL.Path == "/signed/large.bin": if got := r.Header.Get("Authorization"); got != "" { - t.Fatalf("presigned download should not receive tdc auth, got %q", got) + t.Fatalf("presigned download should not receive ti auth, got %q", got) } if got := r.Header.Get("Range"); got != "bytes=2-5" { t.Fatalf("redirected Range = %q", got) diff --git a/internal/api/fs/git_workspace.go b/internal/api/fs/git_workspace.go index 56e4bfc..1db7384 100644 --- a/internal/api/fs/git_workspace.go +++ b/internal/api/fs/git_workspace.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/tidbcloud/tdc/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/apperr" ) type GitWorkspaceRequest struct { diff --git a/internal/api/fs/journal.go b/internal/api/fs/journal.go index bca3e61..371f6f2 100644 --- a/internal/api/fs/journal.go +++ b/internal/api/fs/journal.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "github.com/tidbcloud/tdc/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/apperr" ) const ( diff --git a/internal/api/fs/journal_test.go b/internal/api/fs/journal_test.go index 156895c..7dc5025 100644 --- a/internal/api/fs/journal_test.go +++ b/internal/api/fs/journal_test.go @@ -47,7 +47,7 @@ func TestJournalClientMethods(t *testing.T) { sawSearchMeta = append([]string(nil), r.URL.Query()["meta"]...) sawSearchInclude = r.URL.Query().Get("include") sawSearchSince = r.URL.Query().Get("since") - if r.URL.Query().Get("actor") != "agent:tdc" || r.URL.Query().Get("type") != "tool.call.completed" { + if r.URL.Query().Get("actor") != "agent:ti" || r.URL.Query().Get("type") != "tool.call.completed" { t.Fatalf("unexpected search query: %q", r.URL.RawQuery) } w.Header().Set("Content-Type", "application/x-ndjson") @@ -80,7 +80,7 @@ func TestJournalClientMethods(t *testing.T) { matches, err := client.SearchJournal(ctx, JournalSearchRequest{ Type: "tool.call.completed", ActorType: "agent", - ActorID: "tdc", + ActorID: "ti", Labels: []JournalLabel{{Key: "env", Value: "prod"}, {Key: "env", Value: "us-east"}}, SinceRaw: "1h", Entries: true, diff --git a/internal/api/fs/layer.go b/internal/api/fs/layer.go index d4d38e3..1d7195e 100644 --- a/internal/api/fs/layer.go +++ b/internal/api/fs/layer.go @@ -12,8 +12,8 @@ import ( "strings" "time" - "github.com/tidbcloud/tdc/internal/api" - "github.com/tidbcloud/tdc/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/api" + "github.com/tidbcloud/ti-cli/internal/apperr" ) var ErrLayerCommitConflict = errors.New("fs layer commit conflict") diff --git a/internal/api/fs/upload.go b/internal/api/fs/upload.go index b2430e0..5e5617d 100644 --- a/internal/api/fs/upload.go +++ b/internal/api/fs/upload.go @@ -20,8 +20,8 @@ import ( "time" "unicode/utf8" - tdcapi "github.com/tidbcloud/tdc/internal/api" - "github.com/tidbcloud/tdc/internal/apperr" + tiapi "github.com/tidbcloud/ti-cli/internal/api" + "github.com/tidbcloud/ti-cli/internal/apperr" ) const ( @@ -285,7 +285,7 @@ func (c *Client) InitiateUpload(ctx context.Context, remotePath string, size int defer res.Body.Close() var plan UploadPlan if err := json.NewDecoder(res.Body).Decode(&plan); err != nil { - return UploadPlan{}, apperr.Wrap("api.decode_response", "runtime", 1, "decode tdc fs upload plan", err) + return UploadPlan{}, apperr.Wrap("api.decode_response", "runtime", 1, "decode ti fs upload plan", err) } return plan, nil } @@ -413,7 +413,7 @@ func (c *Client) InitiateUploadV2(ctx context.Context, remotePath string, size i defer res.Body.Close() var plan UploadPlanV2 if err := json.NewDecoder(res.Body).Decode(&plan); err != nil { - return nil, apperr.Wrap("api.decode_response", "runtime", 1, "decode tdc fs v2 upload plan", err) + return nil, apperr.Wrap("api.decode_response", "runtime", 1, "decode ti fs v2 upload plan", err) } return &plan, nil } @@ -464,7 +464,7 @@ func (c *Client) requestResumeByBody(ctx context.Context, uploadID string, check defer res.Body.Close() var plan UploadPlan if err := json.NewDecoder(res.Body).Decode(&plan); err != nil { - return nil, apperr.Wrap("api.decode_response", "runtime", 1, "decode tdc fs upload resume plan", err) + return nil, apperr.Wrap("api.decode_response", "runtime", 1, "decode ti fs upload resume plan", err) } return &plan, nil } @@ -484,7 +484,7 @@ func (c *Client) requestResumeLegacy(ctx context.Context, uploadID string, check defer res.Body.Close() var plan UploadPlan if err := json.NewDecoder(res.Body).Decode(&plan); err != nil { - return nil, apperr.Wrap("api.decode_response", "runtime", 1, "decode tdc fs legacy upload resume plan", err) + return nil, apperr.Wrap("api.decode_response", "runtime", 1, "decode ti fs legacy upload resume plan", err) } return &plan, nil } @@ -612,7 +612,7 @@ func (c *Client) presignPipeline(ctx context.Context, plan *UploadPlanV2, batchS } if err := json.NewDecoder(res.Body).Decode(&response); err != nil { _ = res.Body.Close() - sendUploadErr(ctx, errCh, apperr.Wrap("api.decode_response", "runtime", 1, "decode tdc fs v2 presign batch", err)) + sendUploadErr(ctx, errCh, apperr.Wrap("api.decode_response", "runtime", 1, "decode ti fs v2 presign batch", err)) return } _ = res.Body.Close() @@ -735,7 +735,7 @@ func (c *Client) PresignOnePart(ctx context.Context, uploadID string, partNumber defer res.Body.Close() var part PresignedPart if err := json.NewDecoder(res.Body).Decode(&part); err != nil { - return nil, apperr.Wrap("api.decode_response", "runtime", 1, "decode tdc fs v2 presigned part", err) + return nil, apperr.Wrap("api.decode_response", "runtime", 1, "decode ti fs v2 presigned part", err) } return &part, nil } @@ -761,7 +761,7 @@ func (c *Client) InitiateAppend(ctx context.Context, remotePath string, appendSi defer res.Body.Close() var plan AppendPlan if err := json.NewDecoder(res.Body).Decode(&plan); err != nil { - return AppendPlan{}, apperr.Wrap("api.decode_response", "runtime", 1, "decode tdc fs append plan", err) + return AppendPlan{}, apperr.Wrap("api.decode_response", "runtime", 1, "decode ti fs append plan", err) } return plan, nil } @@ -862,7 +862,7 @@ func (c *Client) PatchFile(ctx context.Context, remotePath string, newSize int64 defer res.Body.Close() var plan PatchPlan if err := json.NewDecoder(res.Body).Decode(&plan); err != nil { - return apperr.Wrap("api.decode_response", "runtime", 1, "decode tdc fs patch plan", err) + return apperr.Wrap("api.decode_response", "runtime", 1, "decode ti fs patch plan", err) } if err := c.UploadPatchParts(ctx, plan, readPart); err != nil { _ = c.AbortUpload(context.Background(), plan.UploadID) @@ -1250,7 +1250,7 @@ func isV2NotAvailable(err error) bool { if errors.Is(err, errV2NotAvailable) { return true } - var apiErr *tdcapi.Error + var apiErr *tiapi.Error if !errors.As(err, &apiErr) { return false } @@ -1265,7 +1265,7 @@ func isV2NotAvailable(err error) bool { } func shouldUseLegacyResume(err error) bool { - var apiErr *tdcapi.Error + var apiErr *tiapi.Error if !errors.As(err, &apiErr) { return false } diff --git a/internal/api/fs/upload_test.go b/internal/api/fs/upload_test.go index 0462470..a762947 100644 --- a/internal/api/fs/upload_test.go +++ b/internal/api/fs/upload_test.go @@ -162,7 +162,7 @@ func TestUploadFileInitiatesUploadsPartsAndCompletes(t *testing.T) { case r.Method == http.MethodPut && r.URL.Path == "/parts/1": uploaded = true if got := r.Header.Get("Authorization"); got != "" { - t.Fatalf("presigned upload should not receive tdc auth, got %q", got) + t.Fatalf("presigned upload should not receive ti auth, got %q", got) } if got := r.Header.Get("X-Upload-Token"); got != "part" { t.Fatalf("X-Upload-Token = %q", got) @@ -246,7 +246,7 @@ func TestUploadFileUsesV2PresignRetryAndCompleteMetadata(t *testing.T) { case r.Method == http.MethodPut && r.URL.Path == "/v2-presigned/expired": firstPartAttempts++ if got := r.Header.Get("Authorization"); got != "" { - t.Fatalf("presigned upload should not receive tdc auth, got %q", got) + t.Fatalf("presigned upload should not receive ti auth, got %q", got) } w.WriteHeader(http.StatusForbidden) case r.Method == http.MethodPost && r.URL.Path == "/v2/uploads/v2-1/presign": @@ -267,7 +267,7 @@ func TestUploadFileUsesV2PresignRetryAndCompleteMetadata(t *testing.T) { case r.Method == http.MethodPut && r.URL.Path == "/v2-presigned/fresh": freshUploaded = true if got := r.Header.Get("Authorization"); got != "" { - t.Fatalf("presigned upload should not receive tdc auth, got %q", got) + t.Fatalf("presigned upload should not receive ti auth, got %q", got) } body, err := io.ReadAll(r.Body) if err != nil { @@ -601,7 +601,7 @@ func TestResumeUploadRequestsActiveUploadAndMissingParts(t *testing.T) { case r.Method == http.MethodPut && r.URL.Path == "/parts/1": uploaded = true if got := r.Header.Get("Authorization"); got != "" { - t.Fatalf("presigned upload should not receive tdc auth, got %q", got) + t.Fatalf("presigned upload should not receive ti auth, got %q", got) } body, err := io.ReadAll(r.Body) if err != nil { @@ -718,7 +718,7 @@ func TestInitiateAppendAndUploadPatchParts(t *testing.T) { }) case r.Method == http.MethodGet && r.URL.Path == "/read/1": if got := r.Header.Get("Authorization"); got != "" { - t.Fatalf("presigned read should not receive tdc auth, got %q", got) + t.Fatalf("presigned read should not receive ti auth, got %q", got) } if got := r.Header.Get("Range"); got != "bytes=0-2" { t.Fatalf("Range = %q", got) @@ -727,7 +727,7 @@ func TestInitiateAppendAndUploadPatchParts(t *testing.T) { case r.Method == http.MethodPut && r.URL.Path == "/patch/1": patchUploaded = true if got := r.Header.Get("Authorization"); got != "" { - t.Fatalf("presigned upload should not receive tdc auth, got %q", got) + t.Fatalf("presigned upload should not receive ti auth, got %q", got) } if got := r.Header.Get("X-Upload-Token"); got != "append" { t.Fatalf("X-Upload-Token = %q", got) @@ -809,7 +809,7 @@ func TestPatchFileUploadsDirtyPartsAndCompletes(t *testing.T) { case r.Method == http.MethodPut && r.URL.Path == "/patch/1": patchUploaded = true if got := r.Header.Get("Authorization"); got != "" { - t.Fatalf("presigned upload should not receive tdc auth, got %q", got) + t.Fatalf("presigned upload should not receive ti auth, got %q", got) } if got := r.Header.Get("X-Upload-Token"); got != "patch" { t.Fatalf("X-Upload-Token = %q", got) diff --git a/internal/api/fs/vault.go b/internal/api/fs/vault.go index 4fe576c..ee349bd 100644 --- a/internal/api/fs/vault.go +++ b/internal/api/fs/vault.go @@ -9,7 +9,7 @@ import ( "strings" "time" - "github.com/tidbcloud/tdc/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/apperr" ) type VaultSecret struct { @@ -54,7 +54,7 @@ func (c *Client) CreateVaultSecret(ctx context.Context, name string, fields map[ body := map[string]any{ "name": name, "fields": fields, - "created_by": "tdc", + "created_by": "ti", } var response VaultSecret if err := c.doVaultJSON(ctx, http.MethodPost, "/v1/vault/secrets", body, &response); err != nil { @@ -66,7 +66,7 @@ func (c *Client) CreateVaultSecret(ctx context.Context, name string, fields map[ func (c *Client) UpdateVaultSecret(ctx context.Context, name string, fields map[string]string) (VaultSecret, error) { body := map[string]any{ "fields": fields, - "updated_by": "tdc", + "updated_by": "ti", } var response VaultSecret if err := c.doVaultJSON(ctx, http.MethodPut, "/v1/vault/secrets/"+url.PathEscape(name), body, &response); err != nil { diff --git a/internal/api/fs/vault_test.go b/internal/api/fs/vault_test.go index f803f19..4b7723e 100644 --- a/internal/api/fs/vault_test.go +++ b/internal/api/fs/vault_test.go @@ -8,7 +8,7 @@ import ( "net/http/httptest" "testing" - "github.com/tidbcloud/tdc/internal/api" + "github.com/tidbcloud/ti-cli/internal/api" ) func TestReadVaultSecretFieldUsesDelegatedBearerToken(t *testing.T) { @@ -123,14 +123,14 @@ func TestRevokeVaultGrantSendsDeleteWithPayload(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Fatalf("decode request: %v", err) } - if req.RevokedBy != "tdc" || req.Reason != "rotated" { + if req.RevokedBy != "ti" || req.Reason != "rotated" { t.Fatalf("unexpected request: %#v", req) } w.WriteHeader(http.StatusNoContent) })) defer server.Close() - if err := testBearerClient(t, server.URL).RevokeVaultGrant(context.Background(), "grant-123", "tdc", "rotated"); err != nil { + if err := testBearerClient(t, server.URL).RevokeVaultGrant(context.Background(), "grant-123", "ti", "rotated"); err != nil { t.Fatalf("RevokeVaultGrant failed: %v", err) } } diff --git a/internal/api/iam/client.go b/internal/api/iam/client.go index 0ad3694..3822a47 100644 --- a/internal/api/iam/client.go +++ b/internal/api/iam/client.go @@ -6,7 +6,7 @@ import ( "net/url" "strconv" - "github.com/tidbcloud/tdc/internal/api" + "github.com/tidbcloud/ti-cli/internal/api" ) type Client struct { diff --git a/internal/api/iam/client_test.go b/internal/api/iam/client_test.go index d18cef3..134a126 100644 --- a/internal/api/iam/client_test.go +++ b/internal/api/iam/client_test.go @@ -7,10 +7,10 @@ import ( "net/http/httptest" "testing" - "github.com/tidbcloud/tdc/internal/api" - "github.com/tidbcloud/tdc/internal/api/endpoints" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" + "github.com/tidbcloud/ti-cli/internal/api" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" ) func TestListProjects(t *testing.T) { @@ -124,21 +124,21 @@ func TestSQLUserLifecycleRequests(t *testing.T) { t.Fatalf("unexpected pageSize %q", got) } _, _ = w.Write([]byte(`{ - "sqlUsers":[{"userName":"prefix.tdc_rw","authMethod":"mysql_native_password","builtinRole":"role_readwrite"}], + "sqlUsers":[{"userName":"prefix.ti_rw","authMethod":"mysql_native_password","builtinRole":"role_readwrite"}], "nextPageToken":"token-2" }`)) return } - _, _ = w.Write([]byte(`{"userName":"prefix.tdc_rw","authMethod":"mysql_native_password","builtinRole":"role_readwrite"}`)) + _, _ = w.Write([]byte(`{"userName":"prefix.ti_rw","authMethod":"mysql_native_password","builtinRole":"role_readwrite"}`)) case http.MethodPost: var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatalf("decode create body: %v", err) } - if body["userName"] != "tdc_rw" || body["builtinRole"] != "role_readwrite" || body["authMethod"] != "mysql_native_password" || body["password"] != "secret" || body["autoPrefix"] != true { + if body["userName"] != "ti_rw" || body["builtinRole"] != "role_readwrite" || body["authMethod"] != "mysql_native_password" || body["password"] != "secret" || body["autoPrefix"] != true { t.Fatalf("unexpected create body: %#v", body) } - _, _ = w.Write([]byte(`{"userName":"prefix.tdc_rw","authMethod":"mysql_native_password","builtinRole":"role_readwrite"}`)) + _, _ = w.Write([]byte(`{"userName":"prefix.ti_rw","authMethod":"mysql_native_password","builtinRole":"role_readwrite"}`)) case http.MethodPatch: var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { @@ -147,7 +147,7 @@ func TestSQLUserLifecycleRequests(t *testing.T) { if body["password"] != "rotated" { t.Fatalf("unexpected update body: %#v", body) } - _, _ = w.Write([]byte(`{"userName":"prefix.tdc_rw","authMethod":"mysql_native_password","builtinRole":"role_readwrite"}`)) + _, _ = w.Write([]byte(`{"userName":"prefix.ti_rw","authMethod":"mysql_native_password","builtinRole":"role_readwrite"}`)) case http.MethodDelete: w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"message":"ok"}`)) @@ -162,11 +162,11 @@ func TestSQLUserLifecycleRequests(t *testing.T) { if err != nil { t.Fatalf("ListSQLUsers failed: %v", err) } - if list.NextPageToken != "token-2" || len(list.SQLUsers) != 1 || list.SQLUsers[0].UserName != "prefix.tdc_rw" { + if list.NextPageToken != "token-2" || len(list.SQLUsers) != 1 || list.SQLUsers[0].UserName != "prefix.ti_rw" { t.Fatalf("unexpected list response: %#v", list) } created, err := client.CreateSQLUser(context.Background(), "cluster-1", CreateSQLUserRequest{ - UserName: "tdc_rw", + UserName: "ti_rw", Password: "secret", AuthMethod: "mysql_native_password", AutoPrefix: true, @@ -178,22 +178,22 @@ func TestSQLUserLifecycleRequests(t *testing.T) { if created.BuiltinRole != "role_readwrite" { t.Fatalf("unexpected created user: %#v", created) } - if _, err := client.GetSQLUser(context.Background(), "cluster-1", "prefix.tdc_rw"); err != nil { + if _, err := client.GetSQLUser(context.Background(), "cluster-1", "prefix.ti_rw"); err != nil { t.Fatalf("GetSQLUser failed: %v", err) } - if _, err := client.UpdateSQLUser(context.Background(), "cluster-1", "prefix.tdc_rw", UpdateSQLUserRequest{Password: "rotated"}); err != nil { + if _, err := client.UpdateSQLUser(context.Background(), "cluster-1", "prefix.ti_rw", UpdateSQLUserRequest{Password: "rotated"}); err != nil { t.Fatalf("UpdateSQLUser failed: %v", err) } - if err := client.DeleteSQLUser(context.Background(), "cluster-1", "prefix.tdc_rw"); err != nil { + if err := client.DeleteSQLUser(context.Background(), "cluster-1", "prefix.ti_rw"); err != nil { t.Fatalf("DeleteSQLUser failed: %v", err) } want := []string{ "GET /v1beta1/clusters/cluster-1/sqlUsers?pageSize=2", "POST /v1beta1/clusters/cluster-1/sqlUsers", - "GET /v1beta1/clusters/cluster-1/sqlUsers/prefix.tdc_rw", - "PATCH /v1beta1/clusters/cluster-1/sqlUsers/prefix.tdc_rw", - "DELETE /v1beta1/clusters/cluster-1/sqlUsers/prefix.tdc_rw", + "GET /v1beta1/clusters/cluster-1/sqlUsers/prefix.ti_rw", + "PATCH /v1beta1/clusters/cluster-1/sqlUsers/prefix.ti_rw", + "DELETE /v1beta1/clusters/cluster-1/sqlUsers/prefix.ti_rw", } for i := range want { if requests[i] != want[i] { diff --git a/internal/api/oplog_transport.go b/internal/api/oplog_transport.go index d875b70..7d36c61 100644 --- a/internal/api/oplog_transport.go +++ b/internal/api/oplog_transport.go @@ -5,9 +5,9 @@ import ( "strings" "time" - "github.com/tidbcloud/tdc/internal/api/endpoints" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/oplog" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/oplog" ) type oplogRoundTripper struct { @@ -66,7 +66,7 @@ func serviceName(service endpoints.Service) string { case endpoints.ServiceIAM: return "tidb_cloud_iam" case endpoints.ServiceFS: - return "tdc_fs" + return "ti_fs" default: if service == "" { return "api" diff --git a/internal/api/starter/branch_test.go b/internal/api/starter/branch_test.go index d5c2d8f..4855518 100644 --- a/internal/api/starter/branch_test.go +++ b/internal/api/starter/branch_test.go @@ -7,7 +7,7 @@ import ( "net/http/httptest" "testing" - "github.com/tidbcloud/tdc/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/apperr" ) func TestListBranches(t *testing.T) { diff --git a/internal/api/starter/client.go b/internal/api/starter/client.go index 44846dd..20057af 100644 --- a/internal/api/starter/client.go +++ b/internal/api/starter/client.go @@ -7,7 +7,7 @@ import ( "strconv" "strings" - "github.com/tidbcloud/tdc/internal/api" + "github.com/tidbcloud/ti-cli/internal/api" ) const ProjectLabelKey = "tidb.cloud/project" diff --git a/internal/api/starter/client_test.go b/internal/api/starter/client_test.go index 5742248..e5b72e6 100644 --- a/internal/api/starter/client_test.go +++ b/internal/api/starter/client_test.go @@ -7,10 +7,10 @@ import ( "net/http/httptest" "testing" - "github.com/tidbcloud/tdc/internal/api" - "github.com/tidbcloud/tdc/internal/api/endpoints" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" + "github.com/tidbcloud/ti-cli/internal/api" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" ) func TestListClusters(t *testing.T) { diff --git a/internal/api/transport/transport.go b/internal/api/transport/transport.go index 8da31f5..6ea5760 100644 --- a/internal/api/transport/transport.go +++ b/internal/api/transport/transport.go @@ -72,12 +72,12 @@ type debugRoundTripper struct { } func (t debugRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - fmt.Fprintf(t.writer, "tdc debug: request %s %s\n", req.Method, t.redactor.Redact(req.URL.String())) + fmt.Fprintf(t.writer, "ti debug: request %s %s\n", req.Method, t.redactor.Redact(req.URL.String())) res, err := t.base.RoundTrip(req) if err != nil { - fmt.Fprintf(t.writer, "tdc debug: error %s\n", t.redactor.Redact(err.Error())) + fmt.Fprintf(t.writer, "ti debug: error %s\n", t.redactor.Redact(err.Error())) return nil, err } - fmt.Fprintf(t.writer, "tdc debug: response %d %s\n", res.StatusCode, http.StatusText(res.StatusCode)) + fmt.Fprintf(t.writer, "ti debug: response %d %s\n", res.StatusCode, http.StatusText(res.StatusCode)) return res, nil } diff --git a/internal/api/transport/transport_test.go b/internal/api/transport/transport_test.go index 648fa51..c8aeb64 100644 --- a/internal/api/transport/transport_test.go +++ b/internal/api/transport/transport_test.go @@ -17,7 +17,7 @@ func TestDigestTransportUsesDigestNotBasic(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if auth == "" { - w.Header().Set("WWW-Authenticate", `Digest realm="tdc", nonce="nonce", qop="auth"`) + w.Header().Set("WWW-Authenticate", `Digest realm="ti", nonce="nonce", qop="auth"`) w.WriteHeader(http.StatusUnauthorized) return } diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 414bf5f..7143b09 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -8,9 +8,9 @@ import ( "strings" "unicode" - "github.com/tidbcloud/tdc/internal/api/transport" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config" + "github.com/tidbcloud/ti-cli/internal/api/transport" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config" ) type Credentials struct { @@ -36,9 +36,9 @@ func LoadProfile(ctx context.Context, opts config.LoadOptions) (*config.Profile, if profileName == "" { profileName = config.DefaultProfile } - return nil, MissingCredentials(profileName, "tdc_public_key", "tdc_private_key") + return nil, MissingCredentials(profileName, "tidb_cloud_public_key", "tidb_cloud_private_key") case "config.env_missing": - if strings.Contains(appErr.Message, "TDC_PUBLIC_KEY") || strings.Contains(appErr.Message, "TDC_PRIVATE_KEY") { + if strings.Contains(appErr.Message, "TIDB_CLOUD_PUBLIC_KEY") || strings.Contains(appErr.Message, "TIDB_CLOUD_PRIVATE_KEY") { return nil, MissingEnvironmentCredentials() } } @@ -49,7 +49,7 @@ func LoadProfile(ctx context.Context, opts config.LoadOptions) (*config.Profile, func ValidateProfile(profile *config.Profile) (Credentials, error) { if profile == nil { - return Credentials{}, MissingCredentials(config.DefaultProfile, "tdc_public_key", "tdc_private_key") + return Credentials{}, MissingCredentials(config.DefaultProfile, "tidb_cloud_public_key", "tidb_cloud_private_key") } profileName := profile.Name @@ -58,27 +58,27 @@ func ValidateProfile(profile *config.Profile) (Credentials, error) { } missing := make([]string, 0, 2) - if strings.TrimSpace(profile.TDCPublicKey) == "" { - missing = append(missing, "tdc_public_key") + if strings.TrimSpace(profile.TiDBCloudPublicKey) == "" { + missing = append(missing, "tidb_cloud_public_key") } - if strings.TrimSpace(profile.TDCPrivateKey) == "" { - missing = append(missing, "tdc_private_key") + if strings.TrimSpace(profile.TiDBCloudPrivateKey) == "" { + missing = append(missing, "tidb_cloud_private_key") } if len(missing) > 0 { return Credentials{}, MissingCredentials(profileName, missing...) } - if malformedCredential(profile.TDCPublicKey) || strings.Contains(profile.TDCPublicKey, ":") { - return Credentials{}, MalformedCredentials(profileName, "tdc_public_key") + if malformedCredential(profile.TiDBCloudPublicKey) || strings.Contains(profile.TiDBCloudPublicKey, ":") { + return Credentials{}, MalformedCredentials(profileName, "tidb_cloud_public_key") } - if malformedCredential(profile.TDCPrivateKey) { - return Credentials{}, MalformedCredentials(profileName, "tdc_private_key") + if malformedCredential(profile.TiDBCloudPrivateKey) { + return Credentials{}, MalformedCredentials(profileName, "tidb_cloud_private_key") } return Credentials{ ProfileName: profileName, - PublicKey: profile.TDCPublicKey, - PrivateKey: profile.TDCPrivateKey, + PublicKey: profile.TiDBCloudPublicKey, + PrivateKey: profile.TiDBCloudPrivateKey, }, nil } @@ -91,14 +91,14 @@ func MissingCredentials(profileName string, keys ...string) error { profileName = config.DefaultProfile } if len(keys) == 0 { - keys = []string{"tdc_public_key", "tdc_private_key"} + keys = []string{"tidb_cloud_public_key", "tidb_cloud_private_key"} } return apperr.New( "auth.missing_credentials", "authentication", 3, fmt.Sprintf( - "authentication required: missing %s for profile %q. Run `tdc configure` or set TDC_PUBLIC_KEY and TDC_PRIVATE_KEY.", + "authentication required: missing %s for profile %q. Run `ti configure` or set TIDB_CLOUD_PUBLIC_KEY and TIDB_CLOUD_PRIVATE_KEY.", joinKeys(keys), profileName, ), @@ -110,7 +110,7 @@ func MissingEnvironmentCredentials() error { "auth.missing_environment_credentials", "authentication", 3, - "authentication required: missing environment credentials. Set both TDC_PUBLIC_KEY and TDC_PRIVATE_KEY, or unset them to use profile credentials.", + "authentication required: missing environment credentials. Set both TIDB_CLOUD_PUBLIC_KEY and TIDB_CLOUD_PRIVATE_KEY, or unset them to use profile credentials.", ) } @@ -122,7 +122,7 @@ func MalformedCredentials(profileName, key string) error { "auth.malformed_credentials", "authentication", 3, - fmt.Sprintf("authentication failed: malformed %s for profile %q. Check ~/.tdc/credentials or create a new API key.", key, profileName), + fmt.Sprintf("authentication failed: malformed %s for profile %q. Check ~/.ti/credentials or create a new API key.", key, profileName), ) } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 4a122cd..89cd52d 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -5,9 +5,9 @@ import ( "strings" "testing" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/config/store" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/config/store" ) func TestValidateProfileMissingCredentials(t *testing.T) { @@ -18,16 +18,16 @@ func TestValidateProfileMissingCredentials(t *testing.T) { if got := apperr.ExitCodeFor(err); got != 3 { t.Fatalf("expected auth exit code 3, got %d", got) } - if got := apperr.MessageFor(err); !strings.Contains(got, "authentication required") || !strings.Contains(got, "tdc_public_key and tdc_private_key") { + if got := apperr.MessageFor(err); !strings.Contains(got, "authentication required") || !strings.Contains(got, "tidb_cloud_public_key and tidb_cloud_private_key") { t.Fatalf("unexpected message %q", got) } } func TestValidateProfileRejectsMalformedCredentials(t *testing.T) { _, err := ValidateProfile(&config.Profile{ - Name: "stage", - TDCPublicKey: "public:key", - TDCPrivateKey: "private", + Name: "stage", + TiDBCloudPublicKey: "public:key", + TiDBCloudPrivateKey: "private", }) if err == nil { t.Fatal("expected malformed credentials to fail") @@ -86,7 +86,7 @@ func TestLoadProfileMissingPartialEnvironmentCredentialsDoesNotReportEnvProfile( _, err := LoadProfile(context.Background(), config.LoadOptions{ HomeDir: home, Env: map[string]string{ - "TDC_PUBLIC_KEY": "env-public", + "TIDB_CLOUD_PUBLIC_KEY": "env-public", }, }) if err == nil { diff --git a/internal/authz/authz.go b/internal/authz/authz.go index f8f8f02..138a038 100644 --- a/internal/authz/authz.go +++ b/internal/authz/authz.go @@ -3,8 +3,8 @@ package authz import ( "fmt" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config" ) type Permission string @@ -45,68 +45,68 @@ const ( ) var commandPermissions = map[string]Permission{ - "tdc organization list-projects": OrganizationProjectRead, - "tdc db create-db-cluster": StarterClusterCreate, - "tdc db list-db-clusters": StarterClusterRead, - "tdc db describe-db-cluster": StarterClusterRead, - "tdc db update-db-cluster": StarterClusterUpdate, - "tdc db delete-db-cluster": StarterClusterDelete, - "tdc db create-db-cluster-branch": StarterBranchCreate, - "tdc db list-db-cluster-branches": StarterBranchRead, - "tdc db describe-db-cluster-branch": StarterBranchRead, - "tdc db delete-db-cluster-branch": StarterBranchDelete, - "tdc db create-db-sql-users": StarterSQLUserCreate, - "tdc db format-db-connection-string": StarterSQLUserRead, - "tdc db execute-sql-statement": StarterSQLExecute, - "tdc fs create-file-system": FSVolumeCreate, - "tdc fs delete-file-system": FSVolumeDelete, - "tdc fs list-file-systems": FSVolumeRead, - "tdc fs describe-file-system": FSVolumeRead, - "tdc fs check-file-system": FSVolumeRead, - "tdc fs copy-file": FSFileWrite, - "tdc fs read-file": FSFileRead, - "tdc fs list-files": FSFileRead, - "tdc fs describe-file": FSFileRead, - "tdc fs move-file": FSFileWrite, - "tdc fs delete-file": FSFileWrite, - "tdc fs create-directory": FSFileWrite, - "tdc fs chmod-file": FSFileWrite, - "tdc fs create-symlink": FSFileWrite, - "tdc fs create-hardlink": FSFileWrite, - "tdc fs search-file-content": FSFileRead, - "tdc fs find-files": FSFileRead, - "tdc fs create-layer": FSFileWrite, - "tdc fs list-layers": FSFileRead, - "tdc fs describe-layer": FSFileRead, - "tdc fs diff-layer": FSFileRead, - "tdc fs create-layer-checkpoint": FSFileWrite, - "tdc fs rollback-layer": FSFileWrite, - "tdc fs commit-layer": FSFileWrite, - "tdc fs pack-file-system": FSFileWrite, - "tdc fs unpack-file-system": FSFileRead, - "tdc fs mount-file-system": FSMount, - "tdc fs drain-file-system": FSMount, - "tdc fs unmount-file-system": FSMount, - "tdc fs-vault create-secret": FSVaultSecretCreate, - "tdc fs-vault replace-secret": FSVaultSecretUpdate, - "tdc fs-vault read-secret": FSVaultSecretRead, - "tdc fs-vault list-secrets": FSVaultSecretRead, - "tdc fs-vault delete-secret": FSVaultSecretDelete, - "tdc fs-vault create-grant": FSVaultGrantCreate, - "tdc fs-vault delete-grant": FSVaultGrantDelete, - "tdc fs-vault list-audit-events": FSVaultAuditRead, - "tdc fs-vault run-with-secret": FSVaultSecretRead, - "tdc fs-vault mount-vault": FSVaultSecretRead, - "tdc fs-vault unmount-vault": FSVaultSecretRead, - "tdc fs-journal create-journal": FSJournalCreate, - "tdc fs-journal append-journal-entries": FSJournalAppend, - "tdc fs-journal read-journal-entries": FSJournalRead, - "tdc fs-journal search-journal-entries": FSJournalSearch, - "tdc fs-journal verify-journal": FSJournalVerify, - "tdc fs-git clone-git-workspace": FSGitWorkspaceWrite, - "tdc fs-git hydrate-git-workspace": FSGitWorkspaceRead, - "tdc fs-git add-git-worktree": FSGitWorkspaceWrite, - "tdc fs-git remove-git-worktree": FSGitWorkspaceWrite, + "ti organization list-projects": OrganizationProjectRead, + "ti db create-db-cluster": StarterClusterCreate, + "ti db list-db-clusters": StarterClusterRead, + "ti db describe-db-cluster": StarterClusterRead, + "ti db update-db-cluster": StarterClusterUpdate, + "ti db delete-db-cluster": StarterClusterDelete, + "ti db create-db-cluster-branch": StarterBranchCreate, + "ti db list-db-cluster-branches": StarterBranchRead, + "ti db describe-db-cluster-branch": StarterBranchRead, + "ti db delete-db-cluster-branch": StarterBranchDelete, + "ti db create-db-sql-users": StarterSQLUserCreate, + "ti db format-db-connection-string": StarterSQLUserRead, + "ti db execute-sql-statement": StarterSQLExecute, + "ti fs create-file-system": FSVolumeCreate, + "ti fs delete-file-system": FSVolumeDelete, + "ti fs list-file-systems": FSVolumeRead, + "ti fs describe-file-system": FSVolumeRead, + "ti fs check-file-system": FSVolumeRead, + "ti fs copy-file": FSFileWrite, + "ti fs read-file": FSFileRead, + "ti fs list-files": FSFileRead, + "ti fs describe-file": FSFileRead, + "ti fs move-file": FSFileWrite, + "ti fs delete-file": FSFileWrite, + "ti fs create-directory": FSFileWrite, + "ti fs chmod-file": FSFileWrite, + "ti fs create-symlink": FSFileWrite, + "ti fs create-hardlink": FSFileWrite, + "ti fs search-file-content": FSFileRead, + "ti fs find-files": FSFileRead, + "ti fs create-layer": FSFileWrite, + "ti fs list-layers": FSFileRead, + "ti fs describe-layer": FSFileRead, + "ti fs diff-layer": FSFileRead, + "ti fs create-layer-checkpoint": FSFileWrite, + "ti fs rollback-layer": FSFileWrite, + "ti fs commit-layer": FSFileWrite, + "ti fs pack-file-system": FSFileWrite, + "ti fs unpack-file-system": FSFileRead, + "ti fs mount-file-system": FSMount, + "ti fs drain-file-system": FSMount, + "ti fs unmount-file-system": FSMount, + "ti fs-vault create-secret": FSVaultSecretCreate, + "ti fs-vault replace-secret": FSVaultSecretUpdate, + "ti fs-vault read-secret": FSVaultSecretRead, + "ti fs-vault list-secrets": FSVaultSecretRead, + "ti fs-vault delete-secret": FSVaultSecretDelete, + "ti fs-vault create-grant": FSVaultGrantCreate, + "ti fs-vault delete-grant": FSVaultGrantDelete, + "ti fs-vault list-audit-events": FSVaultAuditRead, + "ti fs-vault run-with-secret": FSVaultSecretRead, + "ti fs-vault mount-vault": FSVaultSecretRead, + "ti fs-vault unmount-vault": FSVaultSecretRead, + "ti fs-journal create-journal": FSJournalCreate, + "ti fs-journal append-journal-entries": FSJournalAppend, + "ti fs-journal read-journal-entries": FSJournalRead, + "ti fs-journal search-journal-entries": FSJournalSearch, + "ti fs-journal verify-journal": FSJournalVerify, + "ti fs-git clone-git-workspace": FSGitWorkspaceWrite, + "ti fs-git hydrate-git-workspace": FSGitWorkspaceRead, + "ti fs-git add-git-worktree": FSGitWorkspaceWrite, + "ti fs-git remove-git-worktree": FSGitWorkspaceWrite, } func ForCommand(commandPath string) (Permission, error) { diff --git a/internal/authz/authz_test.go b/internal/authz/authz_test.go index 3de7bc7..4919659 100644 --- a/internal/authz/authz_test.go +++ b/internal/authz/authz_test.go @@ -4,19 +4,19 @@ import ( "strings" "testing" - "github.com/tidbcloud/tdc/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/apperr" ) func TestForCommand(t *testing.T) { tests := map[string]Permission{ - "tdc organization list-projects": OrganizationProjectRead, - "tdc db create-db-cluster": StarterClusterCreate, - "tdc db list-db-clusters": StarterClusterRead, - "tdc db execute-sql-statement": StarterSQLExecute, - "tdc fs create-file-system": FSVolumeCreate, - "tdc fs search-file-content": FSFileRead, - "tdc fs mount-file-system": FSMount, - "tdc fs-vault mount-vault": FSVaultSecretRead, + "ti organization list-projects": OrganizationProjectRead, + "ti db create-db-cluster": StarterClusterCreate, + "ti db list-db-clusters": StarterClusterRead, + "ti db execute-sql-statement": StarterSQLExecute, + "ti fs create-file-system": FSVolumeCreate, + "ti fs search-file-content": FSFileRead, + "ti fs mount-file-system": FSMount, + "ti fs-vault mount-vault": FSVaultSecretRead, } for command, want := range tests { diff --git a/internal/cli/commands.go b/internal/cli/commands.go index 0c86d8d..25bc0ef 100644 --- a/internal/cli/commands.go +++ b/internal/cli/commands.go @@ -2,30 +2,30 @@ package cli import ( "fmt" - "os" "strings" "time" "github.com/spf13/cobra" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" - cfgconfigure "github.com/tidbcloud/tdc/internal/config/configure" - "github.com/tidbcloud/tdc/internal/db" - "github.com/tidbcloud/tdc/internal/db/connectionstring" - "github.com/tidbcloud/tdc/internal/dryrun" - tdcfs "github.com/tidbcloud/tdc/internal/fs" - "github.com/tidbcloud/tdc/internal/fs/fscred" - "github.com/tidbcloud/tdc/internal/organization" - outputpkg "github.com/tidbcloud/tdc/internal/output" - "github.com/tidbcloud/tdc/internal/update" - "github.com/tidbcloud/tdc/internal/version" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" + cfgconfigure "github.com/tidbcloud/ti-cli/internal/config/configure" + "github.com/tidbcloud/ti-cli/internal/config/envcompat" + "github.com/tidbcloud/ti-cli/internal/db" + "github.com/tidbcloud/ti-cli/internal/db/connectionstring" + "github.com/tidbcloud/ti-cli/internal/dryrun" + tifs "github.com/tidbcloud/ti-cli/internal/fs" + "github.com/tidbcloud/ti-cli/internal/fs/fscred" + "github.com/tidbcloud/ti-cli/internal/organization" + outputpkg "github.com/tidbcloud/ti-cli/internal/output" + "github.com/tidbcloud/ti-cli/internal/update" + "github.com/tidbcloud/ti-cli/internal/version" ) func newConfigureCommand(info version.Info) *cobra.Command { cmd := newCommand(commandSpec{ Use: "configure", - Short: "Configure TiDB Cloud CLI (tdc) options. If this command runs with no arguments, you will be prompted for configuration values.", + Short: "Configure TiDB Cloud CLI (ti) options. If this command runs with no arguments, you will be prompted for configuration values.", RunE: func(cmd *cobra.Command, _ []string) error { profile, err := cmd.Flags().GetString("profile") if err != nil { @@ -36,18 +36,24 @@ func newConfigureCommand(info version.Info) *cobra.Command { if strings.TrimSpace(profile) == "" { return apperr.New("config.empty_profile", "usage", 2, "--profile cannot be empty") } - } else if envProfile := strings.TrimSpace(os.Getenv("TDC_PROFILE")); envProfile != "" { - profile = envProfile + } else { + envProfile, _, _, err := envcompat.ResolveNames(nil, "TI_PROFILE", "TDC_PROFILE") + if err != nil { + return err + } + if strings.TrimSpace(envProfile) != "" { + profile = strings.TrimSpace(envProfile) + } } regionCode, err := cmd.Flags().GetString("region-code") if err != nil { return err } - publicKey, err := cmd.Flags().GetString("tdc-public-key") + publicKey, err := cmd.Flags().GetString("tidb-cloud-public-key") if err != nil { return err } - privateKey, err := cmd.Flags().GetString("tdc-private-key") + privateKey, err := cmd.Flags().GetString("tidb-cloud-private-key") if err != nil { return err } @@ -60,15 +66,15 @@ func newConfigureCommand(info version.Info) *cobra.Command { return err } result, err := cfgconfigure.Run(cmd.Context(), cfgconfigure.Options{ - Profile: profile, - RegionCode: regionCode, - TDCPublicKey: publicKey, - TDCPrivateKey: privateKey, - NonInteractive: nonInteractive, - In: cmd.InOrStdin(), - Out: cmd.OutOrStdout(), - Debug: debug, - DebugWriter: cmd.ErrOrStderr(), + Profile: profile, + RegionCode: regionCode, + TiDBCloudPublicKey: publicKey, + TiDBCloudPrivateKey: privateKey, + NonInteractive: nonInteractive, + In: cmd.InOrStdin(), + Out: cmd.OutOrStdout(), + Debug: debug, + DebugWriter: cmd.ErrOrStderr(), }) if err != nil { return err @@ -77,9 +83,9 @@ func newConfigureCommand(info version.Info) *cobra.Command { }, }, info) cmd.Flags().String("region-code", "", "Default TiDB Cloud region code for the profile, for example aws-us-east-1 or aws-ap-southeast-1.") - cmd.Flags().String("tdc-public-key", "", "TiDB Cloud API public key.") - cmd.Flags().String("tdc-private-key", "", "TiDB Cloud API private key.") - cmd.Flags().Bool("non-interactive", false, "Use this option to avoid being prompted for configuration values. You must provide at least three configuration values (--tdc-public-key, --tdc-private-key, and --region-code) when using this option. This is useful when running tdc in a script or automated environment.") + cmd.Flags().String("tidb-cloud-public-key", "", "TiDB Cloud API public key.") + cmd.Flags().String("tidb-cloud-private-key", "", "TiDB Cloud API private key.") + cmd.Flags().Bool("non-interactive", false, "Use this option to avoid being prompted for configuration values. You must provide at least three configuration values (--tidb-cloud-public-key, --tidb-cloud-private-key, and --region-code) when using this option. This is useful when running ti in a script or automated environment.") return cmd } @@ -108,7 +114,7 @@ func newUpdateCommand(info version.Info) *cobra.Command { return err } if failIfAvailable && result.UpdateAvailable { - return apperr.New("update.available", "runtime", 1, "a newer tdc release is available") + return apperr.New("update.available", "runtime", 1, "a newer ti release is available") } return nil } @@ -139,9 +145,9 @@ func newUpdateCommand(info version.Info) *cobra.Command { return renderStructured(cmd, result) }, }, info) - cmd.Flags().Bool("check", false, "Check whether a newer TiDB Cloud CLI (tdc) release is available without updating.") + cmd.Flags().Bool("check", false, "Check whether a newer TiDB Cloud CLI (ti) release is available without updating.") cmd.Flags().Bool("fail-if-update-available", false, "With --check, exit with code 1 when an update is available.") - cmd.Flags().String("target-version", "latest", "Target TiDB Cloud CLI (tdc) version, such as latest or v0.1.0.") + cmd.Flags().String("target-version", "latest", "Target TiDB Cloud CLI (ti) version, such as latest or v0.1.0.") cmd.Flags().Bool("dry-run", false, "Show the update plan without changing the local binary.") return cmd } @@ -519,7 +525,7 @@ func newDBDeleteBranchCommand(info version.Info) *cobra.Command { func newDBPrepareQueryAccessCommand(info version.Info) *cobra.Command { cmd := newControlPlaneCommand(controlPlaneCommandSpec{ Use: "create-db-sql-users", - Short: "Provision a group of database users managed by tdc locally, with roles (admin, read-only, and read-write) for developer and agent access. And then, you can call format-db-connection-string to get the connection string for the users selectively.", + Short: "Provision a group of database users managed by ti locally, with roles (admin, read-only, and read-write) for developer and agent access. And then, you can call format-db-connection-string to get the connection string for the users selectively.", Mutation: mutatingCommand, Permission: authz.StarterSQLUserCreate, Run: func(ctx commandContext) (any, error) { @@ -559,7 +565,7 @@ func newDBPrepareQueryAccessCommand(info version.Info) *cobra.Command { func newDBCreateConnectionStringCommand(info version.Info) *cobra.Command { cmd := newControlPlaneCommand(controlPlaneCommandSpec{ Use: "format-db-connection-string", - Short: "Generate a database connection string for tdc-managed database user.", + Short: "Generate a database connection string for ti-managed database user.", Mutation: readOnlyCommand, Permission: authz.StarterSQLUserRead, Run: func(ctx commandContext) (any, error) { @@ -899,7 +905,7 @@ func addFSSelectorFlags(commands []*cobra.Command, excluded ...string) { continue } if command.Flags().Lookup("file-system-name") == nil { - command.Flags().String("file-system-name", "", "The name of the file system. Can also be supplied through TDC_FS_FILE_SYSTEM_NAME.") + command.Flags().String("file-system-name", "", "The name of the file system. Can also be supplied through TI_FS_FILE_SYSTEM_NAME.") } } } @@ -914,7 +920,7 @@ func addFSAuthFlags(commands []*cobra.Command, excluded ...string) { continue } if command.Flags().Lookup("fs-token") == nil { - command.Flags().String("fs-token", "", "File system user token. Default: value taken from the TDC_FS_TOKEN environment variable if not provided.") + command.Flags().String("fs-token", "", "File system user token. Default: value taken from the TI_FS_TOKEN environment variable if not provided.") } } } @@ -926,7 +932,7 @@ func newFSCreateFileSystemCommand(info version.Info) *cobra.Command { Mutation: mutatingCommand, Permission: authz.FSVolumeCreate, Run: func(ctx commandContext) (any, error) { - service, profile, err := fsTDCServiceAndProfile(ctx) + service, profile, err := fsTIServiceAndProfile(ctx) if err != nil { return nil, err } @@ -941,14 +947,14 @@ func newFSCreateFileSystemCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.CreateFileSystem(ctx.cmd.Context(), tdcfs.CreateFileSystemOptions{ + return service.CreateFileSystem(ctx.cmd.Context(), tifs.CreateFileSystemOptions{ Profile: profile, FileSystemName: name, WaitUntilReady: waitUntilReady, }) }, DryRun: func(ctx commandContext) (dryrun.Result, error) { - service, profile, err := fsTDCServiceAndProfile(ctx) + service, profile, err := fsTIServiceAndProfile(ctx) if err != nil { return dryrun.Result{}, err } @@ -960,7 +966,7 @@ func newFSCreateFileSystemCommand(info version.Info) *cobra.Command { if err != nil { return dryrun.Result{}, err } - return service.DryRunCreateFileSystem(ctx.cmd.Context(), ctx.CommandPath(), tdcfs.CreateFileSystemOptions{ + return service.DryRunCreateFileSystem(ctx.cmd.Context(), ctx.CommandPath(), tifs.CreateFileSystemOptions{ Profile: profile, FileSystemName: name, WaitUntilReady: waitUntilReady, @@ -1015,7 +1021,7 @@ func newFSDeleteFileSystemCommand(info version.Info) *cobra.Command { Mutation: mutatingCommand, Permission: authz.FSVolumeDelete, Run: func(ctx commandContext) (any, error) { - service, profile, err := fsTDCResourceServiceAndProfile(ctx) + service, profile, err := fsTIResourceServiceAndProfile(ctx) if err != nil { return nil, err } @@ -1023,13 +1029,13 @@ func newFSDeleteFileSystemCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.DeleteFileSystem(ctx.cmd.Context(), tdcfs.DeleteFileSystemOptions{ + return service.DeleteFileSystem(ctx.cmd.Context(), tifs.DeleteFileSystemOptions{ Profile: profile, FileSystemName: name, }) }, DryRun: func(ctx commandContext) (dryrun.Result, error) { - service, profile, err := fsTDCResourceServiceAndProfile(ctx) + service, profile, err := fsTIResourceServiceAndProfile(ctx) if err != nil { return dryrun.Result{}, err } @@ -1037,7 +1043,7 @@ func newFSDeleteFileSystemCommand(info version.Info) *cobra.Command { if err != nil { return dryrun.Result{}, err } - return service.DryRunDeleteFileSystem(ctx.cmd.Context(), ctx.CommandPath(), tdcfs.DeleteFileSystemOptions{ + return service.DryRunDeleteFileSystem(ctx.cmd.Context(), ctx.CommandPath(), tifs.DeleteFileSystemOptions{ Profile: profile, FileSystemName: name, }) @@ -1059,7 +1065,7 @@ func newFSCheckFileSystemCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.CheckFileSystem(ctx.cmd.Context(), tdcfs.CheckFileSystemOptions{ + return service.CheckFileSystem(ctx.cmd.Context(), tifs.CheckFileSystemOptions{ Profile: profile, }) }, @@ -1086,7 +1092,7 @@ func newFSCopyFileCommand(info version.Info) *cobra.Command { if opts.FromRemote == "" { return nil, apperr.New("fs.invalid_copy_flags", "usage", 2, "--to-stdout requires --from-remote") } - data, err := service.ReadFile(ctx.cmd.Context(), tdcfs.ReadFileOptions{Profile: profile, Path: opts.FromRemote}) + data, err := service.ReadFile(ctx.cmd.Context(), tifs.ReadFileOptions{Profile: profile, Path: opts.FromRemote}) if err != nil { return nil, err } @@ -1140,7 +1146,7 @@ func newFSReadFileCommand(info version.Info) *cobra.Command { if ctx.FlagChanged("offset") != ctx.FlagChanged("length") { return nil, apperr.New("fs.invalid_range", "usage", 2, "--offset and --length must be provided together") } - data, err := service.ReadFile(ctx.cmd.Context(), tdcfs.ReadFileOptions{ + data, err := service.ReadFile(ctx.cmd.Context(), tifs.ReadFileOptions{ Profile: profile, Path: path, Range: rangeSet, @@ -1153,7 +1159,7 @@ func newFSReadFileCommand(info version.Info) *cobra.Command { return outputpkg.Raw{Bytes: data}, nil }, }, info) - cmd.Flags().String("path", "", "tdc fs file path") + cmd.Flags().String("path", "", "ti fs file path") cmd.Flags().Int64("offset", 0, "zero-based byte offset for a ranged read") cmd.Flags().Int64("length", 0, "byte length for a ranged read") markUsageRequired(cmd, "path") @@ -1176,7 +1182,7 @@ func newFSListFilesCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.ListFiles(ctx.cmd.Context(), tdcfs.ListFilesOptions{ + return service.ListFiles(ctx.cmd.Context(), tifs.ListFilesOptions{ Profile: profile, Path: path, }) @@ -1202,7 +1208,7 @@ func newFSDescribeFileCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.DescribeFile(ctx.cmd.Context(), tdcfs.DescribeFileOptions{ + return service.DescribeFile(ctx.cmd.Context(), tifs.DescribeFileOptions{ Profile: profile, Path: path, }) @@ -1237,7 +1243,7 @@ func newFSMoveFileCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.MoveFile(ctx.cmd.Context(), tdcfs.MoveFileOptions{ + return service.MoveFile(ctx.cmd.Context(), tifs.MoveFileOptions{ Profile: profile, FromRemote: fromRemote, ToRemote: toRemote, @@ -1272,7 +1278,7 @@ func newFSDeleteFileCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.DeleteFile(ctx.cmd.Context(), tdcfs.DeleteFileOptions{ + return service.DeleteFile(ctx.cmd.Context(), tifs.DeleteFileOptions{ Profile: profile, Path: path, Recursive: recursive, @@ -1305,7 +1311,7 @@ func newFSCreateDirectoryCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.CreateDirectory(ctx.cmd.Context(), tdcfs.CreateDirectoryOptions{ + return service.CreateDirectory(ctx.cmd.Context(), tifs.CreateDirectoryOptions{ Profile: profile, Path: path, Mode: mode, @@ -1338,7 +1344,7 @@ func newFSChmodFileCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.ChmodFile(ctx.cmd.Context(), tdcfs.ChmodFileOptions{Profile: profile, Path: path, Mode: mode}) + return service.ChmodFile(ctx.cmd.Context(), tifs.ChmodFileOptions{Profile: profile, Path: path, Mode: mode}) }, }, info) cmd.Flags().String("path", "", "File or directory path.") @@ -1367,7 +1373,7 @@ func newFSSymlinkFileCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.SymlinkFile(ctx.cmd.Context(), tdcfs.SymlinkFileOptions{Profile: profile, Target: target, Link: link}) + return service.SymlinkFile(ctx.cmd.Context(), tifs.SymlinkFileOptions{Profile: profile, Target: target, Link: link}) }, }, info) cmd.Flags().String("target", "", "The actual file path being linked to.") @@ -1396,7 +1402,7 @@ func newFSHardlinkFileCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.HardlinkFile(ctx.cmd.Context(), tdcfs.HardlinkFileOptions{Profile: profile, Source: source, Link: link}) + return service.HardlinkFile(ctx.cmd.Context(), tifs.HardlinkFileOptions{Profile: profile, Source: source, Link: link}) }, }, info) cmd.Flags().String("source-path", "", "The existing file path in the TiDB Cloud file system.") @@ -1433,7 +1439,7 @@ func newFSSearchFileContentCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.SearchFileContent(ctx.cmd.Context(), tdcfs.SearchFileContentOptions{ + return service.SearchFileContent(ctx.cmd.Context(), tifs.SearchFileContentOptions{ Profile: profile, Path: path, Pattern: pattern, @@ -1508,7 +1514,7 @@ func newFSCreateLayerCommand(info version.Info) *cobra.Command { if err != nil { return dryrun.Result{}, err } - tags, err := tdcfs.ParseLayerTagsForDryRun(opts.Tags) + tags, err := tifs.ParseLayerTagsForDryRun(opts.Tags) if err != nil { return dryrun.Result{}, err } @@ -1544,7 +1550,7 @@ func newFSListLayersCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.ListLayers(ctx.cmd.Context(), tdcfs.ListLayersOptions{Profile: profile}) + return service.ListLayers(ctx.cmd.Context(), tifs.ListLayersOptions{Profile: profile}) }, }, info) } @@ -1564,7 +1570,7 @@ func newFSDescribeLayerCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.DescribeLayer(ctx.cmd.Context(), tdcfs.DescribeLayerOptions{Profile: profile, LayerID: layerID}) + return service.DescribeLayer(ctx.cmd.Context(), tifs.DescribeLayerOptions{Profile: profile, LayerID: layerID}) }, }, info) cmd.Flags().String("layer-id", "", "The ID of the specified file system layer.") @@ -1650,7 +1656,7 @@ func newFSRollbackLayerCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.RollbackLayer(ctx.cmd.Context(), tdcfs.LayerActionOptions{Profile: profile, LayerID: layerID}) + return service.RollbackLayer(ctx.cmd.Context(), tifs.LayerActionOptions{Profile: profile, LayerID: layerID}) }, DryRun: func(ctx commandContext) (dryrun.Result, error) { service, profile, err := fsServiceAndProfile(ctx) @@ -1684,7 +1690,7 @@ func newFSCommitLayerCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.CommitLayer(ctx.cmd.Context(), tdcfs.LayerActionOptions{Profile: profile, LayerID: layerID}) + return service.CommitLayer(ctx.cmd.Context(), tifs.LayerActionOptions{Profile: profile, LayerID: layerID}) }, DryRun: func(ctx commandContext) (dryrun.Result, error) { service, profile, err := fsServiceAndProfile(ctx) @@ -1698,7 +1704,7 @@ func newFSCommitLayerCommand(info version.Info) *cobra.Command { return service.DryRunLayerMutation(ctx.cmd.Context(), ctx.CommandPath(), "commit_layer", "POST", "/v1/layers/"+layerID+"/commit", map[string]any{}, profile, authz.FSFileWrite) }, }, info) - cmd.Flags().String("layer-id", "", "tdc fs layer id") + cmd.Flags().String("layer-id", "", "ti fs layer id") markUsageRequired(cmd, "layer-id") return cmd } @@ -1809,20 +1815,20 @@ func newFSMountFileSystemCommand(info version.Info) *cobra.Command { return service.DryRunMountFileSystem(ctx.cmd.Context(), ctx.CommandPath(), opts) }, }, info) - cmd.Flags().String("file-system-name", "", "The name of the file system. Can also be supplied through TDC_FS_FILE_SYSTEM_NAME.") + cmd.Flags().String("file-system-name", "", "The name of the file system. Can also be supplied through TI_FS_FILE_SYSTEM_NAME.") cmd.Flags().String("mount-path", "", "Local mount path.") cmd.Flags().String("remote-path", "/", "The TiDB Cloud file system root path to mount.") cmd.Flags().String("driver", "auto", "Mount driver: auto, fuse, or webdav.") cmd.Flags().Bool("foreground", false, "Run the mount runtime in the foreground until interrupted.") cmd.Flags().Bool("read-only", false, "Read-only mount mode.") cmd.Flags().Duration("ready-timeout", 30*time.Second, "Time to wait for a background mount to become ready.") - cmd.Flags().String("cache-dir", "", "Local FUSE cache directory. Default: ~/.tdc/cache/mounts/.") + cmd.Flags().String("cache-dir", "", "Local FUSE cache directory. Default: ~/.ti/cache/mounts/.") cmd.Flags().Int64("read-cache-size-mb", 128, "FUSE read cache size in MiB. 0 uses the default.") cmd.Flags().Int64("read-cache-max-file-mb", 4, "Maximumfile size admitted to the FUSE read cache in MiB. 0 uses the default.") cmd.Flags().Duration("read-cache-ttl", 30*time.Second, "FUSE read cache Time-to-Live.") cmd.Flags().Bool("write-back-cache", true, "Persist FUSE writes locally before writing them to the file system on flush.") cmd.Flags().String("mount-profile", "", "Mount profile: coding-agent, portable, or none. Default: none.") - cmd.Flags().String("local-root", "", "Local overlay root. Default: ~/.tdc/local/fs/.") + cmd.Flags().String("local-root", "", "Local overlay root. Default: ~/.ti/local/fs/.") cmd.Flags().StringArray("pack-path", nil, "Local overlay path included by automatic or manual pack. Repeatable.") cmd.Flags().String("unpack-archive-path", "", "Restore the pack archive before mounting.") cmd.Flags().Bool("no-auto-unpack", false, "Skip default auto-unpack for portable mount profile before mounting.") @@ -1870,32 +1876,32 @@ func newFSUnmountFileSystemCommand(info version.Info) *cobra.Command { return cmd } -func fsUnmountOptions(ctx commandContext, profile *config.Profile) (tdcfs.UnmountFileSystemOptions, error) { +func fsUnmountOptions(ctx commandContext, profile *config.Profile) (tifs.UnmountFileSystemOptions, error) { mountPath, err := ctx.StringFlag("mount-path") if err != nil { - return tdcfs.UnmountFileSystemOptions{}, err + return tifs.UnmountFileSystemOptions{}, err } timeout, err := ctx.DurationFlag("timeout") if err != nil { - return tdcfs.UnmountFileSystemOptions{}, err + return tifs.UnmountFileSystemOptions{}, err } force, err := ctx.BoolFlag("force") if err != nil { - return tdcfs.UnmountFileSystemOptions{}, err + return tifs.UnmountFileSystemOptions{}, err } ignoreAbsent, err := ctx.BoolFlag("ignore-absent") if err != nil { - return tdcfs.UnmountFileSystemOptions{}, err + return tifs.UnmountFileSystemOptions{}, err } packArchivePath, err := ctx.StringFlag("pack-archive-path") if err != nil { - return tdcfs.UnmountFileSystemOptions{}, err + return tifs.UnmountFileSystemOptions{}, err } noAutoPack, err := ctx.BoolFlag("no-auto-pack") if err != nil { - return tdcfs.UnmountFileSystemOptions{}, err + return tifs.UnmountFileSystemOptions{}, err } - return tdcfs.UnmountFileSystemOptions{ + return tifs.UnmountFileSystemOptions{ Profile: profile, MountPath: mountPath, Timeout: timeout, @@ -1926,7 +1932,7 @@ func newFSDrainFileSystemCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.DrainFileSystem(ctx.cmd.Context(), tdcfs.DrainFileSystemOptions{ + return service.DrainFileSystem(ctx.cmd.Context(), tifs.DrainFileSystemOptions{ Profile: profile, MountPath: mountPath, Timeout: timeout, @@ -1945,7 +1951,7 @@ func newFSDrainFileSystemCommand(info version.Info) *cobra.Command { if err != nil { return dryrun.Result{}, err } - return service.DryRunDrainFileSystem(ctx.cmd.Context(), ctx.CommandPath(), tdcfs.DrainFileSystemOptions{ + return service.DryRunDrainFileSystem(ctx.cmd.Context(), ctx.CommandPath(), tifs.DrainFileSystemOptions{ Profile: profile, MountPath: mountPath, Timeout: timeout, @@ -1958,32 +1964,32 @@ func newFSDrainFileSystemCommand(info version.Info) *cobra.Command { return cmd } -func fsPackFileSystemOptions(ctx commandContext, profile *config.Profile) (tdcfs.PackFileSystemOptions, error) { +func fsPackFileSystemOptions(ctx commandContext, profile *config.Profile) (tifs.PackFileSystemOptions, error) { localRoot, err := ctx.StringFlag("local-root") if err != nil { - return tdcfs.PackFileSystemOptions{}, err + return tifs.PackFileSystemOptions{}, err } remoteRoot, err := ctx.StringFlag("remote-root") if err != nil { - return tdcfs.PackFileSystemOptions{}, err + return tifs.PackFileSystemOptions{}, err } mountPath, err := ctx.StringFlag("mount-path") if err != nil { - return tdcfs.PackFileSystemOptions{}, err + return tifs.PackFileSystemOptions{}, err } mountProfile, err := ctx.StringFlag("mount-profile") if err != nil { - return tdcfs.PackFileSystemOptions{}, err + return tifs.PackFileSystemOptions{}, err } archivePath, err := ctx.StringFlag("archive-path") if err != nil { - return tdcfs.PackFileSystemOptions{}, err + return tifs.PackFileSystemOptions{}, err } paths, err := ctx.StringArrayFlag("path") if err != nil { - return tdcfs.PackFileSystemOptions{}, err + return tifs.PackFileSystemOptions{}, err } - return tdcfs.PackFileSystemOptions{ + return tifs.PackFileSystemOptions{ Profile: profile, LocalRoot: localRoot, RemoteRoot: remoteRoot, @@ -1994,32 +2000,32 @@ func fsPackFileSystemOptions(ctx commandContext, profile *config.Profile) (tdcfs }, nil } -func fsUnpackFileSystemOptions(ctx commandContext, profile *config.Profile) (tdcfs.UnpackFileSystemOptions, error) { +func fsUnpackFileSystemOptions(ctx commandContext, profile *config.Profile) (tifs.UnpackFileSystemOptions, error) { localRoot, err := ctx.StringFlag("local-root") if err != nil { - return tdcfs.UnpackFileSystemOptions{}, err + return tifs.UnpackFileSystemOptions{}, err } remoteRoot, err := ctx.StringFlag("remote-root") if err != nil { - return tdcfs.UnpackFileSystemOptions{}, err + return tifs.UnpackFileSystemOptions{}, err } mountPath, err := ctx.StringFlag("mount-path") if err != nil { - return tdcfs.UnpackFileSystemOptions{}, err + return tifs.UnpackFileSystemOptions{}, err } mountProfile, err := ctx.StringFlag("mount-profile") if err != nil { - return tdcfs.UnpackFileSystemOptions{}, err + return tifs.UnpackFileSystemOptions{}, err } archivePath, err := ctx.StringFlag("archive-path") if err != nil { - return tdcfs.UnpackFileSystemOptions{}, err + return tifs.UnpackFileSystemOptions{}, err } noReplace, err := ctx.BoolFlag("no-replace") if err != nil { - return tdcfs.UnpackFileSystemOptions{}, err + return tifs.UnpackFileSystemOptions{}, err } - return tdcfs.UnpackFileSystemOptions{ + return tifs.UnpackFileSystemOptions{ Profile: profile, LocalRoot: localRoot, RemoteRoot: remoteRoot, @@ -2030,68 +2036,68 @@ func fsUnpackFileSystemOptions(ctx commandContext, profile *config.Profile) (tdc }, nil } -func fsCopyFileOptions(ctx commandContext, profile *config.Profile) (tdcfs.CopyFileOptions, error) { +func fsCopyFileOptions(ctx commandContext, profile *config.Profile) (tifs.CopyFileOptions, error) { fromLocal, err := ctx.StringFlag("from-local") if err != nil { - return tdcfs.CopyFileOptions{}, err + return tifs.CopyFileOptions{}, err } fromRemote, err := ctx.StringFlag("from-remote") if err != nil { - return tdcfs.CopyFileOptions{}, err + return tifs.CopyFileOptions{}, err } toLocal, err := ctx.StringFlag("to-local") if err != nil { - return tdcfs.CopyFileOptions{}, err + return tifs.CopyFileOptions{}, err } toRemote, err := ctx.StringFlag("to-remote") if err != nil { - return tdcfs.CopyFileOptions{}, err + return tifs.CopyFileOptions{}, err } fromStdin, err := ctx.BoolFlag("from-stdin") if err != nil { - return tdcfs.CopyFileOptions{}, err + return tifs.CopyFileOptions{}, err } toStdout, err := ctx.BoolFlag("to-stdout") if err != nil { - return tdcfs.CopyFileOptions{}, err + return tifs.CopyFileOptions{}, err } overwrite, err := ctx.BoolFlag("overwrite") if err != nil { - return tdcfs.CopyFileOptions{}, err + return tifs.CopyFileOptions{}, err } createParents, err := ctx.BoolFlag("create-parents") if err != nil { - return tdcfs.CopyFileOptions{}, err + return tifs.CopyFileOptions{}, err } appendFile, err := ctx.BoolFlag("append") if err != nil { - return tdcfs.CopyFileOptions{}, err + return tifs.CopyFileOptions{}, err } recursive, err := ctx.BoolFlag("recursive") if err != nil { - return tdcfs.CopyFileOptions{}, err + return tifs.CopyFileOptions{}, err } resume, err := ctx.BoolFlag("resume") if err != nil { - return tdcfs.CopyFileOptions{}, err + return tifs.CopyFileOptions{}, err } layerID, err := ctx.StringFlag("layer-id") if err != nil { - return tdcfs.CopyFileOptions{}, err + return tifs.CopyFileOptions{}, err } tagValues, err := ctx.StringArrayFlag("tag") if err != nil { - return tdcfs.CopyFileOptions{}, err + return tifs.CopyFileOptions{}, err } - tags, err := tdcfs.ParseFileTags(tagValues) + tags, err := tifs.ParseFileTags(tagValues) if err != nil { - return tdcfs.CopyFileOptions{}, err + return tifs.CopyFileOptions{}, err } description, err := ctx.StringFlag("description") if err != nil { - return tdcfs.CopyFileOptions{}, err + return tifs.CopyFileOptions{}, err } - return tdcfs.CopyFileOptions{ + return tifs.CopyFileOptions{ Profile: profile, FromLocal: fromLocal, FromRemote: fromRemote, @@ -2110,48 +2116,48 @@ func fsCopyFileOptions(ctx commandContext, profile *config.Profile) (tdcfs.CopyF }, nil } -func fsFindFilesOptions(ctx commandContext, profile *config.Profile) (tdcfs.FindFilesOptions, error) { +func fsFindFilesOptions(ctx commandContext, profile *config.Profile) (tifs.FindFilesOptions, error) { path, err := ctx.StringFlag("path") if err != nil { - return tdcfs.FindFilesOptions{}, err + return tifs.FindFilesOptions{}, err } fileNamePattern, err := ctx.StringFlag("file-name-pattern") if err != nil { - return tdcfs.FindFilesOptions{}, err + return tifs.FindFilesOptions{}, err } resourceType, err := ctx.StringFlag("resource-type") if err != nil { - return tdcfs.FindFilesOptions{}, err + return tifs.FindFilesOptions{}, err } tag, err := ctx.StringFlag("tag") if err != nil { - return tdcfs.FindFilesOptions{}, err + return tifs.FindFilesOptions{}, err } layerID, err := ctx.StringFlag("layer-id") if err != nil { - return tdcfs.FindFilesOptions{}, err + return tifs.FindFilesOptions{}, err } newer, err := ctx.StringFlag("newer") if err != nil { - return tdcfs.FindFilesOptions{}, err + return tifs.FindFilesOptions{}, err } older, err := ctx.StringFlag("older") if err != nil { - return tdcfs.FindFilesOptions{}, err + return tifs.FindFilesOptions{}, err } minSizeBytes, err := ctx.Int64Flag("min-size-bytes") if err != nil { - return tdcfs.FindFilesOptions{}, err + return tifs.FindFilesOptions{}, err } maxSizeBytes, err := ctx.Int64Flag("max-size-bytes") if err != nil { - return tdcfs.FindFilesOptions{}, err + return tifs.FindFilesOptions{}, err } limit, err := ctx.Int32Flag("limit") if err != nil { - return tdcfs.FindFilesOptions{}, err + return tifs.FindFilesOptions{}, err } - return tdcfs.FindFilesOptions{ + return tifs.FindFilesOptions{ Profile: profile, Path: path, FileNamePattern: fileNamePattern, @@ -2166,32 +2172,32 @@ func fsFindFilesOptions(ctx commandContext, profile *config.Profile) (tdcfs.Find }, nil } -func fsCreateLayerOptions(ctx commandContext, profile *config.Profile) (tdcfs.CreateLayerOptions, error) { +func fsCreateLayerOptions(ctx commandContext, profile *config.Profile) (tifs.CreateLayerOptions, error) { layerID, err := ctx.StringFlag("layer-id") if err != nil { - return tdcfs.CreateLayerOptions{}, err + return tifs.CreateLayerOptions{}, err } baseRootPath, err := ctx.StringFlag("base-root-path") if err != nil { - return tdcfs.CreateLayerOptions{}, err + return tifs.CreateLayerOptions{}, err } layerName, err := ctx.StringFlag("layer-name") if err != nil { - return tdcfs.CreateLayerOptions{}, err + return tifs.CreateLayerOptions{}, err } tags, err := ctx.StringArrayFlag("tag") if err != nil { - return tdcfs.CreateLayerOptions{}, err + return tifs.CreateLayerOptions{}, err } durabilityMode, err := ctx.StringFlag("durability-mode") if err != nil { - return tdcfs.CreateLayerOptions{}, err + return tifs.CreateLayerOptions{}, err } actorID, err := ctx.StringFlag("actor-id") if err != nil { - return tdcfs.CreateLayerOptions{}, err + return tifs.CreateLayerOptions{}, err } - return tdcfs.CreateLayerOptions{ + return tifs.CreateLayerOptions{ Profile: profile, LayerID: layerID, BaseRootPath: baseRootPath, @@ -2202,36 +2208,36 @@ func fsCreateLayerOptions(ctx commandContext, profile *config.Profile) (tdcfs.Cr }, nil } -func fsLayerEntriesOptions(ctx commandContext, profile *config.Profile) (tdcfs.LayerEntriesOptions, error) { +func fsLayerEntriesOptions(ctx commandContext, profile *config.Profile) (tifs.LayerEntriesOptions, error) { layerID, err := ctx.StringFlag("layer-id") if err != nil { - return tdcfs.LayerEntriesOptions{}, err + return tifs.LayerEntriesOptions{}, err } maxSeq, err := ctx.Int64Flag("max-seq") if err != nil { - return tdcfs.LayerEntriesOptions{}, err + return tifs.LayerEntriesOptions{}, err } - return tdcfs.LayerEntriesOptions{ + return tifs.LayerEntriesOptions{ Profile: profile, LayerID: layerID, MaxSeq: maxSeq, }, nil } -func fsCreateLayerCheckpointOptions(ctx commandContext, profile *config.Profile) (tdcfs.CreateLayerCheckpointOptions, error) { +func fsCreateLayerCheckpointOptions(ctx commandContext, profile *config.Profile) (tifs.CreateLayerCheckpointOptions, error) { layerID, err := ctx.StringFlag("layer-id") if err != nil { - return tdcfs.CreateLayerCheckpointOptions{}, err + return tifs.CreateLayerCheckpointOptions{}, err } checkpointID, err := ctx.StringFlag("checkpoint-id") if err != nil { - return tdcfs.CreateLayerCheckpointOptions{}, err + return tifs.CreateLayerCheckpointOptions{}, err } label, err := ctx.StringFlag("label") if err != nil { - return tdcfs.CreateLayerCheckpointOptions{}, err + return tifs.CreateLayerCheckpointOptions{}, err } - return tdcfs.CreateLayerCheckpointOptions{ + return tifs.CreateLayerCheckpointOptions{ Profile: profile, LayerID: layerID, CheckpointID: checkpointID, @@ -2239,76 +2245,76 @@ func fsCreateLayerCheckpointOptions(ctx commandContext, profile *config.Profile) }, nil } -func fsMountOptions(ctx commandContext, profile *config.Profile) (tdcfs.MountFileSystemOptions, error) { +func fsMountOptions(ctx commandContext, profile *config.Profile) (tifs.MountFileSystemOptions, error) { fileSystemName, err := ctx.StringFlag("file-system-name") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } mountPath, err := ctx.StringFlag("mount-path") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } remotePath, err := ctx.StringFlag("remote-path") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } driver, err := ctx.StringFlag("driver") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } foreground, err := ctx.BoolFlag("foreground") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } readOnly, err := ctx.BoolFlag("read-only") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } readyTimeout, err := ctx.DurationFlag("ready-timeout") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } cacheDir, err := ctx.StringFlag("cache-dir") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } readCacheMB, err := ctx.Int64Flag("read-cache-size-mb") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } readCacheFileMB, err := ctx.Int64Flag("read-cache-max-file-mb") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } readCacheTTL, err := ctx.DurationFlag("read-cache-ttl") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } writeBackCache, err := ctx.BoolFlag("write-back-cache") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } mountProfile, err := ctx.StringFlag("mount-profile") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } localRoot, err := ctx.StringFlag("local-root") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } packPaths, err := ctx.StringArrayFlag("pack-path") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } unpackArchivePath, err := ctx.StringFlag("unpack-archive-path") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } noAutoUnpack, err := ctx.BoolFlag("no-auto-unpack") if err != nil { - return tdcfs.MountFileSystemOptions{}, err + return tifs.MountFileSystemOptions{}, err } - return tdcfs.MountFileSystemOptions{ + return tifs.MountFileSystemOptions{ Profile: profile, FileSystemName: fileSystemName, MountPath: mountPath, @@ -2330,28 +2336,28 @@ func fsMountOptions(ctx commandContext, profile *config.Profile) (tdcfs.MountFil }, nil } -func fsTDCServiceAndProfile(ctx commandContext) (tdcfs.Service, *config.Profile, error) { +func fsTIServiceAndProfile(ctx commandContext) (tifs.Service, *config.Profile, error) { profile, err := ctx.LoadProfile() if err != nil { - return tdcfs.Service{}, nil, err + return tifs.Service{}, nil, err } return fsService(ctx, profile) } -func fsLocalServiceAndProfile(ctx commandContext) (tdcfs.Service, *config.Profile, error) { +func fsLocalServiceAndProfile(ctx commandContext) (tifs.Service, *config.Profile, error) { profile, err := ctx.LoadLocalProfile() if err != nil { - return tdcfs.Service{}, nil, err + return tifs.Service{}, nil, err } return fsService(ctx, profile) } -func fsService(ctx commandContext, profile *config.Profile) (tdcfs.Service, *config.Profile, error) { +func fsService(ctx commandContext, profile *config.Profile) (tifs.Service, *config.Profile, error) { debug, err := ctx.BoolFlag("debug") if err != nil { - return tdcfs.Service{}, nil, err + return tifs.Service{}, nil, err } - service := tdcfs.Service{ + service := tifs.Service{ Timeout: 30 * time.Second, Debug: debug, DebugWriter: ctx.cmd.ErrOrStderr(), @@ -2363,48 +2369,48 @@ func fsService(ctx commandContext, profile *config.Profile) (tdcfs.Service, *con return service, profile, nil } -func fsServiceAndProfile(ctx commandContext) (tdcfs.Service, *config.Profile, error) { +func fsServiceAndProfile(ctx commandContext) (tifs.Service, *config.Profile, error) { return fsAuthenticatedServiceAndProfile(ctx, true) } -func fsTDCResourceServiceAndProfile(ctx commandContext) (tdcfs.Service, *config.Profile, error) { - service, profile, err := fsTDCServiceAndProfile(ctx) +func fsTIResourceServiceAndProfile(ctx commandContext) (tifs.Service, *config.Profile, error) { + service, profile, err := fsTIServiceAndProfile(ctx) if err != nil { - return tdcfs.Service{}, nil, err + return tifs.Service{}, nil, err } selected, err := fsResolveAuthenticatedProfile(ctx, profile, true) if err != nil { - return tdcfs.Service{}, nil, err + return tifs.Service{}, nil, err } if _, err := fscred.Get(profile.HomeDir, profile.Name, selected.FSResourceName); err != nil { - return tdcfs.Service{}, nil, err + return tifs.Service{}, nil, err } return service, selected, nil } -func fsAuthenticatedServiceAndProfile(ctx commandContext, tokenRequired bool) (tdcfs.Service, *config.Profile, error) { +func fsAuthenticatedServiceAndProfile(ctx commandContext, tokenRequired bool) (tifs.Service, *config.Profile, error) { service, profile, err := fsLocalServiceAndProfile(ctx) if err != nil { - return tdcfs.Service{}, nil, err + return tifs.Service{}, nil, err } selected, err := fsResolveAuthenticatedProfile(ctx, profile, tokenRequired) if err != nil { - return tdcfs.Service{}, nil, err + return tifs.Service{}, nil, err } return service, selected, nil } -func fsRegistryServiceAndProfile(ctx commandContext) (tdcfs.Service, *config.Profile, error) { +func fsRegistryServiceAndProfile(ctx commandContext) (tifs.Service, *config.Profile, error) { service, profile, err := fsLocalServiceAndProfile(ctx) if err != nil { - return tdcfs.Service{}, nil, err + return tifs.Service{}, nil, err } selector := "" selectorExplicit := false if ctx.cmd.Flag("file-system-name") != nil { selector, err = ctx.StringFlag("file-system-name") if err != nil { - return tdcfs.Service{}, nil, err + return tifs.Service{}, nil, err } selectorExplicit = ctx.FlagChanged("file-system-name") } @@ -2414,19 +2420,19 @@ func fsRegistryServiceAndProfile(ctx commandContext) (tdcfs.Service, *config.Pro } selected, _, err := resolve(profile.HomeDir, profile, selector, selectorExplicit, nil) if err != nil { - return tdcfs.Service{}, nil, err + return tifs.Service{}, nil, err } return service, selected, nil } -func fsAdjunctServiceAndProfile(ctx commandContext) (tdcfs.Service, *config.Profile, error) { +func fsAdjunctServiceAndProfile(ctx commandContext) (tifs.Service, *config.Profile, error) { return fsServiceAndProfile(ctx) } -func fsVaultServiceAndProfile(ctx commandContext) (tdcfs.Service, *config.Profile, error) { +func fsVaultServiceAndProfile(ctx commandContext) (tifs.Service, *config.Profile, error) { token, err := vaultToken(ctx) if err != nil { - return tdcfs.Service{}, nil, err + return tifs.Service{}, nil, err } return fsAuthenticatedServiceAndProfile(ctx, strings.TrimSpace(token) == "") } @@ -2456,7 +2462,11 @@ func fsResolveAuthenticatedProfile(ctx commandContext, profile *config.Profile, if flag := ctx.cmd.Flag("region"); flag != nil && flag.Changed { regionOverride = strings.TrimSpace(flag.Value.String()) } else { - regionOverride = strings.TrimSpace(os.Getenv("TDC_REGION_CODE")) + value, _, _, err := envcompat.ResolveNames(nil, "TI_REGION_CODE", "TDC_REGION_CODE") + if err != nil { + return nil, err + } + regionOverride = strings.TrimSpace(value) } dryRun, _ := ctx.BoolFlag("dry-run") selected, _, err := fscred.ResolveAuthenticated(profile.HomeDir, profile, fscred.ResolveAuthOptions{ @@ -2522,7 +2532,7 @@ func newVaultCreateSecretCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.CreateVaultSecret(ctx.cmd.Context(), tdcfs.VaultCreateSecretOptions{ + return service.CreateVaultSecret(ctx.cmd.Context(), tifs.VaultCreateSecretOptions{ Profile: profile, SecretName: name, Fields: fields, @@ -2555,7 +2565,7 @@ func newVaultReplaceSecretCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.ReplaceVaultSecret(ctx.cmd.Context(), tdcfs.VaultReplaceSecretOptions{ + return service.ReplaceVaultSecret(ctx.cmd.Context(), tifs.VaultReplaceSecretOptions{ Profile: profile, SecretPath: secretPath, FromDirectory: fromDirectory, @@ -2595,7 +2605,7 @@ func newVaultReadSecretCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - result, err := service.ReadVaultSecret(ctx.cmd.Context(), tdcfs.VaultReadSecretOptions{ + result, err := service.ReadVaultSecret(ctx.cmd.Context(), tifs.VaultReadSecretOptions{ Profile: profile, SecretName: name, Field: field, @@ -2614,7 +2624,7 @@ func newVaultReadSecretCommand(info version.Info) *cobra.Command { cmd.Flags().String("secret-name", "", "Vault secret name.") cmd.Flags().String("field", "", "Field name to read.") cmd.Flags().String("format", "json", "Read output format: json, raw, or env.") - cmd.Flags().String("vault-token", "", "Delegated file system vault token; prefer TDC_VAULT_TOKEN environment variable.") + cmd.Flags().String("vault-token", "", "Delegated file system vault token; prefer TI_VAULT_TOKEN environment variable.") markUsageRequired(cmd, "secret-name") return cmd } @@ -2634,10 +2644,10 @@ func newVaultListSecretsCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.ListVaultSecrets(ctx.cmd.Context(), tdcfs.VaultListSecretsOptions{Profile: profile, VaultToken: token}) + return service.ListVaultSecrets(ctx.cmd.Context(), tifs.VaultListSecretsOptions{Profile: profile, VaultToken: token}) }, }, info) - cmd.Flags().String("vault-token", "", "Delegated file system vault token; prefer TDC_VAULT_TOKEN environment variable.") + cmd.Flags().String("vault-token", "", "Delegated file system vault token; prefer TI_VAULT_TOKEN environment variable.") return cmd } @@ -2656,7 +2666,7 @@ func newVaultDeleteSecretCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.DeleteVaultSecret(ctx.cmd.Context(), tdcfs.VaultDeleteSecretOptions{Profile: profile, SecretName: name}) + return service.DeleteVaultSecret(ctx.cmd.Context(), tifs.VaultDeleteSecretOptions{Profile: profile, SecretName: name}) }, }, info) cmd.Flags().String("secret-name", "", "vault secret name") @@ -2722,11 +2732,11 @@ func newVaultDeleteGrantCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.DeleteVaultGrant(ctx.cmd.Context(), tdcfs.VaultDeleteGrantOptions{Profile: profile, GrantID: grantID, RevokedBy: revokedBy, Reason: reason}) + return service.DeleteVaultGrant(ctx.cmd.Context(), tifs.VaultDeleteGrantOptions{Profile: profile, GrantID: grantID, RevokedBy: revokedBy, Reason: reason}) }, }, info) cmd.Flags().String("grant-id", "", "Vault grant ID.") - cmd.Flags().String("revoked-by", "tdc", "Actor label for the revoke audit entry.") + cmd.Flags().String("revoked-by", "ti", "Actor label for the revoke audit entry.") cmd.Flags().String("reason", "", "The reason for the revoke.") markUsageRequired(cmd, "grant-id") return cmd @@ -2759,7 +2769,7 @@ func newVaultListAuditEventsCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.ListVaultAuditEvents(ctx.cmd.Context(), tdcfs.VaultAuditOptions{ + return service.ListVaultAuditEvents(ctx.cmd.Context(), tifs.VaultAuditOptions{ Profile: profile, SecretName: secretName, AgentID: agentID, @@ -2771,7 +2781,7 @@ func newVaultListAuditEventsCommand(info version.Info) *cobra.Command { cmd.Flags().String("secret-name", "", "Filter by vault secret name.") cmd.Flags().String("agent-id", "", "Filter by agent ID.") cmd.Flags().Duration("since", 0, "Relative time filter for client-side time, for example 24h.") - cmd.Flags().Int32("limit", int32(tdcfs.DefaultVaultAuditLimit), "The maximum number of events to return.") + cmd.Flags().Int32("limit", int32(tifs.DefaultVaultAuditLimit), "The maximum number of events to return.") return cmd } @@ -2793,7 +2803,7 @@ func newVaultRunWithSecretCommand(info version.Info) *cobra.Command { if err != nil { return err } - return service.RunWithVaultSecret(cmd.Context(), tdcfs.VaultRunWithSecretOptions{ + return service.RunWithVaultSecret(cmd.Context(), tifs.VaultRunWithSecretOptions{ Profile: profile, SecretPath: secretPath, VaultToken: token, @@ -2806,7 +2816,7 @@ func newVaultRunWithSecretCommand(info version.Info) *cobra.Command { }, info) cmd.Args = cobra.ArbitraryArgs cmd.Flags().String("secret-path", "", "Vault path in the form /n/vault/.") - cmd.Flags().String("vault-token", "", "Delegated file system vault token; prefer TDC_VAULT_TOKEN.") + cmd.Flags().String("vault-token", "", "Delegated file system vault token; prefer TI_VAULT_TOKEN.") markUsageRequired(cmd, "secret-path") return cmd } @@ -2843,7 +2853,7 @@ func newVaultMountCommand(info version.Info) *cobra.Command { cmd.Flags().String("mount-path", "", "The local mount path.") cmd.Flags().Bool("foreground", false, "Run mount runtime in the foreground until interrupted.") cmd.Flags().Duration("ready-timeout", 30*time.Second, "The time to wait for a background mount to become ready.") - cmd.Flags().String("vault-token", "", "Delegated file system vault token; prefer TDC_VAULT_TOKEN environment variable.") + cmd.Flags().String("vault-token", "", "Delegated file system vault token; prefer TI_VAULT_TOKEN environment variable.") markUsageRequired(cmd, "mount-path") return cmd } @@ -2875,7 +2885,7 @@ func newVaultUnmountCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.UnmountFileSystem(ctx.cmd.Context(), tdcfs.UnmountFileSystemOptions{ + return service.UnmountFileSystem(ctx.cmd.Context(), tifs.UnmountFileSystemOptions{ Profile: profile, MountPath: mountPath, Timeout: timeout, @@ -2905,7 +2915,7 @@ func newVaultUnmountCommand(info version.Info) *cobra.Command { if err != nil { return dryrun.Result{}, err } - return service.DryRunUnmountFileSystem(ctx.cmd.Context(), ctx.CommandPath(), tdcfs.UnmountFileSystemOptions{ + return service.DryRunUnmountFileSystem(ctx.cmd.Context(), ctx.CommandPath(), tifs.UnmountFileSystemOptions{ Profile: profile, MountPath: mountPath, Timeout: timeout, @@ -2923,52 +2933,52 @@ func newVaultUnmountCommand(info version.Info) *cobra.Command { return cmd } -func vaultCreateGrantOptions(ctx commandContext, profile *config.Profile) (tdcfs.VaultCreateGrantOptions, error) { +func vaultCreateGrantOptions(ctx commandContext, profile *config.Profile) (tifs.VaultCreateGrantOptions, error) { agentID, err := ctx.StringFlag("agent-id") if err != nil { - return tdcfs.VaultCreateGrantOptions{}, err + return tifs.VaultCreateGrantOptions{}, err } scopes, err := ctx.StringArrayFlag("scope") if err != nil { - return tdcfs.VaultCreateGrantOptions{}, err + return tifs.VaultCreateGrantOptions{}, err } permission, err := ctx.StringFlag("permission") if err != nil { - return tdcfs.VaultCreateGrantOptions{}, err + return tifs.VaultCreateGrantOptions{}, err } ttl, err := ctx.DurationFlag("ttl") if err != nil { - return tdcfs.VaultCreateGrantOptions{}, err + return tifs.VaultCreateGrantOptions{}, err } labelHint, err := ctx.StringFlag("label-hint") if err != nil { - return tdcfs.VaultCreateGrantOptions{}, err + return tifs.VaultCreateGrantOptions{}, err } tokenOnly, err := ctx.BoolFlag("token-only") if err != nil { - return tdcfs.VaultCreateGrantOptions{}, err + return tifs.VaultCreateGrantOptions{}, err } - return tdcfs.VaultCreateGrantOptions{Profile: profile, AgentID: agentID, Scopes: scopes, Permission: permission, TTL: ttl, LabelHint: labelHint, TokenOnly: tokenOnly}, nil + return tifs.VaultCreateGrantOptions{Profile: profile, AgentID: agentID, Scopes: scopes, Permission: permission, TTL: ttl, LabelHint: labelHint, TokenOnly: tokenOnly}, nil } -func vaultMountOptions(ctx commandContext, profile *config.Profile) (tdcfs.VaultMountOptions, error) { +func vaultMountOptions(ctx commandContext, profile *config.Profile) (tifs.VaultMountOptions, error) { mountPath, err := ctx.StringFlag("mount-path") if err != nil { - return tdcfs.VaultMountOptions{}, err + return tifs.VaultMountOptions{}, err } foreground, err := ctx.BoolFlag("foreground") if err != nil { - return tdcfs.VaultMountOptions{}, err + return tifs.VaultMountOptions{}, err } readyTimeout, err := ctx.DurationFlag("ready-timeout") if err != nil { - return tdcfs.VaultMountOptions{}, err + return tifs.VaultMountOptions{}, err } token, err := vaultToken(ctx) if err != nil { - return tdcfs.VaultMountOptions{}, err + return tifs.VaultMountOptions{}, err } - return tdcfs.VaultMountOptions{ + return tifs.VaultMountOptions{ Profile: profile, MountPath: mountPath, VaultToken: token, @@ -2985,7 +2995,8 @@ func vaultToken(ctx commandContext) (string, error) { if token != "" { return token, nil } - return os.Getenv("TDC_VAULT_TOKEN"), nil + value, _, _, err := envcompat.ResolveNames(nil, "TI_VAULT_TOKEN", "TDC_VAULT_TOKEN") + return value, err } func newFSGitCommand(info version.Info) *cobra.Command { @@ -3047,7 +3058,7 @@ func newGitHydrateWorkspaceCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.HydrateGitWorkspace(ctx.cmd.Context(), tdcfs.GitWorkspaceHydrateOptions{Profile: profile, TargetPath: targetPath, Timeout: timeout}) + return service.HydrateGitWorkspace(ctx.cmd.Context(), tifs.GitWorkspaceHydrateOptions{Profile: profile, TargetPath: targetPath, Timeout: timeout}) }, }, info) cmd.Flags().String("target-path", "", "The workspace path with a file system mounted.") @@ -3104,7 +3115,7 @@ func newGitRemoveWorktreeCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.RemoveGitWorktree(ctx.cmd.Context(), tdcfs.GitWorktreeRemoveOptions{Profile: profile, WorktreePath: worktreePath, Force: force}) + return service.RemoveGitWorktree(ctx.cmd.Context(), tifs.GitWorktreeRemoveOptions{Profile: profile, WorktreePath: worktreePath, Force: force}) }, }, info) cmd.Flags().String("worktree-path", "", "Mounted file system path of the linked worktree.") @@ -3113,56 +3124,56 @@ func newGitRemoveWorktreeCommand(info version.Info) *cobra.Command { return cmd } -func gitWorkspaceCloneOptions(ctx commandContext, profile *config.Profile) (tdcfs.GitWorkspaceCloneOptions, error) { +func gitWorkspaceCloneOptions(ctx commandContext, profile *config.Profile) (tifs.GitWorkspaceCloneOptions, error) { repoURL, err := ctx.StringFlag("repo-url") if err != nil { - return tdcfs.GitWorkspaceCloneOptions{}, err + return tifs.GitWorkspaceCloneOptions{}, err } targetPath, err := ctx.StringFlag("target-path") if err != nil { - return tdcfs.GitWorkspaceCloneOptions{}, err + return tifs.GitWorkspaceCloneOptions{}, err } blobless, err := ctx.BoolFlag("blobless") if err != nil { - return tdcfs.GitWorkspaceCloneOptions{}, err + return tifs.GitWorkspaceCloneOptions{}, err } hydrate, err := ctx.StringFlag("hydrate") if err != nil { - return tdcfs.GitWorkspaceCloneOptions{}, err + return tifs.GitWorkspaceCloneOptions{}, err } - return tdcfs.GitWorkspaceCloneOptions{Profile: profile, RepoURL: repoURL, TargetPath: targetPath, Blobless: blobless, HydrateMode: hydrate}, nil + return tifs.GitWorkspaceCloneOptions{Profile: profile, RepoURL: repoURL, TargetPath: targetPath, Blobless: blobless, HydrateMode: hydrate}, nil } -func gitWorktreeAddOptions(ctx commandContext, profile *config.Profile) (tdcfs.GitWorktreeAddOptions, error) { +func gitWorktreeAddOptions(ctx commandContext, profile *config.Profile) (tifs.GitWorktreeAddOptions, error) { basePath, err := ctx.StringFlag("base-path") if err != nil { - return tdcfs.GitWorktreeAddOptions{}, err + return tifs.GitWorktreeAddOptions{}, err } worktreePath, err := ctx.StringFlag("worktree-path") if err != nil { - return tdcfs.GitWorktreeAddOptions{}, err + return tifs.GitWorktreeAddOptions{}, err } branchName, err := ctx.StringFlag("branch-name") if err != nil { - return tdcfs.GitWorktreeAddOptions{}, err + return tifs.GitWorktreeAddOptions{}, err } detach, err := ctx.BoolFlag("detach") if err != nil { - return tdcfs.GitWorktreeAddOptions{}, err + return tifs.GitWorktreeAddOptions{}, err } blobless, err := ctx.BoolFlag("blobless") if err != nil { - return tdcfs.GitWorktreeAddOptions{}, err + return tifs.GitWorktreeAddOptions{}, err } hydrate, err := ctx.StringFlag("hydrate") if err != nil { - return tdcfs.GitWorktreeAddOptions{}, err + return tifs.GitWorktreeAddOptions{}, err } commitISH, err := ctx.StringFlag("commit-ish") if err != nil { - return tdcfs.GitWorktreeAddOptions{}, err + return tifs.GitWorktreeAddOptions{}, err } - return tdcfs.GitWorktreeAddOptions{ + return tifs.GitWorktreeAddOptions{ Profile: profile, BasePath: basePath, WorktreePath: worktreePath, @@ -3220,7 +3231,7 @@ func newJournalCreateCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.CreateJournal(ctx.cmd.Context(), tdcfs.JournalCreateOptions{ + return service.CreateJournal(ctx.cmd.Context(), tifs.JournalCreateOptions{ Profile: profile, JournalID: journalID, JournalKind: journalKind, @@ -3277,7 +3288,7 @@ func newJournalAppendEntriesCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.AppendJournalEntries(ctx.cmd.Context(), tdcfs.JournalAppendOptions{ + return service.AppendJournalEntries(ctx.cmd.Context(), tifs.JournalAppendOptions{ Profile: profile, JournalID: journalID, IdempotencyKey: idempotencyKey, @@ -3324,7 +3335,7 @@ func newJournalReadEntriesCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.ReadJournalEntries(ctx.cmd.Context(), tdcfs.JournalReadOptions{ + return service.ReadJournalEntries(ctx.cmd.Context(), tifs.JournalReadOptions{ Profile: profile, JournalID: journalID, AfterSeq: afterSeq, @@ -3394,7 +3405,7 @@ func newJournalSearchEntriesCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.SearchJournal(ctx.cmd.Context(), tdcfs.JournalSearchOptions{ + return service.SearchJournal(ctx.cmd.Context(), tifs.JournalSearchOptions{ Profile: profile, EntryType: entryType, Status: status, @@ -3439,7 +3450,7 @@ func newJournalVerifyCommand(info version.Info) *cobra.Command { if err != nil { return nil, err } - return service.VerifyJournal(ctx.cmd.Context(), tdcfs.JournalVerifyOptions{ + return service.VerifyJournal(ctx.cmd.Context(), tifs.JournalVerifyOptions{ Profile: profile, JournalID: journalID, }) diff --git a/internal/cli/root.go b/internal/cli/root.go index e1d06e0..ae20c7f 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -12,21 +12,23 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/auth" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/config/region" - "github.com/tidbcloud/tdc/internal/config/store" - "github.com/tidbcloud/tdc/internal/dryrun" - "github.com/tidbcloud/tdc/internal/oplog" - "github.com/tidbcloud/tdc/internal/output" - "github.com/tidbcloud/tdc/internal/settings" - "github.com/tidbcloud/tdc/internal/telemetry" - "github.com/tidbcloud/tdc/internal/version" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/auth" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/config/envcompat" + "github.com/tidbcloud/ti-cli/internal/config/homemigration" + "github.com/tidbcloud/ti-cli/internal/config/region" + "github.com/tidbcloud/ti-cli/internal/config/store" + "github.com/tidbcloud/ti-cli/internal/dryrun" + "github.com/tidbcloud/ti-cli/internal/oplog" + "github.com/tidbcloud/ti-cli/internal/output" + "github.com/tidbcloud/ti-cli/internal/settings" + "github.com/tidbcloud/ti-cli/internal/telemetry" + "github.com/tidbcloud/ti-cli/internal/version" ) -const usageRequiredFlagAnnotation = "tdc_usage_required" +const usageRequiredFlagAnnotation = "ti_usage_required" func init() { cobra.AddTemplateFunc("usageSynopsis", usageSynopsis) @@ -41,11 +43,23 @@ type Options struct { Query string } +type ExecuteOption func(*executeOptions) + +type executeOptions struct { + migrateHome bool +} + +func WithHomeMigration() ExecuteOption { + return func(opts *executeOptions) { + opts.migrateHome = true + } +} + func NewRootCommand(info version.Info) *cobra.Command { opts := &Options{} root := newCommand(commandSpec{ - Use: "tdc", + Use: "ti", Short: "CLI for TiDB Cloud Filesystem and TiDB Cloud Starter.", Long: "The TiDB Cloud Command Line Interface is a unified tool to manage your TiDB Cloud Filesystem and Starter services.", RunE: func(cmd *cobra.Command, _ []string) error { @@ -56,8 +70,8 @@ func NewRootCommand(info version.Info) *cobra.Command { root.SilenceErrors = true root.SilenceUsage = true root.Annotations = map[string]string{ - "tdc.version": info.Version, - "tdc.commit": info.Commit, + "ti.version": info.Version, + "ti.commit": info.Commit, } root.CompletionOptions.DisableDefaultCmd = true root.SetFlagErrorFunc(flagErrorFunc) @@ -65,7 +79,7 @@ func NewRootCommand(info version.Info) *cobra.Command { root.SetUsageTemplate(usageTemplate) flags := root.PersistentFlags() - flags.StringVar(&opts.Profile, "profile", "default", "The profile name for tdc CLI.") + flags.StringVar(&opts.Profile, "profile", "default", "The profile name for ti CLI.") flags.StringVar(&opts.Region, "region", "", "Override region code in the profile, for example aws-us-east-1.") flags.BoolVar(&opts.Debug, "debug", false, "Enable debug output.") flags.StringVar(&opts.Output, "output", "json", "Output format: json or text.") @@ -95,16 +109,36 @@ func rootCommandRequiredError(cmd *cobra.Command) error { %s -usage: tdc [] [parameters] +usage: ti [] [parameters] To see help information, you can run: - tdc help - tdc help - tdc help`, cmd.Long), + ti help + ti help + ti help`, cmd.Long), ) } -func Execute(ctx context.Context, root *cobra.Command, info version.Info, args []string, stdout, stderr io.Writer) error { +func Execute(ctx context.Context, root *cobra.Command, info version.Info, args []string, stdout, stderr io.Writer, options ...ExecuteOption) error { + executeOpts := executeOptions{} + for _, option := range options { + if option != nil { + option(&executeOpts) + } + } + if !isUpdateInvocation(args) { + if err := envcompat.Validate(nil); err != nil { + return err + } + if executeOpts.migrateHome { + home, err := os.UserHomeDir() + if err != nil { + return apperr.Wrap("config.home_migration_failed", "config", 1, "cannot determine home directory for state migration", err) + } + if _, err := homemigration.Ensure(home); err != nil { + return err + } + } + } recorder, recordCommand := commandRecorder(args, stderr) ctx = oplog.WithRecorder(ctx, recorder) operationStart := time.Now() @@ -186,8 +220,8 @@ func effectiveTelemetryEndpoint(info version.Info) string { if endpoint := strings.TrimSpace(info.TelemetryEndpoint); endpoint != "" { return endpoint } - if info.InstallSource == "local" && os.Getenv("TDC_ALLOW_TEST_ENDPOINTS") == "1" { - return strings.TrimSpace(os.Getenv("TDC_TEST_TELEMETRY_ENDPOINT")) + if info.InstallSource == "local" && os.Getenv("TI_ALLOW_TEST_ENDPOINTS") == "1" { + return strings.TrimSpace(os.Getenv("TI_TEST_TELEMETRY_ENDPOINT")) } return "" } @@ -231,7 +265,7 @@ func telemetryProfileSource(cmd *cobra.Command) string { if flag := cmd.Flag("profile"); flag != nil && flag.Changed { return "explicit" } - if value := strings.TrimSpace(os.Getenv("TDC_PROFILE")); value != "" { + if value := strings.TrimSpace(compatibleEnvironmentValue("TI_PROFILE")); value != "" { return "env" } return "default" @@ -251,7 +285,7 @@ func commandRecorder(args []string, stderr io.Writer) (oplog.Recorder, bool) { logging, err := settings.ResolveLogging(home, nil) if err != nil { if debugRequested(args) && stderr != nil { - _, _ = fmt.Fprintln(stderr, "tdc [DEBUG]: operation logging disabled because global settings could not be loaded") + _, _ = fmt.Fprintln(stderr, "ti [DEBUG]: operation logging disabled because global settings could not be loaded") } return disabled() } @@ -315,17 +349,18 @@ func commandEvent(root, cmd *cobra.Command, err error, duration time.Duration) o } profileName, regionCode := commandProfileSummary(cmd) return oplog.Event{ - Type: "command", - Version: rootAnnotation(root, "tdc.version"), - Commit: rootAnnotation(root, "tdc.commit"), - Profile: profileName, - RegionCode: regionCode, - Command: cmd.CommandPath(), - FlagNames: changedFlagNames(cmd), - DurationMS: duration.Milliseconds(), - ExitCode: apperr.ExitCodeFor(err), - ErrorCode: apperr.CodeFor(err), - ErrorCategory: apperr.CategoryFor(err), + Type: "command", + Version: rootAnnotation(root, "ti.version"), + Commit: rootAnnotation(root, "ti.commit"), + Profile: profileName, + RegionCode: regionCode, + Command: cmd.CommandPath(), + FlagNames: changedFlagNames(cmd), + DurationMS: duration.Milliseconds(), + ExitCode: apperr.ExitCodeFor(err), + ErrorCode: apperr.CodeFor(err), + ErrorCategory: apperr.CategoryFor(err), + DeprecatedEnvironmentVariables: envcompat.LegacyNames(nil), } } @@ -366,14 +401,14 @@ func commandProfileSummary(cmd *cobra.Command) (string, string) { profileExplicit = flag.Changed } if !profileExplicit { - if envProfile := strings.TrimSpace(os.Getenv("TDC_PROFILE")); envProfile != "" { + if envProfile := strings.TrimSpace(compatibleEnvironmentValue("TI_PROFILE")); envProfile != "" { profileName = envProfile } } if regionOverride != "" { return profileName, regionOverride } - if envRegion := strings.TrimSpace(os.Getenv("TDC_REGION_CODE")); envRegion != "" { + if envRegion := strings.TrimSpace(compatibleEnvironmentValue("TI_REGION_CODE")); envRegion != "" { return profileName, envRegion } home, err := os.UserHomeDir() @@ -632,10 +667,10 @@ func defaultControlPlaneDryRun(spec controlPlaneCommandSpec) func(commandContext } func isFSCommandPath(path string) bool { - return strings.HasPrefix(path, "tdc fs ") || - strings.HasPrefix(path, "tdc fs-git ") || - strings.HasPrefix(path, "tdc fs-journal ") || - strings.HasPrefix(path, "tdc fs-vault ") + return strings.HasPrefix(path, "ti fs ") || + strings.HasPrefix(path, "ti fs-git ") || + strings.HasPrefix(path, "ti fs-journal ") || + strings.HasPrefix(path, "ti fs-vault ") } func renderStructured(cmd *cobra.Command, result any) error { @@ -696,7 +731,7 @@ func loadOptionsForCommand(cmd *cobra.Command) (config.LoadOptions, error) { if strings.TrimSpace(profileName) == "" { return config.LoadOptions{}, apperr.New("config.empty_profile", "usage", 2, "--profile cannot be empty") } - } else if envProfile := strings.TrimSpace(os.Getenv("TDC_PROFILE")); envProfile != "" { + } else if envProfile := strings.TrimSpace(compatibleEnvironmentValue("TI_PROFILE")); envProfile != "" { profileName = envProfile profileExplicit = true } @@ -710,6 +745,11 @@ func loadOptionsForCommand(cmd *cobra.Command) (config.LoadOptions, error) { }, nil } +func compatibleEnvironmentValue(canonical string) string { + value, _, _, _ := envcompat.ResolveNames(nil, canonical, envcompat.LegacyNameFor(canonical)) + return value +} + func applyCommandDefaults(cmd *cobra.Command, info version.Info) { cmd.Version = info.String() cmd.SetVersionTemplate("{{.Version}}\n") diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index de511ed..4cfd37b 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -14,14 +14,14 @@ import ( "time" "github.com/spf13/cobra" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config/store" - "github.com/tidbcloud/tdc/internal/dryrun" - "github.com/tidbcloud/tdc/internal/oplog" - "github.com/tidbcloud/tdc/internal/settings" - "github.com/tidbcloud/tdc/internal/telemetry" - "github.com/tidbcloud/tdc/internal/version" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config/store" + "github.com/tidbcloud/ti-cli/internal/dryrun" + "github.com/tidbcloud/ti-cli/internal/oplog" + "github.com/tidbcloud/ti-cli/internal/settings" + "github.com/tidbcloud/ti-cli/internal/telemetry" + "github.com/tidbcloud/ti-cli/internal/version" ) func TestHelpCommands(t *testing.T) { @@ -86,8 +86,8 @@ func TestRootRequiresCommand(t *testing.T) { for _, want := range []string{ "the following arguments are required: command", "The TiDB Cloud Command Line Interface is a unified tool", - "usage: tdc [] [parameters]", - "tdc help", + "usage: ti [] [parameters]", + "ti help", } { if !strings.Contains(message, want) { t.Fatalf("expected root usage error to contain %q, got:\n%s", want, message) @@ -157,15 +157,15 @@ func TestShortHelpFlagIsRejected(t *testing.T) { func TestCommandOperationLogRecordsSafeSummary(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_LOGGING", "on") + t.Setenv("TI_LOGGING", "on") configureIAMForTest(t) _, _, err := executeForTest( "configure", "--profile", "ci", "--region-code", "aws-us-east-1", - "--tdc-public-key", "public-secret", - "--tdc-private-key", "private-secret", + "--tidb-cloud-public-key", "public-secret", + "--tidb-cloud-private-key", "private-secret", "--non-interactive", ) if err != nil { @@ -189,10 +189,10 @@ func TestCommandOperationLogRecordsSafeSummary(t *testing.T) { event = candidate } } - if event.Type != "command" || event.Command != "tdc configure" || event.Profile != "ci" || event.RegionCode != "aws-us-east-1" { + if event.Type != "command" || event.Command != "ti configure" || event.Profile != "ci" || event.RegionCode != "aws-us-east-1" { t.Fatalf("unexpected command event: %#v", event) } - if !containsString(event.FlagNames, "tdc-private-key") || !containsString(event.FlagNames, "tdc-public-key") { + if !containsString(event.FlagNames, "tidb-cloud-private-key") || !containsString(event.FlagNames, "tidb-cloud-public-key") { t.Fatalf("expected flag names only, got %#v", event.FlagNames) } } @@ -200,7 +200,7 @@ func TestCommandOperationLogRecordsSafeSummary(t *testing.T) { func TestCommandOperationLogCanBeDisabled(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_LOGGING", "off") + t.Setenv("TI_LOGGING", "off") _, _, err := executeForTest("help") if err != nil { @@ -214,7 +214,7 @@ func TestCommandOperationLogCanBeDisabled(t *testing.T) { func TestMalformedGlobalSettingsDisableLoggingWithRedactedDebug(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_LOGGING", "on") + t.Setenv("TI_LOGGING", "on") if err := os.MkdirAll(filepath.Dir(settings.Path(home)), 0o700); err != nil { t.Fatal(err) } @@ -273,12 +273,12 @@ func TestIsUpdateInvocation(t *testing.T) { } } -func TestUpdateInvocationsDoNotReadOrWriteTDCHome(t *testing.T) { +func TestUpdateInvocationsDoNotReadOrWriteTIHome(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_LOGGING", "on") - tdcDir := filepath.Join(home, ".tdc") - if err := os.MkdirAll(tdcDir, 0o700); err != nil { + t.Setenv("TI_LOGGING", "on") + tiDir := filepath.Join(home, ".ti") + if err := os.MkdirAll(tiDir, 0o700); err != nil { t.Fatal(err) } files := map[string][]byte{ @@ -296,7 +296,7 @@ func TestUpdateInvocationsDoNotReadOrWriteTDCHome(t *testing.T) { _, _ = w.Write([]byte(`{"tag_name":"v0.1.0","html_url":"https://example.test/v0.1.0","assets":[]}`)) })) defer server.Close() - t.Setenv("TDC_RELEASE_API_BASE_URL", server.URL) + t.Setenv("TI_RELEASE_API_BASE_URL", server.URL) for _, args := range [][]string{ {"update", "--check", "--debug"}, @@ -330,8 +330,8 @@ func TestUpdateInvocationsDoNotReadOrWriteTDCHome(t *testing.T) { func TestTelemetryExclusionsDoNotReadStateOrSend(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_LOGGING", "off") - for _, key := range []string{"CI", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI", "TF_BUILD", "JENKINS_URL", "TDC_TELEMETRY"} { + t.Setenv("TI_LOGGING", "off") + for _, key := range []string{"CI", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI", "TF_BUILD", "JENKINS_URL", "TI_TELEMETRY"} { t.Setenv(key, "") } legacyConfig := "[logging]\nenabled = false\n" @@ -352,7 +352,7 @@ func TestTelemetryExclusionsDoNotReadStateOrSend(t *testing.T) { _, _ = w.Write([]byte(`{"tag_name":"v0.2.0","html_url":"https://example.test/v0.2.0","assets":[]}`)) })) defer releaseServer.Close() - t.Setenv("TDC_RELEASE_API_BASE_URL", releaseServer.URL) + t.Setenv("TI_RELEASE_API_BASE_URL", releaseServer.URL) info := testVersion() info.Version = "0.2.0" info.InstallSource = "archive" @@ -406,10 +406,10 @@ func TestTelemetryExclusionsDoNotReadStateOrSend(t *testing.T) { func TestTelemetrySendsCanonicalSafeCommandEvent(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_LOGGING", "off") - t.Setenv("TDC_TELEMETRY", "on") - t.Setenv("TDC_TELEMETRY_TAG", "e2b-preview") - t.Setenv("TDC_TELEMETRY_EXTRA", `{"campaign":"launch","runtime":"e2b"}`) + t.Setenv("TI_LOGGING", "off") + t.Setenv("TI_TELEMETRY", "on") + t.Setenv("TI_TELEMETRY_TAG", "e2b-preview") + t.Setenv("TI_TELEMETRY_EXTRA", `{"campaign":"launch","runtime":"e2b"}`) withConfigEnv(t) requests := make(chan []byte, 1) @@ -468,7 +468,7 @@ func TestTelemetrySendsCanonicalSafeCommandEvent(t *testing.T) { t.Fatalf("event count = %d", len(payload.Events)) } event := payload.Events[0] - if event.CommandPath != "tdc db create-db-cluster" || event.ExitCode != 0 || event.ErrorCode != "" || event.CloudProvider != "aws" || event.RegionCode != "aws-us-east-1" || event.ProfileSource != "explicit" || event.Tag != "e2b-preview" || string(event.Extra) != `{"campaign":"launch","runtime":"e2b"}` { + if event.CommandPath != "ti db create-db-cluster" || event.ExitCode != 0 || event.ErrorCode != "" || event.CloudProvider != "aws" || event.RegionCode != "aws-us-east-1" || event.ProfileSource != "explicit" || event.Tag != "e2b-preview" || string(event.Extra) != `{"campaign":"launch","runtime":"e2b"}` { t.Fatalf("unexpected telemetry event: %#v", event) } for _, want := range []string{"db-cluster-name", "dry-run", "profile", "project-id"} { @@ -481,8 +481,8 @@ func TestTelemetrySendsCanonicalSafeCommandEvent(t *testing.T) { func TestTelemetryCapturesResolvedValidationError(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_LOGGING", "off") - t.Setenv("TDC_TELEMETRY", "on") + t.Setenv("TI_LOGGING", "off") + t.Setenv("TI_TELEMETRY", "on") requests := make(chan []byte, 1) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) @@ -513,7 +513,7 @@ func TestTelemetryCapturesResolvedValidationError(t *testing.T) { if err := json.Unmarshal(body, &payload); err != nil { t.Fatal(err) } - if got := payload.Events[0]; got.CommandPath != "tdc db describe-db-cluster" || got.ExitCode != 2 || got.ErrorCode == "" { + if got := payload.Events[0]; got.CommandPath != "ti db describe-db-cluster" || got.ExitCode != 2 || got.ErrorCode == "" { t.Fatalf("unexpected validation event: %#v", got) } } @@ -524,10 +524,10 @@ func TestResolvedTelemetryCommandUsesCanonicalLeaf(t *testing.T) { args []string want string }{ - {args: []string{"db", "create-db-cluster"}, want: "tdc db create-db-cluster"}, - {args: []string{"--profile", "stage", "db", "create-db-cluster"}, want: "tdc db create-db-cluster"}, - {args: []string{"fs", "cp"}, want: "tdc fs copy-file"}, - {args: []string{"configure"}, want: "tdc configure"}, + {args: []string{"db", "create-db-cluster"}, want: "ti db create-db-cluster"}, + {args: []string{"--profile", "stage", "db", "create-db-cluster"}, want: "ti db create-db-cluster"}, + {args: []string{"fs", "cp"}, want: "ti fs copy-file"}, + {args: []string{"configure"}, want: "ti configure"}, {args: []string{"db"}}, {args: []string{"db", "help"}}, {args: []string{"db", "missing"}}, @@ -557,14 +557,14 @@ func TestNoTelemetryManagementCommandIsRegistered(t *testing.T) { func TestTelemetryFailureDoesNotChangeCommandResult(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_LOGGING", "off") + t.Setenv("TI_LOGGING", "off") withConfigEnv(t) args := []string{"db", "create-db-cluster", "--db-cluster-name", "demo", "--project-id", "project-1", "--dry-run"} - t.Setenv("TDC_TELEMETRY", "off") + t.Setenv("TI_TELEMETRY", "off") wantStdout, wantStderr, wantErr := executeForTestWithInfo(testVersion(), args...) - t.Setenv("TDC_TELEMETRY", "on") + t.Setenv("TI_TELEMETRY", "on") server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("backend-sensitive-message")) @@ -595,14 +595,14 @@ func TestTelemetryFailureDoesNotChangeCommandResult(t *testing.T) { func TestConfigureRejectsReservedLoggingProfileBeforeWrites(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_LOGGING", "off") + t.Setenv("TI_LOGGING", "off") _, _, err := executeForTest( "configure", "--profile", "logging", "--region-code", "aws-us-east-1", - "--tdc-public-key", "public", - "--tdc-private-key", "private", + "--tidb-cloud-public-key", "public", + "--tidb-cloud-private-key", "private", "--non-interactive", ) if err == nil { @@ -639,7 +639,7 @@ func TestHelpUsageShowsRequiredFirstAndOptionalBracketed(t *testing.T) { if strings.Contains(stdout, "[flags]") { t.Fatalf("help output should not use generic [flags] usage:\n%s", stdout) } - if want := "Usage:\n tdc configure\n [--help]\n [--non-interactive]\n [--region-code ]"; !strings.Contains(stdout, want) { + if want := "Usage:\n ti configure\n [--help]\n [--non-interactive]\n [--region-code ]"; !strings.Contains(stdout, want) { t.Fatalf("expected optional flags to be bracketed in configure usage, got:\n%s", stdout) } @@ -743,12 +743,12 @@ func TestServiceCommandsDeclarePermissions(t *testing.T) { return } path := cmd.CommandPath() - if !strings.HasPrefix(path, "tdc db ") && - !strings.HasPrefix(path, "tdc fs ") && - !strings.HasPrefix(path, "tdc fs-git ") && - !strings.HasPrefix(path, "tdc fs-journal ") && - !strings.HasPrefix(path, "tdc fs-vault ") && - !strings.HasPrefix(path, "tdc organization ") { + if !strings.HasPrefix(path, "ti db ") && + !strings.HasPrefix(path, "ti fs ") && + !strings.HasPrefix(path, "ti fs-git ") && + !strings.HasPrefix(path, "ti fs-journal ") && + !strings.HasPrefix(path, "ti fs-vault ") && + !strings.HasPrefix(path, "ti organization ") { return } if _, err := authz.ForCommand(path); err != nil { @@ -803,7 +803,7 @@ func TestFSUnixAliasesResolveToCanonicalCommands(t *testing.T) { t.Fatalf("expected alias %s to resolve to %s, got %s", tt.alias, tt.canonical, resolved.Name()) } path := resolved.CommandPath() - if want := "tdc fs " + tt.canonical; path != want { + if want := "ti fs " + tt.canonical; path != want { t.Fatalf("expected canonical path %q, got %q", want, path) } if _, err := authz.ForCommand(path); err != nil { @@ -824,19 +824,19 @@ func TestFSUnixAliasesResolveToCanonicalCommands(t *testing.T) { func TestFSAdjunctCommandsRequireConfiguredFSResource(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_REGION_CODE", "") - t.Setenv("TDC_PUBLIC_KEY", "") - t.Setenv("TDC_PRIVATE_KEY", "") + t.Setenv("TI_REGION_CODE", "") + t.Setenv("TIDB_CLOUD_PUBLIC_KEY", "") + t.Setenv("TIDB_CLOUD_PRIVATE_KEY", "") writeCompleteProfile(t, home, "default") _, _, err := executeForTest("fs-vault", "list-secrets") if err == nil { - t.Fatal("expected missing tdc fs resource to fail") + t.Fatal("expected missing ti fs resource to fail") } if got := apperr.ExitCodeFor(err); got != 2 { t.Fatalf("expected config exit code 2, got %d", got) } - if got := apperr.MessageFor(err); !strings.Contains(got, "file system name is required") || !strings.Contains(got, "TDC_FS_FILE_SYSTEM_NAME") { + if got := apperr.MessageFor(err); !strings.Contains(got, "file system name is required") || !strings.Contains(got, "TI_FS_FILE_SYSTEM_NAME") { t.Fatalf("unexpected message %q", got) } } @@ -844,17 +844,17 @@ func TestFSAdjunctCommandsRequireConfiguredFSResource(t *testing.T) { func TestFSOperationalCommandsExposeResourceSelector(t *testing.T) { root := NewRootCommand(testVersion()) excluded := map[string]bool{ - "tdc fs list-file-systems": true, - "tdc fs drain-file-system": true, - "tdc fs unmount-file-system": true, - "tdc fs-vault unmount-vault": true, + "ti fs list-file-systems": true, + "ti fs drain-file-system": true, + "ti fs unmount-file-system": true, + "ti fs-vault unmount-vault": true, } visitCommands(root, func(cmd *cobra.Command) { if cmd.Name() == "help" || cmd.HasSubCommands() || excluded[cmd.CommandPath()] { return } path := cmd.CommandPath() - if !strings.HasPrefix(path, "tdc fs ") && !strings.HasPrefix(path, "tdc fs-git ") && !strings.HasPrefix(path, "tdc fs-journal ") && !strings.HasPrefix(path, "tdc fs-vault ") { + if !strings.HasPrefix(path, "ti fs ") && !strings.HasPrefix(path, "ti fs-git ") && !strings.HasPrefix(path, "ti fs-journal ") && !strings.HasPrefix(path, "ti fs-vault ") { return } if cmd.Flags().Lookup("file-system-name") == nil { @@ -866,19 +866,19 @@ func TestFSOperationalCommandsExposeResourceSelector(t *testing.T) { func TestFSRemoteCommandsExposeTokenFlag(t *testing.T) { root := NewRootCommand(testVersion()) excluded := map[string]bool{ - "tdc fs create-file-system": true, - "tdc fs list-file-systems": true, - "tdc fs describe-file-system": true, - "tdc fs drain-file-system": true, - "tdc fs unmount-file-system": true, - "tdc fs-vault unmount-vault": true, + "ti fs create-file-system": true, + "ti fs list-file-systems": true, + "ti fs describe-file-system": true, + "ti fs drain-file-system": true, + "ti fs unmount-file-system": true, + "ti fs-vault unmount-vault": true, } visitCommands(root, func(cmd *cobra.Command) { if cmd.Name() == "help" || cmd.HasSubCommands() || excluded[cmd.CommandPath()] { return } path := cmd.CommandPath() - if !strings.HasPrefix(path, "tdc fs ") && !strings.HasPrefix(path, "tdc fs-git ") && !strings.HasPrefix(path, "tdc fs-journal ") && !strings.HasPrefix(path, "tdc fs-vault ") { + if !strings.HasPrefix(path, "ti fs ") && !strings.HasPrefix(path, "ti fs-git ") && !strings.HasPrefix(path, "ti fs-journal ") && !strings.HasPrefix(path, "ti fs-vault ") { return } if cmd.Flags().Lookup("fs-token") == nil { @@ -890,23 +890,23 @@ func TestFSRemoteCommandsExposeTokenFlag(t *testing.T) { func TestFSRegistryDryRunDoesNotMigrateLegacyState(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_REGION_CODE", "") - t.Setenv("TDC_PUBLIC_KEY", "") - t.Setenv("TDC_PRIVATE_KEY", "") + t.Setenv("TI_REGION_CODE", "") + t.Setenv("TIDB_CLOUD_PUBLIC_KEY", "") + t.Setenv("TIDB_CLOUD_PRIVATE_KEY", "") if err := store.WriteProfile(home, "default", store.ConfigProfile{ RegionCode: "aws-us-east-1", FSResourceName: "workspace", FSTenantID: "tenant-1", FSCloudProvider: "aws", FSRegionCode: "aws-us-east-1", - }, store.CredentialsProfile{TDCPublicKey: "public", TDCPrivateKey: "private", FSAPIKey: "key-1"}); err != nil { + }, store.CredentialsProfile{TiDBCloudPublicKey: "public", TiDBCloudPrivateKey: "private", FSAPIKey: "key-1"}); err != nil { t.Fatal(err) } _, _, err := executeForTest("fs", "copy-file", "--file-system-name", "workspace", "--from-remote", "/source", "--to-remote", "/target", "--dry-run") if err != nil { t.Fatalf("dry-run failed: %v", err) } - if _, err := os.Stat(filepath.Join(home, store.TDCDirName, "fs_resources")); !os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(home, store.TIDirName, "fs_resources")); !os.IsNotExist(err) { t.Fatalf("dry-run migrated legacy state: %v", err) } } @@ -935,17 +935,17 @@ func TestUpdateCheckUsesReleaseMetadata(t *testing.T) { } _, _ = w.Write([]byte(`{ "tag_name": "v0.1.0", - "html_url": "https://github.com/tidbcloud/tdc/releases/tag/v0.1.0", + "html_url": "https://github.com/tidbcloud/ti-cli/releases/tag/v0.1.0", "assets": [ { - "name": "tdc_linux_amd64.tar.gz", - "browser_download_url": "https://github.com/tidbcloud/tdc/releases/download/v0.1.0/tdc_linux_amd64.tar.gz" + "name": "ti_linux_amd64.tar.gz", + "browser_download_url": "https://github.com/tidbcloud/ti-cli/releases/download/v0.1.0/ti_linux_amd64.tar.gz" } ] }`)) })) defer server.Close() - t.Setenv("TDC_RELEASE_API_BASE_URL", server.URL) + t.Setenv("TI_RELEASE_API_BASE_URL", server.URL) stdout, _, err := executeForTest("update", "--check", "--query", "latest_version") if err != nil { @@ -980,14 +980,14 @@ func TestCLIUpdateRefusesUnownedLocalBuild(t *testing.T) { if err == nil { t.Fatal("expected local build update to fail") } - if got := apperr.MessageFor(err); !strings.Contains(got, "not owned by tdc") { + if got := apperr.MessageFor(err); !strings.Contains(got, "not owned by ti") { t.Fatalf("unexpected message: %q", got) } } func TestControlPlaneCommandSpecRendersImplementedResult(t *testing.T) { root := newCommand(commandSpec{ - Use: "tdc", + Use: "ti", RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }, @@ -1023,7 +1023,7 @@ func TestControlPlaneCommandSpecRendersImplementedResult(t *testing.T) { } func TestControlPlaneCommandSpecUsesCustomDryRun(t *testing.T) { - root := newCommand(commandSpec{Use: "tdc"}, testVersion()) + root := newCommand(commandSpec{Use: "ti"}, testVersion()) root.PersistentFlags().String("output", "json", "output format") root.PersistentFlags().String("query", "", "JMESPath query applied to JSON output") root.AddCommand(newControlPlaneCommand(controlPlaneCommandSpec{ @@ -1083,7 +1083,7 @@ func TestMutatingControlPlaneDryRunRendersJSON(t *testing.T) { if !got.DryRun || !got.WouldSendRequest { t.Fatalf("unexpected dry-run flags: %+v", got) } - if got.Command != "tdc db create-db-cluster" { + if got.Command != "ti db create-db-cluster" { t.Fatalf("unexpected command %q", got.Command) } if got.Operation != "create_db_cluster" { @@ -1098,9 +1098,9 @@ func TestMutatingControlPlaneDryRunRendersJSON(t *testing.T) { } func TestRegionOverrideWinsOverEnvironmentCredentials(t *testing.T) { - t.Setenv("TDC_REGION_CODE", "aws-us-east-1") - t.Setenv("TDC_PUBLIC_KEY", "test-public") - t.Setenv("TDC_PRIVATE_KEY", "test-private") + t.Setenv("TI_REGION_CODE", "aws-us-east-1") + t.Setenv("TIDB_CLOUD_PUBLIC_KEY", "test-public") + t.Setenv("TIDB_CLOUD_PRIVATE_KEY", "test-private") stdout, _, err := executeForTest("--region", "aws-ap-southeast-1", "db", "create-db-cluster", "--db-cluster-name", "demo-cluster", "--db-cluster-type", "starter", "--project-id", "project-1", "--dry-run") if err != nil { @@ -1112,9 +1112,9 @@ func TestRegionOverrideWinsOverEnvironmentCredentials(t *testing.T) { } func TestRegionOverrideAllowsEnvironmentCredentialsWithoutEnvRegion(t *testing.T) { - t.Setenv("TDC_REGION_CODE", "") - t.Setenv("TDC_PUBLIC_KEY", "test-public") - t.Setenv("TDC_PRIVATE_KEY", "test-private") + t.Setenv("TI_REGION_CODE", "") + t.Setenv("TIDB_CLOUD_PUBLIC_KEY", "test-public") + t.Setenv("TIDB_CLOUD_PRIVATE_KEY", "test-private") stdout, _, err := executeForTest("--region", "ali-ap-southeast-1", "db", "create-db-cluster", "--db-cluster-name", "demo-cluster", "--db-cluster-type", "starter", "--project-id", "project-1", "--dry-run") if err != nil { @@ -1128,9 +1128,9 @@ func TestRegionOverrideAllowsEnvironmentCredentialsWithoutEnvRegion(t *testing.T func TestCreateClusterUsesConfiguredDefaultProject(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_REGION_CODE", "") - t.Setenv("TDC_PUBLIC_KEY", "") - t.Setenv("TDC_PRIVATE_KEY", "") + t.Setenv("TI_REGION_CODE", "") + t.Setenv("TIDB_CLOUD_PUBLIC_KEY", "") + t.Setenv("TIDB_CLOUD_PRIVATE_KEY", "") writeCompleteProfile(t, home, "default") stdout, _, err := executeForTest("db", "create-db-cluster", "--db-cluster-name", "demo-cluster", "--db-cluster-type", "starter", "--dry-run") @@ -1145,14 +1145,14 @@ func TestCreateClusterUsesConfiguredDefaultProject(t *testing.T) { func TestCreateClusterAllowsMissingConfiguredProject(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_REGION_CODE", "") - t.Setenv("TDC_PUBLIC_KEY", "") - t.Setenv("TDC_PRIVATE_KEY", "") + t.Setenv("TI_REGION_CODE", "") + t.Setenv("TIDB_CLOUD_PUBLIC_KEY", "") + t.Setenv("TIDB_CLOUD_PRIVATE_KEY", "") if err := store.WriteProfile(home, "default", store.ConfigProfile{ RegionCode: "aws-us-east-1", }, store.CredentialsProfile{ - TDCPublicKey: "test-public", - TDCPrivateKey: "test-private", + TiDBCloudPublicKey: "test-public", + TiDBCloudPrivateKey: "test-private", }); err != nil { t.Fatal(err) } @@ -1169,9 +1169,9 @@ func TestCreateClusterAllowsMissingConfiguredProject(t *testing.T) { func TestCreateClusterExplicitEmptyProjectDoesNotFallback(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_REGION_CODE", "") - t.Setenv("TDC_PUBLIC_KEY", "") - t.Setenv("TDC_PRIVATE_KEY", "") + t.Setenv("TI_REGION_CODE", "") + t.Setenv("TIDB_CLOUD_PUBLIC_KEY", "") + t.Setenv("TIDB_CLOUD_PRIVATE_KEY", "") writeCompleteProfile(t, home, "default") _, _, err := executeForTest("db", "create-db-cluster", "--db-cluster-name", "demo-cluster", "--db-cluster-type", "starter", "--project-id", "", "--dry-run") @@ -1180,23 +1180,23 @@ func TestCreateClusterExplicitEmptyProjectDoesNotFallback(t *testing.T) { } } -func TestConfigureUsesTDCProfileNamespace(t *testing.T) { +func TestConfigureUsesTIProfileNamespace(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_PROFILE", "stage") + t.Setenv("TI_PROFILE", "stage") configureIAMForTest(t) stdout, _, err := executeForTest( "configure", "--non-interactive", "--region-code", "aws-us-east-1", - "--tdc-public-key", "public", - "--tdc-private-key", "private", + "--tidb-cloud-public-key", "public", + "--tidb-cloud-private-key", "private", ) if err != nil { t.Fatalf("configure failed: %v", err) } if !strings.Contains(stdout, `"profile": "stage"`) { - t.Fatalf("configure did not select TDC_PROFILE:\n%s", stdout) + t.Fatalf("configure did not select TI_PROFILE:\n%s", stdout) } doc, err := store.ReadConfig(home) if err != nil { @@ -1212,13 +1212,13 @@ func TestConfigureUsesTDCProfileNamespace(t *testing.T) { func TestConfigureDoesNotDisplayTelemetryNotice(t *testing.T) { t.Setenv("HOME", t.TempDir()) - t.Setenv("TDC_TELEMETRY", "off") + t.Setenv("TI_TELEMETRY", "off") configureIAMForTest(t) _, stderr, err := executeForTest( "configure", "--non-interactive", "--region-code", "aws-us-east-1", - "--tdc-public-key", "public", - "--tdc-private-key", "private", + "--tidb-cloud-public-key", "public", + "--tidb-cloud-private-key", "private", ) if err != nil { t.Fatalf("configure failed: %v", err) @@ -1248,7 +1248,7 @@ func TestMutatingControlPlaneDryRunSupportsTextOutput(t *testing.T) { if err != nil { t.Fatalf("expected dry-run to succeed, got %v", err) } - if !strings.Contains(stdout, "Dry run: tdc db create-db-cluster") { + if !strings.Contains(stdout, "Dry run: ti db create-db-cluster") { t.Fatalf("unexpected text output:\n%s", stdout) } } @@ -1260,7 +1260,7 @@ func TestQueryAppliesToDryRunResult(t *testing.T) { if err != nil { t.Fatalf("expected query to succeed, got %v", err) } - if got := stdout; got != "\"tdc db create-db-cluster\"\n" { + if got := stdout; got != "\"ti db create-db-cluster\"\n" { t.Fatalf("unexpected query output %q", got) } } @@ -1282,9 +1282,9 @@ func TestInvalidQueryFails(t *testing.T) { func TestDryRunRequiresConfigAndCredentials(t *testing.T) { t.Setenv("HOME", t.TempDir()) - t.Setenv("TDC_REGION_CODE", "") - t.Setenv("TDC_PUBLIC_KEY", "") - t.Setenv("TDC_PRIVATE_KEY", "") + t.Setenv("TI_REGION_CODE", "") + t.Setenv("TIDB_CLOUD_PUBLIC_KEY", "") + t.Setenv("TIDB_CLOUD_PRIVATE_KEY", "") writeConfigOnlyProfile(t, "default") _, _, err := executeForTest("db", "create-db-cluster", "--db-cluster-name", "demo-cluster", "--db-cluster-type", "starter", "--project-id", "project-1", "--dry-run") @@ -1299,13 +1299,13 @@ func TestDryRunRequiresConfigAndCredentials(t *testing.T) { } } -func TestTDCProfileEnvironmentSelectsFileProfile(t *testing.T) { +func TestTIProfileEnvironmentSelectsFileProfile(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - t.Setenv("TDC_PROFILE", "stage") - t.Setenv("TDC_REGION_CODE", "") - t.Setenv("TDC_PUBLIC_KEY", "") - t.Setenv("TDC_PRIVATE_KEY", "") + t.Setenv("TI_PROFILE", "stage") + t.Setenv("TI_REGION_CODE", "") + t.Setenv("TIDB_CLOUD_PUBLIC_KEY", "") + t.Setenv("TIDB_CLOUD_PRIVATE_KEY", "") writeCompleteProfile(t, home, "stage") stdout, _, err := executeForTest("db", "create-db-cluster", "--db-cluster-name", "demo-cluster", "--db-cluster-type", "starter", "--project-id", "project-1", "--dry-run") @@ -1313,12 +1313,12 @@ func TestTDCProfileEnvironmentSelectsFileProfile(t *testing.T) { t.Fatalf("expected dry-run to succeed, got %v", err) } if !strings.Contains(stdout, `profile \"stage\" loaded`) { - t.Fatalf("expected TDC_PROFILE to select stage profile, got:\n%s", stdout) + t.Fatalf("expected TI_PROFILE to select stage profile, got:\n%s", stdout) } } func TestExplicitEmptyProfileFails(t *testing.T) { - t.Setenv("TDC_PROFILE", "stage") + t.Setenv("TI_PROFILE", "stage") _, _, err := executeForTest("db", "list-db-clusters", "--profile", "") if err == nil { @@ -1347,9 +1347,9 @@ func TestDryRunRejectedOnReadOnlyCommand(t *testing.T) { func withConfigEnv(t *testing.T) { t.Helper() - t.Setenv("TDC_REGION_CODE", "aws-us-east-1") - t.Setenv("TDC_PUBLIC_KEY", "test-public") - t.Setenv("TDC_PRIVATE_KEY", "test-private") + t.Setenv("TI_REGION_CODE", "aws-us-east-1") + t.Setenv("TIDB_CLOUD_PUBLIC_KEY", "test-public") + t.Setenv("TIDB_CLOUD_PRIVATE_KEY", "test-private") } func writeCompleteProfile(t *testing.T, home, profileName string) { @@ -1358,8 +1358,8 @@ func writeCompleteProfile(t *testing.T, home, profileName string) { RegionCode: "aws-us-east-1", ProjectID: "virtual-test", }, store.CredentialsProfile{ - TDCPublicKey: "test-public", - TDCPrivateKey: "test-private", + TiDBCloudPublicKey: "test-public", + TiDBCloudPrivateKey: "test-private", }) if err != nil { t.Fatal(err) @@ -1382,10 +1382,49 @@ func executeForTest(args ...string) (string, string, error) { return executeForTestWithInfo(testVersion(), args...) } +func TestExecuteWithHomeMigrationMigratesBeforeLogging(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("TI_LOGGING", "off") + legacyRoot := filepath.Join(home, ".tdc") + if err := os.MkdirAll(legacyRoot, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(legacyRoot, "config"), []byte("[default]\nregion_code='aws-us-east-1'\n"), 0o644); err != nil { + t.Fatal(err) + } + var stdout, stderr bytes.Buffer + root := NewRootCommand(testVersion()) + if err := Execute(context.Background(), root, testVersion(), []string{"help"}, &stdout, &stderr, WithHomeMigration()); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(home, ".ti", "config")); err != nil { + t.Fatalf("migration did not publish config: %v", err) + } + if _, err := os.Stat(filepath.Join(legacyRoot, "config")); err != nil { + t.Fatalf("migration modified legacy config: %v", err) + } +} + +func TestExecuteUpdateHelpBypassesHomeMigration(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + for _, name := range []string{".tdc", ".ti"} { + if err := os.MkdirAll(filepath.Join(home, name), 0o700); err != nil { + t.Fatal(err) + } + } + var stdout, stderr bytes.Buffer + root := NewRootCommand(testVersion()) + if err := Execute(context.Background(), root, testVersion(), []string{"update", "help"}, &stdout, &stderr, WithHomeMigration()); err != nil { + t.Fatalf("update help unexpectedly inspected home state: %v", err) + } +} + func executeForTestWithInfo(info version.Info, args ...string) (string, string, error) { - if _, ok := os.LookupEnv("TDC_LOGGING"); !ok { - _ = os.Setenv("TDC_LOGGING", "off") - defer os.Unsetenv("TDC_LOGGING") + if _, ok := os.LookupEnv("TI_LOGGING"); !ok { + _ = os.Setenv("TI_LOGGING", "off") + defer os.Unsetenv("TI_LOGGING") } var stdout bytes.Buffer var stderr bytes.Buffer @@ -1404,8 +1443,8 @@ func configureIAMForTest(t *testing.T) { _, _ = w.Write([]byte(`{"projects":[{"id":"virtual-test","type":"tidbx_virtual"}]}`)) })) t.Cleanup(server.Close) - t.Setenv("TDC_ALLOW_TEST_ENDPOINTS", "1") - t.Setenv("TDC_TEST_IAM_BASE_URL", server.URL) + t.Setenv("TI_ALLOW_TEST_ENDPOINTS", "1") + t.Setenv("TI_TEST_IAM_BASE_URL", server.URL) } func testVersion() version.Info { diff --git a/internal/config/configure/wizard.go b/internal/config/configure/wizard.go index ba4be53..ed87196 100644 --- a/internal/config/configure/wizard.go +++ b/internal/config/configure/wizard.go @@ -11,32 +11,33 @@ import ( "strings" "time" - "github.com/tidbcloud/tdc/internal/api" - "github.com/tidbcloud/tdc/internal/api/endpoints" - apiiam "github.com/tidbcloud/tdc/internal/api/iam" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/config/region" - "github.com/tidbcloud/tdc/internal/config/store" - "github.com/tidbcloud/tdc/internal/secretinput" + "github.com/tidbcloud/ti-cli/internal/api" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + apiiam "github.com/tidbcloud/ti-cli/internal/api/iam" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/config/envcompat" + "github.com/tidbcloud/ti-cli/internal/config/region" + "github.com/tidbcloud/ti-cli/internal/config/store" + "github.com/tidbcloud/ti-cli/internal/secretinput" ) type Options struct { - Profile string - HomeDir string - RegionCode string - TDCPublicKey string - TDCPrivateKey string - NonInteractive bool - Env map[string]string - In io.Reader - Out io.Writer - Resolver endpoints.Resolver - Transport http.RoundTripper - Timeout time.Duration - Debug bool - DebugWriter io.Writer + Profile string + HomeDir string + RegionCode string + TiDBCloudPublicKey string + TiDBCloudPrivateKey string + NonInteractive bool + Env map[string]string + In io.Reader + Out io.Writer + Resolver endpoints.Resolver + Transport http.RoundTripper + Timeout time.Duration + Debug bool + DebugWriter io.Writer } type Result struct { @@ -89,7 +90,11 @@ func Run(ctx context.Context, opts Options) (Result, error) { } defaultRegion := region.DefaultPlacementCode() - regionCode, err := valueOrPrompt(ctx, input, opts.Out, valueOrEnv(opts.RegionCode, opts.Env, "TDC_REGION_CODE"), "default region code", fmt.Sprintf("Default region code (%s): ", defaultRegion), defaultRegion, false, opts.NonInteractive) + regionInput, err := valueOrEnv(opts.RegionCode, opts.Env, "TI_REGION_CODE") + if err != nil { + return Result{}, err + } + regionCode, err := valueOrPrompt(ctx, input, opts.Out, regionInput, "default region code", fmt.Sprintf("Default region code (%s): ", defaultRegion), defaultRegion, false, opts.NonInteractive) if err != nil { return Result{}, err } @@ -98,11 +103,19 @@ func Run(ctx context.Context, opts Options) (Result, error) { return Result{}, apperr.Wrap("config.invalid_region", "config", 2, err.Error(), err) } - publicKey, err := valueOrPrompt(ctx, input, opts.Out, valueOrEnv(opts.TDCPublicKey, opts.Env, "TDC_PUBLIC_KEY"), "TiDB Cloud public key", "TiDB Cloud public key: ", "", false, opts.NonInteractive) + publicKeyInput, err := valueOrEnv(opts.TiDBCloudPublicKey, opts.Env, "TIDB_CLOUD_PUBLIC_KEY") + if err != nil { + return Result{}, err + } + publicKey, err := valueOrPrompt(ctx, input, opts.Out, publicKeyInput, "TiDB Cloud public key", "TiDB Cloud public key: ", "", false, opts.NonInteractive) if err != nil { return Result{}, err } - privateKey, err := valueOrPrompt(ctx, input, opts.Out, valueOrEnv(opts.TDCPrivateKey, opts.Env, "TDC_PRIVATE_KEY"), "TiDB Cloud private key", "TiDB Cloud private key: ", "", true, opts.NonInteractive) + privateKeyInput, err := valueOrEnv(opts.TiDBCloudPrivateKey, opts.Env, "TIDB_CLOUD_PRIVATE_KEY") + if err != nil { + return Result{}, err + } + privateKey, err := valueOrPrompt(ctx, input, opts.Out, privateKeyInput, "TiDB Cloud private key", "TiDB Cloud private key: ", "", true, opts.NonInteractive) if err != nil { return Result{}, err } @@ -112,8 +125,8 @@ func Run(ctx context.Context, opts Options) (Result, error) { PlacementRegionCode: placement.Code, CloudProvider: placement.Provider, RegionCode: placement.NativeCode, - TDCPublicKey: publicKey, - TDCPrivateKey: privateKey, + TiDBCloudPublicKey: publicKey, + TiDBCloudPrivateKey: privateKey, } project, err := discoverVirtualProject(ctx, opts, profile) if err != nil { @@ -124,8 +137,8 @@ func Run(ctx context.Context, opts Options) (Result, error) { RegionCode: placement.Code, ProjectID: project.ID, }, store.CredentialsProfile{ - TDCPublicKey: publicKey, - TDCPrivateKey: privateKey, + TiDBCloudPublicKey: publicKey, + TiDBCloudPrivateKey: privateKey, }); err != nil { return Result{}, apperr.Wrap("config.write_failed", "config", 1, err.Error(), err) } @@ -158,7 +171,7 @@ func discoverVirtualProject(ctx context.Context, opts Options, profile *config.P Timeout: timeout, Debug: opts.Debug, DebugWriter: opts.DebugWriter, - UserAgent: "tdc configure", + UserAgent: "ti configure", }) if err != nil { return apiiam.Project{}, err @@ -228,7 +241,7 @@ func valueOrPrompt(ctx context.Context, in io.Reader, out io.Writer, value, fiel "config.non_interactive_missing", "config", 2, - fmt.Sprintf("%s is required for non-interactive configure; provide a flag or TDC_* environment variable", fieldName), + fmt.Sprintf("%s is required for non-interactive configure; provide a flag or TI_* environment variable", fieldName), ) } @@ -245,12 +258,10 @@ func valueOrPrompt(ctx context.Context, in io.Reader, out io.Writer, value, fiel return strings.TrimSpace(value), nil } -func valueOrEnv(value string, env map[string]string, key string) string { +func valueOrEnv(value string, env map[string]string, key string) (string, error) { if value != "" { - return value - } - if env != nil { - return env[key] + return value, nil } - return os.Getenv(key) + resolved, _, _, err := envcompat.ResolveNames(env, key, envcompat.LegacyNameFor(key)) + return resolved, err } diff --git a/internal/config/configure/wizard_test.go b/internal/config/configure/wizard_test.go index 2a7714f..8c2e2b4 100644 --- a/internal/config/configure/wizard_test.go +++ b/internal/config/configure/wizard_test.go @@ -10,11 +10,11 @@ import ( "strings" "testing" - "github.com/tidbcloud/tdc/internal/api/endpoints" - apiiam "github.com/tidbcloud/tdc/internal/api/iam" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/config/store" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + apiiam "github.com/tidbcloud/ti-cli/internal/api/iam" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/config/store" ) func TestRunWritesProfileAndDoesNotPrintSecret(t *testing.T) { @@ -54,7 +54,7 @@ func TestRunWritesProfileAndDoesNotPrintSecret(t *testing.T) { if profile.CloudProvider != "aws" || profile.RegionCode != "us-east-1" { t.Fatalf("unexpected profile: %#v", profile) } - if profile.TDCPrivateKey != "private-key" { + if profile.TiDBCloudPrivateKey != "private-key" { t.Fatal("private key was not stored") } if profile.ProjectID != "virtual-1" { @@ -84,9 +84,9 @@ func TestRunNonInteractiveUsesEnvironment(t *testing.T) { HomeDir: home, NonInteractive: true, Env: map[string]string{ - "TDC_REGION_CODE": "aws-us-east-1", - "TDC_PUBLIC_KEY": "env-public", - "TDC_PRIVATE_KEY": "env-private", + "TI_REGION_CODE": "aws-us-east-1", + "TIDB_CLOUD_PUBLIC_KEY": "env-public", + "TIDB_CLOUD_PRIVATE_KEY": "env-private", }, Out: &output, Resolver: testProjectResolver(t, `{"projects":[{"id":"virtual-env","type":"tidbx_virtual"}]}`), @@ -112,7 +112,7 @@ func TestRunNonInteractiveUsesEnvironment(t *testing.T) { if profile.CloudProvider != "aws" || profile.RegionCode != "us-east-1" { t.Fatalf("unexpected profile: %#v", profile) } - if profile.TDCPublicKey != "env-public" || profile.TDCPrivateKey != "env-private" { + if profile.TiDBCloudPublicKey != "env-public" || profile.TiDBCloudPrivateKey != "env-private" { t.Fatalf("env credentials not stored: %#v", profile) } if profile.ProjectID != "virtual-env" { @@ -125,8 +125,8 @@ func TestRunNonInteractiveRequiresMissingValues(t *testing.T) { HomeDir: t.TempDir(), NonInteractive: true, Env: map[string]string{ - "TDC_REGION_CODE": "aws-us-east-1", - "TDC_PUBLIC_KEY": "env-public", + "TI_REGION_CODE": "aws-us-east-1", + "TIDB_CLOUD_PUBLIC_KEY": "env-public", }, Out: &bytes.Buffer{}, }) @@ -205,7 +205,7 @@ func TestSelectVirtualProjectPropagatesCancellation(t *testing.T) { func TestRunDiscoveryFailurePreservesExistingProfile(t *testing.T) { home := t.TempDir() - if err := store.WriteProfile(home, "stage", store.ConfigProfile{RegionCode: "aws-us-west-2", ProjectID: "virtual-old"}, store.CredentialsProfile{TDCPublicKey: "old-public", TDCPrivateKey: "old-private"}); err != nil { + if err := store.WriteProfile(home, "stage", store.ConfigProfile{RegionCode: "aws-us-west-2", ProjectID: "virtual-old"}, store.CredentialsProfile{TiDBCloudPublicKey: "old-public", TiDBCloudPrivateKey: "old-private"}); err != nil { t.Fatal(err) } beforeConfig, err := os.ReadFile(store.ConfigPath(home)) @@ -218,13 +218,13 @@ func TestRunDiscoveryFailurePreservesExistingProfile(t *testing.T) { } _, err = Run(context.Background(), Options{ - Profile: "stage", - HomeDir: home, - NonInteractive: true, - RegionCode: "aws-us-east-1", - TDCPublicKey: "new-public", - TDCPrivateKey: "new-private", - Resolver: testProjectResolver(t, `{"projects":[{"id":"regular-1","type":"tidbx"}]}`), + Profile: "stage", + HomeDir: home, + NonInteractive: true, + RegionCode: "aws-us-east-1", + TiDBCloudPublicKey: "new-public", + TiDBCloudPrivateKey: "new-private", + Resolver: testProjectResolver(t, `{"projects":[{"id":"regular-1","type":"tidbx"}]}`), }) if apperr.CodeFor(err) != "config.virtual_project_not_found" { t.Fatalf("unexpected error: %v", err) @@ -238,17 +238,17 @@ func TestRunDiscoveryFailurePreservesExistingProfile(t *testing.T) { func TestRunReconfigureReplacesProjectID(t *testing.T) { home := t.TempDir() - if err := store.WriteProfile(home, "stage", store.ConfigProfile{RegionCode: "aws-us-west-2", ProjectID: "virtual-old"}, store.CredentialsProfile{TDCPublicKey: "old-public", TDCPrivateKey: "old-private"}); err != nil { + if err := store.WriteProfile(home, "stage", store.ConfigProfile{RegionCode: "aws-us-west-2", ProjectID: "virtual-old"}, store.CredentialsProfile{TiDBCloudPublicKey: "old-public", TiDBCloudPrivateKey: "old-private"}); err != nil { t.Fatal(err) } _, err := Run(context.Background(), Options{ - Profile: "stage", - HomeDir: home, - NonInteractive: true, - RegionCode: "aws-us-east-1", - TDCPublicKey: "new-public", - TDCPrivateKey: "new-private", - Resolver: testProjectResolver(t, `{"projects":[{"id":"virtual-new","type":"tidbx_virtual"}]}`), + Profile: "stage", + HomeDir: home, + NonInteractive: true, + RegionCode: "aws-us-east-1", + TiDBCloudPublicKey: "new-public", + TiDBCloudPrivateKey: "new-private", + Resolver: testProjectResolver(t, `{"projects":[{"id":"virtual-new","type":"tidbx_virtual"}]}`), }) if err != nil { t.Fatalf("reconfigure: %v", err) @@ -257,7 +257,7 @@ func TestRunReconfigureReplacesProjectID(t *testing.T) { if err != nil { t.Fatal(err) } - if profile.ProjectID != "virtual-new" || profile.TDCPublicKey != "new-public" { + if profile.ProjectID != "virtual-new" || profile.TiDBCloudPublicKey != "new-public" { t.Fatalf("profile was not refreshed: %#v", profile) } } diff --git a/internal/config/envcompat/envcompat.go b/internal/config/envcompat/envcompat.go new file mode 100644 index 0000000..e2b0106 --- /dev/null +++ b/internal/config/envcompat/envcompat.go @@ -0,0 +1,91 @@ +package envcompat + +import ( + "fmt" + "os" + "sort" + "strings" + + "github.com/tidbcloud/ti-cli/internal/apperr" +) + +type Pair struct { + Canonical string + Legacy string +} + +var PublicPairs = []Pair{ + {Canonical: "TI_PROFILE", Legacy: "TDC_PROFILE"}, + {Canonical: "TI_REGION_CODE", Legacy: "TDC_REGION_CODE"}, + {Canonical: "TIDB_CLOUD_PUBLIC_KEY", Legacy: "TDC_PUBLIC_KEY"}, + {Canonical: "TIDB_CLOUD_PRIVATE_KEY", Legacy: "TDC_PRIVATE_KEY"}, + {Canonical: "TI_FS_TOKEN", Legacy: "TDC_FS_TOKEN"}, + {Canonical: "TI_FS_FILE_SYSTEM_NAME", Legacy: "TDC_FS_FILE_SYSTEM_NAME"}, + {Canonical: "TI_LOGGING", Legacy: "TDC_LOGGING"}, + {Canonical: "TI_TELEMETRY", Legacy: "TDC_TELEMETRY"}, + {Canonical: "TI_TELEMETRY_TAG", Legacy: "TDC_TELEMETRY_TAG"}, + {Canonical: "TI_TELEMETRY_EXTRA", Legacy: "TDC_TELEMETRY_EXTRA"}, + {Canonical: "TI_VAULT_TOKEN", Legacy: "TDC_VAULT_TOKEN"}, + {Canonical: "TI_INSTALL_DIR", Legacy: "TDC_INSTALL_DIR"}, +} + +func Resolve(env map[string]string, pair Pair) (value string, present bool, usedLegacy bool, err error) { + canonicalValue, canonicalSet := lookup(env, pair.Canonical) + legacyValue, legacySet := lookup(env, pair.Legacy) + if canonicalSet && legacySet && canonicalValue != legacyValue { + return "", false, false, apperr.New( + "config.environment_conflict", + "config", + 2, + fmt.Sprintf("environment variables %s and %s contain different values; unset one or make them equal", pair.Canonical, pair.Legacy), + ) + } + if canonicalSet { + return canonicalValue, true, false, nil + } + if legacySet { + return legacyValue, true, true, nil + } + return "", false, false, nil +} + +func ResolveNames(env map[string]string, canonical, legacy string) (string, bool, bool, error) { + return Resolve(env, Pair{Canonical: canonical, Legacy: legacy}) +} + +func LegacyNames(env map[string]string) []string { + names := make([]string, 0) + for _, pair := range PublicPairs { + if _, ok := lookup(env, pair.Legacy); ok { + names = append(names, pair.Legacy) + } + } + sort.Strings(names) + return names +} + +func Validate(env map[string]string) error { + for _, pair := range PublicPairs { + if _, _, _, err := Resolve(env, pair); err != nil { + return err + } + } + return nil +} + +func LegacyNameFor(canonical string) string { + for _, pair := range PublicPairs { + if pair.Canonical == canonical { + return pair.Legacy + } + } + return strings.Replace(canonical, "TI_", "TDC_", 1) +} + +func lookup(env map[string]string, key string) (string, bool) { + if env != nil { + value, ok := env[key] + return value, ok + } + return os.LookupEnv(key) +} diff --git a/internal/config/envcompat/envcompat_test.go b/internal/config/envcompat/envcompat_test.go new file mode 100644 index 0000000..c888c38 --- /dev/null +++ b/internal/config/envcompat/envcompat_test.go @@ -0,0 +1,49 @@ +package envcompat + +import ( + "reflect" + "testing" + + "github.com/tidbcloud/ti-cli/internal/apperr" +) + +func TestResolveCompatibility(t *testing.T) { + pair := Pair{Canonical: "TI_REGION_CODE", Legacy: "TDC_REGION_CODE"} + tests := []struct { + name string + env map[string]string + want string + present bool + usedLegacy bool + wantCode string + }{ + {name: "missing", env: map[string]string{}}, + {name: "canonical", env: map[string]string{"TI_REGION_CODE": "aws-us-east-1"}, want: "aws-us-east-1", present: true}, + {name: "legacy", env: map[string]string{"TDC_REGION_CODE": "aws-us-west-2"}, want: "aws-us-west-2", present: true, usedLegacy: true}, + {name: "equal", env: map[string]string{"TI_REGION_CODE": "aws-us-east-1", "TDC_REGION_CODE": "aws-us-east-1"}, want: "aws-us-east-1", present: true}, + {name: "conflict", env: map[string]string{"TI_REGION_CODE": "aws-us-east-1", "TDC_REGION_CODE": "aws-us-west-2"}, wantCode: "config.environment_conflict"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, present, usedLegacy, err := Resolve(tt.env, pair) + if apperr.CodeFor(err) != tt.wantCode { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want || present != tt.present || usedLegacy != tt.usedLegacy { + t.Fatalf("Resolve() = %q, %v, %v; want %q, %v, %v", got, present, usedLegacy, tt.want, tt.present, tt.usedLegacy) + } + }) + } +} + +func TestLegacyNamesDoesNotExposeValues(t *testing.T) { + got := LegacyNames(map[string]string{ + "TDC_PRIVATE_KEY": "secret", + "TDC_PROFILE": "stage", + "UNRELATED": "value", + }) + want := []string{"TDC_PRIVATE_KEY", "TDC_PROFILE"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("LegacyNames() = %#v, want %#v", got, want) + } +} diff --git a/internal/config/homemigration/migration.go b/internal/config/homemigration/migration.go new file mode 100644 index 0000000..3c849eb --- /dev/null +++ b/internal/config/homemigration/migration.go @@ -0,0 +1,422 @@ +package homemigration + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "runtime" + "sort" + "strings" + + "github.com/pelletier/go-toml/v2" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config/store" +) + +const LegacyDirName = ".tdc" + +const migrationMarkerName = ".migrated-from-tdc" + +var durableNames = []string{ + "config", + "credentials", + ".preferences", + ".telemetry-installation-id", + "db_users", + "fs_resources", + "fs_credentials", +} + +var installationIDPattern = regexp.MustCompile(`^(?:tdc|ti)_[A-Za-z0-9_-]{22}$`) + +type Result struct { + Status string `json:"status"` + Source string `json:"source,omitempty"` + Destination string `json:"destination"` + Copied []string `json:"copied,omitempty"` +} + +type migrationMarker struct { + SchemaVersion int `json:"schema_version"` + Source string `json:"source"` +} + +type options struct { + copyRegular func(string, string, os.FileMode) error + beforePublish func(string) error +} + +func Ensure(homeDir string) (Result, error) { + return ensure(homeDir, options{}) +} + +func ensure(homeDir string, opts options) (Result, error) { + homeDir = strings.TrimSpace(homeDir) + if homeDir == "" { + return Result{}, migrationError("config.home_migration_failed", "home directory is required", nil) + } + legacyRoot := filepath.Join(homeDir, LegacyDirName) + destination := filepath.Join(homeDir, store.TIDirName) + legacyInfo, legacyExists, err := lstatOptional(legacyRoot) + if err != nil { + return Result{}, migrationError("config.home_migration_failed", fmt.Sprintf("inspect legacy state %s", legacyRoot), err) + } + destinationInfo, destinationExists, err := lstatOptional(destination) + if err != nil { + return Result{}, migrationError("config.home_migration_failed", fmt.Sprintf("inspect state %s", destination), err) + } + result := Result{Status: "not_needed", Destination: destination} + if legacyExists && destinationExists { + if validMigrationMarker(destination, legacyRoot) { + return result, nil + } + return Result{}, apperr.New( + "config.home_migration_conflict", + "config", + 2, + fmt.Sprintf("both %s and %s exist; keep one source of truth and remove or move the other before running ti", legacyRoot, destination), + ) + } + if destinationExists { + if !destinationInfo.IsDir() || destinationInfo.Mode()&os.ModeSymlink != 0 { + return Result{}, unsafeSourceError(destination, "new state path is not a regular directory") + } + return result, nil + } + if !legacyExists { + return result, nil + } + if !legacyInfo.IsDir() || legacyInfo.Mode()&os.ModeSymlink != 0 { + return Result{}, unsafeSourceError(legacyRoot, "legacy state path is not a regular directory") + } + if activePath, active, err := activeLegacyMount(legacyRoot); err != nil { + return Result{}, migrationError("config.home_migration_failed", "inspect legacy mount state", err) + } else if active { + return Result{}, apperr.New( + "config.home_migration_active_mount", + "config", + 2, + fmt.Sprintf("legacy mount at %q is still active; run `tdc fs drain-file-system --mount-path %s` and `tdc fs unmount-file-system --mount-path %s` with the old executable before retrying", activePath, shellArgument(activePath), shellArgument(activePath)), + ) + } + + temporary, err := os.MkdirTemp(homeDir, ".ti.migrate-*") + if err != nil { + return Result{}, migrationError("config.home_migration_failed", fmt.Sprintf("create migration staging directory beside %s", destination), err) + } + published := false + defer func() { + if !published { + _ = os.RemoveAll(temporary) + } + }() + if err := os.Chmod(temporary, 0o700); err != nil && runtime.GOOS != "windows" { + return Result{}, migrationError("config.home_migration_failed", "restrict migration staging directory", err) + } + copyRegular := opts.copyRegular + if copyRegular == nil { + copyRegular = copyRegularFile + } + copied := make([]string, 0, len(durableNames)) + for _, name := range durableNames { + source := filepath.Join(legacyRoot, name) + if _, exists, err := lstatOptional(source); err != nil { + return Result{}, migrationError("config.home_migration_failed", fmt.Sprintf("inspect %s", source), err) + } else if !exists { + continue + } + if err := copyEntry(source, filepath.Join(temporary, name), name, copyRegular); err != nil { + return Result{}, err + } + copied = append(copied, name) + } + markerData, err := json.Marshal(migrationMarker{SchemaVersion: 1, Source: legacyRoot}) + if err != nil { + return Result{}, migrationError("config.home_migration_failed", "encode migration marker", err) + } + markerData = append(markerData, '\n') + if err := replaceFile(filepath.Join(temporary, migrationMarkerName), markerData, 0o600); err != nil { + return Result{}, migrationError("config.home_migration_failed", "write migration marker", err) + } + if err := validateStaged(temporary); err != nil { + return Result{}, err + } + if opts.beforePublish != nil { + if err := opts.beforePublish(temporary); err != nil { + return Result{}, migrationError("config.home_migration_failed", "publish migrated state", err) + } + } + if err := os.Rename(temporary, destination); err != nil { + return Result{}, migrationError("config.home_migration_failed", fmt.Sprintf("publish migrated state at %s", destination), err) + } + published = true + sort.Strings(copied) + return Result{Status: "migrated", Source: legacyRoot, Destination: destination, Copied: copied}, nil +} + +func validMigrationMarker(destination, legacyRoot string) bool { + path := filepath.Join(destination, migrationMarkerName) + info, err := os.Lstat(path) + if err != nil || !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 { + return false + } + if runtime.GOOS != "windows" && info.Mode().Perm()&0o077 != 0 { + return false + } + data, err := os.ReadFile(path) + if err != nil { + return false + } + var marker migrationMarker + if err := json.Unmarshal(data, &marker); err != nil { + return false + } + return marker.SchemaVersion == 1 && filepath.Clean(marker.Source) == filepath.Clean(legacyRoot) +} + +func copyEntry(source, destination, relative string, copyRegular func(string, string, os.FileMode) error) error { + info, err := os.Lstat(source) + if err != nil { + return migrationError("config.home_migration_failed", fmt.Sprintf("inspect %s", source), err) + } + if info.Mode()&os.ModeSymlink != 0 { + return unsafeSourceError(source, "symbolic links are not allowed in migrated state") + } + if info.IsDir() { + if err := os.Mkdir(destination, 0o700); err != nil { + return migrationError("config.home_migration_failed", fmt.Sprintf("create migration directory %s", destination), err) + } + entries, err := os.ReadDir(source) + if err != nil { + return migrationError("config.home_migration_failed", fmt.Sprintf("read migration directory %s", source), err) + } + for _, entry := range entries { + childRelative := filepath.Join(relative, entry.Name()) + if err := copyEntry(filepath.Join(source, entry.Name()), filepath.Join(destination, entry.Name()), childRelative, copyRegular); err != nil { + return err + } + } + return nil + } + if !info.Mode().IsRegular() { + return unsafeSourceError(source, "special files are not allowed in migrated state") + } + if secretMigrationPath(relative) && runtime.GOOS != "windows" && info.Mode().Perm()&0o077 != 0 { + return unsafeSourceError(source, "credential file permissions allow group or other access") + } + mode := migratedMode(relative, info.Mode().Perm()) + if err := copyRegular(source, destination, mode); err != nil { + return migrationError("config.home_migration_failed", fmt.Sprintf("copy %s", source), err) + } + return nil +} + +func copyRegularFile(source, destination string, mode os.FileMode) error { + in, err := os.Open(source) + if err != nil { + return err + } + defer in.Close() + out, err := os.OpenFile(destination, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode) + if err != nil { + return err + } + ok := false + defer func() { + _ = out.Close() + if !ok { + _ = os.Remove(destination) + } + }() + if _, err := io.Copy(out, in); err != nil { + return err + } + if err := out.Sync(); err != nil { + return err + } + if err := out.Close(); err != nil { + return err + } + if err := os.Chmod(destination, mode); err != nil && runtime.GOOS != "windows" { + return err + } + ok = true + return nil +} + +func validateStaged(root string) error { + configPath := filepath.Join(root, "config") + if data, exists, err := readOptional(configPath); err != nil { + return migrationError("config.home_migration_failed", fmt.Sprintf("read migrated config %s", configPath), err) + } else if exists { + var value map[string]any + if err := toml.Unmarshal(data, &value); err != nil { + return migrationError("config.home_migration_failed", fmt.Sprintf("parse migrated config %s", configPath), err) + } + } + credentialsPath := filepath.Join(root, "credentials") + if data, exists, err := readOptional(credentialsPath); err != nil { + return migrationError("config.home_migration_failed", fmt.Sprintf("read migrated credentials %s", credentialsPath), err) + } else if exists { + normalized, err := store.NormalizeCredentials(data, credentialsPath) + if err != nil { + return err + } + if err := replaceFile(credentialsPath, normalized, 0o600); err != nil { + return migrationError("config.home_migration_failed", fmt.Sprintf("rewrite migrated credentials %s", credentialsPath), err) + } + } + preferencesPath := filepath.Join(root, ".preferences") + if data, exists, err := readOptional(preferencesPath); err != nil { + return migrationError("config.home_migration_failed", fmt.Sprintf("read migrated preferences %s", preferencesPath), err) + } else if exists { + var value map[string]any + if err := toml.Unmarshal(data, &value); err != nil { + return migrationError("config.home_migration_failed", fmt.Sprintf("parse migrated preferences %s", preferencesPath), err) + } + } + idPath := filepath.Join(root, ".telemetry-installation-id") + if data, exists, err := readOptional(idPath); err != nil { + return migrationError("config.home_migration_failed", fmt.Sprintf("read migrated telemetry identity %s", idPath), err) + } else if exists && !installationIDPattern.MatchString(strings.TrimSpace(string(data))) { + return unsafeSourceError(idPath, "telemetry installation identity is invalid") + } + for _, tree := range []string{"db_users", "fs_resources", "fs_credentials"} { + rootPath := filepath.Join(root, tree) + if err := filepath.WalkDir(rootPath, func(path string, entry os.DirEntry, walkErr error) error { + if errors.Is(walkErr, os.ErrNotExist) { + return filepath.SkipDir + } + if walkErr != nil || entry.IsDir() { + return walkErr + } + if filepath.Base(path) != "config" && filepath.Base(path) != "credentials" { + return nil + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + var value map[string]any + return toml.Unmarshal(data, &value) + }); err != nil && !errors.Is(err, os.ErrNotExist) { + return migrationError("config.home_migration_failed", fmt.Sprintf("validate migrated %s", tree), err) + } + } + return nil +} + +func replaceFile(path string, data []byte, mode os.FileMode) error { + temp, err := os.CreateTemp(filepath.Dir(path), ".credentials.normalize-*") + if err != nil { + return err + } + tempPath := temp.Name() + defer os.Remove(tempPath) + if err := temp.Chmod(mode); err != nil { + temp.Close() + return err + } + if _, err := temp.Write(data); err != nil { + temp.Close() + return err + } + if err := temp.Sync(); err != nil { + temp.Close() + return err + } + if err := temp.Close(); err != nil { + return err + } + if err := os.Rename(tempPath, path); err != nil { + return err + } + return os.Chmod(path, mode) +} + +func activeLegacyMount(legacyRoot string) (string, bool, error) { + mountsDir := filepath.Join(legacyRoot, "mounts") + entries, err := os.ReadDir(mountsDir) + if errors.Is(err, os.ErrNotExist) { + return "", false, nil + } + if err != nil { + return "", false, err + } + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") { + continue + } + data, err := os.ReadFile(filepath.Join(mountsDir, entry.Name())) + if err != nil { + return "", false, err + } + var state struct { + MountPath string `json:"mount_path"` + PID int `json:"pid"` + } + if json.Unmarshal(data, &state) != nil || strings.TrimSpace(state.MountPath) == "" { + continue + } + if (state.PID > 0 && processAlive(state.PID)) || mountPointActive(state.MountPath) || (state.PID == 0 && locatorWithoutPIDBlocksMigration()) { + return state.MountPath, true, nil + } + } + return "", false, nil +} + +func secretMigrationPath(relative string) bool { + relative = filepath.Clean(relative) + if relative == "credentials" || strings.HasPrefix(relative, "db_users"+string(os.PathSeparator)) || strings.HasPrefix(relative, "fs_credentials"+string(os.PathSeparator)) { + return true + } + return strings.HasPrefix(relative, "fs_resources"+string(os.PathSeparator)) && filepath.Base(relative) == "credentials" +} + +func migratedMode(relative string, source os.FileMode) os.FileMode { + if secretMigrationPath(relative) || relative == ".preferences" || relative == ".telemetry-installation-id" { + return 0o600 + } + if relative == "config" || filepath.Base(relative) == "config" { + return 0o644 + } + if source == 0 { + return 0o600 + } + return source & 0o666 +} + +func lstatOptional(path string) (os.FileInfo, bool, error) { + info, err := os.Lstat(path) + if errors.Is(err, os.ErrNotExist) { + return nil, false, nil + } + return info, err == nil, err +} + +func readOptional(path string) ([]byte, bool, error) { + data, err := os.ReadFile(path) + if errors.Is(err, os.ErrNotExist) { + return nil, false, nil + } + return data, err == nil, err +} + +func unsafeSourceError(path, detail string) error { + return apperr.New("config.home_migration_unsafe_source", "config", 2, fmt.Sprintf("unsafe legacy state at %s: %s", path, detail)) +} + +func migrationError(code, action string, err error) error { + if err == nil { + return apperr.New(code, "config", 1, action) + } + return apperr.Wrap(code, "config", 1, action, err) +} + +func shellArgument(value string) string { + return fmt.Sprintf("%q", value) +} diff --git a/internal/config/homemigration/migration_test.go b/internal/config/homemigration/migration_test.go new file mode 100644 index 0000000..b7e08eb --- /dev/null +++ b/internal/config/homemigration/migration_test.go @@ -0,0 +1,222 @@ +package homemigration + +import ( + "errors" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "testing" + + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config/store" +) + +func TestEnsureStateDirectorySelection(t *testing.T) { + t.Run("neither exists", func(t *testing.T) { + home := t.TempDir() + result, err := Ensure(home) + if err != nil || result.Status != "not_needed" { + t.Fatalf("Ensure() = %#v, %v", result, err) + } + if _, err := os.Stat(filepath.Join(home, store.TIDirName)); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("fresh migration unexpectedly created state: %v", err) + } + }) + + t.Run("new state exists", func(t *testing.T) { + home := t.TempDir() + mustMkdir(t, filepath.Join(home, store.TIDirName), 0o700) + result, err := Ensure(home) + if err != nil || result.Status != "not_needed" { + t.Fatalf("Ensure() = %#v, %v", result, err) + } + }) + + t.Run("both states conflict", func(t *testing.T) { + home := t.TempDir() + mustMkdir(t, filepath.Join(home, LegacyDirName), 0o700) + mustMkdir(t, filepath.Join(home, store.TIDirName), 0o700) + _, err := Ensure(home) + if apperr.CodeFor(err) != "config.home_migration_conflict" { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func TestEnsureMigratesDurableStateAndPreservesLegacy(t *testing.T) { + home := t.TempDir() + legacy := filepath.Join(home, LegacyDirName) + mustMkdir(t, legacy, 0o700) + mustWrite(t, filepath.Join(legacy, "config"), "[default]\nregion_code = 'aws-us-east-1'\n", 0o644) + mustWrite(t, filepath.Join(legacy, "credentials"), "[default]\ntdc_public_key = 'public'\ntdc_private_key = 'private'\n", 0o600) + mustWrite(t, filepath.Join(legacy, ".preferences"), "schema_version = 1\n\n[logging]\nenabled = true\n", 0o600) + mustWrite(t, filepath.Join(legacy, ".telemetry-installation-id"), "tdc_0123456789abcdefghijkl\n", 0o600) + mustWrite(t, filepath.Join(legacy, "db_users", "cluster-1", "credentials"), "[read_only]\nusername = 'reader'\npassword = 'secret'\n", 0o600) + mustWrite(t, filepath.Join(legacy, "fs_resources", "profile", "resource", "config"), "file_system_name = 'workspace'\n", 0o644) + mustWrite(t, filepath.Join(legacy, "fs_resources", "profile", "resource", "credentials"), "api_key = 'drive9_secret'\n", 0o600) + mustWrite(t, filepath.Join(legacy, "fs_credentials", "resource", "credentials"), "api_key = 'legacy'\n", 0o600) + for _, excluded := range []string{"logs/ti.jsonl", "cache/blob", "local/blob", "drive9-home/config"} { + mustWrite(t, filepath.Join(legacy, filepath.FromSlash(excluded)), "excluded", 0o600) + } + + result, err := Ensure(home) + if err != nil { + t.Fatal(err) + } + if result.Status != "migrated" || result.Source != legacy { + t.Fatalf("unexpected result: %#v", result) + } + newRoot := filepath.Join(home, store.TIDirName) + credentials, err := os.ReadFile(filepath.Join(newRoot, "credentials")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(credentials), "tidb_cloud_public_key") || strings.Contains(string(credentials), "tdc_public_key") { + t.Fatalf("credentials were not normalized:\n%s", credentials) + } + identity, err := os.ReadFile(filepath.Join(newRoot, ".telemetry-installation-id")) + if err != nil || strings.TrimSpace(string(identity)) != "tdc_0123456789abcdefghijkl" { + t.Fatalf("legacy identity changed: %q, %v", identity, err) + } + for _, copied := range []string{"config", "credentials", ".preferences", ".telemetry-installation-id", "db_users/cluster-1/credentials", "fs_resources/profile/resource/config", "fs_resources/profile/resource/credentials", "fs_credentials/resource/credentials"} { + if _, err := os.Stat(filepath.Join(newRoot, filepath.FromSlash(copied))); err != nil { + t.Fatalf("missing migrated %s: %v", copied, err) + } + } + for _, excluded := range []string{"logs", "cache", "local", "mounts", "drive9-home"} { + if _, err := os.Stat(filepath.Join(newRoot, excluded)); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("excluded path %s was migrated: %v", excluded, err) + } + } + if _, err := os.Stat(filepath.Join(legacy, "credentials")); err != nil { + t.Fatalf("legacy source was modified: %v", err) + } + result, err = Ensure(home) + if err != nil || result.Status != "not_needed" { + t.Fatalf("published migration should remain usable while legacy state is retained: %#v, %v", result, err) + } +} + +func TestEnsureRejectsUntrustedMigrationMarker(t *testing.T) { + home := t.TempDir() + legacy := filepath.Join(home, LegacyDirName) + destination := filepath.Join(home, store.TIDirName) + mustMkdir(t, legacy, 0o700) + mustWrite(t, filepath.Join(destination, migrationMarkerName), `{"schema_version":1,"source":"/different/.tdc"}`, 0o600) + _, err := Ensure(home) + if apperr.CodeFor(err) != "config.home_migration_conflict" { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestEnsureRejectsUnsafeSource(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("POSIX file mode and symlink assertions") + } + t.Run("credential permissions", func(t *testing.T) { + home := t.TempDir() + legacy := filepath.Join(home, LegacyDirName) + mustWrite(t, filepath.Join(legacy, "credentials"), "[default]\ntdc_public_key='public'\ntdc_private_key='private'\n", 0o644) + _, err := Ensure(home) + if apperr.CodeFor(err) != "config.home_migration_unsafe_source" { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("symlink", func(t *testing.T) { + home := t.TempDir() + legacy := filepath.Join(home, LegacyDirName) + mustMkdir(t, legacy, 0o700) + target := filepath.Join(home, "outside") + mustWrite(t, target, "secret", 0o600) + if err := os.Symlink(target, filepath.Join(legacy, "credentials")); err != nil { + t.Fatal(err) + } + _, err := Ensure(home) + if apperr.CodeFor(err) != "config.home_migration_unsafe_source" { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func TestEnsureRejectsConflictingCredentialFields(t *testing.T) { + home := t.TempDir() + legacy := filepath.Join(home, LegacyDirName) + mustWrite(t, filepath.Join(legacy, "credentials"), "[default]\ntidb_cloud_public_key='new'\ntdc_public_key='old'\ntidb_cloud_private_key='same'\ntdc_private_key='same'\n", 0o600) + _, err := Ensure(home) + if apperr.CodeFor(err) != "config.environment_conflict" { + t.Fatalf("unexpected error: %v", err) + } + if _, statErr := os.Stat(filepath.Join(home, store.TIDirName)); !errors.Is(statErr, os.ErrNotExist) { + t.Fatalf("failed migration published destination: %v", statErr) + } +} + +func TestEnsureRejectsActiveLegacyMount(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("process liveness check is Unix-only") + } + home := t.TempDir() + legacy := filepath.Join(home, LegacyDirName) + mountPath := filepath.Join(home, "workspace") + mustMkdir(t, mountPath, 0o700) + mustWrite(t, filepath.Join(legacy, "mounts", "active.json"), `{"mount_path":`+strconv.Quote(mountPath)+`,"pid":`+strconv.Itoa(os.Getpid())+`}`, 0o600) + _, err := Ensure(home) + if apperr.CodeFor(err) != "config.home_migration_active_mount" { + t.Fatalf("unexpected error: %v", err) + } + message := apperr.MessageFor(err) + if !strings.Contains(message, "tdc fs drain-file-system") || !strings.Contains(message, "tdc fs unmount-file-system") { + t.Fatalf("missing old-command instructions: %s", message) + } +} + +func TestEnsureFailureLeavesNoDestination(t *testing.T) { + home := t.TempDir() + legacy := filepath.Join(home, LegacyDirName) + mustWrite(t, filepath.Join(legacy, "config"), "[default]\nregion_code='aws-us-east-1'\n", 0o644) + _, err := ensure(home, options{ + copyRegular: func(string, string, os.FileMode) error { return errors.New("injected copy failure") }, + }) + if apperr.CodeFor(err) != "config.home_migration_failed" { + t.Fatalf("unexpected error: %v", err) + } + assertNoPublishedOrStagedState(t, home) + + _, err = ensure(home, options{beforePublish: func(string) error { return errors.New("injected publish failure") }}) + if apperr.CodeFor(err) != "config.home_migration_failed" { + t.Fatalf("unexpected error: %v", err) + } + assertNoPublishedOrStagedState(t, home) +} + +func assertNoPublishedOrStagedState(t *testing.T, home string) { + t.Helper() + if _, err := os.Stat(filepath.Join(home, store.TIDirName)); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("failed migration published destination: %v", err) + } + matches, err := filepath.Glob(filepath.Join(home, ".ti.migrate-*")) + if err != nil || len(matches) != 0 { + t.Fatalf("migration staging directories remain: %#v, %v", matches, err) + } +} + +func mustWrite(t *testing.T, path, content string, mode os.FileMode) { + t.Helper() + mustMkdir(t, filepath.Dir(path), 0o700) + if err := os.WriteFile(path, []byte(content), mode); err != nil { + t.Fatal(err) + } + if err := os.Chmod(path, mode); err != nil && runtime.GOOS != "windows" { + t.Fatal(err) + } +} + +func mustMkdir(t *testing.T, path string, mode os.FileMode) { + t.Helper() + if err := os.MkdirAll(path, mode); err != nil { + t.Fatal(err) + } +} diff --git a/internal/config/homemigration/mount_unix.go b/internal/config/homemigration/mount_unix.go new file mode 100644 index 0000000..cd62348 --- /dev/null +++ b/internal/config/homemigration/mount_unix.go @@ -0,0 +1,32 @@ +//go:build !windows + +package homemigration + +import ( + "os" + "path/filepath" + "syscall" +) + +func processAlive(pid int) bool { + process, err := os.FindProcess(pid) + return err == nil && process.Signal(syscall.Signal(0)) == nil +} + +func mountPointActive(path string) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + parent, err := os.Stat(filepath.Dir(filepath.Clean(path))) + if err != nil { + return false + } + stat, ok := info.Sys().(*syscall.Stat_t) + parentStat, parentOK := parent.Sys().(*syscall.Stat_t) + return ok && parentOK && stat.Dev != parentStat.Dev +} + +func locatorWithoutPIDBlocksMigration() bool { + return false +} diff --git a/internal/config/homemigration/mount_windows.go b/internal/config/homemigration/mount_windows.go new file mode 100644 index 0000000..028dad1 --- /dev/null +++ b/internal/config/homemigration/mount_windows.go @@ -0,0 +1,31 @@ +//go:build windows + +package homemigration + +import "golang.org/x/sys/windows" + +func processAlive(pid int) bool { + if pid <= 0 { + return false + } + handle, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid)) + if err != nil { + return false + } + defer windows.CloseHandle(handle) + var exitCode uint32 + if err := windows.GetExitCodeProcess(handle, &exitCode); err != nil { + return false + } + return exitCode == 259 // STILL_ACTIVE +} + +func mountPointActive(string) bool { + return false +} + +// Windows mount locators do not record the companion PID. A locator is removed +// by normal unmount, so conservatively require the old CLI to clear it. +func locatorWithoutPIDBlocksMigration() bool { + return true +} diff --git a/internal/config/profile.go b/internal/config/profile.go index 3a0555c..bac9c94 100644 --- a/internal/config/profile.go +++ b/internal/config/profile.go @@ -6,9 +6,10 @@ import ( "os" "strings" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config/region" - "github.com/tidbcloud/tdc/internal/config/store" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config/envcompat" + "github.com/tidbcloud/ti-cli/internal/config/region" + "github.com/tidbcloud/ti-cli/internal/config/store" ) const DefaultProfile = "default" @@ -29,8 +30,8 @@ type Profile struct { CloudProvider string RegionCode string ProjectID string - TDCPublicKey string - TDCPrivateKey string + TiDBCloudPublicKey string + TiDBCloudPrivateKey string FSResourceName string FSTenantID string FSPlacementRegionCode string @@ -77,7 +78,7 @@ func Load(ctx context.Context, opts LoadOptions) (*Profile, error) { return nil, err } - publicKey, privateKey, source, err := resolveTDCCredentials(opts.HomeDir, profileName, creds, hasCreds, opts.Env) + publicKey, privateKey, source, err := resolveTiDBCloudCredentials(opts.HomeDir, profileName, creds, hasCreds, opts.Env) if err != nil { return nil, err } @@ -98,8 +99,8 @@ func Load(ctx context.Context, opts LoadOptions) (*Profile, error) { CloudProvider: placement.Provider, RegionCode: placement.NativeCode, ProjectID: cfg.ProjectID, - TDCPublicKey: publicKey, - TDCPrivateKey: privateKey, + TiDBCloudPublicKey: publicKey, + TiDBCloudPrivateKey: privateKey, FSResourceName: cfg.FSResourceName, FSTenantID: cfg.FSTenantID, FSPlacementRegionCode: fsPlacement.Code, @@ -192,7 +193,11 @@ func ValidateProfileName(name string) error { func resolveOptionalPlacement(cfg store.ConfigProfile, regionOverride string, env map[string]string) (region.Placement, bool, error) { regionCode := strings.TrimSpace(regionOverride) if regionCode == "" { - regionCode = strings.TrimSpace(envValue(env, "TDC_REGION_CODE")) + resolved, _, _, err := envcompat.ResolveNames(env, "TI_REGION_CODE", "TDC_REGION_CODE") + if err != nil { + return region.Placement{}, false, err + } + regionCode = strings.TrimSpace(resolved) } if regionCode == "" { regionCode = strings.TrimSpace(cfg.RegionCode) @@ -210,7 +215,11 @@ func resolveOptionalPlacement(cfg store.ConfigProfile, regionOverride string, en func resolvePlacement(homeDir, profileName string, cfg store.ConfigProfile, hasConfig bool, regionOverride string, env map[string]string) (region.Placement, error) { regionCode := strings.TrimSpace(regionOverride) if regionCode == "" { - regionCode = envValue(env, "TDC_REGION_CODE") + resolved, _, _, err := envcompat.ResolveNames(env, "TI_REGION_CODE", "TDC_REGION_CODE") + if err != nil { + return region.Placement{}, err + } + regionCode = strings.TrimSpace(resolved) } if regionCode == "" { regionCode = cfg.RegionCode @@ -221,7 +230,7 @@ func resolvePlacement(homeDir, profileName string, cfg store.ConfigProfile, hasC "config.profile_not_found", "config", 2, - fmt.Sprintf("profile %q not found in %s; run tdc configure --profile %s or write ~/.tdc/config", profileName, store.ConfigPath(homeDir), profileName), + fmt.Sprintf("profile %q not found in %s; run ti configure --profile %s or write ~/.ti/config", profileName, store.ConfigPath(homeDir), profileName), ) } return region.Placement{}, missingConfig(profileName, store.ConfigPath(homeDir), "region_code") @@ -229,29 +238,37 @@ func resolvePlacement(homeDir, profileName string, cfg store.ConfigProfile, hasC return parsePlacement(regionCode) } -func resolveTDCCredentials(homeDir, profileName string, creds store.CredentialsProfile, hasCreds bool, env map[string]string) (string, string, string, error) { - envPublic := strings.TrimSpace(envValue(env, "TDC_PUBLIC_KEY")) - envPrivate := strings.TrimSpace(envValue(env, "TDC_PRIVATE_KEY")) +func resolveTiDBCloudCredentials(homeDir, profileName string, creds store.CredentialsProfile, hasCreds bool, env map[string]string) (string, string, string, error) { + envPublicValue, _, _, err := envcompat.ResolveNames(env, "TIDB_CLOUD_PUBLIC_KEY", "TDC_PUBLIC_KEY") + if err != nil { + return "", "", "", err + } + envPrivateValue, _, _, err := envcompat.ResolveNames(env, "TIDB_CLOUD_PRIVATE_KEY", "TDC_PRIVATE_KEY") + if err != nil { + return "", "", "", err + } + envPublic := strings.TrimSpace(envPublicValue) + envPrivate := strings.TrimSpace(envPrivateValue) if envPublic != "" || envPrivate != "" { if envPublic == "" { - return "", "", "", envMissing("TDC_PUBLIC_KEY") + return "", "", "", envMissing("TIDB_CLOUD_PUBLIC_KEY") } if envPrivate == "" { - return "", "", "", envMissing("TDC_PRIVATE_KEY") + return "", "", "", envMissing("TIDB_CLOUD_PRIVATE_KEY") } return envPublic, envPrivate, "env", nil } if !hasCreds { - return "", "", "", missingCredential(profileName, store.CredentialsPath(homeDir), "tdc_public_key") + return "", "", "", missingCredential(profileName, store.CredentialsPath(homeDir), "tidb_cloud_public_key") } - if creds.TDCPublicKey == "" { - return "", "", "", missingCredential(profileName, store.CredentialsPath(homeDir), "tdc_public_key") + if creds.TiDBCloudPublicKey == "" { + return "", "", "", missingCredential(profileName, store.CredentialsPath(homeDir), "tidb_cloud_public_key") } - if creds.TDCPrivateKey == "" { - return "", "", "", missingCredential(profileName, store.CredentialsPath(homeDir), "tdc_private_key") + if creds.TiDBCloudPrivateKey == "" { + return "", "", "", missingCredential(profileName, store.CredentialsPath(homeDir), "tidb_cloud_private_key") } - return creds.TDCPublicKey, creds.TDCPrivateKey, "profile", nil + return creds.TiDBCloudPublicKey, creds.TiDBCloudPrivateKey, "profile", nil } func envMissing(key string) error { @@ -259,7 +276,7 @@ func envMissing(key string) error { "config.env_missing", "config", 2, - fmt.Sprintf("%s is required when using TDC_* environment credentials", key), + fmt.Sprintf("%s is required when using TiDB Cloud environment credentials", key), ) } @@ -276,7 +293,7 @@ func missingConfig(profileName, path, key string) error { "config.missing_config", "config", 2, - fmt.Sprintf("%s missing for profile %q in %s; run tdc configure --profile %s or write ~/.tdc/config", key, profileName, path, profileName), + fmt.Sprintf("%s missing for profile %q in %s; run ti configure --profile %s or write ~/.ti/config", key, profileName, path, profileName), ) } @@ -285,13 +302,6 @@ func missingCredential(profileName, path, key string) error { "config.missing_credentials", "config", 2, - fmt.Sprintf("%s missing for profile %q in %s; run tdc configure --profile %s or write ~/.tdc/credentials", key, profileName, path, profileName), + fmt.Sprintf("%s missing for profile %q in %s; run ti configure --profile %s or write ~/.ti/credentials", key, profileName, path, profileName), ) } - -func envValue(env map[string]string, key string) string { - if env != nil { - return env[key] - } - return os.Getenv(key) -} diff --git a/internal/config/profile_test.go b/internal/config/profile_test.go index 9f2147f..c3b2d52 100644 --- a/internal/config/profile_test.go +++ b/internal/config/profile_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config/store" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config/store" ) func TestLoadExplicitProfile(t *testing.T) { @@ -33,7 +33,7 @@ func TestLoadExplicitProfile(t *testing.T) { func TestLoadReadsProjectID(t *testing.T) { home := t.TempDir() - if err := store.WriteProfile(home, "stage", store.ConfigProfile{RegionCode: "aws-us-east-1", ProjectID: "virtual-1"}, store.CredentialsProfile{TDCPublicKey: "public", TDCPrivateKey: "private"}); err != nil { + if err := store.WriteProfile(home, "stage", store.ConfigProfile{RegionCode: "aws-us-east-1", ProjectID: "virtual-1"}, store.CredentialsProfile{TiDBCloudPublicKey: "public", TiDBCloudPrivateKey: "private"}); err != nil { t.Fatal(err) } profile, err := Load(context.Background(), LoadOptions{Profile: "stage", ProfileExplicit: true, HomeDir: home}) @@ -61,7 +61,7 @@ func TestLoadRegionOverrideWinsOverExplicitProfile(t *testing.T) { if profile.Name != "stage" || profile.CloudProvider != "aws" || profile.RegionCode != "ap-southeast-1" || profile.PlacementRegionCode != "aws-ap-southeast-1" { t.Fatalf("region override did not win over profile: %#v", profile) } - if profile.TDCPublicKey != "stage-public" || profile.TDCPrivateKey != "stage-private" { + if profile.TiDBCloudPublicKey != "stage-public" || profile.TiDBCloudPrivateKey != "stage-private" { t.Fatalf("region override changed credentials: %#v", profile) } } @@ -70,9 +70,9 @@ func TestLoadEnvironmentFallback(t *testing.T) { profile, err := Load(context.Background(), LoadOptions{ HomeDir: t.TempDir(), Env: map[string]string{ - "TDC_REGION_CODE": "aws-us-east-1", - "TDC_PUBLIC_KEY": "env-public", - "TDC_PRIVATE_KEY": "env-private", + "TI_REGION_CODE": "aws-us-east-1", + "TIDB_CLOUD_PUBLIC_KEY": "env-public", + "TIDB_CLOUD_PRIVATE_KEY": "env-private", }, }) if err != nil { @@ -81,7 +81,7 @@ func TestLoadEnvironmentFallback(t *testing.T) { if profile.Source != "env" { t.Fatalf("expected env source, got %#v", profile) } - if profile.TDCPrivateKey != "env-private" { + if profile.TiDBCloudPrivateKey != "env-private" { t.Fatalf("env private key not loaded") } } @@ -91,8 +91,8 @@ func TestLoadEnvironmentFallbackUsesRegionOverride(t *testing.T) { HomeDir: t.TempDir(), RegionOverride: "ali-ap-southeast-1", Env: map[string]string{ - "TDC_PUBLIC_KEY": "env-public", - "TDC_PRIVATE_KEY": "env-private", + "TIDB_CLOUD_PUBLIC_KEY": "env-public", + "TIDB_CLOUD_PRIVATE_KEY": "env-private", }, }) if err != nil { @@ -112,9 +112,9 @@ func TestLoadEnvironmentCredentialsUseDefaultProfileNamespace(t *testing.T) { FSCloudProvider: "aws", FSRegionCode: "aws-us-east-1", }, store.CredentialsProfile{ - TDCPublicKey: "local-public", - TDCPrivateKey: "local-private", - FSAPIKey: "fs-secret", + TiDBCloudPublicKey: "local-public", + TiDBCloudPrivateKey: "local-private", + FSAPIKey: "fs-secret", }); err != nil { t.Fatal(err) } @@ -122,9 +122,9 @@ func TestLoadEnvironmentCredentialsUseDefaultProfileNamespace(t *testing.T) { profile, err := Load(context.Background(), LoadOptions{ HomeDir: home, Env: map[string]string{ - "TDC_REGION_CODE": "aws-us-east-1", - "TDC_PUBLIC_KEY": "env-public", - "TDC_PRIVATE_KEY": "env-private", + "TI_REGION_CODE": "aws-us-east-1", + "TIDB_CLOUD_PUBLIC_KEY": "env-public", + "TIDB_CLOUD_PRIVATE_KEY": "env-private", }, }) if err != nil { @@ -133,7 +133,7 @@ func TestLoadEnvironmentCredentialsUseDefaultProfileNamespace(t *testing.T) { if profile.Source != "env" || profile.Name != DefaultProfile { t.Fatalf("expected env credential source with default profile namespace, got %#v", profile) } - if profile.TDCPublicKey != "env-public" || profile.TDCPrivateKey != "env-private" { + if profile.TiDBCloudPublicKey != "env-public" || profile.TiDBCloudPrivateKey != "env-private" { t.Fatalf("expected credentials from environment, got %#v", profile) } if profile.FSResourceName != "dropme-fs" || profile.FSTenantID != "tenant-1" || profile.FSAPIKey != "fs-secret" { @@ -153,9 +153,9 @@ func TestLoadEnvironmentCredentialsUseExplicitProfileNamespace(t *testing.T) { FSCloudProvider: "aws", FSRegionCode: "aws-us-west-2", }, store.CredentialsProfile{ - TDCPublicKey: "stage-local-public", - TDCPrivateKey: "stage-local-private", - FSAPIKey: "stage-fs-secret", + TiDBCloudPublicKey: "stage-local-public", + TiDBCloudPrivateKey: "stage-local-private", + FSAPIKey: "stage-fs-secret", }); err != nil { t.Fatal(err) } @@ -165,8 +165,8 @@ func TestLoadEnvironmentCredentialsUseExplicitProfileNamespace(t *testing.T) { ProfileExplicit: true, HomeDir: home, Env: map[string]string{ - "TDC_PUBLIC_KEY": "env-public", - "TDC_PRIVATE_KEY": "env-private", + "TIDB_CLOUD_PUBLIC_KEY": "env-public", + "TIDB_CLOUD_PRIVATE_KEY": "env-private", }, }) if err != nil { @@ -175,7 +175,7 @@ func TestLoadEnvironmentCredentialsUseExplicitProfileNamespace(t *testing.T) { if profile.Source != "env" || profile.Name != "stage" { t.Fatalf("expected env credential source with explicit profile namespace, got %#v", profile) } - if profile.TDCPublicKey != "env-public" || profile.TDCPrivateKey != "env-private" { + if profile.TiDBCloudPublicKey != "env-public" || profile.TiDBCloudPrivateKey != "env-private" { t.Fatalf("expected credentials from environment, got %#v", profile) } if profile.FSResourceName != "stage-fs" || profile.FSTenantID != "tenant-stage" || profile.FSAPIKey != "stage-fs-secret" { @@ -225,14 +225,14 @@ func TestLoadMissingCredentialsSuggestsConfigure(t *testing.T) { t.Fatal("expected missing credentials to fail") } message := apperr.MessageFor(err) - if !strings.Contains(message, "tdc configure") || !strings.Contains(message, "~/.tdc/credentials") { + if !strings.Contains(message, "ti configure") || !strings.Contains(message, "~/.ti/credentials") { t.Fatalf("missing credentials error did not include guidance: %q", message) } } func TestLoadRejectsServerURLKeys(t *testing.T) { home := t.TempDir() - if err := os.MkdirAll(filepath.Join(home, store.TDCDirName), 0o700); err != nil { + if err := os.MkdirAll(filepath.Join(home, store.TIDirName), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(store.ConfigPath(home), []byte(` @@ -244,8 +244,8 @@ api_endpoint = "https://example.invalid" } if err := os.WriteFile(store.CredentialsPath(home), []byte(` [default] -tdc_public_key = "public" -tdc_private_key = "private" +tidb_cloud_public_key = "public" +tidb_cloud_private_key = "private" `), 0o600); err != nil { t.Fatal(err) } @@ -268,9 +268,9 @@ func TestLoadReadsFSCredentials(t *testing.T) { FSCloudProvider: "aws", FSRegionCode: "aws-us-east-1", }, store.CredentialsProfile{ - TDCPublicKey: "public", - TDCPrivateKey: "private", - FSAPIKey: "fs-secret", + TiDBCloudPublicKey: "public", + TiDBCloudPrivateKey: "private", + FSAPIKey: "fs-secret", }) if err != nil { t.Fatal(err) @@ -290,8 +290,8 @@ func TestLoadLocalDoesNotRequireProfileOrTiDBCloudCredentials(t *testing.T) { profile, err := LoadLocal(context.Background(), LoadOptions{ HomeDir: home, Env: map[string]string{ - "TDC_REGION_CODE": "aws-us-east-1", - "TDC_PUBLIC_KEY": "ignored-without-private-key", + "TI_REGION_CODE": "aws-us-east-1", + "TIDB_CLOUD_PUBLIC_KEY": "ignored-without-private-key", }, }) if err != nil { @@ -300,10 +300,10 @@ func TestLoadLocalDoesNotRequireProfileOrTiDBCloudCredentials(t *testing.T) { if profile.Name != DefaultProfile || profile.HomeDir != home || profile.PlacementRegionCode != "aws-us-east-1" { t.Fatalf("unexpected local profile: %#v", profile) } - if profile.TDCPublicKey != "" || profile.TDCPrivateKey != "" { + if profile.TiDBCloudPublicKey != "" || profile.TiDBCloudPrivateKey != "" { t.Fatalf("local loader must not load TiDB Cloud credentials: %#v", profile) } - if _, err := os.Stat(filepath.Join(home, store.TDCDirName)); !os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(home, store.TIDirName)); !os.IsNotExist(err) { t.Fatalf("LoadLocal wrote local state: %v", err) } } @@ -321,7 +321,7 @@ func TestLoadLocalAllowsMissingPlacement(t *testing.T) { func TestLoadLocalRejectsMalformedEnvironmentPlacement(t *testing.T) { _, err := LoadLocal(context.Background(), LoadOptions{ HomeDir: t.TempDir(), - Env: map[string]string{"TDC_REGION_CODE": "aws-not-a-region"}, + Env: map[string]string{"TI_REGION_CODE": "aws-not-a-region"}, }) if apperr.CodeFor(err) != "config.invalid_region" { t.Fatalf("error = %v, want config.invalid_region", err) @@ -333,8 +333,8 @@ func writeProfile(t *testing.T, home, name, regionCode, publicKey, privateKey st err := store.WriteProfile(home, name, store.ConfigProfile{ RegionCode: regionCode, }, store.CredentialsProfile{ - TDCPublicKey: publicKey, - TDCPrivateKey: privateKey, + TiDBCloudPublicKey: publicKey, + TiDBCloudPrivateKey: privateKey, }) if err != nil { t.Fatal(err) diff --git a/internal/config/store/store.go b/internal/config/store/store.go index d23ecd0..8334010 100644 --- a/internal/config/store/store.go +++ b/internal/config/store/store.go @@ -8,15 +8,16 @@ import ( "strings" "github.com/pelletier/go-toml/v2" + "github.com/tidbcloud/ti-cli/internal/apperr" ) const ( - TDCDirName = ".tdc" + TIDirName = ".ti" ConfigFileName = "config" CredsFileName = "credentials" configFileMode = 0o644 credsFileMode = 0o600 - tdcDirFileMode = 0o700 + tiDirFileMode = 0o700 ) type ConfigDocument map[string]ConfigProfile @@ -34,17 +35,25 @@ type ConfigProfile struct { type CredentialsDocument map[string]CredentialsProfile type CredentialsProfile struct { - TDCPublicKey string `toml:"tdc_public_key,omitempty"` - TDCPrivateKey string `toml:"tdc_private_key,omitempty"` - FSAPIKey string `toml:"fs_api_key,omitempty"` + TiDBCloudPublicKey string `toml:"tidb_cloud_public_key,omitempty"` + TiDBCloudPrivateKey string `toml:"tidb_cloud_private_key,omitempty"` + FSAPIKey string `toml:"fs_api_key,omitempty"` +} + +type credentialsProfileWire struct { + TiDBCloudPublicKey string `toml:"tidb_cloud_public_key,omitempty"` + TiDBCloudPrivateKey string `toml:"tidb_cloud_private_key,omitempty"` + LegacyTDCPublicKey string `toml:"tdc_public_key,omitempty"` + LegacyTDCPrivateKey string `toml:"tdc_private_key,omitempty"` + FSAPIKey string `toml:"fs_api_key,omitempty"` } func ConfigPath(homeDir string) string { - return filepath.Join(homeDir, TDCDirName, ConfigFileName) + return filepath.Join(homeDir, TIDirName, ConfigFileName) } func CredentialsPath(homeDir string) string { - return filepath.Join(homeDir, TDCDirName, CredsFileName) + return filepath.Join(homeDir, TIDirName, CredsFileName) } func ReadConfig(homeDir string) (ConfigDocument, error) { @@ -89,16 +98,82 @@ func ReadCredentials(homeDir string) (CredentialsDocument, error) { return nil, err } - var doc CredentialsDocument - if err := toml.Unmarshal(data, &doc); err != nil { + return DecodeCredentials(data, path) +} + +func DecodeCredentials(data []byte, path string) (CredentialsDocument, error) { + var wire map[string]credentialsProfileWire + if err := toml.Unmarshal(data, &wire); err != nil { return nil, fmt.Errorf("parse credentials %s: %w", path, err) } - if doc == nil { - doc = CredentialsDocument{} + doc := make(CredentialsDocument, len(wire)) + for profileName, profile := range wire { + publicKey, err := resolveCredentialField(path, profileName, "tidb_cloud_public_key", profile.TiDBCloudPublicKey, "tdc_public_key", profile.LegacyTDCPublicKey) + if err != nil { + return nil, err + } + privateKey, err := resolveCredentialField(path, profileName, "tidb_cloud_private_key", profile.TiDBCloudPrivateKey, "tdc_private_key", profile.LegacyTDCPrivateKey) + if err != nil { + return nil, err + } + doc[profileName] = CredentialsProfile{ + TiDBCloudPublicKey: publicKey, + TiDBCloudPrivateKey: privateKey, + FSAPIKey: profile.FSAPIKey, + } } return doc, nil } +func NormalizeCredentials(data []byte, path string) ([]byte, error) { + doc, err := DecodeCredentials(data, path) + if err != nil { + return nil, err + } + if err := rejectDisallowedKeys(data, path); err != nil { + return nil, err + } + if err := rejectDisallowedCredentialKeys(data, path); err != nil { + return nil, err + } + var raw map[string]map[string]any + if err := toml.Unmarshal(data, &raw); err != nil { + return nil, fmt.Errorf("parse credentials %s: %w", path, err) + } + for profileName, profile := range raw { + resolved := doc[profileName] + if resolved.TiDBCloudPublicKey != "" { + profile["tidb_cloud_public_key"] = resolved.TiDBCloudPublicKey + } + if resolved.TiDBCloudPrivateKey != "" { + profile["tidb_cloud_private_key"] = resolved.TiDBCloudPrivateKey + } + delete(profile, "tdc_public_key") + delete(profile, "tdc_private_key") + raw[profileName] = profile + } + normalized, err := toml.Marshal(raw) + if err != nil { + return nil, fmt.Errorf("marshal credentials %s: %w", path, err) + } + return normalized, nil +} + +func resolveCredentialField(path, profileName, canonicalName, canonicalValue, legacyName, legacyValue string) (string, error) { + if canonicalValue != "" && legacyValue != "" && canonicalValue != legacyValue { + return "", apperr.New( + "config.environment_conflict", + "config", + 2, + fmt.Sprintf("credential fields %s and %s contain different values for profile %q in %s; remove one or make them equal", canonicalName, legacyName, profileName, path), + ) + } + if canonicalValue != "" { + return canonicalValue, nil + } + return legacyValue, nil +} + func WriteProfile(homeDir, profileName string, cfg ConfigProfile, creds CredentialsProfile) error { if profileName == "" { profileName = "default" @@ -144,11 +219,11 @@ func WriteProfile(homeDir, profileName string, cfg ConfigProfile, creds Credenti return err } existingCreds := credentialsDoc[profileName] - if creds.TDCPublicKey != "" { - existingCreds.TDCPublicKey = creds.TDCPublicKey + if creds.TiDBCloudPublicKey != "" { + existingCreds.TiDBCloudPublicKey = creds.TiDBCloudPublicKey } - if creds.TDCPrivateKey != "" { - existingCreds.TDCPrivateKey = creds.TDCPrivateKey + if creds.TiDBCloudPrivateKey != "" { + existingCreds.TiDBCloudPrivateKey = creds.TiDBCloudPrivateKey } if creds.FSAPIKey != "" { existingCreds.FSAPIKey = creds.FSAPIKey @@ -252,7 +327,7 @@ func ensureDir(homeDir string) error { if homeDir == "" { return errors.New("home directory is required") } - return os.MkdirAll(filepath.Join(homeDir, TDCDirName), tdcDirFileMode) + return os.MkdirAll(filepath.Join(homeDir, TIDirName), tiDirFileMode) } func writeTOML(path string, value any, mode os.FileMode) error { @@ -262,7 +337,7 @@ func writeTOML(path string, value any, mode os.FileMode) error { } dir := filepath.Dir(path) - if err := os.MkdirAll(dir, tdcDirFileMode); err != nil { + if err := os.MkdirAll(dir, tiDirFileMode); err != nil { return err } @@ -345,7 +420,7 @@ func rejectDisallowedCredentialKeys(data []byte, path string) error { next = prefix + "." + key } if strings.EqualFold(key, "db_users") { - return fmt.Errorf("unsupported DB user credentials key %q in %s; store DB SQL users under ~/.tdc/db_users//credentials", next, path) + return fmt.Errorf("unsupported DB user credentials key %q in %s; store DB SQL users under ~/.ti/db_users//credentials", next, path) } if err := walk(next, nested); err != nil { return err diff --git a/internal/config/store/store_test.go b/internal/config/store/store_test.go index 6c64377..94c7396 100644 --- a/internal/config/store/store_test.go +++ b/internal/config/store/store_test.go @@ -6,6 +6,8 @@ import ( "runtime" "strings" "testing" + + "github.com/tidbcloud/ti-cli/internal/apperr" ) func TestWriteProfileCreatesFilesAndRestrictsCredentials(t *testing.T) { @@ -19,9 +21,9 @@ func TestWriteProfileCreatesFilesAndRestrictsCredentials(t *testing.T) { FSCloudProvider: "aws", FSRegionCode: "aws-us-east-1", }, CredentialsProfile{ - TDCPublicKey: "public", - TDCPrivateKey: "private", - FSAPIKey: "fs-secret", + TiDBCloudPublicKey: "public", + TiDBCloudPrivateKey: "private", + FSAPIKey: "fs-secret", }) if err != nil { t.Fatalf("WriteProfile failed: %v", err) @@ -60,8 +62,8 @@ func TestWriteProfileCreatesFilesAndRestrictsCredentials(t *testing.T) { func TestReadConfigRejectsURLLikeKeys(t *testing.T) { home := t.TempDir() - tdcDir := filepath.Join(home, TDCDirName) - if err := os.MkdirAll(tdcDir, 0o700); err != nil { + tiDir := filepath.Join(home, TIDirName) + if err := os.MkdirAll(tiDir, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(ConfigPath(home), []byte(` @@ -83,7 +85,7 @@ server_url = "https://example.invalid" func TestWriteProfileRejectsReservedLoggingName(t *testing.T) { home := t.TempDir() - err := WriteProfile(home, "LoGgInG", ConfigProfile{RegionCode: "aws-us-east-1"}, CredentialsProfile{TDCPublicKey: "public", TDCPrivateKey: "private"}) + err := WriteProfile(home, "LoGgInG", ConfigProfile{RegionCode: "aws-us-east-1"}, CredentialsProfile{TiDBCloudPublicKey: "public", TiDBCloudPrivateKey: "private"}) if err == nil || !strings.Contains(err.Error(), "reserved") { t.Fatalf("expected reserved profile error, got %v", err) } @@ -97,8 +99,8 @@ func TestWriteProfileRejectsReservedLoggingName(t *testing.T) { func TestRemoveLegacyFSDefaultFileSystemPreservesConfig(t *testing.T) { home := t.TempDir() - tdcDir := filepath.Join(home, TDCDirName) - if err := os.MkdirAll(tdcDir, 0o700); err != nil { + tiDir := filepath.Join(home, TIDirName) + if err := os.MkdirAll(tiDir, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(ConfigPath(home), []byte(` @@ -156,9 +158,9 @@ func TestClearFSResourcePreservesTiDBCloudCredentials(t *testing.T) { FSCloudProvider: "aws", FSRegionCode: "aws-us-east-1", }, CredentialsProfile{ - TDCPublicKey: "public", - TDCPrivateKey: "private", - FSAPIKey: "fs-secret", + TiDBCloudPublicKey: "public", + TiDBCloudPrivateKey: "private", + FSAPIKey: "fs-secret", }); err != nil { t.Fatalf("WriteProfile failed: %v", err) } @@ -179,7 +181,7 @@ func TestClearFSResourcePreservesTiDBCloudCredentials(t *testing.T) { if err != nil { t.Fatalf("ReadCredentials failed: %v", err) } - if got := credentialsDoc["stage"]; got.FSAPIKey != "" || got.TDCPublicKey != "public" || got.TDCPrivateKey != "private" { + if got := credentialsDoc["stage"]; got.FSAPIKey != "" || got.TiDBCloudPublicKey != "public" || got.TiDBCloudPrivateKey != "private" { t.Fatalf("unexpected credentials after clear: %#v", got) } } @@ -190,13 +192,13 @@ func TestReadCredentialsRepairsPermissions(t *testing.T) { } home := t.TempDir() - if err := os.MkdirAll(filepath.Join(home, TDCDirName), 0o700); err != nil { + if err := os.MkdirAll(filepath.Join(home, TIDirName), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(CredentialsPath(home), []byte(` [default] -tdc_public_key = "public" -tdc_private_key = "private" +tidb_cloud_public_key = "public" +tidb_cloud_private_key = "private" `), 0o644); err != nil { t.Fatal(err) } @@ -215,13 +217,13 @@ tdc_private_key = "private" func TestReadCredentialsRejectsDBUsersInMainCredentials(t *testing.T) { home := t.TempDir() - if err := os.MkdirAll(filepath.Join(home, TDCDirName), 0o700); err != nil { + if err := os.MkdirAll(filepath.Join(home, TIDirName), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(CredentialsPath(home), []byte(` [default] -tdc_public_key = "public" -tdc_private_key = "private" +tidb_cloud_public_key = "public" +tidb_cloud_private_key = "private" [default.db_users."cluster-id".read_write] username = "user" @@ -234,7 +236,68 @@ password = "pass" if err == nil { t.Fatal("expected db_users in main credentials to be rejected") } - if !strings.Contains(err.Error(), "~/.tdc/db_users//credentials") { + if !strings.Contains(err.Error(), "~/.ti/db_users//credentials") { t.Fatalf("expected error to mention db user credential path, got %v", err) } } + +func TestReadCredentialsSupportsLegacyFieldsAndWritesCanonicalFields(t *testing.T) { + home := t.TempDir() + path := CredentialsPath(home) + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte("[default]\ntdc_public_key='public'\ntdc_private_key='private'\n"), 0o600); err != nil { + t.Fatal(err) + } + doc, err := ReadCredentials(home) + if err != nil { + t.Fatal(err) + } + if doc["default"].TiDBCloudPublicKey != "public" || doc["default"].TiDBCloudPrivateKey != "private" { + t.Fatalf("legacy credentials were not decoded: %#v", doc) + } + if err := WriteProfile(home, "default", ConfigProfile{RegionCode: "aws-us-east-1"}, CredentialsProfile{}); err != nil { + t.Fatal(err) + } + written, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(written), "tidb_cloud_public_key") || strings.Contains(string(written), "tdc_public_key") { + t.Fatalf("write did not canonicalize credentials:\n%s", written) + } +} + +func TestReadCredentialsRejectsConflictingLegacyFields(t *testing.T) { + home := t.TempDir() + path := CredentialsPath(home) + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + t.Fatal(err) + } + contents := "[default]\ntidb_cloud_public_key='new'\ntdc_public_key='old'\ntidb_cloud_private_key='same'\ntdc_private_key='same'\n" + if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { + t.Fatal(err) + } + _, err := ReadCredentials(home) + if apperr.CodeFor(err) != "config.environment_conflict" { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestNormalizeCredentialsPreservesUnrelatedFields(t *testing.T) { + contents := []byte("[default]\ntdc_public_key='public'\ntdc_private_key='private'\nfs_api_key='drive9-token'\nfuture_secret='preserve-me'\n") + normalized, err := NormalizeCredentials(contents, "credentials") + if err != nil { + t.Fatal(err) + } + text := string(normalized) + for _, want := range []string{"tidb_cloud_public_key", "tidb_cloud_private_key", "fs_api_key", "future_secret", "preserve-me"} { + if !strings.Contains(text, want) { + t.Fatalf("normalized credentials lost %q:\n%s", want, text) + } + } + if strings.Contains(text, "tdc_public_key") || strings.Contains(text, "tdc_private_key") { + t.Fatalf("normalized credentials retained legacy names:\n%s", text) + } +} diff --git a/internal/db/branch.go b/internal/db/branch.go index 6d9c6e4..74270c9 100644 --- a/internal/db/branch.go +++ b/internal/db/branch.go @@ -9,13 +9,13 @@ import ( "text/tabwriter" "time" - "github.com/tidbcloud/tdc/internal/api/endpoints" - apistarter "github.com/tidbcloud/tdc/internal/api/starter" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/db/validate" - "github.com/tidbcloud/tdc/internal/dryrun" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + apistarter "github.com/tidbcloud/ti-cli/internal/api/starter" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/db/validate" + "github.com/tidbcloud/ti-cli/internal/dryrun" ) type ListBranchesOptions struct { @@ -214,7 +214,7 @@ func (s Service) waitUntilBranchActive(ctx context.Context, client *apistarter.C "db.branch_wait_read_failed", "api", 1, - fmt.Sprintf("DB branch %q was created in cluster %q but tdc could not read its state while waiting for ACTIVE; the branch was not deleted", branch.ID, clusterID), + fmt.Sprintf("DB branch %q was created in cluster %q but ti could not read its state while waiting for ACTIVE; the branch was not deleted", branch.ID, clusterID), err, ) } diff --git a/internal/db/branch_test.go b/internal/db/branch_test.go index ca505a4..2574789 100644 --- a/internal/db/branch_test.go +++ b/internal/db/branch_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/tidbcloud/tdc/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/apperr" ) func TestCreateBranch(t *testing.T) { @@ -314,7 +314,7 @@ func TestDryRunCreateBranchDoesNotSendRequest(t *testing.T) { })) defer server.Close() - result, err := testService(server.URL).DryRunCreateBranch(context.Background(), "tdc db create-db-cluster-branch", CreateBranchOptions{ + result, err := testService(server.URL).DryRunCreateBranch(context.Background(), "ti db create-db-cluster-branch", CreateBranchOptions{ Profile: testProfile(), ClusterID: "cluster-1", DisplayName: "dev", @@ -351,7 +351,7 @@ func TestDryRunDeleteBranchDoesNotSendRequest(t *testing.T) { })) defer server.Close() - result, err := testService(server.URL).DryRunDeleteBranch(context.Background(), "tdc db delete-db-cluster-branch", DeleteBranchOptions{ + result, err := testService(server.URL).DryRunDeleteBranch(context.Background(), "ti db delete-db-cluster-branch", DeleteBranchOptions{ Profile: testProfile(), ClusterID: "cluster-1", BranchID: "branch-1", @@ -374,7 +374,7 @@ func TestDryRunDeleteBranchDoesNotSendRequest(t *testing.T) { } func TestCreateBranchRequiresName(t *testing.T) { - _, err := Service{}.DryRunCreateBranch(context.Background(), "tdc db create-db-cluster-branch", CreateBranchOptions{ + _, err := Service{}.DryRunCreateBranch(context.Background(), "ti db create-db-cluster-branch", CreateBranchOptions{ Profile: testProfile(), ClusterID: "cluster-1", }) diff --git a/internal/db/cluster.go b/internal/db/cluster.go index 05570e6..6d51db5 100644 --- a/internal/db/cluster.go +++ b/internal/db/cluster.go @@ -10,14 +10,14 @@ import ( "text/tabwriter" "time" - "github.com/tidbcloud/tdc/internal/api" - "github.com/tidbcloud/tdc/internal/api/endpoints" - apistarter "github.com/tidbcloud/tdc/internal/api/starter" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/db/validate" - "github.com/tidbcloud/tdc/internal/dryrun" + "github.com/tidbcloud/ti-cli/internal/api" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + apistarter "github.com/tidbcloud/ti-cli/internal/api/starter" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/db/validate" + "github.com/tidbcloud/ti-cli/internal/dryrun" ) const ( @@ -291,7 +291,7 @@ func (s Service) waitUntilClusterActive(ctx context.Context, client *apistarter. "db.cluster_wait_read_failed", "api", 1, - fmt.Sprintf("DB cluster %q was created but tdc could not read its state while waiting for ACTIVE; the cluster was not deleted; inspect it with `tdc db describe-db-cluster --db-cluster-id %s`", cluster.ID, cluster.ID), + fmt.Sprintf("DB cluster %q was created but ti could not read its state while waiting for ACTIVE; the cluster was not deleted; inspect it with `ti db describe-db-cluster --db-cluster-id %s`", cluster.ID, cluster.ID), err, ) } @@ -306,7 +306,7 @@ func (s Service) waitUntilClusterActive(ctx context.Context, client *apistarter. "db.cluster_wait_terminal_state", "api", 1, - fmt.Sprintf("DB cluster %q was created but entered state %q before becoming ACTIVE; the cluster was not deleted; inspect it with `tdc db describe-db-cluster --db-cluster-id %s`", cluster.ID, current.State, cluster.ID), + fmt.Sprintf("DB cluster %q was created but entered state %q before becoming ACTIVE; the cluster was not deleted; inspect it with `ti db describe-db-cluster --db-cluster-id %s`", cluster.ID, current.State, cluster.ID), ) } @@ -333,7 +333,7 @@ func clusterWaitContextError(parent, waitCtx context.Context, clusterID string, "db.cluster_wait_timeout", "api", 1, - fmt.Sprintf("DB cluster %q was created but did not become ACTIVE within %s; the cluster was not deleted; inspect it with `tdc db describe-db-cluster --db-cluster-id %s`", clusterID, timeout, clusterID), + fmt.Sprintf("DB cluster %q was created but did not become ACTIVE within %s; the cluster was not deleted; inspect it with `ti db describe-db-cluster --db-cluster-id %s`", clusterID, timeout, clusterID), ) } return nil @@ -448,7 +448,7 @@ func (s Service) waitUntilClusterDeleted(ctx context.Context, client *apistarter "db.cluster_delete_wait_read_failed", "api", 1, - fmt.Sprintf("DB cluster %q deletion was accepted but tdc could not confirm completion; deletion may still be in progress", cluster.ID), + fmt.Sprintf("DB cluster %q deletion was accepted but ti could not confirm completion; deletion may still be in progress", cluster.ID), err, ) } @@ -622,7 +622,7 @@ func (s Service) starterClient(profile *config.Profile, permission authz.Permiss Timeout: s.Timeout, Debug: s.Debug, DebugWriter: s.DebugWriter, - UserAgent: "tdc db cluster", + UserAgent: "ti db cluster", }) if err != nil { return nil, err diff --git a/internal/db/cluster_plan.go b/internal/db/cluster_plan.go index f63bc95..b03fca5 100644 --- a/internal/db/cluster_plan.go +++ b/internal/db/cluster_plan.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - apistarter "github.com/tidbcloud/tdc/internal/api/starter" - "github.com/tidbcloud/tdc/internal/apperr" + apistarter "github.com/tidbcloud/ti-cli/internal/api/starter" + "github.com/tidbcloud/ti-cli/internal/apperr" ) const starterPlan = "starter" @@ -22,7 +22,7 @@ func ensureStarterCluster(cluster apistarter.Cluster) error { "db.not_starter_cluster", "usage", 2, - fmt.Sprintf("cluster %q uses service plan %q; tdc db only manages Starter clusters", cluster.ID, clusterPlanDisplay(cluster)), + fmt.Sprintf("cluster %q uses service plan %q; ti db only manages Starter clusters", cluster.ID, clusterPlanDisplay(cluster)), ) } @@ -78,7 +78,7 @@ func createdClusterPlanError(cluster apistarter.Cluster, cause error) error { apperr.CodeFor(cause), apperr.CategoryFor(cause), apperr.ExitCodeFor(cause), - fmt.Sprintf("DB cluster %q creation was accepted in state %q but tdc could not verify it as a Starter cluster; the cluster was retained; inspect it with `tdc db describe-db-cluster --db-cluster-id %s`", cluster.ID, clusterStateDisplay(cluster), cluster.ID), + fmt.Sprintf("DB cluster %q creation was accepted in state %q but ti could not verify it as a Starter cluster; the cluster was retained; inspect it with `ti db describe-db-cluster --db-cluster-id %s`", cluster.ID, clusterStateDisplay(cluster), cluster.ID), cause, ) } @@ -88,7 +88,7 @@ func updatedClusterPlanError(cluster apistarter.Cluster, cause error) error { apperr.CodeFor(cause), apperr.CategoryFor(cause), apperr.ExitCodeFor(cause), - fmt.Sprintf("DB cluster %q update was accepted but tdc could not verify the response as Starter; the update may have been applied; inspect it with `tdc db describe-db-cluster --db-cluster-id %s`", cluster.ID, cluster.ID), + fmt.Sprintf("DB cluster %q update was accepted but ti could not verify the response as Starter; the update may have been applied; inspect it with `ti db describe-db-cluster --db-cluster-id %s`", cluster.ID, cluster.ID), cause, ) } @@ -98,7 +98,7 @@ func deletingClusterPlanError(cluster apistarter.Cluster, cause error) error { apperr.CodeFor(cause), apperr.CategoryFor(cause), apperr.ExitCodeFor(cause), - fmt.Sprintf("DB cluster %q deletion was accepted but tdc could not verify a later response as Starter; deletion may still be in progress", cluster.ID), + fmt.Sprintf("DB cluster %q deletion was accepted but ti could not verify a later response as Starter; deletion may still be in progress", cluster.ID), cause, ) } diff --git a/internal/db/cluster_plan_test.go b/internal/db/cluster_plan_test.go index 68aaeba..d911fd7 100644 --- a/internal/db/cluster_plan_test.go +++ b/internal/db/cluster_plan_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" - apistarter "github.com/tidbcloud/tdc/internal/api/starter" - "github.com/tidbcloud/tdc/internal/apperr" + apistarter "github.com/tidbcloud/ti-cli/internal/api/starter" + "github.com/tidbcloud/ti-cli/internal/apperr" ) func TestEnsureStarterClusterPlanResolution(t *testing.T) { diff --git a/internal/db/cluster_region.go b/internal/db/cluster_region.go index f9f7ffb..93c61b5 100644 --- a/internal/db/cluster_region.go +++ b/internal/db/cluster_region.go @@ -4,10 +4,10 @@ import ( "fmt" "strings" - "github.com/tidbcloud/tdc/internal/api/endpoints" - apistarter "github.com/tidbcloud/tdc/internal/api/starter" - "github.com/tidbcloud/tdc/internal/config" - configregion "github.com/tidbcloud/tdc/internal/config/region" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + apistarter "github.com/tidbcloud/ti-cli/internal/api/starter" + "github.com/tidbcloud/ti-cli/internal/config" + configregion "github.com/tidbcloud/ti-cli/internal/config/region" ) type clusterRegionScope struct { diff --git a/internal/db/cluster_region_test.go b/internal/db/cluster_region_test.go index 53fe472..78bbdce 100644 --- a/internal/db/cluster_region_test.go +++ b/internal/db/cluster_region_test.go @@ -3,8 +3,8 @@ package db import ( "testing" - apistarter "github.com/tidbcloud/tdc/internal/api/starter" - "github.com/tidbcloud/tdc/internal/config" + apistarter "github.com/tidbcloud/ti-cli/internal/api/starter" + "github.com/tidbcloud/ti-cli/internal/config" ) func TestClusterRegionScopeMatchesDocumentedRepresentations(t *testing.T) { @@ -24,7 +24,7 @@ func TestClusterRegionScopeMatchesDocumentedRepresentations(t *testing.T) { {name: "alicloud name", scope: aliScope, region: apistarter.Region{Name: "regions/alicloud-ap-southeast-1"}, want: true}, {name: "alicloud provider and native id", scope: aliScope, region: apistarter.Region{RegionID: "ap-southeast-1", CloudProvider: "alicloud"}, want: true}, {name: "internal alibaba provider", scope: aliScope, region: apistarter.Region{RegionID: "ap-southeast-1", CloudProvider: "alibaba_cloud"}, want: true}, - {name: "tdc-qualified alibaba id", scope: aliScope, region: apistarter.Region{RegionID: "ali-ap-southeast-1"}, want: true}, + {name: "ti-qualified alibaba id", scope: aliScope, region: apistarter.Region{RegionID: "ali-ap-southeast-1"}, want: true}, {name: "other aws region", scope: awsScope, region: apistarter.Region{Name: "regions/aws-us-west-2"}}, {name: "same native region different provider", scope: aliScope, region: apistarter.Region{Name: "regions/aws-ap-southeast-1"}}, {name: "conflicting name and region id", scope: awsScope, region: apistarter.Region{Name: "regions/aws-us-east-1", RegionID: "us-west-2"}}, diff --git a/internal/db/cluster_test.go b/internal/db/cluster_test.go index 34353b0..3d7f2f1 100644 --- a/internal/db/cluster_test.go +++ b/internal/db/cluster_test.go @@ -10,10 +10,10 @@ import ( "testing" "time" - "github.com/tidbcloud/tdc/internal/api/endpoints" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/db/validate" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/db/validate" ) func TestCreateCluster(t *testing.T) { @@ -256,7 +256,7 @@ func TestCreateClusterProjectResolutionErrors(t *testing.T) { t.Run("explicit empty", func(t *testing.T) { profile := testProfile() profile.ProjectID = "profile-project" - _, err := Service{}.DryRunCreateCluster(context.Background(), "tdc db create-db-cluster", CreateClusterOptions{ + _, err := Service{}.DryRunCreateCluster(context.Background(), "ti db create-db-cluster", CreateClusterOptions{ Profile: profile, DisplayName: "demo", ClusterType: "starter", ProjectIDExplicit: true, MonthlySpendingLimitUSDCents: -1, }) if apperr.CodeFor(err) != "db.empty_project_id" { @@ -490,7 +490,7 @@ func TestDryRunCreateClusterDoesNotSendRequest(t *testing.T) { })) defer server.Close() - result, err := testService(server.URL).DryRunCreateCluster(context.Background(), "tdc db create-db-cluster", CreateClusterOptions{ + result, err := testService(server.URL).DryRunCreateCluster(context.Background(), "ti db create-db-cluster", CreateClusterOptions{ Profile: testProfile(), DisplayName: "demo-cluster", ClusterType: "starter", @@ -519,7 +519,7 @@ func TestDryRunCreateClusterDoesNotSendRequest(t *testing.T) { } func TestDryRunCreateClusterOmitsProjectLabelWhenUnset(t *testing.T) { - result, err := testService("https://starter.test").DryRunCreateCluster(context.Background(), "tdc db create-db-cluster", CreateClusterOptions{ + result, err := testService("https://starter.test").DryRunCreateCluster(context.Background(), "ti db create-db-cluster", CreateClusterOptions{ Profile: testProfile(), DisplayName: "demo-cluster", ClusterType: "starter", @@ -538,7 +538,7 @@ func TestDryRunCreateClusterOmitsProjectLabelWhenUnset(t *testing.T) { } func TestDryRunDeleteClusterDescribesWait(t *testing.T) { - result, err := testService("https://starter.test").DryRunDeleteCluster(context.Background(), "tdc db delete-db-cluster", DeleteClusterOptions{ + result, err := testService("https://starter.test").DryRunDeleteCluster(context.Background(), "ti db delete-db-cluster", DeleteClusterOptions{ Profile: testProfile(), ClusterID: "cluster-1", WaitUntilDeleted: true, @@ -562,7 +562,7 @@ func TestDryRunDeleteClusterDescribesWait(t *testing.T) { } func TestDryRunUpdateClusterDescribesStarterPrecondition(t *testing.T) { - result, err := testService("https://starter.test").DryRunUpdateCluster(context.Background(), "tdc db update-db-cluster", UpdateClusterOptions{ + result, err := testService("https://starter.test").DryRunUpdateCluster(context.Background(), "ti db update-db-cluster", UpdateClusterOptions{ Profile: testProfile(), ClusterID: "cluster-1", DisplayName: "renamed", MonthlySpendingLimitUSDCents: -1, }) if err != nil { @@ -577,7 +577,7 @@ func TestDryRunUpdateClusterDescribesStarterPrecondition(t *testing.T) { } func TestCreateDefaultsToStarterType(t *testing.T) { - result, err := Service{}.DryRunCreateCluster(context.Background(), "tdc db create-db-cluster", CreateClusterOptions{ + result, err := Service{}.DryRunCreateCluster(context.Background(), "ti db create-db-cluster", CreateClusterOptions{ Profile: testProfile(), DisplayName: "demo-cluster", ProjectID: "project-1", @@ -597,7 +597,7 @@ func TestCreateDefaultsToStarterType(t *testing.T) { } func TestCreateRejectsUnsupportedClusterType(t *testing.T) { - _, err := Service{}.DryRunCreateCluster(context.Background(), "tdc db create-db-cluster", CreateClusterOptions{ + _, err := Service{}.DryRunCreateCluster(context.Background(), "ti db create-db-cluster", CreateClusterOptions{ Profile: testProfile(), DisplayName: "demo-cluster", ClusterType: "essential", @@ -616,10 +616,10 @@ func testService(baseURL string) Service { func testProfile() *config.Profile { return &config.Profile{ - Name: "test", - CloudProvider: "aws", - RegionCode: "us-east-1", - TDCPublicKey: "public", - TDCPrivateKey: "private", + Name: "test", + CloudProvider: "aws", + RegionCode: "us-east-1", + TiDBCloudPublicKey: "public", + TiDBCloudPrivateKey: "private", } } diff --git a/internal/db/connectionstring/connectionstring.go b/internal/db/connectionstring/connectionstring.go index b2f2b7e..5bd04d0 100644 --- a/internal/db/connectionstring/connectionstring.go +++ b/internal/db/connectionstring/connectionstring.go @@ -8,8 +8,8 @@ import ( "strconv" "strings" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/db/sqlcred" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/db/sqlcred" ) const ( diff --git a/internal/db/connectionstring/connectionstring_test.go b/internal/db/connectionstring/connectionstring_test.go index 9d939a2..3ac6a30 100644 --- a/internal/db/connectionstring/connectionstring_test.go +++ b/internal/db/connectionstring/connectionstring_test.go @@ -4,24 +4,24 @@ import ( "strings" "testing" - "github.com/tidbcloud/tdc/internal/db/sqlcred" + "github.com/tidbcloud/ti-cli/internal/db/sqlcred" ) func TestBuildFormats(t *testing.T) { input := Input{ ClusterID: "cluster-1", AccessMode: sqlcred.ReadWrite, - Username: "prefix.tdc_rw", + Username: "prefix.ti_rw", Password: "pa:ss@word", Host: "gateway01.us-east-1.prod.aws.tidbcloud.com", Port: 4000, Database: "app db", } tests := map[string][]string{ - FormatMySQLURI: {"mysql://prefix.tdc_rw:pa%3Ass%40word@gateway01.us-east-1.prod.aws.tidbcloud.com:4000/app%20db?ssl-mode=VERIFY_IDENTITY"}, - FormatJDBC: {"jdbc:mysql://gateway01.us-east-1.prod.aws.tidbcloud.com:4000/app%20db?", "user=prefix.tdc_rw", "password=pa%3Ass%40word", "sslMode=VERIFY_IDENTITY"}, - FormatGoSQLDriver: {"prefix.tdc_rw:pa\\:ss\\@word@tcp(gateway01.us-east-1.prod.aws.tidbcloud.com:4000)/app%20db?", "parseTime=true", "tls=true"}, - FormatSQLAlchemy: {"mysql+pymysql://prefix.tdc_rw:pa%3Ass%40word@gateway01.us-east-1.prod.aws.tidbcloud.com:4000/app%20db?ssl_verify_identity=true"}, + FormatMySQLURI: {"mysql://prefix.ti_rw:pa%3Ass%40word@gateway01.us-east-1.prod.aws.tidbcloud.com:4000/app%20db?ssl-mode=VERIFY_IDENTITY"}, + FormatJDBC: {"jdbc:mysql://gateway01.us-east-1.prod.aws.tidbcloud.com:4000/app%20db?", "user=prefix.ti_rw", "password=pa%3Ass%40word", "sslMode=VERIFY_IDENTITY"}, + FormatGoSQLDriver: {"prefix.ti_rw:pa\\:ss\\@word@tcp(gateway01.us-east-1.prod.aws.tidbcloud.com:4000)/app%20db?", "parseTime=true", "tls=true"}, + FormatSQLAlchemy: {"mysql+pymysql://prefix.ti_rw:pa%3Ass%40word@gateway01.us-east-1.prod.aws.tidbcloud.com:4000/app%20db?ssl_verify_identity=true"}, } for format, wants := range tests { t.Run(format, func(t *testing.T) { @@ -46,7 +46,7 @@ func TestEnvFormatIncludesComponents(t *testing.T) { result, err := Build(Input{ ClusterID: "cluster-1", AccessMode: sqlcred.ReadOnly, - Username: "prefix.tdc_ro", + Username: "prefix.ti_ro", Password: "password with space", Host: "host.example.com", Port: 4000, @@ -63,10 +63,10 @@ func TestEnvFormatIncludesComponents(t *testing.T) { for _, want := range []string{ "APP_DB_HOST=host.example.com\n", "APP_DB_PORT=4000\n", - "APP_DB_USER=prefix.tdc_ro\n", + "APP_DB_USER=prefix.ti_ro\n", "APP_DB_PASSWORD=\"password with space\"\n", "APP_DB_ACCESS_MODE=read_only\n", - "APP_DATABASE_URL=mysql://prefix.tdc_ro:password%20with%20space@host.example.com:4000/test?ssl-mode=VERIFY_IDENTITY\n", + "APP_DATABASE_URL=mysql://prefix.ti_ro:password%20with%20space@host.example.com:4000/test?ssl-mode=VERIFY_IDENTITY\n", } { if !strings.Contains(env, want) { t.Fatalf("env output missing %q:\n%s", want, env) diff --git a/internal/db/sql.go b/internal/db/sql.go index 10d2346..d893207 100644 --- a/internal/db/sql.go +++ b/internal/db/sql.go @@ -6,22 +6,22 @@ import ( "os" "strings" - "github.com/tidbcloud/tdc/internal/api" - "github.com/tidbcloud/tdc/internal/api/endpoints" - apiiam "github.com/tidbcloud/tdc/internal/api/iam" - apistarter "github.com/tidbcloud/tdc/internal/api/starter" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/db/connectionstring" - "github.com/tidbcloud/tdc/internal/db/sqlaccess" - "github.com/tidbcloud/tdc/internal/db/sqlcred" - "github.com/tidbcloud/tdc/internal/db/sqlhttp" - "github.com/tidbcloud/tdc/internal/db/sqlmysql" - "github.com/tidbcloud/tdc/internal/db/sqlresult" - "github.com/tidbcloud/tdc/internal/db/sqlsingle" - "github.com/tidbcloud/tdc/internal/db/validate" - "github.com/tidbcloud/tdc/internal/dryrun" + "github.com/tidbcloud/ti-cli/internal/api" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + apiiam "github.com/tidbcloud/ti-cli/internal/api/iam" + apistarter "github.com/tidbcloud/ti-cli/internal/api/starter" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/db/connectionstring" + "github.com/tidbcloud/ti-cli/internal/db/sqlaccess" + "github.com/tidbcloud/ti-cli/internal/db/sqlcred" + "github.com/tidbcloud/ti-cli/internal/db/sqlhttp" + "github.com/tidbcloud/ti-cli/internal/db/sqlmysql" + "github.com/tidbcloud/ti-cli/internal/db/sqlresult" + "github.com/tidbcloud/ti-cli/internal/db/sqlsingle" + "github.com/tidbcloud/ti-cli/internal/db/validate" + "github.com/tidbcloud/ti-cli/internal/dryrun" ) const ( @@ -134,7 +134,7 @@ func (s Service) DryRunPrepareQueryAccess(ctx context.Context, commandPath strin dryrun.RequestSummary{ Method: "GET/POST/PATCH", Path: "/v1beta1/clusters/" + clusterID + "/sqlUsers", - Description: "normal execution verifies the cluster, lists SQL users, creates missing tdc-managed read_only/read_write/admin users, and rotates passwords for missing local credentials", + Description: "normal execution verifies the cluster, lists SQL users, creates missing ti-managed read_only/read_write/admin users, and rotates passwords for missing local credentials", Body: prepareResult, }, dryrun.Check{Name: "config_and_credentials", Status: "passed", Message: fmt.Sprintf("profile %q loaded", profileName(opts.Profile))}, @@ -199,7 +199,7 @@ func (s Service) ExecuteSQL(ctx context.Context, opts ExecuteSQLOptions) (sqlres HTTPClient: s.HTTPClient, Debug: s.Debug, DebugWriter: s.DebugWriter, - UserAgent: "tdc db execute-sql-statement", + UserAgent: "ti db execute-sql-statement", }) case transportMySQL: if err := sqlmysql.ValidateEndpoint(host, port); err != nil { @@ -258,7 +258,7 @@ func (s Service) sqlConnectionInputs(ctx context.Context, profile *config.Profil "db.sql_credentials_missing", "config", 2, - fmt.Sprintf("missing prepared %s DB SQL credentials for cluster %s; run tdc db create-db-sql-users --db-cluster-id %s", mode, clusterID, clusterID), + fmt.Sprintf("missing prepared %s DB SQL credentials for cluster %s; run ti db create-db-sql-users --db-cluster-id %s", mode, clusterID, clusterID), ) } return clusterID, mode, credential, cluster, nil @@ -323,7 +323,7 @@ func (s Service) iamClient(profile *config.Profile, permission authz.Permission, Timeout: s.Timeout, Debug: s.Debug, DebugWriter: s.DebugWriter, - UserAgent: "tdc db sql", + UserAgent: "ti db sql", }) if err != nil { return nil, err diff --git a/internal/db/sql_test.go b/internal/db/sql_test.go index 04079ab..6c7acea 100644 --- a/internal/db/sql_test.go +++ b/internal/db/sql_test.go @@ -10,10 +10,10 @@ import ( "strings" "testing" - "github.com/tidbcloud/tdc/internal/api/endpoints" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/db/connectionstring" - "github.com/tidbcloud/tdc/internal/db/sqlcred" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/db/connectionstring" + "github.com/tidbcloud/ti-cli/internal/db/sqlcred" ) func TestPrepareQueryAccessCreatesAndStoresCredentials(t *testing.T) { @@ -59,10 +59,10 @@ func TestPrepareQueryAccessCreatesAndStoresCredentials(t *testing.T) { if err != nil { t.Fatalf("read credentials: %v", err) } - if !strings.Contains(string(data), "[read_only]") || !strings.Contains(string(data), "prefix.tdc_admin") { + if !strings.Contains(string(data), "[read_only]") || !strings.Contains(string(data), "prefix.ti_admin") { t.Fatalf("unexpected credentials:\n%s", string(data)) } - if strings.Contains(string(data), ".tdc/credentials") { + if strings.Contains(string(data), ".ti/credentials") { t.Fatalf("DB credentials should not be stored in main credentials shape:\n%s", string(data)) } } @@ -88,13 +88,13 @@ func TestPrepareQueryAccessRejectsNonStarterBeforeIAMOrCredentialWrite(t *testin if requests != 1 { t.Fatalf("requests = %d, want only the cluster preflight", requests) } - if _, err := os.Stat(filepath.Join(home, ".tdc", "db_users")); !os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(home, ".ti", "db_users")); !os.IsNotExist(err) { t.Fatalf("non-Starter rejection wrote local DB credentials: %v", err) } } func TestDryRunPrepareQueryAccessDescribesStarterPrecondition(t *testing.T) { - result, err := Service{}.DryRunPrepareQueryAccess(context.Background(), "tdc db create-db-sql-users", PrepareQueryAccessOptions{ + result, err := Service{}.DryRunPrepareQueryAccess(context.Background(), "ti db create-db-sql-users", PrepareQueryAccessOptions{ Profile: testProfile(), ClusterID: "cluster-1", }) if err != nil { @@ -124,11 +124,11 @@ func TestCreateConnectionString(t *testing.T) { if err != nil { t.Fatalf("CreateConnectionString failed: %v", err) } - if result.AccessMode != sqlcred.ReadOnly || result.Username != "prefix.tdc_ro" { + if result.AccessMode != sqlcred.ReadOnly || result.Username != "prefix.ti_ro" { t.Fatalf("unexpected result: %#v", result) } if !strings.Contains(result.ConnectionString, "jdbc:mysql://gateway.example.com:4000/app?") || - !strings.Contains(result.ConnectionString, "user=prefix.tdc_ro") { + !strings.Contains(result.ConnectionString, "user=prefix.ti_ro") { t.Fatalf("unexpected connection string: %s", result.ConnectionString) } } @@ -141,7 +141,7 @@ func TestExecuteSQLHTTP(t *testing.T) { var sqlBody map[string]string sqlServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user, password, ok := r.BasicAuth() - if !ok || user != "prefix.tdc_rw" || password != "rw-pass" { + if !ok || user != "prefix.ti_rw" || password != "rw-pass" { t.Fatalf("unexpected basic auth user=%q password=%q ok=%t", user, password, ok) } if err := json.NewDecoder(r.Body).Decode(&sqlBody); err != nil { @@ -215,7 +215,7 @@ func TestConnectionAndSQLRejectNonStarterBeforeCredentialOrSQLAccess(t *testing. if sqlRequests != 0 { t.Fatalf("SQL requests = %d, want 0", sqlRequests) } - if _, err := os.Stat(filepath.Join(home, ".tdc", "db_users")); !os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(home, ".ti", "db_users")); !os.IsNotExist(err) { t.Fatalf("non-Starter connection path accessed local DB credentials: %v", err) } } @@ -273,9 +273,9 @@ func clusterEndpointServer(t *testing.T) *httptest.Server { func writeSQLCreds(t *testing.T, home, clusterID string) { t.Helper() if err := sqlcred.Write(home, clusterID, sqlcred.Document{ - ReadOnly: sqlcred.Credential{Username: "prefix.tdc_ro", Password: "ro-pass"}, - ReadWrite: sqlcred.Credential{Username: "prefix.tdc_rw", Password: "rw-pass"}, - Admin: sqlcred.Credential{Username: "prefix.tdc_admin", Password: "admin-pass"}, + ReadOnly: sqlcred.Credential{Username: "prefix.ti_ro", Password: "ro-pass"}, + ReadWrite: sqlcred.Credential{Username: "prefix.ti_rw", Password: "rw-pass"}, + Admin: sqlcred.Credential{Username: "prefix.ti_admin", Password: "admin-pass"}, }); err != nil { t.Fatalf("write SQL credentials: %v", err) } @@ -283,7 +283,7 @@ func writeSQLCreds(t *testing.T, home, clusterID string) { if err != nil { t.Fatal(err) } - if !strings.Contains(path, filepath.Join(".tdc", "db_users", clusterID, "credentials")) { + if !strings.Contains(path, filepath.Join(".ti", "db_users", clusterID, "credentials")) { t.Fatalf("unexpected SQL credentials path: %s", path) } } diff --git a/internal/db/sqlaccess/sqlaccess.go b/internal/db/sqlaccess/sqlaccess.go index 5bf419e..de93869 100644 --- a/internal/db/sqlaccess/sqlaccess.go +++ b/internal/db/sqlaccess/sqlaccess.go @@ -7,9 +7,9 @@ import ( "math/big" "strings" - apiiam "github.com/tidbcloud/tdc/internal/api/iam" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/db/sqlcred" + apiiam "github.com/tidbcloud/ti-cli/internal/api/iam" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/db/sqlcred" ) const AuthMethodMySQLNativePassword = "mysql_native_password" @@ -48,9 +48,9 @@ type Plan struct { func Plans() []Plan { return []Plan{ - {Mode: sqlcred.ReadOnly, Suffix: "tdc_ro", BuiltinRole: "role_readonly"}, - {Mode: sqlcred.ReadWrite, Suffix: "tdc_rw", BuiltinRole: "role_readwrite"}, - {Mode: sqlcred.Admin, Suffix: "tdc_admin", BuiltinRole: "role_admin"}, + {Mode: sqlcred.ReadOnly, Suffix: "ti_ro", BuiltinRole: "role_readonly"}, + {Mode: sqlcred.ReadWrite, Suffix: "ti_rw", BuiltinRole: "role_readwrite"}, + {Mode: sqlcred.Admin, Suffix: "ti_admin", BuiltinRole: "role_admin"}, } } @@ -175,7 +175,7 @@ func findManagedUser(users []apiiam.SQLUser, plan Plan) (apiiam.SQLUser, bool, e "db.sql_user_conflict", "usage", 2, - fmt.Sprintf("remote SQL user %q matches tdc suffix %q but has builtinRole=%q authMethod=%q; refusing to modify it", user.UserName, plan.Suffix, user.BuiltinRole, user.AuthMethod), + fmt.Sprintf("remote SQL user %q matches ti suffix %q but has builtinRole=%q authMethod=%q; refusing to modify it", user.UserName, plan.Suffix, user.BuiltinRole, user.AuthMethod), ) } return user, true, nil diff --git a/internal/db/sqlaccess/sqlaccess_test.go b/internal/db/sqlaccess/sqlaccess_test.go index 0a76a35..ab76e34 100644 --- a/internal/db/sqlaccess/sqlaccess_test.go +++ b/internal/db/sqlaccess/sqlaccess_test.go @@ -4,9 +4,9 @@ import ( "context" "testing" - apiiam "github.com/tidbcloud/tdc/internal/api/iam" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/db/sqlcred" + apiiam "github.com/tidbcloud/ti-cli/internal/api/iam" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/db/sqlcred" ) func TestPrepareCreatesMissingUsers(t *testing.T) { @@ -33,15 +33,15 @@ func TestPrepareCreatesMissingUsers(t *testing.T) { func TestPrepareExistingUsersDoesNotCreateDuplicates(t *testing.T) { client := &fakeUserClient{ users: []apiiam.SQLUser{ - {UserName: "prefix.tdc_ro", AuthMethod: AuthMethodMySQLNativePassword, BuiltinRole: "prefix.role_readonly"}, - {UserName: "prefix.tdc_rw", AuthMethod: AuthMethodMySQLNativePassword, BuiltinRole: "prefix.role_readwrite"}, - {UserName: "prefix.tdc_admin", AuthMethod: AuthMethodMySQLNativePassword, BuiltinRole: "prefix.role_admin"}, + {UserName: "prefix.ti_ro", AuthMethod: AuthMethodMySQLNativePassword, BuiltinRole: "prefix.role_readonly"}, + {UserName: "prefix.ti_rw", AuthMethod: AuthMethodMySQLNativePassword, BuiltinRole: "prefix.role_readwrite"}, + {UserName: "prefix.ti_admin", AuthMethod: AuthMethodMySQLNativePassword, BuiltinRole: "prefix.role_admin"}, }, } local := sqlcred.Document{ - ReadOnly: sqlcred.Credential{Username: "prefix.tdc_ro", Password: "ro"}, - ReadWrite: sqlcred.Credential{Username: "prefix.tdc_rw", Password: "rw"}, - Admin: sqlcred.Credential{Username: "prefix.tdc_admin", Password: "admin"}, + ReadOnly: sqlcred.Credential{Username: "prefix.ti_ro", Password: "ro"}, + ReadWrite: sqlcred.Credential{Username: "prefix.ti_rw", Password: "rw"}, + Admin: sqlcred.Credential{Username: "prefix.ti_admin", Password: "admin"}, } result, _, err := Prepare(context.Background(), client, Options{ClusterID: "cluster-1", Local: local}) if err != nil { @@ -58,9 +58,9 @@ func TestPrepareExistingUsersDoesNotCreateDuplicates(t *testing.T) { func TestPrepareRotatesPasswordWhenLocalCredentialMissing(t *testing.T) { client := &fakeUserClient{ users: []apiiam.SQLUser{ - {UserName: "prefix.tdc_ro", AuthMethod: AuthMethodMySQLNativePassword, BuiltinRole: "role_readonly"}, - {UserName: "prefix.tdc_rw", AuthMethod: AuthMethodMySQLNativePassword, BuiltinRole: "role_readwrite"}, - {UserName: "prefix.tdc_admin", AuthMethod: AuthMethodMySQLNativePassword, BuiltinRole: "role_admin"}, + {UserName: "prefix.ti_ro", AuthMethod: AuthMethodMySQLNativePassword, BuiltinRole: "role_readonly"}, + {UserName: "prefix.ti_rw", AuthMethod: AuthMethodMySQLNativePassword, BuiltinRole: "role_readwrite"}, + {UserName: "prefix.ti_admin", AuthMethod: AuthMethodMySQLNativePassword, BuiltinRole: "role_admin"}, }, } result, doc, err := Prepare(context.Background(), client, Options{ClusterID: "cluster-1"}) @@ -81,7 +81,7 @@ func TestPrepareRotatesPasswordWhenLocalCredentialMissing(t *testing.T) { func TestPrepareConflictingRemoteUserFails(t *testing.T) { client := &fakeUserClient{ users: []apiiam.SQLUser{ - {UserName: "prefix.tdc_rw", AuthMethod: AuthMethodMySQLNativePassword, BuiltinRole: "role_admin"}, + {UserName: "prefix.ti_rw", AuthMethod: AuthMethodMySQLNativePassword, BuiltinRole: "role_admin"}, }, } _, _, err := Prepare(context.Background(), client, Options{ClusterID: "cluster-1"}) diff --git a/internal/db/sqlcred/store.go b/internal/db/sqlcred/store.go index 1fb717b..ae687c5 100644 --- a/internal/db/sqlcred/store.go +++ b/internal/db/sqlcred/store.go @@ -10,8 +10,8 @@ import ( "strings" "github.com/pelletier/go-toml/v2" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config/store" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config/store" ) const ( @@ -48,7 +48,7 @@ func CredentialsPath(homeDir, clusterID string) (string, error) { if err != nil { return "", err } - return filepath.Join(homeDir, store.TDCDirName, "db_users", safeClusterID, store.CredsFileName), nil + return filepath.Join(homeDir, store.TIDirName, "db_users", safeClusterID, store.CredsFileName), nil } func SafeClusterID(clusterID string) (string, error) { diff --git a/internal/db/sqlcred/store_test.go b/internal/db/sqlcred/store_test.go index d885888..6ecb82f 100644 --- a/internal/db/sqlcred/store_test.go +++ b/internal/db/sqlcred/store_test.go @@ -39,9 +39,9 @@ func TestSafeClusterIDRejectsUnsafePathSegments(t *testing.T) { func TestReadWriteCredentials(t *testing.T) { home := t.TempDir() doc := Document{ - ReadOnly: Credential{Username: "prefix.tdc_ro", Password: "ro-pass"}, - ReadWrite: Credential{Username: "prefix.tdc_rw", Password: "rw-pass"}, - Admin: Credential{Username: "prefix.tdc_admin", Password: "admin-pass"}, + ReadOnly: Credential{Username: "prefix.ti_ro", Password: "ro-pass"}, + ReadWrite: Credential{Username: "prefix.ti_rw", Password: "rw-pass"}, + Admin: Credential{Username: "prefix.ti_admin", Password: "admin-pass"}, } if err := Write(home, "cluster-1", doc); err != nil { t.Fatalf("Write failed: %v", err) @@ -50,14 +50,14 @@ func TestReadWriteCredentials(t *testing.T) { if err != nil { t.Fatalf("CredentialsPath failed: %v", err) } - if !strings.HasSuffix(path, filepath.Join(".tdc", "db_users", "cluster-1", "credentials")) { + if !strings.HasSuffix(path, filepath.Join(".ti", "db_users", "cluster-1", "credentials")) { t.Fatalf("unexpected path %s", path) } data, err := os.ReadFile(path) if err != nil { t.Fatalf("read credentials: %v", err) } - if !strings.Contains(string(data), "[read_only]") || !strings.Contains(string(data), "prefix.tdc_admin") { + if !strings.Contains(string(data), "[read_only]") || !strings.Contains(string(data), "prefix.ti_admin") { t.Fatalf("unexpected credentials file:\n%s", string(data)) } if runtime.GOOS != "windows" { @@ -77,7 +77,7 @@ func TestReadWriteCredentials(t *testing.T) { if read.ReadWrite.Password != "rw-pass" { t.Fatalf("unexpected read document: %#v", read) } - if credential, ok := read.Credential(ReadOnly); !ok || credential.Username != "prefix.tdc_ro" { + if credential, ok := read.Credential(ReadOnly); !ok || credential.Username != "prefix.ti_ro" { t.Fatalf("unexpected read_only credential: %#v ok=%t", credential, ok) } } diff --git a/internal/db/sqlhttp/sqlhttp.go b/internal/db/sqlhttp/sqlhttp.go index 341bb38..b1d021d 100644 --- a/internal/db/sqlhttp/sqlhttp.go +++ b/internal/db/sqlhttp/sqlhttp.go @@ -13,10 +13,10 @@ import ( "strings" "time" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/db/sqlcred" - "github.com/tidbcloud/tdc/internal/db/sqlresult" - "github.com/tidbcloud/tdc/internal/oplog" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/db/sqlcred" + "github.com/tidbcloud/ti-cli/internal/db/sqlresult" + "github.com/tidbcloud/ti-cli/internal/oplog" ) type Options struct { @@ -59,12 +59,12 @@ func Execute(ctx context.Context, opts Options) (sqlresult.Result, error) { if opts.UserAgent != "" { req.Header.Set("User-Agent", opts.UserAgent) } else { - req.Header.Set("User-Agent", "tdc") + req.Header.Set("User-Agent", "ti") } traceID := traceID() req.Header.Set("X-Debug-Trace-Id", traceID) if opts.Debug && opts.DebugWriter != nil { - _, _ = fmt.Fprintf(opts.DebugWriter, "tdc [DEBUG]: sql https api request id: %s\n", traceID) + _, _ = fmt.Fprintf(opts.DebugWriter, "ti [DEBUG]: sql https api request id: %s\n", traceID) } start := time.Now() diff --git a/internal/db/sqlhttp/sqlhttp_test.go b/internal/db/sqlhttp/sqlhttp_test.go index 3c63a13..7fb420a 100644 --- a/internal/db/sqlhttp/sqlhttp_test.go +++ b/internal/db/sqlhttp/sqlhttp_test.go @@ -8,9 +8,9 @@ import ( "strings" "testing" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/db/sqlcred" - "github.com/tidbcloud/tdc/internal/oplog" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/db/sqlcred" + "github.com/tidbcloud/ti-cli/internal/oplog" ) func TestExecuteHTTP(t *testing.T) { diff --git a/internal/db/sqlmysql/sqlmysql.go b/internal/db/sqlmysql/sqlmysql.go index 182007b..d18830c 100644 --- a/internal/db/sqlmysql/sqlmysql.go +++ b/internal/db/sqlmysql/sqlmysql.go @@ -8,9 +8,9 @@ import ( "strconv" "github.com/go-sql-driver/mysql" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/db/sqlcred" - "github.com/tidbcloud/tdc/internal/db/sqlresult" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/db/sqlcred" + "github.com/tidbcloud/ti-cli/internal/db/sqlresult" ) type Options struct { diff --git a/internal/db/sqlmysql/sqlmysql_test.go b/internal/db/sqlmysql/sqlmysql_test.go index 1eb51d3..f9685c0 100644 --- a/internal/db/sqlmysql/sqlmysql_test.go +++ b/internal/db/sqlmysql/sqlmysql_test.go @@ -8,11 +8,11 @@ import ( "sync/atomic" "testing" - "github.com/tidbcloud/tdc/internal/db/sqlcred" + "github.com/tidbcloud/ti-cli/internal/db/sqlcred" ) func TestExecuteOpensAndClosesOneConnection(t *testing.T) { - driverName := "tdc_sqlmysql_fake" + driverName := "ti_sqlmysql_fake" fake := &fakeDriver{} sql.Register(driverName, fake) diff --git a/internal/db/sqlresult/sqlresult.go b/internal/db/sqlresult/sqlresult.go index 231ff6c..6d92df6 100644 --- a/internal/db/sqlresult/sqlresult.go +++ b/internal/db/sqlresult/sqlresult.go @@ -8,7 +8,7 @@ import ( "strconv" "strings" - "github.com/tidbcloud/tdc/internal/db/sqlcred" + "github.com/tidbcloud/ti-cli/internal/db/sqlcred" ) type Field struct { diff --git a/internal/db/sqlsingle/sqlsingle.go b/internal/db/sqlsingle/sqlsingle.go index 465b848..b9a6abd 100644 --- a/internal/db/sqlsingle/sqlsingle.go +++ b/internal/db/sqlsingle/sqlsingle.go @@ -3,7 +3,7 @@ package sqlsingle import ( "strings" - "github.com/tidbcloud/tdc/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/apperr" ) func Validate(statement string) error { diff --git a/internal/db/validate/validate.go b/internal/db/validate/validate.go index d315adc..34b74b6 100644 --- a/internal/db/validate/validate.go +++ b/internal/db/validate/validate.go @@ -5,7 +5,7 @@ import ( "regexp" "strings" - "github.com/tidbcloud/tdc/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/apperr" ) const ClusterTypeStarter = "starter" diff --git a/internal/fs/control.go b/internal/fs/control.go index 603b80a..fb4dfd1 100644 --- a/internal/fs/control.go +++ b/internal/fs/control.go @@ -10,15 +10,15 @@ import ( "text/tabwriter" "time" - "github.com/tidbcloud/tdc/internal/api" - "github.com/tidbcloud/tdc/internal/api/endpoints" - apifs "github.com/tidbcloud/tdc/internal/api/fs" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/auth" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/dryrun" - "github.com/tidbcloud/tdc/internal/fs/fscred" + "github.com/tidbcloud/ti-cli/internal/api" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + apifs "github.com/tidbcloud/ti-cli/internal/api/fs" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/auth" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/dryrun" + "github.com/tidbcloud/ti-cli/internal/fs/fscred" ) type Service struct { @@ -201,7 +201,7 @@ func (s Service) DryRunDeleteFileSystem(ctx context.Context, commandPath string, Method: http.MethodDelete, Path: "/v1/tenant", Body: redactedDeprovisionRequest(body), - Description: "normal execution uses the stored tdc fs API key before deleting", + Description: "normal execution uses the stored ti fs API key before deleting", }, checks..., ), nil @@ -284,7 +284,7 @@ func (s Service) bearerClient(profile *config.Profile, endpoint endpoints.Endpoi Timeout: s.Timeout, Debug: s.Debug, DebugWriter: s.DebugWriter, - UserAgent: "tdc fs legacy helper", + UserAgent: "ti fs legacy helper", }) if err != nil { return nil, err @@ -395,7 +395,7 @@ func resourceMismatch(existing, requested string) error { "fs.resource_name_mismatch", "usage", 2, - fmt.Sprintf("profile is already configured for tdc fs resource %q; use that name or delete it before creating %q", existing, requested), + fmt.Sprintf("profile is already configured for ti fs resource %q; use that name or delete it before creating %q", existing, requested), ) } @@ -446,7 +446,7 @@ func (r FileSystemResult) Human() string { lines = append(lines, "Location: "+strings.TrimSpace(r.CloudProvider+" "+r.RegionCode)) } if r.CredentialsStored { - lines = append(lines, "Credentials: stored in ~/.tdc/credentials") + lines = append(lines, "Credentials: stored in ~/.ti/credentials") } return strings.Join(lines, "\n") } @@ -463,14 +463,14 @@ func (r DeleteResult) Human() string { lines = append(lines, "Remote deletion state: "+r.RemoteDeletionState) } if r.CredentialsRemoved { - lines = append(lines, "Credentials: removed from ~/.tdc/credentials") + lines = append(lines, "Credentials: removed from ~/.ti/credentials") } return strings.Join(lines, "\n") } func (r CheckResult) Human() string { var out strings.Builder - _, _ = fmt.Fprintf(&out, "tdc fs check: %s\n", r.Status) + _, _ = fmt.Fprintf(&out, "ti fs check: %s\n", r.Status) writer := tabwriter.NewWriter(&out, 0, 0, 2, ' ', 0) _, _ = fmt.Fprintln(writer, "CHECK\tSTATUS\tMESSAGE") for _, check := range r.Checks { diff --git a/internal/fs/data.go b/internal/fs/data.go index dcb164b..5f69462 100644 --- a/internal/fs/data.go +++ b/internal/fs/data.go @@ -14,11 +14,11 @@ import ( "strings" "text/tabwriter" - "github.com/tidbcloud/tdc/internal/api" - apifs "github.com/tidbcloud/tdc/internal/api/fs" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" + "github.com/tidbcloud/ti-cli/internal/api" + apifs "github.com/tidbcloud/ti-cli/internal/api/fs" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" ) type CopyFileOptions struct { diff --git a/internal/fs/drive9_companion.go b/internal/fs/drive9_companion.go index 50cf13f..bc79c8e 100644 --- a/internal/fs/drive9_companion.go +++ b/internal/fs/drive9_companion.go @@ -14,13 +14,13 @@ import ( "strings" "time" - apifs "github.com/tidbcloud/tdc/internal/api/fs" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/config/region" - "github.com/tidbcloud/tdc/internal/fs/fscred" - "github.com/tidbcloud/tdc/internal/fs/mountlocator" - "github.com/tidbcloud/tdc/internal/fswrap" + apifs "github.com/tidbcloud/ti-cli/internal/api/fs" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/config/region" + "github.com/tidbcloud/ti-cli/internal/fs/fscred" + "github.com/tidbcloud/ti-cli/internal/fs/mountlocator" + "github.com/tidbcloud/ti-cli/internal/fswrap" ) const ( @@ -185,7 +185,7 @@ func (s Service) drive9CreateFileSystem(ctx context.Context, opts CreateFileSyst ResourceName: name, Args: args, CaptureStdout: true, - IncludeTDCKeys: true, + IncludeTIKeys: true, IncludeFSAPIKey: false, }) if err != nil { @@ -193,7 +193,7 @@ func (s Service) drive9CreateFileSystem(ctx context.Context, opts CreateFileSyst } var out drive9CreateOutput if err := json.Unmarshal(result.Stdout, &out); err != nil { - return FileSystemResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs provision response", err) + return FileSystemResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs provision response", err) } status := strings.TrimSpace(out.Status) if status == "" { @@ -242,7 +242,7 @@ func (s Service) drive9DeleteFileSystem(ctx context.Context, opts DeleteFileSyst Profile: opts.Profile, Args: args, CaptureStdout: true, - IncludeTDCKeys: true, + IncludeTIKeys: true, IncludeFSAPIKey: true, }) if err != nil { @@ -250,7 +250,7 @@ func (s Service) drive9DeleteFileSystem(ctx context.Context, opts DeleteFileSyst } var out drive9DeleteOutput if err := json.Unmarshal(result.Stdout, &out); err != nil { - return DeleteResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs deletion response", err) + return DeleteResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs deletion response", err) } status := strings.TrimSpace(out.Status) if status == "" { @@ -300,7 +300,7 @@ func (s Service) waitUntilFileSystemReady(ctx context.Context, homeDir string, p "fs.ready_wait_failed", "runtime", 1, - fmt.Sprintf("tdc fs resource %q was provisioned and its credentials were stored, but its Drive9 data plane readiness check failed", name), + fmt.Sprintf("ti fs resource %q was provisioned and its credentials were stored, but its Drive9 data plane readiness check failed", name), err, ) } @@ -319,7 +319,7 @@ func fsReadyWaitContextError(parent, waitCtx context.Context, name string, timeo "fs.ready_wait_canceled", "runtime", 1, - fmt.Sprintf("waiting for tdc fs resource %q to become ready was canceled; the resource and its local credentials were not deleted", name), + fmt.Sprintf("waiting for ti fs resource %q to become ready was canceled; the resource and its local credentials were not deleted", name), parent.Err(), ) } @@ -328,7 +328,7 @@ func fsReadyWaitContextError(parent, waitCtx context.Context, name string, timeo "fs.ready_wait_timeout", "runtime", 1, - fmt.Sprintf("tdc fs resource %q did not become ready within %s; the resource and its local credentials were not deleted", name, timeout), + fmt.Sprintf("ti fs resource %q did not become ready within %s; the resource and its local credentials were not deleted", name, timeout), ) } return nil @@ -372,11 +372,11 @@ func (s Service) drive9CheckFileSystem(ctx context.Context, opts CheckFileSystem return CheckResult{}, apperr.New("fs.missing_profile", "config", 2, "active profile is required") } checks := []Check{ - {Name: "config_and_credentials", Status: "passed", Message: fmt.Sprintf("tdc fs credentials for profile namespace %q loaded", profileName(opts.Profile))}, + {Name: "config_and_credentials", Status: "passed", Message: fmt.Sprintf("ti fs credentials for profile namespace %q loaded", profileName(opts.Profile))}, } resource := fscred.FromProfile(opts.Profile) if resource.Name == "" || !resource.HasAPIKey { - checks = append(checks, Check{Name: "fs_resource_credentials", Status: "warning", Message: "tdc fs resource name or FS token is missing"}) + checks = append(checks, Check{Name: "fs_resource_credentials", Status: "warning", Message: "ti fs resource name or FS token is missing"}) } else { checks = append(checks, Check{Name: "fs_resource_credentials", Status: "passed", Message: resource.Name}) } @@ -390,16 +390,16 @@ func (s Service) drive9CheckFileSystem(ctx context.Context, opts CheckFileSystem checks = append(checks, Check{Name: "companion_binary", Status: "failed", Message: apperr.MessageFor(err)}) return checkResult(opts.Profile, resource, &endpoint, nil, checks), nil } - checks = append(checks, Check{Name: "companion_binary", Status: "passed", Message: "tdc-drive9"}) + checks = append(checks, Check{Name: "companion_binary", Status: "passed", Message: "ti-drive9"}) if !resource.HasAPIKey { - checks = append(checks, Check{Name: "remote_status", Status: "warning", Message: "remote status requires fs_api_key; run tdc fs create-file-system first"}) + checks = append(checks, Check{Name: "remote_status", Status: "warning", Message: "remote status requires fs_api_key; run ti fs create-file-system first"}) return checkResult(opts.Profile, resource, &endpoint, nil, checks), nil } if _, err := s.drive9Run(ctx, opts.Profile, []string{"fs", "stat", "--output", "json", ":/"}, true); err != nil { checks = append(checks, Check{Name: "remote_status", Status: "failed", Message: apperr.MessageFor(err)}) return checkResult(opts.Profile, resource, &endpoint, nil, checks), nil } - remote := apifs.StatusResponse{Status: "reachable", TenantID: resource.TenantID, Kind: "tdc fs"} + remote := apifs.StatusResponse{Status: "reachable", TenantID: resource.TenantID, Kind: "ti fs"} checks = append(checks, Check{Name: "remote_status", Status: "passed", Message: "reachable"}) return checkResult(opts.Profile, resource, &endpoint, &remote, checks), nil } @@ -473,7 +473,7 @@ func (s Service) drive9DescribeFile(ctx context.Context, opts DescribeFileOption } var metadata drive9StatMetadata if err := json.Unmarshal(result.Stdout, &metadata); err != nil { - return DescribeFileResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs stat response", err) + return DescribeFileResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs stat response", err) } mtime := int64(0) if metadata.Mtime != nil { @@ -663,7 +663,7 @@ func (s Service) drive9CreateLayer(ctx context.Context, opts CreateLayerOptions) } var layer apifs.FSLayer if err := json.Unmarshal(result.Stdout, &layer); err != nil { - return LayerResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs layer response", err) + return LayerResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs layer response", err) } return LayerResult{FSLayer: layer}, nil } @@ -675,7 +675,7 @@ func (s Service) drive9ListLayers(ctx context.Context, opts ListLayersOptions) ( } var out LayerListResult if err := json.Unmarshal(result.Stdout, &out); err != nil { - return LayerListResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs layer list response", err) + return LayerListResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs layer list response", err) } return out, nil } @@ -687,7 +687,7 @@ func (s Service) drive9DescribeLayer(ctx context.Context, opts DescribeLayerOpti } var layer apifs.FSLayer if err := json.Unmarshal(result.Stdout, &layer); err != nil { - return LayerResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs layer response", err) + return LayerResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs layer response", err) } return LayerResult{FSLayer: layer}, nil } @@ -700,7 +700,7 @@ func (s Service) drive9DiffLayer(ctx context.Context, opts LayerEntriesOptions) } var out LayerEntriesResult if err := json.Unmarshal(result.Stdout, &out); err != nil { - return LayerEntriesResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs layer diff response", err) + return LayerEntriesResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs layer diff response", err) } out.LayerID = layerID if opts.MaxSeq > 0 { @@ -726,7 +726,7 @@ func (s Service) drive9CreateLayerCheckpoint(ctx context.Context, opts CreateLay } var checkpoint apifs.FSLayerCheckpoint if err := json.Unmarshal(result.Stdout, &checkpoint); err != nil { - return LayerCheckpointResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs layer checkpoint response", err) + return LayerCheckpointResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs layer checkpoint response", err) } return LayerCheckpointResult{FSLayerCheckpoint: checkpoint}, nil } @@ -824,7 +824,7 @@ func (s Service) drive9CreateJournal(ctx context.Context, opts JournalCreateOpti } var journal apifs.Journal if err := json.Unmarshal(result.Stdout, &journal); err != nil { - return JournalResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs journal response", err) + return JournalResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs journal response", err) } return JournalResult(journal), nil } @@ -862,7 +862,7 @@ func (s Service) drive9AppendJournalEntries(ctx context.Context, opts JournalApp } var out apifs.JournalAppendResponse if err := json.Unmarshal(result.Stdout, &out); err != nil { - return JournalAppendResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs journal append response", err) + return JournalAppendResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs journal append response", err) } return JournalAppendResult(out), nil } @@ -891,7 +891,7 @@ func (s Service) drive9ReadJournalEntries(ctx context.Context, opts JournalReadO } var entry apifs.JournalEntry if err := json.Unmarshal([]byte(line), &entry); err != nil { - return JournalEntriesResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs journal entry", err) + return JournalEntriesResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs journal entry", err) } entries = append(entries, entry) } @@ -933,7 +933,7 @@ func (s Service) drive9SearchJournal(ctx context.Context, opts JournalSearchOpti } var match apifs.JournalSearchMatch if err := json.Unmarshal([]byte(line), &match); err != nil { - return JournalSearchResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs journal search response", err) + return JournalSearchResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs journal search response", err) } matches = append(matches, match) } @@ -951,7 +951,7 @@ func (s Service) drive9VerifyJournal(ctx context.Context, opts JournalVerifyOpti } var out apifs.JournalVerifyResult if err := json.Unmarshal(result.Stdout, &out); err != nil { - return JournalVerifyResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs journal verify response", err) + return JournalVerifyResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs journal verify response", err) } return JournalVerifyResult(out), nil } @@ -1031,13 +1031,13 @@ func (s Service) drive9ReadVaultSecret(ctx context.Context, opts VaultReadSecret if opts.Field != "" { var fields map[string]string if err := json.Unmarshal(result.Stdout, &fields); err != nil { - return nil, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs-vault secret", err) + return nil, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs-vault secret", err) } return VaultReadSecretResult{SecretName: name, Field: opts.Field, Value: fields[opts.Field]}, nil } var fields map[string]string if err := json.Unmarshal(result.Stdout, &fields); err != nil { - return nil, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs-vault secret", err) + return nil, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs-vault secret", err) } return VaultReadSecretResult{SecretName: name, Fields: fields}, nil } @@ -1049,7 +1049,7 @@ func (s Service) drive9ListVaultSecrets(ctx context.Context, opts VaultListSecre } var out VaultListSecretsResult if err := json.Unmarshal(result.Stdout, &out); err != nil { - return VaultListSecretsResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs-vault list response", err) + return VaultListSecretsResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs-vault list response", err) } return out, nil } @@ -1087,7 +1087,7 @@ func (s Service) drive9CreateVaultGrant(ctx context.Context, opts VaultCreateGra } var out VaultTokenResult if err := json.Unmarshal(result.Stdout, &out); err != nil { - return VaultTokenResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs-vault grant response", err) + return VaultTokenResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs-vault grant response", err) } return out, nil } @@ -1115,7 +1115,7 @@ func (s Service) drive9ListVaultAuditEvents(ctx context.Context, opts VaultAudit } var out VaultAuditResult if err := json.Unmarshal(result.Stdout, &out); err != nil { - return VaultAuditResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode tdc fs-vault audit response", err) + return VaultAuditResult{}, apperr.Wrap("fs.companion_decode", "runtime", 1, "decode ti fs-vault audit response", err) } return out, nil } @@ -1135,7 +1135,7 @@ func (s Service) drive9RunWithVaultSecret(ctx context.Context, opts VaultRunWith func (s Service) drive9MountVault(ctx context.Context, opts VaultMountOptions) (MountResult, error) { if strings.TrimSpace(opts.VaultToken) == "" { - return MountResult{}, apperr.New("vault.missing_token", "usage", 2, "tdc fs-vault mount-vault requires --vault-token or TDC_VAULT_TOKEN; use create-grant to mint a delegated vault token first") + return MountResult{}, apperr.New("vault.missing_token", "usage", 2, "ti fs-vault mount-vault requires --vault-token or TI_VAULT_TOKEN; use create-grant to mint a delegated vault token first") } args := []string{"mount", "vault"} if opts.Foreground { @@ -1298,7 +1298,7 @@ func (s Service) drive9UnmountFileSystem(ctx context.Context, opts UnmountFileSy return UnmountResult{}, homeErr } if removeErr := mountlocator.Remove(homeDir, opts.MountPath); removeErr != nil { - return UnmountResult{}, apperr.Wrap("fs.remove_mount_locator", "runtime", 1, "remove stale tdc fs mount locator", removeErr) + return UnmountResult{}, apperr.Wrap("fs.remove_mount_locator", "runtime", 1, "remove stale ti fs mount locator", removeErr) } return UnmountResult{Status: "absent", MountPath: opts.MountPath}, nil } @@ -1309,7 +1309,7 @@ func (s Service) drive9UnmountFileSystem(ctx context.Context, opts UnmountFileSy return UnmountResult{}, err } if err := mountlocator.Remove(homeDir, opts.MountPath); err != nil { - return UnmountResult{}, apperr.Wrap("fs.remove_mount_locator", "runtime", 1, "remove tdc fs mount locator", err) + return UnmountResult{}, apperr.Wrap("fs.remove_mount_locator", "runtime", 1, "remove ti fs mount locator", err) } return UnmountResult{Status: "unmounted", MountPath: opts.MountPath}, nil } @@ -1328,10 +1328,10 @@ func (s Service) writeDrive9MountLocator(profile *config.Profile, mountPath, kin } locator, err := mountlocator.New(profile.Name, profile.FSResourceName, profile.FSPlacementRegionCode, companionHome, mountPath, kind) if err != nil { - return apperr.Wrap("fs.write_mount_locator", "runtime", 1, "construct tdc fs mount locator", err) + return apperr.Wrap("fs.write_mount_locator", "runtime", 1, "construct ti fs mount locator", err) } if _, err := mountlocator.Write(homeDir, locator); err != nil { - return apperr.Wrap("fs.write_mount_locator", "runtime", 1, "write tdc fs mount locator", err) + return apperr.Wrap("fs.write_mount_locator", "runtime", 1, "write ti fs mount locator", err) } return nil } @@ -1344,20 +1344,20 @@ func (s Service) drive9MountLocatorProfile(base *config.Profile, mountPath strin locator, _, err := mountlocator.Read(homeDir, mountPath) if err != nil { if os.IsNotExist(err) { - return nil, apperr.New("fs.mount_locator_not_found", "runtime", 1, fmt.Sprintf("no tdc fs mount locator found for %q", mountPath)) + return nil, apperr.New("fs.mount_locator_not_found", "runtime", 1, fmt.Sprintf("no ti fs mount locator found for %q", mountPath)) } - return nil, apperr.Wrap("fs.read_mount_locator", "runtime", 1, fmt.Sprintf("read tdc fs mount locator for %q", mountPath), err) + return nil, apperr.Wrap("fs.read_mount_locator", "runtime", 1, fmt.Sprintf("read ti fs mount locator for %q", mountPath), err) } expectedHome, err := fscred.CompanionHome(homeDir, locator.Profile, locator.FileSystemName) if err != nil { return nil, err } if filepath.Clean(expectedHome) != filepath.Clean(locator.CompanionHome) { - return nil, apperr.New("fs.mount_locator_invalid", "config", 2, "tdc fs mount locator companion home does not match its profile and file system") + return nil, apperr.New("fs.mount_locator_invalid", "config", 2, "ti fs mount locator companion home does not match its profile and file system") } placement, err := region.ParsePlacementCode(locator.RegionCode) if err != nil { - return nil, apperr.Wrap("fs.mount_locator_invalid", "config", 2, "tdc fs mount locator has an invalid region", err) + return nil, apperr.Wrap("fs.mount_locator_invalid", "config", 2, "ti fs mount locator has an invalid region", err) } profile := config.Profile{HomeDir: homeDir} if base != nil { diff --git a/internal/fs/drive9_companion_test.go b/internal/fs/drive9_companion_test.go index a0bca01..340155a 100644 --- a/internal/fs/drive9_companion_test.go +++ b/internal/fs/drive9_companion_test.go @@ -13,13 +13,13 @@ import ( "testing" "time" - "github.com/tidbcloud/tdc/internal/api/endpoints" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/config/store" - "github.com/tidbcloud/tdc/internal/dryrun" - "github.com/tidbcloud/tdc/internal/fs/fscred" - "github.com/tidbcloud/tdc/internal/fs/mountlocator" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/config/store" + "github.com/tidbcloud/ti-cli/internal/dryrun" + "github.com/tidbcloud/ti-cli/internal/fs/fscred" + "github.com/tidbcloud/ti-cli/internal/fs/mountlocator" ) type fakeDrive9Call struct { @@ -30,7 +30,7 @@ type fakeDrive9Call struct { func TestDrive9CreateFileSystemStoresRegistryCredentialsAndUsesCanonicalRegion(t *testing.T) { home := t.TempDir() companion, recordPath := buildFakeDrive9(t) - t.Setenv("TDC_FAKE_DRIVE9_RECORD", recordPath) + t.Setenv("TI_FAKE_DRIVE9_RECORD", recordPath) t.Setenv("DRIVE9_API_KEY", "ambient-drive9-key") profile := testProfile() @@ -93,8 +93,8 @@ func TestDrive9CreateFileSystemStoresRegistryCredentialsAndUsesCanonicalRegion(t func TestDrive9CreateFileSystemWaitsUntilReady(t *testing.T) { home := t.TempDir() companion, recordPath := buildFakeDrive9(t) - t.Setenv("TDC_FAKE_DRIVE9_RECORD", recordPath) - t.Setenv("TDC_FAKE_DRIVE9_STAT_FAILURE_SEQUENCE", filepath.Join(t.TempDir(), "stat-attempted")) + t.Setenv("TI_FAKE_DRIVE9_RECORD", recordPath) + t.Setenv("TI_FAKE_DRIVE9_STAT_FAILURE_SEQUENCE", filepath.Join(t.TempDir(), "stat-attempted")) service := testCompanionService(home, companion) service.FSReadyWaitTimeout = time.Second @@ -127,7 +127,7 @@ func TestDrive9CreateFileSystemWaitsUntilReady(t *testing.T) { func TestDrive9CreateFileSystemReadyTimeoutPreservesCredentials(t *testing.T) { home := t.TempDir() companion, _ := buildFakeDrive9(t) - t.Setenv("TDC_FAKE_DRIVE9_STAT_ALWAYS_FAIL", "1") + t.Setenv("TI_FAKE_DRIVE9_STAT_ALWAYS_FAIL", "1") service := testCompanionService(home, companion) service.FSReadyWaitTimeout = 10 * time.Millisecond @@ -149,15 +149,15 @@ func TestDrive9CreateFileSystemReadyTimeoutPreservesCredentials(t *testing.T) { func TestDrive9CreateFileSystemFromEnvironmentProfileStoresDefaultProfile(t *testing.T) { home := t.TempDir() companion, recordPath := buildFakeDrive9(t) - t.Setenv("TDC_FAKE_DRIVE9_RECORD", recordPath) + t.Setenv("TI_FAKE_DRIVE9_RECORD", recordPath) profile := &config.Profile{ Name: config.DefaultProfile, Source: "env", PlacementRegionCode: "aws-us-east-1", CloudProvider: "aws", RegionCode: "us-east-1", - TDCPublicKey: "env-public", - TDCPrivateKey: "env-private", + TiDBCloudPublicKey: "env-public", + TiDBCloudPrivateKey: "env-private", } if _, err := testCompanionService(home, companion).CreateFileSystem(context.Background(), CreateFileSystemOptions{ @@ -192,7 +192,7 @@ func TestDrive9CreateFileSystemFromEnvironmentProfileStoresDefaultProfile(t *tes func TestDrive9CreateSecondFileSystemUsesIndependentCompanionHome(t *testing.T) { home := t.TempDir() companion, recordPath := buildFakeDrive9(t) - t.Setenv("TDC_FAKE_DRIVE9_RECORD", recordPath) + t.Setenv("TI_FAKE_DRIVE9_RECORD", recordPath) profile := testProfile() service := testCompanionService(home, companion) if _, err := service.CreateFileSystem(context.Background(), CreateFileSystemOptions{Profile: profile, FileSystemName: "workspace"}); err != nil { @@ -236,9 +236,9 @@ func TestDrive9CreateSecondFileSystemUsesIndependentCompanionHome(t *testing.T) func TestDrive9DeleteFileSystemDeletesOnlySelectedRegistryResource(t *testing.T) { home := t.TempDir() companion, recordPath := buildFakeDrive9(t) - t.Setenv("TDC_FAKE_DRIVE9_RECORD", recordPath) + t.Setenv("TI_FAKE_DRIVE9_RECORD", recordPath) profile := dataProfile() - if err := store.WriteProfile(home, profile.Name, store.ConfigProfile{RegionCode: profile.PlacementRegionCode}, store.CredentialsProfile{TDCPublicKey: profile.TDCPublicKey, TDCPrivateKey: profile.TDCPrivateKey}); err != nil { + if err := store.WriteProfile(home, profile.Name, store.ConfigProfile{RegionCode: profile.PlacementRegionCode}, store.CredentialsProfile{TiDBCloudPublicKey: profile.TiDBCloudPublicKey, TiDBCloudPrivateKey: profile.TiDBCloudPrivateKey}); err != nil { t.Fatal(err) } if err := fscred.Store(home, profile, "workspace", "tenant-1", "aws", "aws-us-east-1", "fs-secret"); err != nil { @@ -276,7 +276,7 @@ func TestDrive9DeleteFileSystemDeletesOnlySelectedRegistryResource(t *testing.T) if err != nil { t.Fatalf("ReadCredentials failed: %v", err) } - if got := credentialsDoc["stage"]; got.FSAPIKey != "" || got.TDCPublicKey != "public" { + if got := credentialsDoc["stage"]; got.FSAPIKey != "" || got.TiDBCloudPublicKey != "public" { t.Fatalf("unexpected credentials after delete: %#v", got) } if _, err := fscred.Get(home, "stage", "workspace"); apperr.CodeFor(err) != "fs.resource_not_found" { @@ -289,11 +289,11 @@ func TestDrive9DeleteFileSystemDeletesOnlySelectedRegistryResource(t *testing.T) func TestDrive9CheckFileSystemUsesSelectedResource(t *testing.T) { companion, recordPath := buildFakeDrive9(t) - t.Setenv("TDC_FAKE_DRIVE9_RECORD", recordPath) + t.Setenv("TI_FAKE_DRIVE9_RECORD", recordPath) profile := dataProfile() - profile.TDCPublicKey = "" - profile.TDCPrivateKey = "" + profile.TiDBCloudPublicKey = "" + profile.TiDBCloudPrivateKey = "" result, err := testCompanionService(t.TempDir(), companion).CheckFileSystem(context.Background(), CheckFileSystemOptions{Profile: profile}) if err != nil { t.Fatalf("CheckFileSystem failed: %v", err) @@ -321,7 +321,7 @@ func TestDrive9CheckFileSystemUsesSelectedResource(t *testing.T) { func TestDrive9DataPlaneCommandsTranslateToCompanion(t *testing.T) { home := t.TempDir() companion, recordPath := buildFakeDrive9(t) - t.Setenv("TDC_FAKE_DRIVE9_RECORD", recordPath) + t.Setenv("TI_FAKE_DRIVE9_RECORD", recordPath) localFile := filepath.Join(t.TempDir(), "README.md") if err := os.WriteFile(localFile, []byte("hello"), 0o644); err != nil { t.Fatal(err) @@ -368,7 +368,7 @@ func TestDrive9DataPlaneCommandsTranslateToCompanion(t *testing.T) { func TestDrive9CopyDoesNotTreatNotFoundAfterTransientFailureAsSuccess(t *testing.T) { companion, _ := buildFakeDrive9(t) sequencePath := filepath.Join(t.TempDir(), "copy-attempted") - t.Setenv("TDC_FAKE_DRIVE9_CP_FAILURE_SEQUENCE", sequencePath) + t.Setenv("TI_FAKE_DRIVE9_CP_FAILURE_SEQUENCE", sequencePath) _, err := testCompanionService(t.TempDir(), companion).CopyFile(context.Background(), CopyFileOptions{ Profile: dataProfile(), @@ -395,8 +395,8 @@ func TestDrive9CopyDoesNotRetryNonReplayableStreamsOrAppend(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { companion, recordPath := buildFakeDrive9(t) - t.Setenv("TDC_FAKE_DRIVE9_RECORD", recordPath) - t.Setenv("TDC_FAKE_DRIVE9_CP_FAILURE_SEQUENCE", filepath.Join(t.TempDir(), "copy-attempted")) + t.Setenv("TI_FAKE_DRIVE9_RECORD", recordPath) + t.Setenv("TI_FAKE_DRIVE9_CP_FAILURE_SEQUENCE", filepath.Join(t.TempDir(), "copy-attempted")) tc.opts.Profile = dataProfile() if _, err := testCompanionService(t.TempDir(), companion).CopyFile(context.Background(), tc.opts); err == nil { @@ -418,21 +418,21 @@ func TestDrive9CopyDoesNotRetryNonReplayableStreamsOrAppend(t *testing.T) { func TestDrive9MissingCompanionIsActionable(t *testing.T) { t.Setenv("PATH", t.TempDir()) - _, err := Service{CompanionPath: filepath.Join(t.TempDir(), "missing-tdc-drive9")}.ReadFile(context.Background(), ReadFileOptions{ + _, err := Service{CompanionPath: filepath.Join(t.TempDir(), "missing-ti-drive9")}.ReadFile(context.Background(), ReadFileOptions{ Profile: dataProfile(), Path: "/workspace/README.md", }) if err == nil { t.Fatal("expected missing companion error") } - if message := apperr.MessageFor(err); !strings.Contains(message, "tdc fs requires the Drive9 companion binary") { + if message := apperr.MessageFor(err); !strings.Contains(message, "ti fs requires the Drive9 companion binary") { t.Fatalf("unexpected error: %q", message) } } func TestDrive9EndpointFailurePreventsCompanionExecution(t *testing.T) { companion, recordPath := buildFakeDrive9(t) - t.Setenv("TDC_FAKE_DRIVE9_RECORD", recordPath) + t.Setenv("TI_FAKE_DRIVE9_RECORD", recordPath) resolver := endpoints.Resolver{FSManifest: &endpoints.FSRegionManifest{Regions: []endpoints.FSRegionManifestEntry{ { RegionCode: "aws-us-west-2", @@ -459,7 +459,7 @@ func TestDrive9EndpointFailurePreventsCompanionExecution(t *testing.T) { func TestDrive9MountLocatorRoutesDrainAndUnmountWithoutCredentials(t *testing.T) { home := t.TempDir() companion, recordPath := buildFakeDrive9(t) - t.Setenv("TDC_FAKE_DRIVE9_RECORD", recordPath) + t.Setenv("TI_FAKE_DRIVE9_RECORD", recordPath) service := testCompanionService(home, companion) mountPath := filepath.Join(t.TempDir(), "workspace") profile := dataProfile() @@ -517,7 +517,7 @@ func TestDrive9MountLocatorRoutesDrainAndUnmountWithoutCredentials(t *testing.T) func TestDrive9FailedUnmountPreservesMountLocator(t *testing.T) { home := t.TempDir() companion, recordPath := buildFakeDrive9(t) - t.Setenv("TDC_FAKE_DRIVE9_RECORD", recordPath) + t.Setenv("TI_FAKE_DRIVE9_RECORD", recordPath) service := testCompanionService(home, companion) mountPath := filepath.Join(t.TempDir(), "workspace") if _, err := service.MountFileSystem(context.Background(), MountFileSystemOptions{ @@ -529,7 +529,7 @@ func TestDrive9FailedUnmountPreservesMountLocator(t *testing.T) { t.Fatalf("MountFileSystem failed: %v", err) } - service.CompanionPath = filepath.Join(t.TempDir(), "missing-tdc-drive9") + service.CompanionPath = filepath.Join(t.TempDir(), "missing-ti-drive9") if _, err := service.UnmountFileSystem(context.Background(), UnmountFileSystemOptions{ Profile: &config.Profile{Name: "default", HomeDir: home}, MountPath: mountPath, @@ -544,7 +544,7 @@ func TestDrive9FailedUnmountPreservesMountLocator(t *testing.T) { func TestDryRunCreateFileSystemUsesRedactedProvisionShape(t *testing.T) { profile := testProfile() - result, err := Service{Resolver: supportedFSManifestResolver("https://fs.test")}.DryRunCreateFileSystem(context.Background(), "tdc fs create-file-system", CreateFileSystemOptions{ + result, err := Service{Resolver: supportedFSManifestResolver("https://fs.test")}.DryRunCreateFileSystem(context.Background(), "ti fs create-file-system", CreateFileSystemOptions{ Profile: profile, FileSystemName: "workspace", WaitUntilReady: true, @@ -583,7 +583,7 @@ func TestDryRunDeleteFileSystemReportsRegistryFiles(t *testing.T) { if err := fscred.Store(home, profile, "workspace", "tenant-1", "aws", "aws-us-east-1", "fs-secret"); err != nil { t.Fatal(err) } - result, err := (Service{HomeDir: home, Resolver: supportedFSManifestResolver("https://fs.test")}).DryRunDeleteFileSystem(context.Background(), "tdc fs delete-file-system", DeleteFileSystemOptions{ + result, err := (Service{HomeDir: home, Resolver: supportedFSManifestResolver("https://fs.test")}).DryRunDeleteFileSystem(context.Background(), "ti fs delete-file-system", DeleteFileSystemOptions{ Profile: profile, FileSystemName: "workspace", }) @@ -667,8 +667,8 @@ func main() { _ = json.NewEncoder(os.Stdout).Encode(map[string]string{"status": "deleting"}) case len(args) >= 2 && args[0] == "fs" && args[1] == "cat": fmt.Fprint(os.Stdout, "file bytes") - case len(args) >= 2 && args[0] == "fs" && args[1] == "cp" && os.Getenv("TDC_FAKE_DRIVE9_CP_FAILURE_SEQUENCE") != "": - sequencePath := os.Getenv("TDC_FAKE_DRIVE9_CP_FAILURE_SEQUENCE") + case len(args) >= 2 && args[0] == "fs" && args[1] == "cp" && os.Getenv("TI_FAKE_DRIVE9_CP_FAILURE_SEQUENCE") != "": + sequencePath := os.Getenv("TI_FAKE_DRIVE9_CP_FAILURE_SEQUENCE") if _, err := os.Stat(sequencePath); os.IsNotExist(err) { _ = os.WriteFile(sequencePath, []byte("attempted"), 0600) fmt.Fprintln(os.Stderr, "fs cp: unexpected EOF") @@ -677,11 +677,11 @@ func main() { fmt.Fprintln(os.Stderr, "fs cp: remote resource not found") os.Exit(1) case len(args) >= 2 && args[0] == "fs" && args[1] == "stat": - if os.Getenv("TDC_FAKE_DRIVE9_STAT_ALWAYS_FAIL") == "1" { + if os.Getenv("TI_FAKE_DRIVE9_STAT_ALWAYS_FAIL") == "1" { fmt.Fprintln(os.Stderr, "fs stat: storage backend unavailable; resource is still provisioning") os.Exit(1) } - if sequencePath := os.Getenv("TDC_FAKE_DRIVE9_STAT_FAILURE_SEQUENCE"); sequencePath != "" { + if sequencePath := os.Getenv("TI_FAKE_DRIVE9_STAT_FAILURE_SEQUENCE"); sequencePath != "" { if _, err := os.Stat(sequencePath); os.IsNotExist(err) { _ = os.WriteFile(sequencePath, []byte("attempted"), 0600) fmt.Fprintln(os.Stderr, "fs stat: storage backend unavailable; resource is still provisioning") @@ -695,7 +695,7 @@ func main() { } func record(args []string) { - path := os.Getenv("TDC_FAKE_DRIVE9_RECORD") + path := os.Getenv("TI_FAKE_DRIVE9_RECORD") if path == "" { return } @@ -716,7 +716,7 @@ func record(args []string) { if err := os.WriteFile(sourcePath, []byte(source), 0o600); err != nil { t.Fatalf("write fake companion source: %v", err) } - binPath = filepath.Join(dir, "tdc-drive9") + binPath = filepath.Join(dir, "ti-drive9") if runtime.GOOS == "windows" { binPath += ".exe" } @@ -788,8 +788,8 @@ func testProfile() *config.Profile { PlacementRegionCode: "aws-us-east-1", CloudProvider: "aws", RegionCode: "us-east-1", - TDCPublicKey: "public", - TDCPrivateKey: "private", + TiDBCloudPublicKey: "public", + TiDBCloudPrivateKey: "private", } } @@ -828,6 +828,29 @@ func supportedFSManifestResolver(baseURL string) endpoints.Resolver { } } +func TestScrubTICredEnvRemovesCanonicalLegacyAndCompanionSecrets(t *testing.T) { + base := []string{ + "SAFE=value", + "TIDB_CLOUD_PUBLIC_KEY=canonical-public", + "TIDB_CLOUD_PRIVATE_KEY=canonical-private", + "TI_FS_TOKEN=canonical-fs", + "TI_VAULT_TOKEN=canonical-vault", + "TI_FS_API_KEY=internal-fs", + "TDC_PUBLIC_KEY=legacy-public", + "TDC_PRIVATE_KEY=legacy-private", + "TDC_FS_TOKEN=legacy-fs", + "TDC_VAULT_TOKEN=legacy-vault", + "DRIVE9_PUBLIC_KEY=companion-public", + "DRIVE9_PRIVATE_KEY=companion-private", + "DRIVE9_API_KEY=companion-fs", + "DRIVE9_VAULT_TOKEN=companion-vault", + } + got := scrubTICredEnv(base) + if len(got) != 1 || got[0] != "SAFE=value" { + t.Fatalf("credential environment was not scrubbed: %#v", got) + } +} + func hasCheck(checks []Check, name, status string) bool { for _, check := range checks { if check.Name == name && check.Status == status { diff --git a/internal/fs/fscred/fscred.go b/internal/fs/fscred/fscred.go index 920bdd5..d3ea5c7 100644 --- a/internal/fs/fscred/fscred.go +++ b/internal/fs/fscred/fscred.go @@ -11,10 +11,11 @@ import ( "time" "github.com/pelletier/go-toml/v2" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/config/region" - "github.com/tidbcloud/tdc/internal/config/store" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/config/envcompat" + "github.com/tidbcloud/ti-cli/internal/config/region" + "github.com/tidbcloud/ti-cli/internal/config/store" ) const ( @@ -81,10 +82,10 @@ func Store(homeDir string, profile *config.Profile, resourceName, tenantID, clou return apperr.New("fs.missing_file_system_name", "usage", 2, "--file-system-name is required") } if strings.TrimSpace(apiKey) == "" { - return apperr.New("fs.missing_api_key", "api", 1, "tdc fs provision response did not include an api_key") + return apperr.New("fs.missing_api_key", "api", 1, "ti fs provision response did not include an api_key") } if strings.TrimSpace(tenantID) == "" { - return apperr.New("fs.missing_tenant_id", "api", 1, "tdc fs provision response did not include a tenant_id") + return apperr.New("fs.missing_tenant_id", "api", 1, "ti fs provision response did not include a tenant_id") } canonicalCode, resolvedProvider, err := canonicalPlacement(profile, cloudProvider, regionCode) if err != nil { @@ -92,7 +93,7 @@ func Store(homeDir string, profile *config.Profile, resourceName, tenantID, clou } if existing, err := Get(homeDir, profile.Name, resourceName); err == nil { if existing.TenantID != strings.TrimSpace(tenantID) || existing.APIKey != strings.TrimSpace(apiKey) { - return resourceError("fs.resource_name_conflict", profile.Name, resourceName, "a different local tdc fs resource already uses this name") + return resourceError("fs.resource_name_conflict", profile.Name, resourceName, "a different local ti fs resource already uses this name") } return nil } else if apperr.CodeFor(err) != "fs.resource_not_found" { @@ -110,7 +111,7 @@ func Store(homeDir string, profile *config.Profile, resourceName, tenantID, clou return err } if err := ensureRegistryDirs(homeDir, profile.Name, resourceName); err != nil { - return fmt.Errorf("create tdc fs resource directory: %w", err) + return fmt.Errorf("create ti fs resource directory: %w", err) } if err := writeTOML(filepath.Join(dir, configFileName), resource, 0o644); err != nil { return err @@ -129,7 +130,7 @@ func List(homeDir, profileName string) ([]Resource, error) { return []Resource{}, nil } if err != nil { - return nil, fmt.Errorf("list tdc fs resources for profile %q: %w", normalizedProfile(profileName), err) + return nil, fmt.Errorf("list ti fs resources for profile %q: %w", normalizedProfile(profileName), err) } resources := make([]Resource, 0, len(entries)) for _, entry := range entries { @@ -161,7 +162,7 @@ func Get(homeDir, profileName, resourceName string) (Resource, error) { } resource, err := readResourceDir(dir, profileName, resourceName) if errors.Is(err, os.ErrNotExist) { - return Resource{}, resourceError("fs.resource_not_found", profileName, resourceName, "tdc fs resource is not configured") + return Resource{}, resourceError("fs.resource_not_found", profileName, resourceName, "ti fs resource is not configured") } return resource, err } @@ -183,7 +184,11 @@ func ResolveAuthenticated(homeDir string, profile *config.Profile, opts ResolveA return nil, Resource{}, apperr.New("fs.empty_file_system_name", "usage", 2, "--file-system-name cannot be empty") } if name == "" { - name = strings.TrimSpace(fsEnvValue(opts.Env, "TDC_FS_FILE_SYSTEM_NAME")) + value, err := fsEnvValue(opts.Env, "TI_FS_FILE_SYSTEM_NAME") + if err != nil { + return nil, Resource{}, err + } + name = strings.TrimSpace(value) } if name == "" { return nil, Resource{}, missingFileSystemName() @@ -202,7 +207,11 @@ func ResolveAuthenticated(homeDir string, profile *config.Profile, opts ResolveA return nil, Resource{}, apperr.New("fs.empty_token", "usage", 2, "--fs-token cannot be empty") } if token == "" { - token = strings.TrimSpace(fsEnvValue(opts.Env, "TDC_FS_TOKEN")) + value, err := fsEnvValue(opts.Env, "TI_FS_TOKEN") + if err != nil { + return nil, Resource{}, err + } + token = strings.TrimSpace(value) } resource, resourceErr := Get(homeDir, profile.Name, name) @@ -228,7 +237,7 @@ func ResolveAuthenticated(homeDir string, profile *config.Profile, opts ResolveA token = strings.TrimSpace(resource.APIKey) } if opts.TokenRequired && token == "" { - return nil, Resource{}, apperr.New("fs.missing_token", "authentication", 3, "authentication required: missing FS token; pass --fs-token, set TDC_FS_TOKEN, or select a locally registered file system with credentials") + return nil, Resource{}, apperr.New("fs.missing_token", "authentication", 3, "authentication required: missing FS token; pass --fs-token, set TI_FS_TOKEN, or select a locally registered file system with credentials") } placementCode := strings.TrimSpace(opts.RegionOverride) @@ -239,7 +248,7 @@ func ResolveAuthenticated(homeDir string, profile *config.Profile, opts ResolveA placementCode = strings.TrimSpace(profile.PlacementRegionCode) } if placementCode == "" { - return nil, Resource{}, apperr.New("fs.missing_region", "config", 2, "tdc fs region is required; pass --region, set TDC_REGION_CODE, or configure region_code for the selected file system or profile") + return nil, Resource{}, apperr.New("fs.missing_region", "config", 2, "ti fs region is required; pass --region, set TI_REGION_CODE, or configure region_code for the selected file system or profile") } placement, err := region.ParsePlacementCode(placementCode) if err != nil { @@ -269,11 +278,11 @@ func resolve(homeDir string, profile *config.Profile, selector string, selectorE return nil, Resource{}, apperr.New("fs.empty_file_system_name", "usage", 2, "--file-system-name cannot be empty") } if name == "" { - if env != nil { - name = strings.TrimSpace(env["TDC_FS_FILE_SYSTEM_NAME"]) - } else { - name = strings.TrimSpace(os.Getenv("TDC_FS_FILE_SYSTEM_NAME")) + value, err := fsEnvValue(env, "TI_FS_FILE_SYSTEM_NAME") + if err != nil { + return nil, Resource{}, err } + name = strings.TrimSpace(value) } if name == "" { return nil, Resource{}, missingFileSystemName() @@ -311,14 +320,12 @@ func resolve(homeDir string, profile *config.Profile, selector string, selectorE } func missingFileSystemName() error { - return apperr.New("fs.missing_file_system_name", "usage", 2, "file system name is required; pass --file-system-name or set TDC_FS_FILE_SYSTEM_NAME") + return apperr.New("fs.missing_file_system_name", "usage", 2, "file system name is required; pass --file-system-name or set TI_FS_FILE_SYSTEM_NAME") } -func fsEnvValue(env map[string]string, key string) string { - if env != nil { - return env[key] - } - return os.Getenv(key) +func fsEnvValue(env map[string]string, key string) (string, error) { + value, _, _, err := envcompat.ResolveNames(env, key, envcompat.LegacyNameFor(key)) + return value, err } func Delete(homeDir string, profile *config.Profile, resourceName string) error { @@ -333,7 +340,7 @@ func Delete(homeDir string, profile *config.Profile, resourceName string) error return err } if err := os.RemoveAll(dir); err != nil { - return fmt.Errorf("delete local tdc fs resource %q: %w", resourceName, err) + return fmt.Errorf("delete local ti fs resource %q: %w", resourceName, err) } return nil } @@ -351,7 +358,7 @@ func MigrateLegacy(homeDir string, profile *config.Profile) error { return nil } if !legacyComplete(profile) { - return resourceError("fs.resource_credentials_incomplete", profile.Name, profile.FSResourceName, "legacy flat tdc fs credentials are incomplete; recreate the resource") + return resourceError("fs.resource_credentials_incomplete", profile.Name, profile.FSResourceName, "legacy flat ti fs credentials are incomplete; recreate the resource") } existing, err := Get(homeDir, profile.Name, profile.FSResourceName) if err == nil { @@ -367,7 +374,7 @@ func MigrateLegacy(homeDir string, profile *config.Profile) error { } } if err := store.ClearFSResource(homeDir, profile.Name); err != nil { - return apperr.Wrap("fs.resource_migration_failed", "config", 1, "clear migrated flat tdc fs credentials", err) + return apperr.Wrap("fs.resource_migration_failed", "config", 1, "clear migrated flat ti fs credentials", err) } profile.FSResourceName = "" profile.FSTenantID = "" @@ -388,7 +395,7 @@ func validateLegacy(profile *config.Profile) error { return nil } if !legacyComplete(profile) { - return resourceError("fs.resource_credentials_incomplete", profile.Name, profile.FSResourceName, "legacy flat tdc fs credentials are incomplete; recreate the resource") + return resourceError("fs.resource_credentials_incomplete", profile.Name, profile.FSResourceName, "legacy flat ti fs credentials are incomplete; recreate the resource") } return nil } @@ -419,9 +426,9 @@ func CompanionHome(homeDir, profileName, resourceName string) (string, error) { profileKey := encodeKey(normalizedProfile(profileName)) resourceKey := encodeKey(strings.TrimSpace(resourceName)) if resourceKey == "" { - return "", resourceError("fs.resource_not_configured", profileName, resourceName, "tdc fs resource is required for the companion runtime") + return "", resourceError("fs.resource_not_configured", profileName, resourceName, "ti fs resource is required for the companion runtime") } - return filepath.Join(homeDir, store.TDCDirName, "drive9-home", profileKey, resourceKey), nil + return filepath.Join(homeDir, store.TIDirName, "drive9-home", profileKey, resourceKey), nil } func Paths(homeDir, profileName, resourceName string) (RegistryPaths, error) { @@ -477,11 +484,11 @@ func resourceDir(homeDir, profileName, resourceName string) (string, error) { } func profileDir(homeDir, profileName string) string { - return filepath.Join(homeDir, store.TDCDirName, resourcesDirName, encodeKey(normalizedProfile(profileName))) + return filepath.Join(homeDir, store.TIDirName, resourcesDirName, encodeKey(normalizedProfile(profileName))) } func ensureRegistryDirs(homeDir, profileName, resourceName string) error { - root := filepath.Join(homeDir, store.TDCDirName, resourcesDirName) + root := filepath.Join(homeDir, store.TIDirName, resourcesDirName) profilePath := profileDir(homeDir, profileName) resourcePath, err := resourceDir(homeDir, profileName, resourceName) if err != nil { diff --git a/internal/fs/fscred/fscred_test.go b/internal/fs/fscred/fscred_test.go index ce30c8d..45d9024 100644 --- a/internal/fs/fscred/fscred_test.go +++ b/internal/fs/fscred/fscred_test.go @@ -7,9 +7,9 @@ import ( "strings" "testing" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/config/store" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/config/store" ) func TestRegistryStoreListAndFileModes(t *testing.T) { @@ -23,7 +23,7 @@ func TestRegistryStoreListAndFileModes(t *testing.T) { t.Fatal(err) } for _, path := range []string{ - filepath.Join(home, store.TDCDirName, resourcesDirName), + filepath.Join(home, store.TIDirName, resourcesDirName), profileDir(home, profile.Name), dir, } { @@ -48,7 +48,7 @@ func TestRegistryStoreListAndFileModes(t *testing.T) { t.Fatalf("registry store wrote main config: %v", err) } if runtime.GOOS != "windows" { - assertMode(t, filepath.Join(home, store.TDCDirName, resourcesDirName), 0o700) + assertMode(t, filepath.Join(home, store.TIDirName, resourcesDirName), 0o700) assertMode(t, profileDir(home, profile.Name), 0o700) assertMode(t, dir, 0o700) assertMode(t, filepath.Join(dir, configFileName), 0o644) @@ -75,11 +75,11 @@ func TestResolveSelectionPrecedence(t *testing.T) { if err := Store(home, profile, "scratch", "tenant-2", "aws", "aws-us-west-2", "key-2"); err != nil { t.Fatal(err) } - selected, _, err := Resolve(home, profile, "scratch", true, map[string]string{"TDC_FS_FILE_SYSTEM_NAME": "workspace"}) + selected, _, err := Resolve(home, profile, "scratch", true, map[string]string{"TI_FS_FILE_SYSTEM_NAME": "workspace"}) if err != nil || selected.FSResourceName != "scratch" || selected.FSAPIKey != "key-2" || selected.FSRegionCode != "us-west-2" { t.Fatalf("flag selection failed: selected=%#v err=%v", selected, err) } - selected, _, err = Resolve(home, profile, "", false, map[string]string{"TDC_FS_FILE_SYSTEM_NAME": "scratch"}) + selected, _, err = Resolve(home, profile, "", false, map[string]string{"TI_FS_FILE_SYSTEM_NAME": "scratch"}) if err != nil || selected.FSResourceName != "scratch" { t.Fatalf("environment selection failed: selected=%#v err=%v", selected, err) } @@ -117,9 +117,9 @@ func TestResolveAuthenticatedConfigurationFree(t *testing.T) { selected, resource, err := ResolveAuthenticated(home, profile, ResolveAuthOptions{ TokenRequired: true, Env: map[string]string{ - "TDC_FS_FILE_SYSTEM_NAME": "workspace", - "TDC_FS_TOKEN": "drive9-secret", - "TDC_REGION_CODE": "aws-us-east-1", + "TI_FS_FILE_SYSTEM_NAME": "workspace", + "TI_FS_TOKEN": "drive9-secret", + "TI_REGION_CODE": "aws-us-east-1", }, RegionOverride: "aws-us-east-1", }) @@ -132,7 +132,7 @@ func TestResolveAuthenticatedConfigurationFree(t *testing.T) { if resource.TenantID != "" || !resource.HasAPIKey { t.Fatalf("unexpected ephemeral resource: %#v", resource) } - if _, err := os.Stat(filepath.Join(home, store.TDCDirName)); !os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(home, store.TIDirName)); !os.IsNotExist(err) { t.Fatalf("configuration-free resolution wrote local state: %v", err) } } @@ -153,8 +153,8 @@ func TestResolveAuthenticatedPrecedenceAndMixedSources(t *testing.T) { RegionOverride: "aws-ap-southeast-1", TokenRequired: true, Env: map[string]string{ - "TDC_FS_TOKEN": "env-token", - "TDC_REGION_CODE": "aws-us-east-1", + "TI_FS_TOKEN": "env-token", + "TI_REGION_CODE": "aws-us-east-1", }, }) if err != nil { @@ -167,8 +167,8 @@ func TestResolveAuthenticatedPrecedenceAndMixedSources(t *testing.T) { selected, _, err = ResolveAuthenticated(home, profile, ResolveAuthOptions{ TokenRequired: true, Env: map[string]string{ - "TDC_FS_FILE_SYSTEM_NAME": "workspace", - "TDC_FS_TOKEN": "env-token", + "TI_FS_FILE_SYSTEM_NAME": "workspace", + "TI_FS_TOKEN": "env-token", }, }) if err != nil { @@ -188,7 +188,7 @@ func TestResolveAuthenticatedErrorsAndDelegatedTokenMode(t *testing.T) { }{ { name: "missing name", - opts: ResolveAuthOptions{TokenRequired: true, RegionOverride: "aws-us-east-1", Env: map[string]string{"TDC_FS_TOKEN": "token"}}, + opts: ResolveAuthOptions{TokenRequired: true, RegionOverride: "aws-us-east-1", Env: map[string]string{"TI_FS_TOKEN": "token"}}, code: "fs.missing_file_system_name", }, { @@ -203,7 +203,7 @@ func TestResolveAuthenticatedErrorsAndDelegatedTokenMode(t *testing.T) { }, { name: "empty selector flag", - opts: ResolveAuthOptions{SelectorExplicit: true, Token: "token", TokenExplicit: true, TokenRequired: true, RegionOverride: "aws-us-east-1", Env: map[string]string{"TDC_FS_FILE_SYSTEM_NAME": "workspace"}}, + opts: ResolveAuthOptions{SelectorExplicit: true, Token: "token", TokenExplicit: true, TokenRequired: true, RegionOverride: "aws-us-east-1", Env: map[string]string{"TI_FS_FILE_SYSTEM_NAME": "workspace"}}, code: "fs.empty_file_system_name", }, { @@ -243,7 +243,7 @@ func TestLegacyFlatResourceMigration(t *testing.T) { FSTenantID: "tenant-1", FSCloudProvider: "aws", FSRegionCode: "aws-us-east-1", - }, store.CredentialsProfile{TDCPublicKey: "public", TDCPrivateKey: "private", FSAPIKey: "key-1"}); err != nil { + }, store.CredentialsProfile{TiDBCloudPublicKey: "public", TiDBCloudPrivateKey: "private", FSAPIKey: "key-1"}); err != nil { t.Fatal(err) } profile := registryProfile(home) @@ -265,7 +265,7 @@ func TestLegacyFlatResourceMigration(t *testing.T) { if got := configDoc["stage"]; got.FSResourceName != "" || got.FSTenantID != "" { t.Fatalf("legacy config not cleared: %#v", got) } - if got := credentialsDoc["stage"]; got.FSAPIKey != "" || got.TDCPublicKey != "public" { + if got := credentialsDoc["stage"]; got.FSAPIKey != "" || got.TiDBCloudPublicKey != "public" { t.Fatalf("legacy credentials not cleared safely: %#v", got) } } @@ -375,7 +375,7 @@ func TestResolveDryRunUsesLegacyResourceWithoutMigrating(t *testing.T) { if err != nil || selected.FSAPIKey != "key-1" { t.Fatalf("dry-run legacy selection failed: selected=%#v err=%v", selected, err) } - if _, err := os.Stat(filepath.Join(home, store.TDCDirName, resourcesDirName)); !os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(home, store.TIDirName, resourcesDirName)); !os.IsNotExist(err) { t.Fatalf("dry-run created registry state: %v", err) } } @@ -460,7 +460,7 @@ func TestResourceNameCannotEscapeRegistry(t *testing.T) { if err != nil { t.Fatal(err) } - root := filepath.Join(home, store.TDCDirName, resourcesDirName) + root := filepath.Join(home, store.TIDirName, resourcesDirName) rel, err := filepath.Rel(root, dir) if err != nil || rel == ".." || len(rel) >= 3 && rel[:3] == ".."+string(os.PathSeparator) { t.Fatalf("resource escaped registry: dir=%s rel=%s err=%v", dir, rel, err) @@ -474,8 +474,8 @@ func registryProfile(home string) *config.Profile { PlacementRegionCode: "aws-us-east-1", CloudProvider: "aws", RegionCode: "us-east-1", - TDCPublicKey: "public", - TDCPrivateKey: "private", + TiDBCloudPublicKey: "public", + TiDBCloudPrivateKey: "private", } } diff --git a/internal/fs/fuse_git.go b/internal/fs/fuse_git.go index 4f4eb8d..4895a01 100644 --- a/internal/fs/fuse_git.go +++ b/internal/fs/fuse_git.go @@ -17,7 +17,7 @@ import ( "time" "github.com/hanwen/go-fuse/v2/fuse" - apifs "github.com/tidbcloud/tdc/internal/api/fs" + apifs "github.com/tidbcloud/ti-cli/internal/api/fs" ) const fuseGitCacheTTL = 2 * time.Second diff --git a/internal/fs/fuse_mount.go b/internal/fs/fuse_mount.go index 19f48b6..27a1355 100644 --- a/internal/fs/fuse_mount.go +++ b/internal/fs/fuse_mount.go @@ -20,10 +20,10 @@ import ( gofs "github.com/hanwen/go-fuse/v2/fs" gofuse "github.com/hanwen/go-fuse/v2/fuse" - "github.com/tidbcloud/tdc/internal/api" - apifs "github.com/tidbcloud/tdc/internal/api/fs" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/fs/mountstate" + "github.com/tidbcloud/ti-cli/internal/api" + apifs "github.com/tidbcloud/ti-cli/internal/api/fs" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/fs/mountstate" ) var errFuseWriteConflict = errors.New("remote file changed since it was opened") @@ -57,8 +57,8 @@ func (s Service) mountFUSEForeground(ctx context.Context, inputs mountInputs, re UID: uint32(os.Getuid()), GID: uint32(os.Getgid()), MountOptions: gofuse.MountOptions{ - FsName: "tdc-fs:" + inputs.fileSystemName, - Name: "tdcfs", + FsName: "ti-fs:" + inputs.fileSystemName, + Name: "tifs", }, } if inputs.readOnly { @@ -66,12 +66,12 @@ func (s Service) mountFUSEForeground(ctx context.Context, inputs mountInputs, re } server, err := gofs.Mount(inputs.mountPath, root, options) if err != nil { - return MountResult{}, apperr.Wrap("fs.mount_fuse", "runtime", 1, fmt.Sprintf("mount tdc fs with FUSE at %q", inputs.mountPath), err) + return MountResult{}, apperr.Wrap("fs.mount_fuse", "runtime", 1, fmt.Sprintf("mount ti fs with FUSE at %q", inputs.mountPath), err) } controlServer, err := startMountControlServer(inputs.mountPath, runtime) if err != nil { _ = server.Unmount() - return MountResult{}, apperr.Wrap("fs.mount_control", "runtime", 1, fmt.Sprintf("start tdc fs mount control socket for %q", inputs.mountPath), err) + return MountResult{}, apperr.Wrap("fs.mount_control", "runtime", 1, fmt.Sprintf("start ti fs mount control socket for %q", inputs.mountPath), err) } defer controlServer.Close() @@ -332,7 +332,7 @@ func (r *remoteFuseRuntime) writeFileWithDirty(ctx context.Context, remotePath s func (r *remoteFuseRuntime) upload(ctx context.Context, remotePath string, data []byte, baseVersion fuseObjectVersion, baseSize int64, dirtyRanges []fuseDirtyRange) (fuseObjectVersion, error) { if r == nil || r.client == nil { - return fuseObjectVersion{}, errors.New("tdc fs FUSE runtime has no data-plane client") + return fuseObjectVersion{}, errors.New("ti fs FUSE runtime has no data-plane client") } if err := r.checkWriteBase(ctx, remotePath, baseVersion); err != nil { return fuseObjectVersion{}, err diff --git a/internal/fs/fuse_mount_control.go b/internal/fs/fuse_mount_control.go index 29441f4..2d13539 100644 --- a/internal/fs/fuse_mount_control.go +++ b/internal/fs/fuse_mount_control.go @@ -16,8 +16,8 @@ import ( "time" gofs "github.com/hanwen/go-fuse/v2/fs" - "github.com/tidbcloud/tdc/internal/fs/mountcontrol" - "github.com/tidbcloud/tdc/internal/fs/mountstate" + "github.com/tidbcloud/ti-cli/internal/fs/mountcontrol" + "github.com/tidbcloud/ti-cli/internal/fs/mountstate" ) const drainPendingWorkKind = "pending_work_remaining" diff --git a/internal/fs/fuse_mount_unsupported.go b/internal/fs/fuse_mount_unsupported.go index 8374b88..cdd44b6 100644 --- a/internal/fs/fuse_mount_unsupported.go +++ b/internal/fs/fuse_mount_unsupported.go @@ -5,10 +5,10 @@ package fs import ( "context" - apifs "github.com/tidbcloud/tdc/internal/api/fs" - "github.com/tidbcloud/tdc/internal/apperr" + apifs "github.com/tidbcloud/ti-cli/internal/api/fs" + "github.com/tidbcloud/ti-cli/internal/apperr" ) func (s Service) mountFUSEForeground(ctx context.Context, inputs mountInputs, remote apifs.StatusResponse, checks []MountRuntimeCheck) (MountResult, error) { - return MountResult{}, apperr.New("fs.fuse_unsupported", "runtime", 1, "tdc fs FUSE mount is not supported on Windows; explicitly use --driver webdav if a WebDAV mount is available") + return MountResult{}, apperr.New("fs.fuse_unsupported", "runtime", 1, "ti fs FUSE mount is not supported on Windows; explicitly use --driver webdav if a WebDAV mount is available") } diff --git a/internal/fs/fuse_version.go b/internal/fs/fuse_version.go index af536af..66fd0dd 100644 --- a/internal/fs/fuse_version.go +++ b/internal/fs/fuse_version.go @@ -2,7 +2,7 @@ package fs -import apifs "github.com/tidbcloud/tdc/internal/api/fs" +import apifs "github.com/tidbcloud/ti-cli/internal/api/fs" type fuseObjectVersion struct { Revision int64 `json:"revision,omitempty"` diff --git a/internal/fs/git.go b/internal/fs/git.go index 02e92a2..f0c65cb 100644 --- a/internal/fs/git.go +++ b/internal/fs/git.go @@ -21,11 +21,11 @@ import ( "strings" "time" - apifs "github.com/tidbcloud/tdc/internal/api/fs" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/fs/mountstate" + apifs "github.com/tidbcloud/ti-cli/internal/api/fs" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/fs/mountstate" ) const ( @@ -225,7 +225,7 @@ const ( ) func (s Service) CreateGitWorkspace(ctx context.Context, opts GitWorkspaceCreateOptions) (apifs.GitWorkspace, error) { - client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceWrite, "create tdc fs-git workspace") + client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceWrite, "create ti fs-git workspace") if err != nil { return apifs.GitWorkspace{}, err } @@ -251,7 +251,7 @@ func (s Service) CreateGitWorkspace(ctx context.Context, opts GitWorkspaceCreate } func (s Service) ListGitWorkspaces(ctx context.Context, profile *config.Profile) (apifs.GitWorkspacesResponse, error) { - client, err := s.dataClient(profile, authz.FSGitWorkspaceRead, "list tdc fs-git workspaces") + client, err := s.dataClient(profile, authz.FSGitWorkspaceRead, "list ti fs-git workspaces") if err != nil { return apifs.GitWorkspacesResponse{}, err } @@ -259,7 +259,7 @@ func (s Service) ListGitWorkspaces(ctx context.Context, profile *config.Profile) } func (s Service) DescribeGitWorkspace(ctx context.Context, opts GitWorkspaceDescribeOptions) (apifs.GitWorkspace, error) { - client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "describe tdc fs-git workspace") + client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "describe ti fs-git workspace") if err != nil { return apifs.GitWorkspace{}, err } @@ -270,7 +270,7 @@ func (s Service) DescribeGitWorkspace(ctx context.Context, opts GitWorkspaceDesc } func (s Service) DeleteGitWorkspace(ctx context.Context, opts GitWorkspaceDeleteOptions) (GitDeleteResult, error) { - client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceWrite, "delete tdc fs-git workspace") + client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceWrite, "delete ti fs-git workspace") if err != nil { return GitDeleteResult{}, err } @@ -285,7 +285,7 @@ func (s Service) DeleteGitWorkspace(ctx context.Context, opts GitWorkspaceDelete } func (s Service) ReplaceGitTree(ctx context.Context, opts GitTreeReplaceOptions) (GitDeleteResult, error) { - client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceWrite, "replace tdc fs-git tree") + client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceWrite, "replace ti fs-git tree") if err != nil { return GitDeleteResult{}, err } @@ -304,7 +304,7 @@ func (s Service) ReplaceGitTree(ctx context.Context, opts GitTreeReplaceOptions) } func (s Service) ListGitTree(ctx context.Context, opts GitTreeListOptions) (apifs.GitTreeResponse, error) { - client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "list tdc fs-git tree") + client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "list ti fs-git tree") if err != nil { return apifs.GitTreeResponse{}, err } @@ -316,7 +316,7 @@ func (s Service) ListGitTree(ctx context.Context, opts GitTreeListOptions) (apif } func (s Service) UpsertGitState(ctx context.Context, opts GitStateUpsertOptions) (apifs.GitState, error) { - client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceWrite, "upsert tdc fs-git state") + client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceWrite, "upsert ti fs-git state") if err != nil { return apifs.GitState{}, err } @@ -336,7 +336,7 @@ func (s Service) UpsertGitState(ctx context.Context, opts GitStateUpsertOptions) } func (s Service) DescribeGitState(ctx context.Context, opts GitWorkspaceIDOptions) (apifs.GitState, error) { - client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "describe tdc fs-git state") + client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "describe ti fs-git state") if err != nil { return apifs.GitState{}, err } @@ -348,7 +348,7 @@ func (s Service) DescribeGitState(ctx context.Context, opts GitWorkspaceIDOption } func (s Service) PutGitObjectPack(ctx context.Context, opts GitObjectPackPutOptions) (apifs.GitObjectPack, error) { - client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceWrite, "put tdc fs-git object pack") + client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceWrite, "put ti fs-git object pack") if err != nil { return apifs.GitObjectPack{}, err } @@ -360,7 +360,7 @@ func (s Service) PutGitObjectPack(ctx context.Context, opts GitObjectPackPutOpti } func (s Service) ListGitObjectPacks(ctx context.Context, opts GitWorkspaceIDOptions) (apifs.GitObjectPacksResponse, error) { - client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "list tdc fs-git object packs") + client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "list ti fs-git object packs") if err != nil { return apifs.GitObjectPacksResponse{}, err } @@ -372,7 +372,7 @@ func (s Service) ListGitObjectPacks(ctx context.Context, opts GitWorkspaceIDOpti } func (s Service) DescribeGitObjectPack(ctx context.Context, opts GitObjectPackDescribeOptions) (apifs.GitObjectPack, error) { - client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "describe tdc fs-git object pack") + client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "describe ti fs-git object pack") if err != nil { return apifs.GitObjectPack{}, err } @@ -387,7 +387,7 @@ func (s Service) DescribeGitObjectPack(ctx context.Context, opts GitObjectPackDe } func (s Service) PutGitOverlayEntry(ctx context.Context, opts GitOverlayPutOptions) (apifs.GitOverlayEntry, error) { - client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceWrite, "put tdc fs-git overlay entry") + client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceWrite, "put ti fs-git overlay entry") if err != nil { return apifs.GitOverlayEntry{}, err } @@ -414,7 +414,7 @@ func (s Service) PutGitOverlayEntry(ctx context.Context, opts GitOverlayPutOptio } func (s Service) DescribeGitOverlayEntry(ctx context.Context, opts GitOverlayDescribeOptions) (apifs.GitOverlayEntry, error) { - client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "describe tdc fs-git overlay entry") + client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "describe ti fs-git overlay entry") if err != nil { return apifs.GitOverlayEntry{}, err } @@ -429,7 +429,7 @@ func (s Service) DescribeGitOverlayEntry(ctx context.Context, opts GitOverlayDes } func (s Service) ListGitOverlayEntries(ctx context.Context, opts GitWorkspaceIDOptions) (apifs.GitOverlayEntriesResponse, error) { - client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "list tdc fs-git overlay entries") + client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "list ti fs-git overlay entries") if err != nil { return apifs.GitOverlayEntriesResponse{}, err } @@ -449,7 +449,7 @@ func (s Service) HydrateGitWorkspace(ctx context.Context, opts GitWorkspaceHydra } func (s Service) RestoreGitWorkspace(ctx context.Context, opts GitWorkspaceRestoreOptions) (GitRestoreResult, error) { - client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "restore tdc fs-git workspace") + client, err := s.dataClient(opts.Profile, authz.FSGitWorkspaceRead, "restore ti fs-git workspace") if err != nil { return GitRestoreResult{}, err } @@ -578,7 +578,7 @@ func (s Service) resolveMountedGitTarget(target string) (mountedGitTarget, error state, _, err := mountstate.Read(homeDir, candidate) if err == nil { if strings.TrimSpace(state.RemotePath) == "" { - return mountedGitTarget{}, apperr.New("git.mount_missing_remote_root", "runtime", 1, fmt.Sprintf("tdc fs mount metadata for %q does not include remote_path", candidate)) + return mountedGitTarget{}, apperr.New("git.mount_missing_remote_root", "runtime", 1, fmt.Sprintf("ti fs mount metadata for %q does not include remote_path", candidate)) } absMount, rel, ok, err := relToMountedTarget(absTarget, state.MountPath) if err != nil { @@ -591,7 +591,7 @@ func (s Service) resolveMountedGitTarget(target string) (mountedGitTarget, error } } if !ok { - return mountedGitTarget{}, apperr.New("git.target_outside_mount", "usage", 2, fmt.Sprintf("target %q is outside tdc fs mount %q", target, candidate)) + return mountedGitTarget{}, apperr.New("git.target_outside_mount", "usage", 2, fmt.Sprintf("target %q is outside ti fs mount %q", target, candidate)) } localPath := "/" if rel != "." { @@ -618,7 +618,7 @@ func (s Service) resolveMountedGitTarget(target string) (mountedGitTarget, error } candidate = parent } - return mountedGitTarget{}, apperr.New("git.target_not_mounted", "usage", 2, fmt.Sprintf("target %q is not inside a tdc fs mount with readable mount metadata", target)) + return mountedGitTarget{}, apperr.New("git.target_not_mounted", "usage", 2, fmt.Sprintf("target %q is not inside a ti fs mount with readable mount metadata", target)) } func relToMountedTarget(absTarget, mountPoint string) (absMount string, rel string, ok bool, err error) { @@ -646,7 +646,7 @@ func localGitDirForMountedTarget(localRoot, rel string) (string, error) { return "", nil } if !filepath.IsAbs(localRoot) { - return "", apperr.New("git.invalid_local_root", "runtime", 1, fmt.Sprintf("tdc fs mount metadata local_root must be absolute, got %q", localRoot)) + return "", apperr.New("git.invalid_local_root", "runtime", 1, fmt.Sprintf("ti fs mount metadata local_root must be absolute, got %q", localRoot)) } localPath := filepath.Join(localRoot, "overlay") if rel != "" && rel != "." { @@ -661,7 +661,7 @@ func localOverlayRootForMountedTarget(resolved mountedGitTarget) (string, error) return "", nil } if !filepath.IsAbs(localRoot) { - return "", apperr.New("git.invalid_local_root", "runtime", 1, fmt.Sprintf("tdc fs mount metadata local_root must be absolute, got %q", localRoot)) + return "", apperr.New("git.invalid_local_root", "runtime", 1, fmt.Sprintf("ti fs mount metadata local_root must be absolute, got %q", localRoot)) } localPath := filepath.Join(localRoot, "overlay") if resolved.MountRel != "" && resolved.MountRel != "." { @@ -670,7 +670,7 @@ func localOverlayRootForMountedTarget(resolved mountedGitTarget) (string, error) return localPath, nil } -func sameTDCMount(a, b mountedGitTarget) bool { +func sameTIMount(a, b mountedGitTarget) bool { return filepath.Clean(a.MountPoint) == filepath.Clean(b.MountPoint) && strings.TrimRight(a.RemoteRoot, "/") == strings.TrimRight(b.RemoteRoot, "/") && filepath.Clean(a.LocalRoot) == filepath.Clean(b.LocalRoot) diff --git a/internal/fs/journal.go b/internal/fs/journal.go index 8678862..3d8b15c 100644 --- a/internal/fs/journal.go +++ b/internal/fs/journal.go @@ -13,9 +13,9 @@ import ( "text/tabwriter" "time" - apifs "github.com/tidbcloud/tdc/internal/api/fs" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config" + apifs "github.com/tidbcloud/ti-cli/internal/api/fs" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config" ) type JournalCreateOptions struct { diff --git a/internal/fs/layer.go b/internal/fs/layer.go index 3699f76..70ff688 100644 --- a/internal/fs/layer.go +++ b/internal/fs/layer.go @@ -9,12 +9,12 @@ import ( "strings" "text/tabwriter" - apifs "github.com/tidbcloud/tdc/internal/api/fs" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/dryrun" - "github.com/tidbcloud/tdc/internal/fs/fscred" + apifs "github.com/tidbcloud/ti-cli/internal/api/fs" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/dryrun" + "github.com/tidbcloud/ti-cli/internal/fs/fscred" ) type CreateLayerOptions struct { @@ -164,7 +164,7 @@ func (s Service) DiffLayer(ctx context.Context, opts LayerEntriesOptions) (Layer } func (s Service) ReplayLayer(ctx context.Context, opts LayerEntriesOptions) (LayerEntriesResult, error) { - client, err := s.dataClient(opts.Profile, authz.FSFileRead, "replay tdc fs layer") + client, err := s.dataClient(opts.Profile, authz.FSFileRead, "replay ti fs layer") if err != nil { return LayerEntriesResult{}, err } @@ -182,7 +182,7 @@ func (s Service) ReplayLayer(ctx context.Context, opts LayerEntriesOptions) (Lay } func (s Service) CreateLayerEntry(ctx context.Context, opts CreateLayerEntryOptions) (LayerEntryResult, error) { - client, err := s.dataClient(opts.Profile, authz.FSFileWrite, "write tdc fs layer entry") + client, err := s.dataClient(opts.Profile, authz.FSFileWrite, "write ti fs layer entry") if err != nil { return LayerEntryResult{}, err } @@ -198,7 +198,7 @@ func (s Service) CreateLayerEntry(ctx context.Context, opts CreateLayerEntryOpti } func (s Service) UploadLayerFile(ctx context.Context, opts UploadLayerFileOptions) (LayerEntryResult, error) { - client, err := s.dataClient(opts.Profile, authz.FSFileWrite, "upload tdc fs layer file") + client, err := s.dataClient(opts.Profile, authz.FSFileWrite, "upload ti fs layer file") if err != nil { return LayerEntryResult{}, err } @@ -210,7 +210,7 @@ func (s Service) UploadLayerFile(ctx context.Context, opts UploadLayerFileOption } func (s Service) ReadLayerFile(ctx context.Context, opts ReadLayerFileOptions) ([]byte, error) { - client, err := s.dataClient(opts.Profile, authz.FSFileRead, "read tdc fs layer file") + client, err := s.dataClient(opts.Profile, authz.FSFileRead, "read ti fs layer file") if err != nil { return nil, err } @@ -226,7 +226,7 @@ func (s Service) ReadLayerFile(ctx context.Context, opts ReadLayerFileOptions) ( } func (s Service) DescribeLayerEntry(ctx context.Context, opts DescribeLayerEntryOptions) (LayerEntryResult, error) { - client, err := s.dataClient(opts.Profile, authz.FSFileRead, "describe tdc fs layer entry") + client, err := s.dataClient(opts.Profile, authz.FSFileRead, "describe ti fs layer entry") if err != nil { return LayerEntryResult{}, err } @@ -251,7 +251,7 @@ func (s Service) CreateLayerCheckpoint(ctx context.Context, opts CreateLayerChec } func (s Service) DescribeLayerCheckpoint(ctx context.Context, opts DescribeLayerCheckpointOptions) (LayerCheckpointResult, error) { - client, err := s.dataClient(opts.Profile, authz.FSFileRead, "describe tdc fs layer checkpoint") + client, err := s.dataClient(opts.Profile, authz.FSFileRead, "describe ti fs layer checkpoint") if err != nil { return LayerCheckpointResult{}, err } @@ -263,7 +263,7 @@ func (s Service) DescribeLayerCheckpoint(ctx context.Context, opts DescribeLayer } func (s Service) ListLayerEvents(ctx context.Context, opts ListLayerEventsOptions) (LayerEventsResult, error) { - client, err := s.dataClient(opts.Profile, authz.FSFileRead, "list tdc fs layer events") + client, err := s.dataClient(opts.Profile, authz.FSFileRead, "list ti fs layer events") if err != nil { return LayerEventsResult{}, err } @@ -299,7 +299,7 @@ func (s Service) DryRunLayerMutation(ctx context.Context, commandPath, operation if resource := fscred.FromProfile(profile); resource.HasAPIKey { checks = append(checks, dryrun.Check{Name: "fs_resource_credentials", Status: "passed", Message: resource.Name}) } else { - return dryrun.Result{}, apperr.New("auth.missing_fs_api_key", "authentication", 3, fmt.Sprintf("authentication required: missing fs_api_key for profile %q. Create or configure a tdc fs resource first.", profileName(profile))) + return dryrun.Result{}, apperr.New("auth.missing_fs_api_key", "authentication", 3, fmt.Sprintf("authentication required: missing fs_api_key for profile %q. Create or configure a ti fs resource first.", profileName(profile))) } return dryrun.New( commandPath, diff --git a/internal/fs/metastore.go b/internal/fs/metastore.go index 6202186..c8c0b84 100644 --- a/internal/fs/metastore.go +++ b/internal/fs/metastore.go @@ -10,8 +10,8 @@ import ( "strings" "sync" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config" ) type fsMetadataStore struct { @@ -33,7 +33,7 @@ type fsMetadataEntry struct { func newFSMetadataStore(homeDir string, profile *config.Profile) (*fsMetadataStore, error) { if strings.TrimSpace(homeDir) == "" { - return nil, apperr.New("fs.metadata_missing_home", "config", 1, "home directory is required for tdc fs metadata") + return nil, apperr.New("fs.metadata_missing_home", "config", 1, "home directory is required for ti fs metadata") } key := "profile=" + profileNameForMetadata(profile) + "\x00tenant=" + strings.TrimSpace(profileValue(profile, func(p *config.Profile) string { return p.FSTenantID })) + @@ -41,7 +41,7 @@ func newFSMetadataStore(homeDir string, profile *config.Profile) (*fsMetadataSto "\x00cloud=" + strings.TrimSpace(profileValue(profile, func(p *config.Profile) string { return p.FSCloudProvider })) + "\x00region=" + strings.TrimSpace(profileValue(profile, func(p *config.Profile) string { return p.FSRegionCode })) sum := sha256.Sum256([]byte(key)) - return &fsMetadataStore{path: filepath.Join(homeDir, ".tdc", "fs_metadata", hex.EncodeToString(sum[:8])+".json")}, nil + return &fsMetadataStore{path: filepath.Join(homeDir, ".ti", "fs_metadata", hex.EncodeToString(sum[:8])+".json")}, nil } func profileNameForMetadata(profile *config.Profile) string { @@ -228,19 +228,19 @@ func (s *fsMetadataStore) update(remotePath string, fn func(fsMetadataEntry) (fs } func (s *fsMetadataStore) readLocked() (fsMetadataDoc, error) { - doc := fsMetadataDoc{Schema: "tdc.fs.metadata/v1", Entries: map[string]fsMetadataEntry{}} + doc := fsMetadataDoc{Schema: "ti.fs.metadata/v1", Entries: map[string]fsMetadataEntry{}} data, err := os.ReadFile(s.path) if err != nil { if os.IsNotExist(err) { return doc, nil } - return fsMetadataDoc{}, fmt.Errorf("read tdc fs metadata %q: %w", s.path, err) + return fsMetadataDoc{}, fmt.Errorf("read ti fs metadata %q: %w", s.path, err) } if len(strings.TrimSpace(string(data))) == 0 { return doc, nil } if err := json.Unmarshal(data, &doc); err != nil { - return fsMetadataDoc{}, fmt.Errorf("parse tdc fs metadata %q: %w", s.path, err) + return fsMetadataDoc{}, fmt.Errorf("parse ti fs metadata %q: %w", s.path, err) } if doc.Entries == nil { doc.Entries = map[string]fsMetadataEntry{} @@ -250,25 +250,25 @@ func (s *fsMetadataStore) readLocked() (fsMetadataDoc, error) { func (s *fsMetadataStore) writeLocked(doc fsMetadataDoc) error { if doc.Schema == "" { - doc.Schema = "tdc.fs.metadata/v1" + doc.Schema = "ti.fs.metadata/v1" } if doc.Entries == nil { doc.Entries = map[string]fsMetadataEntry{} } if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { - return fmt.Errorf("create tdc fs metadata directory: %w", err) + return fmt.Errorf("create ti fs metadata directory: %w", err) } data, err := json.MarshalIndent(doc, "", " ") if err != nil { - return fmt.Errorf("encode tdc fs metadata: %w", err) + return fmt.Errorf("encode ti fs metadata: %w", err) } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, append(data, '\n'), 0o644); err != nil { - return fmt.Errorf("write tdc fs metadata temp file: %w", err) + return fmt.Errorf("write ti fs metadata temp file: %w", err) } if err := os.Rename(tmp, s.path); err != nil { _ = os.Remove(tmp) - return fmt.Errorf("replace tdc fs metadata file: %w", err) + return fmt.Errorf("replace ti fs metadata file: %w", err) } return nil } diff --git a/internal/fs/mount.go b/internal/fs/mount.go index 5f24b47..85d3085 100644 --- a/internal/fs/mount.go +++ b/internal/fs/mount.go @@ -18,16 +18,16 @@ import ( "text/tabwriter" "time" - "github.com/tidbcloud/tdc/internal/api/endpoints" - apifs "github.com/tidbcloud/tdc/internal/api/fs" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/dryrun" - "github.com/tidbcloud/tdc/internal/fs/mountcontrol" - "github.com/tidbcloud/tdc/internal/fs/mountdriver" - "github.com/tidbcloud/tdc/internal/fs/mountprocess" - "github.com/tidbcloud/tdc/internal/fs/mountstate" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + apifs "github.com/tidbcloud/ti-cli/internal/api/fs" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/dryrun" + "github.com/tidbcloud/ti-cli/internal/fs/mountcontrol" + "github.com/tidbcloud/ti-cli/internal/fs/mountdriver" + "github.com/tidbcloud/ti-cli/internal/fs/mountprocess" + "github.com/tidbcloud/ti-cli/internal/fs/mountstate" "golang.org/x/net/webdav" ) @@ -170,7 +170,7 @@ func (s Service) DryRunMountFileSystem(ctx context.Context, commandPath string, } else { checks = append(checks, dryrun.Check{Name: "mount_driver", Status: "passed", Message: inputs.driver.Name()}) } - description := "normal execution starts a local tdc fs FUSE runtime and mounts it at the requested path" + description := "normal execution starts a local ti fs FUSE runtime and mounts it at the requested path" if inputs.driver.Name() == "webdav" { description = "normal execution starts a local WebDAV bridge and mounts it at the requested path" } @@ -231,7 +231,7 @@ func (s Service) DryRunDrainFileSystem(ctx context.Context, commandPath string, commandPath, "drain_file_system", dryrun.RequestSummary{ - Description: "normal execution delegates mount drain to the resource-scoped tdc-drive9 companion runtime", + Description: "normal execution delegates mount drain to the resource-scoped ti-drive9 companion runtime", Method: "LOCAL", Path: opts.MountPath, }, @@ -258,7 +258,7 @@ func (s Service) DryRunUnmountFileSystem(ctx context.Context, commandPath string commandPath, "unmount_file_system", dryrun.RequestSummary{ - Description: "normal execution delegates unmount to the resource-scoped tdc-drive9 companion runtime and removes the locator after success", + Description: "normal execution delegates unmount to the resource-scoped ti-drive9 companion runtime and removes the locator after success", Method: "LOCAL", Path: opts.MountPath, }, @@ -280,7 +280,7 @@ func (s Service) readDrainMountState(mountPathInput string) (mountstate.State, s state, stateFile, err := mountstate.Read(homeDir, mountPath) if err != nil { if os.IsNotExist(err) { - return mountstate.State{}, "", "", nil, apperr.New("fs.mount_state_not_found", "runtime", 1, fmt.Sprintf("no tdc fs mount state found for %q", mountPath)) + return mountstate.State{}, "", "", nil, apperr.New("fs.mount_state_not_found", "runtime", 1, fmt.Sprintf("no ti fs mount state found for %q", mountPath)) } return mountstate.State{}, "", "", nil, apperr.Wrap("fs.read_mount_state", "runtime", 1, fmt.Sprintf("read mount state for %q", mountPath), err) } @@ -352,7 +352,7 @@ func (s Service) mountInputs(opts MountFileSystemOptions) (mountInputs, error) { if err != nil { return mountInputs{}, err } - client, err := s.bearerClient(opts.Profile, endpoint, authz.FSMount, "mount tdc fs resource") + client, err := s.bearerClient(opts.Profile, endpoint, authz.FSMount, "mount ti fs resource") if err != nil { return mountInputs{}, err } @@ -435,7 +435,7 @@ func (s Service) mountForeground(ctx context.Context, inputs mountInputs, remote case "webdav": return s.mountWebDAVForeground(ctx, inputs, remote, checks) default: - return MountResult{}, apperr.New("fs.invalid_mount_driver", "usage", 2, fmt.Sprintf("unsupported tdc fs mount driver %q", inputs.driver.Name())) + return MountResult{}, apperr.New("fs.invalid_mount_driver", "usage", 2, fmt.Sprintf("unsupported ti fs mount driver %q", inputs.driver.Name())) } } @@ -486,7 +486,7 @@ func (s Service) mountWebDAVForeground(ctx context.Context, inputs mountInputs, } listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { - return MountResult{}, apperr.Wrap("fs.mount_listen", "runtime", 1, "start local tdc fs WebDAV bridge", err) + return MountResult{}, apperr.Wrap("fs.mount_listen", "runtime", 1, "start local ti fs WebDAV bridge", err) } defer listener.Close() @@ -551,7 +551,7 @@ func (s Service) mountWebDAVForeground(ctx context.Context, inputs mountInputs, err = <-serverErr if err != nil && err != http.ErrServerClosed { _ = inputs.driver.Unmount(context.Background(), inputs.mountPath) - return MountResult{}, apperr.Wrap("fs.mount_server", "runtime", 1, "tdc fs WebDAV bridge stopped unexpectedly", err) + return MountResult{}, apperr.Wrap("fs.mount_server", "runtime", 1, "ti fs WebDAV bridge stopped unexpectedly", err) } checks = append(checks, MountRuntimeCheck{Name: "mount_state", Status: "passed", Message: stateFile}) return mountResult("unmounted", inputs, remote, checks, os.Getpid(), stateFile, ""), nil @@ -560,7 +560,7 @@ func (s Service) mountWebDAVForeground(ctx context.Context, inputs mountInputs, func (s Service) mountBackground(ctx context.Context, inputs mountInputs, remote apifs.StatusResponse, checks []MountRuntimeCheck) (MountResult, error) { executable, err := os.Executable() if err != nil { - return MountResult{}, apperr.Wrap("fs.executable_path", "runtime", 1, "determine tdc executable path for background mount", err) + return MountResult{}, apperr.Wrap("fs.executable_path", "runtime", 1, "determine ti executable path for background mount", err) } if err := os.MkdirAll(filepath.Dir(inputs.logFile), 0o700); err != nil { return MountResult{}, apperr.Wrap("fs.mount_log_dir", "runtime", 1, fmt.Sprintf("create mount log directory %q", filepath.Dir(inputs.logFile)), err) @@ -624,7 +624,7 @@ func startBackgroundMount(ctx context.Context, request backgroundMountRequest) ( cmd.Stdout = logFile cmd.Stderr = logFile if err := cmd.Start(); err != nil { - return 0, apperr.Wrap("fs.start_mount_process", "runtime", 1, "start background tdc fs mount process", err) + return 0, apperr.Wrap("fs.start_mount_process", "runtime", 1, "start background ti fs mount process", err) } deadline := time.Now().Add(request.Timeout) for { @@ -637,7 +637,7 @@ func startBackgroundMount(ctx context.Context, request backgroundMountRequest) ( } if time.Now().After(deadline) { _ = mountprocess.Terminate(cmd.Process.Pid) - return 0, apperr.New("fs.mount_ready_timeout", "runtime", 1, fmt.Sprintf("tdc fs mount at %q did not become ready within %s; inspect %s", request.MountPath, request.Timeout, request.LogFile)) + return 0, apperr.New("fs.mount_ready_timeout", "runtime", 1, fmt.Sprintf("ti fs mount at %q did not become ready within %s; inspect %s", request.MountPath, request.Timeout, request.LogFile)) } time.Sleep(100 * time.Millisecond) } @@ -684,7 +684,7 @@ func mountCacheDir(homeDir string, identity MountCacheIdentity, explicit string) identity.MountPath, identity.APIKeyFingerprint, }, "\x00"))) - return filepath.Join(homeDir, ".tdc", "cache", "mounts", hex.EncodeToString(sum[:8])), nil + return filepath.Join(homeDir, ".ti", "cache", "mounts", hex.EncodeToString(sum[:8])), nil } func mountCacheIdentity(profile *config.Profile, fileSystemName, mountPath, remotePath string, endpoint endpoints.Endpoint) MountCacheIdentity { @@ -739,7 +739,7 @@ func defaultMountLocalRoot(homeDir string, identity MountCacheIdentity) string { identity.RemotePath, identity.APIKeyFingerprint, }, "\x00"))) - return filepath.Join(homeDir, ".tdc", "local", "fs", hex.EncodeToString(sum[:8])) + return filepath.Join(homeDir, ".ti", "local", "fs", hex.EncodeToString(sum[:8])) } func mergeMountPackPaths(groups ...[]string) []string { @@ -776,7 +776,7 @@ func randomWebDAVPrefix() (string, error) { if _, err := rand.Read(buf[:]); err != nil { return "", err } - return "/tdc-" + hex.EncodeToString(buf[:]), nil + return "/ti-" + hex.EncodeToString(buf[:]), nil } func statusMessage(value string) string { diff --git a/internal/fs/mountdriver/driver.go b/internal/fs/mountdriver/driver.go index 8a16c07..831e99b 100644 --- a/internal/fs/mountdriver/driver.go +++ b/internal/fs/mountdriver/driver.go @@ -48,7 +48,7 @@ func ResolveWithDeps(name, goos string, lookPath func(string) (string, error), s case "webdav": return webdav, nil default: - return nil, fmt.Errorf("unsupported tdc fs mount driver %q; supported values: auto, fuse, webdav", name) + return nil, fmt.Errorf("unsupported ti fs mount driver %q; supported values: auto, fuse, webdav", name) } } @@ -93,18 +93,18 @@ func (d FUSE) CheckPrerequisites() error { } return fmt.Errorf("missing fusermount3 or fusermount: install FUSE userspace tools, or explicitly use --driver webdav where supported") case "windows": - return fmt.Errorf("tdc fs FUSE mount is not supported on Windows; explicitly use --driver webdav if a WebDAV mount is available") + return fmt.Errorf("ti fs FUSE mount is not supported on Windows; explicitly use --driver webdav if a WebDAV mount is available") default: - return fmt.Errorf("tdc fs FUSE mount is not supported on %s", goos) + return fmt.Errorf("ti fs FUSE mount is not supported on %s", goos) } } func (d FUSE) Mount(ctx context.Context, serverURL, mountPath string) error { - return fmt.Errorf("tdc fs FUSE mounts are handled by the in-process FUSE runtime") + return fmt.Errorf("ti fs FUSE mounts are handled by the in-process FUSE runtime") } func (d FUSE) Unmount(ctx context.Context, mountPath string) error { - return fmt.Errorf("tdc fs FUSE unmount is handled by the running FUSE server") + return fmt.Errorf("ti fs FUSE unmount is handled by the running FUSE server") } func (d FUSE) goos() string { @@ -147,15 +147,15 @@ func (d WebDAV) CheckPrerequisites() error { return fmt.Errorf("missing mount_webdav: install or enable the macOS WebDAV filesystem helper") } if _, err := lookPath("umount"); err != nil { - return fmt.Errorf("missing umount: tdc fs cannot detach WebDAV mounts on this system") + return fmt.Errorf("missing umount: ti fs cannot detach WebDAV mounts on this system") } return nil case "linux": - return fmt.Errorf("tdc fs WebDAV mount is not supported on Linux yet; use tdc fs data-plane commands or run mount on macOS") + return fmt.Errorf("ti fs WebDAV mount is not supported on Linux yet; use ti fs data-plane commands or run mount on macOS") case "windows": - return fmt.Errorf("tdc fs WebDAV mount is not supported on Windows yet; use tdc fs data-plane commands or run mount on macOS") + return fmt.Errorf("ti fs WebDAV mount is not supported on Windows yet; use ti fs data-plane commands or run mount on macOS") default: - return fmt.Errorf("tdc fs mount is not supported on %s", goos) + return fmt.Errorf("ti fs mount is not supported on %s", goos) } } @@ -175,7 +175,7 @@ func (d WebDAV) Unmount(ctx context.Context, mountPath string) error { switch goos { case "darwin": if _, err := d.lookPath()("umount"); err != nil { - return fmt.Errorf("missing umount: tdc fs cannot detach WebDAV mounts on this system") + return fmt.Errorf("missing umount: ti fs cannot detach WebDAV mounts on this system") } cmd := d.command(ctx, "umount", mountPath) if output, err := cmd.CombinedOutput(); err != nil { diff --git a/internal/fs/mountlocator/locator.go b/internal/fs/mountlocator/locator.go index 4e08045..90c0104 100644 --- a/internal/fs/mountlocator/locator.go +++ b/internal/fs/mountlocator/locator.go @@ -10,7 +10,7 @@ import ( "strings" ) -const schema = "tdc.fs.mount-locator/v1" +const schema = "ti.fs.mount-locator/v1" type Locator struct { Schema string `json:"schema"` @@ -61,7 +61,7 @@ func Path(homeDir, mountPath string) (string, error) { return "", err } sum := sha256.Sum256([]byte(canonical)) - return filepath.Join(homeDir, ".tdc", "mounts", hex.EncodeToString(sum[:8])+".locator.json"), nil + return filepath.Join(homeDir, ".ti", "mounts", hex.EncodeToString(sum[:8])+".locator.json"), nil } func Write(homeDir string, locator Locator) (string, error) { diff --git a/internal/fs/mountlocator/locator_test.go b/internal/fs/mountlocator/locator_test.go index d84cde1..be1e82e 100644 --- a/internal/fs/mountlocator/locator_test.go +++ b/internal/fs/mountlocator/locator_test.go @@ -60,7 +60,7 @@ func TestReadRejectsIncompleteLocator(t *testing.T) { if err != nil { t.Fatal(err) } - if err := os.WriteFile(path, []byte(`{"schema":"tdc.fs.mount-locator/v1"}`), 0o600); err != nil { + if err := os.WriteFile(path, []byte(`{"schema":"ti.fs.mount-locator/v1"}`), 0o600); err != nil { t.Fatal(err) } if _, _, err := Read(home, "./workspace"); err == nil { diff --git a/internal/fs/mountstate/state.go b/internal/fs/mountstate/state.go index 09b9aa1..1092e02 100644 --- a/internal/fs/mountstate/state.go +++ b/internal/fs/mountstate/state.go @@ -28,7 +28,7 @@ type State struct { StartedAt time.Time `json:"started_at"` } -const schema = "tdc.fs.mount/v1" +const schema = "ti.fs.mount/v1" func New(profile, fileSystemName, mountPath, remotePath, driver, endpoint string, pid int, readOnly bool, startedAt time.Time) (State, error) { canonical, err := CanonicalMountPath(mountPath) @@ -77,7 +77,7 @@ func Path(homeDir, mountPath string) (string, error) { return "", err } sum := sha256.Sum256([]byte(canonical)) - return filepath.Join(homeDir, ".tdc", "mounts", hex.EncodeToString(sum[:8])+".json"), nil + return filepath.Join(homeDir, ".ti", "mounts", hex.EncodeToString(sum[:8])+".json"), nil } func LogPath(homeDir, mountPath string) (string, error) { @@ -86,7 +86,7 @@ func LogPath(homeDir, mountPath string) (string, error) { return "", err } sum := sha256.Sum256([]byte(canonical)) - return filepath.Join(homeDir, ".tdc", "mounts", hex.EncodeToString(sum[:8])+".log"), nil + return filepath.Join(homeDir, ".ti", "mounts", hex.EncodeToString(sum[:8])+".log"), nil } func ControlSocketPath(mountPath string) (string, error) { @@ -98,9 +98,9 @@ func ControlSocketPath(mountPath string) (string, error) { sum := sha256.Sum256([]byte(uid + "\x00" + canonical)) dir := strings.TrimSpace(os.Getenv("XDG_RUNTIME_DIR")) if dir == "" || !filepath.IsAbs(dir) { - dir = filepath.Join(os.TempDir(), "tdc-"+uid) + dir = filepath.Join(os.TempDir(), "ti-"+uid) } - return filepath.Join(dir, "tdc-mount-"+hex.EncodeToString(sum[:8])+".sock"), nil + return filepath.Join(dir, "ti-mount-"+hex.EncodeToString(sum[:8])+".sock"), nil } func Write(homeDir string, state State) (string, error) { diff --git a/internal/fs/pack.go b/internal/fs/pack.go index 9f355aa..bb3e010 100644 --- a/internal/fs/pack.go +++ b/internal/fs/pack.go @@ -19,24 +19,24 @@ import ( "strings" "time" - apifs "github.com/tidbcloud/tdc/internal/api/fs" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/dryrun" - "github.com/tidbcloud/tdc/internal/fs/mountstate" + apifs "github.com/tidbcloud/ti-cli/internal/api/fs" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/dryrun" + "github.com/tidbcloud/ti-cli/internal/fs/mountstate" ) const ( defaultMountProfile = "coding-agent" noneMountProfile = "none" portableMountProfile = "portable" - packArchiveFormat = "tdc.pack.v1" + packArchiveFormat = "ti.pack.v1" drive9PackArchiveFormat = "drive9.pack.v1" - packManifestEntryName = ".tdc-pack-manifest.json" + packManifestEntryName = ".ti-pack-manifest.json" drive9ManifestEntryName = ".drive9-pack-manifest.json" packArchiveEntryPrefix = "entries/" - defaultPackRoot = "/.tdc/packs" + defaultPackRoot = "/.ti/packs" ) type PackFileSystemOptions struct { @@ -161,14 +161,14 @@ func (s Service) DryRunPackFileSystem(ctx context.Context, commandPath string, o if err != nil { return dryrun.Result{}, err } - if _, err := s.dataClient(opts.Profile, authz.FSFileWrite, "pack tdc fs local overlay"); err != nil { + if _, err := s.dataClient(opts.Profile, authz.FSFileWrite, "pack ti fs local overlay"); err != nil { return dryrun.Result{}, err } return dryrun.New( commandPath, "pack_file_system", dryrun.RequestSummary{ - Description: "normal execution creates a tar.gz archive from local-root/overlay and uploads it as a tdc fs file", + Description: "normal execution creates a tar.gz archive from local-root/overlay and uploads it as a ti fs file", Method: "PUT", Path: request.ArchivePath, Body: map[string]any{ @@ -191,14 +191,14 @@ func (s Service) DryRunUnpackFileSystem(ctx context.Context, commandPath string, if err != nil { return dryrun.Result{}, err } - if _, err := s.dataClient(opts.Profile, authz.FSFileRead, "unpack tdc fs local overlay"); err != nil { + if _, err := s.dataClient(opts.Profile, authz.FSFileRead, "unpack ti fs local overlay"); err != nil { return dryrun.Result{}, err } return dryrun.New( commandPath, "unpack_file_system", dryrun.RequestSummary{ - Description: "normal execution downloads a tdc fs pack archive and restores it into local-root/overlay", + Description: "normal execution downloads a ti fs pack archive and restores it into local-root/overlay", Method: "GET", Path: request.ArchivePath, Body: map[string]any{ @@ -317,7 +317,7 @@ func (s Service) packMountState(mountPath string) (mountstate.State, error) { state, _, err := mountstate.Read(homeDir, mountPath) if err != nil { if os.IsNotExist(err) { - return mountstate.State{}, apperr.New("fs.mount_state_not_found", "runtime", 1, fmt.Sprintf("no tdc fs mount state found for %q", mountPath)) + return mountstate.State{}, apperr.New("fs.mount_state_not_found", "runtime", 1, fmt.Sprintf("no ti fs mount state found for %q", mountPath)) } return mountstate.State{}, apperr.Wrap("fs.read_mount_state", "runtime", 1, fmt.Sprintf("read mount state for %q", mountPath), err) } @@ -388,7 +388,7 @@ func writePackArchive(ctx context.Context, w io.Writer, opts packArchiveOptions) func readPackArchiveManifest(ctx context.Context, r io.Reader) (*packManifest, error) { gz, err := gzip.NewReader(r) if err != nil { - return nil, apperr.Wrap("fs.pack_read_gzip", "runtime", 1, "open tdc fs pack gzip stream", err) + return nil, apperr.Wrap("fs.pack_read_gzip", "runtime", 1, "open ti fs pack gzip stream", err) } defer gz.Close() tr := tar.NewReader(gz) @@ -397,7 +397,7 @@ func readPackArchiveManifest(ctx context.Context, r io.Reader) (*packManifest, e return nil, apperr.New("fs.invalid_pack_archive", "runtime", 1, "invalid pack archive: missing manifest") } if err != nil { - return nil, apperr.Wrap("fs.pack_read_manifest", "runtime", 1, "read tdc fs pack manifest entry", err) + return nil, apperr.Wrap("fs.pack_read_manifest", "runtime", 1, "read ti fs pack manifest entry", err) } if err := ctx.Err(); err != nil { return nil, err @@ -407,7 +407,7 @@ func readPackArchiveManifest(ctx context.Context, r io.Reader) (*packManifest, e } var manifest packManifest if err := json.NewDecoder(tr).Decode(&manifest); err != nil { - return nil, apperr.Wrap("fs.pack_decode_manifest", "runtime", 1, "decode tdc fs pack manifest", err) + return nil, apperr.Wrap("fs.pack_decode_manifest", "runtime", 1, "decode ti fs pack manifest", err) } if !supportedPackFormat(manifest.Format) { return nil, apperr.New("fs.unsupported_pack_format", "runtime", 1, fmt.Sprintf("unsupported pack format %q", manifest.Format)) @@ -424,7 +424,7 @@ func extractPackArchive(ctx context.Context, r io.Reader, opts unpackArchiveOpti if err := os.MkdirAll(localParent, 0o755); err != nil { return nil, apperr.Wrap("fs.unpack_prepare_local_root", "runtime", 1, fmt.Sprintf("prepare local root parent %q", localParent), err) } - stageRoot, err := os.MkdirTemp(localParent, ".tdc-unpack-") + stageRoot, err := os.MkdirTemp(localParent, ".ti-unpack-") if err != nil { return nil, apperr.Wrap("fs.unpack_stage", "runtime", 1, "create staged unpack root", err) } @@ -434,7 +434,7 @@ func extractPackArchive(ctx context.Context, r io.Reader, opts unpackArchiveOpti } gz, err := gzip.NewReader(r) if err != nil { - return nil, apperr.Wrap("fs.unpack_gzip", "runtime", 1, "open tdc fs pack gzip stream", err) + return nil, apperr.Wrap("fs.unpack_gzip", "runtime", 1, "open ti fs pack gzip stream", err) } defer gz.Close() tr := tar.NewReader(gz) @@ -450,12 +450,12 @@ func extractPackArchive(ctx context.Context, r io.Reader, opts unpackArchiveOpti break } if err != nil { - return nil, apperr.Wrap("fs.unpack_read_entry", "runtime", 1, "read tdc fs pack entry", err) + return nil, apperr.Wrap("fs.unpack_read_entry", "runtime", 1, "read ti fs pack entry", err) } if hdr.Name == packManifestEntryName || hdr.Name == drive9ManifestEntryName { sawManifest = true if err := json.NewDecoder(tr).Decode(&manifest); err != nil { - return nil, apperr.Wrap("fs.unpack_decode_manifest", "runtime", 1, "decode tdc fs pack manifest", err) + return nil, apperr.Wrap("fs.unpack_decode_manifest", "runtime", 1, "decode ti fs pack manifest", err) } if !supportedPackFormat(manifest.Format) { return nil, apperr.New("fs.unsupported_pack_format", "runtime", 1, fmt.Sprintf("unsupported pack format %q", manifest.Format)) @@ -1378,9 +1378,9 @@ func defaultPackArchivePath(remoteRoot, mountProfile string) (string, error) { } func packArchiveTags(mountProfile string) map[string]string { - tags := map[string]string{"tdc.pack.format": packArchiveFormat} + tags := map[string]string{"ti.pack.format": packArchiveFormat} if mountProfile = strings.TrimSpace(mountProfile); mountProfile != "" { - tags["tdc.pack.profile"] = mountProfile + tags["ti.pack.profile"] = mountProfile } return tags } diff --git a/internal/fs/vault.go b/internal/fs/vault.go index 3565f64..333d81e 100644 --- a/internal/fs/vault.go +++ b/internal/fs/vault.go @@ -11,14 +11,14 @@ import ( "text/tabwriter" "time" - "github.com/tidbcloud/tdc/internal/api" - apifs "github.com/tidbcloud/tdc/internal/api/fs" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/dryrun" - "github.com/tidbcloud/tdc/internal/fs/mountdriver" - "github.com/tidbcloud/tdc/internal/fs/mountstate" + "github.com/tidbcloud/ti-cli/internal/api" + apifs "github.com/tidbcloud/ti-cli/internal/api/fs" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/dryrun" + "github.com/tidbcloud/ti-cli/internal/fs/mountdriver" + "github.com/tidbcloud/ti-cli/internal/fs/mountstate" ) const ( @@ -173,7 +173,7 @@ func (s Service) DryRunMountVault(ctx context.Context, commandPath string, opts commandPath, "mount_vault", dryrun.RequestSummary{ - Description: "normal execution starts a local read-only FUSE runtime exposing tdc fs-vault secrets as //", + Description: "normal execution starts a local read-only FUSE runtime exposing ti fs-vault secrets as //", Method: "GET", Path: "/v1/vault/secrets or /v1/vault/read", Body: map[string]any{ @@ -188,7 +188,7 @@ func (s Service) DryRunMountVault(ctx context.Context, commandPath string, opts func (s Service) mountVaultBackground(ctx context.Context, inputs vaultMountInputs, checks []MountRuntimeCheck) (MountResult, error) { executable, err := os.Executable() if err != nil { - return MountResult{}, apperr.Wrap("vault.executable_path", "runtime", 1, "determine tdc executable path for background vault mount", err) + return MountResult{}, apperr.Wrap("vault.executable_path", "runtime", 1, "determine ti executable path for background vault mount", err) } if err := os.MkdirAll(filepath.Dir(inputs.logFile), 0o700); err != nil { return MountResult{}, apperr.Wrap("vault.mount_log_dir", "runtime", 1, fmt.Sprintf("create mount log directory %q", filepath.Dir(inputs.logFile)), err) @@ -201,7 +201,7 @@ func (s Service) mountVaultBackground(ctx context.Context, inputs vaultMountInpu } env := []string(nil) if strings.TrimSpace(inputs.vaultToken) != "" { - env = append(env, "TDC_VAULT_TOKEN="+strings.TrimSpace(inputs.vaultToken)) + env = append(env, "TI_VAULT_TOKEN="+strings.TrimSpace(inputs.vaultToken)) } pid, err := startBackgroundMount(ctx, backgroundMountRequest{ Executable: executable, @@ -232,7 +232,7 @@ func (s Service) vaultMountInputs(ctx context.Context, opts VaultMountOptions) ( if err != nil { return vaultMountInputs{}, nil, err } - client, ownerMode, err := s.vaultReadClient(opts.Profile, opts.VaultToken, authz.FSVaultSecretRead, "mount tdc fs-vault") + client, ownerMode, err := s.vaultReadClient(opts.Profile, opts.VaultToken, authz.FSVaultSecretRead, "mount ti fs-vault") if err != nil { return vaultMountInputs{}, nil, err } @@ -374,7 +374,7 @@ func (s Service) vaultReadClient(profile *config.Profile, token string, permissi Timeout: s.Timeout, Debug: s.Debug, DebugWriter: s.DebugWriter, - UserAgent: "tdc fs-vault", + UserAgent: "ti fs-vault", }) if err != nil { return nil, false, err @@ -578,7 +578,7 @@ func indexOfForbiddenVaultEnvByte(value string) int { return -1 } -func scrubTDCCredEnv(base []string) []string { +func scrubTICredEnv(base []string) []string { out := make([]string, 0, len(base)) for _, entry := range base { key, _, ok := strings.Cut(entry, "=") @@ -587,7 +587,9 @@ func scrubTDCCredEnv(base []string) []string { continue } switch key { - case "TDC_PRIVATE_KEY", "TDC_PUBLIC_KEY", "TDC_VAULT_TOKEN", "TDC_FS_API_KEY": + case "TIDB_CLOUD_PRIVATE_KEY", "TIDB_CLOUD_PUBLIC_KEY", "TI_VAULT_TOKEN", "TI_FS_TOKEN", "TI_FS_API_KEY", + "TDC_PRIVATE_KEY", "TDC_PUBLIC_KEY", "TDC_VAULT_TOKEN", "TDC_FS_TOKEN", + "DRIVE9_PRIVATE_KEY", "DRIVE9_PUBLIC_KEY", "DRIVE9_VAULT_TOKEN", "DRIVE9_API_KEY": continue } out = append(out, entry) diff --git a/internal/fs/vault_mount.go b/internal/fs/vault_mount.go index 03dfb6d..74c0fec 100644 --- a/internal/fs/vault_mount.go +++ b/internal/fs/vault_mount.go @@ -14,9 +14,9 @@ import ( gofs "github.com/hanwen/go-fuse/v2/fs" gofuse "github.com/hanwen/go-fuse/v2/fuse" - apifs "github.com/tidbcloud/tdc/internal/api/fs" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/fs/mountstate" + apifs "github.com/tidbcloud/ti-cli/internal/api/fs" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/fs/mountstate" ) func (s Service) mountVaultForeground(ctx context.Context, inputs vaultMountInputs, checks []MountRuntimeCheck) (MountResult, error) { @@ -33,14 +33,14 @@ func (s Service) mountVaultForeground(ctx context.Context, inputs vaultMountInpu UID: uint32(os.Getuid()), GID: uint32(os.Getgid()), MountOptions: gofuse.MountOptions{ - FsName: "tdc-vault", - Name: "tdcvault", + FsName: "ti-vault", + Name: "tivault", Options: []string{"ro"}, }, } server, err := gofs.Mount(inputs.mountPath, root, options) if err != nil { - return MountResult{}, apperr.Wrap("vault.mount_fuse", "runtime", 1, fmt.Sprintf("mount tdc fs-vault with FUSE at %q", inputs.mountPath), err) + return MountResult{}, apperr.Wrap("vault.mount_fuse", "runtime", 1, fmt.Sprintf("mount ti fs-vault with FUSE at %q", inputs.mountPath), err) } state, err := mountstate.New(inputs.profile.Name, "vault", inputs.mountPath, "/n/vault", "fuse", inputs.endpoint, os.Getpid(), true, time.Now().UTC()) diff --git a/internal/fs/vault_mount_unsupported.go b/internal/fs/vault_mount_unsupported.go index 0a1bcbb..e0ddebb 100644 --- a/internal/fs/vault_mount_unsupported.go +++ b/internal/fs/vault_mount_unsupported.go @@ -5,12 +5,12 @@ package fs import ( "context" - "github.com/tidbcloud/tdc/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/apperr" ) func (s Service) mountVaultForeground(ctx context.Context, inputs vaultMountInputs, checks []MountRuntimeCheck) (MountResult, error) { _ = ctx _ = inputs _ = checks - return MountResult{}, apperr.New("vault.mount_unsupported", "runtime", 1, "tdc fs-vault FUSE mount is not supported on Windows; use tdc fs-vault read-secret/list-secrets or run mount-vault on macOS or Linux") + return MountResult{}, apperr.New("vault.mount_unsupported", "runtime", 1, "ti fs-vault FUSE mount is not supported on Windows; use ti fs-vault read-secret/list-secrets or run mount-vault on macOS or Linux") } diff --git a/internal/fs/webdavfs.go b/internal/fs/webdavfs.go index ee6df17..6f61f85 100644 --- a/internal/fs/webdavfs.go +++ b/internal/fs/webdavfs.go @@ -14,8 +14,8 @@ import ( "sync" "time" - "github.com/tidbcloud/tdc/internal/api" - apifs "github.com/tidbcloud/tdc/internal/api/fs" + "github.com/tidbcloud/ti-cli/internal/api" + apifs "github.com/tidbcloud/ti-cli/internal/api/fs" "golang.org/x/net/webdav" ) diff --git a/internal/fswrap/runner.go b/internal/fswrap/runner.go index aa6d02c..27b6824 100644 --- a/internal/fswrap/runner.go +++ b/internal/fswrap/runner.go @@ -12,16 +12,16 @@ import ( "runtime" "strings" - "github.com/tidbcloud/tdc/internal/api/endpoints" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config" - "github.com/tidbcloud/tdc/internal/fs/fscred" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config" + "github.com/tidbcloud/ti-cli/internal/fs/fscred" ) const ( - defaultCompanionName = "tdc-drive9" - envCompanionBin = "TDC_DRIVE9_BIN" - envAllowDrive9 = "TDC_ALLOW_STANDALONE_DRIVE9" + defaultCompanionName = "ti-drive9" + envCompanionBin = "TI_DRIVE9_BIN" + envAllowDrive9 = "TI_ALLOW_STANDALONE_DRIVE9" ) type Runner struct { @@ -40,7 +40,7 @@ type RunOptions struct { ResourceName string Args []string CaptureStdout bool - IncludeTDCKeys bool + IncludeTIKeys bool IncludeFSAPIKey bool VaultToken string } @@ -91,7 +91,7 @@ func (r Runner) Run(ctx context.Context, opts RunOptions) (Result, error) { } if r.Debug && r.DebugWriter != nil { - _, _ = fmt.Fprintf(r.DebugWriter, "tdc [DEBUG]: running %s %s\n", path, strings.Join(redactArgs(opts.Args), " ")) + _, _ = fmt.Fprintf(r.DebugWriter, "ti [DEBUG]: running %s %s\n", path, strings.Join(redactArgs(opts.Args), " ")) } if err := cmd.Run(); err != nil { exitCode := 1 @@ -101,7 +101,7 @@ func (r Runner) Run(ctx context.Context, opts RunOptions) (Result, error) { } message := strings.TrimSpace(stderr.String()) if message == "" { - message = fmt.Sprintf("tdc fs companion command failed: %s %s", path, strings.Join(redactArgs(opts.Args), " ")) + message = fmt.Sprintf("ti fs companion command failed: %s %s", path, strings.Join(redactArgs(opts.Args), " ")) } return Result{CompanionPath: path, Stdout: stdout.Bytes(), Stderr: stderr.Bytes()}, apperr.Wrap("fs.companion_failed", "runtime", exitCode, redactRunSecrets(message, opts), err) } @@ -137,7 +137,9 @@ func (r Runner) drive9Env(homeDir string, opts RunOptions) ([]string, error) { continue } switch key { - case "HOME", "USERPROFILE", "TDC_PUBLIC_KEY", "TDC_PRIVATE_KEY", "TDC_FS_TOKEN", "TDC_VAULT_TOKEN": + case "HOME", "USERPROFILE", + "TIDB_CLOUD_PUBLIC_KEY", "TIDB_CLOUD_PRIVATE_KEY", "TI_FS_TOKEN", "TI_VAULT_TOKEN", "TI_FS_API_KEY", + "TDC_PUBLIC_KEY", "TDC_PRIVATE_KEY", "TDC_FS_TOKEN", "TDC_VAULT_TOKEN": continue } env = append(env, entry) @@ -149,26 +151,26 @@ func (r Runner) drive9Env(homeDir string, opts RunOptions) ([]string, error) { if profile := opts.Profile; profile != nil { provider, regionCode, placementCode := fsPlacement(profile) if provider == "" || regionCode == "" || placementCode == "" { - return nil, apperr.New("fs.resource_credentials_incomplete", "config", 2, fmt.Sprintf("tdc fs resource placement is incomplete for profile %q", profile.Name)) + return nil, apperr.New("fs.resource_credentials_incomplete", "config", 2, fmt.Sprintf("ti fs resource placement is incomplete for profile %q", profile.Name)) } endpoint, err := r.resolver().ResolveFS(provider, regionCode) if err != nil { return nil, err } if endpoint.BaseURL == "" { - return nil, apperr.New("api.fs_endpoint_missing", "config", 2, fmt.Sprintf("tdc fs endpoint is unavailable for %s", placementCode)) + return nil, apperr.New("api.fs_endpoint_missing", "config", 2, fmt.Sprintf("ti fs endpoint is unavailable for %s", placementCode)) } env = append(env, "DRIVE9_SERVER="+endpoint.BaseURL) env = append(env, "DRIVE9_REGION_CODE="+placementCode) if opts.IncludeFSAPIKey && strings.TrimSpace(profile.FSAPIKey) != "" { env = append(env, "DRIVE9_API_KEY="+strings.TrimSpace(profile.FSAPIKey)) } - if opts.IncludeTDCKeys { - if strings.TrimSpace(profile.TDCPublicKey) != "" { - env = append(env, "DRIVE9_PUBLIC_KEY="+strings.TrimSpace(profile.TDCPublicKey)) + if opts.IncludeTIKeys { + if strings.TrimSpace(profile.TiDBCloudPublicKey) != "" { + env = append(env, "DRIVE9_PUBLIC_KEY="+strings.TrimSpace(profile.TiDBCloudPublicKey)) } - if strings.TrimSpace(profile.TDCPrivateKey) != "" { - env = append(env, "DRIVE9_PRIVATE_KEY="+strings.TrimSpace(profile.TDCPrivateKey)) + if strings.TrimSpace(profile.TiDBCloudPrivateKey) != "" { + env = append(env, "DRIVE9_PRIVATE_KEY="+strings.TrimSpace(profile.TiDBCloudPrivateKey)) } } } @@ -205,7 +207,7 @@ func (r Runner) locateCompanion() (string, error) { return resolved, nil } } - return "", apperr.New("fs.companion_missing", "runtime", 1, "tdc fs requires the Drive9 companion binary; reinstall tdc or set TDC_DRIVE9_BIN to a compatible drive9 binary") + return "", apperr.New("fs.companion_missing", "runtime", 1, "ti fs requires the Drive9 companion binary; reinstall ti or set TI_DRIVE9_BIN to a compatible drive9 binary") } func (r Runner) homeDir(opts RunOptions) (string, error) { @@ -237,7 +239,7 @@ func (r Runner) resolver() endpoints.Resolver { func prepareHome(homeDir string) error { if err := os.MkdirAll(filepath.Join(homeDir, ".drive9"), 0o700); err != nil { - return apperr.Wrap("fs.companion_home", "runtime", 1, "prepare tdc fs companion home", err) + return apperr.Wrap("fs.companion_home", "runtime", 1, "prepare ti fs companion home", err) } return nil } @@ -297,8 +299,8 @@ func redactRunSecrets(message string, opts RunOptions) string { if opts.Profile != nil { for _, value := range []string{ opts.Profile.FSAPIKey, - opts.Profile.TDCPublicKey, - opts.Profile.TDCPrivateKey, + opts.Profile.TiDBCloudPublicKey, + opts.Profile.TiDBCloudPrivateKey, } { if strings.TrimSpace(value) == "" { continue diff --git a/internal/fswrap/runner_test.go b/internal/fswrap/runner_test.go index dfce1b3..2bebc7a 100644 --- a/internal/fswrap/runner_test.go +++ b/internal/fswrap/runner_test.go @@ -4,15 +4,19 @@ import ( "strings" "testing" - "github.com/tidbcloud/tdc/internal/api/endpoints" - "github.com/tidbcloud/tdc/internal/config" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + "github.com/tidbcloud/ti-cli/internal/config" ) -func TestDrive9EnvironmentSanitizesTDCSecrets(t *testing.T) { - t.Setenv("TDC_PUBLIC_KEY", "ambient-public") - t.Setenv("TDC_PRIVATE_KEY", "ambient-private") - t.Setenv("TDC_FS_TOKEN", "ambient-fs-token") - t.Setenv("TDC_VAULT_TOKEN", "ambient-vault-token") +func TestDrive9EnvironmentSanitizesTISecrets(t *testing.T) { + t.Setenv("TIDB_CLOUD_PUBLIC_KEY", "ambient-public") + t.Setenv("TIDB_CLOUD_PRIVATE_KEY", "ambient-private") + t.Setenv("TI_FS_TOKEN", "ambient-fs-token") + t.Setenv("TI_VAULT_TOKEN", "ambient-vault-token") + t.Setenv("TDC_PUBLIC_KEY", "legacy-public") + t.Setenv("TDC_PRIVATE_KEY", "legacy-private") + t.Setenv("TDC_FS_TOKEN", "legacy-fs-token") + t.Setenv("TDC_VAULT_TOKEN", "legacy-vault-token") t.Setenv("DRIVE9_API_KEY", "ambient-drive9-token") profile := &config.Profile{ Name: "default", @@ -21,8 +25,8 @@ func TestDrive9EnvironmentSanitizesTDCSecrets(t *testing.T) { FSCloudProvider: "aws", FSRegionCode: "us-east-1", FSAPIKey: "selected-fs-token", - TDCPublicKey: "selected-public", - TDCPrivateKey: "selected-private", + TiDBCloudPublicKey: "selected-public", + TiDBCloudPrivateKey: "selected-private", } runner := Runner{Resolver: endpoints.Resolver{FSBaseURLs: map[endpoints.ProviderRegion]string{ {Provider: "aws", Region: "us-east-1"}: "https://fs.test", @@ -37,7 +41,10 @@ func TestDrive9EnvironmentSanitizesTDCSecrets(t *testing.T) { t.Fatal(err) } values := environmentMap(env) - for _, key := range []string{"TDC_PUBLIC_KEY", "TDC_PRIVATE_KEY", "TDC_FS_TOKEN", "TDC_VAULT_TOKEN"} { + for _, key := range []string{ + "TIDB_CLOUD_PUBLIC_KEY", "TIDB_CLOUD_PRIVATE_KEY", "TI_FS_TOKEN", "TI_VAULT_TOKEN", + "TDC_PUBLIC_KEY", "TDC_PRIVATE_KEY", "TDC_FS_TOKEN", "TDC_VAULT_TOKEN", + } { if _, ok := values[key]; ok { t.Fatalf("%s leaked into companion environment", key) } @@ -52,7 +59,7 @@ func TestDrive9EnvironmentSanitizesTDCSecrets(t *testing.T) { t.Fatalf("data-plane environment included TiDB Cloud private key") } - env, err = runner.drive9Env(t.TempDir(), RunOptions{Profile: profile, IncludeTDCKeys: true}) + env, err = runner.drive9Env(t.TempDir(), RunOptions{Profile: profile, IncludeTIKeys: true}) if err != nil { t.Fatal(err) } diff --git a/internal/oplog/oplog.go b/internal/oplog/oplog.go index cd1e9f5..9bb2f45 100644 --- a/internal/oplog/oplog.go +++ b/internal/oplog/oplog.go @@ -26,23 +26,24 @@ type Config struct { } type Event struct { - Timestamp time.Time `json:"ts"` - Type string `json:"type"` - Version string `json:"version,omitempty"` - Commit string `json:"commit,omitempty"` - Profile string `json:"profile,omitempty"` - RegionCode string `json:"region_code,omitempty"` - Command string `json:"command,omitempty"` - FlagNames []string `json:"flag_names,omitempty"` - DurationMS int64 `json:"duration_ms,omitempty"` - ExitCode int `json:"exit_code,omitempty"` - ErrorCode string `json:"error_code,omitempty"` - ErrorCategory string `json:"error_category,omitempty"` - Service string `json:"service,omitempty"` - Operation string `json:"operation,omitempty"` - Method string `json:"method,omitempty"` - StatusCode int `json:"status_code,omitempty"` - RequestID string `json:"request_id,omitempty"` + Timestamp time.Time `json:"ts"` + Type string `json:"type"` + Version string `json:"version,omitempty"` + Commit string `json:"commit,omitempty"` + Profile string `json:"profile,omitempty"` + RegionCode string `json:"region_code,omitempty"` + Command string `json:"command,omitempty"` + FlagNames []string `json:"flag_names,omitempty"` + DurationMS int64 `json:"duration_ms,omitempty"` + ExitCode int `json:"exit_code,omitempty"` + ErrorCode string `json:"error_code,omitempty"` + ErrorCategory string `json:"error_category,omitempty"` + Service string `json:"service,omitempty"` + Operation string `json:"operation,omitempty"` + Method string `json:"method,omitempty"` + StatusCode int `json:"status_code,omitempty"` + RequestID string `json:"request_id,omitempty"` + DeprecatedEnvironmentVariables []string `json:"deprecated_environment_variables,omitempty"` } type Recorder interface { @@ -56,7 +57,7 @@ func (noopRecorder) Record(context.Context, Event) {} type contextKey struct{} func Path(homeDir string) string { - return filepath.Join(homeDir, ".tdc", "logs", "tdc.jsonl") + return filepath.Join(homeDir, ".ti", "logs", "ti.jsonl") } func WithRecorder(ctx context.Context, recorder Recorder) context.Context { diff --git a/internal/oplog/oplog_test.go b/internal/oplog/oplog_test.go index 708c1e0..9671d2b 100644 --- a/internal/oplog/oplog_test.go +++ b/internal/oplog/oplog_test.go @@ -19,7 +19,7 @@ func TestDisabledRecorderDoesNotCreateFile(t *testing.T) { MaxFiles: 2, }) - recorder.Record(context.Background(), Event{Type: "command", Command: "tdc help"}) + recorder.Record(context.Background(), Event{Type: "command", Command: "ti help"}) if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatalf("expected no log file, got err=%v", err) } @@ -38,7 +38,7 @@ func TestRecorderWritesJSONL(t *testing.T) { recorder.Record(context.Background(), Event{ Timestamp: time.Date(2026, 7, 14, 1, 2, 3, 0, time.UTC), Type: "command", - Command: "tdc db create-db-cluster", + Command: "ti db create-db-cluster", FlagNames: []string{"db-cluster-name", "dry-run"}, ExitCode: 0, DurationMS: 12, @@ -48,7 +48,7 @@ func TestRecorderWritesJSONL(t *testing.T) { if len(events) != 1 { t.Fatalf("expected one event, got %d", len(events)) } - if events[0].Command != "tdc db create-db-cluster" || events[0].FlagNames[0] != "db-cluster-name" { + if events[0].Command != "ti db create-db-cluster" || events[0].FlagNames[0] != "db-cluster-name" { t.Fatalf("unexpected event: %#v", events[0]) } if data, err := os.ReadFile(path); err != nil { @@ -69,7 +69,7 @@ func TestRecorderRotatesByTotalFileCount(t *testing.T) { }) for i := 0; i < 4; i++ { - recorder.Record(context.Background(), Event{Type: "command", Command: "tdc help"}) + recorder.Record(context.Background(), Event{Type: "command", Command: "ti help"}) } if _, err := os.Stat(path); err != nil { diff --git a/internal/organization/organization.go b/internal/organization/organization.go index b7ddf39..26fa342 100644 --- a/internal/organization/organization.go +++ b/internal/organization/organization.go @@ -9,12 +9,12 @@ import ( "text/tabwriter" "time" - "github.com/tidbcloud/tdc/internal/api" - "github.com/tidbcloud/tdc/internal/api/endpoints" - apiiam "github.com/tidbcloud/tdc/internal/api/iam" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/authz" - "github.com/tidbcloud/tdc/internal/config" + "github.com/tidbcloud/ti-cli/internal/api" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + apiiam "github.com/tidbcloud/ti-cli/internal/api/iam" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/authz" + "github.com/tidbcloud/ti-cli/internal/config" ) type Service struct { @@ -58,7 +58,7 @@ func (s Service) ListProjects(ctx context.Context, opts ListProjectsOptions) (Li Timeout: s.Timeout, Debug: s.Debug, DebugWriter: s.DebugWriter, - UserAgent: "tdc organization list-projects", + UserAgent: "ti organization list-projects", }) if err != nil { return ListProjectsResult{}, err diff --git a/internal/organization/organization_test.go b/internal/organization/organization_test.go index fce3df0..5825d03 100644 --- a/internal/organization/organization_test.go +++ b/internal/organization/organization_test.go @@ -7,9 +7,9 @@ import ( "strings" "testing" - "github.com/tidbcloud/tdc/internal/api/endpoints" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/config" + "github.com/tidbcloud/ti-cli/internal/api/endpoints" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/config" ) func TestListProjects(t *testing.T) { @@ -109,10 +109,10 @@ func TestListProjectsAuthAndPermissionErrors(t *testing.T) { func testProfile() *config.Profile { return &config.Profile{ - Name: "test", - CloudProvider: "aws", - RegionCode: "us-east-1", - TDCPublicKey: "public", - TDCPrivateKey: "private", + Name: "test", + CloudProvider: "aws", + RegionCode: "us-east-1", + TiDBCloudPublicKey: "public", + TiDBCloudPrivateKey: "private", } } diff --git a/internal/output/output.go b/internal/output/output.go index 15c04c0..533562d 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -6,8 +6,8 @@ import ( "io" "strings" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/query" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/query" ) const ( diff --git a/internal/output/output_test.go b/internal/output/output_test.go index 18968bf..90333c9 100644 --- a/internal/output/output_test.go +++ b/internal/output/output_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/tidbcloud/tdc/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/apperr" ) type testHuman struct { diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 0767ec1..6a8d34e 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -10,7 +10,8 @@ import ( "strings" "github.com/pelletier/go-toml/v2" - "github.com/tidbcloud/tdc/internal/config/store" + "github.com/tidbcloud/ti-cli/internal/config/envcompat" + "github.com/tidbcloud/ti-cli/internal/config/store" ) const ( @@ -20,8 +21,8 @@ const ( DefaultMaxFiles = 5 settingsFileMode os.FileMode = 0o600 profileConfigFileMode os.FileMode = 0o644 - tdcDirMode os.FileMode = 0o700 - envLogging = "TDC_LOGGING" + tiDirMode os.FileMode = 0o700 + envLogging = "TI_LOGGING" ) type Document struct { @@ -47,7 +48,7 @@ type LoggingConfig struct { } func Path(homeDir string) string { - return filepath.Join(homeDir, store.TDCDirName, FileName) + return filepath.Join(homeDir, store.TIDirName, FileName) } func Load(homeDir string) (Document, bool, error) { @@ -63,7 +64,12 @@ func Load(homeDir string) (Document, bool, error) { func ResolveLogging(homeDir string, env map[string]string) (LoggingConfig, error) { cfg := defaultLoggingConfig() - if value, ok := envValue(env, envLogging); ok { + value, hasOverride, _, resolveErr := envcompat.ResolveNames(env, envLogging, "TDC_LOGGING") + if resolveErr != nil { + cfg.Enabled = false + return cfg, resolveErr + } + if hasOverride { enabled, valid := parseBool(value) if !valid { cfg.Enabled = false @@ -85,7 +91,7 @@ func ResolveLogging(homeDir string, env map[string]string) (LoggingConfig, error return cfg, err } - if _, ok := envValue(env, envLogging); ok { + if hasOverride { cfg.Enabled = true doc, exists, err := read(homeDir) if err != nil { @@ -265,8 +271,8 @@ func createSettings(homeDir string, doc Document) error { if err != nil { return fmt.Errorf("marshal settings: %w", err) } - dir := filepath.Join(homeDir, store.TDCDirName) - if err := os.MkdirAll(dir, tdcDirMode); err != nil { + dir := filepath.Join(homeDir, store.TIDirName) + if err := os.MkdirAll(dir, tiDirMode); err != nil { return fmt.Errorf("create settings directory: %w", err) } temp, err := os.CreateTemp(dir, ".preferences.tmp-*") @@ -329,14 +335,6 @@ func writeTOMLAtomic(path string, value any, mode os.FileMode) error { return os.Chmod(path, mode) } -func envValue(env map[string]string, key string) (string, bool) { - if env != nil { - value, ok := env[key] - return value, ok - } - return os.LookupEnv(key) -} - func parseBool(value string) (bool, bool) { switch strings.ToLower(strings.TrimSpace(value)) { case "on", "true", "1", "yes": diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 1a67c27..94e292c 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/tidbcloud/tdc/internal/config/store" + "github.com/tidbcloud/ti-cli/internal/config/store" ) func TestLoadMissingSettingsDoesNotCreateFile(t *testing.T) { @@ -34,11 +34,11 @@ func TestLoadMissingSettingsDoesNotCreateFile(t *testing.T) { func TestPathUsesHiddenPreferencesAndIgnoresUnshippedSettingsPath(t *testing.T) { home := t.TempDir() - wantPath := filepath.Join(home, ".tdc", ".preferences") + wantPath := filepath.Join(home, ".ti", ".preferences") if got := Path(home); got != wantPath { t.Fatalf("Path(%q) = %q, want %q", home, got, wantPath) } - writeFile(t, filepath.Join(home, ".tdc", "settings"), "[logging]\nenabled = false\n", 0o600) + writeFile(t, filepath.Join(home, ".ti", "settings"), "[logging]\nenabled = false\n", 0o600) cfg, err := ResolveLogging(home, map[string]string{}) if err != nil { @@ -110,7 +110,7 @@ max_files = 3 {value: "no", want: false}, } { t.Run(test.value, func(t *testing.T) { - cfg, err := ResolveLogging(home, map[string]string{"TDC_LOGGING": test.value}) + cfg, err := ResolveLogging(home, map[string]string{"TI_LOGGING": test.value}) if err != nil { t.Fatalf("override failed: %v", err) } @@ -128,7 +128,7 @@ func TestResolveLoggingDisabledEnvironmentDoesNotReadMalformedSettings(t *testin home := t.TempDir() writeFile(t, Path(home), "not valid toml = [", 0o600) writeFile(t, store.ConfigPath(home), "not valid toml = [", 0o644) - cfg, err := ResolveLogging(home, map[string]string{"TDC_LOGGING": "off"}) + cfg, err := ResolveLogging(home, map[string]string{"TI_LOGGING": "off"}) if err != nil { t.Fatalf("disabled environment should short-circuit settings: %v", err) } @@ -141,8 +141,8 @@ func TestResolveLoggingInvalidEnvironmentFailsClosed(t *testing.T) { home := t.TempDir() writeFile(t, Path(home), "not valid toml = [", 0o600) writeFile(t, store.ConfigPath(home), "not valid toml = [", 0o644) - cfg, err := ResolveLogging(home, map[string]string{"TDC_LOGGING": "sometimes"}) - if err == nil || !strings.Contains(err.Error(), "invalid TDC_LOGGING") { + cfg, err := ResolveLogging(home, map[string]string{"TI_LOGGING": "sometimes"}) + if err == nil || !strings.Contains(err.Error(), "invalid TI_LOGGING") { t.Fatalf("expected invalid environment error, got %v", err) } if cfg.Enabled { @@ -188,7 +188,7 @@ enabled = false max_file_mb = 3 max_files = 2 `, 0o644) - credentials := []byte("[default]\ntdc_public_key = 'public'\ntdc_private_key = 'private'\n") + credentials := []byte("[default]\ntidb_cloud_public_key = 'public'\ntidb_cloud_private_key = 'private'\n") writeFile(t, store.CredentialsPath(home), string(credentials), 0o600) doc, exists, err := Load(home) diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go index 261ac1d..52c238f 100644 --- a/internal/telemetry/telemetry.go +++ b/internal/telemetry/telemetry.go @@ -19,17 +19,18 @@ import ( "time" "unicode/utf8" - "github.com/tidbcloud/tdc/internal/config/store" - "github.com/tidbcloud/tdc/internal/settings" - "github.com/tidbcloud/tdc/internal/version" + "github.com/tidbcloud/ti-cli/internal/config/envcompat" + "github.com/tidbcloud/ti-cli/internal/config/store" + "github.com/tidbcloud/ti-cli/internal/settings" + "github.com/tidbcloud/ti-cli/internal/version" ) const ( - EnvironmentVariable = "TDC_TELEMETRY" - TagEnvironmentVariable = "TDC_TELEMETRY_TAG" - ExtraEnvironmentVariable = "TDC_TELEMETRY_EXTRA" + EnvironmentVariable = "TI_TELEMETRY" + TagEnvironmentVariable = "TI_TELEMETRY_TAG" + ExtraEnvironmentVariable = "TI_TELEMETRY_EXTRA" installationIDFile = ".telemetry-installation-id" - eventName = "tdc.command.finished" + eventName = "ti.command.finished" schemaVersion = 2 deliveryTimeout = 3 * time.Second maxTagBytes = 128 @@ -37,7 +38,7 @@ const ( maxExtraDepth = 8 ) -var installationIDPattern = regexp.MustCompile(`^tdc_[A-Za-z0-9_-]{22}$`) +var installationIDPattern = regexp.MustCompile(`^(?:ti|tdc)_[A-Za-z0-9_-]{22}$`) type Config struct { Eligible bool @@ -102,7 +103,7 @@ type wireEvent struct { } func InstallationIDPath(homeDir string) string { - return filepath.Join(homeDir, store.TDCDirName, installationIDFile) + return filepath.Join(homeDir, store.TIDirName, installationIDFile) } func Start(cfg Config) *Session { @@ -117,11 +118,21 @@ func Start(cfg Config) *Session { if !enabled { return nil } - tag, tagOK := normalizeTag(envValue(cfg.Environment, TagEnvironmentVariable)) + tagValue, tagExists, _, err := envcompat.ResolveNames(cfg.Environment, TagEnvironmentVariable, "TDC_TELEMETRY_TAG") + if err != nil { + debug(cfg.Debug, cfg.DebugWriter, "telemetry disabled because environment metadata conflicts") + return nil + } + tag, tagOK := normalizeTag(tagValue, tagExists) if !tagOK { debug(cfg.Debug, cfg.DebugWriter, "telemetry tag was omitted because it is invalid") } - extra, extraOK := normalizeExtra(envValue(cfg.Environment, ExtraEnvironmentVariable)) + extraValue, extraExists, _, err := envcompat.ResolveNames(cfg.Environment, ExtraEnvironmentVariable, "TDC_TELEMETRY_EXTRA") + if err != nil { + debug(cfg.Debug, cfg.DebugWriter, "telemetry disabled because environment metadata conflicts") + return nil + } + extra, extraOK := normalizeExtra(extraValue, extraExists) if !extraOK { debug(cfg.Debug, cfg.DebugWriter, "telemetry extra metadata was omitted because it is invalid") } @@ -219,7 +230,7 @@ func (s *Session) Finish(input EventInput) { return } request.Header.Set("Content-Type", "application/json") - request.Header.Set("User-Agent", "tdc/"+normalizedVersion(s.info.Version)) + request.Header.Set("User-Agent", "ti/"+normalizedVersion(s.info.Version)) response, err := s.client.Do(request) if err != nil { debug(s.debug, s.debugWriter, "telemetry delivery failed; the command result was not affected") @@ -304,7 +315,11 @@ var prohibitedMetadataKeys = map[string]struct{}{ } func resolveEnabled(cfg Config) (bool, error) { - if raw, exists := envValue(cfg.Environment, EnvironmentVariable); exists { + raw, exists, _, err := envcompat.ResolveNames(cfg.Environment, EnvironmentVariable, "TDC_TELEMETRY") + if err != nil { + return false, err + } + if exists { enabled, valid := parseOverride(raw) if !valid { return false, fmt.Errorf("invalid telemetry override") @@ -336,7 +351,7 @@ func loadOrCreateInstallationID(homeDir string) (string, error) { if err != nil || exists { return id, err } - id, err = randomIdentifier("tdc_") + id, err = randomIdentifier("ti_") if err != nil { return "", err } @@ -496,6 +511,6 @@ func runtimeValue(value, fallback string) string { func debug(enabled bool, writer io.Writer, message string) { if enabled && writer != nil { - _, _ = fmt.Fprintln(writer, "tdc [DEBUG]: "+message) + _, _ = fmt.Fprintln(writer, "ti [DEBUG]: "+message) } } diff --git a/internal/telemetry/telemetry_test.go b/internal/telemetry/telemetry_test.go index 705ea58..c46195c 100644 --- a/internal/telemetry/telemetry_test.go +++ b/internal/telemetry/telemetry_test.go @@ -14,9 +14,9 @@ import ( "time" "unicode/utf8" - "github.com/tidbcloud/tdc/internal/config/store" - "github.com/tidbcloud/tdc/internal/settings" - "github.com/tidbcloud/tdc/internal/version" + "github.com/tidbcloud/ti-cli/internal/config/store" + "github.com/tidbcloud/ti-cli/internal/settings" + "github.com/tidbcloud/ti-cli/internal/version" ) func TestStartResolutionAndStateCreation(t *testing.T) { @@ -240,7 +240,7 @@ func TestInstallationIDIsPrivateStableAndRaceSafe(t *testing.T) { func TestMalformedOrUnreadableInstallationIDFailsClosed(t *testing.T) { t.Run("malformed", func(t *testing.T) { home := t.TempDir() - before := "tdc_not-valid!\n" + before := "ti_not-valid!\n" writeTestFile(t, InstallationIDPath(home), before, 0o600) if session := Start(enabledConfig(home)); session != nil { t.Fatal("malformed identity should disable telemetry") @@ -271,7 +271,7 @@ func TestFinishSendsOnlyAllowlistedEventFields(t *testing.T) { if got := request.Header.Get("Content-Type"); got != "application/json" { t.Errorf("Content-Type = %q", got) } - if got := request.Header.Get("User-Agent"); got != "tdc/0.2.0" { + if got := request.Header.Get("User-Agent"); got != "ti/0.2.0" { t.Errorf("User-Agent = %q", got) } body, _ := io.ReadAll(request.Body) @@ -286,8 +286,8 @@ func TestFinishSendsOnlyAllowlistedEventFields(t *testing.T) { cfg.Now = func() time.Time { return time.Date(2026, 7, 28, 1, 2, 3, 0, time.UTC) } session := Start(cfg) session.Finish(EventInput{ - CommandPath: "tdc db execute-sql-statement", - FlagNames: []string{"sql", "db-cluster-id", "tdc-private-key"}, + CommandPath: "ti db execute-sql-statement", + FlagNames: []string{"sql", "db-cluster-id", "tidb-cloud-private-key"}, ExitCode: 2, ErrorCode: "db.invalid_sql", Duration: 182 * time.Millisecond, @@ -307,11 +307,11 @@ func TestFinishSendsOnlyAllowlistedEventFields(t *testing.T) { } events := decoded["events"].([]any) event := events[0].(map[string]any) - if event["command_path"] != "tdc db execute-sql-statement" || event["error_code"] != "db.invalid_sql" { + if event["command_path"] != "ti db execute-sql-statement" || event["error_code"] != "db.invalid_sql" { t.Fatalf("unexpected event: %#v", event) } flags := event["flag_names"].([]any) - if strings.Join([]string{flags[0].(string), flags[1].(string), flags[2].(string)}, ",") != "db-cluster-id,sql,tdc-private-key" { + if strings.Join([]string{flags[0].(string), flags[1].(string), flags[2].(string)}, ",") != "db-cluster-id,sql,tidb-cloud-private-key" { t.Fatalf("flags were not canonical names only: %#v", flags) } } @@ -334,7 +334,7 @@ func TestTelemetryEnvironmentMetadataIsBoundedAndNotPersisted(t *testing.T) { if session == nil { t.Fatal("telemetry session was not created") } - session.Finish(EventInput{CommandPath: "tdc db list-db-clusters"}) + session.Finish(EventInput{CommandPath: "ti db list-db-clusters"}) body := <-requests var payload struct { SchemaVersion int `json:"schema_version"` @@ -355,7 +355,7 @@ func TestTelemetryEnvironmentMetadataIsBoundedAndNotPersisted(t *testing.T) { if got := string(payload.Events[0].Extra); got != `{"campaign":"launch","runtime":"e2b"}` { t.Fatalf("extra = %s", got) } - for _, path := range []string{settings.Path(home), store.ConfigPath(home), filepath.Join(home, ".tdc", "credentials")} { + for _, path := range []string{settings.Path(home), store.ConfigPath(home), filepath.Join(home, ".ti", "credentials")} { if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatalf("metadata unexpectedly persisted to %s: %v", path, err) } @@ -379,7 +379,7 @@ func TestInvalidTelemetryEnvironmentMetadataIsOmittedWithoutLeaking(t *testing.T cfg.DebugWriter = &diagnostic cfg.Environment[TagEnvironmentVariable] = string([]byte{0xff}) cfg.Environment[ExtraEnvironmentVariable] = `{"password":"must-not-leak"}` - Start(cfg).Finish(EventInput{CommandPath: "tdc db list-db-clusters"}) + Start(cfg).Finish(EventInput{CommandPath: "ti db list-db-clusters"}) body := <-requests if strings.Contains(string(body), "must-not-leak") || strings.Contains(diagnostic.String(), "must-not-leak") { t.Fatalf("metadata leaked: payload=%s debug=%s", body, diagnostic.String()) @@ -475,7 +475,7 @@ func TestDeliveryFailuresAreSilentByDefault(t *testing.T) { cfg.Endpoint = server.URL var debug strings.Builder cfg.DebugWriter = &debug - Start(cfg).Finish(EventInput{CommandPath: "tdc organization list-projects"}) + Start(cfg).Finish(EventInput{CommandPath: "ti organization list-projects"}) server.Close() if debug.Len() != 0 { t.Fatalf("status %d produced normal output: %q", status, debug.String()) @@ -497,7 +497,7 @@ func TestDeliveryDoesNotFollowRedirects(t *testing.T) { defer server.Close() cfg := enabledConfig(t.TempDir()) cfg.Endpoint = server.URL - Start(cfg).Finish(EventInput{CommandPath: "tdc organization list-projects"}) + Start(cfg).Finish(EventInput{CommandPath: "ti organization list-projects"}) if redirected { t.Fatal("telemetry followed a redirect away from its configured endpoint") } diff --git a/internal/telemetrybackend/client_contract_test.go b/internal/telemetrybackend/client_contract_test.go index 1798b47..3542103 100644 --- a/internal/telemetrybackend/client_contract_test.go +++ b/internal/telemetrybackend/client_contract_test.go @@ -7,8 +7,8 @@ import ( "testing" "time" - clienttelemetry "github.com/tidbcloud/tdc/internal/telemetry" - "github.com/tidbcloud/tdc/internal/version" + clienttelemetry "github.com/tidbcloud/ti-cli/internal/telemetry" + "github.com/tidbcloud/ti-cli/internal/version" ) func TestCLIEventMatchesBackendContract(t *testing.T) { @@ -42,7 +42,7 @@ func TestCLIEventMatchesBackendContract(t *testing.T) { t.Fatal("telemetry session was not created") } session.Finish(clienttelemetry.EventInput{ - CommandPath: "tdc fs create-file-system", + CommandPath: "ti fs create-file-system", FlagNames: []string{"file-system-name", "wait"}, ExitCode: 0, Duration: 150 * time.Millisecond, diff --git a/internal/telemetrybackend/event.go b/internal/telemetrybackend/event.go index 193226b..c84866f 100644 --- a/internal/telemetrybackend/event.go +++ b/internal/telemetrybackend/event.go @@ -21,8 +21,8 @@ const ( var ( eventIDPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{16,64}$`) - installationIDPattern = regexp.MustCompile(`^tdc_[A-Za-z0-9_-]{16,96}$`) - commandPathPattern = regexp.MustCompile(`^tdc(?: [a-z][a-z0-9-]{0,63}){0,2}$`) + installationIDPattern = regexp.MustCompile(`^(?:ti|tdc)_[A-Za-z0-9_-]{16,96}$`) + commandPathPattern = regexp.MustCompile(`^(?:ti|tdc)(?: [a-z][a-z0-9-]{0,63}){0,2}$`) flagNamePattern = regexp.MustCompile(`^[a-z][a-z0-9-]{0,63}$`) errorCodePattern = regexp.MustCompile(`^[A-Za-z0-9._-]{0,64}$`) cliVersionPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9.+_-]{0,63}$`) @@ -174,7 +174,7 @@ func validateEvent(raw wireEvent, schemaVersion int, receivedAt time.Time) (Even if !eventIDPattern.MatchString(raw.EventID) { return Event{}, errors.New("invalid event_id") } - if raw.EventName != "tdc.command.finished" { + if raw.EventName != "ti.command.finished" && raw.EventName != "tdc.command.finished" { return Event{}, errors.New("invalid event_name") } occurredAt, err := time.Parse(time.RFC3339Nano, raw.OccurredAt) diff --git a/internal/telemetrybackend/event_test.go b/internal/telemetrybackend/event_test.go index 3e4e024..1804d3d 100644 --- a/internal/telemetrybackend/event_test.go +++ b/internal/telemetrybackend/event_test.go @@ -14,7 +14,7 @@ func TestDecodeAndValidateBatchAcceptsAllowlistedEvent(t *testing.T) { if err != nil { t.Fatalf("decodeAndValidateBatch returned error: %v", err) } - if len(events) != 1 || events[0].EventName != "tdc.command.finished" { + if len(events) != 1 || events[0].EventName != "ti.command.finished" { t.Fatalf("events = %#v", events) } if !events[0].ReceivedAt.Equal(receivedAt.UTC()) { @@ -22,6 +22,19 @@ func TestDecodeAndValidateBatchAcceptsAllowlistedEvent(t *testing.T) { } } +func TestDecodeAndValidateBatchAcceptsLegacyTDCEvent(t *testing.T) { + body := bytes.ReplaceAll(validRequestBody(), []byte("ti.command.finished"), []byte("tdc.command.finished")) + body = bytes.ReplaceAll(body, []byte("ti_01j0a0n8m9f4q2x6cn0b9q3k3z"), []byte("tdc_01j0a0n8m9f4q2x6cn0b9q3k3z")) + body = bytes.ReplaceAll(body, []byte("ti db"), []byte("tdc db")) + events, err := decodeAndValidateBatch(body, 20, time.Now()) + if err != nil { + t.Fatalf("legacy event should remain accepted during v0.2.x: %v", err) + } + if len(events) != 1 || events[0].EventName != "tdc.command.finished" { + t.Fatalf("unexpected legacy event: %#v", events) + } +} + func TestDecodeAndValidateBatchSupportsV1AndV2Metadata(t *testing.T) { receivedAt := time.Now() for _, test := range []struct { @@ -166,7 +179,7 @@ func TestDecodeAndValidateBatchRejectsInvalidEnumsAndLimits(t *testing.T) { }{ {"event_name", "custom.event"}, {"command_path", "rm -rf"}, - {"command_path", "tdc db list-db-clusters select secret"}, + {"command_path", "ti db list-db-clusters select secret"}, {"cloud_provider", "gcp"}, {"region_code", "us-east-1"}, {"install_source", "curl"}, diff --git a/internal/telemetrybackend/migrations/migrations.go b/internal/telemetrybackend/migrations/migrations.go index ae7cb9d..52e243a 100644 --- a/internal/telemetrybackend/migrations/migrations.go +++ b/internal/telemetrybackend/migrations/migrations.go @@ -9,7 +9,7 @@ import ( "github.com/pressly/goose/v3" ) -const VersionTable = "tdc_telemetry_schema_migrations" +const VersionTable = "ti_telemetry_schema_migrations" //go:embed sql/*.sql var files embed.FS diff --git a/internal/telemetrybackend/posthog.go b/internal/telemetrybackend/posthog.go index fa2672d..c326572 100644 --- a/internal/telemetrybackend/posthog.go +++ b/internal/telemetrybackend/posthog.go @@ -47,7 +47,7 @@ type postHogProperties struct { Arch string `json:"arch"` InstallSource string `json:"install_source"` ProfileSource string `json:"profile_source"` - TDCEnvironment string `json:"tdc_environment"` + TIEnvironment string `json:"ti_environment"` Tag string `json:"tag,omitempty"` Extra json.RawMessage `json:"extra,omitempty"` } @@ -110,7 +110,7 @@ func (s *PostHogSink) Write(ctx context.Context, events []Event) error { Arch: event.Arch, InstallSource: event.InstallSource, ProfileSource: event.ProfileSource, - TDCEnvironment: s.environment, + TIEnvironment: s.environment, Tag: event.Tag, Extra: event.Extra, }, diff --git a/internal/telemetrybackend/server.go b/internal/telemetrybackend/server.go index a891d41..c88047b 100644 --- a/internal/telemetrybackend/server.go +++ b/internal/telemetrybackend/server.go @@ -263,8 +263,8 @@ func validContentType(raw string) bool { } func validUserAgent(value string) bool { - return strings.HasPrefix(value, "tdc/") && - len(value) > len("tdc/") && + return strings.HasPrefix(value, "ti/") && + len(value) > len("ti/") && len(value) <= 128 && !strings.ContainsAny(value, "\r\n\t ") } diff --git a/internal/telemetrybackend/server_test.go b/internal/telemetrybackend/server_test.go index dee5097..900075b 100644 --- a/internal/telemetrybackend/server_test.go +++ b/internal/telemetrybackend/server_test.go @@ -86,7 +86,7 @@ func TestServerRejectsInvalidRequestsWithGenericErrors(t *testing.T) { server := NewServer(cfg, batcher, readinessStub{}, readinessStub{}, discardLogger(), nil) request := httptest.NewRequest(test.method, "/v1/telemetry/batch", bytes.NewReader(test.body)) request.Header.Set("Content-Type", "application/json") - request.Header.Set("User-Agent", "tdc/0.2.0") + request.Header.Set("User-Agent", "ti/0.2.0") if test.header != nil { test.header(request) } @@ -119,7 +119,7 @@ func TestServerDoesNotLogRequestBodiesOrInstallationIDs(t *testing.T) { if response.Code != http.StatusBadRequest { t.Fatalf("status = %d", response.Code) } - for _, prohibited := range []string{"highly-sensitive", "tdc_01j0a0n8m9f4q2x6cn0b9q3k3z"} { + for _, prohibited := range []string{"highly-sensitive", "ti_01j0a0n8m9f4q2x6cn0b9q3k3z"} { if strings.Contains(logs.String(), prohibited) { t.Fatalf("logs contain prohibited value %q: %s", prohibited, logs.String()) } @@ -216,7 +216,7 @@ func TestServerExportsAggregateMetricsWithoutEventValues(t *testing.T) { !strings.Contains(response.Body.String(), "telemetry_buffer_events 1") { t.Fatalf("metrics body = %s", response.Body.String()) } - if strings.Contains(response.Body.String(), "tdc_01j0a0n8m9f4q2x6cn0b9q3k3z") { + if strings.Contains(response.Body.String(), "ti_01j0a0n8m9f4q2x6cn0b9q3k3z") { t.Fatalf("metrics exposed installation ID: %s", response.Body.String()) } } @@ -254,7 +254,7 @@ func performBatchRequest(handler http.Handler, body []byte) *httptest.ResponseRe func newBatchRequest(body []byte) *http.Request { request := httptest.NewRequest(http.MethodPost, "/v1/telemetry/batch", bytes.NewReader(body)) request.Header.Set("Content-Type", "application/json") - request.Header.Set("User-Agent", "tdc/0.2.0") + request.Header.Set("User-Agent", "ti/0.2.0") return request } diff --git a/internal/telemetrybackend/test_helpers_test.go b/internal/telemetrybackend/test_helpers_test.go index 0e8b1ec..6e15220 100644 --- a/internal/telemetrybackend/test_helpers_test.go +++ b/internal/telemetrybackend/test_helpers_test.go @@ -34,11 +34,11 @@ func testConfig() Config { func testEvent() Event { return Event{ EventID: "018f7e67-8fe4-7cc2-9ca5-2d3536c7fb44", - EventName: "tdc.command.finished", + EventName: "ti.command.finished", OccurredAt: time.Date(2026, 7, 24, 12, 0, 0, 0, time.UTC), ReceivedAt: time.Date(2026, 7, 24, 12, 0, 1, 0, time.UTC), - AnonymousInstallationID: "tdc_01j0a0n8m9f4q2x6cn0b9q3k3z", - CommandPath: "tdc fs create-file-system", + AnonymousInstallationID: "ti_01j0a0n8m9f4q2x6cn0b9q3k3z", + CommandPath: "ti fs create-file-system", FlagNames: []string{"file-system-name", "output"}, ExitCode: 0, ErrorCode: "", @@ -62,10 +62,10 @@ func validRequestBody() []byte { SentAt: "2026-07-24T12:00:00Z", Events: []wireEvent{{ EventID: "018f7e67-8fe4-7cc2-9ca5-2d3536c7fb44", - EventName: "tdc.command.finished", + EventName: "ti.command.finished", OccurredAt: "2026-07-24T12:00:00Z", - AnonymousInstallationID: "tdc_01j0a0n8m9f4q2x6cn0b9q3k3z", - CommandPath: "tdc fs create-file-system", + AnonymousInstallationID: "ti_01j0a0n8m9f4q2x6cn0b9q3k3z", + CommandPath: "ti fs create-file-system", FlagNames: []string{"file-system-name", "output"}, ExitCode: &exitCode, DurationMS: &duration, diff --git a/internal/update/update.go b/internal/update/update.go index 5de18ed..018846a 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -21,13 +21,13 @@ import ( "strings" "time" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/version" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/version" ) const ( - DefaultReleaseAPIBaseURL = "https://api.github.com/repos/tidbcloud/tdc" - checksumAssetName = "tdc_checksums.txt" + DefaultReleaseAPIBaseURL = "https://api.github.com/repos/tidbcloud/ti-cli" + checksumAssetName = "ti_checksums.txt" drive9ReleaseBaseURL = "https://drive9.ai/releases" drive9ChecksumAssetName = "checksums.txt" ) @@ -105,11 +105,11 @@ type ApplyResult struct { func (r ApplyResult) Human() string { var b strings.Builder if r.DryRun { - fmt.Fprintf(&b, "Dry run: update tdc from %s to %s\n", r.CurrentVersion, r.TargetVersion) + fmt.Fprintf(&b, "Dry run: update ti from %s to %s\n", r.CurrentVersion, r.TargetVersion) } else if r.Updated { - fmt.Fprintf(&b, "Updated tdc from %s to %s\n", r.CurrentVersion, r.TargetVersion) + fmt.Fprintf(&b, "Updated ti from %s to %s\n", r.CurrentVersion, r.TargetVersion) } else { - fmt.Fprintf(&b, "tdc is already at %s\n", r.CurrentVersion) + fmt.Fprintf(&b, "ti is already at %s\n", r.CurrentVersion) } fmt.Fprintf(&b, "Install source: %s\n", r.InstallSource) fmt.Fprintf(&b, "Target path: %s\n", r.TargetPath) @@ -187,7 +187,7 @@ func Apply(ctx context.Context, info version.Info, opts ApplyOptions) (ApplyResu "update.unknown_install", "runtime", 1, - fmt.Sprintf("tdc install source %q is not owned by tdc; reinstall with scripts/install.sh or scripts/install.ps1 before using tdc update", source.Name), + fmt.Sprintf("ti install source %q is not owned by ti; reinstall with scripts/install.sh or scripts/install.ps1 before using ti update", source.Name), ) } @@ -196,7 +196,7 @@ func Apply(ctx context.Context, info version.Info, opts ApplyOptions) (ApplyResu "update.unsupported_platform", "runtime", 1, - "tdc update cannot safely replace the running Windows executable yet; rerun the PowerShell installer for the target version", + "ti update cannot safely replace the running Windows executable yet; rerun the PowerShell installer for the target version", ) } @@ -215,7 +215,7 @@ func Apply(ctx context.Context, info version.Info, opts ApplyOptions) (ApplyResu "update.no_update_available", "runtime", 1, - fmt.Sprintf("The latest version of tdc (%s) is already installed", info.Version), + fmt.Sprintf("The latest version of ti (%s) is already installed", info.Version), ) } @@ -271,7 +271,7 @@ func Apply(ctx context.Context, info version.Info, opts ApplyOptions) (ApplyResu "update.permission_denied", "runtime", 1, - fmt.Sprintf("tdc cannot update the protected installation at %s; rerun the installer to migrate to ~/.tdc/bin", targetPath), + fmt.Sprintf("ti cannot update the protected installation at %s; rerun the installer to migrate to ~/.ti/bin", targetPath), ) } @@ -294,18 +294,18 @@ func Apply(ctx context.Context, info version.Info, opts ApplyOptions) (ApplyResu } defer companionCleanup() - stagedTDC, err := stageBinary(extracted, targetPath) + stagedTI, err := stageBinary(extracted, targetPath) if err != nil { return ApplyResult{}, err } - defer os.Remove(stagedTDC) + defer os.Remove(stagedTI) companionTarget := companionPathForExecutable(targetPath) stagedCompanion, err := stageBinary(companion.Path, companionTarget) if err != nil { return ApplyResult{}, err } defer os.Remove(stagedCompanion) - if err := replaceBinary(ctx, stagedTDC, targetPath); err != nil { + if err := replaceBinary(ctx, stagedTI, targetPath); err != nil { return ApplyResult{}, err } if err := replaceFile(stagedCompanion, companionTarget); err != nil { @@ -318,7 +318,7 @@ func Apply(ctx context.Context, info version.Info, opts ApplyOptions) (ApplyResu func newClient(baseURL string, httpClient *http.Client) client { if strings.TrimSpace(baseURL) == "" { - baseURL = strings.TrimRight(os.Getenv("TDC_RELEASE_API_BASE_URL"), "/") + baseURL = strings.TrimRight(os.Getenv("TI_RELEASE_API_BASE_URL"), "/") } if strings.TrimSpace(baseURL) == "" { baseURL = DefaultReleaseAPIBaseURL @@ -363,7 +363,7 @@ func (c client) download(ctx context.Context, url string) ([]byte, error) { if err != nil { return nil, apperr.Wrap("update.invalid_url", "runtime", 1, "build release request", err) } - req.Header.Set("User-Agent", "tdc") + req.Header.Set("User-Agent", "ti") req.Header.Set("Accept", "application/json, application/octet-stream") resp, err := c.httpClient.Do(req) if err != nil { @@ -424,12 +424,12 @@ func artifactName(goos, goarch string) (string, error) { if goarch != "amd64" && goarch != "arm64" { return "", unsupportedTarget(goos, goarch) } - return fmt.Sprintf("tdc_%s_%s.tar.gz", goos, goarch), nil + return fmt.Sprintf("ti_%s_%s.tar.gz", goos, goarch), nil case "windows": if goarch != "amd64" { return "", unsupportedTarget(goos, goarch) } - return fmt.Sprintf("tdc_%s_%s.zip", goos, goarch), nil + return fmt.Sprintf("ti_%s_%s.zip", goos, goarch), nil default: return "", unsupportedTarget(goos, goarch) } @@ -440,7 +440,7 @@ func unsupportedTarget(goos, goarch string) error { "update.unsupported_target", "runtime", 1, - fmt.Sprintf("no tdc release artifact is defined for %s/%s", goos, goarch), + fmt.Sprintf("no ti release artifact is defined for %s/%s", goos, goarch), ) } @@ -493,16 +493,16 @@ func isHexSHA256(value string) bool { } func extractBinary(archiveBytes []byte, artifactName string) (string, func(), error) { - tempDir, err := os.MkdirTemp("", "tdc-update-*") + tempDir, err := os.MkdirTemp("", "ti-update-*") if err != nil { return "", func() {}, apperr.Wrap("update.temp_dir", "runtime", 1, "create update temp directory", err) } cleanup := func() { _ = os.RemoveAll(tempDir) } - targetName := "tdc" + targetName := "ti" if strings.HasSuffix(artifactName, ".zip") { - targetName = "tdc.exe" + targetName = "ti.exe" } targetPath := filepath.Join(tempDir, targetName) var extractErr error @@ -517,7 +517,7 @@ func extractBinary(archiveBytes []byte, artifactName string) (string, func(), er } if err := os.Chmod(targetPath, 0o755); err != nil { cleanup() - return "", func() {}, apperr.Wrap("update.extract_artifact", "runtime", 1, "make downloaded tdc executable", err) + return "", func() {}, apperr.Wrap("update.extract_artifact", "runtime", 1, "make downloaded ti executable", err) } return targetPath, cleanup, nil } @@ -542,7 +542,7 @@ func extractTarGzBinary(archiveBytes []byte, targetName, targetPath string) erro } return writeExtractedBinary(targetPath, tr) } - return apperr.New("update.extract_artifact", "runtime", 1, "release archive did not contain tdc binary") + return apperr.New("update.extract_artifact", "runtime", 1, "release archive did not contain ti binary") } func extractZipBinary(archiveBytes []byte, targetName, targetPath string) error { @@ -556,22 +556,22 @@ func extractZipBinary(archiveBytes []byte, targetName, targetPath string) error } rc, err := file.Open() if err != nil { - return apperr.Wrap("update.extract_artifact", "runtime", 1, "open zipped tdc binary", err) + return apperr.Wrap("update.extract_artifact", "runtime", 1, "open zipped ti binary", err) } defer rc.Close() return writeExtractedBinary(targetPath, rc) } - return apperr.New("update.extract_artifact", "runtime", 1, "release archive did not contain tdc binary") + return apperr.New("update.extract_artifact", "runtime", 1, "release archive did not contain ti binary") } func writeExtractedBinary(targetPath string, r io.Reader) error { file, err := os.OpenFile(targetPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o755) if err != nil { - return apperr.Wrap("update.extract_artifact", "runtime", 1, "create extracted tdc binary", err) + return apperr.Wrap("update.extract_artifact", "runtime", 1, "create extracted ti binary", err) } defer file.Close() if _, err := io.Copy(file, r); err != nil { - return apperr.Wrap("update.extract_artifact", "runtime", 1, "write extracted tdc binary", err) + return apperr.Wrap("update.extract_artifact", "runtime", 1, "write extracted ti binary", err) } return nil } @@ -600,13 +600,13 @@ type downloadedBinary struct { func isTargetWritable(path string) (bool, error) { info, err := os.Stat(path) if err != nil { - return false, apperr.Wrap("update.permission_denied", "runtime", 1, "check current tdc binary", err) + return false, apperr.Wrap("update.permission_denied", "runtime", 1, "check current ti binary", err) } if info.IsDir() { - return false, apperr.New("update.permission_denied", "runtime", 1, "current tdc executable path is a directory") + return false, apperr.New("update.permission_denied", "runtime", 1, "current ti executable path is a directory") } dir := filepath.Dir(path) - temp, err := os.CreateTemp(dir, ".tdc-update-write-test-*") + temp, err := os.CreateTemp(dir, ".ti-update-write-test-*") if err != nil { if errors.Is(err, os.ErrPermission) { return false, nil @@ -615,7 +615,7 @@ func isTargetWritable(path string) (bool, error) { "update.permission_denied", "runtime", 1, - fmt.Sprintf("tdc cannot stage an update in %s", dir), + fmt.Sprintf("ti cannot stage an update in %s", dir), err, ) } @@ -660,10 +660,10 @@ func stageBinary(sourcePath, targetPath string) (string, error) { } func replaceBinary(ctx context.Context, newBinary, targetPath string) error { - backupPath := targetPath + ".tdc-backup" + backupPath := targetPath + ".ti-backup" _ = os.Remove(backupPath) if err := os.Rename(targetPath, backupPath); err != nil { - return apperr.Wrap("update.permission_denied", "runtime", 1, "move current tdc binary aside", err) + return apperr.Wrap("update.permission_denied", "runtime", 1, "move current ti binary aside", err) } installed := false defer func() { @@ -675,7 +675,7 @@ func replaceBinary(ctx context.Context, newBinary, targetPath string) error { }() if err := os.Rename(newBinary, targetPath); err != nil { - return apperr.Wrap("update.permission_denied", "runtime", 1, "install new tdc binary", err) + return apperr.Wrap("update.permission_denied", "runtime", 1, "install new ti binary", err) } if err := validateInstalledBinary(ctx, targetPath); err != nil { return err @@ -689,13 +689,13 @@ func validateInstalledBinary(ctx context.Context, targetPath string) error { validateCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() cmd := exec.CommandContext(validateCtx, targetPath, "--version") - cmd.Env = append(os.Environ(), "TDC_LOGGING=off") + cmd.Env = append(os.Environ(), "TI_LOGGING=off") if output, err := cmd.CombinedOutput(); err != nil { return apperr.Wrap( "update.validation_failed", "runtime", 1, - fmt.Sprintf("new tdc binary failed --version validation: %s", strings.TrimSpace(string(output))), + fmt.Sprintf("new ti binary failed --version validation: %s", strings.TrimSpace(string(output))), err, ) } @@ -726,9 +726,9 @@ func (c client) downloadDrive9Companion(ctx context.Context, baseURL string) (do if err := verifyChecksum(binaryBytes, expectedSHA, artifactName); err != nil { return downloadedBinary{}, func() {}, err } - tempDir, err := os.MkdirTemp("", "tdc-drive9-update-*") + tempDir, err := os.MkdirTemp("", "ti-drive9-update-*") if err != nil { - return downloadedBinary{}, func() {}, apperr.Wrap("update.temp_dir", "runtime", 1, "create tdc-drive9 update temp directory", err) + return downloadedBinary{}, func() {}, apperr.Wrap("update.temp_dir", "runtime", 1, "create ti-drive9 update temp directory", err) } cleanup := func() { _ = os.RemoveAll(tempDir) @@ -736,11 +736,11 @@ func (c client) downloadDrive9Companion(ctx context.Context, baseURL string) (do tempPath := filepath.Join(tempDir, companionBinaryName()) if err := os.WriteFile(tempPath, binaryBytes, 0o755); err != nil { cleanup() - return downloadedBinary{}, func() {}, apperr.Wrap("update.extract_artifact", "runtime", 1, "write temporary tdc-drive9 binary", err) + return downloadedBinary{}, func() {}, apperr.Wrap("update.extract_artifact", "runtime", 1, "write temporary ti-drive9 binary", err) } if err := os.Chmod(tempPath, 0o755); err != nil { cleanup() - return downloadedBinary{}, func() {}, apperr.Wrap("update.extract_artifact", "runtime", 1, "make tdc-drive9 executable", err) + return downloadedBinary{}, func() {}, apperr.Wrap("update.extract_artifact", "runtime", 1, "make ti-drive9 executable", err) } return downloadedBinary{Path: tempPath, SHA256: expectedSHA}, cleanup, nil } @@ -768,8 +768,8 @@ func drive9ArtifactName(goos, goarch string) (string, error) { } } -func companionPathForExecutable(tdcPath string) string { - path := strings.TrimSpace(tdcPath) +func companionPathForExecutable(tiPath string) string { + path := strings.TrimSpace(tiPath) if path == "" { current, err := os.Executable() if err == nil { @@ -784,9 +784,9 @@ func companionPathForExecutable(tdcPath string) string { func companionBinaryName() string { if runtime.GOOS == "windows" { - return "tdc-drive9.exe" + return "ti-drive9.exe" } - return "tdc-drive9" + return "ti-drive9" } func companionStatusText(path string, installed bool) string { @@ -805,13 +805,13 @@ func fileExists(path string) bool { } func replaceFile(sourcePath, targetPath string) error { - backupPath := targetPath + ".tdc-backup" + backupPath := targetPath + ".ti-backup" _ = os.Remove(backupPath) hadTarget := false if _, err := os.Stat(targetPath); err == nil { hadTarget = true if err := os.Rename(targetPath, backupPath); err != nil { - return apperr.Wrap("update.permission_denied", "runtime", 1, "move current tdc-drive9 binary aside", err) + return apperr.Wrap("update.permission_denied", "runtime", 1, "move current ti-drive9 binary aside", err) } } installed := false @@ -823,7 +823,7 @@ func replaceFile(sourcePath, targetPath string) error { _ = os.Rename(backupPath, targetPath) }() if err := os.Rename(sourcePath, targetPath); err != nil { - return apperr.Wrap("update.permission_denied", "runtime", 1, "install tdc-drive9 binary", err) + return apperr.Wrap("update.permission_denied", "runtime", 1, "install ti-drive9 binary", err) } installed = true if hadTarget { @@ -840,12 +840,12 @@ type installSource struct { func detectInstallSource(source, path string) installSource { name := normalizedInstallSource(source) - lowerPath := strings.ToLower(filepath.ToSlash(path)) + lowerPath := strings.ToLower(strings.ReplaceAll(filepath.ToSlash(path), `\`, "/")) switch { - case strings.Contains(lowerPath, "/homebrew/cellar/tdc/"), - strings.Contains(lowerPath, "/cellar/tdc/"): + case strings.Contains(lowerPath, "/homebrew/cellar/ti-cli/"), + strings.Contains(lowerPath, "/cellar/ti-cli/"): name = "homebrew" - case strings.Contains(lowerPath, "/scoop/apps/tdc/"): + case strings.Contains(lowerPath, "/scoop/apps/ti-cli/"): name = "scoop" } @@ -871,18 +871,18 @@ func normalizedInstallSource(source string) string { func managedInstallError(source string) error { command := map[string]string{ - "homebrew": "brew upgrade tidbcloud/tap/tdc", - "scoop": "scoop update tdc", - "winget": "winget upgrade tidbcloud.tdc", + "homebrew": "brew upgrade tidbcloud/tap/ti-cli", + "scoop": "scoop update ti-cli", + "winget": "winget upgrade TiDBCloud.TiCLI", }[source] if command == "" { - command = "use the package manager that installed tdc" + command = "use the package manager that installed ti" } return apperr.New( "update.managed_install", "runtime", 1, - fmt.Sprintf("tdc is managed by %s; update it with `%s`", source, command), + fmt.Sprintf("ti is managed by %s; update it with `%s`", source, command), ) } diff --git a/internal/update/update_test.go b/internal/update/update_test.go index a21f970..8bbf271 100644 --- a/internal/update/update_test.go +++ b/internal/update/update_test.go @@ -17,18 +17,18 @@ import ( "strings" "testing" - "github.com/tidbcloud/tdc/internal/apperr" - "github.com/tidbcloud/tdc/internal/version" + "github.com/tidbcloud/ti-cli/internal/apperr" + "github.com/tidbcloud/ti-cli/internal/version" ) func TestCheckReportsAvailableRelease(t *testing.T) { server := fakeReleaseServer(t, releaseFixture{ tag: "v0.2.0", assets: map[string][]byte{ - "tdc_darwin_arm64.tar.gz": []byte("not downloaded by check"), + "ti_darwin_arm64.tar.gz": []byte("not downloaded by check"), }, }) - t.Setenv("TDC_RELEASE_API_BASE_URL", server.URL) + t.Setenv("TI_RELEASE_API_BASE_URL", server.URL) result, err := Check(context.Background(), version.Info{ Version: "0.1.0", @@ -42,7 +42,7 @@ func TestCheckReportsAvailableRelease(t *testing.T) { if !result.UpdateAvailable || result.LatestVersion != "0.2.0" { t.Fatalf("unexpected result: %+v", result) } - if result.ArtifactName != "tdc_darwin_arm64.tar.gz" { + if result.ArtifactName != "ti_darwin_arm64.tar.gz" { t.Fatalf("unexpected artifact name %q", result.ArtifactName) } } @@ -51,10 +51,10 @@ func TestApplyDryRunPlansOwnedInstall(t *testing.T) { server := fakeReleaseServer(t, releaseFixture{ tag: "v0.2.0", assets: map[string][]byte{ - "tdc_linux_amd64.tar.gz": []byte("not downloaded by dry-run"), + "ti_linux_amd64.tar.gz": []byte("not downloaded by dry-run"), }, }) - current := filepath.Join(t.TempDir(), "tdc") + current := filepath.Join(t.TempDir(), "ti") if err := os.WriteFile(current, []byte("current"), 0o755); err != nil { t.Fatal(err) } @@ -85,7 +85,7 @@ func TestApplyDryRunPlansOwnedInstall(t *testing.T) { } func TestApplyRefusesUnknownInstall(t *testing.T) { - current := filepath.Join(t.TempDir(), "tdc") + current := filepath.Join(t.TempDir(), "ti") if err := os.WriteFile(current, []byte("current"), 0o755); err != nil { t.Fatal(err) } @@ -102,17 +102,43 @@ func TestApplyRefusesUnknownInstall(t *testing.T) { if err == nil { t.Fatal("expected unknown install to fail") } - if got := apperr.MessageFor(err); !strings.Contains(got, "not owned by tdc") { + if got := apperr.MessageFor(err); !strings.Contains(got, "not owned by ti") { t.Fatalf("unexpected error %q", got) } } +func TestPackageManagerNamingUsesTiCLI(t *testing.T) { + tests := []struct { + name string + source string + path string + wantManager string + wantCommand string + }{ + {name: "homebrew metadata", source: "homebrew", path: "/opt/homebrew/bin/ti", wantManager: "homebrew", wantCommand: "brew upgrade tidbcloud/tap/ti-cli"}, + {name: "homebrew path", source: "archive", path: "/opt/homebrew/Cellar/ti-cli/0.2.0/bin/ti", wantManager: "homebrew", wantCommand: "brew upgrade tidbcloud/tap/ti-cli"}, + {name: "scoop path", source: "archive", path: `C:\Users\test\scoop\apps\ti-cli\current\ti.exe`, wantManager: "scoop", wantCommand: "scoop update ti-cli"}, + {name: "winget metadata", source: "winget", path: `C:\Program Files\ti\ti.exe`, wantManager: "winget", wantCommand: "winget upgrade TiDBCloud.TiCLI"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := detectInstallSource(tt.source, tt.path) + if got.Name != tt.wantManager || !got.Managed || got.Owned { + t.Fatalf("detectInstallSource() = %#v", got) + } + if message := apperr.MessageFor(managedInstallError(got.Name)); !strings.Contains(message, tt.wantCommand) { + t.Fatalf("managed install message %q does not contain %q", message, tt.wantCommand) + } + }) + } +} + func TestApplyReplacesOwnedUnixBinary(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("running Windows executables cannot be replaced in-process") } - archiveBytes := tarGzBinary(t, "tdc", "#!/bin/sh\necho 'tdc 0.2.0 (test, now, linux/amd64)'\n") + archiveBytes := tarGzBinary(t, "ti", "#!/bin/sh\necho 'ti 0.2.0 (test, now, linux/amd64)'\n") companionArtifact, err := drive9ArtifactName(runtime.GOOS, runtime.GOARCH) if err != nil { t.Fatal(err) @@ -124,7 +150,7 @@ func TestApplyReplacesOwnedUnixBinary(t *testing.T) { companionArtifact: []byte("#!/bin/sh\necho companion\n"), }, }) - current := filepath.Join(t.TempDir(), "tdc") + current := filepath.Join(t.TempDir(), "ti") if err := os.WriteFile(current, []byte("#!/bin/sh\necho current\n"), 0o755); err != nil { t.Fatal(err) } @@ -166,7 +192,7 @@ func TestApplyRefusesProtectedTargetWithoutChangingFiles(t *testing.T) { t.Skip("Windows self-update is unsupported") } - archiveBytes := tarGzBinary(t, "tdc", "#!/bin/sh\necho 'tdc 0.2.0 (test, now, linux/amd64)'\n") + archiveBytes := tarGzBinary(t, "ti", "#!/bin/sh\necho 'ti 0.2.0 (test, now, linux/amd64)'\n") companionArtifact, err := drive9ArtifactName(runtime.GOOS, runtime.GOARCH) if err != nil { t.Fatal(err) @@ -178,7 +204,7 @@ func TestApplyRefusesProtectedTargetWithoutChangingFiles(t *testing.T) { companionArtifact: []byte("#!/bin/sh\necho companion-protected\n"), }, }) - current := filepath.Join(t.TempDir(), "tdc") + current := filepath.Join(t.TempDir(), "ti") if err := os.WriteFile(current, []byte("#!/bin/sh\necho current\n"), 0o755); err != nil { t.Fatal(err) } @@ -206,7 +232,7 @@ func TestApplyRefusesProtectedTargetWithoutChangingFiles(t *testing.T) { if got := apperr.CodeFor(err); got != "update.permission_denied" { t.Fatalf("unexpected error code %q", got) } - if got := apperr.MessageFor(err); !strings.Contains(got, "migrate to ~/.tdc/bin") { + if got := apperr.MessageFor(err); !strings.Contains(got, "migrate to ~/.ti/bin") { t.Fatalf("unexpected error message %q", got) } currentBytes, err := os.ReadFile(current) @@ -218,12 +244,12 @@ func TestApplyRefusesProtectedTargetWithoutChangingFiles(t *testing.T) { t.Fatal(err) } if string(currentBytes) != "#!/bin/sh\necho current\n" || string(companionBytes) != "old companion" { - t.Fatalf("targets changed after protected update refusal: tdc=%q companion=%q", currentBytes, companionBytes) + t.Fatalf("targets changed after protected update refusal: ti=%q companion=%q", currentBytes, companionBytes) } } func TestChecksumForGoReleaserLine(t *testing.T) { - got, err := checksumFor([]byte("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef tdc_linux_amd64.tar.gz\n"), "tdc_linux_amd64.tar.gz") + got, err := checksumFor([]byte("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef ti_linux_amd64.tar.gz\n"), "ti_linux_amd64.tar.gz") if err != nil { t.Fatal(err) } @@ -248,7 +274,7 @@ func fakeReleaseServer(t *testing.T, fixture releaseFixture) *httptest.Server { mux.HandleFunc("/releases/tags/"+fixture.tag, func(w http.ResponseWriter, r *http.Request) { writeReleaseJSON(t, w, server.URL, fixture, checksums) }) - mux.HandleFunc("/assets/tdc_checksums.txt", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/assets/ti_checksums.txt", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(checksums)) }) mux.HandleFunc("/assets/"+drive9ChecksumAssetName, func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/version/version.go b/internal/version/version.go index 0f3e3e4..7b1d6c6 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -39,5 +39,5 @@ func Current() Info { } func (i Info) String() string { - return fmt.Sprintf("tdc %s (%s, %s, %s/%s)", i.Version, i.Commit, i.Date, i.OS, i.Arch) + return fmt.Sprintf("ti %s (%s, %s, %s/%s)", i.Version, i.Commit, i.Date, i.OS, i.Arch) } diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 4072485..6443437 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -6,11 +6,11 @@ param( ) $ErrorActionPreference = "Stop" -$Repo = "tidbcloud/tdc" -$DefaultInstallDir = Join-Path (Join-Path $HOME ".tdc") "bin" +$Repo = "tidbcloud/ti-cli" +$DefaultInstallDir = Join-Path (Join-Path $HOME ".ti") "bin" function Fail($Message) { - Write-Error "tdc install [ERROR]: $Message" + Write-Error "ti install [ERROR]: $Message" exit 1 } @@ -23,12 +23,21 @@ function Warn($Message) { } function Resolve-InstallDir { + if (-not [string]::IsNullOrWhiteSpace($env:TI_INSTALL_DIR) -and + -not [string]::IsNullOrWhiteSpace($env:TDC_INSTALL_DIR) -and + $env:TI_INSTALL_DIR -ne $env:TDC_INSTALL_DIR) { + Fail "TI_INSTALL_DIR and deprecated TDC_INSTALL_DIR contain different values" + } if (-not [string]::IsNullOrWhiteSpace($InstallDir)) { Info "Install dir: $InstallDir (from -InstallDir)" return $InstallDir } + if (-not [string]::IsNullOrWhiteSpace($env:TI_INSTALL_DIR)) { + Info "Install dir: $env:TI_INSTALL_DIR (from TI_INSTALL_DIR)" + return $env:TI_INSTALL_DIR + } if (-not [string]::IsNullOrWhiteSpace($env:TDC_INSTALL_DIR)) { - Info "Install dir: $env:TDC_INSTALL_DIR (from TDC_INSTALL_DIR)" + Info "Install dir: $env:TDC_INSTALL_DIR (from deprecated TDC_INSTALL_DIR)" return $env:TDC_INSTALL_DIR } @@ -36,11 +45,22 @@ function Resolve-InstallDir { return $DefaultInstallDir } +function Invoke-HomeMigration($MigrationBinary) { + if ([string]::IsNullOrWhiteSpace($HOME)) { + return + } + Info "Checking for state to migrate from $HOME\.tdc" + & $MigrationBinary __migrate-home + if ($LASTEXITCODE -ne 0) { + Fail "state migration failed; the legacy directory was left unchanged" + } +} + function Bootstrap-Config { if ([string]::IsNullOrWhiteSpace($HOME)) { return } - $ConfigDir = Join-Path $HOME ".tdc" + $ConfigDir = Join-Path $HOME ".ti" $ConfigFile = Join-Path $ConfigDir "config" New-Item -ItemType Directory -Force -Path $ConfigDir | Out-Null if (-not (Test-Path $ConfigFile)) { @@ -53,16 +73,16 @@ region_code = 'aws-us-east-1' } function Report-PathStatus { - $active = Get-Command tdc -ErrorAction SilentlyContinue | Select-Object -First 1 + $active = Get-Command ti -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $active -or -not $active.Source) { - Warn "tdc is installed at $Target, but $InstallDir is not on your PATH" + Warn "ti is installed at $Target, but $InstallDir is not on your PATH" } elseif ($active.Source -ne $Target) { - Warn "PATH shadowing detected: tdc resolves to $($active.Source)" + Warn "PATH shadowing detected: ti resolves to $($active.Source)" Warn "Installed binary: $Target" } else { return } - Warn "Add tdc to the current PowerShell PATH:" + Warn "Add ti to the current PowerShell PATH:" Warn ('$env:Path = "{0};$env:Path"' -f $InstallDir) Warn "Add $InstallDir to your user PATH to persist it" } @@ -73,7 +93,7 @@ function Print-Regions { Write-Output " aws-us-east-1, aws-us-west-2, aws-eu-central-1, aws-ap-northeast-1, aws-ap-southeast-1" Write-Output " ali-ap-southeast-1" Write-Output "" - Write-Output " tdc fs regions:" + Write-Output " ti fs regions:" try { $manifest = Invoke-RestMethod -Uri "https://drive9.ai/manifest/regions/drive9-regions.json" $regions = @($manifest.regions | Where-Object { $_.mode -eq "tidb_cloud_native" } | ForEach-Object { @@ -89,50 +109,50 @@ function Print-Regions { } } catch { } - Write-Output " aws-us-east-1, aws-ap-southeast-1" - Warn "Could not fetch the latest tdc fs region manifest; run tdc fs check-file-system after configure" + Write-Output " aws-us-east-1, aws-us-west-2, aws-ap-southeast-1, ali-ap-southeast-1" + Warn "Could not fetch the latest ti fs region manifest; run ti fs check-file-system after configure" } function Print-NextSteps { Write-Output "" Write-Output " Get started:" Write-Output "" - Write-Output " 1. Add tdc to PATH" + Write-Output " 1. Add ti to PATH" Write-Output (' $env:Path = "{0};$env:Path"' -f $InstallDir) Write-Output "" Write-Output " 2. Configure credentials" - Write-Output " tdc configure" + Write-Output " ti configure" Write-Output "" Write-Output " 3. List projects" - Write-Output " tdc organization list-projects --output text" + Write-Output " ti organization list-projects --output text" Write-Output "" - Write-Output " 4. Create or check tdc fs" - Write-Output " tdc fs create-file-system --file-system-name workspace" - Write-Output " tdc fs check-file-system --output text" + Write-Output " 4. Create or check ti fs" + Write-Output " ti fs create-file-system --file-system-name workspace" + Write-Output " ti fs check-file-system --output text" Write-Output "" - Write-Output " 5. Mount tdc fs when FUSE is available" - Write-Output " tdc fs mount-file-system --file-system-name workspace --mount-path ./workspace" + Write-Output " 5. Mount ti fs when FUSE is available" + Write-Output " ti fs mount-file-system --file-system-name workspace --mount-path ./workspace" Write-Output "" - Write-Output " Docs: https://github.com/tidbcloud/tdc" + Write-Output " Docs: https://github.com/tidbcloud/ti-cli" } function Print-TelemetryNotice { Write-Output "" Write-Output " Anonymous telemetry:" Write-Output "" - Write-Output " tdc collects anonymous command usage and reliability telemetry in release builds." + Write-Output " ti collects anonymous command usage and reliability telemetry in release builds." Write-Output " It collects command and flag names (never values), exit and stable error codes," - Write-Output " duration, region, tdc version, OS, and architecture." + Write-Output " duration, region, ti version, OS, and architecture." Write-Output "" Write-Output " It never collects credentials, tokens, SQL text, file paths or contents," Write-Output " command output, API response payloads, or cloud resource IDs." Write-Output "" - Write-Output " To disable telemetry, create or edit ~/.tdc/.preferences:" + Write-Output " To disable telemetry, create or edit ~/.ti/.preferences:" Write-Output "" Write-Output " [telemetry]" Write-Output " enabled = false" Write-Output "" - Write-Output " For one process: TDC_TELEMETRY=off tdc ..." + Write-Output " For one process: TI_TELEMETRY=off ti ..." } $arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture @@ -153,17 +173,17 @@ if ($Version -eq "latest") { $ReleaseBase = "https://github.com/$Repo/releases/download/$Tag" } -$Artifact = "tdc_windows_amd64.zip" +$Artifact = "ti_windows_amd64.zip" $ArchiveUrl = "$ReleaseBase/$Artifact" -$ChecksumsUrl = "$ReleaseBase/tdc_checksums.txt" -$Target = Join-Path $InstallDir "tdc.exe" +$ChecksumsUrl = "$ReleaseBase/ti_checksums.txt" +$Target = Join-Path $InstallDir "ti.exe" $CompanionArtifact = "drive9-windows-amd64.exe" $CompanionUrl = "https://drive9.ai/releases/$CompanionArtifact" $CompanionChecksumsUrl = "https://drive9.ai/releases/checksums.txt" -$CompanionTarget = Join-Path $InstallDir "tdc-drive9.exe" +$CompanionTarget = Join-Path $InstallDir "ti-drive9.exe" if ($DryRun) { - Write-Output "tdc install dry-run" + Write-Output "ti install dry-run" Write-Output "version: $Version" Write-Output "artifact: $Artifact" Write-Output "archive_url: $ArchiveUrl" @@ -179,17 +199,16 @@ if ($DryRun) { if ((Test-Path $Target) -and -not $Yes) { $answer = Read-Host "Replace existing $Target? [y/N]" if ($answer -notin @("y", "Y", "yes", "YES")) { - Write-Error "tdc install [ERROR]: cancelled" + Write-Error "ti install [ERROR]: cancelled" exit 130 } } -New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null -$TempDir = New-Item -ItemType Directory -Path (Join-Path ([System.IO.Path]::GetTempPath()) ("tdc-install-" + [System.Guid]::NewGuid().ToString())) +$TempDir = New-Item -ItemType Directory -Path (Join-Path ([System.IO.Path]::GetTempPath()) ("ti-install-" + [System.Guid]::NewGuid().ToString())) try { $ArchivePath = Join-Path $TempDir.FullName $Artifact - $ChecksumsPath = Join-Path $TempDir.FullName "tdc_checksums.txt" + $ChecksumsPath = Join-Path $TempDir.FullName "ti_checksums.txt" $CompanionPath = Join-Path $TempDir.FullName $CompanionArtifact $CompanionChecksumsPath = Join-Path $TempDir.FullName "drive9_checksums.txt" Invoke-WebRequest -Uri $ArchiveUrl -OutFile $ArchivePath @@ -218,16 +237,18 @@ try { } Expand-Archive -Path $ArchivePath -DestinationPath $TempDir.FullName -Force - $Extracted = Get-ChildItem -Path $TempDir.FullName -Recurse -Filter "tdc.exe" | Select-Object -First 1 + $Extracted = Get-ChildItem -Path $TempDir.FullName -Recurse -Filter "ti.exe" | Select-Object -First 1 if (-not $Extracted) { - Fail "archive did not contain tdc.exe" + Fail "archive did not contain ti.exe" } + Invoke-HomeMigration $Extracted.FullName + New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null Move-Item -Force -Path $Extracted.FullName -Destination $Target Move-Item -Force -Path $CompanionPath -Destination $CompanionTarget & $Target --version - Write-Output "tdc installed to $Target" - Write-Output "tdc fs companion installed to $CompanionTarget" + Write-Output "ti installed to $Target" + Write-Output "ti fs companion installed to $CompanionTarget" Bootstrap-Config Report-PathStatus Print-Regions diff --git a/scripts/install.sh b/scripts/install.sh index 8c9b664..06732ab 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -REPO="tidbcloud/tdc" +REPO="tidbcloud/ti-cli" VERSION="latest" INSTALL_DIR="" INSTALL_DIR_EXPLICIT=0 @@ -26,18 +26,18 @@ fi info() { printf " ${DIM}%s${RESET}\n" "$1"; } success() { printf " ${GREEN}%s${RESET}\n" "$1"; } warn() { printf " ${YELLOW}%s${RESET}\n" "$1"; } -error() { printf " ${RED}tdc install [ERROR]:${RESET} %s\n" "$1" >&2; exit 1; } +error() { printf " ${RED}ti install [ERROR]:${RESET} %s\n" "$1" >&2; exit 1; } usage() { cat <<'USAGE' -Install tdc from GitHub Releases. +Install ti from GitHub Releases. Usage: - install.sh [--version latest|v0.1.0] [--install-dir PATH] [--dry-run] [--yes] + install.sh [--version latest|v0.2.0] [--install-dir PATH] [--dry-run] [--yes] Options: --version Release version to install. Defaults to latest. - --install-dir Directory that will receive the tdc binary. Overrides TDC_INSTALL_DIR. + --install-dir Directory that will receive the ti binary. Overrides TI_INSTALL_DIR. --dry-run Print the planned download and install path without changes. --yes Replace an existing binary without prompting. --help Show this help. @@ -68,7 +68,7 @@ while [ "$#" -gt 0 ]; do exit 0 ;; *) - echo "tdc install [ERROR]: unknown argument: $1" >&2 + echo "ti install [ERROR]: unknown argument: $1" >&2 usage >&2 exit 2 ;; @@ -128,22 +128,39 @@ case "$(uname -m)" in esac resolve_install_dir() { + if [ -n "${TI_INSTALL_DIR:-}" ] && [ -n "${TDC_INSTALL_DIR:-}" ] && [ "$TI_INSTALL_DIR" != "$TDC_INSTALL_DIR" ]; then + error "TI_INSTALL_DIR and deprecated TDC_INSTALL_DIR contain different values" + fi if [ "$INSTALL_DIR_EXPLICIT" -eq 1 ]; then [ -n "$INSTALL_DIR" ] || error "--install-dir cannot be empty" info "Install dir: ${INSTALL_DIR} (from --install-dir)" return fi + if [ -n "${TI_INSTALL_DIR:-}" ]; then + INSTALL_DIR="$TI_INSTALL_DIR" + info "Install dir: ${INSTALL_DIR} (from TI_INSTALL_DIR)" + return + fi if [ -n "${TDC_INSTALL_DIR:-}" ]; then INSTALL_DIR="$TDC_INSTALL_DIR" - info "Install dir: ${INSTALL_DIR} (from TDC_INSTALL_DIR)" + info "Install dir: ${INSTALL_DIR} (from deprecated TDC_INSTALL_DIR)" return fi - [ -n "${HOME:-}" ] || error "HOME is required when --install-dir and TDC_INSTALL_DIR are not set" - INSTALL_DIR="${HOME}/.tdc/bin" + [ -n "${HOME:-}" ] || error "HOME is required when --install-dir and TI_INSTALL_DIR are not set" + INSTALL_DIR="${HOME}/.ti/bin" info "Install dir: ${INSTALL_DIR} (default user install)" } +run_home_migration() { + migration_binary="$1" + if [ -z "${HOME:-}" ]; then + return + fi + info "Checking for state to migrate from ${HOME}/.tdc" + "$migration_binary" __migrate-home +} + resolve_install_dir if [ "$VERSION" = "latest" ]; then @@ -156,18 +173,18 @@ else RELEASE_BASE="https://github.com/${REPO}/releases/download/${TAG}" fi -ARTIFACT="tdc_${OS}_${ARCH}.tar.gz" +ARTIFACT="ti_${OS}_${ARCH}.tar.gz" ARCHIVE_URL="${RELEASE_BASE}/${ARTIFACT}" -CHECKSUMS_URL="${RELEASE_BASE}/tdc_checksums.txt" -TARGET="${INSTALL_DIR}/tdc" +CHECKSUMS_URL="${RELEASE_BASE}/ti_checksums.txt" +TARGET="${INSTALL_DIR}/ti" COMPANION_ARTIFACT="drive9-${OS}-${ARCH}" COMPANION_URL="https://drive9.ai/releases/${COMPANION_ARTIFACT}" COMPANION_CHECKSUMS_URL="https://drive9.ai/releases/checksums.txt" -COMPANION_TARGET="${INSTALL_DIR}/tdc-drive9" +COMPANION_TARGET="${INSTALL_DIR}/ti-drive9" if [ "$DRY_RUN" -eq 1 ]; then cat <&2 + echo "ti install [ERROR]: cancelled" >&2 exit 130 ;; esac @@ -197,7 +214,7 @@ bootstrap_config() { if [ -z "${HOME:-}" ]; then return fi - CONFIG_DIR="${HOME}/.tdc" + CONFIG_DIR="${HOME}/.ti" CONFIG_FILE="${CONFIG_DIR}/config" mkdir -p "$CONFIG_DIR" 2>/dev/null || true chmod 700 "$CONFIG_DIR" 2>/dev/null || true @@ -216,7 +233,7 @@ install_file() { target_path="$2" mkdir -p "$INSTALL_DIR" 2>/dev/null || error "cannot create ${INSTALL_DIR}; choose a user-writable directory with --install-dir" [ -w "$INSTALL_DIR" ] || error "cannot write to ${INSTALL_DIR}; choose a user-writable directory with --install-dir" - stage_path="$(mktemp "${INSTALL_DIR}/.tdc-install.XXXXXX")" || error "cannot stage installation in ${INSTALL_DIR}" + stage_path="$(mktemp "${INSTALL_DIR}/.ti-install.XXXXXX")" || error "cannot stage installation in ${INSTALL_DIR}" cp "$source_path" "$stage_path" || { rm -f "$stage_path" error "cannot copy binary into ${INSTALL_DIR}" @@ -232,13 +249,13 @@ install_file() { } report_path_status() { - ACTIVE="$(command -v tdc 2>/dev/null || true)" + ACTIVE="$(command -v ti 2>/dev/null || true)" if [ -n "$ACTIVE" ] && [ "$ACTIVE" != "$TARGET" ]; then - warn "PATH shadowing detected: tdc resolves to ${ACTIVE}" + warn "PATH shadowing detected: ti resolves to ${ACTIVE}" warn "Installed binary: ${TARGET}" fi if [ "$ACTIVE" != "$TARGET" ]; then - warn "Add tdc to the current shell PATH:" + warn "Add ti to the current shell PATH:" warn "export PATH=\"${INSTALL_DIR}:\$PATH\"" warn "Add the same line to your shell profile to persist it" fi @@ -250,7 +267,7 @@ print_regions() { printf " aws-us-east-1, aws-us-west-2, aws-eu-central-1, aws-ap-northeast-1, aws-ap-southeast-1\n" printf " ali-ap-southeast-1\n" printf "\n" - printf " ${BOLD}tdc fs regions:${RESET}\n" + printf " ${BOLD}ti fs regions:${RESET}\n" MANIFEST="$(download_quiet_stdout "https://drive9.ai/manifest/regions/drive9-regions.json")" FS_REGIONS="$(printf "%s\n" "$MANIFEST" | awk ' /"mode"[[:space:]]*:[[:space:]]*"tidb_cloud_native"/ { native=1 } @@ -278,8 +295,8 @@ print_regions() { if [ -n "$FS_REGIONS" ]; then printf "%s\n" "$FS_REGIONS" else - printf " aws-us-east-1, aws-ap-southeast-1\n" - warn "Could not fetch the latest tdc fs region manifest; run tdc fs check-file-system after configure" + printf " aws-us-east-1, aws-us-west-2, aws-ap-southeast-1, ali-ap-southeast-1\n" + warn "Could not fetch the latest ti fs region manifest; run ti fs check-file-system after configure" fi } @@ -287,42 +304,42 @@ print_next_steps() { printf "\n" printf " ${BOLD}Get started:${RESET}\n" printf "\n" - printf " ${BOLD}1.${RESET} Add tdc to PATH\n" + printf " ${BOLD}1.${RESET} Add ti to PATH\n" printf " ${DIM}\$${RESET} export PATH=\"${INSTALL_DIR}:\$PATH\"\n" printf "\n" printf " ${BOLD}2.${RESET} Configure credentials\n" - printf " ${DIM}\$${RESET} tdc configure\n" + printf " ${DIM}\$${RESET} ti configure\n" printf "\n" printf " ${BOLD}3.${RESET} List projects\n" - printf " ${DIM}\$${RESET} tdc organization list-projects --output text\n" + printf " ${DIM}\$${RESET} ti organization list-projects --output text\n" printf "\n" - printf " ${BOLD}4.${RESET} Create or check tdc fs\n" - printf " ${DIM}\$${RESET} tdc fs create-file-system --file-system-name workspace\n" - printf " ${DIM}\$${RESET} tdc fs check-file-system --output text\n" + printf " ${BOLD}4.${RESET} Create or check ti fs\n" + printf " ${DIM}\$${RESET} ti fs create-file-system --file-system-name workspace\n" + printf " ${DIM}\$${RESET} ti fs check-file-system --output text\n" printf "\n" - printf " ${BOLD}5.${RESET} Mount tdc fs when FUSE is available\n" - printf " ${DIM}\$${RESET} tdc fs mount-file-system --file-system-name workspace --mount-path ./workspace\n" + printf " ${BOLD}5.${RESET} Mount ti fs when FUSE is available\n" + printf " ${DIM}\$${RESET} ti fs mount-file-system --file-system-name workspace --mount-path ./workspace\n" printf "\n" - printf " Docs: ${DIM}https://github.com/tidbcloud/tdc${RESET}\n" + printf " Docs: ${DIM}https://github.com/tidbcloud/ti-cli${RESET}\n" } print_telemetry_notice() { printf "\n" printf " ${BOLD}Anonymous telemetry:${RESET}\n" printf "\n" - printf " tdc collects anonymous command usage and reliability telemetry in release builds.\n" + printf " ti collects anonymous command usage and reliability telemetry in release builds.\n" printf " It collects command and flag names (never values), exit and stable error codes,\n" - printf " duration, region, tdc version, OS, and architecture.\n" + printf " duration, region, ti version, OS, and architecture.\n" printf "\n" printf " It never collects credentials, tokens, SQL text, file paths or contents,\n" printf " command output, API response payloads, or cloud resource IDs.\n" printf "\n" - printf " To disable telemetry, create or edit ~/.tdc/.preferences:\n" + printf " To disable telemetry, create or edit ~/.ti/.preferences:\n" printf "\n" printf " [telemetry]\n" printf " enabled = false\n" printf "\n" - printf " For one process: ${DIM}TDC_TELEMETRY=off tdc ...${RESET}\n" + printf " For one process: ${DIM}TI_TELEMETRY=off ti ...${RESET}\n" } TMP_DIR="$(mktemp -d)" @@ -332,7 +349,7 @@ cleanup() { trap cleanup EXIT INT TERM printf "\n" -printf " ${BOLD}tdc${RESET} installer\n" +printf " ${BOLD}ti${RESET} installer\n" printf " ${DIM}────────────────────────────${RESET}\n" printf "\n" info "Platform: ${OS}/${ARCH}" @@ -340,11 +357,11 @@ info "Artifact: ${ARTIFACT}" info "Companion: ${COMPANION_ARTIFACT}" download "$ARCHIVE_URL" "${TMP_DIR}/${ARTIFACT}" -download "$CHECKSUMS_URL" "${TMP_DIR}/tdc_checksums.txt" +download "$CHECKSUMS_URL" "${TMP_DIR}/ti_checksums.txt" download "$COMPANION_URL" "${TMP_DIR}/${COMPANION_ARTIFACT}" download "$COMPANION_CHECKSUMS_URL" "${TMP_DIR}/drive9_checksums.txt" -EXPECTED="$(awk -v name="$ARTIFACT" '$2 == name { print $1 }' "${TMP_DIR}/tdc_checksums.txt" | head -n 1)" +EXPECTED="$(awk -v name="$ARTIFACT" '$2 == name { print $1 }' "${TMP_DIR}/ti_checksums.txt" | head -n 1)" if [ -z "$EXPECTED" ]; then error "checksum for ${ARTIFACT} not found" fi @@ -375,18 +392,19 @@ if [ "$COMPANION_EXPECTED" != "$COMPANION_ACTUAL" ]; then fi tar -xzf "${TMP_DIR}/${ARTIFACT}" -C "$TMP_DIR" -FOUND="$(find "$TMP_DIR" -type f -name tdc | head -n 1)" +FOUND="$(find "$TMP_DIR" -type f -name ti | head -n 1)" if [ -z "$FOUND" ]; then - error "archive did not contain tdc" + error "archive did not contain ti" fi chmod 0755 "$FOUND" chmod 0755 "${TMP_DIR}/${COMPANION_ARTIFACT}" +run_home_migration "$FOUND" install_file "$FOUND" "$TARGET" install_file "${TMP_DIR}/${COMPANION_ARTIFACT}" "$COMPANION_TARGET" "$TARGET" --version -success "tdc installed to ${TARGET}" -success "tdc fs companion installed to ${COMPANION_TARGET}" +success "ti installed to ${TARGET}" +success "ti fs companion installed to ${COMPANION_TARGET}" bootstrap_config report_path_status print_regions