nimbus_cli/cli.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
use std::{ffi::OsString, path::PathBuf};
use chrono::Utc;
use clap::{Args, Parser, Subcommand};
#[derive(Parser)]
#[command(
author,
long_about = r#"Mozilla Nimbus' command line tool for mobile apps"#
)]
pub(crate) struct Cli {
/// The app name according to Nimbus.
#[arg(short, long, value_name = "APP")]
pub(crate) app: Option<String>,
/// The channel according to Nimbus. This determines which app to talk to.
#[arg(short, long, value_name = "CHANNEL")]
pub(crate) channel: Option<String>,
/// The device id of the simulator, emulator or device.
#[arg(short, long, value_name = "DEVICE_ID")]
pub(crate) device_id: Option<String>,
#[command(subcommand)]
pub(crate) command: CliCommand,
}
#[derive(Subcommand, Clone)]
pub(crate) enum CliCommand {
/// Send a complete JSON file to the Nimbus SDK and apply it immediately.
ApplyFile {
/// The filename to be loaded into the SDK.
file: PathBuf,
/// Keeps existing enrollments and experiments before enrolling.
///
/// This is unlikely what you want to do.
#[arg(long, default_value = "false")]
preserve_nimbus_db: bool,
#[command(flatten)]
open: OpenArgs,
},
/// Capture the logs into a file.
CaptureLogs {
/// The file to put the logs.
file: PathBuf,
},
/// Print the defaults for the manifest.
Defaults {
/// An optional feature-id
#[arg(short, long = "feature")]
feature_id: Option<String>,
/// An optional file to print the manifest defaults.
#[arg(short, long, value_name = "OUTPUT_FILE")]
output: Option<PathBuf>,
#[command(flatten)]
manifest: ManifestArgs,
},
/// Enroll into an experiment or a rollout.
///
/// The experiment slug is a combination of the actual slug, and the server it came from.
///
/// * `release`/`stage` determines the server.
///
/// * `preview` selects the preview collection.
///
/// These can be further combined: e.g. $slug, preview/$slug, stage/$slug, stage/preview/$slug
Enroll {
#[command(flatten)]
experiment: ExperimentArgs,
/// The branch slug.
#[arg(short, long, value_name = "BRANCH")]
branch: String,
/// Optional rollout slugs, including the server and collection.
#[arg(value_name = "ROLLOUTS")]
rollouts: Vec<String>,
/// Preserves the original experiment targeting
#[arg(long, default_value = "false")]
preserve_targeting: bool,
/// Preserves the original experiment bucketing
#[arg(long, default_value = "false")]
preserve_bucketing: bool,
#[command(flatten)]
open: OpenArgs,
/// Keeps existing enrollments and experiments before enrolling.
///
/// This is unlikely what you want to do.
#[arg(long, default_value = "false")]
preserve_nimbus_db: bool,
/// Don't validate the feature config files before enrolling
#[arg(long, default_value = "false")]
no_validate: bool,
#[command(flatten)]
manifest: ManifestArgs,
},
/// Print the feature configuration involved in the branch of an experiment.
///
/// This can be optionally merged with the defaults from the feature manifest.
Features {
#[command(flatten)]
manifest: ManifestArgs,
#[command(flatten)]
experiment: ExperimentArgs,
/// The branch of the experiment
#[arg(short, long)]
branch: String,
/// If set, then merge the experimental configuration with the defaults from the manifest
#[arg(short, long, default_value = "false")]
validate: bool,
/// An optional feature-id: if it exists in this branch, print this feature
/// on its own.
#[arg(short, long = "feature")]
feature_id: Option<String>,
/// Print out the features involved in this branch as in a format:
/// `{ $feature_id: $value }`.
///
/// Automated tools should use this, since the output is predictable.
#[arg(short, long = "multi", default_value = "false")]
multi: bool,
/// An optional file to print the output.
#[arg(short, long, value_name = "OUTPUT_FILE")]
output: Option<PathBuf>,
},
/// Fetch one or more named experiments and rollouts and put them in a file.
Fetch {
/// The file to download the recipes to.
#[arg(short, long, value_name = "OUTPUT_FILE")]
output: Option<PathBuf>,
#[command(flatten)]
experiment: ExperimentArgs,
/// The recipe slugs, including server.
///
/// Use once per recipe to download. e.g.
/// fetch --output file.json preview/my-experiment my-rollout
///
/// Cannot be used with the server option: use `fetch-list` instead.
#[arg(value_name = "RECIPE")]
recipes: Vec<String>,
},
/// Fetch a list of experiments and put it in a file.
FetchList {
/// The file to download the recipes to.
#[arg(short, long, value_name = "OUTPUT_FILE")]
output: Option<PathBuf>,
#[command(flatten)]
list: ExperimentListArgs,
},
/// Execute a nimbus-fml command. See
///
/// nimbus-cli fml -- --help
///
/// for more.
Fml { args: Vec<OsString> },
/// Displays information about an experiment
Info {
#[command(flatten)]
experiment: ExperimentArgs,
/// An optional file to print the output.
#[arg(short, long, value_name = "OUTPUT_FILE")]
output: Option<PathBuf>,
},
/// List the experiments from a server
List {
#[command(flatten)]
list: ExperimentListArgs,
},
/// Print the state of the Nimbus database to logs.
///
/// This causes a restart of the app.
LogState {
#[command(flatten)]
open: OpenArgs,
},
/// Open the app without changing the state of experiment enrollments.
Open {
#[command(flatten)]
open: OpenArgs,
/// By default, the app is terminated before sending the a deeplink.
///
/// If this flag is set, then do not terminate the app if it is already running.
#[arg(long, default_value = "false")]
no_clobber: bool,
},
/// Start a server
#[cfg(feature = "server")]
StartServer,
/// Reset the app back to its just installed state
ResetApp,
/// Follow the logs for the given app.
TailLogs,
/// Configure an application feature with one or more feature config files.
///
/// One file per branch. The branch slugs will correspond to the file names.
///
/// By default, the files are validated against the manifest; this can be
/// overridden with `--no-validate`.
TestFeature {
/// The identifier of the feature to configure
feature_id: String,
/// One or more files containing a feature config for the feature.
files: Vec<PathBuf>,
/// An optional patch file, used to patch feature configurations
///
/// This is of the format that comes from the
/// `features --multi` or `defaults` commands.
#[arg(long, value_name = "PATCH_FILE")]
patch: Option<PathBuf>,
#[command(flatten)]
open: OpenArgs,
/// Don't validate the feature config files before enrolling
#[arg(long, default_value = "false")]
no_validate: bool,
#[command(flatten)]
manifest: ManifestArgs,
},
/// Unenroll from all experiments and rollouts
Unenroll {
#[command(flatten)]
open: OpenArgs,
},
/// Validate an experiment against a feature manifest
Validate {
#[command(flatten)]
experiment: ExperimentArgs,
#[command(flatten)]
manifest: ManifestArgs,
},
}
#[derive(Args, Clone, Debug, Default)]
pub(crate) struct ManifestArgs {
/// An optional manifest file
#[arg(long, value_name = "MANIFEST_FILE")]
pub(crate) manifest: Option<String>,
/// An optional version of the app.
/// If present, constructs the `ref` from an app specific template.
/// Due to inconsistencies in branching names, this isn't always
/// reliable.
#[arg(long, value_name = "APP_VERSION")]
pub(crate) version: Option<String>,
/// The branch/tag/commit for the version of the manifest
/// to get from Github.
#[arg(long, value_name = "APP_VERSION", default_value = "main")]
pub(crate) ref_: String,
}
#[derive(Args, Clone, Debug, Default)]
pub(crate) struct OpenArgs {
/// Optional deeplink. If present, launch with this link.
#[arg(long, value_name = "DEEPLINK")]
pub(crate) deeplink: Option<String>,
/// Resets the app back to its initial state before launching
#[arg(long, default_value = "false")]
pub(crate) reset_app: bool,
/// Instead of opening via adb or xcrun simctl, construct a deeplink
/// and put it into the pastebuffer.
///
/// If present, then the app is not launched, so this option does not work with
/// `--reset-app` or passthrough arguments.
#[arg(long, default_value = "false")]
pub(crate) pbcopy: bool,
/// Instead of opening via adb or xcrun simctl, construct a deeplink
/// and put it into the pastebuffer.
///
/// If present, then the app is not launched, so this option does not work with
/// `--reset-app` or passthrough arguments.
#[arg(long, default_value = "false")]
pub(crate) pbpaste: bool,
/// Optionally, add platform specific arguments to the adb or xcrun command.
///
/// By default, arguments are added to the end of the command, likely to be passed
/// directly to the app.
///
/// Arguments before a special placeholder `{}` are passed to
/// `adb am start` or `xcrun simctl launch` commands directly.
#[arg(last = true, value_name = "PASSTHROUGH_ARGS")]
pub(crate) passthrough: Vec<String>,
/// An optional file to dump experiments into.
///
/// If present, then the app is not launched, so this option does not work with
/// `--reset-app` or passthrough arguments.
#[arg(long, value_name = "OUTPUT_FILE")]
pub(crate) output: Option<PathBuf>,
}
#[derive(Args, Clone, Debug, Default)]
pub(crate) struct ExperimentArgs {
/// The experiment slug, including the server and collection.
#[arg(value_name = "EXPERIMENT_SLUG")]
pub(crate) experiment: String,
/// An optional file from which to get the experiment.
///
/// By default, the file is fetched from the server.
#[arg(long, value_name = "EXPERIMENTS_FILE")]
pub(crate) file: Option<PathBuf>,
/// Use remote settings to fetch the experiment recipe.
///
/// By default, the file is fetched from the v6 api of experimenter.
#[arg(long, default_value = "false")]
pub(crate) use_rs: bool,
/// An optional patch file, used to patch feature configurations
///
/// This is of the format that comes from the
/// `features --multi` or `defaults` commands.
#[arg(long, value_name = "PATCH_FILE")]
pub(crate) patch: Option<PathBuf>,
}
#[derive(Args, Clone, Debug, Default)]
pub(crate) struct ExperimentListArgs {
#[command(flatten)]
pub(crate) source: ExperimentListSourceArgs,
#[command(flatten)]
pub(crate) filter: ExperimentListFilterArgs,
}
#[derive(Args, Clone, Debug, Default)]
pub(crate) struct ExperimentListSourceArgs {
/// A server slug e.g. preview, release, stage, stage/preview
#[arg(default_value = "")]
pub(crate) server: String,
/// An optional file
#[arg(short, long, value_name = "FILE")]
pub(crate) file: Option<PathBuf>,
/// Use the v6 API to fetch the experiment recipes.
///
/// By default, the file is fetched from the Remote Settings.
///
/// The API contains *all* launched experiments, past and present,
/// so this is considerably slower and longer than Remote Settings.
#[arg(long, default_value = "false")]
pub(crate) use_api: bool,
}
#[derive(Args, Clone, Debug, Default)]
pub(crate) struct ExperimentListFilterArgs {
#[arg(short = 'S', long, value_name = "SLUG_PATTERN")]
pub(crate) slug: Option<String>,
#[arg(short = 'F', long, value_name = "FEATURE_PATTERN")]
pub(crate) feature: Option<String>,
#[arg(short = 'A', long, value_name = "DATE", value_parser=validate_date)]
pub(crate) active_on: Option<String>,
#[arg(short = 'E', long, value_name = "DATE", value_parser=validate_date)]
pub(crate) enrolling_on: Option<String>,
#[arg(short = 'C', long, value_name = "CHANNEL")]
pub(crate) channel: Option<String>,
#[arg(short = 'R', long, value_name = "FLAG")]
pub(crate) is_rollout: Option<bool>,
}
fn validate_num(s: &str, l: usize) -> Result<(), &'static str> {
if !s.chars().all(char::is_numeric) {
Err("String contains non-numeric characters")
} else if s.len() != l {
Err("String is the wrong length")
} else {
Ok(())
}
}
fn validate_date_parts(yyyy: &str, mm: &str, dd: &str) -> Result<(), &'static str> {
validate_num(yyyy, 4)?;
validate_num(mm, 2)?;
validate_num(dd, 2)?;
Ok(())
}
fn validate_date(s: &str) -> Result<String, String> {
if s == "today" {
let now = Utc::now();
return Ok(format!("{}", now.format("%Y-%m-%d")));
}
match s.splitn(3, '-').collect::<Vec<_>>().as_slice() {
[yyyy, mm, dd] if validate_date_parts(yyyy, mm, dd).is_ok() => Ok(s.to_string()),
_ => Err("Date string must be yyyy-mm-dd".to_string()),
}
}