From a980770ca570bfd3fff6cd04c8d1814a1468dec7 Mon Sep 17 00:00:00 2001 From: Cheese Date: Thu, 20 Aug 2026 13:04:41 +0800 Subject: [PATCH] fix(auth): link missing credentials to API key page --- README.md | 2 +- e2e/cli_test.go | 28 ++++++++++++++++++++++++++++ internal/auth/auth.go | 5 ++++- internal/auth/auth_test.go | 4 ++-- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1722d35..f0474a7 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Automation should move to `TI_*` and `TIDB_CLOUD_*` environment variables. The v ### Configure -- Authentication: a TiDB Cloud Public Key and a Private Key from the [TiDB Cloud API Keys](https://tidbcloud.com/org-settings/api-keys) console. +- Authentication: a TiDB Cloud Public Key and a Private Key from the [TiDB Cloud API Keys](https://tidbcloud.com/org-settings/api-keys) console. When either credential is missing, `ti` links to this page in its authentication error before suggesting `ti configure` or the corresponding environment variables. - 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 alicloud-ap-southeast-1. - Regions supporting TiDB Cloud Filesystem: aws-us-east-1, aws-ap-southeast-1, aws-us-west-2, or alicloud-ap-southeast-1. These endpoints are built into `ti`; endpoint resolution does not download a Drive9 region manifest. - Regions supporting TiDB Cloud Starter: aws-us-east-1, aws-us-west-2, aws-eu-central-1, aws-ap-northeast-1, aws-ap-southeast-1, or alicloud-ap-southeast-1. diff --git a/e2e/cli_test.go b/e2e/cli_test.go index 9532023..d359194 100644 --- a/e2e/cli_test.go +++ b/e2e/cli_test.go @@ -222,6 +222,34 @@ func TestErrorsAreRenderedAtCLIBoundary(t *testing.T) { invalidQuery.wantStderrContains("ti [ERROR]: invalid --query expression") } +func TestMissingTiDBCloudCredentialsLinkToAPIKeysPage(t *testing.T) { + bin := tiBinary(t) + home := t.TempDir() + writeE2EFile(t, filepath.Join(home, ".ti", "config"), "[default]\nregion_code = 'aws-us-east-1'\n", 0o600) + env := []string{ + "HOME=" + home, + "TIDB_CLOUD_PUBLIC_KEY=", + "TIDB_CLOUD_PRIVATE_KEY=", + "TDC_PUBLIC_KEY=", + "TDC_PRIVATE_KEY=", + } + + commands := [][]string{ + {"fs", "create-file-system", "--display-name", "agent-workspace", "--wait"}, + {"db", "create-db-cluster", "--db-cluster-type", "starter", "--db-cluster-name", "agent-database", "--wait"}, + } + for _, args := range commands { + result := runTIWithInput(t, bin, "", env, args...) + result.wantExitCode(3) + result.wantStderrContains("authentication required: missing tidb_cloud_public_key and tidb_cloud_private_key") + result.wantStderrContains("Run `ti configure` or set TIDB_CLOUD_PUBLIC_KEY and TIDB_CLOUD_PRIVATE_KEY") + result.wantStderrContains("https://tidbcloud.com/org-settings/api-keys") + if result.stdout != "" { + result.fail("stdout should be empty") + } + } +} + func TestHomeMigrationThroughRealBinary(t *testing.T) { bin := tiBinary(t) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 7143b09..194d081 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -19,6 +19,8 @@ type Credentials struct { PrivateKey string } +const tiDBCloudAPIKeysURL = "https://tidbcloud.com/org-settings/api-keys" + func LoadProfile(ctx context.Context, opts config.LoadOptions) (*config.Profile, error) { profile, err := config.Load(ctx, opts) if err == nil { @@ -98,9 +100,10 @@ func MissingCredentials(profileName string, keys ...string) error { "authentication", 3, fmt.Sprintf( - "authentication required: missing %s for profile %q. Run `ti configure` or set TIDB_CLOUD_PUBLIC_KEY and TIDB_CLOUD_PRIVATE_KEY.", + "authentication required: missing %s for profile %q. Run `ti configure` or set TIDB_CLOUD_PUBLIC_KEY and TIDB_CLOUD_PRIVATE_KEY. If you do not have a TiDB Cloud API key pair, generate one at %s.", joinKeys(keys), profileName, + tiDBCloudAPIKeysURL, ), ) } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 89cd52d..3da2a6a 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -18,7 +18,7 @@ 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, "tidb_cloud_public_key and tidb_cloud_private_key") { + if got := apperr.MessageFor(err); !strings.Contains(got, "authentication required") || !strings.Contains(got, "tidb_cloud_public_key and tidb_cloud_private_key") || !strings.Contains(got, tiDBCloudAPIKeysURL) { t.Fatalf("unexpected message %q", got) } } @@ -56,7 +56,7 @@ func TestLoadProfileMapsMissingFileCredentialsToAuthError(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") { + if got := apperr.MessageFor(err); !strings.Contains(got, "authentication required") || !strings.Contains(got, tiDBCloudAPIKeysURL) { t.Fatalf("unexpected message %q", got) } }