~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/asyncai

0
0
main
221 lines · 6.8 KB Raw History
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::atomic::{AtomicU64, Ordering};

use serde_json::json;

use super::discovery::{workspace_url, AdminConfig, KcpInfo};
use super::runner::{shell_quote, Runner};

pub const READONLY_SERVICE_ACCOUNT: &str = "socialweb-computer-kcp-readonly";
pub const READONLY_CLUSTER_ROLE: &str = "socialweb-computer-kcp-readonly";
const TOKEN_DIR: &str = "socialweb-computer-kcp-mcp";

static TOKEN_FILE_SEQUENCE: AtomicU64 = AtomicU64::new(0);

const READONLY_MANIFESTS: &str = r#"apiVersion: v1
kind: ServiceAccount
metadata:
  name: socialweb-computer-kcp-readonly
  namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: socialweb-computer-kcp-readonly
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["get", "list", "watch"]
- apiGroups: [""]
  resources: ["pods/log", "pods/status", "namespaces/status", "nodes/status"]
  verbs: ["get", "list"]
- nonResourceURLs: ["/api", "/apis", "/version", "/healthz", "/livez", "/readyz", "/openapi/*"]
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: socialweb-computer-kcp-readonly
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: socialweb-computer-kcp-readonly
subjects:
- kind: ServiceAccount
  name: socialweb-computer-kcp-readonly
  namespace: default
"#;

#[derive(Clone, Debug)]
pub struct ReadonlyCredential {
    pub kubeconfig: String,
    pub token_expires_in_seconds: u64,
}

pub async fn ensure_readonly_credential(
    runner: &Runner,
    kcp: &KcpInfo,
    workspace: &str,
    admin: &AdminConfig,
    ttl_seconds: u64,
) -> Result<ReadonlyCredential, String> {
    let server = workspace_url(&kcp.server, workspace);

    let apply_args = vec![
        "--kubeconfig".to_string(),
        kcp.kubeconfig.clone(),
        "--server".to_string(),
        server.clone(),
        "--request-timeout=30s".to_string(),
        "apply".to_string(),
        "-f".to_string(),
        "-".to_string(),
    ];
    runner
        .kubectl(&apply_args, Some(READONLY_MANIFESTS))
        .await
        .map_err(|error| {
            format!("failed to ensure the read-only service account in {workspace}: {error}")
        })?;

    let token_args = vec![
        "--kubeconfig".to_string(),
        kcp.kubeconfig.clone(),
        "--server".to_string(),
        server.clone(),
        "--request-timeout=30s".to_string(),
        "create".to_string(),
        "token".to_string(),
        READONLY_SERVICE_ACCOUNT.to_string(),
        "-n".to_string(),
        "default".to_string(),
        format!("--duration={ttl_seconds}s"),
    ];
    let token = runner
        .kubectl(&token_args, None)
        .await
        .map_err(|error| format!("failed to mint the read-only token in {workspace}: {error}"))?
        .trim()
        .to_string();
    if token.is_empty() {
        return Err(format!(
            "kubectl create token returned an empty token in {workspace}"
        ));
    }

    let kubeconfig = token_kubeconfig(&server, admin, &token);
    let path = write_token_kubeconfig(runner, kcp, workspace, &kubeconfig).await?;
    Ok(ReadonlyCredential {
        kubeconfig: path,
        token_expires_in_seconds: ttl_seconds,
    })
}

fn token_kubeconfig(server: &str, admin: &AdminConfig, token: &str) -> String {
    let mut cluster = json!({
        "server": server,
    });
    if let Some(ca) = &admin.ca_data {
        cluster["certificate-authority-data"] = json!(ca);
    }
    if let Some(name) = &admin.tls_server_name {
        cluster["tls-server-name"] = json!(name);
    }
    json!({
        "apiVersion": "v1",
        "kind": "Config",
        "clusters": [{"name": "kcp", "cluster": cluster}],
        "users": [{"name": "readonly", "user": {"token": token}}],
        "contexts": [{"name": "readonly", "context": {"cluster": "kcp", "user": "readonly"}}],
        "current-context": "readonly",
    })
    .to_string()
}

fn local_token_dir() -> std::path::PathBuf {
    #[cfg(unix)]
    {
        let uid = unsafe { libc::geteuid() };
        std::env::temp_dir().join(format!("{TOKEN_DIR}-{uid}"))
    }
    #[cfg(not(unix))]
    {
        std::env::temp_dir().join(TOKEN_DIR)
    }
}

async fn write_token_kubeconfig(
    runner: &Runner,
    kcp: &KcpInfo,
    workspace: &str,
    contents: &str,
) -> Result<String, String> {
    let name = token_file_name(kcp, workspace);
    if runner.ssh_host().is_some() {
        let directory = format!("/tmp/{TOKEN_DIR}");
        let path = format!("{directory}/{name}.kubeconfig");
        let script = format!(
            "umask 077; mkdir -p {} && cat > {}",
            shell_quote(&directory),
            shell_quote(&path)
        );
        runner
            .shell_with_stdin(&script, Some(contents))
            .await
            .map_err(|error| {
                format!("failed to write the read-only kubeconfig on the host: {error}")
            })?;
        return Ok(path);
    }

    let path = local_token_dir().join(format!("{name}.kubeconfig"));
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|error| format!("failed to create {}: {error}", parent.display()))?;
    }
    std::fs::write(&path, contents)
        .map_err(|error| format!("failed to write {}: {error}", path.display()))?;
    set_private_permissions(&path)?;
    Ok(path.display().to_string())
}

fn token_file_name(kcp: &KcpInfo, workspace: &str) -> String {
    let mut hasher = DefaultHasher::new();
    kcp.id.hash(&mut hasher);
    workspace.hash(&mut hasher);
    let sequence = TOKEN_FILE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
    format!("{}-{sequence}-{:016x}", std::process::id(), hasher.finish())
}

#[cfg(unix)]
fn set_private_permissions(path: &std::path::Path) -> Result<(), String> {
    use std::os::unix::fs::PermissionsExt;
    let permissions = std::fs::Permissions::from_mode(0o600);
    std::fs::set_permissions(path, permissions)
        .map_err(|error| format!("failed to chmod {}: {error}", path.display()))
}

#[cfg(not(unix))]
fn set_private_permissions(_path: &std::path::Path) -> Result<(), String> {
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{token_kubeconfig, AdminConfig};

    #[test]
    fn token_kubeconfig_has_no_client_credentials() {
        let admin = AdminConfig {
            ca_data: Some("Y2E=".to_string()),
            tls_server_name: Some("127.0.0.1".to_string()),
        };
        let rendered = token_kubeconfig("https://127.0.0.1:33059/clusters/root:app", &admin, "tok");
        assert!(rendered.contains("\"token\":\"tok\""));
        assert!(rendered.contains("certificate-authority-data"));
        assert!(rendered.contains("tls-server-name"));
        assert!(!rendered.contains("client-certificate"));
        assert!(!rendered.contains("client-key"));
    }
}