kubernix 0.3.1

Kubernetes development cluster bootstrapping with Nix packages
Documentation
//! Kubernetes API server component.
//!
//! Starts `kube-apiserver` with TLS, etcd connectivity, RBAC authorization,
//! and encryption-at-rest, then applies the kubelet API RBAC rules.

use crate::{
    component::{ClusterContext, Component, Phase},
    config::Config,
    encryptionconfig::EncryptionConfig,
    kubectl::Kubectl,
    network::Network,
    pki::Pki,
    process::{Process, ProcessState, Stoppable},
    write_if_changed,
};
use anyhow::{Context, Result};
use log::debug;
use std::{fs::create_dir_all, path::Path};

/// Component wrapper for registry-based startup.
pub struct ApiServerComponent;

impl Component for ApiServerComponent {
    fn name(&self) -> &str {
        "API Server"
    }

    fn phase(&self) -> Phase {
        Phase::ControlPlane
    }

    fn start(&self, ctx: &ClusterContext<'_>) -> ProcessState {
        ApiServer::start(
            ctx.config,
            ctx.network,
            ctx.pki,
            ctx.encryptionconfig,
            ctx.kubectl,
        )
    }
}

/// Manages the `kube-apiserver` process lifecycle.
pub struct ApiServer {
    process: Process,
}

impl ApiServer {
    /// Start the API server with the given cluster configuration.
    pub fn start(
        config: &Config,
        network: &Network,
        pki: &Pki,
        encryptionconfig: &EncryptionConfig,
        kubectl: &Kubectl,
    ) -> ProcessState {
        let dir = config.root().join("apiserver");
        create_dir_all(&dir)?;

        let mut process = Process::start(
            &dir,
            "API Server",
            "kube-apiserver",
            &[
                "--audit-log-maxage=30",
                "--audit-log-maxbackup=3",
                "--audit-log-maxsize=100",
                &format!("--audit-log-path={}", dir.join("audit.log").display()),
                "--authorization-mode=Node,RBAC",
                "--bind-address=0.0.0.0",
                &format!("--client-ca-file={}", pki.ca().cert().display()),
                &format!("--etcd-cafile={}", pki.ca().cert().display()),
                &format!("--etcd-certfile={}", pki.apiserver().cert().display()),
                &format!("--etcd-keyfile={}", pki.apiserver().key().display()),
                &format!("--etcd-servers=https://{}", network.etcd_client()),
                "--event-ttl=1h",
                &format!(
                    "--encryption-provider-config={}",
                    encryptionconfig.path().display()
                ),
                &format!(
                    "--kubelet-certificate-authority={}",
                    pki.ca().cert().display()
                ),
                &format!(
                    "--kubelet-client-certificate={}",
                    pki.apiserver().cert().display()
                ),
                &format!("--kubelet-client-key={}", pki.apiserver().key().display()),
                "--runtime-config=api/all=true",
                "--service-account-issuer=https://kubernetes.default.svc.cluster.local",
                &format!(
                    "--service-account-key-file={}",
                    pki.service_account().cert().display()
                ),
                &format!(
                    "--service-account-signing-key-file={}",
                    pki.service_account().key().display()
                ),
                &format!("--service-cluster-ip-range={}", network.service_cidr()),
                &format!("--tls-cert-file={}", pki.apiserver().cert().display()),
                &format!("--tls-private-key-file={}", pki.apiserver().key().display()),
                "--v=2",
            ],
        )?;

        process.wait_ready("Serving securely")?;
        Self::setup_rbac(&dir, kubectl)?;
        Ok(Box::new(Self { process }))
    }

    fn setup_rbac(dir: &Path, kubectl: &Kubectl) -> Result<()> {
        debug!("Creating API Server RBAC rule for kubelet");
        let file = dir.join("rbac.yml");
        write_if_changed(&file, include_str!("assets/apiserver.yml"))?;

        kubectl
            .apply(&file)
            .context("Unable to deploy API server RBAC rules")?;

        debug!("API Server RBAC rule created");
        Ok(())
    }
}

impl Stoppable for ApiServer {
    fn stop(&mut self) -> Result<()> {
        self.process.stop()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rbac_asset_contains_expected_rules() {
        let rbac = include_str!("assets/apiserver.yml");
        assert!(rbac.contains("kind: ClusterRole"));
        assert!(rbac.contains("kind: ClusterRoleBinding"));
        assert!(rbac.contains("system:kube-apiserver-to-kubelet"));
        assert!(rbac.contains("nodes/proxy"));
        assert!(rbac.contains("nodes/stats"));
    }

    #[test]
    fn component_metadata() {
        let c = ApiServerComponent;
        assert_eq!(c.name(), "API Server");
        assert_eq!(c.phase(), Phase::ControlPlane);
    }
}