Skip to content

Commit 733ae64

Browse files
raymondkclaude
andcommitted
feat: list controllers one per line, and name the viewer per setting
Applies the per-line rendering to the controller list too, so the two reports list principals the same way throughout: Controllers: controller: 2vxsx-fae ... Log visibility: Allowed viewers log viewer: aaaaa-aa Status visibility: Allowed viewers status viewer: 2vxsx-fae `format_principal_list` holds the shared shape — sorted, one entry per line, nested two spaces past the label, with a note where the entries would have gone when the list is empty. Controllers were not sorted before; they are now, since the replica does not promise an order. Each entry names what it grants rather than a bare `viewer:`, because a report carries a line for both visibility settings and the entries under them would otherwise not say which they belong to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QooMLZ15EWRrknWtXfFZNp
1 parent 8797478 commit 733ae64

10 files changed

Lines changed: 134 additions & 63 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ air-gapped signing
1010

1111
* feat: a canister can now declare `upgrade_args` alongside `init_args`, in its own manifest and as a per-canister environment override. It is passed when `icp deploy` upgrades the canister, where `init_args` is passed when it installs or reinstalls it. It takes exactly the forms `init_args` does (inline Candid string, or `{ value | path, format }`), and paths resolve against the canister's own directory the same way. A canister that declares no `upgrade_args` is upgraded with its `init_args`, as before, and `--args` / `--args-file` still override whichever applies.
1212
* feat: a new `status_visibility` canister setting controls who may read a canister's status (its running state, cycles, memory usage, and settings) through the management canister. It takes the same forms as `log_visibility``controllers` (the default), `public`, or `{ allowed_viewers: [...] }` — and can be set in a manifest's `settings:` block or with `icp canister settings update --status-visibility / --add-status-viewer / --remove-status-viewer / --set-status-viewer`. `icp canister status` and `icp canister settings show` now report it. See the [canister settings reference](docs/reference/canister-settings.md#status_visibility).
13-
* `icp canister status` and `icp canister settings show` now list allowed viewers one per line under the setting (`viewer: <principal>`) rather than comma-separated on the label's line. This applies to `log_visibility` as well, so scripts matching that line need updating.
13+
* `icp canister status` and `icp canister settings show` now list principals one per line under their label rather than comma-separated on the label's line: `log viewer:` / `status viewer: <principal>` for the allowed viewers of a visibility setting, `controller: <principal>` for a canister's controllers. This applies to `log_visibility` and to the controller list as well, so scripts matching those lines need updating. Both lists are sorted, which the controller list previously was not.
1414
* This raises the minimum replica version: reading a canister's status now requires one that reports `status_visibility`, and against an older replica `canister_status` fails to decode — which affects `icp deploy`, `icp canister status`, and `icp canister settings show`/`sync`, not just the new setting. Every mainnet subnet reports it. A `managed` network resolves the launcher to `latest` unless it pins `version:`, so only a pinned launcher older than `15.0.0-2026-08-13-03-55` is affected; raise the pin to that version or later.
1515
* feat: `script` build steps now receive `ICP_CLI_ENVIRONMENT`, the name of the environment the canisters are being built for, so a build can vary by environment the way a sync step already could.
1616
* feat: `icp completions <SHELL>` prints a shell completion script for `bash`, `zsh`, `fish`, `powershell`, or `elvish` to stdout. See the [installation guide](docs/guides/installation.md#shell-completions) for where to put it.

crates/icp-cli/src/commands/canister/mod.rs

Lines changed: 90 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,59 @@ pub(crate) mod status;
1717
pub(crate) mod stop;
1818
pub(crate) mod top_up;
1919

20+
/// Lists principals one per line below a label, indented two spaces past
21+
/// `indent` — the indent of the label itself — so they nest the way the other
22+
/// lists in `canister status` and `canister settings show` do. `noun` names what
23+
/// each line holds, and stands in for the list when it is empty. Principals are
24+
/// sorted so repeated calls print the same order.
25+
fn format_principal_list(
26+
principals: impl IntoIterator<Item = String>,
27+
noun: &str,
28+
indent: &str,
29+
) -> String {
30+
let mut principals: Vec<String> = principals.into_iter().collect();
31+
principals.sort();
32+
33+
if principals.is_empty() {
34+
return format!("\n{indent} {noun} list is empty");
35+
}
36+
37+
principals
38+
.iter()
39+
.map(|principal| format!("\n{indent} {noun}: {principal}"))
40+
.collect()
41+
}
42+
2043
/// Renders a visibility setting for `canister status` and `canister settings show`.
2144
///
22-
/// The policy goes on the label's own line; allowed viewers are listed one per
23-
/// line below it, indented two spaces past `indent` — the indent of the label
24-
/// itselfso they nest the way the other lists in those reports do. Viewers
25-
/// are sorted so repeated calls print the same order.
26-
pub(crate) fn format_visibility(visibility: &Visibility, indent: &str) -> String {
45+
/// The policy goes on the label's own line, with any allowed viewers listed
46+
/// below it. `viewer` names what the setting grants — "log viewer", "status
47+
/// viewer"since a report carries one line per setting and the entries would
48+
/// otherwise not say which they belong to.
49+
pub(crate) fn format_visibility(visibility: &Visibility, viewer: &str, indent: &str) -> String {
2750
match visibility {
2851
Visibility::Controllers => "Controllers".to_string(),
2952
Visibility::Public => "Public".to_string(),
30-
Visibility::AllowedViewers(viewers) if viewers.is_empty() => {
31-
"Allowed viewers list is empty".to_string()
32-
}
33-
Visibility::AllowedViewers(viewers) => {
34-
let mut viewers: Vec<String> = viewers.iter().map(|p| p.to_string()).collect();
35-
viewers.sort();
36-
let mut out = "Allowed viewers".to_string();
37-
for viewer in viewers {
38-
out.push_str(&format!("\n{indent} viewer: {viewer}"));
39-
}
40-
out
41-
}
53+
Visibility::AllowedViewers(viewers) => format!(
54+
"Allowed viewers{}",
55+
format_principal_list(viewers.iter().map(|p| p.to_string()), viewer, indent)
56+
),
4257
}
4358
}
4459

60+
/// Renders a canister's controllers for the same two reports. Unlike a
61+
/// visibility setting, the list stands alone rather than qualifying a policy, so
62+
/// the label is part of what this returns.
63+
pub(crate) fn format_controllers(
64+
controllers: impl IntoIterator<Item = String>,
65+
indent: &str,
66+
) -> String {
67+
format!(
68+
"{indent}Controllers:{}",
69+
format_principal_list(controllers, "controller", indent)
70+
)
71+
}
72+
4573
/// Perform canister operations against a network
4674
#[derive(Debug, Subcommand)]
4775
#[allow(clippy::large_enum_variant)]
@@ -87,31 +115,64 @@ mod tests {
87115

88116
// `settings show`, where the label is not indented.
89117
assert_eq!(
90-
format!("Status visibility: {}", format_visibility(&viewers, "")),
118+
format!(
119+
"Status visibility: {}",
120+
format_visibility(&viewers, "status viewer", "")
121+
),
91122
"Status visibility: Allowed viewers\n \
92-
viewer: aaaaa-aa\n \
93-
viewer: ryjl3-tyaaa-aaaaa-aaaba-cai"
123+
status viewer: aaaaa-aa\n \
124+
status viewer: ryjl3-tyaaa-aaaaa-aaaba-cai"
94125
);
95126
// `canister status`, where it sits two spaces in.
96127
assert_eq!(
97-
format!(" Status visibility: {}", format_visibility(&viewers, " ")),
98-
" Status visibility: Allowed viewers\n \
99-
viewer: aaaaa-aa\n \
100-
viewer: ryjl3-tyaaa-aaaaa-aaaba-cai"
128+
format!(
129+
" Log visibility: {}",
130+
format_visibility(&viewers, "log viewer", " ")
131+
),
132+
" Log visibility: Allowed viewers\n \
133+
log viewer: aaaaa-aa\n \
134+
log viewer: ryjl3-tyaaa-aaaaa-aaaba-cai"
101135
);
102136
}
103137

104-
/// The other policies stay on the label's line.
138+
/// The fixed policies stay on the label's line, and an empty list says so
139+
/// where its entries would have gone.
105140
#[test]
106141
fn fixed_policies_are_rendered_inline() {
107142
assert_eq!(
108-
format_visibility(&Visibility::Controllers, " "),
143+
format_visibility(&Visibility::Controllers, "log viewer", " "),
109144
"Controllers"
110145
);
111-
assert_eq!(format_visibility(&Visibility::Public, " "), "Public");
112146
assert_eq!(
113-
format_visibility(&Visibility::AllowedViewers(vec![]), " "),
114-
"Allowed viewers list is empty"
147+
format_visibility(&Visibility::Public, "log viewer", " "),
148+
"Public"
149+
);
150+
assert_eq!(
151+
format_visibility(&Visibility::AllowedViewers(vec![]), "log viewer", " "),
152+
"Allowed viewers\n log viewer list is empty"
153+
);
154+
}
155+
156+
/// Controllers are listed the same way, but carry their own label.
157+
#[test]
158+
fn controllers_are_listed_one_per_line() {
159+
let controllers = ["ryjl3-tyaaa-aaaaa-aaaba-cai", "aaaaa-aa"].map(str::to_string);
160+
161+
assert_eq!(
162+
format_controllers(controllers.clone(), " "),
163+
" Controllers:\n \
164+
controller: aaaaa-aa\n \
165+
controller: ryjl3-tyaaa-aaaaa-aaaba-cai"
166+
);
167+
assert_eq!(
168+
format_controllers(controllers, ""),
169+
"Controllers:\n \
170+
controller: aaaaa-aa\n \
171+
controller: ryjl3-tyaaa-aaaaa-aaaba-cai"
172+
);
173+
assert_eq!(
174+
format_controllers([], " "),
175+
" Controllers:\n controller list is empty"
115176
);
116177
}
117178
}

crates/icp-cli/src/commands/canister/settings/show.rs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ use std::fmt::Write;
66

77
use icp::operations::proxy_management;
88

9-
use crate::commands::{args::CanisterCommandArgs, canister::format_visibility};
9+
use crate::commands::{
10+
args::CanisterCommandArgs,
11+
canister::{format_controllers, format_visibility},
12+
};
1013

1114
/// Show the settings of a canister.
1215
///
@@ -67,12 +70,8 @@ fn build_output(s: &DefiniteCanisterSettings) -> String {
6770
let mut buf = String::new();
6871
writeln!(
6972
&mut buf,
70-
"Controllers: {}",
71-
s.controllers
72-
.iter()
73-
.map(|p| p.to_string())
74-
.collect::<Vec<_>>()
75-
.join(", ")
73+
"{}",
74+
format_controllers(s.controllers.iter().map(|p| p.to_string()), "")
7675
)
7776
.unwrap();
7877
writeln!(&mut buf, "Compute allocation: {}", s.compute_allocation).unwrap();
@@ -96,13 +95,13 @@ fn build_output(s: &DefiniteCanisterSettings) -> String {
9695
writeln!(
9796
&mut buf,
9897
"Log visibility: {}",
99-
format_visibility(&s.log_visibility.clone().into(), "")
98+
format_visibility(&s.log_visibility.clone().into(), "log viewer", "")
10099
)
101100
.unwrap();
102101
writeln!(
103102
&mut buf,
104103
"Status visibility: {}",
105-
format_visibility(&s.status_visibility.clone().into(), "")
104+
format_visibility(&s.status_visibility.clone().into(), "status viewer", "")
106105
)
107106
.unwrap();
108107

crates/icp-cli/src/commands/canister/status.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ use tracing::debug;
1515
use icp::operations::{proxy::UpdateOrProxyError, proxy_management};
1616

1717
use crate::{
18-
commands::{args, canister::format_visibility},
18+
commands::{
19+
args,
20+
canister::{format_controllers, format_visibility},
21+
},
1922
options,
2023
};
2124

@@ -471,7 +474,11 @@ fn build_public_output(result: &PublicCanisterStatusResult) -> Result<String, an
471474
}
472475
writeln!(&mut buf, "Canister Status Report:")?;
473476

474-
writeln!(&mut buf, " Controllers: {}", result.controllers.join(", "))?;
477+
writeln!(
478+
&mut buf,
479+
"{}",
480+
format_controllers(result.controllers.iter().cloned(), " ")
481+
)?;
475482
writeln!(
476483
&mut buf,
477484
" Module hash: {}",
@@ -494,8 +501,8 @@ fn build_output(result: &SerializableCanisterStatusResult) -> Result<String, any
494501
let settings = &result.settings;
495502
writeln!(
496503
&mut buf,
497-
" Controllers: {}",
498-
settings.controllers.join(", ")
504+
"{}",
505+
format_controllers(settings.controllers.iter().cloned(), " ")
499506
)?;
500507
writeln!(
501508
&mut buf,
@@ -537,12 +544,12 @@ fn build_output(result: &SerializableCanisterStatusResult) -> Result<String, any
537544
writeln!(
538545
&mut buf,
539546
" Log visibility: {}",
540-
format_visibility(&settings.log_visibility.0, " ")
547+
format_visibility(&settings.log_visibility.0, "log viewer", " ")
541548
)?;
542549
writeln!(
543550
&mut buf,
544551
" Status visibility: {}",
545-
format_visibility(&settings.status_visibility.0, " ")
552+
format_visibility(&settings.status_visibility.0, "status viewer", " ")
546553
)?;
547554

548555
// Display environment variables configured for this canister

crates/icp-cli/tests/canister_create_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -973,7 +973,7 @@ async fn canister_create_with_unresolved_canister_controller_warns_and_syncs() {
973973
])
974974
.assert()
975975
.success()
976-
.stdout(contains("Controllers: 2vxsx-fae"));
976+
.stdout(contains("controller: 2vxsx-fae"));
977977

978978
// Creating "b" triggers sync_controller_dependents, which updates "a"'s controller list.
979979
ctx.icp()

crates/icp-cli/tests/canister_info_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ async fn canister_status() {
6161
])
6262
.assert()
6363
.success()
64-
.stdout(contains("Controllers: 2vxsx-fae").and(contains(
64+
.stdout(contains("controller: 2vxsx-fae").and(contains(
6565
"Module hash: 0x17a05e36278cd04c7ae6d3d3226c136267b9df7525a0657521405e22ec96be7a",
6666
)));
6767
}

crates/icp-cli/tests/canister_settings_tests.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ async fn canister_settings_update_controllers() {
7575
])
7676
.assert()
7777
.success()
78-
.stdout(contains("Controllers: 2vxsx-fae").and(contains(principal_alice.as_str()).not()));
78+
.stdout(contains("controller: 2vxsx-fae").and(contains(principal_alice.as_str()).not()));
7979

8080
// Add controller
8181
ctx.icp()
@@ -106,7 +106,7 @@ async fn canister_settings_update_controllers() {
106106
])
107107
.assert()
108108
.success()
109-
.stdout(contains("Controllers: 2vxsx-fae").and(contains(principal_alice.as_str())));
109+
.stdout(contains("controller: 2vxsx-fae").and(contains(principal_alice.as_str())));
110110

111111
// Add and remove controller.
112112
ctx.icp()
@@ -140,7 +140,7 @@ async fn canister_settings_update_controllers() {
140140
.assert()
141141
.success()
142142
.stdout(
143-
contains("Controllers: 2vxsx-fae")
143+
contains("controller: 2vxsx-fae")
144144
.and(contains(principal_alice.as_str()).not())
145145
.and(contains(principal_bob.as_str())),
146146
);
@@ -174,7 +174,7 @@ async fn canister_settings_update_controllers() {
174174
])
175175
.assert()
176176
.success()
177-
.stdout(contains("Controllers: 2vxsx-fae").and(contains(principal_bob.as_str()).not()));
177+
.stdout(contains("controller: 2vxsx-fae").and(contains(principal_bob.as_str()).not()));
178178

179179
// Add multiple controllers
180180
ctx.icp()
@@ -208,7 +208,7 @@ async fn canister_settings_update_controllers() {
208208
.assert()
209209
.success()
210210
.stdout(
211-
contains("Controllers: 2vxsx-fae")
211+
contains("controller: 2vxsx-fae")
212212
.and(contains(principal_alice.as_str()))
213213
.and(contains(principal_bob.as_str())),
214214
);
@@ -245,7 +245,7 @@ async fn canister_settings_update_controllers() {
245245
.assert()
246246
.success()
247247
.stdout(
248-
contains("Controllers: 2vxsx-fae")
248+
contains("controller: 2vxsx-fae")
249249
.and(contains(principal_alice.as_str()).not())
250250
.and(contains(principal_bob.as_str()).not()),
251251
);
@@ -572,7 +572,9 @@ async fn canister_settings_update_log_visibility() {
572572
])
573573
.assert()
574574
.success()
575-
.stdout(contains("Log visibility: Allowed viewers list is empty"));
575+
.stdout(contains(
576+
"Log visibility: Allowed viewers\n log viewer list is empty",
577+
));
576578

577579
// Add multiple log viewers.
578580
ctx.icp()
@@ -642,7 +644,9 @@ async fn canister_settings_update_log_visibility() {
642644
])
643645
.assert()
644646
.success()
645-
.stdout(contains("Log visibility: Allowed viewers list is empty"));
647+
.stdout(contains(
648+
"Log visibility: Allowed viewers\n log viewer list is empty",
649+
));
646650

647651
// Set multiple log viewers.
648652
ctx.icp()
@@ -954,7 +958,7 @@ async fn canister_settings_update_environment_variables() {
954958
.assert()
955959
.success()
956960
.stdout(
957-
contains("Controllers: 2vxsx-fae")
961+
contains("controller: 2vxsx-fae")
958962
.and(contains("Environment variables:"))
959963
.and(contains("PUBLIC_CANISTER_ID:my-canister")),
960964
);
@@ -1397,7 +1401,7 @@ async fn canister_settings_sync_log_visibility() {
13971401
confirm_log_visibility(
13981402
&ctx,
13991403
&project_dir,
1400-
"Allowed viewers\n viewer: 2vxsx-fae\n viewer: aaaaa-aa",
1404+
"Allowed viewers\n log viewer: 2vxsx-fae\n log viewer: aaaaa-aa",
14011405
);
14021406

14031407
// status_visibility takes the same manifest forms, and a single sync has to
@@ -1438,7 +1442,7 @@ async fn canister_settings_sync_log_visibility() {
14381442
.assert()
14391443
.success()
14401444
.stdout(contains(
1441-
"Status visibility: Allowed viewers\n viewer: 2vxsx-fae\n viewer: aaaaa-aa",
1445+
"Status visibility: Allowed viewers\n status viewer: 2vxsx-fae\n status viewer: aaaaa-aa",
14421446
));
14431447
}
14441448

crates/icp-cli/tests/canister_start_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ async fn canister_start() {
8080
.stdout(
8181
starts_with("Canister Id:")
8282
.and(contains("Status: Stopped"))
83-
.and(contains("Controllers: 2vxsx-fae")),
83+
.and(contains("controller: 2vxsx-fae")),
8484
);
8585

8686
// Start canister
@@ -111,7 +111,7 @@ async fn canister_start() {
111111
.stdout(
112112
starts_with("Canister Id:")
113113
.and(contains("Status: Running"))
114-
.and(contains("Controllers: 2vxsx-fae")),
114+
.and(contains("controller: 2vxsx-fae")),
115115
);
116116
}
117117

0 commit comments

Comments
 (0)