Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -855,12 +855,11 @@ 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 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.
`https://iam.tidbapi.com`. The ti fs host is resolved from the four-region
endpoint mapping owned by `internal/api/endpoints`. If the mapping 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 or restore a runtime
manifest dependency.
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
Expand Down Expand Up @@ -951,11 +950,11 @@ Supported MVP placement values:
| `aws-eu-central-1` | AWS | Frankfurt |
| `aws-ap-northeast-1` | AWS | Tokyo |
| `aws-ap-southeast-1` | AWS | Singapore |
| `ali-ap-southeast-1` | Alibaba Cloud | Singapore |
| `alicloud-ap-southeast-1` | Alibaba Cloud | Singapore |

The prefix before the first `-` is the cloud provider selector. `aws` maps to
internal provider `aws`; `ali` maps to internal provider `alibaba_cloud`. Keep
this mapping centralized in `internal/config/region`.
internal provider `aws`; `alicloud` maps to internal provider
`alibaba_cloud`. Keep this mapping centralized in `internal/config/region`.

Do not store secrets in logs, telemetry, generated docs examples, or test
fixtures.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ 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.
- 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-us-west-2, aws-ap-southeast-1, or 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.
- 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.

Set up a default profile with one command:

Expand Down
4 changes: 2 additions & 2 deletions docs/priciples.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ Supported TiDB Cloud Starter placement values are:
| `aws-eu-central-1` | AWS | Frankfurt |
| `aws-ap-northeast-1` | AWS | Tokyo |
| `aws-ap-southeast-1` | AWS | Singapore |
| `ali-ap-southeast-1` | Alibaba Cloud | Singapore |
| `alicloud-ap-southeast-1` | Alibaba Cloud | Singapore |

`aws` maps to the internal provider `aws`; `ali` maps to `alibaba_cloud`. TiDB Cloud Filesystem availability is resolved from the hosted Drive9 region manifest and can be a subset of the Starter regions.
`aws` maps to the internal provider `aws`; `alicloud` maps to `alibaba_cloud`. TiDB Cloud Filesystem supports `aws-us-east-1`, `aws-ap-southeast-1`, `aws-us-west-2`, and `alicloud-ap-southeast-1` through endpoint mappings built into `ti`.

## TiDB Cloud Authentication

Expand Down
33 changes: 32 additions & 1 deletion e2e/installer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ case "$url" in
*/%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
Expand All @@ -196,6 +195,14 @@ fi
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 strings.Contains(string(output), "ti fs companion installed to") {
t.Fatalf("installer exposed the companion installation path:\n%s", output)
}
for _, regionCode := range []string{"aws-us-east-1", "aws-ap-southeast-1", "aws-us-west-2", "alicloud-ap-southeast-1"} {
if !strings.Contains(string(output), regionCode) {
t.Fatalf("installer did not list ti fs region %q:\n%s", regionCode, 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)
}
Expand Down Expand Up @@ -246,6 +253,30 @@ func TestInstallersDoNotEscalatePrivileges(t *testing.T) {
}
}

func TestInstallersUseProductOwnedFSRegionsAndHideCompanionPath(t *testing.T) {
for _, path := range []string{
filepath.Join("..", "scripts", "install.sh"),
filepath.Join("..", "scripts", "install.ps1"),
} {
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
content := string(data)
if strings.Contains(content, "drive9-regions.json") {
t.Fatalf("%s still downloads the Drive9 region manifest", path)
}
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"} {
if !strings.Contains(content, regionCode) {
t.Fatalf("%s does not list ti fs region %q", path, regionCode)
}
}
}
}

