Skip to content

Commit e979a2d

Browse files
Make context check for outdated /etc/hosts
1 parent 8f5619a commit e979a2d

File tree

4 files changed

+244
-58
lines changed

4 files changed

+244
-58
lines changed

Cargo.lock

Lines changed: 14 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/k8-config/Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "k8-config"
3-
version = "1.1.1"
3+
version = "1.2.0"
44
authors = ["Fluvio Contributors <[email protected]>"]
55
edition = "2018"
66
description = "Read Kubernetes config"
@@ -13,7 +13,8 @@ context = ["tera"]
1313
[dependencies]
1414
log = "0.4.8"
1515
dirs = "2.0.2"
16-
serde = { version ="1.0.103", features = ['derive'] }
16+
serde = { version = "1.0.103", features = ['derive'] }
1717
serde_yaml = "0.8.9"
18+
serde_json = "1.0.57"
1819
tera = { version = "1.3.0", optional = true }
19-
20+
hostfile = "0.2.0"

src/k8-config/src/context.rs

Lines changed: 221 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
1+
use std::env;
2+
use std::io::Write;
3+
use std::fs::OpenOptions;
4+
use std::net::IpAddr;
5+
use std::os::unix::fs::OpenOptionsExt;
6+
use std::process::{Command, Stdio};
7+
8+
use serde::Deserialize;
19
use log::debug;
10+
use tera::Context;
11+
use tera::Tera;
212

3-
use crate::K8Config;
13+
use crate::{K8Config, ConfigError};
414

