Skip to content

Commit

Permalink
feat: implement rollout sync status poll
Browse files Browse the repository at this point in the history
  • Loading branch information
bbortt committed Aug 31, 2024
1 parent e3533e8 commit 7be999f
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 14 deletions.
105 changes: 94 additions & 11 deletions src/argo_cd.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use log::{debug, info, warn};
use reqwest::Client;
use reqwest::{Client, RequestBuilder};
use serde::Deserialize;
use std::env;
use std::thread::sleep;
use std::time::{Duration, Instant};
use tokio::runtime::{Builder, Runtime};
use urlencoding::encode;

Expand Down Expand Up @@ -41,15 +44,7 @@ impl ArgoCD {
);

let request_builder = self.client.post(url.as_str()).json("&body"); // TODO

let vault_token = env::var(ARGO_CD_TOKEN);
let request_builder = match vault_token {
Ok(token) => request_builder.header("Authorization", format!("Bearer {}", token)),
Err(_) => {
warn!("You're accessing ArgoCD without authentication (missing {} environment variable)", ARGO_CD_TOKEN);
request_builder
}
};
let request_builder = Self::enhance_with_authorization_token_if_applicable(request_builder);

let request = request_builder
.build()
Expand All @@ -71,12 +66,79 @@ impl ArgoCD {
}

pub(crate) fn wait_for_rollout(&mut self) {
let timeout_seconds = self.argo_config.sync_timeout_seconds.unwrap_or(60u16);
let timeout_seconds: u64 = match self.argo_config.sync_timeout_seconds {
Some(seconds) => seconds as u64,
None => 60,
};

info!(
"Waiting for rollout of ArgoCD application '{}' to finish - timeout is {} seconds",
self.argo_config.application, timeout_seconds
);

let url = format!(
"{}/api/v1/applications/{name}",
self.argo_config.base_url,
name = encode(self.argo_config.application.as_str())
);

let request_builder = self.client.get(url.as_str());
let request_builder = Self::enhance_with_authorization_token_if_applicable(request_builder);

let request = request_builder
.build()
.expect("Failed to build ArgoCD sync status request");

let start_time = Instant::now();
let timeout_duration = Duration::from_secs(timeout_seconds);

loop {
if start_time.elapsed() > timeout_duration {
panic!("Timeout reached while waiting for ArgoCD rollout to complete");
}

let response = self
.rt
.block_on(self.client.execute(request.try_clone().unwrap()))
.expect("Failed to get ArgoCD sync status");

if response.status().is_success() {
let app_information: Application = self
.rt
.block_on(response.json())
.expect("Failed to read ArgoCD sync status response");

if app_information.status.sync.status == "Synced"
&& app_information.status.health.status == "Healthy"
{
info!("Application rollout completed successfully");
return;
} else {
debug!(
"Application rollout not finished yet: {{ 'sync': '{}', 'health': '{}' }}",
app_information.status.sync.status, app_information.status.health.status
);
}
} else {
debug!("Failed to get application status: {}", response.status());
}

// Wait for 5 seconds before checking again
sleep(Duration::from_secs(5));
}
}

fn enhance_with_authorization_token_if_applicable(
request_builder: RequestBuilder,
) -> RequestBuilder {
let argocd_token = env::var(ARGO_CD_TOKEN);
match argocd_token {
Ok(token) => request_builder.header("Authorization", format!("Bearer {}", token)),
Err(_) => {
warn!("You're accessing ArgoCD without authentication (missing {} environment variable)", ARGO_CD_TOKEN);
request_builder
}
}
}

fn get_argocd_client(argo_config: ArgoConfig) -> Client {
Expand All @@ -90,5 +152,26 @@ impl ArgoCD {
}
}

#[derive(Deserialize)]
struct Application {
status: ApplicationStatus,
}

#[derive(Deserialize)]
struct ApplicationStatus {
sync: SyncStatus,
health: HealthStatus,
}

