Skip to content

feat: add environment variables to canister settings #653

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
14 changes: 13 additions & 1 deletion ic-utils/src/interfaces/management_canister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,16 @@ pub enum LogVisibility {
AllowedViewers(Vec<Principal>),
}

/// A generic environment variable struct defining a key-value pair, meant to be used for defining a canister environment
#[derive(Debug, Clone, CandidType, Deserialize, PartialEq)]
pub struct EnvironmentVariable {
/// Variable key/name
pub key: String,

/// Variable value
pub value: String,
}

/// The concrete settings of a canister.
#[derive(Clone, Debug, Deserialize, CandidType)]
pub struct DefiniteCanisterSettings {
Expand All @@ -197,6 +207,8 @@ pub struct DefiniteCanisterSettings {
pub wasm_memory_threshold: Option<Nat>,
/// The canister log visibility. Defines which principals are allowed to fetch logs.
pub log_visibility: LogVisibility,
/// A set of dynamically-configurable environment variables for a canister
pub environment_variables: Vec<EnvironmentVariable>,
}

impl std::fmt::Display for StatusCallResult {
Expand Down Expand Up @@ -686,7 +698,7 @@ impl<'agent> ManagementCanister<'agent> {
}

/// Creates a canister snapshot, optionally replacing an existing snapshot.
///
///
/// <div class="warning">Canisters should be stopped before running this method!</div>
pub fn take_canister_snapshot(
&self,
Expand Down
71 changes: 71 additions & 0 deletions ic-utils/src/interfaces/management_canister/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub use super::attributes::{
};
use super::{ChunkHash, LogVisibility, ManagementCanister};
use crate::call::CallFuture;
use crate::interfaces::management_canister::EnvironmentVariable;
use crate::{
call::AsyncCall, canister::Argument, interfaces::management_canister::MgmtMethod, Canister,
};
Expand Down Expand Up @@ -89,6 +90,9 @@ pub struct CanisterSettings {
///
/// If unspecified and a canister is being created with these settings, defaults to `Controllers`, i.e. private by default.
pub log_visibility: Option<LogVisibility>,

/// A set of dynamically-configurable environment variables for a canister
pub environment_variables: Option<Vec<EnvironmentVariable>>,
}

/// A builder for a `create_canister` call.
Expand All @@ -104,6 +108,7 @@ pub struct CreateCanisterBuilder<'agent, 'canister: 'agent> {
wasm_memory_limit: Option<Result<WasmMemoryLimit, AgentError>>,
wasm_memory_threshold: Option<Result<WasmMemoryLimit, AgentError>>,
log_visibility: Option<Result<LogVisibility, AgentError>>,
environment_variables: Option<Result<Vec<EnvironmentVariable>, AgentError>>,
is_provisional_create: bool,
amount: Option<u128>,
specified_id: Option<Principal>,
Expand All @@ -123,6 +128,7 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> {
wasm_memory_limit: None,
wasm_memory_threshold: None,
log_visibility: None,
environment_variables: None,
is_provisional_create: false,
amount: None,
specified_id: None,
Expand Down Expand Up @@ -389,6 +395,31 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> {
}
}

/// Pass in a set of environment variables for the canister.
pub fn with_environment_variables<C, E>(self, environment_variables: C) -> Self
where
E: std::fmt::Display,
C: TryInto<Vec<EnvironmentVariable>, Error = E>,
{
self.with_optional_environment_variables(Some(environment_variables))
}

/// Pass in a set of optional environment variables for the canister. If this is [`None`],
/// it will revert the environment variables to default.
pub fn with_optional_environment_variables<E, C>(self, environment_variables: Option<C>) -> Self
where
E: std::fmt::Display,
C: TryInto<Vec<EnvironmentVariable>, Error = E>,
{
Self {
environment_variables: environment_variables.map(|vs| {
vs.try_into()
.map_err(|e| AgentError::MessageError(format!("{e}")))
}),
..self
}
}

/// Create an [`AsyncCall`] implementation that, when called, will create a
/// canister.
pub fn build(self) -> Result<impl 'agent + AsyncCall<Value = (Principal,)>, AgentError> {
Expand Down Expand Up @@ -432,6 +463,11 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> {
Some(Ok(x)) => Some(x),
None => None,
};
let environment_variables = match self.environment_variables {
Some(Err(x)) => return Err(AgentError::MessageError(format!("{x}"))),
Some(Ok(x)) => Some(x),
None => None,
};

#[derive(Deserialize, CandidType)]
struct Out {
Expand All @@ -456,6 +492,7 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> {
wasm_memory_limit,
wasm_memory_threshold,
log_visibility,
environment_variables,
},
specified_id: self.specified_id,
};
Expand All @@ -475,6 +512,7 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> {
wasm_memory_limit,
wasm_memory_threshold,
log_visibility,
environment_variables,
})
.with_effective_canister_id(self.effective_canister_id)
};
Expand Down Expand Up @@ -1004,6 +1042,7 @@ pub struct UpdateCanisterBuilder<'agent, 'canister: 'agent> {
wasm_memory_limit: Option<Result<WasmMemoryLimit, AgentError>>,
wasm_memory_threshold: Option<Result<WasmMemoryLimit, AgentError>>,
log_visibility: Option<Result<LogVisibility, AgentError>>,
environment_variables: Option<Result<Vec<EnvironmentVariable>, AgentError>>,
}

impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> {
Expand All @@ -1020,6 +1059,7 @@ impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> {
wasm_memory_limit: None,
wasm_memory_threshold: None,
log_visibility: None,
environment_variables: None,
}
}

Expand Down Expand Up @@ -1240,6 +1280,31 @@ impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> {
}
}

/// Pass in a set of environment variables for the canister.
pub fn with_environment_variables<C, E>(self, environment_variables: C) -> Self
where
E: std::fmt::Display,
C: TryInto<Vec<EnvironmentVariable>, Error = E>,
{
self.with_optional_environment_variables(Some(environment_variables))
}

/// Pass in a set of optional environment variables for the canister. If this is [`None`],
/// it will revert the environment variables to default.
pub fn with_optional_environment_variables<E, C>(self, environment_variables: Option<C>) -> Self
where
E: std::fmt::Display,
C: TryInto<Vec<EnvironmentVariable>, Error = E>,
{
Self {
environment_variables: environment_variables.map(|vs| {
vs.try_into()
.map_err(|e| AgentError::MessageError(format!("{e}")))
}),
..self
}
}

/// Create an [`AsyncCall`] implementation that, when called, will update a
/// canisters settings.
pub fn build(self) -> Result<impl 'agent + AsyncCall<Value = ()>, AgentError> {
Expand Down Expand Up @@ -1289,6 +1354,11 @@ impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> {
Some(Ok(x)) => Some(x),
None => None,
};
let environment_variables = match self.environment_variables {
Some(Err(x)) => return Err(AgentError::MessageError(format!("{x}"))),
Some(Ok(x)) => Some(x),
None => None,
};

Ok(self
.canister
Expand All @@ -1304,6 +1374,7 @@ impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> {
wasm_memory_limit,
wasm_memory_threshold,
log_visibility,
environment_variables,
},
})
.with_effective_canister_id(self.canister_id)
Expand Down
4 changes: 4 additions & 0 deletions ic-utils/src/interfaces/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ impl<'agent> WalletCanister<'agent> {
wasm_memory_limit: None,
wasm_memory_threshold: None,
log_visibility: None,
environment_variables: None,
};

self.update("wallet_create_canister")
Expand Down Expand Up @@ -709,6 +710,7 @@ impl<'agent> WalletCanister<'agent> {
wasm_memory_limit: None,
wasm_memory_threshold: None,
log_visibility: None,
environment_variables: None,
};

self.update("wallet_create_canister128")
Expand Down Expand Up @@ -839,6 +841,7 @@ impl<'agent> WalletCanister<'agent> {
wasm_memory_limit: None,
wasm_memory_threshold: None,
log_visibility: None,
environment_variables: None,
};

self.update("wallet_create_wallet")
Expand Down Expand Up @@ -871,6 +874,7 @@ impl<'agent> WalletCanister<'agent> {
wasm_memory_limit: None,
wasm_memory_threshold: None,
log_visibility: None,
environment_variables: None,
};

