Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[COR-166] Generate password and create secret #65

Merged
merged 14 commits into from
Jan 26, 2023
47 changes: 47 additions & 0 deletions coredb-operator/Cargo.lock

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

1 change: 1 addition & 0 deletions coredb-operator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ opentelemetry = { version = "0.18.0", features = ["trace", "rt-tokio"] }
opentelemetry-otlp = { version = "0.11.0", features = ["tokio"], optional = true }
tonic = { version = "0.8.3", optional = true }
thiserror = "1.0.37"
passwords = "3.1.12"

[dev-dependencies]
assert-json-diff = "2.0.2"
Expand Down
6 changes: 6 additions & 0 deletions coredb-operator/src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use kube::{
CustomResource, Resource,
};

use crate::secret::reconcile_secret;
use k8s_openapi::api::core::v1::Pod;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -116,6 +117,11 @@ impl CoreDB {
.await
.map_err(Error::KubeError)?;

// reconcile secret
reconcile_secret(self, ctx.clone())
.await
.expect("error reconciling secret");

// reconcile statefulset
reconcile_sts(self, ctx.clone())
.await
Expand Down
37 changes: 34 additions & 3 deletions coredb-operator/src/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use assert_json_diff::assert_json_include;
use futures::pin_mut;
use http::{Request, Response};
use hyper::{body::to_bytes, Body};
use kube::{Client, Resource, ResourceExt};
use k8s_openapi::api::core::v1::Secret;
use kube::{core::ObjectList, Client, Resource, ResourceExt};
use prometheus::Registry;
use std::sync::Arc;
use tokio::task::JoinHandle;
Expand Down Expand Up @@ -98,7 +99,37 @@ impl ApiServerVerifier {
// pass through coredb "patch accepted"
send.send_response(Response::builder().body(Body::from(response)).unwrap());

// After the PATCH to CoreDB, we expect a PATCH to StatefulSet
// After the PATCH to CoreDB, we expect a GET on Secrets
let (request, send) = handle
.next_request()
.await
.expect("Kube API called to GET Secret");
assert_eq!(request.method(), http::Method::GET);
assert_eq!(
request.uri().to_string(),
format!("/api/v1/namespaces/testns/secrets?&labelSelector=app%3Dcoredb")
);
// We need to send an empty ObjectList<Secret> back as our response
let obj: ObjectList<Secret> = ObjectList {
metadata: Default::default(),
items: vec![],
};
let response = serde_json::to_vec(&obj).unwrap();
send.send_response(Response::builder().body(Body::from(response)).unwrap());
// After the GET on Secrets, we expect a PATCH to Secret
let (request, send) = handle
.next_request()
.await
.expect("Kube API called to PATCH Secret");
assert_eq!(request.method(), http::Method::PATCH);
assert_eq!(
request.uri().to_string(),
format!(
"/api/v1/namespaces/testns/secrets/testdb-connection?&force=true&fieldManager=cntrlr"
)
);
send.send_response(Response::builder().body(request.into_body()).unwrap());
// After the PATCH to Secret, we expect a PATCH to StatefulSet
let (request, send) = handle
.next_request()
.await
Expand All @@ -111,7 +142,7 @@ impl ApiServerVerifier {
)
);
send.send_response(Response::builder().body(request.into_body()).unwrap());
// After the PATCH to CoreDB, we expect a PATCH to Service
// After the PATCH to StatefulSet, we expect a PATCH to Service
let (request, send) = handle
.next_request()
.await
Expand Down
1 change: 1 addition & 0 deletions coredb-operator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub use metrics::Metrics;
mod defaults;
#[cfg(test)] pub mod fixtures;
mod psql;
mod secret;
mod service;
mod statefulset;
use thiserror::Error;
Expand Down
106 changes: 106 additions & 0 deletions coredb-operator/src/secret.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use crate::{Context, CoreDB, Error};
use k8s_openapi::{api::core::v1::Secret, apimachinery::pkg::apis::meta::v1::ObjectMeta, ByteString};
use kube::{
api::{ListParams, Patch, PatchParams},
Api, Resource, ResourceExt,
};
use passwords::PasswordGenerator;
use std::{collections::BTreeMap, sync::Arc};
use tracing::debug;

pub async fn reconcile_secret(cdb: &CoreDB, ctx: Arc<Context>) -> Result<(), Error> {
let client = ctx.client.clone();
let ns = cdb.namespace().unwrap();
let name = format!("{}-connection", cdb.name_any());
let mut labels: BTreeMap<String, String> = BTreeMap::new();
let secret_api: Api<Secret> = Api::namespaced(client, &ns);
let oref = cdb.controller_owner_ref(&()).unwrap();
labels.insert("app".to_owned(), "coredb".to_owned());

// check for existing secret
let lp = ListParams::default().labels("app=coredb");
let secrets = secret_api.list(&lp).await.expect("could not get Secrets");

// if the secret has already been created, return (avoids overwriting password value)
if !secrets.items.is_empty() {
for s in &secrets.items {
if s.name_any() == name {
debug!("skipping secret creation: secret {} exists", &name);
return Ok(());
}
}
}

// generate secret data
let data = secret_data(&cdb, &name, &ns);

let secret: Secret = Secret {
metadata: ObjectMeta {
name: Some(name.to_owned()),
namespace: Some(ns.to_owned()),
labels: Some(labels.clone()),
owner_references: Some(vec![oref]),
..ObjectMeta::default()
},
data: Some(data),
..Secret::default()
};

let ps = PatchParams::apply("cntrlr").force();
let _o = secret_api
.patch(&name, &ps, &Patch::Apply(&secret))
.await
.map_err(Error::KubeError)?;
Ok(())
}

fn secret_data(cdb: &CoreDB, name: &str, ns: &str) -> BTreeMap<String, ByteString> {
let mut data = BTreeMap::new();

// encode and insert user into secret data
let user = "postgres".to_owned();
let b64_user = b64_encode(&user);
data.insert("user".to_owned(), b64_user);

// encode and insert password into secret data
let password = generate_password();
let b64_password = b64_encode(&password);
data.insert("password".to_owned(), b64_password);

// encode and insert port into secret data
let port = cdb.spec.port.to_string();
let b64_port = b64_encode(&port);
data.insert("port".to_owned(), b64_port);

// encode and insert host into secret data
let host = format!("{}.{}.svc.cluster.local", &name, &ns);
let b64_host = b64_encode(&host);
data.insert("host".to_owned(), b64_host);

// encode and insert uri into secret data
let uri = format!("postgresql://{}:{}@{}:{}", &user, &password, &host, &port);
let b64_uri = b64_encode(&uri);
data.insert("uri".to_owned(), b64_uri);
ianstanton marked this conversation as resolved.
Show resolved Hide resolved

data
}

fn b64_encode(string: &str) -> ByteString {
let bytes_vec = string.as_bytes().to_vec();
let byte_string = ByteString(bytes_vec);
byte_string
}

fn generate_password() -> String {
let pg = PasswordGenerator {
length: 8,
ianstanton marked this conversation as resolved.
Show resolved Hide resolved
numbers: true,
lowercase_letters: true,
uppercase_letters: true,
symbols: false,
spaces: false,
exclude_similar_characters: false,
strict: true,
};
pg.generate_one().unwrap()
}
16 changes: 12 additions & 4 deletions coredb-operator/src/statefulset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use k8s_openapi::{
api::{
apps::v1::{StatefulSet, StatefulSetSpec},
core::v1::{
Container, ContainerPort, EnvVar, ExecAction, PersistentVolumeClaim, PersistentVolumeClaimSpec,
PodSpec, PodTemplateSpec, Probe, ResourceRequirements, SecurityContext, VolumeMount,
Container, ContainerPort, EnvVar, EnvVarSource, ExecAction, PersistentVolumeClaim,
PersistentVolumeClaimSpec, PodSpec, PodTemplateSpec, Probe, ResourceRequirements,
SecretKeySelector, SecurityContext, VolumeMount,
},
},
apimachinery::pkg::{api::resource::Quantity, apis::meta::v1::LabelSelector},
Expand All @@ -28,8 +29,15 @@ pub fn stateful_set_from_cdb(cdb: &CoreDB) -> StatefulSet {

let postgres_env = Some(vec![EnvVar {
name: "POSTGRES_PASSWORD".to_owned(),
value: Some("password".to_owned()),
value_from: None,
value: None,
value_from: Some(EnvVarSource {
secret_key_ref: Some(SecretKeySelector {
key: "password".to_string(),
name: Some(format!("{}-connection", &name)),
optional: None,
}),
..EnvVarSource::default()
}),
}]);

let postgres_volume_mounts = Some(vec![VolumeMount {
Expand Down