#[derive(Deserialize)]
struct SyncStatus {
status: String,
}

#[derive(Deserialize)]
struct HealthStatus {
status: String,
}

#[cfg(test)]
mod tests {}
3 changes: 1 addition & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub(crate) struct RotateArgs {
#[clap(flatten)] // Inherit arguments from BaseArgs
pub(crate) base: BaseArgs,

/// Whether the CLI should write a recovery log (contains sensitive information!) or not
/// The length of the randomly generated alphanumeric password
#[clap(short, long, default_value = "20")]
pub(crate) password_length: usize,

Expand All @@ -53,5 +53,4 @@ pub(crate) struct RotateArgs {
pub(crate) struct InitVaultArgs {
#[clap(flatten)] // Inherit arguments from BaseArgs
pub(crate) base: BaseArgs,
// Additional arguments for vault initialization (if any) can be added here.
}
81 changes: 80 additions & 1 deletion tests/rotate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ async fn rotate_secrets() {
"
argo_cd:
application: 'propeller'
danger_accept_insecure: true
base_url: 'http://127.0.0.1:{argocd_port}'
danger_accept_insecure: true
postgres:
host: '{postgres_host}'
port: {postgres_port}
Expand Down Expand Up @@ -169,6 +169,85 @@ async fn rotate_secrets() {
delete_argocd_deployment();
}

#[tokio::test]
#[timeout(120_000)]
async fn rotate_application_sync_timeout() {
deploy_argocd();

let postgres_container = common::postgres_container().await;

let postgres_host = postgres_container.get_host().await.unwrap().to_string();
let postgres_port = postgres_container
.get_host_port_ipv4(5432)
.await
.unwrap()
.to_string();

let vault_container = common::vault_container().await;

let vault_host = vault_container.get_host().await.unwrap();
let vault_port = vault_container.get_host_port_ipv4(8200).await.unwrap();

let http_client = Client::new();

let vault_url = format!("http://{vault_host}:{vault_port}/v1/secret/data/rotate/secrets");
reset_vault_secret_path(&http_client, vault_url.as_str()).await;

let mut postgres_client = connect_postgres_client(
postgres_host.as_str(),
postgres_port.as_str(),
"demo",
"demo_password",
)
.await;

reset_role_initial_password(&mut postgres_client, "user1").await;
reset_role_initial_password(&mut postgres_client, "user2").await;

let (argocd_port, mut port_forward) = open_argocd_server_port_forward();
let argocd_url = format!("http://localhost:{}", argocd_port);

let argocd_token = get_argocd_access_token(argocd_url.as_str()).await;
create_argocd_application(argocd_url.as_str(), argocd_token.as_str()).await;

Command::new(&*BIN_PATH)
.arg("rotate")
.arg("-c")
.arg(common::write_string_to_tempfile(
format!(
// language=yaml
"
argo_cd:
application: 'propeller'
base_url: 'http://127.0.0.1:{argocd_port}'
danger_accept_insecure: true
sync_timeout_seconds: 5
postgres:
host: '{postgres_host}'
port: {postgres_port}
database: 'demo'
vault:
base_url: 'http://{vault_host}:{vault_port}'
path: 'rotate/secrets'
"
)
.as_str(),
))
.env("ARGO_CD_TOKEN", argocd_token)
.env("VAULT_TOKEN", "root-token")
.assert()
.failure()
.stderr(contains(
// The configured sync timeout of 5 seconds is no match for the 10 seconds sleep in the pre-sync hook
"Timeout reached while waiting for ArgoCD rollout to complete",
));

// Kill `kubectl port-forward` process
let _ = port_forward
.kill()
.expect("Failed to stop port forward-process");
}

#[tokio::test]
#[timeout(30_000)]
async fn rotate_invalid_initialized_secret() {
Expand Down

0 comments on commit 7be999f

Please sign in to comment.