Skip to content
Open
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
31 changes: 31 additions & 0 deletions README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,17 +178,48 @@ velero install \

#### Backup Storage Location Configuration Parameters

#### Backup Storage Location Configuration Parameters

| Parameter | Type | Description | Example |
|:-----|:-----|:-----|:-----|
| `region` | Required | The region where the OSS bucket is located | `cn-hangzhou` |
| `network` | Optional | Network type. Options: `internal` (internal network), `accelerate` (accelerate domain). Default is public network | `internal` |
| `endpoint` | Optional | Custom OSS endpoint | `https://oss-custom.example.com` |
| `accessKeyId` | Optional | Alibaba Cloud Access Key ID. Alternative to `spec.credential`; takes priority over env vars when combined with `accessKeySecret` | `LTAI5t...` |
| `accessKeySecret` | Optional | Alibaba Cloud Access Key Secret. Required when `accessKeyId` is set | `xxxxx` |
| `stsToken` | Optional | STS security token (only needed when using temporary credentials) | `xxxxx` |

#### Volume Snapshot Location Configuration Parameters

| Parameter | Type | Description | Example |
|:-----|:-----|:-----|:-----|
| `region` | Required | The region where ECS snapshots are located | `cn-hangzhou` |
| `accessKeyId` | Optional | Alibaba Cloud Access Key ID. Alternative to `spec.credential`; takes priority over env vars when combined with `accessKeySecret` | `LTAI5t...` |
| `accessKeySecret` | Optional | Alibaba Cloud Access Key Secret. Required when `accessKeyId` is set | `xxxxx` |
| `stsToken` | Optional | STS security token (only needed when using temporary credentials) | `xxxxx` |

> **Credential patterns:**
>
> **Pattern 1 — Shared credential (recommended for most cases):**
> A single Kubernetes Secret is used for both BSL and VSL. Pass it to Velero via
> `--secret-file` at install time, or reference it with `spec.credential` on the BSL/VSL.
> Velero mounts the secret and injects `credentialsFile` into the plugin config automatically.
>
> **Pattern 2 — Per-location credentials via Kubernetes Secret (recommended when BSL and VSL need different credentials):**
> Set `spec.credential` on each BSL/VSL object to reference a separate Kubernetes Secret.
> Velero v1.10+ mounts each secret independently and injects `credentialsFile` per location.
> This is the same approach used by the AWS plugin.
>
> **Credential resolution priority order:**
> 1. `credentialsFile` config key (set automatically by Velero when `spec.credential` is used) — highest priority
> 2. `ALIBABA_CLOUD_CREDENTIALS_FILE` environment variable
> 3. `ALIBABA_CLOUD_ACCESS_KEY_ID` / `ALIBABA_CLOUD_ACCESS_KEY_SECRET` environment variables
> 4. `ALIBABA_CLOUD_RAM_ROLE` environment variable (custom RAM role)
> 5. ECS instance RAM role (ACK environments only)
>
> The `accessKeyId`/`accessKeySecret` config keys in the tables above are an alternative
> when neither a Kubernetes Secret nor environment variables are available. They store
> credentials directly in the BSL/VSL config object.

#### Other common Optional Parameters

