seaplane_cli/api/
locks.rs

1use reqwest::Url;
2use seaplane::{
3    api::{
4        identity::v0::AccessToken,
5        locks::v1::{
6            HeldLock as HeldLockModel, LockId, LockInfo as LockInfoModel, LockInfoRange, LockName,
7            LocksRequest, LocksRequestBuilder,
8        },
9        shared::v1::{Directory, RangeQueryContext},
10        ApiErrorKind,
11    },
12    error::SeaplaneError,
13};
14
15use crate::{
16    api::request_token,
17    context::Ctx,
18    error::{CliError, Result},
19};
20
21/// Wraps an SDK `LocksRequest` where we do additional things like re-use request access
22/// tokens, allow changing the Locks this request is pointed to, and map errors appropriately.
23#[derive(Debug)]
24pub struct LocksReq {
25    api_key: String,
26    lock_id: Option<String>,
27    name: Option<LockName>,
28    token: Option<AccessToken>,
29    inner: Option<LocksRequest>,
30    identity_url: Option<Url>,
31    locks_url: Option<Url>,
32    insecure_urls: bool,
33    invalid_certs: bool,
34}
35
36impl LocksReq {
37    pub fn new(ctx: &Ctx) -> Result<Self> {
38        Ok(Self {
39            api_key: ctx.args.api_key()?.into(),
40            lock_id: None,
41            name: None,
42            token: None,
43            inner: None,
44            identity_url: ctx.identity_url.clone(),
45            locks_url: ctx.locks_url.clone(),
46            #[cfg(feature = "allow_insecure_urls")]
47            insecure_urls: ctx.insecure_urls,
48            #[cfg(not(feature = "allow_insecure_urls"))]
49            insecure_urls: false,
50            #[cfg(feature = "allow_invalid_certs")]
51            invalid_certs: ctx.invalid_certs,
52            #[cfg(not(feature = "allow_invalid_certs"))]
53            invalid_certs: false,
54        })
55    }
56
57    pub fn set_identifiers<S: Into<String>>(
58        &mut self,
59        name: Option<LockName>,
60        lock_id: Option<S>,
61    ) -> Result<()> {
62        self.name = name;
63        self.lock_id = lock_id.map(|s| s.into());
64        self.refresh_inner()
65    }
66
67    pub fn set_name(&mut self, name: LockName) -> Result<()> {
68        self.name = Some(name);
69        self.refresh_inner()
70    }
71
72    /// Request a new Access Token
73    pub fn refresh_token(&mut self) -> Result<()> {
74        self.token = Some(request_token(
75            &self.api_key,
76            self.identity_url.as_ref(),
77            self.insecure_urls,
78            self.invalid_certs,
79        )?);
80        Ok(())
81    }
82
83    /// Re-build the inner `LocksRequest`. This is mostly useful when one wants to point at a
84    /// different Lock than the original request was pointed at (i.e. via `set_name`). This
85    /// method will also refresh the access token, only if required.
86    fn refresh_inner(&mut self) -> Result<()> {
87        let mut builder = LocksRequest::builder().token(self.token_or_refresh()?);
88
89        #[cfg(feature = "allow_insecure_urls")]
90        {
91            builder = builder.allow_http(self.insecure_urls);
92        }
93        #[cfg(feature = "allow_invalid_certs")]
94        {
95            builder = builder.allow_invalid_certs(self.invalid_certs);
96        }
97        if self.name.is_none() {
98            panic!("all LocksRequests must have a name")
99        }
100
101        match &self.lock_id {
102            Some(lock_id) => {
103                let default_sequencer_value = 0u32;
104                builder = builder.held_lock(HeldLockModel::new(
105                    self.name.clone().unwrap(),
106                    LockId::from_encoded(lock_id),
107                    default_sequencer_value,
108                ));
109            }
110            None => builder = builder.lock_name(self.name.clone().unwrap()),
111        }
112
113        if let Some(url) = &self.locks_url {
114            builder = builder.base_url(url);
115        }
116
117        self.inner = Some(builder.build().map_err(CliError::from)?);
118        Ok(())
119    }
120
121    /// Retrieves the JWT access token, requesting a new one if required.
122    pub fn token_or_refresh(&mut self) -> Result<&str> {
123        if self.token.is_none() {
124            self.refresh_token()?;
125        }
126        Ok(&self.token.as_ref().unwrap().token)
127    }
128
129    /// Gets a page of held locks from `dir` if present (or the root) if not, optionally starting
130    /// from `next_key`
131    pub fn get_page(
132        &mut self,
133        next_key: Option<LockName>,
134        dir: Option<LockName>,
135    ) -> Result<LockInfoRange> {
136        // get_page doesn't use `inner` here, since it doesn't refer to any lock name
137        // (Specifically, get_page() doesn't refer to any individual lock)
138        let mut range = RangeQueryContext::new();
139        if let Some(k) = next_key {
140            range.set_from(k);
141        }
142
143        if let Some(d) = dir {
144            range.set_directory(Directory::from_encoded(d.encoded()));
145        }
146
147        let mut builder = LocksRequestBuilder::new()
148            .token(self.token_or_refresh()?)
149            .range(range.clone());
150        #[cfg(feature = "allow_insecure_urls")]
151        {
152            builder = builder.allow_http(self.insecure_urls);
153        }
154        #[cfg(feature = "allow_invalid_certs")]
155        {
156            builder = builder.allow_http(self.invalid_certs);
157        }
158
159        if let Some(url) = &self.locks_url {
160            builder = builder.base_url(url);
161        }
162
163        let req = builder.build().unwrap();
164
165        match req.get_page() {
166            Err(SeaplaneError::ApiResponse(ae)) if ae.kind == ApiErrorKind::Unauthorized => {
167                self.token = Some(request_token(
168                    &self.api_key,
169                    self.identity_url.as_ref(),
170                    self.insecure_urls,
171                    self.invalid_certs,
172                )?);
173                let next_req = LocksRequestBuilder::new()
174                    .token(self.token_or_refresh()?)
175                    .range(range)
176                    .build()
177                    .unwrap();
178
179                Ok(next_req.get_page()?)
180            }
181            result => result.map_err(CliError::from),
182        }
183    }
184}
185
186// Wrapped LocksRequest methods to handle expired token retries
187//
188impl LocksReq {
189    pub fn acquire(&mut self, ttl: u32, client_id: &str) -> Result<HeldLockModel> {
190        maybe_retry!(self.acquire(ttl, client_id))
191    }
192    pub fn release(&mut self) -> Result<()> { maybe_retry!(self.release()) }
193    pub fn renew(&mut self, ttl: u32) -> Result<()> { maybe_retry!(self.renew(ttl)) }
194    pub fn get_lock_info(&mut self) -> Result<LockInfoModel> { maybe_retry!(self.get_lock_info()) }
195}