• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

yaleman / goatns / #2

14 Jan 2026 05:03AM UTC coverage: 32.503% (+0.8%) from 31.674%
#2

push

yaleman
updating justfile

1083 of 3332 relevant lines covered (32.5%)

60.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

23.08
/src/cli.rs
1
//! Code related to CLI things
2
//!
3

4
use clap::*;
5
use dialoguer::theme::ColorfulTheme;
6
use dialoguer::{Confirm, Input};
7
use tokio::io::AsyncWriteExt;
8
use tokio::sync::{mpsc, oneshot};
9
use tokio::time::sleep;
10
use tracing::{debug, error, info, warn};
11

12
use crate::config::ConfigFile;
13
use crate::datastore::Command;
14
use crate::zones::FileZone;
15

16
#[derive(Parser, Clone)]
17
pub struct SharedOpts {
18
    #[clap(short, long, help = "Configuration file")]
19
    config: Option<String>,
20
    #[clap(short, long)]
21
    debug: bool,
22
}
23

24
#[derive(Subcommand)]
25
pub enum Commands {
26
    Server {
27
        #[clap(flatten)]
28
        sopt: SharedOpts,
29
    },
30
    AddAdmin {
31
        #[clap(flatten)]
32
        sopt: SharedOpts,
33
    },
34
    ImportZones {
35
        #[clap(flatten)]
36
        sopt: SharedOpts,
37
        filename: String,
38
        #[clap(short, long, help = "Specific Zone name to import")]
39
        zone: Option<String>,
40
    },
41
    ConfigCheck {
42
        #[clap(flatten)]
43
        sopt: SharedOpts,
44
    },
45
    ExportConfig {
46
        #[clap(flatten)]
47
        sopt: SharedOpts,
48
    },
49
    ExportZone {
50
        #[clap(flatten)]
51
        sopt: SharedOpts,
52
        zone_name: String,
53
        output_filename: String,
54
    },
55
}
56

57
impl Default for Commands {
58
    fn default() -> Self {
×
59
        Commands::Server {
60
            sopt: SharedOpts {
×
61
                config: None,
62
                debug: false,
63
            },
64
        }
65
    }
66
}
67

68
#[derive(Parser)]
69
#[command(arg_required_else_help(false))]
70
/// Yet another authoritative DNS name server. But with goat references.
71
pub struct Cli {
72
    #[command(subcommand)]
73
    pub command: Commands,
74
}
75

76
impl Cli {
77
    pub fn config(&self) -> Option<String> {
×
78
        match &self.command {
×
79
            Commands::Server { sopt } => sopt.config.clone(),
×
80
            _ => None,
×
81
        }
82
    }
83

84
    pub fn debug(&self) -> bool {
×
85
        match &self.command {
×
86
            Commands::Server { sopt, .. } => sopt.debug,
×
87
            _ => false,
×
88
        }
89
    }
90
}
91

92
/// Output a default configuration file, based on the [crate::config::ConfigFile] object.
93
pub fn default_config() {
1✔
94
    let output = match serde_json::to_string_pretty(&ConfigFile::default()) {
95
        Ok(value) => value,
2✔
96
        Err(_) => {
97
            error!("I don't know how, but we couldn't parse our own config file def.");
98
            "".to_string()
99
        }
×
100
    };
101
    println!("{output}");
102
}
103

104
/// Dump a zone to a file
105
pub async fn export_zone_file(
106
    tx: mpsc::Sender<Command>,
107
    zone_name: &str,
108
    filename: &str,
1✔
109
) -> Result<(), String> {
110
    // make a channel
111

112
    let (tx_oneshot, rx_oneshot) = oneshot::channel();
113
    let ds_req: Command = Command::GetZone {
114
        id: None,
115
        name: Some(zone_name.to_string()),
3✔
116
        resp: tx_oneshot,
117
    };
118
    if let Err(error) = tx.send(ds_req).await {
1✔
119
        return Err(format!(
120
            "failed to send to datastore from export_zone_file {error:?}"
121
        ));
4✔
122
    };
123
    debug!("Sent request to datastore");
124

125
    let zone: Option<FileZone> = match rx_oneshot.await {
126
        Ok(value) => value,
1✔
127
        Err(err) => return Err(format!("rx from ds failed {err:?}")),
128
    };
3✔
129
    eprintln!("Got filezone: {zone:?}");
130

×
131
    let zone_bytes = match zone {
132
        None => {
133
            warn!("Couldn't find the zone {zone_name}");
134
            return Ok(());
1✔
135
        }
136
        Some(zone) => serde_json::to_string_pretty(&zone).map_err(|err| {
×
137
            format!(
138
                "Failed to serialize zone {zone_name} to json: {err:?}"
139
            )
3✔
140
        })?,
1✔
141
    };
142

143
    // open the file
144
    let mut file = tokio::fs::File::create(filename)
1✔
145
        .await
146
        .map_err(|e| format!("Failed to open file {e:?}"))?;
1✔
147
    // write the thing
148
    file.write_all(zone_bytes.as_bytes())
149
        .await
150
        .map_err(|e| format!("Failed to write file: {e:?}"))?;
1✔
151
    // make some cake
152

153
    Ok(())
1✔
154
}
155

