Skip to main content

xianyun_cli/commands/
setkubecontext.rs

1//! `caproni set-kube-context`: merge the cluster into the user's kubeconfig.
2//!
3//! Port of Go's `cmd/setkubecontext` package.
4
5use anyhow::Context as _;
6use clap::Args;
7use xianyun_cluster::kubeconfig::{file, merge};
8use xianyun_core::config::caproni::load_from_context;
9use xianyun_core::config::global::Context;
10
11#[derive(Args, Debug)]
12pub struct SetKubeContextArgs {
13    /// Also make the context your current context.
14    #[arg(short = 'u', long = "use")]
15    pub use_context: bool,
16
17    /// Remove the context from the kubeconfig instead of adding it.
18    #[arg(long = "remove")]
19    pub remove: bool,
20
21    /// Show what would be changed without modifying the kubeconfig.
22    #[arg(long = "dry-run")]
23    pub dry_run: bool,
24
25    /// Kubeconfig to modify (default: $KUBECONFIG, or ~/.kube/config).
26    #[arg(long = "kubeconfig")]
27    pub kubeconfig: Option<String>,
28}
29
30/// Port of Go's `options` struct.
31#[derive(Debug, Default, Clone)]
32pub struct Options {
33    pub use_context: bool,
34    pub remove: bool,
35    pub dry_run: bool,
36    pub kubeconfig: String,
37}
38
39impl From<&SetKubeContextArgs> for Options {
40    fn from(args: &SetKubeContextArgs) -> Self {
41        Self {
42            use_context: args.use_context,
43            remove: args.remove,
44            dry_run: args.dry_run,
45            kubeconfig: args.kubeconfig.clone().unwrap_or_default(),
46        }
47    }
48}
49
50pub fn run(args: &SetKubeContextArgs, ctx: &Context) -> anyhow::Result<()> {
51    run_set_kube_context(ctx, &Options::from(args))
52}
53
54/// Port of `setkubecontext.runSetKubeContext`: loads the configuration,
55/// resolves the target kubeconfig path, and merges (or removes) the
56/// cluster's context into it.
57///
58/// # Errors
59///
60/// Returns an error if configuration loading, resolving the kubeconfig
61/// path, reading the kubeconfig, resolving the cluster manager (unless
62/// `opts.remove` is set), getting the cluster's kubeconfig, merging, or
63/// writing fails.
64pub fn run_set_kube_context(ctx: &Context, opts: &Options) -> anyhow::Result<()> {
65    crate::drivers::ensure_registered();
66
67    let container = load_from_context(ctx).context("loading configuration")?;
68
69    let context_name = xianyun_core::naming::context_name(&container.global.profile_name);
70
71    let path = file::target_path(&opts.kubeconfig).context("resolving kubeconfig path")?;
72
73    let current = file::read(&path)
74        .context("reading kubeconfig")?
75        .unwrap_or_default();
76
77    let updated = render_update(&container, opts, &current, &context_name)?;
78
79    apply(opts, &path, &context_name, &current, &updated)
80}
81
82/// Port of `setkubecontext.renderUpdate`: produces the kubeconfig Caproni
83/// wants the file to contain.
84fn render_update(
85    container: &xianyun_core::config::container::Container,
86    opts: &Options,
87    current: &[u8],
88    context_name: &str,
89) -> anyhow::Result<Vec<u8>> {
90    // Removal never touches the cluster, so it still works once the cluster is gone.
91    if opts.remove {
92        return merge::uninstall(current, context_name)
93            .with_context(|| format!("removing context {context_name}"));
94    }
95
96    let manager = xianyun_cluster::registry::new_manager(
97        container.caproni.cluster.clone(),
98        container.global.profile_name.clone(),
99    )
100    .context("creating cluster manager")?;
101
102    let cluster_kubeconfig = manager
103        .get_kubeconfig()
104        .context("getting kubeconfig from the cluster")?;
105
106    merge::install(
107        current,
108        cluster_kubeconfig.as_bytes(),
109        context_name,
110        opts.use_context,
111    )
112    .with_context(|| format!("merging context {context_name}"))
113}
114
115/// Port of `setkubecontext.apply`: writes the rendered kubeconfig, unless
116/// nothing changed or this is a dry run.
117fn apply(
118    opts: &Options,
119    path: &str,
120    context_name: &str,
121    current: &[u8],
122    updated: &[u8],
123) -> anyhow::Result<()> {
124    if current == updated {
125        tracing::info!(
126            context = context_name,
127            kubeconfig = path,
128            "kubeconfig already up to date"
129        );
130        return Ok(());
131    }
132
133    if opts.dry_run {
134        tracing::info!(
135            context = context_name,
136            kubeconfig = path,
137            action = action(opts.remove),
138            "dry-run mode: would update kubeconfig"
139        );
140        tracing::info!("run without --dry-run to apply changes");
141        return Ok(());
142    }
143
144    let backup_path = file::write(path, updated).context("writing kubeconfig")?;
145
146    tracing::info!(
147        context = context_name,
148        kubeconfig = path,
149        action = action(opts.remove),
150        backup = backup_path,
151        "updated kubeconfig"
152    );
153
154    if opts.remove {
155        return Ok(());
156    }
157
158    tracing::info!(
159        kubectl = format!("kubectl --context {context_name} get pods"),
160        helm = format!("helm --kube-context {context_name} list"),
161        "cluster is now reachable outside caproni"
162    );
163
164    Ok(())
165}
166
167fn action(remove: bool) -> &'static str {
168    if remove { "remove" } else { "add" }
169}