Created
May 12, 2023 07:17
-
-
Save jeffguorg/ab6ec485d2a074c68f6cc24769823e11 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#![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