func TestInstallersExplainTelemetryControls(t *testing.T) {
for _, path := range []string{
filepath.Join("..", "scripts", "install.sh"),
Expand Down
152 changes: 22 additions & 130 deletions internal/api/endpoints/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@ package endpoints

import (
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strings"
"time"
Expand All @@ -29,24 +26,9 @@ const (
const (
DefaultStarterBaseURL = "https://serverless.tidbapi.com"
DefaultIAMBaseURL = "https://iam.tidbapi.com"
DefaultFSManifestURL = "https://drive9.ai/manifest/regions/drive9-regions.json"
DefaultFSMode = "tidb_cloud_native"
)

var errFSManifestUnavailable = errors.New("ti fs region manifest unavailable")

const (
fsManifestFetchAttempts = 3
fsManifestRetryDelay = 200 * time.Millisecond
fsManifestCacheMaxAge = 24 * time.Hour
)

type cachedFSRegionManifest struct {
ManifestURL string `json:"manifest_url"`
FetchedAt time.Time `json:"fetched_at"`
Manifest FSRegionManifest `json:"manifest"`
}

type ProviderRegion struct {
Provider string
Region string
Expand Down Expand Up @@ -75,7 +57,6 @@ func NewResolver() Resolver {
resolver := Resolver{
StarterBaseURL: DefaultStarterBaseURL,
IAMBaseURL: DefaultIAMBaseURL,
FSManifestURL: DefaultFSManifestURL,
FSMode: DefaultFSMode,
}
if os.Getenv("TI_ALLOW_TEST_ENDPOINTS") == "1" {
Expand Down Expand Up @@ -217,10 +198,14 @@ func (r Resolver) fsManifest() (*FSRegionManifest, error) {
return &manifest, nil
}
manifestURL := strings.TrimSpace(r.FSManifestURL)
if manifestURL == "" {
manifestURL = DefaultFSManifestURL
if manifestURL != "" {
return fetchFSManifest(context.Background(), manifestURL, r.FSManifestHTTPClient)
}
return fetchFSManifest(context.Background(), manifestURL, r.FSManifestHTTPClient)
manifest := builtInFSRegionManifest()
if err := validateFSManifest(manifest); err != nil {
return nil, err
}
return manifest, nil
}

func fetchFSManifest(ctx context.Context, manifestURL string, client *http.Client) (*FSRegionManifest, error) {
Expand All @@ -230,119 +215,38 @@ func fetchFSManifest(ctx context.Context, manifestURL string, client *http.Clien
client = http.DefaultClient
}

var lastErr error
for attempt := 0; attempt < fsManifestFetchAttempts; attempt++ {
manifest, retry, err := fetchFSManifestOnce(ctx, manifestURL, client)
if err == nil {
storeCachedFSManifest(manifestURL, manifest)
return manifest, nil
}
lastErr = err
if !retry || attempt == fsManifestFetchAttempts-1 {
break
}
timer := time.NewTimer(time.Duration(attempt+1) * fsManifestRetryDelay)
select {
case <-ctx.Done():
timer.Stop()
lastErr = apperr.Wrap("api.fs_manifest_unavailable", "api", 1, fmt.Sprintf("%s: fetch %s", errFSManifestUnavailable, manifestURL), ctx.Err())
if cached, cacheErr := loadCachedFSManifest(manifestURL); cacheErr == nil {
return cached, nil
}
return nil, lastErr
case <-timer.C:
}
}
if cached, cacheErr := loadCachedFSManifest(manifestURL); cacheErr == nil {
return cached, nil
}
return nil, lastErr
}

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 ti fs region manifest request", err)
return nil, apperr.Wrap("api.fs_manifest_request", "api", 1, "build test ti fs region manifest request", err)
}
res, err := client.Do(req)
if err != nil {
return nil, true, apperr.Wrap("api.fs_manifest_unavailable", "api", 1, fmt.Sprintf("%s: fetch %s", errFSManifestUnavailable, manifestURL), err)
return nil, apperr.Wrap("api.fs_manifest_unavailable", "api", 1, fmt.Sprintf("test ti fs region manifest unavailable: fetch %s", manifestURL), err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, retryableFSManifestStatus(res.StatusCode), apperr.New("api.fs_manifest_unavailable", "api", 1, fmt.Sprintf("%s: fetch %s returned HTTP %d", errFSManifestUnavailable, manifestURL, res.StatusCode))
return nil, apperr.New("api.fs_manifest_unavailable", "api", 1, fmt.Sprintf("test ti fs region manifest fetch %s returned HTTP %d", manifestURL, res.StatusCode))
}
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 ti fs region manifest", err)
return nil, apperr.Wrap("api.fs_manifest_decode", "api", 1, "decode test ti fs region manifest", err)
}
if err := validateFSManifest(&manifest); err != nil {
return nil, false, err
}
return &manifest, false, nil
}

func retryableFSManifestStatus(statusCode int) bool {
return statusCode == http.StatusTooManyRequests || statusCode >= 500
}

func loadCachedFSManifest(manifestURL string) (*FSRegionManifest, error) {
cachePath, err := fsManifestCachePath(manifestURL)
if err != nil {
return nil, err
}
data, err := os.ReadFile(cachePath)
if err != nil {
return nil, err
}
var cached cachedFSRegionManifest
if err := json.Unmarshal(data, &cached); err != nil {
return nil, err
}
if cached.ManifestURL != manifestURL {
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 ti fs manifest is expired")
}
manifest := cached.Manifest
if err := validateFSManifest(&manifest); err != nil {
return nil, err
}
return &manifest, nil
}

func storeCachedFSManifest(manifestURL string, manifest *FSRegionManifest) {
if manifest == nil {
return
func builtInFSRegionManifest() *FSRegionManifest {
return &FSRegionManifest{
Service: "ti-fs",
Regions: []FSRegionManifestEntry{
{RegionCode: "aws-us-east-1", Mode: DefaultFSMode, ServerURL: "https://aws-us-east-1.drive9.ai", CloudProvider: "aws", TiDBRegion: "us-east-1"},
{RegionCode: "aws-ap-southeast-1", Mode: DefaultFSMode, ServerURL: "https://aws-ap-southeast-1.drive9.ai", CloudProvider: "aws", TiDBRegion: "ap-southeast-1"},
{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"},
},
}
cachePath, err := fsManifestCachePath(manifestURL)
if err != nil {
return
}
if err := os.MkdirAll(filepath.Dir(cachePath), 0o755); err != nil {
return
}
cached := cachedFSRegionManifest{
ManifestURL: manifestURL,
FetchedAt: time.Now().UTC(),
Manifest: *manifest,
}
data, err := json.MarshalIndent(cached, "", " ")
if err != nil {
return
}
_ = os.WriteFile(cachePath, data, 0o644)
}

func fsManifestCachePath(manifestURL string) (string, error) {
home, err := os.UserHomeDir()
if err != nil || strings.TrimSpace(home) == "" {
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, ".ti", "cache", name), nil
}

func validateFSManifest(manifest *FSRegionManifest) error {
Expand Down Expand Up @@ -428,11 +332,7 @@ func fsManifestEntryMatches(entry FSRegionManifestEntry, provider, apiProvider,
}

func fsRegionCode(provider, regionCode string) string {
prefix := APIProvider(provider)
if provider == region.ProviderAlibabaCloud {
prefix = "ali"
}
return prefix + "-" + regionCode
return APIProvider(provider) + "-" + regionCode
}

func supportedFSRegions(entries []FSRegionManifestEntry, mode string) string {
Expand All @@ -442,15 +342,7 @@ func supportedFSRegions(entries []FSRegionManifestEntry, mode string) string {
if strings.TrimSpace(entry.Mode) != mode {
continue
}
provider := entry.CloudProvider
if provider == "" {
provider = strings.SplitN(entry.RegionCode, "-", 2)[0]
}
regionCode := entry.TiDBRegion
if regionCode == "" {
regionCode = strings.TrimPrefix(entry.RegionCode, provider+"-")
}
value := provider + "/" + regionCode
value := entry.RegionCode
if _, ok := seen[value]; ok {
continue
}
Expand Down
Loading