self.update("wallet_create_wallet128")
Expand Down
128 changes: 127 additions & 1 deletion ref-tests/tests/ic-ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,7 @@ mod management_canister {
wasm_memory_limit: None,
wasm_memory_threshold: None,
log_visibility: None,
environment_variables: None,
},
};

Expand Down Expand Up @@ -1033,6 +1034,7 @@ mod simple_calls {
}

mod extras {

use candid::Nat;
use ic_agent::{
agent::{RejectCode, RejectResponse},
Expand All @@ -1042,7 +1044,9 @@ mod extras {
use ic_utils::{
call::AsyncCall,
interfaces::{
management_canister::{builders::ComputeAllocation, LogVisibility},
management_canister::{
builders::ComputeAllocation, EnvironmentVariable, LogVisibility,
},
ManagementCanister,
},
};
Expand Down Expand Up @@ -1412,4 +1416,126 @@ mod extras {
Ok(())
})
}

#[ignore]
#[test]
fn create_with_environment_variables() {
with_agent(|agent| async move {
let ic00 = ManagementCanister::create(&agent);

let (canister_id,) = ic00
.create_canister()
.as_provisional_create_with_amount(None)
.with_effective_canister_id(get_effective_canister_id().await)
.with_environment_variables(vec![EnvironmentVariable {
key: "key-1".to_string(),
value: "value-1".to_string(),
}])
.call_and_wait()
.await
.unwrap();

let result = ic00.canister_status(&canister_id).call_and_wait().await?;

// Assert environment variables
assert_eq!(
result.0.settings.environment_variables,
vec![EnvironmentVariable {
key: "key-1".to_string(),
value: "value-1".to_string(),
}],
);

Ok(())
})
}

#[ignore]
#[test]
fn update_environment_variables() {
with_agent(|agent| async move {
let ic00 = ManagementCanister::create(&agent);

// Create with Controllers.
let (canister_id,) = ic00
.create_canister()
.as_provisional_create_with_amount(Some(20_000_000_000_000_u128))
.with_effective_canister_id(get_effective_canister_id().await)
.with_environment_variables(vec![EnvironmentVariable {
key: "key-1".to_string(),
value: "value-1".to_string(),
}])
.call_and_wait()
.await?;

let result = ic00.canister_status(&canister_id).call_and_wait().await?;

// Assert environment variables
assert_eq!(
result.0.settings.environment_variables,
vec![EnvironmentVariable {
key: "key-1".to_string(),
value: "value-1".to_string(),
}],
);

// Add an environment variable.
ic00.update_settings(&canister_id)
.with_environment_variables(vec![
EnvironmentVariable {
key: "key-1".to_string(),
value: "value-1".to_string(),
},
EnvironmentVariable {
key: "key-2".to_string(),
value: "value-2".to_string(),
},
])
.call_and_wait()
.await?;

let result = ic00.canister_status(&canister_id).call_and_wait().await?;

// Assert environment variables
assert_eq!(
result.0.settings.environment_variables,
vec![
EnvironmentVariable {
key: "key-1".to_string(),
value: "value-1".to_string(),
},
EnvironmentVariable {
key: "key-2".to_string(),
value: "value-2".to_string(),
}
],
);

// Update with no change.
let no_change: Option<Vec<EnvironmentVariable>> = None;
ic00.update_settings(&canister_id)
.with_optional_environment_variables(no_change)
.call_and_wait()
.await?;

let result = ic00.canister_status(&canister_id).call_and_wait().await?;

// Assert environment variables
assert_eq!(
result.0.settings.environment_variables,
vec![
EnvironmentVariable {
key: "key-1".to_string(),
value: "value-1".to_string(),
},
EnvironmentVariable {
key: "key-2".to_string(),
value: "value-2".to_string(),
}
],
);

Ok(())
})
}
}
1 change: 1 addition & 0 deletions ref-tests/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ fn wallet_create_wallet() {
wasm_memory_limit: None,
wasm_memory_threshold: None,
log_visibility: None,
environment_variables: None,
},
};
let args = Argument::from_candid((create_args,));
Expand Down
Loading