Skip to content

Commit

Permalink
[suiop] add support for deploy key automation (#17509)
Browse files Browse the repository at this point in the history
## Description 

With this change, we can now create/recreate/delete deploy keys for our
users and the private keys will be stored securely instead of on users'
laptop.
suiop here is just a client, all it does is authn the user and use the
returned Okta token to the infra-metadata-service endpoint for ssh key
generation.

## Test plan 

Manually tested, for example, to create a deploy key:
`suiop ci key create --repo-name suiop-test`
```
Public Key:
-------------------------

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPn4Rj84+AGRPJNx2QVV7R8gURJHxicC0YW1UKWLOlz0 pulumi_automation_deploy_key

-------------------------

Please add the public key above to your repository suiop-test via the link below:
https://github.com/MystenLabs/suiop-test/settings/keys/new
```
A new key will be created automatically and user will be prompted to put
the corresponding pubkey to their private repo so that we can have
control over it.

---

## Release notes

Check each box that your changes affect. If none of the boxes relate to
your changes, release notes aren't required.

For each box you select, include information after the relevant heading
that describes the impact of your changes that a user might notice and
any actions they must take to implement updates.

- [ ] Protocol: 
- [ ] Nodes (Validators and Full nodes): 
- [ ] Indexer: 
- [ ] JSON-RPC: 
- [ ] GraphQL: 
- [ ] CLI: 
- [ ] Rust SDK:
  • Loading branch information
pei-mysten authored May 7, 2024
1 parent ef49d37 commit aa86996
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 17 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/suiop-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ edition = "2021"
license = "Apache-2.0"
name = "suiop-cli"
publish = false
version = "0.1.9"
version = "0.2.0"

[lib]
name = "suioplib"
Expand Down
146 changes: 146 additions & 0 deletions crates/suiop-cli/src/cli/ci/key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use crate::cli::lib::{get_api_server, get_oauth_token};
use anyhow::Result;

use clap::Parser;
use colored::Colorize;
use tracing::debug;

#[derive(Parser, Debug)]
pub struct KeyArgs {
#[command(subcommand)]
action: KeyAction,
}

#[derive(clap::Subcommand, Debug)]
pub enum KeyAction {
#[command(name = "create")]
Create {
#[arg(short, long)]
repo_name: String,
},
#[command(name = "recreate")]
ReCreate {
#[arg(short, long)]
repo_name: String,
},
#[command(name = "delete")]
Delete {
#[arg(short, long)]
repo_name: String,
},
}

#[derive(serde::Serialize)]
struct KeyRequest {
repo_name: String,
}

const ENDPOINT: &str = "/automation/deploy-key";

pub async fn key_cmd(args: &KeyArgs) -> Result<()> {
let token = get_oauth_token().await?;
debug!("token: {}", token.access_token);
send_key_request(&token.access_token, &args.action).await?;

Ok(())
}

#[derive(serde::Deserialize)]
struct KeyResponse {
pub_key: Option<String>,
message: String,
}

async fn send_key_request(token: &str, action: &KeyAction) -> Result<()> {
let req = generate_key_request(token, action);

println!(
"Processing request... Please wait patiently. It may take about 20 seconds to complete."
);
let resp = req.send().await?;
debug!("resp: {:?}", resp);

let status = resp.status();
let json_resp = resp.json::<KeyResponse>().await?;

if status.is_success() {
match action {
KeyAction::Create { repo_name } | KeyAction::ReCreate { repo_name } => {
if let Some(pubkey) = json_resp.pub_key {
let add_key_link = format!(
"https://github.com/MystenLabs/{}/settings/keys/new",
repo_name
);
println!(
r#"Public Key:
-------------------------
{}
-------------------------
Please add the public key above to your repository {} via the link below:
{}"#,
pubkey.yellow(),
repo_name.bright_purple(),
add_key_link.yellow()
)
} else {
return Err(anyhow::anyhow!(
"Failed to get public key for repo {}.",
repo_name.bright_purple()
));
}
}
KeyAction::Delete { repo_name } => {
println!("Key for repo {} deleted", repo_name.bright_purple());
}
}
Ok(())
} else {
Err(anyhow::anyhow!(
"Failed to manage keys: {} - {}",
status,
json_resp.message.yellow()
))
}
}

fn generate_headers_with_auth(token: &str) -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::AUTHORIZATION,
reqwest::header::HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(),
);
headers
}

fn generate_key_request(token: &str, action: &KeyAction) -> reqwest::RequestBuilder {
let client = reqwest::Client::new();
let api_server = get_api_server();
let full_url = format!("{}{}", api_server, ENDPOINT);
debug!("full_url: {}", full_url);
let req = match action {
KeyAction::Create { repo_name } => client
.post(full_url)
.headers(generate_headers_with_auth(token))
.json(&KeyRequest {
repo_name: repo_name.to_string(),
}),
KeyAction::ReCreate { repo_name } => client
.put(full_url)
.headers(generate_headers_with_auth(token))
.json(&KeyRequest {
repo_name: repo_name.to_string(),
}),
KeyAction::Delete { repo_name } => client
.delete(full_url)
.headers(generate_headers_with_auth(token))
.json(&KeyRequest {
repo_name: repo_name.to_string(),
}),
};
debug!("req: {:?}", req);

req
}
31 changes: 31 additions & 0 deletions crates/suiop-cli/src/cli/ci/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