156
/// Import zones from a file
157
pub async fn import_zones(
×
158
    tx: mpsc::Sender<Command>,
159
    filename: &str,
160
    zone_name: Option<String>,
161
) -> Result<(), String> {
162
    let (tx_oneshot, mut rx_oneshot) = oneshot::channel();
×
163
    let msg = Command::ImportFile {
164
        filename: filename.to_string(),
×
165
        resp: tx_oneshot,
166
        zone_name,
167
    };
168
    if let Err(err) = tx.send(msg).await {
×
169
        error!("Failed to send message to datastore: {err:?}");
×
170
    }
171
    loop {
172
        let res = rx_oneshot.try_recv();
×
173
        match res {
×
174
            Err(error) => {
×
175
                if let oneshot::error::TryRecvError::Closed = error {
×
176
                    break;
×
177
                }
178
            }
179
            Ok(()) => break,
×
180
        };
181
        sleep(std::time::Duration::from_micros(500)).await;
×
182
    }
183
    Ok(())
×
184
    // rx_oneshot.await.map_err(|e| format!("Failed to receive result: {e:?}"))
185
}
186

187
/// Presents the CLI UI to add an admin user.
188
pub async fn add_admin_user(tx: mpsc::Sender<Command>) -> Result<(), ()> {
×
189
    // prompt for the username
190
    println!("Creating admin user, please enter their username from the identity provider");
×
191
    let username: String = Input::with_theme(&ColorfulTheme::default())
×
192
        .with_prompt("Username")
193
        .interact_text()
194
        .map_err(|e| {
×
195
            error!("Failed to get username from user: {e:?}");
×
196
        })?;
197

198
    println!(
199
        "The authentication reference is the unique user identifier in the Identity Provider."
200
    );
201
    let authref: String = Input::with_theme(&ColorfulTheme::default())
×
202
        .with_prompt("Authentication Reference:")
203
        .interact_text()
204
        .map_err(|e| {
×
205
            error!("Failed to get auth reference from user: {e:?}");
×
206
        })?;
207

208
    println!(
209
        r#"
210

211
Creating the following user:
212

213

214
Username: {username}
215
Authref:  {authref}
216

217
"#
218
    );
219
    // show the details and confirm them
220
    let confirm = Confirm::with_theme(&ColorfulTheme::default())
221
        .with_prompt("Do these details look correct?")
222
        .interact_opt();
223

224
    match confirm {
×
225
        Ok(Some(true)) => {}
×
226
        Ok(Some(false)) | Ok(None) | Err(_) => {
227
            warn!("Cancelled user creation");
×
228
            return Err(());
229
        }
230
    }
231

232
    // create oneshot
233
    let (tx_oneshot, rx_oneshot) = oneshot::channel();
×
234

235
    let new_user = Command::CreateUser {
236
        username: username.clone(),
×
237
        authref: authref.clone(),
×
238
        admin: true,
239
        disabled: false,
240
        resp: tx_oneshot,
241
    };
242
    // send command
243
    if let Err(error) = tx.send(new_user).await {
×
244
        error!("Failed to send new user command for username {username:?}: {error:?}");
×
245
        return Err(());
246
    };
247
    // wait for the response
248
    match rx_oneshot.await {
×
249
        Ok(res) => match res {
×
250
            true => {
251
                info!("Successfully created user!");
×
252
                Ok(())
×
253
            }
254
            false => {
255
                error!("Failed to create user! Check datastore logs.");
×
256
                Err(())
257
            }
258
        },
259
        Err(error) => {
×
260
            debug!("Failed to rx result from datastore: {error:?}");
×
261
            Err(())
×
262
        }
263
    }
264
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc