Skip to content

Commit 08eac60

Browse files
authored
Merge pull request #2566 from devigned/az-cosmos-wid
Add support for workload identity in the Azure CosmosDB Key/Value impl
2 parents 5d028fb + 6656e68 commit 08eac60

File tree

7 files changed

+185
-54
lines changed

7 files changed

+185
-54
lines changed

Cargo.lock

Lines changed: 38 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/key-value-azure/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ edition = { workspace = true }
66

77
[dependencies]
88
anyhow = "1"
9-
azure_data_cosmos = "0.11.0"
9+
azure_data_cosmos = { git = "https://github.com/azure/azure-sdk-for-rust.git", rev = "8c4caa251c3903d5eae848b41bb1d02a4d65231c" }
10+
azure_identity = { git = "https://github.com/azure/azure-sdk-for-rust.git", rev = "8c4caa251c3903d5eae848b41bb1d02a4d65231c" }
1011
futures = "0.3.28"
1112
serde = { version = "1.0", features = ["derive"] }
1213
spin-key-value = { path = "../key-value" }

crates/key-value-azure/src/lib.rs

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,73 @@ pub struct KeyValueAzureCosmos {
1515
client: CollectionClient,
1616
}
1717

18+
/// Azure Cosmos Key / Value runtime config literal options for authentication
19+
#[derive(Clone, Debug)]
20+
pub struct KeyValueAzureCosmosRuntimeConfigOptions {
21+
key: String,
22+
}
23+
24+
impl KeyValueAzureCosmosRuntimeConfigOptions {
25+
pub fn new(key: String) -> Self {
26+
Self { key }
27+
}
28+
}
29+
30+
/// Azure Cosmos Key / Value enumeration for the possible authentication options
31+
#[derive(Clone, Debug)]
32+
pub enum KeyValueAzureCosmosAuthOptions {
33+
/// Runtime Config values indicates the account and key have been specified directly
34+
RuntimeConfigValues(KeyValueAzureCosmosRuntimeConfigOptions),
35+
/// Environmental indicates that the environment variables of the process should be used to
36+
/// create the TokenCredential for the Cosmos client. This will use the Azure Rust SDK's
37+
/// DefaultCredentialChain to derive the TokenCredential based on what environment variables
38+
/// have been set.
39+
///
40+
/// Service Principal with client secret:
41+
/// - `AZURE_TENANT_ID`: ID of the service principal's Azure tenant.
42+
/// - `AZURE_CLIENT_ID`: the service principal's client ID.
43+
/// - `AZURE_CLIENT_SECRET`: one of the service principal's secrets.
44+
///
45+
/// Service Principal with certificate:
46+
/// - `AZURE_TENANT_ID`: ID of the service principal's Azure tenant.
47+
/// - `AZURE_CLIENT_ID`: the service principal's client ID.
48+
/// - `AZURE_CLIENT_CERTIFICATE_PATH`: path to a PEM or PKCS12 certificate file including the private key.
49+
/// - `AZURE_CLIENT_CERTIFICATE_PASSWORD`: (optional) password for the certificate file.
50+
///
51+
/// Workload Identity (Kubernetes, injected by the Workload Identity mutating webhook):
52+
/// - `AZURE_TENANT_ID`: ID of the service principal's Azure tenant.
53+
/// - `AZURE_CLIENT_ID`: the service principal's client ID.
54+
/// - `AZURE_FEDERATED_TOKEN_FILE`: TokenFilePath is the path of a file containing a Kubernetes service account token.
55+
///
56+
/// Managed Identity (User Assigned or System Assigned identities):
57+
/// - `AZURE_CLIENT_ID`: (optional) if using a user assigned identity, this will be the client ID of the identity.
58+
///
59+
/// Azure CLI:
60+
/// - `AZURE_TENANT_ID`: (optional) use a specific tenant via the Azure CLI.
61+
///
62+
/// Common across each:
63+
/// - `AZURE_AUTHORITY_HOST`: (optional) the host for the identity provider. For example, for Azure public cloud the host defaults to "https://login.microsoftonline.com".
64+
/// See also: https://github.com/Azure/azure-sdk-for-rust/blob/main/sdk/identity/README.md
65+
Environmental,
66+
}
67+
1868
impl KeyValueAzureCosmos {
19-
pub fn new(key: String, account: String, database: String, container: String) -> Result<Self> {
20-
let token = AuthorizationToken::primary_from_base64(&key).map_err(log_error)?;
69+
pub fn new(
70+
account: String,
71+
database: String,
72+
container: String,
73+
auth_options: KeyValueAzureCosmosAuthOptions,
74+
) -> Result<Self> {
75+
let token = match auth_options {
76+
KeyValueAzureCosmosAuthOptions::RuntimeConfigValues(config) => {
77+
AuthorizationToken::primary_key(config.key).map_err(log_error)?
78+
}
79+
KeyValueAzureCosmosAuthOptions::Environmental => {
80+
AuthorizationToken::from_token_credential(
81+
azure_identity::create_default_credential()?,
82+
)
83+
}
84+
};
2185
let cosmos_client = CosmosClient::new(account, token);
2286
let database_client = cosmos_client.database_client(database);
2387
let client = database_client.collection_client(container);
@@ -47,13 +111,20 @@ struct AzureCosmosStore {
47111

48112
#[async_trait]
49113
impl Store for AzureCosmosStore {
50-
#[instrument(name = "spin_key_value_azure.get", skip(self), err(level = Level::INFO), fields(otel.kind = "client"))]
114+
#[instrument(name = "spin_key_value_azure.get", skip(self), err(level = Level::INFO), fields(
115+
otel.kind = "client"
116+
))]
51117
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, Error> {
52118
let pair = self.get_pair(key).await?;
53119
Ok(pair.map(|p| p.value))
54120
}
55121

56-
#[instrument(name = "spin_key_value_azure.set", skip(self, value), err(level = Level::INFO), fields(otel.kind = "client"))]
122+
#[instrument(
123+
name = "spin_key_value_azure.set",
124+
skip(self, value),
125+
err(level = Level::INFO),
126+
fields(otel.kind = "client")
127+
)]
57128
async fn set(&self, key: &str, value: &[u8]) -> Result<(), Error> {
58129
let pair = Pair {
59130
id: key.to_string(),
@@ -67,7 +138,9 @@ impl Store for AzureCosmosStore {
67138
Ok(())
68139
}
69140

70-
#[instrument(name = "spin_key_value_azure.delete", skip(self), err(level = Level::INFO), fields(otel.kind = "client"))]
141+
#[instrument(name = "spin_key_value_azure.delete", skip(self), err(level = Level::INFO), fields(
142+
otel.kind = "client"
143+
))]
71144
async fn delete(&self, key: &str) -> Result<(), Error> {
72145
if self.exists(key).await? {
73146
let document_client = self.client.document_client(key, &key).map_err(log_error)?;
@@ -76,12 +149,19 @@ impl Store for AzureCosmosStore {
76149
Ok(())
77150
}
78151

79-
#[instrument(name = "spin_key_value_azure.exists", skip(self), err(level = Level::INFO), fields(otel.kind = "client"))]
152+
#[instrument(name = "spin_key_value_azure.exists", skip(self), err(level = Level::INFO), fields(
153+
otel.kind = "client"
154+
))]
80155
async fn exists(&self, key: &str) -> Result<bool, Error> {
81156
Ok(self.get_pair(key).await?.is_some())
82157
}
83158

84-
#[instrument(name = "spin_key_value_azure.get_keys", skip(self), err(level = Level::INFO), fields(otel.kind = "client"))]
159+
#[instrument(
160+
name = "spin_key_value_azure.get_keys",
161+
skip(self),
162+
err(level = Level::INFO),
163+
fields(otel.kind = "client")
164+
)]
85165
async fn get_keys(&self) -> Result<Vec<String>, Error> {
86166
self.get_keys().await
87167
}

crates/oci/src/client.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,15 +1141,13 @@ mod test {
11411141
.expect("should have version annotation")
11421142
);
11431143
assert!(
1144-
annotations
1145-
.get(oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_DESCRIPTION)
1146-
.is_none(),
1144+
!annotations
1145+
.contains_key(oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_DESCRIPTION),
11471146
"empty description should not have generated annotation"
11481147
);
11491148
assert!(
11501149
annotations
1151-
.get(oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_CREATED)
1152-
.is_some(),
1150+
.contains_key(oci_distribution::annotations::ORG_OPENCONTAINERS_IMAGE_CREATED),
11531151
"creation annotation should have been generated"
11541152
);
11551153
}

crates/trigger/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ impl<Executor: TriggerExecutor> TriggerAppEngine<Executor> {
442442
&self,
443443
component_id: &str,
444444
) -> Option<HashMap<Authority, ParsedClientTlsOpts>> {
445-
self.client_tls_opts.get(&component_id.to_string()).cloned()
445+
self.client_tls_opts.get(component_id).cloned()
446446
}
447447

448448
pub fn resolve_template(

crates/trigger/src/runtime_config/key_value.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use spin_key_value::{
88
CachingStoreManager, DelegatingStoreManager, KeyValueComponent, StoreManager,
99
KEY_VALUE_STORES_KEY,
1010
};
11-
use spin_key_value_azure::KeyValueAzureCosmos;
11+
use spin_key_value_azure::{
12+
KeyValueAzureCosmos, KeyValueAzureCosmosAuthOptions, KeyValueAzureCosmosRuntimeConfigOptions,
13+
};
1214
use spin_key_value_sqlite::{DatabaseLocation, KeyValueSqlite};
1315

1416
use super::{resolve_config_path, RuntimeConfigOpts};
@@ -122,19 +124,30 @@ impl RedisKeyValueStoreOpts {
122124

123125
#[derive(Clone, Debug, Deserialize)]
124126
pub struct AzureCosmosConfig {
125-
key: String,
127+
key: Option<String>,
126128
account: String,
127129
database: String,
128130
container: String,
129131
}
130132

131133
impl AzureCosmosConfig {
132134
pub fn build_store(&self) -> Result<Arc<dyn StoreManager>> {
135+
let auth_options = match self.key.clone() {
136+
Some(key) => {
137+
tracing::debug!("Azure key value is using key auth.");
138+
let config_values = KeyValueAzureCosmosRuntimeConfigOptions::new(key);
139+
KeyValueAzureCosmosAuthOptions::RuntimeConfigValues(config_values)
140+
}
141+
None => {
142+
tracing::debug!("Azure key value is using environmental auth.");
143+
KeyValueAzureCosmosAuthOptions::Environmental
144+
}
145+
};
133146
let kv_azure_cosmos = KeyValueAzureCosmos::new(
134-
self.key.clone(),
135147
self.account.clone(),
136148
self.database.clone(),
137149
self.container.clone(),
150+
auth_options,
138151
)?;
139152
Ok(Arc::new(kv_azure_cosmos))
140153
}

0 commit comments

Comments
 (0)