seaplane_cli/
context.rs

1//! Context describes the normalized and processed "settings" that a command can use at runtime.
2//! This differs from the "config" or the "CLI Arguments" as the Context is built and updated from
3//! those sources. This means the context is responsible for de-conflicting mutually exclusive
4//! options, or overriding values.
5//!
6//! The Context is source of truth for all runtime decisions.
7//!
8//! The order of evaluation is as follows (note lower layers override layers above):
9//!
10//! 1. System configuration files are loaded (if any...currently none are defined)
11//! 2. User configuration files are loaded (if any are found)
12//! 3. Environment Variables (currently none are defined)
13//! 4. Command Line Arguments
14//!   4a. Because we use subcommands and global arguments each subcommand acts as it's own set of
15//!   Command Line Arguments, and can thus affect the Context at each level in the command
16//! hierarchy.   4b. Before the Context is handed off mutably to the next nested level, all updates
17//! from the   parent should be finalized.
18//!
19//! After these steps the final Context is what is used to make runtime decisions.
20//!
21//! The context struct itself contains "global" values or those that apply in many scenarios or to
22//! many commands. It also contains specialized contexts that contain values only relevant to those
23//! commands or processes that need them. These specialized contexts should be lazily derived.
24
25pub mod flight;
26pub use flight::FlightCtx;
27pub mod formation;
28pub use formation::{FormationCfgCtx, FormationCtx};
29pub mod metadata;
30pub use metadata::MetadataCtx;
31pub mod locks;
32pub use locks::LocksCtx;
33pub mod restrict;
34use std::path::{Path, PathBuf};
35
36use clap_complete::Shell;
37use once_cell::unsync::OnceCell;
38use reqwest::Url;
39pub use restrict::RestrictCtx;
40
41use crate::{
42    config::RawConfig,
43    error::{CliErrorKind, Context, Result},
44    fs::{self, FromDisk, ToDisk},
45    ops::{flight::Flights, formation::Formations},
46    printer::{ColorChoice, OutputFormat},
47};
48
49const FLIGHTS_FILE: &str = "flights.json";
50const FORMATIONS_FILE: &str = "formations.json";
51/// The registry to use for image references when the registry is omitted by the user
52pub const DEFAULT_IMAGE_REGISTRY_URL: &str = "registry.cplane.cloud";
53
54#[derive(Debug, Default, Clone)]
55pub struct Args {
56    /// when the answer is "yes...but only if the stream is a TTY." In these cases an enum of
57    /// Never, Auto, Always would be more appropriate
58    ///
59    /// Should be display ANSI color codes in output?
60    pub color: ColorChoice,
61
62    /// The name or local ID of an item
63    pub name_id: Option<String>,
64
65    /// What to overwrite
66    pub overwrite: Vec<String>,
67
68    /// Do items need to be exact to match
69    pub exact: bool,
70
71    /// Match all items
72    pub all: bool,
73
74    /// Display third party licenses
75    pub third_party: bool,
76
77    /// The shell to generate completions for
78    pub shell: Option<Shell>,
79
80    /// How to display output
81    pub out_format: OutputFormat,
82
83    /// Try to force the operation to happen
84    pub force: bool,
85
86    /// Do not use local state files
87    pub stateless: bool,
88
89    /// The API Key associated with an account provided by the CLI, env, or Config used to request
90    /// access tokens
91    pub api_key: Option<String>,
92
93    /// Should we fetch remote refs?
94    pub fetch: bool,
95}
96
97impl Args {
98    pub fn api_key(&self) -> Result<&str> {
99        self.api_key
100            .as_deref()
101            .ok_or_else(|| CliErrorKind::MissingApiKey.into_err())
102    }
103}
104
105/// The source of truth "Context" that is passed to all runtime processes to make decisions based
106/// on user configuration
107// TODO: we may not want to derive this we implement circular references
108#[derive(Debug)]
109pub struct Ctx {
110    /// The platform specific path to a data location
111    data_dir: PathBuf,
112
113    /// Context relate to exclusively to Flight operations and commands
114    pub flight_ctx: LateInit<FlightCtx>,
115
116    /// Context relate to exclusively to Formation operations and commands
117    pub formation_ctx: LateInit<FormationCtx>,
118
119    /// Context relate to exclusively to key-value operations and commands
120    pub md_ctx: LateInit<MetadataCtx>,
121
122    /// Context relate to exclusively to Locks operations and commands
123    pub locks_ctx: LateInit<LocksCtx>,
124
125    /// Context relate to exclusively to Restrict operations and commands
126    pub restrict_ctx: LateInit<RestrictCtx>,
127
128    /// Where the configuration files were loaded from
129    pub conf_files: Vec<PathBuf>,
130
131    /// Common CLI arguments
132    pub args: Args,
133
134    /// The in memory databases
135    pub db: Db,
136
137    /// Allows tracking if we're running a command internally and skippy certain checks or output
138    pub internal_run: bool,
139
140    /// Did we run initialization automatically or not on startup?
141    pub did_init: bool,
142
143    /// Disable progress bar indicators
144    pub disable_pb: bool,
145
146    /// The container image registry to infer if not provided
147    pub registry: String,
148
149    /// Set the base URL for the request
150    pub compute_url: Option<Url>,
151    pub identity_url: Option<Url>,
152    pub metadata_url: Option<Url>,
153    pub locks_url: Option<Url>,
154    pub insecure_urls: bool,
155    pub invalid_certs: bool,
156}
157
158impl Clone for Ctx {
159    fn clone(&self) -> Self {
160        Self {
161            data_dir: self.data_dir.clone(),
162            flight_ctx: if self.flight_ctx.get().is_some() {
163                let li = LateInit::default();
164                li.init(self.flight_ctx.get().cloned().unwrap());
165                li
166            } else {
167                LateInit::default()
168            },
169            formation_ctx: if self.formation_ctx.get().is_some() {
170                let li = LateInit::default();
171                li.init(self.formation_ctx.get().cloned().unwrap());
172                li
173            } else {
174                LateInit::default()
175            },
176            md_ctx: if self.md_ctx.get().is_some() {
177                let li = LateInit::default();
178                li.init(self.md_ctx.get().cloned().unwrap());
179                li
180            } else {
181                LateInit::default()
182            },
183            locks_ctx: if self.locks_ctx.get().is_some() {
184                let li = LateInit::default();
185                li.init(self.locks_ctx.get().cloned().unwrap());
186                li
187            } else {
188                LateInit::default()
189            },
190            restrict_ctx: if self.restrict_ctx.get().is_some() {
191                let li = LateInit::default();
192                li.init(self.restrict_ctx.get().cloned().unwrap());
193                li
194            } else {
195                LateInit::default()
196            },
197            conf_files: self.conf_files.clone(),
198            args: self.args.clone(),
199            db: self.db.clone(),
200            internal_run: self.internal_run,
201            did_init: self.did_init,
202            disable_pb: self.disable_pb,
203            registry: self.registry.clone(),
204            compute_url: self.compute_url.clone(),
205            identity_url: self.identity_url.clone(),
206            metadata_url: self.metadata_url.clone(),
207            locks_url: self.locks_url.clone(),
208            insecure_urls: self.insecure_urls,
209            invalid_certs: self.invalid_certs,
210        }
211    }
212}
213
214impl Default for Ctx {
215    fn default() -> Self {
216        Self {
217            data_dir: fs::data_dir(),
218            flight_ctx: LateInit::default(),
219            formation_ctx: LateInit::default(),
220            md_ctx: LateInit::default(),
221            locks_ctx: LateInit::default(),
222            restrict_ctx: LateInit::default(),
223            conf_files: Vec::new(),
224            args: Args::default(),
225            db: Db::default(),
226            internal_run: false,
227            did_init: false,
228            disable_pb: false,
229            compute_url: None,
230            identity_url: None,
231            metadata_url: None,
232            locks_url: None,
233            insecure_urls: false,
234            invalid_certs: false,
235            registry: DEFAULT_IMAGE_REGISTRY_URL.into(),
236        }
237    }
238}
239
240impl From<RawConfig> for Ctx {
241    fn from(cfg: RawConfig) -> Self {
242        Self {
243            data_dir: fs::data_dir(),
244            conf_files: cfg.loaded_from.clone(),
245            args: Args {
246                // We default to using color. Later when the context is updated from the CLI args,
247                // this may change.
248                color: cfg.seaplane.color.unwrap_or_default(),
249                api_key: cfg.account.api_key,
250                ..Default::default()
251            },
252            registry: cfg
253                .seaplane
254                .default_registry_url
255                .unwrap_or_else(|| DEFAULT_IMAGE_REGISTRY_URL.into())
256                .trim_end_matches('/')
257                .to_string(),
258            compute_url: cfg.api.compute_url,
259            identity_url: cfg.api.identity_url,
260            metadata_url: cfg.api.metadata_url,
261            locks_url: cfg.api.locks_url,
262            did_init: cfg.did_init,
263            #[cfg(feature = "allow_insecure_urls")]
264            insecure_urls: cfg.danger_zone.allow_insecure_urls,
265            #[cfg(feature = "allow_invalid_certs")]
266            invalid_certs: cfg.danger_zone.allow_invalid_certs,
267            ..Self::default()
268        }
269    }
270}
271
272impl Ctx {
273    pub fn update_from_env(&mut self) -> Result<()> {
274        // TODO: this just gets it compiling. Using `todo!` blocks progress since loading the
275        // context happens at program startup, so we cannot panic on unimplemented
276        Ok(())
277    }
278
279    #[inline]
280    pub fn data_dir(&self) -> &Path { &self.data_dir }
281
282    pub fn conf_files(&self) -> &[PathBuf] { &self.conf_files }
283
284    pub fn flights_file(&self) -> PathBuf { self.data_dir.join(FLIGHTS_FILE) }
285
286    pub fn formations_file(&self) -> PathBuf { self.data_dir.join(FORMATIONS_FILE) }
287
288    /// Write out an entirely new JSON file if `--stateless` wasn't used
289    pub fn persist_formations(&self) -> Result<()> {
290        self.db
291            .formations
292            .persist_if(!self.args.stateless)
293            .with_context(|| format!("Path: {:?}\n", self.formations_file()))
294    }
295
296    /// Write out an entirely new JSON file if `--stateless` wasn't used
297    pub fn persist_flights(&self) -> Result<()> {
298        self.db
299            .flights
300            .persist_if(!self.args.stateless)
301            .with_context(|| format!("Path: {:?}\n", self.flights_file()))
302    }
303}
304
305/// The in memory "Databases"
306#[derive(Debug, Default, Clone)]
307pub struct Db {
308    /// The in memory Flights database
309    pub flights: Flights,
310
311    /// The in memory Formations database
312    pub formations: Formations,
313
314    /// A *hint* that we should persist at some point. Not gospel
315    pub needs_persist: bool,
316}
317
318impl Db {
319    pub fn load<P: AsRef<Path>>(flights: P, formations: P) -> Result<Self> {
320        Self::load_if(flights, formations, true)
321    }
322
323    pub fn load_if<P: AsRef<Path>>(flights: P, formations: P, yes: bool) -> Result<Self> {
324        Ok(Self {
325            flights: FromDisk::load_if(flights, yes).unwrap_or_else(|| Ok(Flights::default()))?,
326            formations: FromDisk::load_if(formations, yes)
327                .unwrap_or_else(|| Ok(Formations::default()))?,
328            needs_persist: false,
329        })
330    }
331}
332
333// TODO: we may not want to derive this we implement circular references
334#[derive(Debug)]
335pub struct LateInit<T> {
336    inner: OnceCell<T>,
337}
338
339impl<T> Default for LateInit<T> {
340    fn default() -> Self { Self { inner: OnceCell::default() } }
341}
342
343impl<T> LateInit<T> {
344    pub fn init(&self, val: T) { assert!(self.inner.set(val).is_ok()) }
345    pub fn get(&self) -> Option<&T> { self.inner.get() }
346    pub fn get_mut(&mut self) -> Option<&mut T> { self.inner.get_mut() }
347}
348
349impl<T: Default> LateInit<T> {
350    pub fn get_or_init(&self) -> &T { self.inner.get_or_init(|| T::default()) }
351    pub fn get_mut_or_init(&mut self) -> &mut T {
352        self.inner.get_or_init(|| T::default());
353        self.inner.get_mut().unwrap()
354    }
355}