From 51c566ee45deabb3d30a64ae97e2d8966182b4d8 Mon Sep 17 00:00:00 2001 From: siddon Date: Wed, 14 Oct 2026 16:28:46 -0700 Subject: [PATCH 1/5] fix(fs): add Azure Central US placement support Signed-off-by: siddon --- AGENTS.md | 6 ++- README.md | 4 +- docs/priciples.md | 2 +- docs/telemetry-backend-design.md | 2 +- e2e/cli_test.go | 43 +++++++++++++++++++++ e2e/installer_test.go | 4 +- internal/api/endpoints/endpoints.go | 1 + internal/api/endpoints/endpoints_test.go | 1 + internal/cli/root_test.go | 14 +++++++ internal/config/region/region.go | 7 ++++ internal/config/region/region_test.go | 9 ++++- internal/fs/fscred/credential_test.go | 31 +++++++++++++++ internal/telemetry/telemetry.go | 2 +- internal/telemetry/telemetry_test.go | 27 +++++++++++++ internal/telemetrybackend/event.go | 3 +- internal/telemetrybackend/event_test.go | 48 ++++++++++++++---------- scripts/install.ps1 | 4 +- scripts/install.sh | 4 +- 18 files changed, 178 insertions(+), 34 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2a2ec4e..10e062b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1001,11 +1001,13 @@ Supported MVP placement values: | `aws-ap-southeast-1` | AWS | Singapore | | `alicloud-ap-southeast-1` | Alibaba Cloud | Singapore | | `gcp-us-east1` | Google Cloud | South Carolina | +| `azure-centralus` | Microsoft Azure | Central US | The prefix before the first `-` is the cloud provider selector. `aws` maps to internal provider `aws`; `alicloud` maps to internal provider -`alibaba_cloud`; `gcp` maps to internal provider `gcp`. Keep this mapping -centralized in `internal/config/region`. +`alibaba_cloud`; `gcp` maps to internal provider `gcp`; `azure` maps to +internal provider `azure`. Keep this mapping centralized in +`internal/config/region`. Do not store secrets in logs, telemetry, generated docs examples, or test fixtures. diff --git a/README.md b/README.md index 932632d..2f284bd 100644 --- a/README.md +++ b/README.md @@ -104,8 +104,8 @@ 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. 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, alicloud-ap-southeast-1, or gcp-us-east1. - - Regions supporting TiDB Cloud Filesystem: aws-us-east-1, aws-ap-southeast-1, aws-us-west-2, alicloud-ap-southeast-1, or gcp-us-east1. These endpoints are built into `ti`; endpoint resolution does not download a Drive9 region manifest. +- Default region: one of aws-us-east-1, aws-us-west-2, aws-eu-central-1, aws-ap-northeast-1, aws-ap-southeast-1, alicloud-ap-southeast-1, gcp-us-east1, or azure-centralus. + - Regions supporting TiDB Cloud Filesystem: aws-us-east-1, aws-ap-southeast-1, aws-us-west-2, alicloud-ap-southeast-1, gcp-us-east1, or azure-centralus. 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. Set up a default profile with one command: diff --git a/docs/priciples.md b/docs/priciples.md index 838d122..45e6dd9 100644 --- a/docs/priciples.md +++ b/docs/priciples.md @@ -77,7 +77,7 @@ Supported TiDB Cloud Starter placement values are: | `aws-ap-southeast-1` | AWS | Singapore | | `alicloud-ap-southeast-1` | Alibaba Cloud | Singapore | -`aws` maps to the internal provider `aws`; `alicloud` maps to `alibaba_cloud`; `gcp` maps to `gcp`. TiDB Cloud Filesystem supports `aws-us-east-1`, `aws-ap-southeast-1`, `aws-us-west-2`, `alicloud-ap-southeast-1`, and `gcp-us-east1` through endpoint mappings built into `ti`. +`aws` maps to the internal provider `aws`; `alicloud` maps to `alibaba_cloud`; `gcp` maps to `gcp`; `azure` maps to `azure`. TiDB Cloud Filesystem supports `aws-us-east-1`, `aws-ap-southeast-1`, `aws-us-west-2`, `alicloud-ap-southeast-1`, `gcp-us-east1`, and `azure-centralus` through endpoint mappings built into `ti`. ## TiDB Cloud Authentication diff --git a/docs/telemetry-backend-design.md b/docs/telemetry-backend-design.md index 822d42f..26e0128 100644 --- a/docs/telemetry-backend-design.md +++ b/docs/telemetry-backend-design.md @@ -192,7 +192,7 @@ Allowed event fields: | `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`, `gcp`, `unknown`, or empty. | +| `cloud_provider` | string | no | `aws`, `alibaba_cloud`, `gcp`, `azure`, `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. | diff --git a/e2e/cli_test.go b/e2e/cli_test.go index 8f06bff..c61bf26 100644 --- a/e2e/cli_test.go +++ b/e2e/cli_test.go @@ -1077,6 +1077,49 @@ func TestFSRemoteInventoryAndIDCredentialSelectionAcrossCommandFamilies(t *testi } } +func TestFSCloudRegionsUseBuiltInEndpoints(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("fake companion build path is covered by unit tests on Windows") + } + bin := tiBinary(t) + companion := filepath.Join(t.TempDir(), "ti-drive9") + build := exec.Command("go", "build", "-o", companion, "./testdata/fake-drive9.go") + if output, err := build.CombinedOutput(); err != nil { + t.Fatalf("build fake companion: %v\n%s", err, output) + } + for _, regionCode := range []string{"gcp-us-east1", "azure-centralus"} { + t.Run(regionCode, func(t *testing.T) { + home := t.TempDir() + recordPath := filepath.Join(t.TempDir(), "calls.jsonl") + const fileSystemID = "tenant-cloud-sandbox" + token := drive9TestToken(fileSystemID) + env := []string{ + "HOME=" + home, + "TI_PROFILE=default", + "TI_REGION_CODE=aws-us-east-1", + "TI_DRIVE9_BIN=" + companion, + "TI_FS_TOKEN=" + token, + "TIDB_CLOUD_PUBLIC_KEY=", + "TIDB_CLOUD_PRIVATE_KEY=", + "TI_ALLOW_TEST_ENDPOINTS=", + "TI_TEST_FS_MANIFEST_URL=http://127.0.0.1:1/must-not-be-used", + "FAKE_DRIVE9_RECORD=" + recordPath, + "DRIVE9_SERVER=https://wrong.example.test", + "DRIVE9_REGION_CODE=aws-us-west-2", + "DRIVE9_API_KEY=wrong-credential", + } + result := runTIWithInput(t, bin, "", env, "--region", regionCode, "fs", "list-files", "--path", "/") + result.wantExitCode(0) + assertFakeDrive9Call(t, readFakeDrive9Calls(t, recordPath), []string{"fs", "ls"}, token, home, "default", fileSystemID, "https://"+regionCode+".drive9.ai", regionCode) + for _, name := range []string{"config", "credentials", "fs_credentials"} { + if _, err := os.Stat(filepath.Join(home, ".ti", name)); !os.IsNotExist(err) { + t.Fatalf("token-only access persisted %s: %v", name, err) + } + } + }) + } +} + func TestFSConfigurationFreeAccess(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("fake companion build path is covered by unit tests on Windows") diff --git a/e2e/installer_test.go b/e2e/installer_test.go index 6333f79..0507bc0 100644 --- a/e2e/installer_test.go +++ b/e2e/installer_test.go @@ -208,7 +208,7 @@ fi t.Fatalf("installer did not validate companion command surface %q; calls:\n%s", want, companionCallData) } } - for _, regionCode := range []string{"aws-us-east-1", "aws-ap-southeast-1", "aws-us-west-2", "alicloud-ap-southeast-1", "gcp-us-east1"} { + for _, regionCode := range []string{"aws-us-east-1", "aws-ap-southeast-1", "aws-us-west-2", "alicloud-ap-southeast-1", "gcp-us-east1", "azure-centralus"} { if !strings.Contains(string(output), regionCode) { t.Fatalf("installer did not list ti fs region %q:\n%s", regionCode, output) } @@ -294,7 +294,7 @@ func TestInstallersUseProductOwnedFSRegionsAndHideCompanionPath(t *testing.T) { if strings.Contains(content, "ti fs companion installed to") { t.Fatalf("%s still reports the companion installation path", path) } - for _, regionCode := range []string{"aws-us-east-1", "aws-ap-southeast-1", "aws-us-west-2", "alicloud-ap-southeast-1", "gcp-us-east1"} { + for _, regionCode := range []string{"aws-us-east-1", "aws-ap-southeast-1", "aws-us-west-2", "alicloud-ap-southeast-1", "gcp-us-east1", "azure-centralus"} { if !strings.Contains(content, regionCode) { t.Fatalf("%s does not list ti fs region %q", path, regionCode) } diff --git a/internal/api/endpoints/endpoints.go b/internal/api/endpoints/endpoints.go index b184727..4c3c1ce 100644 --- a/internal/api/endpoints/endpoints.go +++ b/internal/api/endpoints/endpoints.go @@ -246,6 +246,7 @@ func builtInFSRegionManifest() *FSRegionManifest { {RegionCode: "aws-us-west-2", Mode: DefaultFSMode, ServerURL: "https://aws-us-west-2.drive9.ai", CloudProvider: "aws", TiDBRegion: "us-west-2"}, {RegionCode: "alicloud-ap-southeast-1", Mode: DefaultFSMode, ServerURL: "https://alicloud-ap-southeast-1.drive9.ai", CloudProvider: "alicloud", TiDBRegion: "ap-southeast-1"}, {RegionCode: "gcp-us-east1", Mode: DefaultFSMode, ServerURL: "https://gcp-us-east1.drive9.ai", CloudProvider: "gcp", TiDBRegion: "us-east1"}, + {RegionCode: "azure-centralus", Mode: DefaultFSMode, ServerURL: "https://azure-centralus.drive9.ai", CloudProvider: "azure", TiDBRegion: "centralus"}, }, } } diff --git a/internal/api/endpoints/endpoints_test.go b/internal/api/endpoints/endpoints_test.go index 159d268..3539266 100644 --- a/internal/api/endpoints/endpoints_test.go +++ b/internal/api/endpoints/endpoints_test.go @@ -160,6 +160,7 @@ func TestResolveFSUsesBuiltInRegionMapping(t *testing.T) { {name: "AWS Oregon", provider: region.ProviderAWS, regionCode: "us-west-2", baseURL: "https://aws-us-west-2.drive9.ai", regionName: "aws-us-west-2"}, {name: "Alibaba Cloud Singapore", provider: region.ProviderAlibabaCloud, regionCode: "ap-southeast-1", baseURL: "https://alicloud-ap-southeast-1.drive9.ai", regionName: "alicloud-ap-southeast-1"}, {name: "GCP South Carolina", provider: region.ProviderGCP, regionCode: "us-east1", baseURL: "https://gcp-us-east1.drive9.ai", regionName: "gcp-us-east1"}, + {name: "Azure Central US", provider: region.ProviderAzure, regionCode: "centralus", baseURL: "https://azure-centralus.drive9.ai", regionName: "azure-centralus"}, } resolver := NewResolver() if resolver.FSManifestURL != "" { diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 1ef5034..378f1b4 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -1133,6 +1133,20 @@ func TestRegionOverrideWinsOverEnvironmentCredentials(t *testing.T) { } } +func TestAzureFileSystemRegionOverride(t *testing.T) { + withConfigEnv(t) + + stdout, _, err := executeForTest("--region", "azure-centralus", "fs", "create-file-system", "--dry-run") + if err != nil { + t.Fatalf("expected Azure file system dry-run to succeed: %v", err) + } + for _, want := range []string{"azure centralus", "/v1/admin/tenants"} { + if !strings.Contains(stdout, want) { + t.Fatalf("Azure dry-run missing %q:\n%s", want, stdout) + } + } +} + func TestRegionOverrideAllowsEnvironmentCredentialsWithoutEnvRegion(t *testing.T) { t.Setenv("TI_REGION_CODE", "") t.Setenv("TIDB_CLOUD_PUBLIC_KEY", "test-public") diff --git a/internal/config/region/region.go b/internal/config/region/region.go index 61b3beb..46baa9b 100644 --- a/internal/config/region/region.go +++ b/internal/config/region/region.go @@ -10,11 +10,13 @@ const ( ProviderAWS = "aws" ProviderAlibabaCloud = "alibaba_cloud" ProviderGCP = "gcp" + ProviderAzure = "azure" ProviderPrefixAWS = "aws" ProviderPrefixAlibabaCloud = "alicloud" ProviderPrefixAlibabaCloudLegacy = "ali" ProviderPrefixGCP = "gcp" + ProviderPrefixAzure = "azure" ) type Region struct { @@ -45,6 +47,9 @@ var supported = map[string][]Region{ ProviderGCP: { {Code: "us-east1", Label: "South Carolina"}, }, + ProviderAzure: { + {Code: "centralus", Label: "Central US"}, + }, } var providerPrefixes = map[string]string{ @@ -52,12 +57,14 @@ var providerPrefixes = map[string]string{ ProviderPrefixAlibabaCloud: ProviderAlibabaCloud, ProviderPrefixAlibabaCloudLegacy: ProviderAlibabaCloud, ProviderPrefixGCP: ProviderGCP, + ProviderPrefixAzure: ProviderAzure, } var providerToPrefix = map[string]string{ ProviderAWS: ProviderPrefixAWS, ProviderAlibabaCloud: ProviderPrefixAlibabaCloud, ProviderGCP: ProviderPrefixGCP, + ProviderAzure: ProviderPrefixAzure, } func Providers() []string { diff --git a/internal/config/region/region_test.go b/internal/config/region/region_test.go index 50208dc..bb283d3 100644 --- a/internal/config/region/region_test.go +++ b/internal/config/region/region_test.go @@ -14,6 +14,7 @@ func TestValidateSupportedProviderRegions(t *testing.T) { {ProviderAWS, "ap-southeast-1"}, {ProviderAlibabaCloud, "ap-southeast-1"}, {ProviderGCP, "us-east1"}, + {ProviderAzure, "centralus"}, } for _, tt := range tests { @@ -31,6 +32,7 @@ func TestValidateRejectsUnsupportedProviderRegions(t *testing.T) { region string }{ {"azure", "eastus"}, + {"unsupported", "centralus"}, {ProviderAlibabaCloud, "us-east-1"}, {ProviderAWS, "cn-hangzhou"}, } @@ -56,6 +58,7 @@ func TestParsePlacementCode(t *testing.T) { {"alicloud-ap-southeast-1", "alicloud-ap-southeast-1", ProviderAlibabaCloud, "ap-southeast-1"}, {"ali-ap-southeast-1", "alicloud-ap-southeast-1", ProviderAlibabaCloud, "ap-southeast-1"}, {"gcp-us-east1", "gcp-us-east1", ProviderGCP, "us-east1"}, + {"azure-centralus", "azure-centralus", ProviderAzure, "centralus"}, } for _, tt := range tests { @@ -67,12 +70,16 @@ func TestParsePlacementCode(t *testing.T) { if placement.Code != tt.canonical || placement.Provider != tt.provider || placement.NativeCode != tt.nativeCode { t.Fatalf("unexpected placement: %#v", placement) } + canonical, err := CanonicalCode(placement.Provider, placement.NativeCode) + if err != nil || canonical != tt.canonical { + t.Fatalf("canonical placement = %q, %v; want %q", canonical, err, tt.canonical) + } }) } } func TestParsePlacementCodeRejectsUnsupportedValues(t *testing.T) { - for _, code := range []string{"us-east-1", "ali-us-east-1", "gcp-us-east-1"} { + for _, code := range []string{"us-east-1", "ali-us-east-1", "gcp-us-east-1", "azure-eastus", "azure-central-us"} { t.Run(code, func(t *testing.T) { if _, err := ParsePlacementCode(code); err == nil { t.Fatal("expected placement code to be rejected") diff --git a/internal/fs/fscred/credential_test.go b/internal/fs/fscred/credential_test.go index fa0345a..eee7df8 100644 --- a/internal/fs/fscred/credential_test.go +++ b/internal/fs/fscred/credential_test.go @@ -53,6 +53,37 @@ func TestCredentialStoreAndResolveByID(t *testing.T) { } } +func TestCredentialPreservesCloudRegionAcrossProfilePlacements(t *testing.T) { + for _, test := range []struct { + provider string + region string + }{ + {provider: "gcp", region: "gcp-us-east1"}, + {provider: "azure", region: "azure-centralus"}, + } { + t.Run(test.provider, func(t *testing.T) { + home := t.TempDir() + profile := credentialTestProfile() + const fileSystemID = "tenant-cloud-region" + if _, err := StoreCredential(home, profile, fileSystemID, test.region, wrappedToken(t, fileSystemID), false); err != nil { + t.Fatal(err) + } + selected, credential, err := ResolveCredential(home, profile, ResolveCredentialOptions{ + FileSystemID: fileSystemID, FileSystemIDExplicit: true, TokenRequired: true, + }) + if err != nil { + t.Fatal(err) + } + if credential.RegionCode != test.region || selected.FSPlacementRegionCode != test.region || selected.FSCloudProvider != test.provider { + t.Fatalf("stored region = %q, selected region = %q, provider = %q", credential.RegionCode, selected.FSPlacementRegionCode, selected.FSCloudProvider) + } + if profile.CloudProvider != "aws" || profile.PlacementRegionCode != "aws-us-east-1" { + t.Fatal("resolving the resource changed the profile placement") + } + }) + } +} + func TestCredentialOptionalTokenMetadataAndExactDelete(t *testing.T) { home := t.TempDir() profile := credentialTestProfile() diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go index 5d438fc..e5b2ad1 100644 --- a/internal/telemetry/telemetry.go +++ b/internal/telemetry/telemetry.go @@ -468,7 +468,7 @@ func normalizedInstallSource(value string) string { func normalizedProvider(value string) string { switch value { - case "aws", "alibaba_cloud", "gcp": + case "aws", "alibaba_cloud", "gcp", "azure": return value case "": return "" diff --git a/internal/telemetry/telemetry_test.go b/internal/telemetry/telemetry_test.go index 8fb6bf9..5628dd3 100644 --- a/internal/telemetry/telemetry_test.go +++ b/internal/telemetry/telemetry_test.go @@ -316,6 +316,33 @@ func TestFinishSendsOnlyAllowlistedEventFields(t *testing.T) { } } +func TestFinishPreservesAzurePlacement(t *testing.T) { + requests := make(chan []byte, 1) + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + body, _ := io.ReadAll(request.Body) + requests <- body + writer.WriteHeader(http.StatusAccepted) + })) + defer server.Close() + + cfg := enabledConfig(t.TempDir()) + cfg.Endpoint = server.URL + session := Start(cfg) + session.Finish(EventInput{CommandPath: "ti fs list-file-systems", CloudProvider: "azure", RegionCode: "azure-centralus"}) + var batch struct { + Events []struct { + CloudProvider string `json:"cloud_provider"` + RegionCode string `json:"region_code"` + } `json:"events"` + } + if err := json.Unmarshal(<-requests, &batch); err != nil { + t.Fatal(err) + } + if len(batch.Events) != 1 || batch.Events[0].CloudProvider != "azure" || batch.Events[0].RegionCode != "azure-centralus" { + t.Fatalf("Azure placement was not preserved: %#v", batch.Events) + } +} + func TestTelemetryEnvironmentMetadataIsBoundedAndNotPersisted(t *testing.T) { requests := make(chan []byte, 1) server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { diff --git a/internal/telemetrybackend/event.go b/internal/telemetrybackend/event.go index 99a9743..d2d6016 100644 --- a/internal/telemetrybackend/event.go +++ b/internal/telemetrybackend/event.go @@ -72,6 +72,7 @@ var allowedRegions = map[string]struct{}{ "ali-ap-southeast-1": {}, "alicloud-ap-southeast-1": {}, "gcp-us-east1": {}, + "azure-centralus": {}, } var allowedOperatingSystems = map[string]struct{}{ @@ -206,7 +207,7 @@ func validateEvent(raw wireEvent, schemaVersion int, receivedAt time.Time) (Even if raw.DurationMS == nil || *raw.DurationMS < 0 || *raw.DurationMS > 86_400_000 { return Event{}, errors.New("invalid duration_ms") } - if !oneOf(raw.CloudProvider, "", "aws", "alibaba_cloud", "gcp", "unknown") { + if !oneOf(raw.CloudProvider, "", "aws", "alibaba_cloud", "gcp", "azure", "unknown") { return Event{}, errors.New("invalid cloud_provider") } if _, ok := allowedRegions[raw.RegionCode]; !ok { diff --git a/internal/telemetrybackend/event_test.go b/internal/telemetrybackend/event_test.go index 58de5ea..fb4997e 100644 --- a/internal/telemetrybackend/event_test.go +++ b/internal/telemetrybackend/event_test.go @@ -22,24 +22,34 @@ func TestDecodeAndValidateBatchAcceptsAllowlistedEvent(t *testing.T) { } } -func TestDecodeAndValidateBatchAcceptsGCPPlacement(t *testing.T) { - var request map[string]any - if err := json.Unmarshal(validRequestBody(), &request); err != nil { - t.Fatal(err) - } - event := request["events"].([]any)[0].(map[string]any) - event["cloud_provider"] = "gcp" - event["region_code"] = "gcp-us-east1" - body, err := json.Marshal(request) - if err != nil { - t.Fatal(err) - } - events, err := decodeAndValidateBatch(body, 20, time.Now()) - if err != nil { - t.Fatalf("gcp placement should be accepted: %v", err) - } - if len(events) != 1 || events[0].CloudProvider != "gcp" || events[0].RegionCode != "gcp-us-east1" { - t.Fatalf("unexpected gcp event: %#v", events) +func TestDecodeAndValidateBatchAcceptsFilesystemPlacements(t *testing.T) { + for _, test := range []struct { + provider string + region string + }{ + {provider: "gcp", region: "gcp-us-east1"}, + {provider: "azure", region: "azure-centralus"}, + } { + t.Run(test.provider, func(t *testing.T) { + var request map[string]any + if err := json.Unmarshal(validRequestBody(), &request); err != nil { + t.Fatal(err) + } + event := request["events"].([]any)[0].(map[string]any) + event["cloud_provider"] = test.provider + event["region_code"] = test.region + body, err := json.Marshal(request) + if err != nil { + t.Fatal(err) + } + events, err := decodeAndValidateBatch(body, 20, time.Now()) + if err != nil { + t.Fatalf("placement should be accepted: %v", err) + } + if len(events) != 1 || events[0].CloudProvider != test.provider || events[0].RegionCode != test.region { + t.Fatalf("unexpected placement: %#v", events) + } + }) } } @@ -201,7 +211,7 @@ func TestDecodeAndValidateBatchRejectsInvalidEnumsAndLimits(t *testing.T) { {"event_name", "custom.event"}, {"command_path", "rm -rf"}, {"command_path", "ti db list-db-clusters select secret"}, - {"cloud_provider", "azure"}, + {"cloud_provider", "unsupported"}, {"region_code", "us-east-1"}, {"install_source", "curl"}, {"profile_source", "production"}, diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 3374fef..760fa15 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -113,10 +113,10 @@ function Print-Regions { Write-Output "" Write-Output " Config 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 " alicloud-ap-southeast-1" + Write-Output " alicloud-ap-southeast-1, gcp-us-east1, azure-centralus" Write-Output "" Write-Output " ti fs regions:" - Write-Output " aws-us-east-1, aws-ap-southeast-1, aws-us-west-2, alicloud-ap-southeast-1, gcp-us-east1" + Write-Output " aws-us-east-1, aws-ap-southeast-1, aws-us-west-2, alicloud-ap-southeast-1, gcp-us-east1, azure-centralus" } function Print-NextSteps { diff --git a/scripts/install.sh b/scripts/install.sh index 40d928b..c124d99 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -277,10 +277,10 @@ print_regions() { printf "\n" printf " ${BOLD}Config regions:${RESET}\n" printf " aws-us-east-1, aws-us-west-2, aws-eu-central-1, aws-ap-northeast-1, aws-ap-southeast-1\n" - printf " alicloud-ap-southeast-1\n" + printf " alicloud-ap-southeast-1, gcp-us-east1, azure-centralus\n" printf "\n" printf " ${BOLD}ti fs regions:${RESET}\n" - printf " aws-us-east-1, aws-ap-southeast-1, aws-us-west-2, alicloud-ap-southeast-1, gcp-us-east1\n" + printf " aws-us-east-1, aws-ap-southeast-1, aws-us-west-2, alicloud-ap-southeast-1, gcp-us-east1, azure-centralus\n" } print_next_steps() { From 2fcecd3567664dca26ad2917d8fdeedfa5ee5285 Mon Sep 17 00:00:00 2001 From: siddon Date: Wed, 14 Oct 2026 16:28:46 -0700 Subject: [PATCH 2/5] test(fs): check WebDAV fixture access before readiness assertions Signed-off-by: siddon --- internal/fs/mountready_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/internal/fs/mountready_test.go b/internal/fs/mountready_test.go index c179fdc..d805c4c 100644 --- a/internal/fs/mountready_test.go +++ b/internal/fs/mountready_test.go @@ -2,6 +2,7 @@ package fs import ( "context" + "errors" "fmt" "net" "net/http" @@ -118,6 +119,16 @@ func TestDefaultMountPointActiveAcceptsRealWebDAVMount(t *testing.T) { } defer func() { _ = exec.Command("umount", mp).Run() }() + // macOS can permit mount_webdav while denying this process access to the + // mounted directory. Check the kernel mount independently of our probe so + // privacy or sandbox restrictions are not reported as a probe regression. + if _, err := os.ReadDir(mp); err != nil { + if errors.Is(err, os.ErrPermission) { + t.Skipf("macOS does not permit reading the WebDAV fixture: %v", err) + } + t.Fatalf("read WebDAV fixture: %v", err) + } + if active, err := defaultMountPointActive(mp); err != nil || !active { t.Fatalf("real WebDAV mount must count as active: active=%v err=%v", active, err) } From 57e51594ecb9040f7597e08f570c7e087f4605c8 Mon Sep 17 00:00:00 2001 From: srstack Date: Thu, 15 Oct 2026 20:07:46 +0800 Subject: [PATCH 3/5] fix(fs): keep azure-centralus a Filesystem-only placement `region.supported` is shared by TiDB Cloud Starter and Filesystem resolution, so adding azure-centralus there also let `ti db` commands accept the placement locally and send `regions/azure-centralus` to the Starter API, which the product documentation does not list as a Starter placement. Mark the placement as Filesystem-only instead. `region.Validate` (Starter) now rejects it with `placement "azure-centralus" serves TiDB Cloud Filesystem only, not TiDB Cloud Starter`, while the new `region.ValidateFilesystem` accepts it and `ResolveFS` validates through that function. `ParsePlacementCode` and `CanonicalCode` keep accepting every known placement, so `--region`, `ti configure`, profiles, and stored credentials are unaffected. - README, docs/priciples.md, and AGENTS.md document the rule. - Tests cover the Filesystem-only rejection in internal/config/region, internal/api/endpoints, and internal/cli, and move the azure placement out of the Starter placement table. `gcp-us-east1` keeps its current behavior; applying the same model to it is a one-line addition to `filesystemOnly`. --- AGENTS.md | 8 ++++ README.md | 2 +- docs/priciples.md | 2 +- internal/api/endpoints/endpoints.go | 2 +- internal/api/endpoints/endpoints_test.go | 23 ++++++++++ internal/cli/root_test.go | 15 +++++++ internal/config/region/region.go | 41 +++++++++++++++++- internal/config/region/region_test.go | 54 ++++++++++++++++++++++-- 8 files changed, 139 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 10e062b..f1de51f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1009,6 +1009,14 @@ internal provider `aws`; `alicloud` maps to internal provider internal provider `azure`. Keep this mapping centralized in `internal/config/region`. +Filesystem-only placements are marked in +`internal/config/region.filesystemOnly`. `azure-centralus` is the current +Filesystem-only placement: `region.ValidateFilesystem` accepts it for +`ti fs`, while `region.Validate` keeps it out of TiDB Cloud Starter placement +validation so `ti db` commands fail locally with an explicit Filesystem-only +error. The built-in Filesystem endpoint mapping remains the authority on which +placements have a Filesystem endpoint. + Do not store secrets in logs, telemetry, generated docs examples, or test fixtures. diff --git a/README.md b/README.md index 2f284bd..56598eb 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ Automation should move to `TI_*` and `TIDB_CLOUD_*` environment variables. The v - 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, alicloud-ap-southeast-1, gcp-us-east1, or azure-centralus. - Regions supporting TiDB Cloud Filesystem: aws-us-east-1, aws-ap-southeast-1, aws-us-west-2, alicloud-ap-southeast-1, gcp-us-east1, or azure-centralus. 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. + - 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. `azure-centralus` is a Filesystem-only placement, so `ti db` commands reject it during local placement validation. Set up a default profile with one command: diff --git a/docs/priciples.md b/docs/priciples.md index 45e6dd9..f874dfe 100644 --- a/docs/priciples.md +++ b/docs/priciples.md @@ -77,7 +77,7 @@ Supported TiDB Cloud Starter placement values are: | `aws-ap-southeast-1` | AWS | Singapore | | `alicloud-ap-southeast-1` | Alibaba Cloud | Singapore | -`aws` maps to the internal provider `aws`; `alicloud` maps to `alibaba_cloud`; `gcp` maps to `gcp`; `azure` maps to `azure`. TiDB Cloud Filesystem supports `aws-us-east-1`, `aws-ap-southeast-1`, `aws-us-west-2`, `alicloud-ap-southeast-1`, `gcp-us-east1`, and `azure-centralus` through endpoint mappings built into `ti`. +`aws` maps to the internal provider `aws`; `alicloud` maps to `alibaba_cloud`; `gcp` maps to `gcp`; `azure` maps to `azure`. TiDB Cloud Filesystem supports `aws-us-east-1`, `aws-ap-southeast-1`, `aws-us-west-2`, `alicloud-ap-southeast-1`, `gcp-us-east1`, and `azure-centralus` through endpoint mappings built into `ti`. `azure-centralus` is a Filesystem-only placement: TiDB Cloud Starter commands reject it during local placement validation instead of sending an unsupported region to the Starter API. ## TiDB Cloud Authentication diff --git a/internal/api/endpoints/endpoints.go b/internal/api/endpoints/endpoints.go index 4c3c1ce..0464f5f 100644 --- a/internal/api/endpoints/endpoints.go +++ b/internal/api/endpoints/endpoints.go @@ -133,7 +133,7 @@ func (r Resolver) ResolveIAM() (Endpoint, error) { } func (r Resolver) ResolveFS(provider, regionCode string) (Endpoint, error) { - if err := region.Validate(provider, regionCode); err != nil { + if err := region.ValidateFilesystem(provider, regionCode); err != nil { return Endpoint{}, apperr.Wrap("config.invalid_region", "config", 2, err.Error(), err) } diff --git a/internal/api/endpoints/endpoints_test.go b/internal/api/endpoints/endpoints_test.go index 3539266..9b08794 100644 --- a/internal/api/endpoints/endpoints_test.go +++ b/internal/api/endpoints/endpoints_test.go @@ -179,6 +179,29 @@ func TestResolveFSUsesBuiltInRegionMapping(t *testing.T) { } } +func TestResolveStarterRejectsFilesystemOnlyPlacement(t *testing.T) { + resolver := NewResolver() + + endpoint, err := resolver.ResolveFS(region.ProviderAzure, "centralus") + if err != nil { + t.Fatalf("ResolveFS failed for the Filesystem-only placement: %v", err) + } + if endpoint.BaseURL != "https://azure-centralus.drive9.ai" || endpoint.RegionName != "azure-centralus" { + t.Fatalf("unexpected fs endpoint: %#v", endpoint) + } + + if _, err := resolver.ResolveStarter(region.ProviderAzure, "centralus"); err == nil { + t.Fatal("expected Starter resolution to reject the Filesystem-only placement") + } else { + if got := apperr.ExitCodeFor(err); got != 2 { + t.Fatalf("expected exit code 2, got %d", got) + } + if message := apperr.MessageFor(err); !strings.Contains(message, "azure-centralus") || !strings.Contains(message, "Filesystem only") { + t.Fatalf("unexpected Starter error message %q", message) + } + } +} + func TestResolveFSUnsupportedManifestRegion(t *testing.T) { resolver := Resolver{ FSManifest: &FSRegionManifest{ diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 378f1b4..2b57dfb 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -1147,6 +1147,21 @@ func TestAzureFileSystemRegionOverride(t *testing.T) { } } +func TestAzureFilesystemOnlyRegionRejectedForStarter(t *testing.T) { + withConfigEnv(t) + + _, _, err := executeForTest("--region", "azure-centralus", "db", "create-db-cluster", "--db-cluster-type", "starter", "--db-cluster-name", "demo-cluster", "--dry-run") + if err == nil { + t.Fatal("expected the Filesystem-only placement to fail for TiDB Cloud Starter") + } + if got := apperr.ExitCodeFor(err); got != 2 { + t.Fatalf("expected exit code 2, got %d", got) + } + if got := apperr.MessageFor(err); !strings.Contains(got, "azure-centralus") || !strings.Contains(got, "Filesystem only") { + t.Fatalf("unexpected error message %q", got) + } +} + func TestRegionOverrideAllowsEnvironmentCredentialsWithoutEnvRegion(t *testing.T) { t.Setenv("TI_REGION_CODE", "") t.Setenv("TIDB_CLOUD_PUBLIC_KEY", "test-public") diff --git a/internal/config/region/region.go b/internal/config/region/region.go index 46baa9b..483884d 100644 --- a/internal/config/region/region.go +++ b/internal/config/region/region.go @@ -52,6 +52,19 @@ var supported = map[string][]Region{ }, } +// filesystemOnly marks placements that serve TiDB Cloud Filesystem without a +// TiDB Cloud Starter placement. TiDB Cloud Starter resolution rejects them +// through Validate, while Filesystem resolution accepts them through +// ValidateFilesystem; the built-in Filesystem endpoint mapping remains the +// authority on whether a Filesystem endpoint exists for a placement. +var filesystemOnly = map[string]struct{}{ + placementKey(ProviderAzure, "centralus"): {}, +} + +func placementKey(provider, regionCode string) string { + return provider + "\x00" + regionCode +} + var providerPrefixes = map[string]string{ ProviderPrefixAWS: ProviderAWS, ProviderPrefixAlibabaCloud: ProviderAlibabaCloud, @@ -116,8 +129,11 @@ func DefaultRegion(provider string) (string, error) { return regions[0].Code, nil } +// CanonicalCode returns the canonical placement code for an accepted +// provider/region pair. It accepts every placement ti can address, including +// Filesystem-only placements such as azure-centralus. func CanonicalCode(provider, nativeCode string) (string, error) { - if err := Validate(provider, nativeCode); err != nil { + if err := validatePlacement(provider, nativeCode); err != nil { return "", err } prefix, ok := providerToPrefix[provider] @@ -137,7 +153,7 @@ func ParsePlacementCode(code string) (Placement, error) { if !ok { return Placement{}, fmt.Errorf("unsupported cloud provider prefix %q in region code %q; supported prefixes: %s", prefix, code, joinProviderPrefixes()) } - if err := Validate(provider, nativeCode); err != nil { + if err := validatePlacement(provider, nativeCode); err != nil { return Placement{}, err } regionLabel := "" @@ -158,7 +174,28 @@ func ParsePlacementCode(code string) (Placement, error) { }, nil } +// Validate reports whether provider/regionCode is a supported TiDB Cloud +// Starter placement. A placement that only serves TiDB Cloud Filesystem is +// rejected with an explicit Filesystem-only error instead of being reported as +// unknown. func Validate(provider, regionCode string) error { + if err := validatePlacement(provider, regionCode); err != nil { + return err + } + if _, ok := filesystemOnly[placementKey(provider, regionCode)]; ok { + return fmt.Errorf("placement %q serves TiDB Cloud Filesystem only, not TiDB Cloud Starter", providerToPrefix[provider]+"-"+regionCode) + } + return nil +} + +// ValidateFilesystem reports whether provider/regionCode can be routed to a +// TiDB Cloud Filesystem endpoint. Whether an endpoint exists for the placement +// is decided by the built-in Filesystem region mapping. +func ValidateFilesystem(provider, regionCode string) error { + return validatePlacement(provider, regionCode) +} + +func validatePlacement(provider, regionCode string) error { regions, ok := supported[provider] if !ok { return fmt.Errorf("unsupported cloud provider %q; supported providers: %s", provider, joinProviders()) diff --git a/internal/config/region/region_test.go b/internal/config/region/region_test.go index bb283d3..5d49ff8 100644 --- a/internal/config/region/region_test.go +++ b/internal/config/region/region_test.go @@ -1,6 +1,9 @@ package region -import "testing" +import ( + "strings" + "testing" +) func TestValidateSupportedProviderRegions(t *testing.T) { tests := []struct { @@ -14,7 +17,6 @@ func TestValidateSupportedProviderRegions(t *testing.T) { {ProviderAWS, "ap-southeast-1"}, {ProviderAlibabaCloud, "ap-southeast-1"}, {ProviderGCP, "us-east1"}, - {ProviderAzure, "centralus"}, } for _, tt := range tests { @@ -31,7 +33,7 @@ func TestValidateRejectsUnsupportedProviderRegions(t *testing.T) { provider string region string }{ - {"azure", "eastus"}, + {ProviderAzure, "eastus"}, {"unsupported", "centralus"}, {ProviderAlibabaCloud, "us-east-1"}, {ProviderAWS, "cn-hangzhou"}, @@ -46,6 +48,52 @@ func TestValidateRejectsUnsupportedProviderRegions(t *testing.T) { } } +func TestValidateFilesystemAcceptsFilesystemPlacements(t *testing.T) { + tests := []struct { + provider string + region string + }{ + {ProviderAWS, "us-east-1"}, + {ProviderAWS, "ap-southeast-1"}, + {ProviderAlibabaCloud, "ap-southeast-1"}, + {ProviderGCP, "us-east1"}, + {ProviderAzure, "centralus"}, + } + + for _, tt := range tests { + t.Run(tt.provider+"/"+tt.region, func(t *testing.T) { + if err := ValidateFilesystem(tt.provider, tt.region); err != nil { + t.Fatalf("expected filesystem placement to be valid: %v", err) + } + }) + } + + for _, tt := range []struct { + provider string + region string + }{ + {"unsupported", "centralus"}, + {ProviderAzure, "eastus"}, + {ProviderAWS, "cn-hangzhou"}, + } { + t.Run("reject/"+tt.provider+"/"+tt.region, func(t *testing.T) { + if err := ValidateFilesystem(tt.provider, tt.region); err == nil { + t.Fatal("expected filesystem provider/region to be rejected") + } + }) + } +} + +func TestValidateRejectsFilesystemOnlyPlacementForStarter(t *testing.T) { + err := Validate(ProviderAzure, "centralus") + if err == nil { + t.Fatal("expected the Filesystem-only placement to be rejected for Starter") + } + if got := err.Error(); !strings.Contains(got, "azure-centralus") || !strings.Contains(got, "Filesystem only") { + t.Fatalf("unexpected Filesystem-only error %q", got) + } +} + func TestParsePlacementCode(t *testing.T) { tests := []struct { code string From 15c7886d26459eb3abc5a6cc650f283cb39731da Mon Sep 17 00:00:00 2001 From: srstack Date: Thu, 15 Oct 2026 20:42:13 +0800 Subject: [PATCH 4/5] chore: re-run checks for the Filesystem-only azure placement Empty commit to force a fresh mergeability evaluation after the CLA status turned green and the reviewing account switched to APPROVED. No content change. From afa9a892f4e1bfa86cda55ae887f8c0f375f03da Mon Sep 17 00:00:00 2001 From: srstack Date: Thu, 15 Oct 2026 20:51:29 +0800 Subject: [PATCH 5/5] fix(fs): treat gcp-us-east1 as a Filesystem-only placement `gcp-us-east1` serves TiDB Cloud Filesystem only. The v0.2.5 release note and the product documentation already limit TiDB Cloud Starter to the AWS and Alibaba Cloud placements, but the shared placement table still let `ti db` commands accept the placement locally and send `regions/gcp-us-east1` to the Starter API. Add it to `filesystemOnly` so `region.Validate` rejects it with `placement "gcp-us-east1" serves TiDB Cloud Filesystem only, not TiDB Cloud Starter`, while `region.ValidateFilesystem` keeps routing `ti fs` to the built-in endpoint. README, docs/priciples.md, and AGENTS.md now list both Filesystem-only placements, and the Starter rejection tests cover gcp and azure. --- AGENTS.md | 6 ++--- README.md | 2 +- docs/priciples.md | 2 +- internal/api/endpoints/endpoints_test.go | 32 +++++++++++++++++------- internal/cli/root_test.go | 24 ++++++++++-------- internal/config/region/region.go | 1 + internal/config/region/region_test.go | 24 ++++++++++++------ 7 files changed, 60 insertions(+), 31 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f1de51f..e6af776 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1010,9 +1010,9 @@ internal provider `azure`. Keep this mapping centralized in `internal/config/region`. Filesystem-only placements are marked in -`internal/config/region.filesystemOnly`. `azure-centralus` is the current -Filesystem-only placement: `region.ValidateFilesystem` accepts it for -`ti fs`, while `region.Validate` keeps it out of TiDB Cloud Starter placement +`internal/config/region.filesystemOnly`. `gcp-us-east1` and `azure-centralus` +are the Filesystem-only placements: `region.ValidateFilesystem` accepts them for +`ti fs`, while `region.Validate` keeps them out of TiDB Cloud Starter placement validation so `ti db` commands fail locally with an explicit Filesystem-only error. The built-in Filesystem endpoint mapping remains the authority on which placements have a Filesystem endpoint. diff --git a/README.md b/README.md index 56598eb..51ae14d 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ Automation should move to `TI_*` and `TIDB_CLOUD_*` environment variables. The v - 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, alicloud-ap-southeast-1, gcp-us-east1, or azure-centralus. - Regions supporting TiDB Cloud Filesystem: aws-us-east-1, aws-ap-southeast-1, aws-us-west-2, alicloud-ap-southeast-1, gcp-us-east1, or azure-centralus. 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. `azure-centralus` is a Filesystem-only placement, so `ti db` commands reject it during local placement validation. + - 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. `gcp-us-east1` and `azure-centralus` are Filesystem-only placements, so `ti db` commands reject them during local placement validation. Set up a default profile with one command: diff --git a/docs/priciples.md b/docs/priciples.md index f874dfe..1eaf760 100644 --- a/docs/priciples.md +++ b/docs/priciples.md @@ -77,7 +77,7 @@ Supported TiDB Cloud Starter placement values are: | `aws-ap-southeast-1` | AWS | Singapore | | `alicloud-ap-southeast-1` | Alibaba Cloud | Singapore | -`aws` maps to the internal provider `aws`; `alicloud` maps to `alibaba_cloud`; `gcp` maps to `gcp`; `azure` maps to `azure`. TiDB Cloud Filesystem supports `aws-us-east-1`, `aws-ap-southeast-1`, `aws-us-west-2`, `alicloud-ap-southeast-1`, `gcp-us-east1`, and `azure-centralus` through endpoint mappings built into `ti`. `azure-centralus` is a Filesystem-only placement: TiDB Cloud Starter commands reject it during local placement validation instead of sending an unsupported region to the Starter API. +`aws` maps to the internal provider `aws`; `alicloud` maps to `alibaba_cloud`; `gcp` maps to `gcp`; `azure` maps to `azure`. TiDB Cloud Filesystem supports `aws-us-east-1`, `aws-ap-southeast-1`, `aws-us-west-2`, `alicloud-ap-southeast-1`, `gcp-us-east1`, and `azure-centralus` through endpoint mappings built into `ti`. `gcp-us-east1` and `azure-centralus` are Filesystem-only placements: TiDB Cloud Starter commands reject them during local placement validation instead of sending an unsupported region to the Starter API. ## TiDB Cloud Authentication diff --git a/internal/api/endpoints/endpoints_test.go b/internal/api/endpoints/endpoints_test.go index 9b08794..34ca70e 100644 --- a/internal/api/endpoints/endpoints_test.go +++ b/internal/api/endpoints/endpoints_test.go @@ -190,15 +190,29 @@ func TestResolveStarterRejectsFilesystemOnlyPlacement(t *testing.T) { t.Fatalf("unexpected fs endpoint: %#v", endpoint) } - if _, err := resolver.ResolveStarter(region.ProviderAzure, "centralus"); err == nil { - t.Fatal("expected Starter resolution to reject the Filesystem-only placement") - } else { - if got := apperr.ExitCodeFor(err); got != 2 { - t.Fatalf("expected exit code 2, got %d", got) - } - if message := apperr.MessageFor(err); !strings.Contains(message, "azure-centralus") || !strings.Contains(message, "Filesystem only") { - t.Fatalf("unexpected Starter error message %q", message) - } + for _, test := range []struct { + provider string + region string + canonical string + }{ + {provider: region.ProviderGCP, region: "us-east1", canonical: "gcp-us-east1"}, + {provider: region.ProviderAzure, region: "centralus", canonical: "azure-centralus"}, + } { + t.Run(test.canonical, func(t *testing.T) { + if _, err := resolver.ResolveFS(test.provider, test.region); err != nil { + t.Fatalf("ResolveFS failed for the Filesystem-only placement: %v", err) + } + if _, err := resolver.ResolveStarter(test.provider, test.region); err == nil { + t.Fatal("expected Starter resolution to reject the Filesystem-only placement") + } else { + if got := apperr.ExitCodeFor(err); got != 2 { + t.Fatalf("expected exit code 2, got %d", got) + } + if message := apperr.MessageFor(err); !strings.Contains(message, test.canonical) || !strings.Contains(message, "Filesystem only") { + t.Fatalf("unexpected Starter error message %q", message) + } + } + }) } } diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 2b57dfb..5db9b7d 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -1147,18 +1147,22 @@ func TestAzureFileSystemRegionOverride(t *testing.T) { } } -func TestAzureFilesystemOnlyRegionRejectedForStarter(t *testing.T) { +func TestFilesystemOnlyRegionsRejectedForStarter(t *testing.T) { withConfigEnv(t) - _, _, err := executeForTest("--region", "azure-centralus", "db", "create-db-cluster", "--db-cluster-type", "starter", "--db-cluster-name", "demo-cluster", "--dry-run") - if err == nil { - t.Fatal("expected the Filesystem-only placement to fail for TiDB Cloud Starter") - } - if got := apperr.ExitCodeFor(err); got != 2 { - t.Fatalf("expected exit code 2, got %d", got) - } - if got := apperr.MessageFor(err); !strings.Contains(got, "azure-centralus") || !strings.Contains(got, "Filesystem only") { - t.Fatalf("unexpected error message %q", got) + for _, regionCode := range []string{"gcp-us-east1", "azure-centralus"} { + t.Run(regionCode, func(t *testing.T) { + _, _, err := executeForTest("--region", regionCode, "db", "create-db-cluster", "--db-cluster-type", "starter", "--db-cluster-name", "demo-cluster", "--dry-run") + if err == nil { + t.Fatal("expected the Filesystem-only placement to fail for TiDB Cloud Starter") + } + if got := apperr.ExitCodeFor(err); got != 2 { + t.Fatalf("expected exit code 2, got %d", got) + } + if got := apperr.MessageFor(err); !strings.Contains(got, regionCode) || !strings.Contains(got, "Filesystem only") { + t.Fatalf("unexpected error message %q", got) + } + }) } } diff --git a/internal/config/region/region.go b/internal/config/region/region.go index 483884d..8da1127 100644 --- a/internal/config/region/region.go +++ b/internal/config/region/region.go @@ -58,6 +58,7 @@ var supported = map[string][]Region{ // ValidateFilesystem; the built-in Filesystem endpoint mapping remains the // authority on whether a Filesystem endpoint exists for a placement. var filesystemOnly = map[string]struct{}{ + placementKey(ProviderGCP, "us-east1"): {}, placementKey(ProviderAzure, "centralus"): {}, } diff --git a/internal/config/region/region_test.go b/internal/config/region/region_test.go index 5d49ff8..365d44d 100644 --- a/internal/config/region/region_test.go +++ b/internal/config/region/region_test.go @@ -16,7 +16,6 @@ func TestValidateSupportedProviderRegions(t *testing.T) { {ProviderAWS, "ap-northeast-1"}, {ProviderAWS, "ap-southeast-1"}, {ProviderAlibabaCloud, "ap-southeast-1"}, - {ProviderGCP, "us-east1"}, } for _, tt := range tests { @@ -85,12 +84,23 @@ func TestValidateFilesystemAcceptsFilesystemPlacements(t *testing.T) { } func TestValidateRejectsFilesystemOnlyPlacementForStarter(t *testing.T) { - err := Validate(ProviderAzure, "centralus") - if err == nil { - t.Fatal("expected the Filesystem-only placement to be rejected for Starter") - } - if got := err.Error(); !strings.Contains(got, "azure-centralus") || !strings.Contains(got, "Filesystem only") { - t.Fatalf("unexpected Filesystem-only error %q", got) + for _, test := range []struct { + provider string + region string + canonical string + }{ + {provider: ProviderGCP, region: "us-east1", canonical: "gcp-us-east1"}, + {provider: ProviderAzure, region: "centralus", canonical: "azure-centralus"}, + } { + t.Run(test.canonical, func(t *testing.T) { + err := Validate(test.provider, test.region) + if err == nil { + t.Fatal("expected the Filesystem-only placement to be rejected for Starter") + } + if got := err.Error(); !strings.Contains(got, test.canonical) || !strings.Contains(got, "Filesystem only") { + t.Fatalf("unexpected Filesystem-only error %q", got) + } + }) } }