Skip to content

Instantly share code, notes, and snippets.

@jeffguorg
Created May 12, 2023 07:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeffguorg/ab6ec485d2a074c68f6cc24769823e11 to your computer and use it in GitHub Desktop.
Save jeffguorg/ab6ec485d2a074c68f6cc24769823e11 to your computer and use it in GitHub Desktop.
#![feature(new_uninit)]
use backoff::ExponentialBackoff;
use futures::{pin_mut, TryStreamExt};
use hyper::{body::*, Body, Response};
use hyper_tls::HttpsConnector;
use kube::{api::ResourceExt, runtime::WatchStreamExt};
use base64::prelude::*;
use hmac::Mac;
use pem::EncodeConfig;
use serde::{Deserialize, Serialize};
use tokio::{
io::{stdout, AsyncWriteExt},
sync::Mutex,
};
use std::sync::Arc;
type HyperClient = hyper::client::Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>>;
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
struct CertList {
#[serde(default)]
marker: Option<String>,
certs: Vec<Cert>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
struct Cert {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
certid: Option<String>,
#[serde(default)]
name: String,
#[serde(default)]
common_name: String,
#[serde(default)]
pri: Option<String>,
#[serde(default)]
ca: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
dnsnames: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
not_before: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
not_after: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
create_time: Option<u64>,
}
struct Q {
access_key: String,
secret_key: String,
http_client: Arc<Mutex<HyperClient>>,
}
// construction
impl Q {
pub fn new(
access_id: String,
access_key: String,
http_client: Arc<Mutex<HyperClient>>,
) -> Self {
Self {
access_key: access_id,
secret_key: access_key,
http_client: http_client.clone(),
}
}
}
// generic request
impl Q {
fn request(
&self,
domain: &str,
path: &str,
method: &str,
body: Option<&[u8]>,
) -> anyhow::Result<hyper::Request<Body>> {
let uri = hyper::Uri::builder()
.scheme("https")
.authority(domain)
.path_and_query(path.clone())
.build()?;
let body = if let Some(body) = body { body } else { &[] };
let sign = self.sign(format!("{}\n", path))?;
let authorization = format!("QBox {}:{sign}", self.access_key);
Ok(hyper::Request::builder()
.method(method)
.uri(uri)
.header(
"Authorization",
hyper::header::HeaderValue::from_str(&authorization)?,
)
.body(Body::from(Vec::from(body)))?)
}
async fn do_request(
&self,
request: hyper::Request<hyper::Body>,
) -> Result<Response<Body>, hyper::Error> {
let client = self.http_client.lock().await;
let response = client.request(request).await?;
Ok(response)
}
fn sign(&self, signstr: String) -> anyhow::Result<String> {
let mut mac = hmac::Hmac::<sha1::Sha1>::new_from_slice(self.secret_key.as_bytes())?;
mac.update(signstr.as_bytes());
let result = mac.finalize().into_bytes();
let encoded_signature = BASE64_URL_SAFE.encode(result);
Ok(encoded_signature)
}
}
// certs api
impl Q {
pub async fn list_certs(&self) -> anyhow::Result<CertList> {
let request = self.request("api.qiniu.com", "/sslcert", "GET", None)?;
let response = self.do_request(request).await?;
if !response.status().is_success() {
return Err(anyhow::anyhow!(
"unexpected status code: {}",
response.status()
));
}
let bytes = hyper::body::to_bytes(response.into_body()).await?;
let slice = bytes.chunk();
let string = String::from_utf8_lossy(slice);
let cert_list: CertList = serde_json::from_str(string.to_string().as_str())?;
Ok(cert_list)
}
pub async fn delete_cert(&self, certid: String) -> anyhow::Result<()> {
let request = self.request(
"api.qiniu.com",
format!("/sslcert/{certid}").as_str(),
"DELETE",
None,
)?;
let response = self.do_request(request).await?;
if response.status().is_success() {
Ok(())
} else {
Err(anyhow::anyhow!(
"unexpected status code: {}",
response.status()
))
}
}
pub async fn upload_cert(
&self,
name: String,
common_name: String,
pri: String,
cert: String,
) -> anyhow::Result<()> {
let cert = Cert {
name,
common_name,
pri: Some(pri),
ca: cert,
..Default::default()
};
let body = serde_json::to_string(&cert)?;
println!("{body}");
let request = self.request(
"api.qiniu.com",
format!("/sslcert").as_str(),
"POST",
Some(body.as_bytes()),
)?;
let response = self.do_request(request).await?;
let status = response.status();
if status.is_success() {
Ok(())
} else {
let bytes = hyper::body::to_bytes(response.into_body()).await?;
Err(anyhow::anyhow!(
"unexpected status code({}): {}",
status,
String::from_utf8_lossy(&bytes),
))
}
}
}
#[test]
fn test_make_request() {
let example_access_key = "MY_ACCESS_KEY";
let example_secret_key = "MY_SECRET_KEY";
let example_domain = "rs.qiniu.com";
let example_path_query = "/move/bmV3ZG9jczpmaW5kX21hbi50eHQ=/bmV3ZG9jczpmaW5kLm1hbi50eHQ=";
let https = HttpsConnector::new();
let http_client = Arc::new(Mutex::new(hyper::Client::builder().build(https)));
let q = Q::new(
example_access_key.to_string(),
example_secret_key.to_string(),
http_client,
);
let request = q
.request(example_domain, example_path_query, "GET", None)
.unwrap();
let header = request.headers();
let auth_header = header.get("Authorization").map(|v| v.to_str().unwrap());
assert_eq!(
auth_header,
Some("QBox MY_ACCESS_KEY:FXsYh0wKHYPEsIAgdPD9OfjkeEM=")
);
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = kube::client::Client::try_default().await?;
let info = client.apiserver_version().await?;
println!("{info:?}");
let https = HttpsConnector::new();
let http_client = hyper::Client::builder().build(https);
let q = Q::new(
std::env::var("QINIU_ACCESS_KEY")?,
std::env::var("QINIU_SECRET_KEY")?,
Arc::new(Mutex::new(http_client)),
);
let secrets = kube::Api::<k8s_openapi::api::core::v1::Secret>::all(client);
let watcher = kube::runtime::watcher(secrets, kube::runtime::watcher::Config::default())
.backoff(ExponentialBackoff::default())
.applied_objects();
pin_mut!(watcher);
while let Ok(Some(secret)) = watcher.try_next().await {
let secret_type = match secret.type_.clone() {
Some(t) => t,
None => continue,
};
if secret_type != "kubernetes.io/tls" {
continue;
}
if let Some(issuer_name) = secret.annotations().get("cert-manager.io/issuer-name") {
if issuer_name != "letsencrypt-prod" {
continue;
}
} else {
continue;
}
let certlist = match q.list_certs().await {
Ok(certlist) => certlist,
Err(err) => {
eprintln!("err: {}", err);
continue;
}
};
let data = match secret.data.clone() {
Some(data) => data,
_ => {
continue;
}
};
let cert = match data.get("tls.crt") {
Some(cert) => String::from_utf8(cert.0.clone())?,
_ => {
continue;
}
};
let key = String::from_utf8(data.get("tls.key").unwrap().0.clone())?;
println!("{}/{}", secret.namespace().unwrap(), secret.name_any());
let common_name = secret
.annotations()
.get("cert-manager.io/common-name")
.unwrap();
let mut certs = match pem::parse_many(cert) {
Ok(certs) => certs,
Err(err) => {
eprintln!("err: {}", err);
continue;
}
};
certs.truncate(2);
let cert = pem::encode_many_config(
certs.as_slice(),
EncodeConfig {
line_ending: pem::LineEnding::LF,
},
);
println!("{cert}");
q.upload_cert(
format!("{}-{}", secret.namespace().unwrap(), secret.name_any()),
common_name.clone(),
key,
cert,
)
.await?;
}
Ok(())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment