diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..40dc6e4 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,79 @@ +name: Build Translator + +on: + push: + branches: + - '**' + tags-ignore: + - 'v*' + paths: + - 'wazuh-operator/**' + +env: + CARGO_TERM_COLOR: always + REGISTRY: ghcr.io + IMAGE_NAME: wazuh-operator + +jobs: + build: + + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Build Translator + working-directory: wazuh-operator + run: cargo build --verbose && cargo test --verbose + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to the Docker registry + id: login + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - id: lowercase + name: Lowercase image name + uses: AsZc/change-string-case-action@v6 + with: + string: ${{ env.IMAGE_NAME }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.lowercase }} + tags: | + type=raw,value=latest + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + if: github.event_name != 'pull_request' + with: + context: ./wazuh-operator + push: "${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}" + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64,linux/arm/v7 + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new \ No newline at end of file diff --git a/example/simple.yaml b/example/simple.yaml index 5a5388f..cbf414f 100644 --- a/example/simple.yaml +++ b/example/simple.yaml @@ -4,4 +4,4 @@ metadata: name: wazuh-cluster-sample namespace: my-namespace spec: - replicas: 3 + replicas: 3 \ No newline at end of file diff --git a/wazuh-operator/Cargo.lock b/wazuh-operator/Cargo.lock index 23f4fa6..5904d80 100644 --- a/wazuh-operator/Cargo.lock +++ b/wazuh-operator/Cargo.lock @@ -864,6 +864,7 @@ checksum = "19501afb943ae5806548bc3ebd7f3374153ca057a38f480ef30adfde5ef09755" dependencies = [ "base64 0.22.1", "chrono", + "schemars", "serde", "serde-value", "serde_json", @@ -871,9 +872,9 @@ dependencies = [ [[package]] name = "kube" -version = "0.92.1" +version = "0.93.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "231c5a5392d9e2a9b0d923199760d3f1dd73b95288f2871d16c7c90ba4954506" +checksum = "0365920075af1a2d23619c1ca801c492f2400157de42627f041a061716e76416" dependencies = [ "k8s-openapi", "kube-client", @@ -884,9 +885,9 @@ dependencies = [ [[package]] name = "kube-client" -version = "0.92.1" +version = "0.93.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4bf54135062ff60e2a0dfb3e7a9c8e931fc4a535b4d6bd561e0a1371321c61" +checksum = "d81336eb3a5b10a40c97a5a97ad66622e92bad942ce05ee789edd730aa4f8603" dependencies = [ "base64 0.22.1", "bytes", @@ -925,9 +926,9 @@ dependencies = [ [[package]] name = "kube-core" -version = "0.92.1" +version = "0.93.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40fb9bd8141cbc0fe6b0d9112d371679b4cb607b45c31dd68d92e40864a12975" +checksum = "cce373a74d787d439063cdefab0f3672860bd7bac01a38e39019177e764a0fe6" dependencies = [ "chrono", "form_urlencoded", @@ -942,9 +943,9 @@ dependencies = [ [[package]] name = "kube-derive" -version = "0.92.1" +version = "0.93.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08fc86f70076921fdf2f433bbd2a796dc08ac537dc1db1f062cfa63ed4fa15fb" +checksum = "04a26c9844791e127329be5dce9298b03f9e2ff5939076d5438c92dea5eb78f2" dependencies = [ "darling", "proc-macro2", @@ -955,9 +956,9 @@ dependencies = [ [[package]] name = "kube-runtime" -version = "0.92.1" +version = "0.93.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7eb2fb986f81770eb55ec7f857e197019b31b38768d2410f6c1046ffac34225" +checksum = "3b84733c0fed6085c9210b43ffb96248676c1e800d0ba38d15043275a792ffa4" dependencies = [ "ahash", "async-broadcast", diff --git a/wazuh-operator/Cargo.toml b/wazuh-operator/Cargo.toml index 2918f68..b8bd20e 100644 --- a/wazuh-operator/Cargo.toml +++ b/wazuh-operator/Cargo.toml @@ -4,9 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -kube = { version = "0.92.1", features = ["runtime", "derive", "admission", "rustls-tls", "ws", "socks5", "runtime"] } +kube = { version = "0.93.1", features = ["runtime", "derive", "admission", "rustls-tls", "ws", "socks5", "runtime"] } schemars = "0.8.21" -k8s-openapi = { version = "0.22.0", features = ["latest"] } +k8s-openapi = { version = "0.22.0", features = ["latest", "schemars"] } tokio = { version = "1.38.1", features = ["full"] } serde = { version = "1.0.203", features = ["derive"] } serde_json = { version = "1.0.120" } diff --git a/wazuh-operator/Dockerfile b/wazuh-operator/Dockerfile new file mode 100644 index 0000000..383e080 --- /dev/null +++ b/wazuh-operator/Dockerfile @@ -0,0 +1,19 @@ +FROM rust as base + +WORKDIR /app + +FROM base as builder + +# Copy the Cargo.toml and Cargo.lock files to cache dependencies +COPY ./ ./ + +# Build the dependencies +RUN cargo build --release + +FROM base + +WORKDIR /app + +COPY --from=builder /app/target/release/wazuh-operator /app/wazuh-operator + +CMD ["/app/wazuh-operator"] \ No newline at end of file diff --git a/wazuh-operator/src/controller/error_handler.rs b/wazuh-operator/src/controller/error_handler.rs new file mode 100644 index 0000000..4140910 --- /dev/null +++ b/wazuh-operator/src/controller/error_handler.rs @@ -0,0 +1,12 @@ +use std::sync::Arc; +use std::time::Duration; + +use kube::runtime::controller::Action; +use crate::errors::*; +use crate::crds::wazuh_cluster::WazuhCluster; +use crate::models::data::Data; + +pub fn error_policy(wazuh: Arc, error: &Error, _ctx: Arc) -> Action { + eprintln!("Reconciliation error:\n{:?}.\n{:?}", error, wazuh); + Action::requeue(Duration::from_secs(60)) +} \ No newline at end of file diff --git a/wazuh-operator/src/controller/finalizer.rs b/wazuh-operator/src/controller/finalizer.rs new file mode 100644 index 0000000..61660e4 --- /dev/null +++ b/wazuh-operator/src/controller/finalizer.rs @@ -0,0 +1,42 @@ +use kube::{Api, Client}; +use anyhow::*; +use kube::api::{Patch, PatchParams}; +use serde_json::Value; +use crate::crds::wazuh_cluster::WazuhCluster; + +pub async fn add_finalizer(client: Client, name: &str, namespace: &str) -> Result<()> { + let api: Api = Api::namespaced(client, namespace); + let finalizer = json!({ + "metadata": { + "finalizers": ["wazuh.adorsys.team/finalizer"] + } + }); + + let patch: Patch<&Value> = Patch::Merge(&finalizer); + api.patch(name, &PatchParams::default(), &patch).await?; + + Ok(()) +} + +/// Removes all finalizers from an `Echo` resource. If there are no finalizers already, this +/// action has no effect. +/// +/// # Arguments: +/// - `client` - Kubernetes client to modify the `Echo` resource with. +/// - `name` - Name of the `Echo` resource to modify. Existence is not verified +/// - `namespace` - Namespace where the `Echo` resource with given `name` resides. +/// +/// Note: Does not check for resource's existence for simplicity. +pub async fn delete_finalizer(client: Client, name: &str, namespace: &str) -> Result<()> { + let api: Api = Api::namespaced(client, namespace); + let finalizer: Value = json!({ + "metadata": { + "finalizers": null + } + }); + + let patch: Patch<&Value> = Patch::Merge(&finalizer); + api.patch(name, &PatchParams::default(), &patch).await?; + + Ok(()) +} \ No newline at end of file diff --git a/wazuh-operator/src/controller/handler.rs b/wazuh-operator/src/controller/handler.rs index 1129584..7e724f9 100644 --- a/wazuh-operator/src/controller/handler.rs +++ b/wazuh-operator/src/controller/handler.rs @@ -1,46 +1,26 @@ use std::sync::Arc; -use std::time::Duration; use futures::StreamExt; use kube::{Api, Client}; use kube::runtime::Controller; -use kube::runtime::controller::Action; -use kube::runtime::reflector::ObjectRef; use kube::runtime::watcher::Config; +use crate::controller::error_handler::error_policy; +use crate::controller::reconcile_wazuh::reconcile_wazuh; use crate::crds::wazuh_cluster::WazuhCluster; +use crate::models::cluster_ref::WazuhClusterRef; use crate::models::data::Data; -type WazuhClusterRef = ObjectRef; - pub async fn watch_wazuh_cluster(client: Client) { // Create an API for the WazuhCluster CRD let crd_api: Api = Api::all(client.clone()); - // Define the reconciliation function - async fn reconcile(_wazuh: Arc, _ctx: Arc) -> Result { - info!("Reconciling WazuhCluster"); - - let patch = json!({"spec": { - "activeDeadlineSeconds": 5 - }}); - - // Implement your reconciliation logic here - Ok(Action::requeue(Duration::from_secs(300))) - } - - // Define the error policy - fn error_policy(_wazuh: Arc, _error: &kube::Error, _ctx: Arc) -> Action { - error!("Reconciliation failed"); - Action::requeue(Duration::from_secs(60)) - } - // Create a controller for the WazuhCluster CRD Controller::new(crd_api, Config::default()) - .run(reconcile, error_policy, Arc::from(Data::new(client.clone()))) + .run(reconcile_wazuh, error_policy, Arc::from(Data::new(client.clone()))) .for_each(|res| async move { match res { - Ok((WazuhClusterRef { name, .. }, _)) => info!("Reconciled {:?}", name), + Ok((WazuhClusterRef { name, namespace, .. }, _)) => debug!("Reconciled {:?} in {:?}", name, namespace.unwrap_or_else(|| "default".to_string())), Err(e) => error!("Reconcile failed: {:?}", e), } }) diff --git a/wazuh-operator/src/controller/mod.rs b/wazuh-operator/src/controller/mod.rs index ef7d850..4de7989 100644 --- a/wazuh-operator/src/controller/mod.rs +++ b/wazuh-operator/src/controller/mod.rs @@ -1 +1,4 @@ -pub mod handler; \ No newline at end of file +pub mod handler; +pub mod error_handler; +mod reconcile_wazuh; +mod finalizer; \ No newline at end of file diff --git a/wazuh-operator/src/controller/reconcile_wazuh.rs b/wazuh-operator/src/controller/reconcile_wazuh.rs new file mode 100644 index 0000000..be6cc16 --- /dev/null +++ b/wazuh-operator/src/controller/reconcile_wazuh.rs @@ -0,0 +1,47 @@ +use std::sync::Arc; +use std::time::Duration; + +use kube::runtime::controller::Action; +use kube::runtime::reflector::Lookup; + +use crate::controller::finalizer::{add_finalizer, delete_finalizer}; +use crate::crds::wazuh_cluster::WazuhCluster; +use crate::models::crd_action::WazuhClusterAction; +use crate::models::data::Data; +use crate::services::determine_action::determine_action; +use crate::services::nginx_deployment::{update_deployment, delete_deployment, update_status}; +use crate::errors::*; + +pub async fn reconcile_wazuh(wazuh: Arc, ctx: Arc) -> Result { + info!("Reconciling WazuhCluster"); + + let namespace = &wazuh.namespace().unwrap(); + let name = &wazuh.name().unwrap(); + + match determine_action(&wazuh) { + WazuhClusterAction::Create => { + debug!("Creating WazuhCluster {:?}", name); + // Add the finalizer and create the deployment + add_finalizer(ctx.client.clone(), name, namespace).await?; + update_deployment(ctx.client.clone(), &wazuh, name, namespace).await?; + update_status(ctx.client.clone(), &wazuh, name, namespace).await?; + debug!("Created WazuhCluster {:?}", name); + Ok(Action::requeue(Duration::from_secs(20))) + } + WazuhClusterAction::Delete => { + debug!("Deleting WazuhCluster {:?}", name); + // Delete the deployment and remove the finalizer + delete_deployment(ctx.client.clone(), &wazuh, name, namespace).await?; + delete_finalizer(ctx.client.clone(), name, namespace).await?; + debug!("Deleted WazuhCluster {:?}", name); + Ok(Action::await_change()) + } + WazuhClusterAction::Update => { + debug!("Updating WazuhCluster {:?}", name); + update_deployment(ctx.client.clone(), &wazuh, name, namespace).await?; + update_status(ctx.client.clone(), &wazuh, name, namespace).await?; + debug!("Updated WazuhCluster {:?}", name); + Ok(Action::requeue(Duration::from_secs(10))) + } + } +} \ No newline at end of file diff --git a/wazuh-operator/src/crds/wazuh_cluster.rs b/wazuh-operator/src/crds/wazuh_cluster.rs index 82b911a..ab8300c 100644 --- a/wazuh-operator/src/crds/wazuh_cluster.rs +++ b/wazuh-operator/src/crds/wazuh_cluster.rs @@ -4,11 +4,12 @@ use schemars::JsonSchema; #[derive(CustomResource, Debug, Clone, Deserialize, Serialize, JsonSchema)] #[kube(shortname = "wzcl", group = "wazuh.adorsys.team", version = "v1", kind = "WazuhCluster", namespaced)] +#[kube(status = "WazuhClusterStatus")] pub struct WazuhClusterSpec { pub replicas: i32, } -#[derive(Deserialize, Serialize, Clone, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug, Default, JsonSchema)] pub struct WazuhClusterStatus { pub available_replicas: i32, } \ No newline at end of file diff --git a/wazuh-operator/src/errors.rs b/wazuh-operator/src/errors.rs index 65e8bee..8a4b025 100644 --- a/wazuh-operator/src/errors.rs +++ b/wazuh-operator/src/errors.rs @@ -1,10 +1,14 @@ -use thiserror::Error; - -#[derive(Debug, Error)] +#[derive(Debug, thiserror::Error)] pub enum Error { - #[error("Failed to create WazuhCluster: {0}")] - WazuhClusterCreationFailed(#[source] kube::Error), + #[error("App reported error: {source}")] + AnyError { + #[from] + source: anyhow::Error, + }, - #[error("MissingObjectKey: {0}")] - MissingObjectKey(&'static str), + #[error("Kubernetes reported error: {source}")] + KubeError { + #[from] + source: kube::Error, + }, } \ No newline at end of file diff --git a/wazuh-operator/src/main.rs b/wazuh-operator/src/main.rs index bb129f1..6adb30a 100644 --- a/wazuh-operator/src/main.rs +++ b/wazuh-operator/src/main.rs @@ -13,6 +13,7 @@ mod crds; mod models; mod controller; mod errors; +mod services; #[tokio::main] async fn main() -> Result<()> { diff --git a/wazuh-operator/src/models/cluster_ref.rs b/wazuh-operator/src/models/cluster_ref.rs new file mode 100644 index 0000000..176362e --- /dev/null +++ b/wazuh-operator/src/models/cluster_ref.rs @@ -0,0 +1,4 @@ +use kube::runtime::reflector::ObjectRef; +use crate::crds::wazuh_cluster::WazuhCluster; + +pub type WazuhClusterRef = ObjectRef; \ No newline at end of file diff --git a/wazuh-operator/src/models/crd_action.rs b/wazuh-operator/src/models/crd_action.rs new file mode 100644 index 0000000..544a1ad --- /dev/null +++ b/wazuh-operator/src/models/crd_action.rs @@ -0,0 +1,5 @@ +pub enum WazuhClusterAction { + Delete, // Delete + Create, // Create or Update + Update, // Update or No-op +} \ No newline at end of file diff --git a/wazuh-operator/src/models/data.rs b/wazuh-operator/src/models/data.rs index a1cfc75..6a3ff5e 100644 --- a/wazuh-operator/src/models/data.rs +++ b/wazuh-operator/src/models/data.rs @@ -2,7 +2,7 @@ use kube::Client; #[derive(Clone)] pub struct Data { - client: Client + pub(crate) client: Client } impl Data { diff --git a/wazuh-operator/src/models/mod.rs b/wazuh-operator/src/models/mod.rs index 12e35bb..6fff484 100644 --- a/wazuh-operator/src/models/mod.rs +++ b/wazuh-operator/src/models/mod.rs @@ -1 +1,3 @@ -pub mod data; \ No newline at end of file +pub mod data; +pub mod cluster_ref; +pub mod crd_action; \ No newline at end of file diff --git a/wazuh-operator/src/services/determine_action.rs b/wazuh-operator/src/services/determine_action.rs new file mode 100644 index 0000000..077600d --- /dev/null +++ b/wazuh-operator/src/services/determine_action.rs @@ -0,0 +1,18 @@ +use kube::Resource; + +use crate::crds::wazuh_cluster::WazuhCluster; +use crate::models::crd_action::WazuhClusterAction; + +pub fn determine_action(cluster: &WazuhCluster) -> WazuhClusterAction { + if cluster.meta().deletion_timestamp.is_some() { + WazuhClusterAction::Delete + } else if cluster + .meta() + .finalizers + .as_ref() + .map_or(true, |finalizers| finalizers.is_empty()) { + WazuhClusterAction::Create + } else { + WazuhClusterAction::Update + } +} \ No newline at end of file diff --git a/wazuh-operator/src/services/mod.rs b/wazuh-operator/src/services/mod.rs new file mode 100644 index 0000000..19a024e --- /dev/null +++ b/wazuh-operator/src/services/mod.rs @@ -0,0 +1,2 @@ +pub mod nginx_deployment; +pub mod determine_action; diff --git a/wazuh-operator/src/services/nginx_deployment.rs b/wazuh-operator/src/services/nginx_deployment.rs new file mode 100644 index 0000000..4dccf36 --- /dev/null +++ b/wazuh-operator/src/services/nginx_deployment.rs @@ -0,0 +1,101 @@ +use anyhow::*; +use k8s_openapi::api::apps::v1::{Deployment, DeploymentStatus}; +use k8s_openapi::api::core::v1::{Container, ContainerPort, PodSpec, PodTemplateSpec}; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::*; +use kube::{Api, Client, ResourceExt}; +use kube::api::{DeleteParams, Patch, PatchParams, PostParams}; + +use crate::crds::wazuh_cluster::{WazuhCluster, WazuhClusterStatus}; + +fn get_deployment_name(name: &str) -> String { + let s = format!("{name}-nginx"); + s.to_owned() +} + +pub async fn update_deployment(client: Client, app: &WazuhCluster, name: &str, namespace: &str) -> Result<()> { + let deployments: Api = Api::namespaced(client.clone(), namespace); + + let mut labels = app.metadata.labels.clone().unwrap_or_default(); + labels.insert("app".to_owned(), app.name_any().to_owned()); + let app_name = get_deployment_name(name); + + match deployments.get_opt(&app_name).await? { + Some(_) => { + let fs = json!({ + "spec": { + "replicas": app.spec.replicas, + } + }); + let patch = Patch::Merge(fs); + deployments.patch(&app_name, &PatchParams::default(), &patch).await?; + } + None => { + let dp = Deployment { + metadata: ObjectMeta { + name: Some(app_name.to_owned()), + namespace: Some(namespace.to_owned()), + labels: Some(labels.clone()), + ..ObjectMeta::default() + }, + spec: Some(k8s_openapi::api::apps::v1::DeploymentSpec { + replicas: Some(app.spec.replicas), + selector: LabelSelector { + match_labels: Some(labels.clone()), + ..Default::default() + }, + template: PodTemplateSpec { + metadata: Some(ObjectMeta { + labels: Some(labels.clone()), + ..Default::default() + }), + spec: Some(PodSpec { + containers: vec![Container { + name: app_name.to_owned(), + image: Some("nginx".to_string()), + ports: Some(vec![ContainerPort { + container_port: 80, + ..ContainerPort::default() + }]), + ..Default::default() + }], + ..Default::default() + }), + }, + ..Default::default() + }), + ..Default::default() + }; + deployments.create(&PostParams::default(), &dp).await?; + } + } + + Ok(()) +} + +pub async fn update_status(client: Client, _app: &WazuhCluster, name: &str, namespace: &str) -> Result<()> { + let deployments: Api = Api::namespaced(client.clone(), namespace); + let api: Api = Api::namespaced(client.clone(), namespace); + + let app_name = get_deployment_name(name); + let dp = deployments.get(&app_name).await?; + let available_replicas = match dp.status { + None => 0, + Some(DeploymentStatus { available_replicas, .. }) => available_replicas.unwrap_or_else(|| 0) + }; + let fs = json!({ + "status": WazuhClusterStatus { available_replicas } + }); + let patch = Patch::Merge(fs); + + api + .patch(name, &PatchParams::default(), &patch) + .await?; + Ok(()) +} + +pub async fn delete_deployment(client: Client, _app: &WazuhCluster, name: &str, namespace: &str) -> Result<()> { + let api: Api = Api::namespaced(client, namespace); + let app_name = get_deployment_name(name); + api.delete(&app_name, &DeleteParams::default()).await?; + Ok(()) +} \ No newline at end of file