pipedash-plugin-fluxcd 0.4.0

FluxCD plugin for Pipedash
use pipedash_plugin_api::{
    status_page_field,
    ConfigField,
    ConfigFieldType,
    ConfigSchema,
    Feature,
    Permission,
    PluginCapabilities,
    PluginMetadata,
    CONTROLLER_HEALTH_FEATURE,
};

use crate::{
    config,
    schema,
    types::Kind,
};

const NAMESPACE_LIST_REGEX: &str =
    r"^$|^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?(\s*,\s*[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?)*$";
const FLUX_NAMESPACE_REGEX: &str = r"^$|^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$";

pub(crate) struct PermissionSpec {
    pub name: &'static str,
    pub description: &'static str,
    pub required: bool,
    pub group: &'static str,
    pub resource: &'static str,
    pub verbs: &'static [&'static str],
    pub names: &'static [&'static str],
}

impl PermissionSpec {
    pub fn permission(&self) -> Permission {
        Permission {
            name: self.name.to_string(),
            description: self.description.to_string(),
            required: self.required,
        }
    }
}

const READ_KUSTOMIZATIONS: &str = "Read Kustomizations";
const READ_HELMRELEASES: &str = "Read HelmReleases";
const RECONCILE_KUSTOMIZATIONS: &str = "Request Kustomization reconciliation";
const RECONCILE_HELMRELEASES: &str = "Request HelmRelease reconciliation";
const READ_CONTROLLERS: &str = "Read Flux controller deployments";

pub(crate) const PERMISSIONS: [PermissionSpec; 5] = [
    PermissionSpec {
        name: READ_KUSTOMIZATIONS,
        description: "get and list kustomizations.kustomize.toolkit.fluxcd.io to show status, conditions and history",
        required: true,
        group: Kind::Kustomization.group(),
        resource: Kind::Kustomization.plural(),
        verbs: &["get", "list"],
        names: &[],
    },
    PermissionSpec {
        name: READ_HELMRELEASES,
        description: "get and list helmreleases.helm.toolkit.fluxcd.io to show status, conditions and release history",
        required: true,
        group: Kind::HelmRelease.group(),
        resource: Kind::HelmRelease.plural(),
        verbs: &["get", "list"],
        names: &[],
    },
    PermissionSpec {
        name: RECONCILE_KUSTOMIZATIONS,
        description: "patch kustomizations.kustomize.toolkit.fluxcd.io to set the reconcile.fluxcd.io/requestedAt annotation",
        required: false,
        group: Kind::Kustomization.group(),
        resource: Kind::Kustomization.plural(),
        verbs: &["patch"],
        names: &[],
    },
    PermissionSpec {
        name: RECONCILE_HELMRELEASES,
        description: "patch helmreleases.helm.toolkit.fluxcd.io to set the reconcile.fluxcd.io/requestedAt annotation",
        required: false,
        group: Kind::HelmRelease.group(),
        resource: Kind::HelmRelease.plural(),
        verbs: &["patch"],
        names: &[],
    },
    PermissionSpec {
        name: READ_CONTROLLERS,
        description: "get deployments.apps named source-controller, kustomize-controller and helm-controller in the Flux namespace to show controller health",
        required: false,
        group: "apps",
        resource: "deployments",
        verbs: &["get"],
        names: &config::CONTROLLERS,
    },
];

pub fn create_metadata() -> PluginMetadata {
    PluginMetadata {
        name: "FluxCD".to_string(),
        provider_type: config::PROVIDER_TYPE.to_string(),
        version: "0.1.0".to_string(),
        description: "Monitor Flux Kustomizations and HelmReleases, inspect their status and history, and request reconciliation. Flux retains a limited history and groups repeated reconciliations.".to_string(),
        author: Some("Pipedash Team".to_string()),
        icon: Some("/provider-icons/fluxcd.svg".to_string()),
        config_schema: create_config_schema(),
        table_schema: schema::create_table_schema(),
        capabilities: create_capabilities(),
        required_permissions: PERMISSIONS.iter().map(PermissionSpec::permission).collect(),
        features: create_features(),
    }
}

fn create_config_schema() -> ConfigSchema {
    ConfigSchema::new()
        .add_field(ConfigField {
            key: "kubeconfig".to_string(),
            label: "Kubeconfig".to_string(),
            description: Some(
                "Path on the machine running Pipedash, not in your browser. Defaults to $KUBECONFIG or ~/.kube/config. Separate files with ':' (';' on Windows). Monitoring needs get/list permissions. Reconciliation also needs patch permission."
                    .to_string(),
            ),
            field_type: ConfigFieldType::Text,
            required: false,
            default_value: Some(serde_json::Value::String(config::default_kubeconfig())),
            options: None,
            validation_regex: None,
            validation_message: None,
        })
        .add_field(ConfigField {
            key: "context".to_string(),
            label: "Kubernetes context".to_string(),
            description: Some(
                "Context from the kubeconfig that points at the Flux cluster. Leave empty to use current-context."
                    .to_string(),
            ),
            field_type: ConfigFieldType::Select,
            required: false,
            default_value: None,
            options: Some(Vec::new()),
            validation_regex: None,
            validation_message: None,
        })
        .add_field(ConfigField {
            key: "namespaces".to_string(),
            label: "Namespaces".to_string(),
            description: Some(
                "Comma-separated namespaces to monitor, for example flux-system,apps. Leave empty to monitor Kustomizations and HelmReleases in every namespace, which needs cluster-wide list permission. List your namespaces when your RBAC is namespace-scoped."
                    .to_string(),
            ),
            field_type: ConfigFieldType::Text,
            required: false,
            default_value: None,
            options: None,
            validation_regex: Some(NAMESPACE_LIST_REGEX.to_string()),
            validation_message: Some(
                "Use comma-separated Kubernetes namespace names: lowercase letters, numbers and hyphens, starting and ending with a letter or number, up to 63 characters each."
                    .to_string(),
            ),
        })
        .add_field(ConfigField {
            key: "flux_namespace".to_string(),
            label: "Flux namespace".to_string(),
            description: Some(
                "Namespace where the Flux controllers run. Pipedash reads the source-controller, kustomize-controller and helm-controller deployments there to report controller health. Leave empty to use flux-system."
                    .to_string(),
            ),
            field_type: ConfigFieldType::Text,
            required: false,
            default_value: Some(serde_json::Value::String(
                config::DEFAULT_FLUX_NAMESPACE.to_string(),
            )),
            options: None,
            validation_regex: Some(FLUX_NAMESPACE_REGEX.to_string()),
            validation_message: Some(
                "Use a Kubernetes namespace name: lowercase letters, numbers and hyphens, starting and ending with a letter or number, up to 63 characters."
                    .to_string(),
            ),
        })
        .add_field(status_page_field())
}

fn create_capabilities() -> PluginCapabilities {
    PluginCapabilities {
        pipelines: true,
        pipeline_runs: true,
        trigger: true,
        cancel: false,
        agents: false,
        artifacts: false,
        queues: false,
        custom_tables: false,
        retains_run_history: true,
    }
}

fn create_features() -> Vec<Feature> {
    vec![
        Feature {
            id: "view_status".to_string(),
            name: "View status and history".to_string(),
            description:
                "Show Flux conditions and the reconciliation history kept in status.history"
                    .to_string(),
            required_permissions: vec![
                READ_KUSTOMIZATIONS.to_string(),
                READ_HELMRELEASES.to_string(),
            ],
        },
        Feature {
            id: "request_reconcile".to_string(),
            name: "Request reconciliation".to_string(),
            description:
                "Ask Flux to reconcile a resource now. Suspended resources are never resumed."
                    .to_string(),
            required_permissions: vec![
                RECONCILE_KUSTOMIZATIONS.to_string(),
                RECONCILE_HELMRELEASES.to_string(),
            ],
        },
        Feature {
            id: CONTROLLER_HEALTH_FEATURE.to_string(),
            name: "Controller health".to_string(),
            description:
                "Report when the Flux controllers are scaled to zero, not running or only partly available"
                    .to_string(),
            required_permissions: vec![READ_CONTROLLERS.to_string()],
        },
    ]
}