mod key;

use anyhow::Result;
use key::key_cmd;

use clap::Parser;

use self::key::KeyArgs;

#[derive(Parser, Debug)]
pub struct CIArgs {
#[command(subcommand)]
action: CIAction,
}

#[derive(clap::Subcommand, Debug)]
pub(crate) enum CIAction {
#[clap(aliases = ["k", "key"])]
Keys(KeyArgs),
}

pub async fn ci_cmd(args: &CIArgs) -> Result<()> {
match &args.action {
CIAction::Keys(keys) => key_cmd(keys).await?,
}

Ok(())
}
15 changes: 7 additions & 8 deletions crates/suiop-cli/src/cli/iam/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@

mod whoami;

use crate::cli::lib::get_oauth_token;
use crate::cli::lib::{get_api_server, get_oauth_token};
use anyhow::Result;
use clap::Parser;
use tracing::{error, info};

const API_SERVER: &str = "https://meta-svc.api.mystenlabs.com";
use colored::Colorize;
use tracing::error;

#[derive(Parser, Debug, Clone)]
pub struct IAMArgs {
Expand All @@ -19,19 +18,19 @@ pub struct IAMArgs {
#[derive(clap::Subcommand, Debug, Clone)]
pub enum IAMAction {
#[command(name = "whoami", aliases=["w"])]
WhoAmI {},
WhoAmI,
}

pub async fn iam_cmd(args: &IAMArgs) -> Result<()> {
match &args.action {
IAMAction::WhoAmI {} => {
IAMAction::WhoAmI => {
let token_resp = get_oauth_token().await;
match token_resp {
Ok(token) => {
let resp = whoami::get_identity(API_SERVER, &token.access_token).await;
let resp = whoami::get_identity(&get_api_server(), &token.access_token).await;
match resp {
Ok(username) => {
info!("You are: {}", username);
println!("You are: {}", username.bright_purple());
Ok(())
}
Err(e) => {
Expand Down
8 changes: 3 additions & 5 deletions crates/suiop-cli/src/cli/iam/whoami.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use std::collections::HashMap;

use anyhow::Result;
use reqwest;
use tracing::debug;
Expand All @@ -13,10 +11,10 @@ pub async fn get_identity(base_url: &str, token: &str) -> Result<String> {
let full_url = format!("{}{}", base_url, ENDPOINT);
debug!("full_url: {}", full_url);
let client = reqwest::Client::new();
let mut body = HashMap::new();
body.insert("token", token);

let req = client.post(full_url).json(&body);
let req = client
.get(full_url)
.header("Authorization", format!("Bearer {}", token));
debug!("req: {:?}", req);

let resp = req.send().await?;
Expand Down
9 changes: 9 additions & 0 deletions crates/suiop-cli/src/cli/lib/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,12 @@
mod oauth;

pub use oauth::get_oauth_token;

pub fn get_api_server() -> String {
// if env var is set, use that
if let Ok(api_server) = std::env::var("API_SERVER") {
return api_server.to_string();
}

"https://meta-svc.api.mystenlabs.com".to_string()
}
2 changes: 2 additions & 0 deletions crates/suiop-cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

mod ci;
pub mod docker;
mod iam;
mod incidents;
mod lib;
pub mod pulumi;
pub mod service;

pub use ci::{ci_cmd, CIArgs};
pub use docker::{docker_cmd, DockerArgs};
pub use iam::{iam_cmd, IAMArgs};
pub use incidents::{incidents_cmd, IncidentsArgs};
Expand Down
9 changes: 7 additions & 2 deletions crates/suiop-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
use anyhow::Result;
use clap::Parser;
use suioplib::cli::{
docker_cmd, iam_cmd, incidents_cmd, pulumi_cmd, service_cmd, DockerArgs, IAMArgs,
IncidentsArgs, PulumiArgs, ServiceArgs,
ci_cmd, docker_cmd, iam_cmd, incidents_cmd, pulumi_cmd, service_cmd, CIArgs, DockerArgs,
IAMArgs, IncidentsArgs, PulumiArgs, ServiceArgs,
};
use tracing_subscriber::{
filter::{EnvFilter, LevelFilter},
Expand All @@ -32,6 +32,8 @@ pub(crate) enum Resource {
Pulumi(PulumiArgs),
#[clap(aliases = ["s", "svc"])]
Service(ServiceArgs),
#[clap()]
CI(CIArgs),
}

#[tokio::main(flavor = "current_thread")]
Expand Down Expand Up @@ -63,6 +65,9 @@ async fn main() -> Result<()> {
Resource::Service(args) => {
service_cmd(&args).await?;
}
Resource::CI(args) => {
ci_cmd(&args).await?;
}
}

Ok(())
Expand Down

0 comments on commit aa86996

Please sign in to comment.