use std::env;
use std::fs::OpenOptions;
use std::io::Write;
use std::net::IpAddr;
use std::os::unix::fs::OpenOptionsExt;
use std::process::{Command, Stdio};
use crate::ConfigError;
use serde::Deserialize;
use tracing::debug;
pub use v1::*;
pub struct MinikubeContext {
name: String,
profile: MinikubeProfile,
}
impl MinikubeContext {
pub fn try_from_system() -> Result<Self, ConfigError> {
Ok(Self {
name: "flvkube".to_string(),
profile: MinikubeProfile::load()?,
})
}
pub fn with_name<S: Into<String>>(mut self, name: S) -> Self {
self.name = name.into();
self
}
pub fn save(&self) -> Result<(), ConfigError> {
if !self.profile.matches_hostfile()? {
debug!("hosts file is outdated: updating");
self.update_hosts()?;
}
self.update_kubectl_context()?;
Ok(())
}
fn update_kubectl_context(&self) -> Result<(), ConfigError> {
Command::new("kubectl")
.args(["config", "set-cluster", &self.name])
.arg(format!(
"--server=https://minikubeCA:{}",
self.profile.port()
))
.arg(format!("--certificate-authority={}", load_cert_auth()))
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()?;
Command::new("kubectl")
.args(["config", "set-context", &self.name])
.arg("--user=minikube")
.arg(format!("--cluster={}", &self.name))
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()?;
Command::new("kubectl")
.args(["config", "use-context", &self.name])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()?;
Ok(())
}
fn update_hosts(&self) -> Result<(), ConfigError> {
let render = format!(
r#"
#!/bin/bash
# Get IP from context, if available
export IP={ip}
# If there is no IP in context, use "minikube ip"
export IP="${{IP:-$(minikube ip)}}"
sudo sed -i'' -e '/minikubeCA/d' /etc/hosts
echo "$IP minikubeCA" | sudo tee -a /etc/hosts
"#,
ip = &self.profile.ip()
);
let tmp_file = env::temp_dir().join("flv_minikube.sh");
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.mode(0o755)
.open(tmp_file.clone())
.expect("temp script can't be created");
file.write_all(render.as_bytes())
.expect("file write failed");
file.sync_all().expect("sync");
drop(file);
debug!("script {}", render);
Command::new(tmp_file)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()?;
Ok(())
}
}
#[derive(Debug, Deserialize)]
struct MinikubeNode {
#[serde(rename = "IP")]
ip: IpAddr,
#[serde(rename = "Port")]
port: u16,
}
#[derive(Debug, Deserialize)]
struct MinikubeConfig {
#[serde(rename = "Name")]
_name: String,
#[serde(rename = "Nodes")]
nodes: Vec<MinikubeNode>,
}
#[derive(Debug, Deserialize)]
struct MinikubeProfileWrapper {
valid: Vec<MinikubeProfileJson>,
}
#[derive(Debug, Deserialize)]
struct MinikubeProfileJson {
#[serde(rename = "Name")]
name: String,
#[serde(rename = "Status")]
_status: String,
#[serde(rename = "Config")]
config: MinikubeConfig,
}
#[derive(Debug)]
struct MinikubeProfile {
_name: String,
node: MinikubeNode,
}
impl MinikubeProfile {
fn load() -> Result<MinikubeProfile, ConfigError> {
let output = Command::new("minikube")
.args(["profile", "list", "-o", "json"])
.output()?;
let output_string = String::from_utf8(output.stdout).map_err(|e| {
ConfigError::Other(format!(
"`minikube profile list -o json` did not give UTF-8: {}",
e
))
})?;
let profiles: MinikubeProfileWrapper =
serde_json::from_str(&output_string).map_err(|e| {
ConfigError::Other(format!(
"`minikube profile list -o json` did not give valid JSON: {}",
e
))
})?;
let profile_json = profiles
.valid
.into_iter()
.next()
.ok_or_else(|| ConfigError::Other("no valid minikube profiles".to_string()))?;
let node = profile_json
.config
.nodes
.into_iter()
.next()
.ok_or_else(|| ConfigError::Other("Minikube has no active nodes".to_string()))?;
let profile = MinikubeProfile {
_name: profile_json.name,
node,
};
Ok(profile)
}
fn ip(&self) -> IpAddr {
self.node.ip
}
fn port(&self) -> u16 {
self.node.port
}
fn matches_hostfile(&self) -> Result<bool, ConfigError> {
let matches = get_host_entry("minikubeCA")?
.map(|ip| ip == self.node.ip)
.unwrap_or(false);
Ok(matches)
}
}
fn get_host_entry(hostname: &str) -> Result<std::option::Option<IpAddr>, ConfigError> {
let hosts = hostfile::parse_hostfile()
.map_err(|e| ConfigError::Other(format!("failed to get /etc/hosts entries: {}", e)))?;
let minikube_entry = hosts
.into_iter()
.find(|entry| entry.names.iter().any(|name| name == hostname));
Ok(minikube_entry.map(|entry| entry.ip))
}
pub mod v1 {
use std::env;
use std::fs::OpenOptions;
use std::io;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::process::Command;
use tera::Context;
use tera::Tera;
use tracing::debug;
pub use crate::K8Config;
pub(crate) fn load_cert_auth() -> String {
let k8_config = K8Config::load().expect("loading");
let ctx = match k8_config {
K8Config::Pod(_) => panic!("should not be pod"),
K8Config::KubeConfig(ctx) => ctx,
};
let config = ctx.config;
let cluster = config
.current_cluster()
.expect("should have current context");
cluster
.cluster
.certificate_authority
.as_ref()
.expect("certificate authority")
.to_string()
}
#[deprecated(note = "Please use MinikubeContext instead")]
pub struct Option {
pub ctx_name: String,
}
#[allow(deprecated)]
impl Default for Option {
fn default() -> Self {
Self {
ctx_name: "flvkube".to_owned(),
}
}
}
#[allow(deprecated)]
#[deprecated(note = "Please use MinikubeContext instead")]
pub fn create_dns_context(option: Option) {
const TEMPLATE: &str = r#"
#!/bin/bash
export IP=$(minikube ip)
sudo sed -i '' '/minikubeCA/d' /etc/hosts
echo "$IP minikubeCA" | sudo tee -a /etc/hosts
cd ~
kubectl config set-cluster {{ name }} --server=https://minikubeCA:8443 --certificate-authority={{ ca }}
kubectl config set-context {{ name }} --user=minikube --cluster={{ name }}
kubectl config use-context {{ name }}
"#;
let mut tera = Tera::default();
tera.add_raw_template("cube.sh", TEMPLATE)
.expect("string compilation");
let mut context = Context::new();
context.insert("name", &option.ctx_name);
context.insert("ca", &load_cert_auth());
let render = tera.render("cube.sh", &context).expect("rendering");
let tmp_file = env::temp_dir().join("flv_minikube.sh");
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.mode(0o755)
.open(tmp_file.clone())
.expect("temp script can't be created");
file.write_all(render.as_bytes())
.expect("file write failed");
file.sync_all().expect("sync");
drop(file);
debug!("script {}", render);
let output = Command::new(tmp_file).output().expect("cluster command");
io::stdout().write_all(&output.stdout).unwrap();
io::stderr().write_all(&output.stderr).unwrap();
}
}