515
fn load_cert_auth() -> String {
616
let k8_config = K8Config::load().expect("loading");
@@ -25,71 +35,234 @@ fn load_cert_auth() -> String {
2535
.to_string()
2636
}
2737

28-
pub struct Option {
29-
pub ctx_name: String,
38+
/// Configuration options to wire up Fluvio on Minikube
39+
pub struct MinikubeContext {
40+
name: String,
41+
profile: MinikubeProfile,
3042
}
3143

32-
impl Default for Option {
33-
fn default() -> Self {
34-
Option {
35-
ctx_name: "flvkube".to_owned(),
44+
impl MinikubeContext {
45+
/// Attempts to derive a `MinikubeContext` from the system
46+
///
47+
/// This requires the presence of the `minikube` executable,
48+
/// which will tell us the current IP and port that minikube
49+
/// is running on.
50+
///
51+
/// # Example
52+
///
53+
/// ```no_run
54+
/// use k8_config::context::MinikubeContext;
55+
/// let context = MinikubeContext::try_from_system().unwrap();
56+
/// ```
57+
pub fn try_from_system() -> Result<Self, ConfigError> {
58+
Ok(Self {
59+
name: "flvkube".to_string(),
60+
profile: MinikubeProfile::load()?,
61+
})
62+
}
63+
64+
/// Sets the name of the context
65+
///
66+
/// # Example
67+
///
68+
/// ```no_run
69+
/// use k8_config::context::MinikubeContext;
70+
/// let context = MinikubeContext::try_from_system().unwrap()
71+
/// .with_name("my-minikube");
72+
/// ```
73+
pub fn with_name<S: Into<String>>(mut self, name: S) -> Self {
74+
self.name = name.into();
75+
self
76+
}
77+
78+
/// Saves the Minikube context for kubectl and updates the minikube IP
79+
///
80+
/// # Example
81+
///
82+
/// ```no_run
83+
/// use k8_config::context::MinikubeContext;
84+
/// let context = MinikubeContext::try_from_system().unwrap();
85+
/// context.save().unwrap();
86+
/// ```
87+
pub fn save(&self) -> Result<(), ConfigError> {
88+
// Check if the detected minikube IP matches the /etc/hosts entry
89+
if !self.profile.matches_hostfile()? {
90+
// If the /etc/hosts file is not up to date, update it
91+
debug!("hosts file is outdated: updating");
92+
self.update_hosts()?;
3693
}
94+
self.update_kubectl_context()?;
95+
Ok(())
3796
}
38-
}
3997

40-
/// create kube context that copy current cluster configuration
41-
pub fn create_dns_context(option: Option) {
42-
const TEMPLATE: &'static str = r#"
98+
/// Updates the `kubectl` context to use the current settings
99+
fn update_kubectl_context(&self) -> Result<(), ConfigError> {
100+
Command::new("kubectl")
101+
.args(&["config", "set-cluster", &self.name])
102+
.arg(&format!("--server=https://minikubeCA:{}", self.profile.port()))
103+
.arg(&format!("--certificate-authority={}", load_cert_auth()))
104+
.stdout(Stdio::inherit())
105+
.stderr(Stdio::inherit())
106+
.status()?;
107+
108+
Command::new("kubectl")
109+
.args(&["config", "set-context", &self.name])
110+
.arg("--user=minikube")
111+
.arg(&format!("--cluster={}", &self.name))
112+
.stdout(Stdio::inherit())
113+
.stderr(Stdio::inherit())
114+
.status()?;
115+
116+
Command::new("kubectl")
117+
.args(&["config", "use-context", &self.name])
118+
.stdout(Stdio::inherit())
119+
.stderr(Stdio::inherit())
120+
.status()?;
121+
122+
Ok(())
123+
}
124+
125+
/// Updates the `/etc/hosts` file by rewriting the line with `minikubeCA`
126+
fn update_hosts(&self) -> Result<(), ConfigError> {
127+
const TEMPLATE: &'static str = r#"
43128
#!/bin/bash
44-
export IP=$(minikube ip)
45-
sudo sed -i '' '/minikubeCA/d' /etc/hosts
129+
# Get IP from context, if available
130+
export IP={{ ip }}
131+
# If there is no IP in context, use "minikube ip"
132+
export IP="${IP:-$(minikube ip)}"
133+
sudo sed -i'' -e '/minikubeCA/d' /etc/hosts
46134
echo "$IP minikubeCA" | sudo tee -a /etc/hosts
47-
cd ~
48-
kubectl config set-cluster {{ name }} --server=https://minikubeCA:8443 --certificate-authority={{ ca }}
49-
kubectl config set-context {{ name }} --user=minikube --cluster={{ name }}
50-
kubectl config use-context {{ name }}
51135
"#;
52136

53-
use std::env;
54-
use std::fs::OpenOptions;
55-
use std::io;
56-
use std::io::Write;
57-
use std::os::unix::fs::OpenOptionsExt;
58-
use std::process::Command;
137+
let mut tera = Tera::default();
138+
tera.add_raw_template("cube.sh", TEMPLATE)
139+
.expect("string compilation");
140+
141+
let mut context = Context::new();
142+
context.insert("ip", &self.profile.ip());
143+
144+
let render = tera.render("cube.sh", &context).expect("rendering");
145+
let tmp_file = env::temp_dir().join("flv_minikube.sh");
146+
147+
let mut file = OpenOptions::new()
148+
.create(true)
149+
.write(true)
150+
.truncate(true)
151+
.mode(0o755)
152+
.open(tmp_file.clone())
153+
.expect("temp script can't be created");
154+
155+
file.write_all(render.as_bytes())
156+
.expect("file write failed");
59157

60-
use tera::Context;
61-
use tera::Tera;
158+
file.sync_all().expect("sync");
159+
drop(file);
62160

63-
let mut tera = Tera::default();
161+
debug!("script {}", render);
64162

65-
tera.add_raw_template("cube.sh", TEMPLATE)
66-
.expect("string compilation");
163+
Command::new(tmp_file)
164+
.stdout(Stdio::inherit())
165+
.stderr(Stdio::inherit())
166+
.status()?;
67167

68-
let mut context = Context::new();
69-
context.insert("name", &option.ctx_name);
70-
context.insert("ca", &load_cert_auth());
168+
Ok(())
169+
}
170+
}
71171

72-
let render = tera.render("cube.sh", &context).expect("rendering");
172+
#[derive(Debug, Deserialize)]
173+
struct MinikubeNode {
174+
#[serde(rename = "IP")]
175+
ip: IpAddr,
176+
#[serde(rename = "Port")]
177+
port: u16,
178+
}
73179

74-
let tmp_file = env::temp_dir().join("flv_minikube.sh");
180+
#[derive(Debug, Deserialize)]
181+
struct MinikubeConfig {
182+
#[serde(rename = "Name")]
183+
name: String,
184+
#[serde(rename = "Nodes")]
185+
nodes: Vec<MinikubeNode>,
186+
}
75187

76-
let mut file = OpenOptions::new()
77-
.create(true)
78-
.write(true)
79-
.truncate(true)
80-
.mode(0o755)
81-
.open(tmp_file.clone())
82-
.expect("temp script can't be created");
188+
#[derive(Debug, Deserialize)]
189+
struct MinikubeProfileWrapper {
190+
valid: Vec<MinikubeProfileJson>,
191+
}
83192

84-
file.write_all(render.as_bytes())
85-
.expect("file write failed");
193+
#[derive(Debug, Deserialize)]
194+
struct MinikubeProfileJson {
195+
#[serde(rename = "Name")]
196+
name: String,
197+
#[serde(rename = "Status")]
198+
status: String,
199+
#[serde(rename = "Config")]
200+
config: MinikubeConfig,
201+
}
86202

87-
file.sync_all().expect("sync");
88-
drop(file);
203+
/// A description of the active Minikube instance, including IP and port
204+
#[derive(Debug)]
205+
struct MinikubeProfile {
206+
/// The name of the minikube profile, usually "minikube"
207+
name: String,
208+
/// The active minikube node, with IP and port
209+
node: MinikubeNode,
210+
}
89211

90-
debug!("script {}", render);
212+
impl MinikubeProfile {
213+
/// Gets minikube's current profile
214+
fn load() -> Result<MinikubeProfile, ConfigError> {
215+
let output = Command::new("minikube")
216+
.args(&["profile", "list", "-o", "json"])
217+
.output()?;
218+
let output_string = String::from_utf8(output.stdout)
219+
.map_err(|e| ConfigError::Other(format!("`minikube profile list -o json` did not give UTF-8: {}", e)))?;
220+
let profiles: MinikubeProfileWrapper = serde_json::from_str(&output_string)
221+
.map_err(|e| ConfigError::Other(format!("`minikube profile list -o json` did not give valid JSON: {}", e)))?;
222+
let profile_json = profiles.valid.into_iter().next()
223+
.ok_or(ConfigError::Other("no valid minikube profiles".to_string()))?;
224+
let node = profile_json.config.nodes.into_iter().next()
225+
.ok_or(ConfigError::Other("Minikube has no active nodes".to_string()))?;
226+
let profile = MinikubeProfile {
227+
name: profile_json.name,
228+
node,
229+
};
230+
Ok(profile)
231+
}
232+
233+
fn ip(&self) -> IpAddr {
234+
self.node.ip
235+
}
236+
237+
fn port(&self) -> u16 {
238+
self.node.port
239+
}
240+
241+
/// Checks whether the `/etc/hosts` file has an up-to-date entry for minikube
242+
///
243+
/// Returns `Ok(true)` when the hostfile is up-to-date and no action is required.
244+
///
245+
/// Returns `Ok(false)` when the hostfile is out of date or has no `minikubeCA` entry.
246+
/// In this case, the `/etc/hosts` file needs to be edited.
247+
///
248+
/// Returns `Err(_)` when there is an error detecting the current Minikube ip address
249+
/// or if there is an error reading the hosts file.
250+
fn matches_hostfile(&self) -> Result<bool, ConfigError> {
251+
// Check if the /etc/hosts file matches the active node IP
252+
let matches = get_host_entry("minikubeCA")?
253+
.map(|ip| ip == self.node.ip)
254+
.unwrap_or(false);
255+
Ok(matches)
256+
}
257+
}
91258

92-
let output = Command::new(tmp_file).output().expect("cluster command");
93-
io::stdout().write_all(&output.stdout).unwrap();
94-
io::stderr().write_all(&output.stderr).unwrap();
259+
/// Gets the current entry for a given host in `/etc/hosts` if there is one
260+
fn get_host_entry(hostname: &str) -> Result<Option<IpAddr>, ConfigError> {
261+
// Get all of the host entries
262+
let hosts = hostfile::parse_hostfile()
263+
.map_err(|e| ConfigError::Other(format!("failed to get /etc/hosts entries: {}", e)))?;
264+
// Try to find a host entry with the given hostname
265+
let minikube_entry = hosts.into_iter()
266+
.find(|entry| entry.names.iter().any(|name| name == hostname));
267+
Ok(minikube_entry.map(|entry| entry.ip))
95268
}

src/k8-config/src/error.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@ use std::io::Error as StdIoError;
33

44
use serde_yaml::Error as SerdYamlError;
55

6-
76
#[derive(Debug)]
87
pub enum ConfigError {
98
IoError(StdIoError),
109
SerdeError(SerdYamlError),
11-
NoCurrentContext
10+
NoCurrentContext,
11+
Other(String),
1212
}
1313

1414
impl fmt::Display for ConfigError {
1515
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1616
match self {
1717
Self::IoError(err) => write!(f, "{}", err),
18-
Self::SerdeError(err) => write!(f,"{}",err),
19-
Self::NoCurrentContext => write!(f,"no current context")
18+
Self::SerdeError(err) => write!(f, "{}", err),
19+
Self::NoCurrentContext => write!(f, "no current context"),
20+
Self::Other(err) => write!(f, "{}", err),
2021
}
2122
}
2223
}

0 commit comments

Comments
 (0)