Expand Down
89 changes: 60 additions & 29 deletions velero-plugin-alibabacloud/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ const (
notOnECSConfigKey = "notOnECS"
credFileConfigKey = "credentialsFile"

// Optional config keys for per-BSL/VSL credentials.
// Velero v1.10+ supports spec.credential on BSL/VSL objects, which references a
// Kubernetes Secret. Velero mounts the secret and injects credentialsFile into the
// plugin config. These keys are the alternative path for passing credentials directly
// via the BSL/VSL config map (lower precedence than credentialsFile/env vars from
// a mounted secret). When both accessKeyId and accessKeySecret are present, they
// take highest priority over all other credential sources for that location.
accessKeyIDConfigKey = "accessKeyId"
accessKeySecretConfigKey = "accessKeySecret"
stsTokenConfigKey = "stsToken"

networkTypeAccelerate = "accelerate"
networkTypeInternal = "internal"

Expand All @@ -45,6 +56,9 @@ var validConfigKeys = []string{
endpointConfigKey,
notOnECSConfigKey,
credFileConfigKey,
accessKeyIDConfigKey,
accessKeySecretConfigKey,
stsTokenConfigKey,
}

// loadCredentialFileFromEnv loads environment variables from a credentials file.
Expand Down Expand Up @@ -174,59 +188,76 @@ func veleroForAck(config map[string]string) bool {
}

// getCredentials retrieves OSS credentials based on the environment and configuration.
// It supports multiple authentication methods with the following priority order:
// It supports two usage patterns:
//
// Pattern 1 — Shared credential (existing approach, recommended for most cases):
// - A single Kubernetes Secret is referenced by the Velero installation (e.g. --secret-file).
// - Both BSL and VSL use the same credential source.
// - Credential resolution order: credentialsFile config key → ALIBABA_CLOUD_CREDENTIALS_FILE
// env var → ALIBABA_CLOUD_ACCESS_KEY_ID/SECRET env vars → ALIBABA_CLOUD_RAM_ROLE env var
// → ECS instance RAM role (ACK only).
//
// Pattern 2 — Per-location credential via Kubernetes Secret (new approach):
// - Each BSL/VSL has its own spec.credential field referencing a separate Kubernetes Secret.
// - Velero v1.10+ mounts the secret and injects credentialsFile=/tmp/... into plugin config.
// - The plugin reads the file via loadCredentialFileFromEnv (step 2 below).
// - This allows BSL and VSL to use different credentials (e.g. different RAM users).
//
// Full credential resolution priority order within this function:
//
// 1. AccessKey credentials (highest priority):
// - Load from file (if ALIBABA_CLOUD_CREDENTIALS_FILE is set) and/or environment variables
// - Environment variables: ALIBABA_CLOUD_ACCESS_KEY_ID, ALIBABA_CLOUD_ACCESS_KEY_SECRET
// - Optional: ALIBABA_CLOUD_ACCESS_STS_TOKEN
// - If both AccessKey ID and Secret are provided, they take precedence over RAM role
// 1. Per-location config keys (accessKeyId + accessKeySecret in BSL/VSL config map):
// - When both are present, they are used directly as the highest-priority fallback.
// - Note: the recommended per-location approach is Pattern 2 above (spec.credential).
// These config keys are an alternative when a Kubernetes Secret is not available.
//
// 2. Custom RAM Role (via environment variable):
// - Environment variable: ALIBABA_CLOUD_RAM_ROLE
// - Allows specifying a custom RAM role name instead of using the ECS instance's default role
// - Works in both ACK and non-ACK environments
// - The function will use this role to obtain STS credentials via getSTSAK()
// 2. Credentials file (Pattern 2 — spec.credential path lands here):
// - Config key: credentialsFile (takes precedence over ALIBABA_CLOUD_CREDENTIALS_FILE env var)
// - File format: dotenv key=value pairs (ALIBABA_CLOUD_ACCESS_KEY_ID, etc.)
//
// 3. ECS Instance RAM Role (ACK environment fallback):
// - For ACK environments: automatically detect the RAM role from ECS metadata
// - Only used if no AccessKey credentials and no custom RAM role are provided
// - Requires the ECS instance to have a RAM role attached
// 3. AccessKey credentials from environment variables:
// - ALIBABA_CLOUD_ACCESS_KEY_ID, ALIBABA_CLOUD_ACCESS_KEY_SECRET (required)
// - ALIBABA_CLOUD_ACCESS_STS_TOKEN (optional)
//
// 4. Error (non-ACK environment without credentials):
// - For non-ACK environments: returns error if no AccessKey and no custom RAM role are provided
// 4. Custom RAM Role (via environment variable):
// - ALIBABA_CLOUD_RAM_ROLE — custom RAM role name
//
// Parameters:
// - config: configuration map that may contain:
// - "credentialsFile": path to credentials file (takes precedence over ALIBABA_CLOUD_CREDENTIALS_FILE env var)
// - "notOnECS": if set to "true", indicates not running on ECS (affects RAM role detection)
// 5. ECS Instance RAM Role (ACK environment fallback):
// - Automatically detected from ECS metadata service
//
// Returns:
// - ossCredentials: contains accessKeyID, accessKeySecret, stsToken, and ramRole
// - error: if credentials cannot be obtained
// 6. Error (non-ACK environment without any credentials)
func getCredentials(config map[string]string) (*ossCredentials, error) {
cred := &ossCredentials{}

// Step 1: Load credentials from file if specified (this may set env vars)
// Step 1: Per-location config keys (highest priority).
// If accessKeyId and accessKeySecret are both present in the BSL/VSL config map,
// use them directly — credentialsFile and env vars are not consulted for this location.
if config != nil && config[accessKeyIDConfigKey] != "" && config[accessKeySecretConfigKey] != "" {
cred.accessKeyID = config[accessKeyIDConfigKey]
cred.accessKeySecret = config[accessKeySecretConfigKey]
cred.stsToken = config[stsTokenConfigKey] // optional
return cred, nil
}

// Step 2: Load credentials from file if specified (this may set env vars)
if err := loadCredentialFileFromEnv(config); err != nil {
return nil, err
}

// Step 2: Get credentials from environment variables
// Step 3: Get credentials from environment variables
// These may be set by loadCredentialFileFromEnv or directly by the user
cred.accessKeyID = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
cred.accessKeySecret = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
cred.stsToken = os.Getenv("ALIBABA_CLOUD_ACCESS_STS_TOKEN") // Token may be empty
cred.ramRole = os.Getenv("ALIBABA_CLOUD_RAM_ROLE") // Custom RAM role name

// Step 3: If we have both accessKeyID and accessKeySecret, use them directly
// Step 4: If we have both accessKeyID and accessKeySecret, use them directly
// AccessKey credentials take precedence over RAM role
if len(cred.accessKeyID) != 0 && len(cred.accessKeySecret) != 0 {
cred.ramRole = ""
return cred, nil
}

// Step 4: Handle RAM role authentication
// Step 5: Handle RAM role authentication
// If no AccessKey credentials are available, try to use RAM role
if !veleroForAck(config) && cred.ramRole == "" {
// For non-ACK environment: if no AccessKey and no custom RAM role, return error
Expand All @@ -244,7 +275,7 @@ func getCredentials(config map[string]string) (*ossCredentials, error) {
cred.ramRole = ramRole
}

// Step 5: Get STS credentials from the RAM role
// Step 6: Get STS credentials from the RAM role
var err error
cred.accessKeyID, cred.accessKeySecret, cred.stsToken, err = getSTSAK(cred.ramRole)
if err != nil {
Expand Down
79 changes: 78 additions & 1 deletion velero-plugin-alibabacloud/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ ALIBABA_CLOUD_ACCESS_STS_TOKEN=config-file-token
expectedError: "Failed to get sts token from ram role CustomVeleroRole",
},
{
name: "success: custom RAM role takes precedence over AccessKey",
name: "success: AccessKey takes precedence over custom RAM role",
config: nil,
setupEnv: func(t *testing.T) map[string]string {
t.Setenv("ALIBABA_CLOUD_CREDENTIALS_FILE", "")
Expand Down Expand Up @@ -289,6 +289,9 @@ ALIBABA_CLOUD_ACCESS_STS_TOKEN=config-file-token
require.NoError(t, err)

t.Setenv("ALIBABA_CLOUD_CREDENTIALS_FILE", credFile)
t.Setenv("ALIBABA_CLOUD_ACCESS_KEY_ID", "")
t.Setenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET", "")
t.Setenv("ALIBABA_CLOUD_ACCESS_STS_TOKEN", "")
// Cleanup after test
t.Cleanup(func() {
os.RemoveAll(tmpDir)
Expand All @@ -299,6 +302,80 @@ ALIBABA_CLOUD_ACCESS_STS_TOKEN=config-file-token
// but it verifies that the custom RAM role from file is used
expectedError: "Failed to get sts token from ram role FileCustomRole",
},
{
name: "success: per-location accessKeyId+accessKeySecret in config (no env vars needed)",
config: map[string]string{
"notOnECS": "true",
accessKeyIDConfigKey: "inline-ak",
accessKeySecretConfigKey: "inline-sk",
},
setupEnv: func(t *testing.T) map[string]string {
t.Setenv("ALIBABA_CLOUD_ACCESS_KEY_ID", "")
t.Setenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET", "")
t.Setenv("ALIBABA_CLOUD_ACCESS_STS_TOKEN", "")
return nil
},
validateCred: func(t *testing.T, cred *ossCredentials) {
assert.Equal(t, "inline-ak", cred.accessKeyID)
assert.Equal(t, "inline-sk", cred.accessKeySecret)
assert.Empty(t, cred.stsToken)
assert.Empty(t, cred.ramRole)
},
},
{
name: "success: per-location credentials with optional stsToken",
config: map[string]string{
"notOnECS": "true",
accessKeyIDConfigKey: "inline-ak",
accessKeySecretConfigKey: "inline-sk",
stsTokenConfigKey: "inline-sts",
},
setupEnv: func(t *testing.T) map[string]string {
t.Setenv("ALIBABA_CLOUD_ACCESS_KEY_ID", "")
t.Setenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET", "")
t.Setenv("ALIBABA_CLOUD_ACCESS_STS_TOKEN", "")
return nil
},
validateCred: func(t *testing.T, cred *ossCredentials) {
assert.Equal(t, "inline-ak", cred.accessKeyID)
assert.Equal(t, "inline-sk", cred.accessKeySecret)
assert.Equal(t, "inline-sts", cred.stsToken)
assert.Empty(t, cred.ramRole)
},
},
{
name: "success: per-location credentials take priority over env vars",
config: map[string]string{
accessKeyIDConfigKey: "inline-ak",
accessKeySecretConfigKey: "inline-sk",
},
setupEnv: func(t *testing.T) map[string]string {
t.Setenv("ALIBABA_CLOUD_ACCESS_KEY_ID", "env-ak")
t.Setenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET", "env-sk")
t.Setenv("ALIBABA_CLOUD_ACCESS_STS_TOKEN", "")
return nil
},
validateCred: func(t *testing.T, cred *ossCredentials) {
assert.Equal(t, "inline-ak", cred.accessKeyID, "per-location config must win over env vars")
assert.Equal(t, "inline-sk", cred.accessKeySecret)
assert.Empty(t, cred.ramRole)
},
},
{
name: "fallback: only accessKeyId without accessKeySecret falls through to env vars",
config: map[string]string{
"notOnECS": "true",
accessKeyIDConfigKey: "inline-ak-only",
},
setupEnv: func(t *testing.T) map[string]string {
t.Setenv("ALIBABA_CLOUD_ACCESS_KEY_ID", "")
t.Setenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET", "")
t.Setenv("ALIBABA_CLOUD_ACCESS_STS_TOKEN", "")
t.Setenv("ALIBABA_CLOUD_RAM_ROLE", "")
return nil
},
expectedError: "ALIBABA_CLOUD_ACCESS_KEY_ID or ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variable is not set",
},
}

for _, tc := range tests {
Expand Down
Loading