lenso-cli 0.2.13

Lenso command-line interface for scaffolding and operating Lenso backend projects.
use std::fs;
use std::path::PathBuf;

use anyhow::{Context, Result};
use serde_json::json;

#[derive(Clone, Debug)]
pub struct ExportCrdBundleOptions {
    pub output: PathBuf,
    pub image: String,
    pub namespace: String,
    pub json: bool,
}

impl From<&crate::OperatorExportCrdArgs> for ExportCrdBundleOptions {
    fn from(args: &crate::OperatorExportCrdArgs) -> Self {
        Self {
            output: args.output.clone(),
            image: args.image.clone(),
            namespace: args.namespace.clone(),
            json: args.json,
        }
    }
}

pub fn export_crd_bundle(options: ExportCrdBundleOptions) -> Result<()> {
    fs::create_dir_all(&options.output)
        .with_context(|| format!("create directory {}", options.output.display()))?;
    let files = [
        ("lenso.dev_lensoserviceproviders.yaml", crd_yaml()),
        ("rbac.yaml", rbac_yaml(&options.namespace)),
        (
            "deployment.yaml",
            deployment_yaml(&options.namespace, &options.image),
        ),
        ("kustomization.yaml", kustomization_yaml()),
        ("README.md", readme(&options.namespace)),
    ];
    for (name, contents) in files {
        fs::write(options.output.join(name), contents)
            .with_context(|| format!("write {}", options.output.join(name).display()))?;
    }

    if options.json {
        println!(
            "{}",
            serde_json::to_string_pretty(&json!({
                "output": options.output,
                "namespace": options.namespace,
                "files": [
                    "lenso.dev_lensoserviceproviders.yaml",
                    "rbac.yaml",
                    "deployment.yaml",
                    "kustomization.yaml",
                    "README.md"
                ]
            }))?
        );
    } else {
        println!("Wrote Lenso Operator bundle: {}", options.output.display());
        println!("next: kubectl apply -k {}", options.output.display());
    }
    Ok(())
}

fn crd_yaml() -> String {
    r#"apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: lensoserviceproviders.lenso.dev
spec:
  group: lenso.dev
  names:
    kind: LensoServiceProvider
    plural: lensoserviceproviders
    singular: lensoserviceprovider
  scope: Namespaced
  versions:
    - name: v1alpha1
      served: true
      storage: true
      subresources:
        status: {}
      additionalPrinterColumns:
        - name: State
          type: string
          jsonPath: .status.state
        - name: Release
          type: string
          jsonPath: .status.observedReleaseId
        - name: Image
          type: string
          jsonPath: .status.observedImage
        - name: Ready
          type: integer
          jsonPath: .status.readyReplicas
      schema:
        openAPIV3Schema:
          type: object
          required: [spec]
          properties:
            spec:
              type: object
              required: [serviceName, environment, image, port]
              properties:
                serviceName: { type: string }
                environment: { type: string }
                image: { type: string }
                releaseId: { type: string }
                manifestReference: { type: string }
                modules:
                  type: array
                  items: { type: string }
                replicas:
                  type: integer
                  format: int32
                  default: 1
                port:
                  type: integer
                  format: int32
                envFrom:
                  type: object
                  properties:
                    configMap: { type: string }
                    secret: { type: string }
                ingress:
                  type: object
                  properties:
                    host: { type: string }
                autoscaling:
                  type: object
                  properties:
                    enabled: { type: boolean }
                    minReplicas: { type: integer, format: int32, default: 1 }
                    maxReplicas: { type: integer, format: int32, default: 3 }
                    targetCpuUtilization: { type: integer, format: int32, default: 70 }
                disruptionBudget:
                  type: object
                  properties:
                    enabled: { type: boolean }
                    minAvailable: { type: integer, format: int32, default: 1 }
                networkPolicy:
                  type: object
                  properties:
                    enabled: { type: boolean }
            status:
              type: object
              properties:
                state: { type: string }
                observedGeneration: { type: integer, format: int64 }
                observedReleaseId: { type: string }
                observedImage: { type: string }
                readyReplicas: { type: integer, format: int32 }
                desiredReplicas: { type: integer, format: int32 }
                availableReplicas: { type: integer, format: int32 }
                manifestReference: { type: string }
                conditions:
                  type: array
                  items:
                    type: object
                    properties:
                      type: { type: string }
                      status: { type: string }
                      reason: { type: string }
                      message: { type: string }
                      lastTransitionTime: { type: string }
"#
    .to_owned()
}

fn rbac_yaml(namespace: &str) -> String {
    format!(
        r#"apiVersion: v1
kind: Namespace
metadata:
  name: {namespace}
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: lenso-operator
  namespace: {namespace}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: lenso-operator
rules:
  - apiGroups: ["lenso.dev"]
    resources: ["lensoserviceproviders", "lensoserviceproviders/status"]
    verbs: ["get", "list", "watch", "patch", "update"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "patch"]
  - apiGroups: [""]
    resources: ["services"]
    verbs: ["get", "list", "watch", "patch"]
  - apiGroups: ["networking.k8s.io"]
    resources: ["ingresses", "networkpolicies"]
    verbs: ["get", "list", "watch", "patch"]
  - apiGroups: ["autoscaling"]
    resources: ["horizontalpodautoscalers"]
    verbs: ["get", "list", "watch", "patch"]
  - apiGroups: ["policy"]
    resources: ["poddisruptionbudgets"]
    verbs: ["get", "list", "watch", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: lenso-operator
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: lenso-operator
subjects:
  - kind: ServiceAccount
    name: lenso-operator
    namespace: {namespace}
"#
    )
}

fn deployment_yaml(namespace: &str, image: &str) -> String {
    format!(
        r#"apiVersion: apps/v1
kind: Deployment
metadata:
  name: lenso-operator
  namespace: {namespace}
  labels:
    app.kubernetes.io/name: lenso-operator
    app.kubernetes.io/part-of: lenso
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: lenso-operator
  template:
    metadata:
      labels:
        app.kubernetes.io/name: lenso-operator
        app.kubernetes.io/part-of: lenso
    spec:
      serviceAccountName: lenso-operator
      containers:
        - name: lenso-operator
          image: {image}
          env:
            - name: RUST_LOG
              value: lenso_operator=info,kube=info
"#
    )
}

fn kustomization_yaml() -> String {
    "resources:\n  - lenso.dev_lensoserviceproviders.yaml\n  - rbac.yaml\n  - deployment.yaml\n"
        .to_owned()
}

fn readme(namespace: &str) -> String {
    format!(
        "# Lenso Operator\n\n```sh\nkubectl apply -k .\nkubectl get deployment lenso-operator -n {namespace}\n```\n"
    )
}