From bf0ea9cfaf33dcfe0d0ac5a273a516069f13338d Mon Sep 17 00:00:00 2001 From: tomjo Date: Thu, 28 Dec 2023 12:41:09 +0100 Subject: [PATCH] feat!: Rename project from bitwarden-operator to warden-secret-operator to respect trademarks --- .github/workflows/build.yaml | 6 +- Cargo.toml | 2 +- Dockerfile | 4 +- README.md | 54 ++--- .../crd/wardensecrets.tomjo.net.yaml | 14 +- deploy/manifests/README.md | 2 +- .../bitwarden-operator.configmap.yaml | 4 +- .../bitwarden-operator.deployment.yaml | 18 +- deploy/manifests/bitwarden-operator.sa.yaml | 4 +- .../manifests/bitwarden-operator.service.yaml | 6 +- src/bw.rs | 63 +++--- src/crd/mod.rs | 4 +- src/crd/v1.rs | 16 +- src/crd/v2.rs | 20 +- src/main.rs | 198 +++++++++--------- ...o.net.yaml => wardensecrets.tomjo.net.yaml | 14 +- 16 files changed, 216 insertions(+), 213 deletions(-) rename bitwardensecrets.tomjo.net.yaml => deploy/crd/wardensecrets.tomjo.net.yaml (92%) rename deploy/crd/bitwardensecrets.tomjo.net.yaml => wardensecrets.tomjo.net.yaml (92%) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0bb5c21..17f6a26 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -81,15 +81,15 @@ jobs: if: ${{ inputs.upload }} uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 with: - name: bitwarden-operator - path: target/release/bitwarden-operator + name: warden-secret-operator + path: target/release/warden-secret-operator - name: Add binary to release if: ${{ github.event_name == 'release' && inputs.upload }} uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 with: tag_name: ${{ inputs.version }} files: | - bitwarden-operator + warden-secret-operator - name: Install cosign if: ${{ inputs.sign }} uses: sigstore/cosign-installer@9614fae9e5c5eddabb09f90a270fcb487c9f7149 # v3.3.0 diff --git a/Cargo.toml b/Cargo.toml index bc80a07..2306720 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bitwarden-operator" +name = "warden-secret-operator" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/Dockerfile b/Dockerfile index 3ac3771..0b44c41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ RUN apt-get update \ USER 200000 -COPY /target/release/bitwarden-operator /bitwarden-operator +COPY /target/release/warden-secret-operator /warden-secret-operator WORKDIR / -CMD ["/bitwarden-operator"] +CMD ["/warden-secret-operator"] diff --git a/README.md b/README.md index 56440c4..f32942e 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,14 @@ -# bitwarden-operator +# warden-secret-operator -Kubernetes operator written in Rust to provision Kubernetes Secret resources sourced from a Bitwarden/Vaultwarden vault. +warden-secret-operator is a Kubernetes Operator written in Rust using [kube-rs](https://kube.rs) to provision Kubernetes Secret resources sourced from a [Bitwarden](https://bitwarden.com)/[Vaultwarden vault](https://github.com/dani-garcia/vaultwarden). -## Motivation +## Motivation / Disclaimer This was written to scratch my own urge, using Vaultwarden as a source for secrets in my homelab Kubernetes environment. -As well as getting my hands dirty with Rust for the first time. +As well as getting my hands dirty with Rust for the *first time*. This means the code is probably far from idiomatic, efficient or sane; please suggest improvements! -## Usage - -Create a BitwardenSecret resource that references your secret but does not contain it, -this means it is safe to commit to source control. - -The type in the BitwardenSecret spec will be used as the type for the Kubernetes Secret resource. -Labels and annotations will also appear on the created secret. - -The item field in the spec references the secret in the vault, -it should be in the format `[collection]/secret` where collection is optional. - -```yaml -apiVersion: bitwarden-operator.k8s.io/v1alpha1 -kind: BitwardenSecret -metadata: - name: example -spec: - item: my-collection/my-secret - type: Opaque -``` +## Getting started ### Prerequisites @@ -55,7 +36,7 @@ organization = "my-bitwarden-organization-uuid" #### Environment variables -All configuration environment variables are prefixed with `BW_OPERATOR_`. Followed by the name of the configuration key, +All configuration environment variables are prefixed with `BW_OPERATOR_`. Followed by the name of the configuration key, where the key is in uppercase and words are separated by underscores. #### Options @@ -70,3 +51,26 @@ where the key is in uppercase and words are separated by underscores. * **webserver_tls** - enables TLS for the webserver | **Default:** `false` * **tls_cert_path** - path to the certificate used when TLS is enabled | **Default:** `/certs/tls.crt` * **tls_key_path** - path to the certificate key used when TLS is enabled | **Default:** `/certs/tls.key` + +### Usage + +Create a WardenSecret resource that references your secret but does not contain it, +this means it is safe to commit to source control. + +The type in the WardenSecret spec will be used as the type for the Kubernetes Secret resource. +Labels and annotations will also appear on the created secret. + +The item field in the spec references the secret in the vault, +it should be in the format `[collection]/secret` where collection is optional. + +All associated fields and attachment of the vault secret will be mapped to the kubernetes secret. + +```yaml +apiVersion: warden-secret-operator.k8s.io/v1alpha1 +kind: WardenSecret +metadata: + name: example +spec: + item: my-collection/my-secret + type: Opaque +``` diff --git a/bitwardensecrets.tomjo.net.yaml b/deploy/crd/wardensecrets.tomjo.net.yaml similarity index 92% rename from bitwardensecrets.tomjo.net.yaml rename to deploy/crd/wardensecrets.tomjo.net.yaml index 9597dcb..01e22e1 100644 --- a/bitwardensecrets.tomjo.net.yaml +++ b/deploy/crd/wardensecrets.tomjo.net.yaml @@ -1,7 +1,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: bitwardensecrets.tomjo.net + name: wardensecrets.tomjo.net namespace: default # For easier deployment and avoid permissions collisions on most clusters, the resource is namespace-scoped. More information at: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/ spec: group: tomjo.net @@ -15,15 +15,15 @@ spec: # - v1 # clientConfig: # service: -# namespace: bitwarden-operator -# name: bitwarden-operator-webhook +# namespace: warden-secret-operator +# name: warden-secret-operator-webhook # path: /crdconvert names: - kind: BitwardenSecret - plural: bitwardensecrets - singular: bitwardensecret + kind: WardenSecret + plural: wardensecrets + singular: wardensecret shortNames: - - bitwardensecret + - wardensecret - bw scope: Namespaced versions: diff --git a/deploy/manifests/README.md b/deploy/manifests/README.md index f3f8fb1..af9ce04 100644 --- a/deploy/manifests/README.md +++ b/deploy/manifests/README.md @@ -2,7 +2,7 @@ This directory contains the manifests for a basic deployment of the application. -You will need to supply your own secret named bitwarden-operator containing following keys: +You will need to supply your own secret named warden-secret-operator containing following keys: - `BW_OPERATOR_USER`: Bitwarden user - `BW_OPERATOR_PASS`: Bitwarden password diff --git a/deploy/manifests/bitwarden-operator.configmap.yaml b/deploy/manifests/bitwarden-operator.configmap.yaml index af92838..c86f219 100644 --- a/deploy/manifests/bitwarden-operator.configmap.yaml +++ b/deploy/manifests/bitwarden-operator.configmap.yaml @@ -1,8 +1,8 @@ apiVersion: v1 kind: ConfigMap metadata: - name: bitwarden-operator + name: warden-secret-operator labels: - app.kubernetes.io/name: bitwarden-operator + app.kubernetes.io/name: warden-secret-operator data: RUST_LOG: "info" diff --git a/deploy/manifests/bitwarden-operator.deployment.yaml b/deploy/manifests/bitwarden-operator.deployment.yaml index cec6031..334a94d 100644 --- a/deploy/manifests/bitwarden-operator.deployment.yaml +++ b/deploy/manifests/bitwarden-operator.deployment.yaml @@ -2,34 +2,34 @@ apiVersion: apps/v1 kind: Deployment metadata: labels: - app.kubernetes.io/name: bitwarden-operator - name: bitwarden-operator + app.kubernetes.io/name: warden-secret-operator + name: warden-secret-operator spec: progressDeadlineSeconds: 600 replicas: 1 revisionHistoryLimit: 3 selector: matchLabels: - app.kubernetes.io/name: bitwarden-operator + app.kubernetes.io/name: warden-secret-operator strategy: type: RollingUpdate template: metadata: labels: - app.kubernetes.io/name: bitwarden-operator + app.kubernetes.io/name: warden-secret-operator spec: containers: - envFrom: - secretRef: - name: bitwarden-operator + name: warden-secret-operator - configMapRef: - name: bitwarden-operator - image: ghcr.io/tomjo/bitwarden-operator:latest + name: warden-secret-operator + image: ghcr.io/tomjo/warden-secret-operator:latest imagePullPolicy: Always - name: bitwarden-operator + name: warden-secret-operator ports: - containerPort: 8080 name: web protocol: TCP - serviceAccountName: bitwarden-operator + serviceAccountName: warden-secret-operator terminationGracePeriodSeconds: 30 diff --git a/deploy/manifests/bitwarden-operator.sa.yaml b/deploy/manifests/bitwarden-operator.sa.yaml index f737921..9239d70 100644 --- a/deploy/manifests/bitwarden-operator.sa.yaml +++ b/deploy/manifests/bitwarden-operator.sa.yaml @@ -1,6 +1,6 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: bitwarden-operator + name: warden-secret-operator labels: - app.kubernetes.io/name: bitwarden-operator + app.kubernetes.io/name: warden-secret-operator diff --git a/deploy/manifests/bitwarden-operator.service.yaml b/deploy/manifests/bitwarden-operator.service.yaml index d0ac6db..ab53b76 100644 --- a/deploy/manifests/bitwarden-operator.service.yaml +++ b/deploy/manifests/bitwarden-operator.service.yaml @@ -2,8 +2,8 @@ apiVersion: v1 kind: Service metadata: labels: - app.kubernetes.io/name: bitwarden-operator - name: bitwarden-operator + app.kubernetes.io/name: warden-secret-operator + name: warden-secret-operator spec: ports: - name: web @@ -11,5 +11,5 @@ spec: protocol: TCP targetPort: 8080 selector: - app.kubernetes.io/name: bitwarden-operator + app.kubernetes.io/name: warden-secret-operator type: ClusterIP diff --git a/src/bw.rs b/src/bw.rs index 8831ef8..c802bac 100644 --- a/src/bw.rs +++ b/src/bw.rs @@ -15,7 +15,7 @@ use thiserror::Error; static DEFAULT_INSTANCE_URL: &str = "https://vault.bitwarden.com"; #[derive(Clone)] -pub struct BitwardenClientWrapper { +pub struct WardenClientWrapper { bw_path: String, url: String, user: SecStr, @@ -24,10 +24,10 @@ pub struct BitwardenClientWrapper { session_token: Option, } -impl BitwardenClientWrapper { +impl WardenClientWrapper { #[must_use] pub fn new(config: &Config) -> Self { - let bw = BitwardenClientWrapper { + let bw = WardenClientWrapper { bw_path: config .get_string("bw_path") .unwrap_or("/usr/bin/bw".to_owned()), @@ -37,13 +37,13 @@ impl BitwardenClientWrapper { user: SecStr::from( config .get_string("user") - .expect("Bitwarden user not configured.") + .expect("User not configured.") .as_str(), ), password: SecStr::from( config .get_string("pass") - .expect("Bitwarden password not configured.") + .expect("Password not configured.") .as_str(), ), organization: config.get_string("organization").ok(), @@ -57,11 +57,11 @@ impl BitwardenClientWrapper { ], BTreeMap::new(), ) - .expect("Could not configure bitwarden server"); + .expect("Could not configure bitwarden/vaultwarden server"); return bw; } - fn find_collection_id(&self, collection: String) -> Result { + fn find_collection_id(&self, collection: String) -> Result { return if self.organization.is_some() { let org = self.organization.as_ref().unwrap().clone(); self.command_with_env(format!("bw list org-collections --organizationid '{org}' --search \"{collection}\" | jq -r -c '.[] | select( .name == \"{collection}\") | .id'"), self.create_session_env()) @@ -73,13 +73,13 @@ impl BitwardenClientWrapper { pub fn fetch_item_fields( &mut self, item: String, - ) -> Result, BitwardenCommandError> { + ) -> Result, WardenCommandError> { self.verify_session_token()?; let item_id: String = self.find_item_id(&item)?; return self.get_item_fields(&item_id); } - fn verify_session_token(&mut self) -> Result<(), BitwardenCommandError> { + fn verify_session_token(&mut self) -> Result<(), WardenCommandError> { if self.session_token.is_none() { self.session_token = Some(self.login()?); self.sync()?; @@ -90,13 +90,13 @@ impl BitwardenClientWrapper { pub fn fetch_item_attachments( &mut self, item: String, - ) -> Result, BitwardenCommandError> { + ) -> Result, WardenCommandError> { self.verify_session_token()?; let item_id: String = self.find_item_id(&item)?; return self.get_item_attachments(&item_id); } - pub fn sync(&mut self) -> Result<(), BitwardenCommandError> { + pub fn sync(&mut self) -> Result<(), WardenCommandError> { self.verify_session_token()?; self.command_with_env(format!("bw sync"), self.create_session_env())?; Ok(()) @@ -110,7 +110,7 @@ impl BitwardenClientWrapper { fn get_item_fields( &self, item_id: &str, - ) -> Result, BitwardenCommandError> { + ) -> Result, WardenCommandError> { let mut fields: BTreeMap = BTreeMap::new(); let json_fields: String = self.command_with_env( format!("bw get item '{item_id}' | jq '[select(.fields != null) | .fields[]]'"), @@ -126,7 +126,7 @@ impl BitwardenClientWrapper { fn get_item_attachments( &self, item_id: &str, - ) -> Result, BitwardenCommandError> { + ) -> Result, WardenCommandError> { let mut attachments: BTreeMap = BTreeMap::new(); let json_attachments: String = self.command_with_env(format!("bw get item '{item_id}' | jq '[select(.attachments != null) | .attachments[] | {{fileName: .fileName, id: .id}}]'"), self.create_session_env())?; let attachments_with_ids: Vec = serde_json::from_str(&json_attachments)?; @@ -142,7 +142,7 @@ impl BitwardenClientWrapper { return Ok(attachments); } - fn find_item_id(&self, item: &str) -> Result { + fn find_item_id(&self, item: &str) -> Result { let split_item = item.split("/").collect::>(); let item_name = split_item[1]; if split_item[0].len() > 0 { @@ -152,7 +152,7 @@ impl BitwardenClientWrapper { return self.command_with_env(format!("bw list items --search '{item_name}' | jq -r -c '.[] | select( .name == \"{item_name}\") | .id'"), self.create_session_env()); } - fn login(&self) -> Result { + fn login(&self) -> Result { let mut env: BTreeMap = BTreeMap::new(); env.insert( "BW_USER".to_string(), @@ -162,7 +162,7 @@ impl BitwardenClientWrapper { "BW_PASS".to_string(), String::from_utf8(self.password.unsecure().to_vec())?, ); - let login_result: Result = self.bw_command_with_env( + let login_result: Result = self.bw_command_with_env( vec![ "login".to_owned(), "$BW_USER".to_owned(), @@ -172,19 +172,18 @@ impl BitwardenClientWrapper { env, ); if login_result.is_ok() { - debug!("Bitwarden: Logged in"); return Ok(SecStr::from(login_result.unwrap())); } - let err: BitwardenCommandError = login_result.unwrap_err(); + let err: WardenCommandError = login_result.unwrap_err(); return Err(match err.to_string().as_str() { "Email address is invalid." | "Username or password is incorrect. Try again" => { - BitwardenCommandError::InvalidCredentials(err.to_string()) + WardenCommandError::InvalidCredentials(err.to_string()) } - _ => BitwardenCommandError::Other(err.to_string()), + _ => WardenCommandError::Other(err.to_string()), }); } - fn bw_command(&self, args: Vec) -> Result { + fn bw_command(&self, args: Vec) -> Result { if args[0] == "login" || args[0] == "logout" { return self.bw_command_with_env(args, BTreeMap::new()); } @@ -209,7 +208,7 @@ impl BitwardenClientWrapper { &self, args: Vec, env: BTreeMap, - ) -> Result { + ) -> Result { return self.command_with_env(format!("{} {}", self.bw_path, args.join(" ")), env); } @@ -217,7 +216,7 @@ impl BitwardenClientWrapper { &self, command: String, env: BTreeMap, - ) -> Result { + ) -> Result { #[cfg(not(target_os = "windows"))] let shell: &str = "/bin/bash"; #[cfg(not(target_os = "windows"))] @@ -243,7 +242,7 @@ impl BitwardenClientWrapper { if output.status.success() { return Ok(out); } - return Err(BitwardenCommandError::BitwardenCommandError(err)); + return Err(WardenCommandError::WardenCommandError(err)); } } @@ -272,9 +271,9 @@ pub struct ItemField { } #[derive(Debug, Error, Eq, PartialEq)] -pub enum BitwardenCommandError { +pub enum WardenCommandError { #[error("Bitwarden CLI error {0}")] - BitwardenCommandError(String), + WardenCommandError(String), #[error("Session expired: {0}")] SessionExpired(String), #[error("Locked: {0}")] @@ -287,20 +286,20 @@ pub enum BitwardenCommandError { Other(String), } -impl From for BitwardenCommandError { +impl From for WardenCommandError { fn from(err: FromUtf8Error) -> Self { - BitwardenCommandError::IO(err.to_string()) + WardenCommandError::IO(err.to_string()) } } -impl From for BitwardenCommandError { +impl From for WardenCommandError { fn from(err: io::Error) -> Self { - BitwardenCommandError::IO(err.to_string()) + WardenCommandError::IO(err.to_string()) } } -impl From for BitwardenCommandError { +impl From for WardenCommandError { fn from(err: serde_json::Error) -> Self { - BitwardenCommandError::IO(err.to_string()) + WardenCommandError::IO(err.to_string()) } } diff --git a/src/crd/mod.rs b/src/crd/mod.rs index e8f5030..6018ce2 100644 --- a/src/crd/mod.rs +++ b/src/crd/mod.rs @@ -2,10 +2,10 @@ pub mod v1; pub mod v2; pub use self::v2::{ - get_api_version, ApplyCondition, BitwardenSecret, BitwardenSecretStatus, + get_api_version, ApplyCondition, WardenSecret, WardenSecretStatus, ConditionStatus, ConditionType, }; pub fn get_kind() -> &'static str { - return "BitwardenSecret"; + return "WardenSecret"; } diff --git a/src/crd/v1.rs b/src/crd/v1.rs index f55286c..10fc2b8 100644 --- a/src/crd/v1.rs +++ b/src/crd/v1.rs @@ -2,27 +2,27 @@ use kube::CustomResource; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -/// Struct corresponding to the Specification (`spec`) part of the `BitwardenSecret` resource, directly -/// reflects context of the `bitwardensecrets.tomjo.net.yaml` file to be found in this repository. -/// The `BitwardenSecret` struct will be generated by the `CustomResource` derive macro. +/// Struct corresponding to the Specification (`spec`) part of the `WardenSecret` resource, directly +/// reflects context of the `wardensecrets.tomjo.net.yaml` file to be found in this repository. +/// The `WardenSecret` struct will be generated by the `CustomResource` derive macro. #[derive(CustomResource, Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)] #[kube( group = "tomjo.net", version = "v1", - kind = "BitwardenSecret", - plural = "bitwardensecrets", + kind = "WardenSecret", + plural = "wardensecrets", derive = "PartialEq", - status = "BitwardenSecretStatus", + status = "WardenSecretStatus", namespaced )] -pub struct BitwardenSecretSpec { +pub struct WardenSecretSpec { #[serde(alias = "type")] pub type_: String, pub item: String, } #[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, PartialEq)] -pub struct BitwardenSecretStatus { +pub struct WardenSecretStatus { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub conditions: Vec, pub phase: Option, diff --git a/src/crd/v2.rs b/src/crd/v2.rs index 564c402..13eb7a1 100644 --- a/src/crd/v2.rs +++ b/src/crd/v2.rs @@ -3,27 +3,27 @@ use schemars::JsonSchema; use serde::{de, Deserialize, Serialize}; use serde_json::Value; -/// Struct corresponding to the Specification (`spec`) part of the `BitwardenSecret` resource, directly -/// reflects context of the `bitwardensecrets.tomjo.net.yaml` file to be found in this repository. -/// The `BitwardenSecret` struct will be generated by the `CustomResource` derive macro. +/// Struct corresponding to the Specification (`spec`) part of the `WardenSecret` resource, directly +/// reflects context of the `wardensecrets.tomjo.net.yaml` file to be found in this repository. +/// The `WardenSecret` struct will be generated by the `CustomResource` derive macro. #[derive(CustomResource, Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)] #[kube( group = "tomjo.net", version = "v2", - kind = "BitwardenSecret", - plural = "bitwardensecrets", + kind = "WardenSecret", + plural = "wardensecrets", derive = "PartialEq", - status = "BitwardenSecretStatus", + status = "WardenSecretStatus", namespaced )] -pub struct BitwardenSecretSpec { +pub struct WardenSecretSpec { #[serde(alias = "type")] pub type_: String, pub item: String, } #[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, PartialEq)] -pub struct BitwardenSecretStatus { +pub struct WardenSecretStatus { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub conditions: Vec, #[serde(rename = "startTime")] @@ -118,7 +118,7 @@ impl Default for ConditionType { } } -impl BitwardenSecretStatus { +impl WardenSecretStatus { pub fn is_ready(&self) -> bool { for i in 0..self.conditions.len() { if self.conditions[i].type_ == ConditionType::Ready { @@ -129,7 +129,7 @@ impl BitwardenSecretStatus { } } -impl BitwardenSecret { +impl WardenSecret { pub fn get_observed_generation(&self) -> Option { return self .status diff --git a/src/main.rs b/src/main.rs index c540d14..6d767ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,9 +28,9 @@ use tokio::sync::{Mutex, MutexGuard}; use tokio::time::Duration; use warp::Filter; -use crate::bw::{BitwardenClientWrapper, BitwardenCommandError}; +use crate::bw::{WardenClientWrapper, WardenCommandError}; use crate::crd::{ - get_api_version, get_kind, ApplyCondition, BitwardenSecret, BitwardenSecretStatus, ConditionStatus, ConditionType, + get_api_version, get_kind, ApplyCondition, WardenSecret, WardenSecretStatus, ConditionStatus, ConditionType, }; mod bw; @@ -48,7 +48,7 @@ const DEFAULT_TLS_CERT_PATH: &'static str = "/certs/tls.crt"; const DEFAULT_TLS_KEY_PATH: &'static str = "/certs/tls.key"; const DEFAULT_NOOP_REQUEUE_DELAY: u64 = 300; -// TODO Watch secret deletion, if owner refs contains a bitwardensecret, recreate +// TODO Watch secret deletion, if owner refs contains a wardensecret, recreate // TODO refresh interval in spec (optional) -> default 5min? // TODO config option vault sync interval // TODO exponential backoff on error @@ -66,13 +66,13 @@ async fn main() { setup_webserver(&config); - let bw_client = BitwardenClientWrapper::new(&config); + let bw_client = WardenClientWrapper::new(&config); let kubernetes_client: Client = Client::try_default() .await .expect("Expected a valid KUBECONFIG environment variable."); - let api: Api = Api::all(kubernetes_client.clone()); + let api: Api = Api::all(kubernetes_client.clone()); let bw_mutex = Mutex::new(bw_client); let context: Arc = Arc::new(ContextData::new(kubernetes_client.clone(), bw_mutex)); @@ -80,18 +80,18 @@ async fn main() { // The controller comes from the `kube_runtime` crate and manages the reconciliation process. // It requires the following information: - // - `kube::Api` this controller "owns". In this case, `T = BitwardenSecret`, as this controller owns the `BitwardenSecret` resource, - // - `kube::api::ListParams` to select the `BitwardenSecret` resources with. Can be used for BitwardenSecret filtering `BitwardenSecret` resources before reconciliation, - // - `reconcile` function with reconciliation logic to be called each time a resource of `BitwardenSecret` kind is created/updated/deleted, + // - `kube::Api` this controller "owns". In this case, `T = WardenSecret`, as this controller owns the `WardenSecret` resource, + // - `kube::api::ListParams` to select the `WardenSecret` resources with. Can be used for WardenSecret filtering `WardenSecret` resources before reconciliation, + // - `reconcile` function with reconciliation logic to be called each time a resource of `WardenSecret` kind is created/updated/deleted, // - `on_error` function to call whenever reconciliation fails. Controller::new(api.clone(), WatcherConfig::default()) .run(reconcile, on_error, context) .for_each(|reconciliation_result| async move { match reconciliation_result { - Ok(bitwarden_secret_resource) => { + Ok(warden_secret_resource) => { trace!( "Reconciliation successful. Resource: {:?}", - bitwarden_secret_resource + warden_secret_resource ); } Err(reconciliation_err) => { @@ -109,11 +109,11 @@ fn setup_periodic_vault_sync(context: Arc, period: Duration) { loop { interval.tick().await; let mutex_guard_fut = context.bw_client.lock(); - let mut bw_client: MutexGuard = mutex_guard_fut.await; - debug!("Syncing BitWarden vault"); + let mut bw_client: MutexGuard = mutex_guard_fut.await; + debug!("Syncing vault"); let result = bw_client.sync(); if result.is_err() { - error!("Could not sync BitWarden vault: {}", result.err().unwrap()); + error!("Could not sync vault: {}", result.err().unwrap()); bw_client.reset(); } drop(bw_client); @@ -162,16 +162,16 @@ fn setup_webserver(config: &Config) { struct ContextData { client: Client, - bw_client: Mutex, + bw_client: Mutex, } impl ContextData { - pub fn new(client: Client, bw_client: Mutex) -> Self { + pub fn new(client: Client, bw_client: Mutex) -> Self { ContextData { client, bw_client } } } -enum BitwardenSecretAction { +enum WardenSecretAction { Create, Update, Delete, @@ -179,24 +179,24 @@ enum BitwardenSecretAction { } async fn reconcile( - bitwarden_secret: Arc, + warden_secret: Arc, context: Arc, ) -> Result { let client: Client = context.client.clone(); - let namespace: String = match bitwarden_secret.namespace() { + let namespace: String = match warden_secret.namespace() { None => "default".to_string(), Some(namespace) => namespace, }; - let name = bitwarden_secret.name_any(); + let name = warden_secret.name_any(); - return match determine_action(&bitwarden_secret) { - BitwardenSecretAction::Create => { - debug!("Creating BitwardenSecret: {}", name); - let bitwarden_secret_api: Api = + return match determine_action(&warden_secret) { + WardenSecretAction::Create => { + debug!("Creating WardenSecret: {}", name); + let warden_secret_api: Api = Api::namespaced(client.clone(), &namespace); - let mut status = BitwardenSecretStatus { + let mut status = WardenSecretStatus { start_time: Some(Utc::now().to_rfc3339()), conditions: vec![ApplyCondition { type_: ConditionType::Ready, @@ -205,10 +205,10 @@ async fn reconcile( message: None, reason: None, }], - observed_generation: bitwarden_secret.meta().generation, + observed_generation: warden_secret.meta().generation, }; - patch_status(&bitwarden_secret_api, &name, &status).await?; - add_finalizer(&bitwarden_secret_api, &name).await?; + patch_status(&warden_secret_api, &name, &status).await?; + add_finalizer(&warden_secret_api, &name).await?; status = update_ready_condition( status, ApplyCondition { @@ -219,32 +219,32 @@ async fn reconcile( reason: None, }, ); - patch_status(&bitwarden_secret_api, &name, &status).await?; + patch_status(&warden_secret_api, &name, &status).await?; update_secret_with_vault_secrets( - &bitwarden_secret, + &warden_secret, &client, &namespace, &name, - &bitwarden_secret_api, + &warden_secret_api, status, context, ) .await?; - info!("Created BitwardenSecret {:?}", bitwarden_secret); + info!("Created WardenSecret {:?}", warden_secret); Ok(Action::requeue(Duration::from_secs(10))) } - BitwardenSecretAction::Update => { - let bitwarden_secret_api: Api = + WardenSecretAction::Update => { + let warden_secret_api: Api = Api::namespaced(client.clone(), &namespace); - let status = bitwarden_secret_api + let status = warden_secret_api .get_status(&name) .await? .status .and_then(|mut x| { - x.observed_generation = bitwarden_secret.meta().generation; + x.observed_generation = warden_secret.meta().generation; Some(x) }) - .unwrap_or(BitwardenSecretStatus { + .unwrap_or(WardenSecretStatus { conditions: vec![ApplyCondition { type_: ConditionType::Ready, status: ConditionStatus::Unknown, @@ -252,28 +252,28 @@ async fn reconcile( message: None, reason: None, }], - observed_generation: bitwarden_secret.meta().generation, + observed_generation: warden_secret.meta().generation, start_time: Some(Utc::now().to_rfc3339()), }); update_secret_with_vault_secrets( - &bitwarden_secret, + &warden_secret, &client, &namespace, &name, - &bitwarden_secret_api, + &warden_secret_api, status, context, ) .await?; - info!("Updated BitwardenSecret {:?}", bitwarden_secret); + info!("Updated WardenSecret {:?}", warden_secret); Ok(Action::requeue(Duration::from_secs(10))) } - BitwardenSecretAction::Delete => { + WardenSecretAction::Delete => { delete_secret(client.clone(), &name, &namespace).await?; delete_finalizer(client, &name, &namespace).await?; Ok(Action::await_change()) } - BitwardenSecretAction::NoOp => Ok(Action::requeue(Duration::from_secs( + WardenSecretAction::NoOp => Ok(Action::requeue(Duration::from_secs( env::var(ENV_NOOP_REQUEUE_DELAY) .map(|x| { x.parse::() @@ -285,32 +285,32 @@ async fn reconcile( } async fn update_secret_with_vault_secrets( - bitwarden_secret: &Arc, + warden_secret: &Arc, client: &Client, namespace: &String, name: &String, - bitwarden_secret_api: &Api, - mut status: BitwardenSecretStatus, + warden_secret_api: &Api, + mut status: WardenSecretStatus, context: Arc, ) -> Result<(), Error> { let mutex_guard_fut = context.bw_client.lock(); - let mut bw_client: MutexGuard = mutex_guard_fut.await; - let fields_result = bw_client.fetch_item_fields(bitwarden_secret.spec.item.to_owned()); + let mut bw_client: MutexGuard = mutex_guard_fut.await; + let fields_result = bw_client.fetch_item_fields(warden_secret.spec.item.to_owned()); let attachments_result = - bw_client.fetch_item_attachments(bitwarden_secret.spec.item.to_owned()); + bw_client.fetch_item_attachments(warden_secret.spec.item.to_owned()); if fields_result.is_ok() && attachments_result.is_ok() { let secret_api: Api = Api::namespaced(client.clone(), &namespace); let string_secrets: BTreeMap = fields_result.unwrap(); let secrets: BTreeMap = attachments_result.unwrap(); - let owner_ref = create_owner_ref_for(&bitwarden_secret)?; + let owner_ref = create_owner_ref_for(&warden_secret)?; - let labels: BTreeMap = bitwarden_secret + let labels: BTreeMap = warden_secret .metadata .labels .clone() .unwrap_or(BTreeMap::new()); - let annotations: BTreeMap = bitwarden_secret + let annotations: BTreeMap = warden_secret .metadata .annotations .clone() @@ -326,7 +326,7 @@ async fn update_secret_with_vault_secrets( secret_api, owner_references, &name, - &bitwarden_secret.spec.type_, + &warden_secret.spec.type_, string_secrets, secrets, labels, @@ -339,7 +339,7 @@ async fn update_secret_with_vault_secrets( owner_ref, &name, &namespace, - &bitwarden_secret.spec.type_, + &warden_secret.spec.type_, string_secrets, secrets, labels, @@ -347,7 +347,7 @@ async fn update_secret_with_vault_secrets( ) .await?; } - update_status(&bitwarden_secret_api, &name, status, None).await?; + update_status(&warden_secret_api, &name, status, None).await?; } else { status = update_ready_condition( status, @@ -355,15 +355,15 @@ async fn update_secret_with_vault_secrets( type_: ConditionType::Ready, status: ConditionStatus::False, last_transition: Utc::now().to_rfc3339(), - message: Some("Failed to fetch Bitwarden item fields or attachments".to_string()), + message: Some("Failed to fetch item fields or attachments".to_string()), reason: Some("ObtainingSecretsFailed".to_string()), }, ); - patch_status(&bitwarden_secret_api, &name, &status).await?; + patch_status(&warden_secret_api, &name, &status).await?; if fields_result.is_err() { error( - &bitwarden_secret_api, + &warden_secret_api, &name, status, fields_result.err().unwrap(), @@ -371,7 +371,7 @@ async fn update_secret_with_vault_secrets( .await?; } else if attachments_result.is_err() { error( - &bitwarden_secret_api, + &warden_secret_api, &name, status, attachments_result.err().unwrap(), @@ -393,32 +393,32 @@ fn add_or_update_owner_ref(owner_references: &mut Vec, owner_ref owner_references.push(owner_ref); } -fn create_owner_ref_for(bitwarden_secret: &Arc) -> Result { - let bitwarden_secret_object_ref = bitwarden_secret.object_ref(&()); +fn create_owner_ref_for(warden_secret: &Arc) -> Result { + let warden_secret_object_ref = warden_secret.object_ref(&()); Ok(OwnerReference { - api_version: bitwarden_secret_object_ref.api_version.unwrap(), - kind: bitwarden_secret_object_ref.kind.unwrap(), - name: bitwarden_secret + api_version: warden_secret_object_ref.api_version.unwrap(), + kind: warden_secret_object_ref.kind.unwrap(), + name: warden_secret .metadata .name .clone() .ok_or(Error::UserInputError( format!( - "BitwardenSecret without name, uid: {}", - bitwarden_secret.uid().unwrap() + "WardenSecret without name, uid: {}", + warden_secret.uid().unwrap() ) .to_string(), ))?, - uid: bitwarden_secret.uid().unwrap(), + uid: warden_secret.uid().unwrap(), block_owner_deletion: Some(true), controller: None, }) } fn update_ready_condition( - mut status: BitwardenSecretStatus, + mut status: WardenSecretStatus, new_condition: ApplyCondition, -) -> BitwardenSecretStatus { +) -> WardenSecretStatus { if new_condition.type_ != ConditionType::Ready { panic!("Can only update Ready condition") } @@ -433,40 +433,40 @@ fn update_ready_condition( } async fn error( - bitwarden_secret_api: &Api, + warden_secret_api: &Api, name: &String, - status: BitwardenSecretStatus, - err: BitwardenCommandError, + status: WardenSecretStatus, + err: WardenCommandError, ) -> Result<(), Error> { error!("{}", err.to_string()); - update_status(&bitwarden_secret_api, &name, status, Some(err)).await?; + update_status(&warden_secret_api, &name, status, Some(err)).await?; Ok(()) } -fn determine_action(bitwarden_secret: &BitwardenSecret) -> BitwardenSecretAction { - return if bitwarden_secret.meta().deletion_timestamp.is_some() { - BitwardenSecretAction::Delete - } else if bitwarden_secret +fn determine_action(warden_secret: &WardenSecret) -> WardenSecretAction { + return if warden_secret.meta().deletion_timestamp.is_some() { + WardenSecretAction::Delete + } else if warden_secret .meta() .finalizers .as_ref() .map_or(true, |finalizers| finalizers.is_empty()) { - BitwardenSecretAction::Create - } else if bitwarden_secret.metadata.generation != bitwarden_secret.get_observed_generation() { - BitwardenSecretAction::Update + WardenSecretAction::Create + } else if warden_secret.metadata.generation != warden_secret.get_observed_generation() { + WardenSecretAction::Update } else { - BitwardenSecretAction::NoOp + WardenSecretAction::NoOp }; } pub async fn add_finalizer( - api: &Api, + api: &Api, name: &str, -) -> Result { +) -> Result { let finalizer: Value = json!({ "metadata": { - "finalizers": ["bitwardensecrets.tomjo.net/finalizer.secret"] + "finalizers": ["wardensecrets.tomjo.net/finalizer.secret"] } }); @@ -475,27 +475,27 @@ pub async fn add_finalizer( } pub async fn patch_status( - bitwarden_secret_api: &Api, + warden_secret_api: &Api, name: &str, - status: &BitwardenSecretStatus, -) -> Result { + status: &WardenSecretStatus, +) -> Result { let status_json: Value = json!({ "apiVersion": get_api_version(), "kind": get_kind(), "status": status }); let patch: Patch<&Value> = Patch::Apply(&status_json); - let pp = PatchParams::apply("bitwarden-operator").force(); - let o = bitwarden_secret_api.patch_status(name, &pp, &patch).await?; + let pp = PatchParams::apply("warden-secret-operator").force(); + let o = warden_secret_api.patch_status(name, &pp, &patch).await?; Ok(o) } pub async fn update_status( - bitwarden_secret_api: &Api, + warden_secret_api: &Api, name: &str, - mut status: BitwardenSecretStatus, - error: Option, -) -> Result { + mut status: WardenSecretStatus, + error: Option, +) -> Result { for (pos, condition) in status.conditions.iter().enumerate() { if condition.type_ == ConditionType::Ready { let mut copy = condition.clone(); @@ -512,7 +512,7 @@ pub async fn update_status( copy.last_transition = Utc::now().to_rfc3339(); } status.conditions[pos] = copy; - return patch_status(bitwarden_secret_api, name, &status).await; + return patch_status(warden_secret_api, name, &status).await; } } if error.is_some() { @@ -523,7 +523,7 @@ pub async fn update_status( message: Some("Could not fetch secret".to_string()), reason: error.map(|e| e.to_string()), }); - return patch_status(bitwarden_secret_api, name, &status).await; + return patch_status(warden_secret_api, name, &status).await; } status.conditions.push(ApplyCondition { type_: ConditionType::Ready, @@ -532,11 +532,11 @@ pub async fn update_status( message: None, reason: None, }); - return patch_status(bitwarden_secret_api, name, &status).await; + return patch_status(warden_secret_api, name, &status).await; } pub async fn delete_finalizer(client: Client, name: &str, namespace: &str) -> Result<(), Error> { - let api: Api = Api::namespaced(client, namespace); + let api: Api = Api::namespaced(client, namespace); let has_resource = api.get_opt(name).await?.is_some(); if has_resource { let finalizer: Value = json!({ @@ -616,13 +616,13 @@ pub async fn delete_secret(client: Client, name: &str, namespace: &str) -> Resul } fn on_error( - bitwarden_secret: Arc, + warden_secret: Arc, error: &Error, _context: Arc, ) -> Action { error!( "Reconciliation error:\n{:?}.\n{:?}", - error, bitwarden_secret + error, warden_secret ); Action::requeue(Duration::from_secs(5)) } @@ -639,6 +639,6 @@ pub enum Error { #[from] source: serde_json::Error, }, - #[error("Invalid BitwardenSecret CRD: {0}")] + #[error("Invalid WardenSecret CRD: {0}")] UserInputError(String), } diff --git a/deploy/crd/bitwardensecrets.tomjo.net.yaml b/wardensecrets.tomjo.net.yaml similarity index 92% rename from deploy/crd/bitwardensecrets.tomjo.net.yaml rename to wardensecrets.tomjo.net.yaml index 9597dcb..01e22e1 100644 --- a/deploy/crd/bitwardensecrets.tomjo.net.yaml +++ b/wardensecrets.tomjo.net.yaml @@ -1,7 +1,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: bitwardensecrets.tomjo.net + name: wardensecrets.tomjo.net namespace: default # For easier deployment and avoid permissions collisions on most clusters, the resource is namespace-scoped. More information at: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/ spec: group: tomjo.net @@ -15,15 +15,15 @@ spec: # - v1 # clientConfig: # service: -# namespace: bitwarden-operator -# name: bitwarden-operator-webhook +# namespace: warden-secret-operator +# name: warden-secret-operator-webhook # path: /crdconvert names: - kind: BitwardenSecret - plural: bitwardensecrets - singular: bitwardensecret + kind: WardenSecret + plural: wardensecrets + singular: wardensecret shortNames: - - bitwardensecret + - wardensecret - bw scope: Namespaced versions: