diff --git a/hack/dummy_trust_bundle.pem b/hack/dummy_trust_bundle.pem new file mode 100644 index 00000000..177a60d0 --- /dev/null +++ b/hack/dummy_trust_bundle.pem @@ -0,0 +1,48 @@ +# Foo +-----BEGIN CERTIFICATE----- +MIIDZTCCAk2gAwIBAgIUP+AxIkXJXTEhNGLH2qjmE6Gp0fowDQYJKoZIhvcNAQEL +BQAwQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE +CgwTRGVmYXVsdCBDb21wYW55IEx0ZDAeFw0yNDAzMDExMDIyNTlaFw0yNTAzMDEx +MDIyNTlaMEIxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAa +BgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQC0wzg+7X2Amb5g60g0TstLgC0XnJRZq/YZUUsJMmm3qMb/+GYJ +AJzHxiycUfbRJtYvjx0SBmAX/kDRVCEQKcN5d/y3zeq709YO40kvouScfstsxM8l +PFLOmM8/Dqey1WblSJERBLbLherDnMwR7EMXkyZ/AfHUXmhVoIZE9ywsZpNcVW6Z +7x/+Izbj1s305vrxEkZDw6b3oMG5uooQgP5NZFXSamzJgviP0L/usvbRMtAWphoj +WhMeNuOdymLwRzm2l+2Qp/JDWktgHccmrbbi1c6pwhsIJBj4KOyb9zROTnYXyS/j +0b7GzVcffveV6E58rGa2ILyIsCv6gt8LgFnxAgMBAAGjUzBRMB0GA1UdDgQWBBQ5 +nh0SeZxZ969ps+9ywPEoOVasxTAfBgNVHSMEGDAWgBQ5nh0SeZxZ969ps+9ywPEo +OVasxTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAWxsqfdm8h +AeNY8vPcRdtB9KYU5sZLs4NtlBFSdn+pHmeXZwwkjQDNhVcEMKdpZI4BpS11Ggwh +1d3BCCC/5M6yVqm+EMKJvA9VCeM8d1WJ1yVyXcgGuegf8kr3v+lr7Ll59qZGP5Ir +WwE8WRns7uFOCqYJCxo1VFXitZZuIugr3NUSimBPoJf1hDYdye3K3Q+grF2GyNII +5Yo+/VSR4ejIvJYAFp91Ycep7S0/+qhFpsjEG0Qw3Ly6WqQoCqdmIsyqFgWHsIlY +oJxV5wTX/c9DDZLR0VUD19aDV3B9kb7Cf+h7S4RsORWCyi7+58FKkkD6Ryc0I1K6 +xw3RWhfd9o1d +-----END CERTIFICATE----- + + + +# All +# the Bars +-----BEGIN CERTIFICATE----- +MIIDZTCCAk2gAwIBAgIULnisjJLte3Vvt4o1f+5vSQg542cwDQYJKoZIhvcNAQEL +BQAwQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE +CgwTRGVmYXVsdCBDb21wYW55IEx0ZDAeFw0yNDAzMDExMDI1MDFaFw0yNTAzMDEx +MDI1MDFaMEIxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAa +BgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQC2dhK7xTnoTB3wN1l3NsLTp5YR0KFfBTjMcDgSzUy/GN79c2cF +JzSuiYUi7SCmFjn3soNqpXHFzCox6KIs9R6PL4epaQM76EVG/Xy6mdDvFnZvqypi +wmK6J0AGajOxItYUGb2a3Zmt/2nliW6t8sW/vhovHRu7YROo4uJygIp2UUFct2Lk +8C7XkJX5RXW+sKTiNddIjhmDFD0vHfvNvQ6AIayJTmXy272+aqYNJWB2wS/2uD3Z ++WOpiINetCtkASoiE7nzBQw+WsTfeFJH2TnI5pnSaHdLRUQtzoLO0/FgQ5WBfJg5 +aH03DLfQ9GEdzlsOkPOEgHXqDFMjTQCwcue3AgMBAAGjUzBRMB0GA1UdDgQWBBRd +0Zs+cm0gPHGKoQrerC18Pa3B3zAfBgNVHSMEGDAWgBRd0Zs+cm0gPHGKoQrerC18 +Pa3B3zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAepPrWqB9h +JkqtgJrP8SkQVulTVKYj66J5JxM5vZR96Z4UnbA3WNxezev0jMCYuV0twHPN8avs +Jern+/n7vgQ3ziiLVdtrN8PqK1X1apSurVmaiIw4tRcv5TVL5OD95sTyJh5bUBpM +DGtCTraPZxLIDKm9byunobXtJVcutw4oHKtFy/LlFWePCnvFzvx6ZFswLAXgxhf9 +EtjDf3v0cjDn9yRzjYFrwHiQ53A75YTwFyk21q7Gh1G0yspfBeq7cej2wK1PnfiC +42TI0UzcqRV4CWDoARMSV8yMLajZ0g1eEreUprwmFcOy17V7KCeV6E8lKb21OU8M +Ad9q3H0iXjct +-----END CERTIFICATE----- diff --git a/hack/dummy_use_cert.crt b/hack/dummy_use_cert.crt new file mode 100644 index 00000000..efbed8d2 --- /dev/null +++ b/hack/dummy_use_cert.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICyzCCAbMCFAoie5EUqnUAHimqxbJBHV0MGVbwMA0GCSqGSIb3DQEBCwUAMCIx +IDAeBgNVBAMMF2FkbWluLWt1YmVjb25maWctc2lnbmVyMB4XDTI0MDEwOTEzMTky +NVoXDTI0MDIwODEzMTkyNVowIjEgMB4GA1UEAwwXYWRtaW4ta3ViZWNvbmZpZy1z +aWduZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2fz96uc8fDoNV +RaBB9iQ+i5Y76IZf0XOdGID8WVaqPlqH+NgLUaFa39T+78FhZW3794Lbeyu/PnYT +ufMyKnJEulVO7W7gPHaqWyuN08/m6SH5ycTEgUAXK1q1yVR/vM6HnV/UPUCfbDaW +RFOrUgGNwNywhEjqyzyUxJFixxS6Rk7JmouROD2ciNhBn6wNFByVHN9j4nQUOhXC +A0JjuiPH7ybvcHjmg3mKDJusyVq4pl0faahOxn0doILfXaHHwRxyEnP3V3arpPer +FvwlHh2Cfat+ijFPSD9pN3KmoeAviOHZVLQ/jKzkQvzlvva3mhEpLE5Zje1lMpvq +fjDheW9bAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAC7oi/Ht0lidcx6XvOBz6W1m +LU02e2yHuDzw6E3WuNoqAdPpleFRV4mLDnv8mEavH5sje0L5veHtOq3Ny4pc06B+ +ETB2aCW4GQ4mPvN9Jyi6sxLQQaVLpFrtPPB08NawNbbcYWUrAihO1uIXLhaCYZWw +H3aWlqRvGECazYZIPcFoV20jygrcwMhixSZjYyHhJN0LYO5sjiKcMnI8EkHuqE17 +7CPogicZte+m49Mo+f7b8asmKBSafdTUSVAt9Q3Fc3PTJSMW5lxfx1vIR/og33WJ +BgIejfD1dYW2Fp02z5sF6Pw6vhobpfDYgsTAKNonh5P6NxMiD14eQxYrNJ6DAF0= +-----END CERTIFICATE----- diff --git a/run_seed.sh b/run_seed.sh index 2c3941b5..95e3e498 100755 --- a/run_seed.sh +++ b/run_seed.sh @@ -70,11 +70,18 @@ if [[ -n "$WITH_CONFIG" ]]; then RECERT_CONFIG=<(echo ' dry_run: false etcd_endpoint: localhost:2379 -static_dirs: +crypto_dirs: - backup/etc/kubernetes - backup/var/lib/kubelet - backup/etc/machine-config-daemon -static_files: +crypto_files: +- backup/etc/mcs-machine-config-content.json +cluster_customization_dirs: +- backup/etc/kubernetes +- backup/var/lib/kubelet +- backup/etc/machine-config-daemon +- backup/etc/pki/ca-trust +cluster_customization_files: - backup/etc/mcs-machine-config-content.json cn_san_replace_rules: - api-int.seed.redhat.com:api-int.new-name.foo.com @@ -104,6 +111,55 @@ cluster_rename: new-name:foo.com:some-random-infra-id hostname: test.hostname ip: 192.168.126.99 kubeadmin_password_hash: "$2a$10$20Q4iRLy7cWZkjn/D07bF.RZQZonKwstyRGH0qiYbYRkx5Pe4Ztyi" +additional_trust_bundle: | + # Foo + -----BEGIN CERTIFICATE----- + MIIDZTCCAk2gAwIBAgIUP+AxIkXJXTEhNGLH2qjmE6Gp0fowDQYJKoZIhvcNAQEL + BQAwQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE + CgwTRGVmYXVsdCBDb21wYW55IEx0ZDAeFw0yNDAzMDExMDIyNTlaFw0yNTAzMDEx + MDIyNTlaMEIxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAa + BgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IB + DwAwggEKAoIBAQC0wzg+7X2Amb5g60g0TstLgC0XnJRZq/YZUUsJMmm3qMb/+GYJ + AJzHxiycUfbRJtYvjx0SBmAX/kDRVCEQKcN5d/y3zeq709YO40kvouScfstsxM8l + PFLOmM8/Dqey1WblSJERBLbLherDnMwR7EMXkyZ/AfHUXmhVoIZE9ywsZpNcVW6Z + 7x/+Izbj1s305vrxEkZDw6b3oMG5uooQgP5NZFXSamzJgviP0L/usvbRMtAWphoj + WhMeNuOdymLwRzm2l+2Qp/JDWktgHccmrbbi1c6pwhsIJBj4KOyb9zROTnYXyS/j + 0b7GzVcffveV6E58rGa2ILyIsCv6gt8LgFnxAgMBAAGjUzBRMB0GA1UdDgQWBBQ5 + nh0SeZxZ969ps+9ywPEoOVasxTAfBgNVHSMEGDAWgBQ5nh0SeZxZ969ps+9ywPEo + OVasxTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAWxsqfdm8h + AeNY8vPcRdtB9KYU5sZLs4NtlBFSdn+pHmeXZwwkjQDNhVcEMKdpZI4BpS11Ggwh + 1d3BCCC/5M6yVqm+EMKJvA9VCeM8d1WJ1yVyXcgGuegf8kr3v+lr7Ll59qZGP5Ir + WwE8WRns7uFOCqYJCxo1VFXitZZuIugr3NUSimBPoJf1hDYdye3K3Q+grF2GyNII + 5Yo+/VSR4ejIvJYAFp91Ycep7S0/+qhFpsjEG0Qw3Ly6WqQoCqdmIsyqFgWHsIlY + oJxV5wTX/c9DDZLR0VUD19aDV3B9kb7Cf+h7S4RsORWCyi7+58FKkkD6Ryc0I1K6 + xw3RWhfd9o1d + -----END CERTIFICATE----- + + + + # All + # the Bars + -----BEGIN CERTIFICATE----- + MIIDZTCCAk2gAwIBAgIULnisjJLte3Vvt4o1f+5vSQg542cwDQYJKoZIhvcNAQEL + BQAwQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE + CgwTRGVmYXVsdCBDb21wYW55IEx0ZDAeFw0yNDAzMDExMDI1MDFaFw0yNTAzMDEx + MDI1MDFaMEIxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAa + BgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IB + DwAwggEKAoIBAQC2dhK7xTnoTB3wN1l3NsLTp5YR0KFfBTjMcDgSzUy/GN79c2cF + JzSuiYUi7SCmFjn3soNqpXHFzCox6KIs9R6PL4epaQM76EVG/Xy6mdDvFnZvqypi + wmK6J0AGajOxItYUGb2a3Zmt/2nliW6t8sW/vhovHRu7YROo4uJygIp2UUFct2Lk + 8C7XkJX5RXW+sKTiNddIjhmDFD0vHfvNvQ6AIayJTmXy272+aqYNJWB2wS/2uD3Z + +WOpiINetCtkASoiE7nzBQw+WsTfeFJH2TnI5pnSaHdLRUQtzoLO0/FgQ5WBfJg5 + aH03DLfQ9GEdzlsOkPOEgHXqDFMjTQCwcue3AgMBAAGjUzBRMB0GA1UdDgQWBBRd + 0Zs+cm0gPHGKoQrerC18Pa3B3zAfBgNVHSMEGDAWgBRd0Zs+cm0gPHGKoQrerC18 + Pa3B3zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAepPrWqB9h + JkqtgJrP8SkQVulTVKYj66J5JxM5vZR96Z4UnbA3WNxezev0jMCYuV0twHPN8avs + Jern+/n7vgQ3ziiLVdtrN8PqK1X1apSurVmaiIw4tRcv5TVL5OD95sTyJh5bUBpM + DGtCTraPZxLIDKm9byunobXtJVcutw4oHKtFy/LlFWePCnvFzvx6ZFswLAXgxhf9 + EtjDf3v0cjDn9yRzjYFrwHiQ53A75YTwFyk21q7Gh1G0yspfBeq7cej2wK1PnfiC + 42TI0UzcqRV4CWDoARMSV8yMLajZ0g1eEreUprwmFcOy17V7KCeV6E8lKb21OU8M + Ad9q3H0iXjct + -----END CERTIFICATE----- summary_file: summary.yaml summary_file_clean: summary_redacted.yaml extend_expiration: true @@ -113,23 +169,36 @@ threads: 1 ') cargo run --release else # shellcheck disable=2016 - cargo run --release -- \ + cargo run -- \ --etcd-endpoint localhost:2379 \ - --static-dir backup/etc/kubernetes \ - --static-dir backup/var/lib/kubelet \ - --static-dir backup/etc/machine-config-daemon \ - --static-file backup/etc/mcs-machine-config-content.json \ + \ + --crypto-dir backup/etc/kubernetes \ + --crypto-dir backup/var/lib/kubelet \ + --crypto-dir backup/etc/machine-config-daemon \ + --crypto-file backup/etc/mcs-machine-config-content.json \ + \ + --cluster-customization-dir backup/etc/kubernetes \ + --cluster-customization-dir backup/var/lib/kubelet \ + --cluster-customization-dir backup/etc/machine-config-daemon \ + --cluster-customization-dir backup/etc/pki/ca-trust \ + --cluster-customization-file backup/etc/mcs-machine-config-content.json \ + \ --cn-san-replace api-int.seed.redhat.com:api-int.new-name.foo.com \ --cn-san-replace api.seed.redhat.com:api.new-name.foo.com \ --cn-san-replace *.apps.seed.redhat.com:*.apps.new-name.foo.com \ --cn-san-replace 192.168.126.10:192.168.127.11 \ + --use-cert ./hack/dummy_use_cert.crt \ + \ --cluster-rename new-name:foo.com:some-random-infra-id \ --hostname test.hostname \ --ip 192.168.126.99 \ --kubeadmin-password-hash '$2a$10$20Q4iRLy7cWZkjn/D07bF.RZQZonKwstyRGH0qiYbYRkx5Pe4Ztyi' \ + --additional-trust-bundle ./hack/dummy_trust_bundle.pem \ + --pull-secret '{"auths":{"empty_registry":{"username":"empty","password":"empty","auth":"ZW1wdHk6ZW1wdHk=","email":""}}}' \ + \ --summary-file summary.yaml \ --summary-file-clean summary_redacted.yaml \ - --pull-secret '{"auths":{"empty_registry":{"username":"empty","password":"empty","auth":"ZW1wdHk6ZW1wdHk=","email":""}}}' \ + \ --extend-expiration # --regenerate-server-ssh-keys backup/etc/ssh/ \ fi diff --git a/src/cluster_crypto/scanning.rs b/src/cluster_crypto/scanning.rs index c6e26dff..9081aef7 100644 --- a/src/cluster_crypto/scanning.rs +++ b/src/cluster_crypto/scanning.rs @@ -5,7 +5,7 @@ use super::{ }; use crate::{ cluster_crypto::{crypto_objects::process_unknown_value, json_crawl}, - config::ConfigPath, + config::path::ConfigPath, file_utils::{self, read_file_to_string}, k8s_etcd::InMemoryK8sEtcd, recert::timing::RunTime, diff --git a/src/cnsanreplace.rs b/src/cnsanreplace.rs index e408c21d..57cdd02f 100644 --- a/src/cnsanreplace.rs +++ b/src/cnsanreplace.rs @@ -16,7 +16,7 @@ impl std::fmt::Display for CnSanReplace { } impl CnSanReplace { - pub(crate) fn cli_parse(value: &str) -> Result { + pub(crate) fn parse(value: &str) -> Result { // Also allow comma separation to support IPv6 let split = if value.contains(',') { value.split(',') } else { value.split(':') }.collect::>(); diff --git a/src/config.rs b/src/config.rs index fc9b1710..3d11dd61 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,4 @@ -use std::{env, ops::Deref, path::Path, sync::atomic::Ordering::Relaxed}; - +use self::{cli::Cli, path::ConfigPath}; use crate::{ cluster_crypto::REDACT_SECRETS, cnsanreplace::{CnSanReplace, CnSanReplaceRules}, @@ -9,47 +8,19 @@ use crate::{ }; use anyhow::{ensure, Context, Result}; use clap::Parser; -use clio::ClioPath; use itertools::Itertools; use serde::Serialize; use serde_json::Value; - -use self::cli::Cli; +use std::{env, path::PathBuf, sync::atomic::Ordering::Relaxed}; mod cli; - -#[derive(Clone, Debug)] -pub(crate) struct ConfigPath(pub(crate) ClioPath); - -impl AsRef for ConfigPath { - fn as_ref(&self) -> &ClioPath { - &self.0 - } -} - -impl Deref for ConfigPath { - type Target = Path; - - fn deref(&self) -> &Self::Target { - self.0.path() - } -} - -impl From for ConfigPath { - fn from(clio_path: ClioPath) -> Self { - Self(clio_path) - } -} - -impl serde::Serialize for ConfigPath { - fn serialize(&self, serializer: S) -> std::result::Result { - serializer.serialize_str(self.0.to_string_lossy().as_ref()) - } -} +pub(crate) mod path; /// All the user requested customizations, coalesced into a single struct for convenience #[derive(serde::Serialize)] pub(crate) struct CryptoCustomizations { + pub(crate) dirs: Vec, + pub(crate) files: Vec, pub(crate) cn_san_replace_rules: CnSanReplaceRules, pub(crate) use_key_rules: UseKeyRules, pub(crate) use_cert_rules: UseCertRules, @@ -59,11 +30,14 @@ pub(crate) struct CryptoCustomizations { #[derive(serde::Serialize)] pub(crate) struct ClusterCustomizations { + pub(crate) dirs: Vec, + pub(crate) files: Vec, pub(crate) cluster_rename: Option, pub(crate) hostname: Option, pub(crate) ip: Option, pub(crate) kubeadmin_password_hash: Option, pub(crate) pull_secret: Option, + pub(crate) additional_trust_bundle: Option, } /// All parsed CLI arguments, coalesced into a single struct for convenience @@ -71,8 +45,6 @@ pub(crate) struct ClusterCustomizations { pub(crate) struct RecertConfig { pub(crate) dry_run: bool, pub(crate) etcd_endpoint: Option, - pub(crate) static_dirs: Vec, - pub(crate) static_files: Vec, pub(crate) crypto_customizations: CryptoCustomizations, pub(crate) cluster_customizations: ClusterCustomizations, pub(crate) threads: Option, @@ -131,178 +103,16 @@ impl RecertConfig { let mut value = value.as_object().context("config file must be a YAML object")?.clone(); - let dry_run = value - .remove("dry_run") - .unwrap_or(Value::Bool(false)) - .as_bool() - .context("dry_run must be a boolean")?; + let (crypto_dirs, crypto_files, cluster_customization_dirs, cluster_customization_files) = parse_dir_file_config(&mut value)?; - let etcd_endpoint = match value.remove("etcd_endpoint") { - Some(value) => Some(value.as_str().context("etcd_endpoint must be a string")?.to_string()), - None => None, - }; + let (cn_san_replace_rules, use_key_rules, use_cert_rules, extend_expiration, force_expire) = + parse_crypto_customization_config(&mut value)?; - let static_dirs = match value.remove("static_dirs") { - Some(value) => value - .as_array() - .context("static_dirs must be an array")? - .iter() - .map(|value| { - let clio_path = ClioPath::new(value.as_str().context("static_dirs must be an array of strings")?) - .context(format!("config dir {}", value.as_str().unwrap()))?; - - ensure!(clio_path.try_exists()?, format!("static_dir must exist: {}", clio_path)); - ensure!(clio_path.is_dir(), format!("static_dir must be a directory: {}", clio_path)); - - Ok(ConfigPath::from(clio_path)) - }) - .collect::>>()?, - None => vec![], - }; - - let static_files = match value.remove("static_files") { - Some(value) => value - .as_array() - .context("static_files must be an array")? - .iter() - .map(|value| { - let clio_path = ClioPath::new(value.as_str().context("static_files must be an array of strings")?) - .context(format!("config file {}", value.as_str().unwrap()))?; + let (cluster_rename, hostname, ip, pull_secret, set_kubeadmin_password_hash, additional_trust_bundle) = + parse_cluster_customization_config(&mut value)?; - ensure!(clio_path.try_exists()?, format!("static_file must exist: {}", clio_path)); - ensure!(clio_path.is_file(), format!("static_file must be a file: {}", clio_path)); - - Ok(ConfigPath::from(clio_path)) - }) - .collect::>>()?, - None => vec![], - }; - - let cn_san_replace_rules = match value.remove("cn_san_replace_rules") { - Some(value) => CnSanReplaceRules( - value - .as_array() - .context("cn_san_replace_rules must be an array")? - .iter() - .map(|value| { - CnSanReplace::cli_parse(value.as_str().context("cn_san_replace_rules must be an array of strings")?) - .context(format!("cn_san_replace_rule {}", value.as_str().unwrap())) - }) - .collect::>>()?, - ), - None => CnSanReplaceRules(vec![]), - }; - - let use_key_rules = match value.remove("use_key_rules") { - Some(value) => UseKeyRules( - value - .as_array() - .context("use_key_rules must be an array")? - .iter() - .map(|value| { - UseKey::cli_parse(value.as_str().context("use_key_rules must be an array of strings")?) - .context(format!("use_key_rule {}", value.as_str().unwrap())) - }) - .collect::>>()?, - ), - None => UseKeyRules(vec![]), - }; - - let use_cert_rules = match value.remove("use_cert_rules") { - Some(value) => UseCertRules( - value - .as_array() - .context("use_cert_rules must be an array")? - .iter() - .map(|value| { - UseCert::cli_parse(value.as_str().context("use_cert_rules must be an array of strings")?) - .context(format!("use_cert_rule {}", value.as_str().unwrap())) - }) - .collect::>>()?, - ), - None => UseCertRules(vec![]), - }; - - let extend_expiration = value - .remove("extend_expiration") - .unwrap_or(Value::Bool(false)) - .as_bool() - .context("extend_expiration must be a boolean")?; - - let force_expire = value - .remove("force_expire") - .unwrap_or(Value::Bool(false)) - .as_bool() - .context("force_expire must be a boolean")?; - - let cluster_rename = match value.remove("cluster_rename") { - Some(value) => Some( - ClusterNamesRename::cli_parse(value.as_str().context("cluster_rename must be a string")?) - .context(format!("cluster_rename {}", value.as_str().unwrap()))?, - ), - None => None, - }; - - let hostname = match value.remove("hostname") { - Some(value) => Some(value.as_str().context("hostname must be a string")?.to_string()), - None => None, - }; - - let ip = match value.remove("ip") { - Some(value) => Some(value.as_str().context("ip must be a string")?.to_string()), - None => None, - }; - - let pull_secret = match value.remove("pull_secret") { - Some(value) => Some(value.as_str().context("pull_secret must be a string")?.to_string()), - None => None, - }; - - let set_kubeadmin_password_hash = match value.remove("kubeadmin_password_hash") { - Some(value) => Some(value.as_str().context("set_kubeadmin_password_hash must be a string")?.to_string()), - None => None, - }; - - let threads = match value.remove("threads") { - Some(value) => Some( - value - .as_u64() - .context("threads must be an integer")? - .try_into() - .context("threads must be an integer")?, - ), - None => None, - }; - - let regenerate_server_ssh_keys = match value.remove("regenerate_server_ssh_keys") { - Some(value) => { - let clio_path = ConfigPath::from( - ClioPath::new(value.as_str().context("regenerate_server_ssh_keys must be a string")?) - .context(format!("regenerate_server_ssh_keys {}", value.as_str().unwrap()))?, - ); - - ensure!(clio_path.try_exists()?, "regenerate_server_ssh_keys must exist"); - ensure!(clio_path.is_dir(), "regenerate_server_ssh_keys must be a directory"); - Some(clio_path) - } - None => None, - }; - - let summary_file = match value.remove("summary_file") { - Some(value) => Some(ConfigPath::from( - ClioPath::new(value.as_str().context("summary_file must be a string")?) - .context(format!("summary_file {}", value.as_str().unwrap()))?, - )), - None => None, - }; - - let summary_file_clean = match value.remove("summary_file_clean") { - Some(value) => Some(ConfigPath::from( - ClioPath::new(value.as_str().context("summary_file_clean must be a string")?) - .context(format!("summary_file_clean {}", value.as_str().unwrap()))?, - )), - None => None, - }; + let (dry_run, etcd_endpoint, threads, regenerate_server_ssh_keys, summary_file, summary_file_clean) = + parse_misc_config(&mut value)?; ensure!( value.is_empty(), @@ -310,25 +120,32 @@ impl RecertConfig { value.keys().map(|key| key.to_string()).join(", ") ); + let crypto_customizations = CryptoCustomizations { + dirs: crypto_dirs, + files: crypto_files, + cn_san_replace_rules, + use_key_rules, + use_cert_rules, + extend_expiration, + force_expire, + }; + + let cluster_customizations = ClusterCustomizations { + dirs: cluster_customization_dirs, + files: cluster_customization_files, + cluster_rename, + hostname, + ip, + kubeadmin_password_hash: set_kubeadmin_password_hash, + pull_secret, + additional_trust_bundle, + }; + let recert_config = Self { dry_run, etcd_endpoint, - static_dirs, - static_files, - crypto_customizations: CryptoCustomizations { - cn_san_replace_rules, - use_key_rules, - use_cert_rules, - extend_expiration, - force_expire, - }, - cluster_customizations: ClusterCustomizations { - cluster_rename, - hostname, - ip, - kubeadmin_password_hash: set_kubeadmin_password_hash, - pull_secret, - }, + crypto_customizations, + cluster_customizations, threads, regenerate_server_ssh_keys, summary_file, @@ -360,9 +177,17 @@ impl RecertConfig { Ok(Self { dry_run: cli.dry_run, etcd_endpoint: cli.etcd_endpoint, - static_dirs: cli.static_dir.into_iter().map(ConfigPath::from).collect(), - static_files: cli.static_file.into_iter().map(ConfigPath::from).collect(), crypto_customizations: CryptoCustomizations { + dirs: if cli.static_dir.is_empty() { + cli.crypto_dir.into_iter().map(ConfigPath::from).collect() + } else { + cli.static_dir.clone().into_iter().map(ConfigPath::from).collect() + }, + files: if cli.static_file.is_empty() { + cli.crypto_file.into_iter().map(ConfigPath::from).collect() + } else { + cli.static_file.clone().into_iter().map(ConfigPath::from).collect() + }, cn_san_replace_rules: CnSanReplaceRules(cli.cn_san_replace), use_key_rules: UseKeyRules(cli.use_key), use_cert_rules: UseCertRules(cli.use_cert), @@ -370,11 +195,22 @@ impl RecertConfig { force_expire: cli.force_expire, }, cluster_customizations: ClusterCustomizations { + dirs: if cli.static_dir.is_empty() { + cli.cluster_customization_dir.into_iter().map(ConfigPath::from).collect() + } else { + cli.static_dir.into_iter().map(ConfigPath::from).collect() + }, + files: if cli.static_file.is_empty() { + cli.cluster_customization_file.into_iter().map(ConfigPath::from).collect() + } else { + cli.static_file.into_iter().map(ConfigPath::from).collect() + }, cluster_rename: cli.cluster_rename, hostname: cli.hostname, ip: cli.ip, kubeadmin_password_hash: cli.kubeadmin_password_hash, pull_secret: cli.pull_secret, + additional_trust_bundle: cli.additional_trust_bundle, }, threads: cli.threads, regenerate_server_ssh_keys: cli.regenerate_server_ssh_keys.map(ConfigPath::from), @@ -403,3 +239,372 @@ impl RecertConfig { }) } } + +#[allow(clippy::type_complexity)] +fn parse_misc_config( + value: &mut serde_json::Map, +) -> Result<( + bool, + Option, + Option, + Option, + Option, + Option, +)> { + let dry_run = value + .remove("dry_run") + .unwrap_or(Value::Bool(false)) + .as_bool() + .context("dry_run must be a boolean")?; + let etcd_endpoint = match value.remove("etcd_endpoint") { + Some(value) => Some(value.as_str().context("etcd_endpoint must be a string")?.to_string()), + None => None, + }; + let threads = match value.remove("threads") { + Some(value) => Some( + value + .as_u64() + .context("threads must be an integer")? + .try_into() + .context("threads must be an integer")?, + ), + None => None, + }; + let regenerate_server_ssh_keys = match value.remove("regenerate_server_ssh_keys") { + Some(value) => { + let config_path = ConfigPath::new(value.as_str().context("regenerate_server_ssh_keys must be a string")?) + .context(format!("regenerate_server_ssh_keys {}", value.as_str().unwrap()))?; + + ensure!(config_path.try_exists()?, "regenerate_server_ssh_keys must exist"); + ensure!(config_path.is_dir(), "regenerate_server_ssh_keys must be a directory"); + Some(config_path) + } + None => None, + }; + let summary_file = match value.remove("summary_file") { + Some(value) => Some( + ConfigPath::new(value.as_str().context("summary_file must be a string")?) + .context(format!("summary_file {}", value.as_str().unwrap()))?, + ), + None => None, + }; + let summary_file_clean = match value.remove("summary_file_clean") { + Some(value) => Some( + ConfigPath::new(value.as_str().context("summary_file_clean must be a string")?) + .context(format!("summary_file_clean {}", value.as_str().unwrap()))?, + ), + None => None, + }; + Ok(( + dry_run, + etcd_endpoint, + threads, + regenerate_server_ssh_keys, + summary_file, + summary_file_clean, + )) +} + +#[allow(clippy::type_complexity)] +fn parse_cluster_customization_config( + value: &mut serde_json::Map, +) -> Result<( + Option, + Option, + Option, + Option, + Option, + Option, +)> { + let cluster_rename = match value.remove("cluster_rename") { + Some(value) => Some( + ClusterNamesRename::parse(value.as_str().context("cluster_rename must be a string")?) + .context(format!("cluster_rename {}", value.as_str().unwrap()))?, + ), + None => None, + }; + let hostname = match value.remove("hostname") { + Some(value) => Some(value.as_str().context("hostname must be a string")?.to_string()), + None => None, + }; + let ip = match value.remove("ip") { + Some(value) => Some(value.as_str().context("ip must be a string")?.to_string()), + None => None, + }; + let pull_secret = match value.remove("pull_secret") { + Some(value) => Some(value.as_str().context("pull_secret must be a string")?.to_string()), + None => None, + }; + let set_kubeadmin_password_hash = match value.remove("kubeadmin_password_hash") { + Some(value) => Some(value.as_str().context("set_kubeadmin_password_hash must be a string")?.to_string()), + None => None, + }; + let additional_trust_bundle = match value.remove("additional_trust_bundle") { + Some(value) => Some(parse_additional_trust_bundle( + value.as_str().context("additional_trust_bundle must be a string")?, + )?), + None => None, + }; + Ok(( + cluster_rename, + hostname, + ip, + pull_secret, + set_kubeadmin_password_hash, + additional_trust_bundle, + )) +} + +fn parse_crypto_customization_config( + value: &mut serde_json::Map, +) -> Result<(CnSanReplaceRules, UseKeyRules, UseCertRules, bool, bool)> { + let cn_san_replace_rules = match value.remove("cn_san_replace_rules") { + Some(value) => CnSanReplaceRules( + value + .as_array() + .context("cn_san_replace_rules must be an array")? + .iter() + .map(|value| { + CnSanReplace::parse(value.as_str().context("cn_san_replace_rules must be an array of strings")?) + .context(format!("cn_san_replace_rule {}", value.as_str().unwrap())) + }) + .collect::>>()?, + ), + None => CnSanReplaceRules(vec![]), + }; + let use_key_rules = match value.remove("use_key_rules") { + Some(value) => UseKeyRules( + value + .as_array() + .context("use_key_rules must be an array")? + .iter() + .map(|value| { + UseKey::parse(value.as_str().context("use_key_rules must be an array of strings")?) + .context(format!("use_key_rule {}", value.as_str().unwrap())) + }) + .collect::>>()?, + ), + None => UseKeyRules(vec![]), + }; + let use_cert_rules = match value.remove("use_cert_rules") { + Some(value) => UseCertRules( + value + .as_array() + .context("use_cert_rules must be an array")? + .iter() + .map(|value| { + UseCert::parse(value.as_str().context("use_cert_rules must be an array of strings")?) + .context(format!("use_cert_rule {}", value.as_str().unwrap())) + }) + .collect::>>()?, + ), + None => UseCertRules(vec![]), + }; + let extend_expiration = value + .remove("extend_expiration") + .unwrap_or(Value::Bool(false)) + .as_bool() + .context("extend_expiration must be a boolean")?; + let force_expire = value + .remove("force_expire") + .unwrap_or(Value::Bool(false)) + .as_bool() + .context("force_expire must be a boolean")?; + Ok((cn_san_replace_rules, use_key_rules, use_cert_rules, extend_expiration, force_expire)) +} + +#[allow(clippy::type_complexity)] +fn parse_dir_file_config( + value: &mut serde_json::Map, +) -> Result<(Vec, Vec, Vec, Vec)> { + let static_dirs = match value.remove("static_dirs") { + Some(value) => { + ensure!( + value.get("crypto_dirs").is_none(), + "static_dirs and crypto_dirs are mutually exclusive" + ); + ensure!( + value.get("cluster_customization_dirs").is_none(), + "static_dirs and cluster_customization_dirs are mutually exclusive" + ); + ensure!( + value.get("additional_trust_bundle").is_none(), + "static_dirs and cluster_customization_dirs are mutually exclusive" + ); + + value + .as_array() + .context("static_dirs must be an array")? + .iter() + .map(|value| { + let config_path = ConfigPath::new(value.as_str().context("static_dirs must be an array of strings")?) + .context(format!("config dir {}", value.as_str().unwrap()))?; + + ensure!(config_path.try_exists()?, format!("static_dir must exist: {}", config_path)); + ensure!(config_path.is_dir(), format!("static_dir must be a directory: {}", config_path)); + + Ok(config_path) + }) + .collect::>>()? + } + None => vec![], + }; + let static_files = match value.remove("static_files") { + Some(value) => { + ensure!( + value.get("crypto_files").is_none(), + "static_files and crypto_files are mutually exclusive" + ); + ensure!( + value.get("cluster_customization_files").is_none(), + "static_files and cluster_customization_files are mutually exclusive" + ); + ensure!( + value.get("additional_trust_bundle").is_none(), + "static_files and cluster_customization_files are mutually exclusive" + ); + + value + .as_array() + .context("static_files must be an array")? + .iter() + .map(|value| { + let config_path = ConfigPath::new(value.as_str().context("static_files must be an array of strings")?) + .context(format!("config file {}", value.as_str().unwrap()))?; + + ensure!(config_path.try_exists()?, format!("static_file must exist: {}", config_path)); + ensure!(config_path.is_file(), format!("static_file must be a file: {}", config_path)); + + Ok(config_path) + }) + .collect::>>()? + } + None => vec![], + }; + let crypto_dirs = if static_dirs.is_empty() { + match value.remove("crypto_dirs") { + Some(value) => value + .as_array() + .context("crypto_dirs must be an array")? + .iter() + .map(|value| { + let config_path = ConfigPath::new(value.as_str().context("crypto_dirs must be an array of strings")?) + .context(format!("crypto dir {}", value.as_str().unwrap()))?; + + ensure!(config_path.try_exists()?, format!("crypto_dir must exist: {}", config_path)); + ensure!(config_path.is_dir(), format!("crypto_dir must be a directory: {}", config_path)); + + Ok(config_path) + }) + .collect::>>()?, + None => vec![], + } + } else { + static_dirs.clone() + }; + let crypto_files = if static_files.is_empty() { + match value.remove("crypto_files") { + Some(value) => value + .as_array() + .context("crypto_files must be an array")? + .iter() + .map(|value| { + let config_path = ConfigPath::new(value.as_str().context("crypto_files must be an array of strings")?) + .context(format!("crypto file {}", value.as_str().unwrap()))?; + + ensure!(config_path.try_exists()?, format!("crypto_file must exist: {}", config_path)); + ensure!(config_path.is_file(), format!("crypto_file must be a file: {}", config_path)); + + Ok(config_path) + }) + .collect::>>()?, + None => vec![], + } + } else { + static_files.clone() + }; + + let cluster_customization_dirs = if static_dirs.is_empty() { + match value.remove("cluster_customization_dirs") { + Some(value) => value + .as_array() + .context("cluster_customization_dirs must be an array")? + .iter() + .map(|value| { + let config_path = ConfigPath::new(value.as_str().context("cluster_customization_dirs must be an array of strings")?) + .context(format!("cluster_customization dir {}", value.as_str().unwrap()))?; + + ensure!( + config_path.try_exists()?, + format!("cluster_customization_dir must exist: {}", config_path) + ); + ensure!( + config_path.is_dir(), + format!("cluster_customization_dir must be a directory: {}", config_path) + ); + + Ok(config_path) + }) + .collect::>>()?, + None => vec![], + } + } else { + static_dirs + }; + + let cluster_customization_files = if static_files.is_empty() { + match value.remove("cluster_customization_files") { + Some(value) => value + .as_array() + .context("cluster_customization_files must be an array")? + .iter() + .map(|value| { + let config_path = ConfigPath::new(value.as_str().context("cluster_customization_files must be an array of strings")?) + .context(format!("cluster_customization file {}", value.as_str().unwrap()))?; + + ensure!( + config_path.try_exists()?, + format!("cluster_customization_file must exist: {}", config_path) + ); + ensure!( + config_path.is_file(), + format!("cluster_customization_file must be a file: {}", config_path) + ); + + Ok(config_path) + }) + .collect::>>()?, + None => vec![], + } + } else { + static_files + }; + Ok((crypto_dirs, crypto_files, cluster_customization_dirs, cluster_customization_files)) +} + +pub(crate) fn parse_additional_trust_bundle(value: &str) -> Result { + let bundle = if !value.contains('\n') { + let path = PathBuf::from(&value); + + ensure!(path.try_exists()?, "additional_trust_bundle must exist"); + ensure!(path.is_file(), "additional_trust_bundle must be a file"); + + String::from_utf8(std::fs::read(&path).context("failed to read additional_trust_bundle")?) + .context("additional_trust_bundle must be valid UTF-8")? + } else { + value.to_string() + }; + + let pems = pem::parse_many(bundle.as_bytes()).context("additional_trust_bundle must be valid PEM")?; + + ensure!(!pems.is_empty(), "additional_trust_bundle must contain at least one certificate"); + + ensure!( + pems.iter().all(|pem| pem.tag() == "CERTIFICATE"), + "additional_trust_bundle must contain only certificates" + ); + + // After parsing, we still return the raw bundle, as OpenShift also preserves the original + // comments and whitespace in the user's additional trust bundle + Ok(bundle) +} diff --git a/src/config/cli.rs b/src/config/cli.rs index b8203d2c..b5a41cb9 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -12,27 +12,49 @@ pub(crate) struct Cli { #[clap(long)] pub(crate) etcd_endpoint: Option, + // DEPRECATED: Use --crypto-dir and --cluster-customization-dir instead. This option will be + // removed in a future release. Cannot be used with --crypto-dir or --cluster-customization-dir + // or --additional-trust-bundle + #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), groups = &["crypto_dir_paths", "cluster_customization_dir_paths", "adt_dirs"])] + pub(crate) static_dir: Vec, + + /// DEPRECATED: Use --crypto-file and --cluster-customization-file instead. This option will be + /// removed in a future release. Cannot be used with --crypto-file or + /// --cluster-customization-file or --additional-trust-bundle + #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_file(), groups = &["crypto_file_paths", "cluster_customization_file_paths", "adt_files"])] + pub(crate) static_file: Vec, + /// Directory to recertify, such as /var/lib/kubelet, /etc/kubernetes and /// /etc/machine-config-daemon. Can specify multiple times - #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_dir())] - pub(crate) static_dir: Vec, + #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), group = "crypto_dir_paths")] + pub(crate) crypto_dir: Vec, /// A file to recertify, such as /etc/mcs-machine-config-content.json. Can specify multiple /// times - #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_file())] - pub(crate) static_file: Vec, + #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_file(), group = "crypto_file_paths")] + pub(crate) crypto_file: Vec, + + /// Directory containing files involved in cluster customization, such as /var/lib/kubelet, + /// /etc/kubernetes, /etc/pki/ca-trust, etc. Can specify multiple. + #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), group = "cluster_customization_dir_paths")] + pub(crate) cluster_customization_dir: Vec, + + /// File involved in cluster customization, such as /etc/mcs-machine-config-content.json. Can + /// specify multiple. + #[clap(long, value_parser = clap::value_parser!(ClioPath).exists().is_file(), group = "cluster_customization_file_paths")] + pub(crate) cluster_customization_file: Vec, /// A list of strings to replace in the subject name of all certificates. Can specify multiple. /// --cn-san-replace foo:bar --cn-san-replace baz:qux will replace all instances of "foo" with /// "bar" and all instances of "baz" with "qux" in the CN/SAN of all certificates. - #[clap(long, value_parser = CnSanReplace::cli_parse)] + #[clap(long, value_parser = CnSanReplace::parse)] pub(crate) cn_san_replace: Vec, /// Experimental feature. Colon separated cluster name and cluster base domain. If given, many /// cluster resources which refer to a cluster name / cluster base domain (typically through /// URLs which they happen to contian) will be modified to use this cluster name and base /// domain instead. - #[clap(long, value_parser = ClusterNamesRename::cli_parse)] + #[clap(long, value_parser = ClusterNamesRename::parse)] pub(crate) cluster_rename: Option, /// If given, the cluster resources that include the hostname will be modified to use this one @@ -63,6 +85,16 @@ pub(crate) struct Cli { #[clap(long)] pub(crate) pull_secret: Option, + /// Change a cluster's trust bundle. Changes all locations where the trust bundle is stored in + /// the cluster. If an existing trust bundle is not found, this will cause an error, as + /// creating the relevant resources is beyond the scope of this tool. The trust bundle's + /// validity will not be checked. When using a RECERT_CONFIG file, raw PEMS can be used instead + /// of paths to trust bundle files. When using this option it is recommended to also run + /// update-ca-trust after running recert to ensure that the trust bundle is properly updated in + /// all locations. + #[clap(long, value_parser = super::parse_additional_trust_bundle, groups = &["adt_dirs", "adt_files"])] + pub(crate) additional_trust_bundle: Option, + /// A list of CNs and the private keys to use for their certs. By default, new keys will be /// generated for all regenerated certificates, this option allows you to use existing keys /// instead. Must come in pairs of CN and private key file path, separated by a space. For @@ -72,7 +104,7 @@ pub(crate) struct Cli { /// regenerated. /// /// When using a RECERT_CONFIG file, raw PEMS can be used instead of paths to key files. - #[clap(long, value_parser = UseKey::cli_parse)] + #[clap(long, value_parser = UseKey::parse)] pub(crate) use_key: Vec, /// Same as --use-key, but for when a cert needs to be replaced in its entirety, rather than @@ -83,7 +115,7 @@ pub(crate) struct Cli { /// the --extend-expiration flag is used. /// /// When using a RECERT_CONFIG file, raw PEMS can be used instead of paths to cert files. - #[clap(long, value_parser = UseCert::cli_parse)] + #[clap(long, value_parser = UseCert::parse)] pub(crate) use_cert: Vec, /// Extend expiration of all certificates to (original_expiration + (now - issue date)), and diff --git a/src/config/path.rs b/src/config/path.rs new file mode 100644 index 00000000..26437196 --- /dev/null +++ b/src/config/path.rs @@ -0,0 +1,44 @@ +use anyhow::Result; +use clio::ClioPath; +use std::{ops::Deref, path::Path}; + +#[derive(Clone, Debug)] +pub(crate) struct ConfigPath(pub(crate) ClioPath); + +impl std::fmt::Display for ConfigPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.to_string_lossy().fmt(f) + } +} + +impl AsRef for ConfigPath { + fn as_ref(&self) -> &ClioPath { + &self.0 + } +} + +impl Deref for ConfigPath { + type Target = Path; + + fn deref(&self) -> &Self::Target { + self.0.path() + } +} + +impl From for ConfigPath { + fn from(clio_path: ClioPath) -> Self { + Self(clio_path) + } +} + +impl ConfigPath { + pub(crate) fn new(path: &str) -> Result { + Ok(Self(ClioPath::new(path)?)) + } +} + +impl serde::Serialize for ConfigPath { + fn serialize(&self, serializer: S) -> std::result::Result { + serializer.serialize_str(self.0.to_string_lossy().as_ref()) + } +} diff --git a/src/ocp_postprocess.rs b/src/ocp_postprocess.rs index a1da9543..ef8c028a 100644 --- a/src/ocp_postprocess.rs +++ b/src/ocp_postprocess.rs @@ -1,7 +1,7 @@ use self::cluster_domain_rename::params::ClusterNamesRename; use crate::{ cluster_crypto::locations::K8sResourceLocation, - config::{ClusterCustomizations, ConfigPath}, + config::{path::ConfigPath, ClusterCustomizations}, file_utils::{self, read_file_to_string}, k8s_etcd::{self, get_etcd_json, put_etcd_yaml}, }; @@ -15,8 +15,10 @@ use k8s_etcd::InMemoryK8sEtcd; use sha2::Digest; use std::{collections::HashSet, sync::Arc}; +pub(crate) mod additional_trust_bundle; pub(crate) mod cluster_domain_rename; mod fnv; +mod go_base32; pub(crate) mod hostname_rename; pub(crate) mod ip_rename; pub(crate) mod pull_secret_rename; @@ -25,8 +27,6 @@ pub(crate) mod pull_secret_rename; pub(crate) async fn ocp_postprocess( in_memory_etcd_client: &Arc, cluster_customizations: &ClusterCustomizations, - static_dirs: &Vec, - static_files: &Vec, ) -> Result<()> { fix_olm_secret_hash_annotation(in_memory_etcd_client) .await @@ -40,11 +40,11 @@ pub(crate) async fn ocp_postprocess( .await .context("deleting node-kubeconfigs")?; - sync_webhook_authenticators(in_memory_etcd_client, static_dirs) + sync_webhook_authenticators(in_memory_etcd_client, &cluster_customizations.dirs) .await .context("syncing webhook authenticators")?; - run_cluster_customizations(cluster_customizations, in_memory_etcd_client, static_dirs, static_files).await?; + run_cluster_customizations(cluster_customizations, in_memory_etcd_client).await?; fix_deployment_dep_annotations( in_memory_etcd_client, @@ -66,25 +66,24 @@ pub(crate) async fn ocp_postprocess( async fn run_cluster_customizations( cluster_customizations: &ClusterCustomizations, in_memory_etcd_client: &Arc, - static_dirs: &Vec, - static_files: &Vec, ) -> Result<(), anyhow::Error> { + let dirs = &cluster_customizations.dirs; + let files = &cluster_customizations.files; + if let Some(cluster_names_rename) = &cluster_customizations.cluster_rename { - cluster_rename(in_memory_etcd_client, cluster_names_rename, static_dirs, static_files) + cluster_rename(in_memory_etcd_client, cluster_names_rename, dirs, files) .await .context("renaming cluster")?; } if let Some(hostname) = &cluster_customizations.hostname { - hostname_rename(in_memory_etcd_client, hostname, static_dirs, static_files) + hostname_rename(in_memory_etcd_client, hostname, dirs, files) .await .context("renaming hostname")?; } if let Some(ip) = &cluster_customizations.ip { - ip_rename(in_memory_etcd_client, ip, static_dirs, static_files) - .await - .context("renaming IP")?; + ip_rename(in_memory_etcd_client, ip, dirs, files).await.context("renaming IP")?; } if let Some(kubeadmin_password_hash) = &cluster_customizations.kubeadmin_password_hash { @@ -96,11 +95,17 @@ async fn run_cluster_customizations( if let Some(pull_secret) = &cluster_customizations.pull_secret { log::info!("setting new pull_secret"); - pull_secret_rename(in_memory_etcd_client, pull_secret, static_dirs, static_files) + pull_secret_rename(in_memory_etcd_client, pull_secret, dirs, files) .await .context("renaming pull_secret")?; }; + if let Some(additional_trust_bundle) = &cluster_customizations.additional_trust_bundle { + additional_trust_bundle_rename(in_memory_etcd_client, additional_trust_bundle, dirs, files) + .await + .context("renaming additional_trust_bundle")?; + } + Ok(()) } @@ -485,3 +490,18 @@ pub(crate) async fn pull_secret_rename( Ok(()) } + +pub(crate) async fn additional_trust_bundle_rename( + in_memory_etcd_client: &Arc, + additional_trust_bundle: &str, + static_dirs: &[ConfigPath], + static_files: &[ConfigPath], +) -> Result<()> { + let etcd_client = in_memory_etcd_client; + + additional_trust_bundle::rename_all(etcd_client, additional_trust_bundle, static_dirs, static_files) + .await + .context("renaming all")?; + + Ok(()) +} diff --git a/src/ocp_postprocess/additional_trust_bundle.rs b/src/ocp_postprocess/additional_trust_bundle.rs new file mode 100644 index 00000000..6e08f170 --- /dev/null +++ b/src/ocp_postprocess/additional_trust_bundle.rs @@ -0,0 +1,95 @@ +use crate::{config::path::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; +use anyhow::{Context, Result}; +use std::{path::Path, sync::Arc}; + +mod etcd_rename; +mod filesystem_rename; +mod utils; + +pub(crate) async fn rename_all( + etcd_client: &Arc, + additional_trust_bundle: &str, + static_dirs: &[ConfigPath], + static_files: &[ConfigPath], +) -> Result<()> { + let new_merged_bundle = fix_etcd_resources(etcd_client, additional_trust_bundle) + .await + .context("renaming etcd resources")?; + + fix_filesystem_resources(&new_merged_bundle, additional_trust_bundle, static_dirs, static_files) + .await + .context("renaming filesystem resources")?; + + Ok(()) +} + +async fn fix_filesystem_resources( + additional_trust_bundle: &str, + new_merged_bundle: &str, + static_dirs: &[ConfigPath], + static_files: &[ConfigPath], +) -> Result<()> { + for dir in static_dirs { + fix_dir_resources(additional_trust_bundle, new_merged_bundle, dir).await?; + } + for file in static_files { + fix_file_resources(additional_trust_bundle, new_merged_bundle, file).await?; + } + + Ok(()) +} + +async fn fix_dir_resources(additional_trust_bundle: &str, new_merged_bundle: &str, dir: &Path) -> Result<()> { + // NOTE: This only fixes the trust anchors, the user should run "update-ca-trust" to fully + // update the system trust store after this change (this is also what MCO does). + filesystem_rename::fix_filesystem_ca_trust_anchors(additional_trust_bundle, dir) + .await + .context("fixing ca trust anchors")?; + + filesystem_rename::fix_filesystem_currentconfig(additional_trust_bundle, dir) + .await + .context("renaming currentconfig")?; + + filesystem_rename::fix_static_configmap_trusted_ca_bundle(new_merged_bundle, dir) + .await + .context("fixing static configmap trusted ca bundle")?; + + Ok(()) +} + +async fn fix_file_resources(_additional_trust_bundle: &str, _new_merged_bundle: &str, _file: &Path) -> Result<()> { + Ok(()) +} + +async fn fix_etcd_resources(etcd_client: &Arc, additional_trust_bundle: &str) -> Result { + // kubernetes.io/configmaps/openshift-config/custom-ca + let original_additional_trust_bundle = etcd_rename::fix_original_additional_trust_bundle(etcd_client, additional_trust_bundle) + .await + .context("fixing labeled configmaps")?; + + let system_certs = utils::derive_system_certs_from_merged_bundle( + original_additional_trust_bundle, + utils::get_merged_bundle(etcd_client).await.context("getting merged bundle")?, + ) + .context("getting unmerged bundle")?; + + let new_merged_bundle = utils::merge_bundles(additional_trust_bundle, &system_certs); + + etcd_rename::fix_labeled_configmaps(etcd_client, &new_merged_bundle) + .await + .context("fixing labeled configmaps")?; + + etcd_rename::fix_monitoring_configmaps(etcd_client, &new_merged_bundle) + .await + .context("fixing labeled configmaps")?; + + etcd_rename::fix_machineconfigs(etcd_client, additional_trust_bundle) + .await + .context("fixing machineconfigs")?; + + etcd_rename::fix_kcm_openshift_user_ca(etcd_client, additional_trust_bundle) + .await + .context("fixing kcm openshift user ca")?; + + Ok(new_merged_bundle) +} diff --git a/src/ocp_postprocess/additional_trust_bundle/etcd_rename.rs b/src/ocp_postprocess/additional_trust_bundle/etcd_rename.rs new file mode 100644 index 00000000..4330504b --- /dev/null +++ b/src/ocp_postprocess/additional_trust_bundle/etcd_rename.rs @@ -0,0 +1,270 @@ +use super::utils::fix_machineconfig; +use crate::{ + cluster_crypto::locations::K8sResourceLocation, + k8s_etcd::{get_etcd_json, put_etcd_yaml, InMemoryK8sEtcd}, + ocp_postprocess::fnv::fnv1_64, + ocp_postprocess::go_base32::base32_encode as go_base32_encode, +}; +use anyhow::{ensure, Context, Result}; +use futures_util::future::join_all; +use regex::Regex; +use serde_json::Value; +use std::sync::Arc; + +pub(crate) async fn fix_machineconfigs(etcd_client: &Arc, additional_trust_bundle: &str) -> Result<()> { + join_all( + etcd_client + .list_keys("machineconfiguration.openshift.io/machineconfigs") + .await? + .into_iter() + .map(|key| async move { + let etcd_result = etcd_client + .get(key.clone()) + .await + .with_context(|| format!("getting key {:?}", key))? + .context("key disappeared")?; + let value: Value = serde_yaml::from_slice(etcd_result.value.as_slice()) + .with_context(|| format!("deserializing value of key {:?}", key,))?; + let k8s_resource_location = K8sResourceLocation::try_from(&value)?; + + let mut machineconfig = get_etcd_json(etcd_client, &k8s_resource_location) + .await? + .context("no machineconfig")?; + + fix_machineconfig(&mut machineconfig, additional_trust_bundle).context("fixing machineconfig")?; + + put_etcd_yaml(etcd_client, &k8s_resource_location, machineconfig).await?; + + Ok(()) + }), + ) + .await + .into_iter() + .collect::>>()?; + + Ok(()) +} + +// There's an OCP operator that injects the trusted CA bundle into configmaps which have this +// label. We simply emulate that behavior here, should be a bit more robust than hardcoding a list +// of configmaps +pub(crate) async fn fix_labeled_configmaps(etcd_client: &InMemoryK8sEtcd, full_merged_bundle: &str) -> Result<()> { + join_all(etcd_client.list_keys("configmaps/").await?.into_iter().map(|key| async move { + let etcd_result = etcd_client + .get(key.clone()) + .await + .with_context(|| format!("getting key {:?}", key))? + .context("key disappeared")?; + let value: Value = + serde_yaml::from_slice(etcd_result.value.as_slice()).with_context(|| format!("deserializing value of key {:?}", key,))?; + + let slash = "~1"; + if value + .pointer(&format!("/metadata/labels/config.openshift.io{slash}inject-trusted-cabundle")) + .is_none() + { + let unlabeled_exceptions = [ + // All other certs are injected from this configmap, so we need to fix it as well, + // even though it's not labeled + K8sResourceLocation::new(Some("openshift-config-managed"), "ConfigMap", "trusted-ca-bundle", "v1"), + // ccm is quirky and builds its own configmap with a merged bundle. Usually it + // contains also certs taken from the cloud config, so it could look different than + // what network operator injects, but since we're doing SNO-none, the result is + // identical to what the network operator injects, so we can fix it as well + K8sResourceLocation::new(Some("openshift-cloud-controller-manager"), "ConfigMap", "ccm-trusted-ca", "v1"), + ]; + + if unlabeled_exceptions.iter().all(|location| location.as_etcd_key() != key) { + // This is not a configmap we want to inject into and neither is it the source of + // the injection, so it doesn't need to be fixed + return Ok(()); + } + } + + let k8s_resource_location = K8sResourceLocation::try_from(&value)?; + + let mut configmap = get_etcd_json(etcd_client, &k8s_resource_location) + .await? + .context("no machineconfig")?; + + let data = configmap + .pointer_mut("/data") + .context("no /data in configmap")? + .as_object_mut() + .context("/data not an object")?; + + data.insert( + "ca-bundle.crt".to_string(), + serde_json::Value::String(full_merged_bundle.to_string()), + ); + + put_etcd_yaml(etcd_client, &k8s_resource_location, configmap).await?; + + Ok(()) + })) + .await + .into_iter() + .collect::>>()?; + + Ok(()) +} + +pub(crate) async fn fix_original_additional_trust_bundle(etcd_client: &InMemoryK8sEtcd, additional_trust_bundle: &str) -> Result { + let proxy_config_k8s_resource_location = K8sResourceLocation::new(None, "Proxy", "cluster", "config.openshift.io"); + + let config = get_etcd_json(etcd_client, &proxy_config_k8s_resource_location) + .await? + .context("could not find proxy cluster config")?; + + let trusted_ca_configmap_name = config + .pointer("/spec/trustedCA/name") + .context("no trustedCA in proxy cluster config")? + .as_str() + .context("trustedCA not a string")?; + + let ca_configmap_k8s_resource_location = + K8sResourceLocation::new(Some("openshift-config"), "ConfigMap", trusted_ca_configmap_name, "v1"); + + let mut configmap = get_etcd_json(etcd_client, &ca_configmap_k8s_resource_location) + .await? + .context("could not find trustedCA configmap")?; + + let data = configmap + .pointer_mut("/data") + .context("no /data in configmap")? + .as_object_mut() + .context("/data not an object")?; + + let original_additional_trust_bundle = data.insert( + "ca-bundle.crt".to_string(), + serde_json::Value::String(additional_trust_bundle.to_string()), + ); + + put_etcd_yaml(etcd_client, &ca_configmap_k8s_resource_location, configmap).await?; + + Ok(original_additional_trust_bundle + .context("no ca-bundle.crt in trustedCA configmap")? + .as_str() + .context("ca-bundle.crt not a string")? + .to_string()) +} + +pub(crate) async fn fix_monitoring_configmaps(etcd_client: &InMemoryK8sEtcd, new_merged_bundle: &str) -> Result<()> { + join_all( + etcd_client + .list_keys("configmaps/openshift-monitoring/") + .await? + .into_iter() + .map(|key| async move { + let etcd_result = etcd_client + .get(key.clone()) + .await + .with_context(|| format!("getting key {:?}", key))? + .context("key disappeared")?; + let value: Value = serde_json::from_slice(etcd_result.value.as_slice()) + .with_context(|| format!("deserializing value of key {:?}", key,))?; + + let k8s_resource_location = K8sResourceLocation::try_from(&value)?; + + let regex = &Regex::new(r"(?P.*)-trusted-ca-bundle-(?P[0-9a-z]+)").context("compiling regex")?; + let matches = regex.captures(&k8s_resource_location.name); + + let matches = match matches { + Some(matches) => matches, + None => return Ok(()), + }; + + let component = matches.name("component").context("no component")?.as_str(); + + let mut configmap = get_etcd_json(etcd_client, &k8s_resource_location) + .await? + .context("no machineconfig")?; + + let current_data = configmap + .pointer("/data/ca-bundle.crt") + .context("no ca-bundle.crt in configmap")? + .as_str() + .context("ca-bundle.crt not a string")? + .as_bytes(); + + let recert_calculated_original_hash = go_base32_encode(fnv1_64(current_data)); + + let operator_calculated_hash = configmap + .pointer("/metadata/labels/monitoring.openshift.io~1hash") + .context("no monitoring.openshift.io/hash in configmap")? + .as_str() + .context("monitoring.openshift.io/hash not a string")?; + + // Sanity check to make sure our hash function is compatible with the one used by + // the monitoring operator + ensure!( + recert_calculated_original_hash == operator_calculated_hash, + format!("hash mismatch: {} != {}", recert_calculated_original_hash, operator_calculated_hash) + ); + + let new_hash = go_base32_encode(fnv1_64(new_merged_bundle.as_bytes())); + + configmap + .pointer_mut("/metadata/labels") + .context("no /metadata/labels in configmap")? + .as_object_mut() + .context("/metadata/labels not an object")? + .insert( + "monitoring.openshift.io/hash".to_string(), + serde_json::Value::String(new_hash.clone()), + ); + + let data = configmap + .pointer_mut("/data") + .context("no /data in configmap")? + .as_object_mut() + .context("/data not an object")?; + + data.insert( + "ca-bundle.crt".to_string(), + serde_json::Value::String(new_merged_bundle.to_string()), + ); + + let new_resource_location = K8sResourceLocation::new( + k8s_resource_location.namespace.as_deref(), + &k8s_resource_location.kind, + &format!("{component}-trusted-ca-bundle-{new_hash}"), + &k8s_resource_location.apiversion, + ); + + put_etcd_yaml(etcd_client, &new_resource_location, configmap) + .await + .context("putting new configmap")?; + + etcd_client.delete(&key).await.context("deleting old configmap")?; + + Ok(()) + }), + ) + .await + .into_iter() + .collect::>>()?; + + Ok(()) +} + +pub(crate) async fn fix_kcm_openshift_user_ca(etcd_client: &InMemoryK8sEtcd, additional_trust_bundle: &str) -> Result<()> { + let k8s_resource_location = K8sResourceLocation::new(Some("openshift-controller-manager"), "ConfigMap", "openshift-user-ca", "v1"); + + let mut configmap = get_etcd_json(etcd_client, &k8s_resource_location).await?.context("no configmap")?; + + let data = configmap + .pointer_mut("/data") + .context("no /data in configmap")? + .as_object_mut() + .context("/data not an object")?; + + data.insert( + "ca-bundle.crt".to_string(), + serde_json::Value::String(additional_trust_bundle.to_string()), + ); + + put_etcd_yaml(etcd_client, &k8s_resource_location, configmap).await?; + + Ok(()) +} diff --git a/src/ocp_postprocess/additional_trust_bundle/filesystem_rename.rs b/src/ocp_postprocess/additional_trust_bundle/filesystem_rename.rs new file mode 100644 index 00000000..debc291b --- /dev/null +++ b/src/ocp_postprocess/additional_trust_bundle/filesystem_rename.rs @@ -0,0 +1,95 @@ +use crate::file_utils::{self, commit_file, read_file_to_string}; +use anyhow::{self, Context, Result}; +use futures_util::future::join_all; +use serde_json::Value; +use std::path::Path; + +use super::utils::fix_machineconfig; + +pub(crate) async fn fix_filesystem_ca_trust_anchors(additional_trust_bundle: &str, dir: &Path) -> Result<()> { + join_all( + file_utils::globvec(dir, "**/anchors/openshift-config-user-ca-bundle.crt")? + .into_iter() + .map(|file_path| { + let crt_file_path = file_path.clone(); + let additional_trust_bundle = additional_trust_bundle.to_string(); + tokio::spawn(async move { + async move { + commit_file(file_path, additional_trust_bundle.clone()) + .await + .context("writing to disk")?; + + anyhow::Ok(()) + } + .await + .context("fixing system CA bundle") + }) + }), + ) + .await + .into_iter() + .collect::, _>>()? + .into_iter() + .collect::>>()?; + + Ok(()) +} + +pub(crate) async fn fix_filesystem_currentconfig(additional_trust_bundle: &str, dir: &Path) -> Result<()> { + join_all(file_utils::globvec(dir, "**/currentconfig")?.into_iter().map(|file_path| { + let kcm_config_path = file_path.clone(); + let additional_trust_bundle = additional_trust_bundle.to_string(); + tokio::spawn(async move { + async move { + let contents = read_file_to_string(&file_path) + .await + .context("reading currentconfig")?; + let mut config: Value = serde_json::from_str(&contents).context("parsing currentconfig")?; + + fix_machineconfig(&mut config, &additional_trust_bundle)?; + + commit_file(file_path, serde_json::to_string(&config).context("serializing currentconfig")?) + .await + .context("writing currentconfig to disk")?; + + anyhow::Ok(()) + } + .await + .context(format!("fixing currentconfig {:?}", kcm_config_path)) + }) + })) + .await + .into_iter() + .collect::, _>>()? + .into_iter() + .collect::>>()?; + + Ok(()) +} + +pub(crate) async fn fix_static_configmap_trusted_ca_bundle(new_merged_bundle: &str, dir: &Path) -> Result<()> { + join_all( + file_utils::globvec(dir, "**/configmaps/trusted-ca-bundle/ca-bundle.crt")? + .into_iter() + .map(|file_path| { + let ca_bundle_path = file_path.clone(); + let new_merged_bundle = new_merged_bundle.to_string(); + tokio::spawn(async move { + async move { + commit_file(file_path, new_merged_bundle.clone()).await.context("writing to disk")?; + + anyhow::Ok(()) + } + .await + .context(format!("fixing static configmap trusted ca bundle {:?}", ca_bundle_path)) + }) + }), + ) + .await + .into_iter() + .collect::, _>>()? + .into_iter() + .collect::>>()?; + + Ok(()) +} diff --git a/src/ocp_postprocess/additional_trust_bundle/utils.rs b/src/ocp_postprocess/additional_trust_bundle/utils.rs new file mode 100644 index 00000000..30d25283 --- /dev/null +++ b/src/ocp_postprocess/additional_trust_bundle/utils.rs @@ -0,0 +1,88 @@ +use crate::{ + cluster_crypto::locations::K8sResourceLocation, + file_utils, + k8s_etcd::{get_etcd_json, InMemoryK8sEtcd}, +}; +use anyhow::{Context, Result}; +use serde_json::Value; + +pub(crate) fn fix_machineconfig(machineconfig: &mut Value, additional_trust_bundle: &str) -> Result<()> { + let pointer_mut = machineconfig.pointer_mut("/spec/config/storage/files"); + if pointer_mut.is_none() { + // Not all machineconfigs have files to look at and that's ok + return Ok(()); + }; + + let find_map = pointer_mut + .context("no /spec/config/storage/files")? + .as_array_mut() + .context("files not an array")? + .iter_mut() + .find_map(|file| { + (file.pointer("/path")? == "/etc/pki/ca-trust/source/anchors/openshift-config-user-ca-bundle.crt").then_some(file) + }); + + if find_map.is_none() { + // Not all machineconfigs have the file we're looking for and that's ok + return Ok(()); + }; + + let file_contents = find_map + .context("no /etc/kubernetes/apiserver-url.env file in machineconfig")? + .pointer_mut("/contents") + .context("no .contents")? + .as_object_mut() + .context("annotations not an object")?; + + file_contents.insert( + "source".to_string(), + serde_json::Value::String(file_utils::dataurl_encode(additional_trust_bundle)), + ); + + Ok(()) +} + +pub(crate) async fn get_merged_bundle(etcd_client: &InMemoryK8sEtcd) -> Result { + let k8s_resource_location = K8sResourceLocation::new(Some("openshift-config-managed"), "ConfigMap", "trusted-ca-bundle", "v1"); + + let config = get_etcd_json(etcd_client, &k8s_resource_location) + .await + .context("failed to get trusted-ca-bundle configmap")? + .context("could not find trusted-ca-bundle configmap")?; + + let data = config + .pointer("/data/ca-bundle.crt") + .context("no ca-bundle.crt in trusted-ca-bundle configmap")? + .as_str() + .context("ca-bundle.crt not a string")?; + + Ok(data.to_string()) +} + +/// There's no place where we can get just the system certificates, that don't already contain the +/// seed's additional trust bundle, so we have to derive it ourselves by taking the entire merged +/// bundle and removing from it just the certs that also appear in the seed's additional trust +/// bundle. What's left after removal should be just the seed's system certs +pub(crate) fn derive_system_certs_from_merged_bundle(original_additional_trust_bundle: String, merged_bundle: String) -> Result { + let last_original_cert = pem::parse_many(original_additional_trust_bundle.as_bytes()) + .context("failed to parse original additional trust bundle")? + .into_iter() + .last() + .context("no certs in original additional trust bundle")?; + + let last_original_cert_encoded = + pem::encode_config(&last_original_cert, pem::EncodeConfig::new().set_line_ending(pem::LineEnding::LF)).to_string(); + let position_of_last_original_cert = merged_bundle + .find(&last_original_cert_encoded) + .context("last original cert not found in merged bundle")?; + + let system_certs = merged_bundle + .get(position_of_last_original_cert + last_original_cert_encoded.len()..) + .context("failed to get system certs")?; + + Ok(system_certs.to_string()) +} + +pub(crate) fn merge_bundles(additional_trust_bundle: &str, system_certs: &str) -> String { + format!("{}{}", additional_trust_bundle, system_certs) +} diff --git a/src/ocp_postprocess/cluster_domain_rename.rs b/src/ocp_postprocess/cluster_domain_rename.rs index 12b1d771..b85beec6 100644 --- a/src/ocp_postprocess/cluster_domain_rename.rs +++ b/src/ocp_postprocess/cluster_domain_rename.rs @@ -1,5 +1,5 @@ use self::params::ClusterNamesRename; -use crate::{cluster_crypto::locations::K8sResourceLocation, config::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; +use crate::{cluster_crypto::locations::K8sResourceLocation, config::path::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; use anyhow::{Context, Result}; use std::{path::Path, sync::Arc}; diff --git a/src/ocp_postprocess/cluster_domain_rename/params.rs b/src/ocp_postprocess/cluster_domain_rename/params.rs index caff0ea5..781423d9 100644 --- a/src/ocp_postprocess/cluster_domain_rename/params.rs +++ b/src/ocp_postprocess/cluster_domain_rename/params.rs @@ -8,7 +8,7 @@ pub(crate) struct ClusterNamesRename { } impl ClusterNamesRename { - pub(crate) fn cli_parse(value: &str) -> Result { + pub(crate) fn parse(value: &str) -> Result { let parts = value.split(':').collect::>(); ensure!( diff --git a/src/ocp_postprocess/fnv.rs b/src/ocp_postprocess/fnv.rs index fe44616d..6f96a9f6 100644 --- a/src/ocp_postprocess/fnv.rs +++ b/src/ocp_postprocess/fnv.rs @@ -6,3 +6,12 @@ pub(crate) fn fnv1_32(data: &[u8]) -> u32 { } hash } + +pub(crate) fn fnv1_64(data: &[u8]) -> u64 { + let mut hash = 0xcbf29ce484222325u64; + for byte in data { + hash = hash.wrapping_mul(0x100000001b3); + hash ^= u64::from(*byte); + } + hash +} diff --git a/src/ocp_postprocess/go_base32.rs b/src/ocp_postprocess/go_base32.rs new file mode 100644 index 00000000..dfa96628 --- /dev/null +++ b/src/ocp_postprocess/go_base32.rs @@ -0,0 +1,65 @@ +const BASE32_DIGITS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz"; +const BASE32: u64 = 32; +const BASE32_MASK: u64 = BASE32 - 1; +const BASE32_SHIFT: u64 = 5; +const MAX_BASE32_U64_DIGITS: usize = 64 + 1; + +pub(crate) fn base32_encode(mut num: u64) -> String { + let mut output_array = [0u8; MAX_BASE32_U64_DIGITS]; + let mut output_index = output_array.len(); + + while num >= BASE32 { + output_index -= 1; + output_array[output_index] = BASE32_DIGITS[(num & BASE32_MASK) as usize]; + + num >>= BASE32_SHIFT; + } + + output_index -= 1; + output_array[output_index] = BASE32_DIGITS[num as usize]; + + String::from_utf8(output_array[output_index..].to_vec()).unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + // package main + // import ( + // "math/rand" + // "fmt" + // "strconv" + // ) + // func main() { + // for i := 0; i < 10; i++ { + // fmt.Printf("assert_eq!(base32_encode(%d), %q);\n", i, strconv.FormatUint(uint64(i), 32)) + // } + // for i := 0; i < 10; i++ { + // x := rand.Uint64() + // fmt.Printf("assert_eq!(base32_encode(%d), %q);\n", x, strconv.FormatUint(x, 32)) + // } + // } + fn test_base32_encode() { + assert_eq!(base32_encode(0), "0"); + assert_eq!(base32_encode(1), "1"); + assert_eq!(base32_encode(2), "2"); + assert_eq!(base32_encode(3), "3"); + assert_eq!(base32_encode(4), "4"); + assert_eq!(base32_encode(5), "5"); + assert_eq!(base32_encode(6), "6"); + assert_eq!(base32_encode(7), "7"); + assert_eq!(base32_encode(8), "8"); + assert_eq!(base32_encode(9), "9"); + assert_eq!(base32_encode(9571486601897812948), "89l60b8v8f8uk"); + assert_eq!(base32_encode(14972217520619435323), "cvi00g906dd9r"); + assert_eq!(base32_encode(828543677970655149), "mvsk2nb5c9td"); + assert_eq!(base32_encode(10165118770545495894), "8q4e1l8l0a3qm"); + assert_eq!(base32_encode(13003616107026089108), "b8tgvug3dd14k"); + assert_eq!(base32_encode(17310030582050129450), "f0ecl38b3u1ha"); + assert_eq!(base32_encode(5246586088816823604), "4hjsu805mtj9k"); + assert_eq!(base32_encode(9506301410689701343), "87r9aogrv8lev"); + assert_eq!(base32_encode(17418413831566181087), "f3el32vak1gmv"); + assert_eq!(base32_encode(10879890617200402116), "9dv92rt4lbnm4"); + } +} diff --git a/src/ocp_postprocess/hostname_rename.rs b/src/ocp_postprocess/hostname_rename.rs index d8a9c178..15712204 100644 --- a/src/ocp_postprocess/hostname_rename.rs +++ b/src/ocp_postprocess/hostname_rename.rs @@ -1,4 +1,4 @@ -use crate::{config::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; +use crate::{config::path::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; use anyhow::{Context, Result}; use std::{path::Path, sync::Arc}; diff --git a/src/ocp_postprocess/ip_rename.rs b/src/ocp_postprocess/ip_rename.rs index ffb90be5..ad7ae404 100644 --- a/src/ocp_postprocess/ip_rename.rs +++ b/src/ocp_postprocess/ip_rename.rs @@ -1,4 +1,4 @@ -use crate::{config::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; +use crate::{config::path::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; use anyhow::{Context, Result}; use std::{path::Path, sync::Arc}; diff --git a/src/ocp_postprocess/pull_secret_rename.rs b/src/ocp_postprocess/pull_secret_rename.rs index 8cb648fc..d08cdd06 100644 --- a/src/ocp_postprocess/pull_secret_rename.rs +++ b/src/ocp_postprocess/pull_secret_rename.rs @@ -1,4 +1,4 @@ -use crate::{config::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; +use crate::{config::path::ConfigPath, k8s_etcd::InMemoryK8sEtcd}; use anyhow::{Context, Result}; use std::{path::Path, sync::Arc}; diff --git a/src/recert.rs b/src/recert.rs index 3289a803..51cf87c6 100644 --- a/src/recert.rs +++ b/src/recert.rs @@ -1,6 +1,6 @@ use crate::{ cluster_crypto::{crypto_utils::ensure_openssl_version, scanning, ClusterCryptoObjects}, - config::{ClusterCustomizations, ConfigPath, CryptoCustomizations, RecertConfig}, + config::{ClusterCustomizations, CryptoCustomizations, RecertConfig}, k8s_etcd::InMemoryK8sEtcd, ocp_postprocess::ocp_postprocess, rsa_key_pool, server_ssh_keys, @@ -21,8 +21,6 @@ pub(crate) async fn run(recert_config: &RecertConfig, cluster_crypto: &mut Clust let recertify_timing = recertify( cluster_crypto, Arc::clone(&in_memory_etcd_client), - recert_config.static_dirs.clone(), - recert_config.static_files.clone(), &recert_config.crypto_customizations, ) .await @@ -32,8 +30,6 @@ pub(crate) async fn run(recert_config: &RecertConfig, cluster_crypto: &mut Clust Arc::clone(&in_memory_etcd_client), cluster_crypto, &recert_config.cluster_customizations, - &recert_config.static_dirs, - &recert_config.static_files, recert_config.regenerate_server_ssh_keys.as_deref(), recert_config.dry_run, ) @@ -58,8 +54,6 @@ async fn get_etcd_endpoint(recert_config: &RecertConfig) -> Result, - static_dirs: Vec, - static_files: Vec, crypto_customizations: &CryptoCustomizations, ) -> Result { let external_certs = if in_memory_etcd_client.etcd_client.is_some() { @@ -74,8 +68,8 @@ async fn recertify( // a long time and are independent let all_discovered_crypto_objects = tokio::spawn(scanning::crypto_scan( in_memory_etcd_client, - static_dirs, - static_files, + crypto_customizations.dirs.clone(), + crypto_customizations.files.clone(), external_certs.clone(), )); let rsa_keys = tokio::spawn(fill_keys()); @@ -108,8 +102,6 @@ async fn finalize( in_memory_etcd_client: Arc, cluster_crypto: &mut ClusterCryptoObjects, cluster_customizations: &ClusterCustomizations, - static_dirs: &Vec, - static_files: &Vec, regenerate_server_ssh_keys: Option<&Path>, dry_run: bool, ) -> Result { @@ -122,7 +114,7 @@ async fn finalize( let start = std::time::Instant::now(); if in_memory_etcd_client.etcd_client.is_some() { - ocp_postprocess(&in_memory_etcd_client, cluster_customizations, static_dirs, static_files) + ocp_postprocess(&in_memory_etcd_client, cluster_customizations) .await .context("performing ocp specific post-processing")?; } diff --git a/src/use_cert.rs b/src/use_cert.rs index fc2b40f6..cd50ee4b 100644 --- a/src/use_cert.rs +++ b/src/use_cert.rs @@ -18,7 +18,7 @@ impl std::fmt::Display for UseCert { } impl UseCert { - pub(crate) fn cli_parse(cert_path_or_pem: &str) -> Result { + pub(crate) fn parse(cert_path_or_pem: &str) -> Result { let pem = pem::parse_many(if cert_path_or_pem.contains('\n') { cert_path_or_pem.as_bytes().to_vec() } else { diff --git a/src/use_key.rs b/src/use_key.rs index 8bdce6f1..c35104fd 100644 --- a/src/use_key.rs +++ b/src/use_key.rs @@ -40,7 +40,7 @@ impl std::fmt::Display for UseKey { } impl UseKey { - pub(crate) fn cli_parse(value: &str) -> Result { + pub(crate) fn parse(value: &str) -> Result { // TODO: ' ' is legacy, remove eventually let parts = if value.contains(':') { value.split(':') } else { value.split(' ') }.collect::>();