|
| 1 | +use std::collections::BTreeMap; |
| 2 | + |
| 3 | +use base64::Engine; |
| 4 | +use k8s_openapi::api::core::v1::Secret; |
| 5 | +use kube::{Api, Resource, ResourceExt, api::DeleteParams}; |
| 6 | +use rand::{RngCore, SeedableRng, rngs::StdRng}; |
| 7 | +use snafu::{OptionExt, ResultExt, Snafu}; |
| 8 | + |
| 9 | +use crate::{builder::meta::ObjectMetaBuilder, client::Client}; |
| 10 | + |
| 11 | +#[derive(Snafu, Debug)] |
| 12 | +pub enum Error { |
| 13 | + #[snafu(display("object defines no namespace"))] |
| 14 | + ObjectHasNoNamespace, |
| 15 | + |
| 16 | + #[snafu(display("failed to retrieve random secret"))] |
| 17 | + RetrieveRandomSecret { source: crate::client::Error }, |
| 18 | + |
| 19 | + #[snafu(display("failed to create random secret"))] |
| 20 | + CreateRandomSecret { source: crate::client::Error }, |
| 21 | + |
| 22 | + #[snafu(display("failed to delete random secret"))] |
| 23 | + DeleteRandomSecret { source: kube::Error }, |
| 24 | + |
| 25 | + #[snafu(display("object is missing metadata to build owner reference"))] |
| 26 | + ObjectMissingMetadataForOwnerRef { source: crate::builder::meta::Error }, |
| 27 | +} |
| 28 | + |
| 29 | +/// This function creates a random Secret if it doesn't already exist. |
| 30 | +/// |
| 31 | +/// As this function generates random Secret contents, if the Secret already exists, it will *not* |
| 32 | +/// be patched, as otherwise we would generate new Secret contents on every reconcile. Which would |
| 33 | +/// in turn cause Pods restarts, which would cause reconciles ;) |
| 34 | +/// |
| 35 | +/// However, there is one special handling needed: |
| 36 | +/// |
| 37 | +/// We can't mark Secrets as immutable, as this caused problems, see https://github.com/stackabletech/issues/issues/843. |
| 38 | +/// As Secrets have been created as immutable up to SDP release 26.3.0, we need to delete the, to be |
| 39 | +/// able to re-create them as mutable. This function detects old (immutable) Secrets and re-creates |
| 40 | +/// them as mutable. The contents of the Secret will be kept to prevent unnecessary Secret content |
| 41 | +/// changes. |
| 42 | +// |
| 43 | +// TODO: This can be removed in a future SDP release, likely 26.11, as all Secrets have been migrated. |
| 44 | +pub async fn create_random_secret_if_not_exists<R>( |
| 45 | + secret_name: &str, |
| 46 | + secret_key: &str, |
| 47 | + secret_size_bytes: usize, |
| 48 | + stacklet: &R, |
| 49 | + client: &Client, |
| 50 | +) -> Result<(), Error> |
| 51 | +where |
| 52 | + R: Resource<DynamicType = ()>, |
| 53 | +{ |
| 54 | + let secret_namespace = stacklet.namespace().context(ObjectHasNoNamespaceSnafu)?; |
| 55 | + let existing_secret = client |
| 56 | + .get_opt::<Secret>(secret_name, &secret_namespace) |
| 57 | + .await |
| 58 | + .context(RetrieveRandomSecretSnafu)?; |
| 59 | + |
| 60 | + match existing_secret { |
| 61 | + Some( |
| 62 | + existing_secret @ Secret { |
| 63 | + immutable: Some(true), |
| 64 | + .. |
| 65 | + }, |
| 66 | + ) => { |
| 67 | + tracing::info!( |
| 68 | + k8s.secret.name = secret_name, |
| 69 | + k8s.secret.namespace = secret_namespace, |
| 70 | + "Old (immutable) random Secret detected, re-creating it to be able to make it mutable. The contents will stay the same." |
| 71 | + ); |
| 72 | + Api::<Secret>::namespaced(client.as_kube_client(), &secret_namespace) |
| 73 | + .delete(secret_name, &DeleteParams::default()) |
| 74 | + .await |
| 75 | + .context(DeleteRandomSecretSnafu)?; |
| 76 | + |
| 77 | + let mut mutable_secret = existing_secret; |
| 78 | + mutable_secret.immutable = Some(false); |
| 79 | + // Prevent "ApiError: resourceVersion should not be set on objects to be created" |
| 80 | + mutable_secret.metadata.resource_version = None; |
| 81 | + |
| 82 | + client |
| 83 | + .create(&mutable_secret) |
| 84 | + .await |
| 85 | + .context(CreateRandomSecretSnafu)?; |
| 86 | + |
| 87 | + // Note: restart-controller will restart all Pods mounting this Secret, as it has |
| 88 | + // changed. |
| 89 | + } |
| 90 | + Some(_) => { |
| 91 | + tracing::debug!( |
| 92 | + k8s.secret.name = secret_name, |
| 93 | + k8s.secret.namespace = secret_namespace, |
| 94 | + "Existing (mutable) random Secret detected, nothing to do" |
| 95 | + ); |
| 96 | + } |
| 97 | + None => { |
| 98 | + tracing::info!( |
| 99 | + k8s.secret.name = secret_name, |
| 100 | + k8s.secret.namespace = secret_namespace, |
| 101 | + "Random Secret missing, creating it" |
| 102 | + ); |
| 103 | + let secret = Secret { |
| 104 | + metadata: ObjectMetaBuilder::new() |
| 105 | + .name(secret_name) |
| 106 | + .namespace_opt(stacklet.namespace()) |
| 107 | + .ownerreference_from_resource(stacklet, None, Some(true)) |
| 108 | + .context(ObjectMissingMetadataForOwnerRefSnafu)? |
| 109 | + .build(), |
| 110 | + string_data: Some(BTreeMap::from([( |
| 111 | + secret_key.to_string(), |
| 112 | + get_random_base64(secret_size_bytes), |
| 113 | + )])), |
| 114 | + ..Secret::default() |
| 115 | + }; |
| 116 | + client |
| 117 | + .create(&secret) |
| 118 | + .await |
| 119 | + .context(CreateRandomSecretSnafu)?; |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + Ok(()) |
| 124 | +} |
| 125 | + |
| 126 | +/// Generates a cryptographically secure base64 String with the specified size in bytes. |
| 127 | +fn get_random_base64(size_bytes: usize) -> String { |
| 128 | + // As we are using the OS rng, we are using `getrandom`, which should be cryptographically |
| 129 | + // secure |
| 130 | + let mut rng = StdRng::from_os_rng(); |
| 131 | + |
| 132 | + let mut bytes = vec![0u8; size_bytes]; |
| 133 | + rng.fill_bytes(&mut bytes); |
| 134 | + |
| 135 | + base64::engine::general_purpose::STANDARD.encode(bytes) |
| 136 | +} |
0 commit comments