Skip to content

Commit

Permalink
Adding the ability to rerun an install on conditions changes
Browse files Browse the repository at this point in the history
  • Loading branch information
sebt3 committed May 30, 2024
1 parent d0fe8e4 commit 7c4f35a
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 51 deletions.
38 changes: 13 additions & 25 deletions dist/src/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub fn gen_index_yaml(dest_dir: &PathBuf) -> Result<()> {
file.push(dest_dir);
file.push("index.yaml");
gen_file(&file, &"
---
apiVersion: vinyl.solidite.fr/v1beta1
kind: Component
category:
Expand Down Expand Up @@ -101,13 +102,13 @@ options:
properties:
type:
enum:
- Filesystem
- Block
- Filesystem
- Block
accessMode:
enum:
- ReadWriteOnce
- ReadOnlyMany
- ReadWriteMany
- ReadWriteOnce
- ReadOnlyMany
- ReadWriteMany
images:
default:
postgresql:
Expand All @@ -118,39 +119,26 @@ options:
registry: quay.io
repository: opstree/redis
tag: v7.0.12
pullPolicy: IfNotPresent
pull_policy: IfNotPresent
redis_exporter:
registry: quay.io
repository: opstree/redis-exporter
tag: v1.44.0
pullPolicy: IfNotPresent
pull_policy: IfNotPresent
app:
registry: docker.io
repository: to-be/defined
tag: v1.0.0
pullPolicy: IfNotPresent
pull_policy: IfNotPresent
properties:
app:
properties:
pullPolicy:
enum:
- Always
- Never
- IfNotPresent
redis:
app: &pull_properties
properties:
pullPolicy:
enum:
- Always
- Never
- IfNotPresent
redis_exporter:
properties:
pullPolicy:
pull_policy:
enum:
- Always
- Never
- IfNotPresent
redis: *pull_properties
redis_exporter: *pull_properties
".to_string(), false)
}

6 changes: 6 additions & 0 deletions k8s/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub const STATUS_AGENT_STARTED: &str = "agent started";
pub const STATUS_MISSING_DIST: &str = "missing distribution";
pub const STATUS_MISSING_COMP: &str = "missing component";
pub const STATUS_CHECK_FAIL: &str = "Validations failed";
pub const STATUS_CONDITIONS_FAIL: &str = "Conditions script had errors";
pub const STATUS_MISSING_PROV: &str = "missing provider config";
pub const STATUS_MISSING_DEPS: &str = "missing dependencies";
pub const STATUS_WAITING_DEPS: &str = "waiting dependencies";
Expand Down Expand Up @@ -185,6 +186,9 @@ impl Install {
pub async fn update_status_check_failed(&self, client: Client, manager: &str, errors: Vec<String>) -> Result<Install, kube::Error> {
self.update_status_typed(client, manager, errors, STATUS_CHECK_FAIL).await
}
pub async fn update_status_conditions_failed(&self, client: Client, manager: &str, errors: Vec<String>) -> Result<Install, kube::Error> {
self.update_status_typed(client, manager, errors, STATUS_CONDITIONS_FAIL).await
}
pub async fn update_status_missing_provider(&self, client: Client, manager: &str, errors: Vec<String>) -> Result<Install, kube::Error> {
self.update_status_typed(client, manager, errors, STATUS_MISSING_PROV).await
}
Expand Down Expand Up @@ -295,6 +299,8 @@ impl Install {
};
insts.patch_status(&name, &pp, &patch).await
}

// TODO: should actually set the digest value (or remove that field from status)
pub async fn update_status_apply(&self, client: Client, manager: &str, tfstate: serde_json::Map<String, serde_json::Value>, commit_id: String) -> Result<Install, kube::Error> {
let name = self.name();
let insts: Api<Install> = Api::namespaced(client, self.metadata.namespace.clone().unwrap().as_str());
Expand Down
38 changes: 26 additions & 12 deletions operator/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub async fn reconcile(inst: Arc<Install>, ctx: Arc<Context>) -> Result<Action>
let ns = inst.namespace(); // inst is namespace scoped
let insts: Api<Install> = Api::namespaced(ctx.client.clone(), &ns);

info!("Reconciling Install \"{}\" in {}", inst.name_any(), ns);
debug!("Reconciling Install \"{}\" in {}", inst.name_any(), ns);
finalizer(&insts, INSTALL_FINALIZER, inst, |event| async {
match event {
Finalizer::Apply(inst) => inst.reconcile(ctx.clone()).await,
Expand All @@ -52,9 +52,16 @@ impl Reconciler for Install {
let mut jobs = JobHandler::new(ctx.client.clone(), my_ns);
let agent_name = format!("{ns}--{name}--agent");
let dist_name = self.spec.distrib.as_str();
let dist = match dists.get(dist_name).await {Ok(d) => d, Err(e) => {
self.update_status_missing_distrib(client, OPERATOR, vec!(format!("{:?}", e))).await.map_err(Error::KubeError)?;
return Err(Error::IllegalDistrib);
let dist = match dists.get(dist_name).await {Ok(d) => d, Err(e) => match e {
kube::Error::Service(b) => {
// the api server might be overwhelmed, retry in 2mn
info!("Network error while querying for distrib: {:}", b);
return Ok(Action::requeue(Duration::from_secs(120)))
},
e => {
self.update_status_missing_distrib(client, OPERATOR, vec!(format!("{:?}", e))).await.map_err(Error::KubeError)?;
return Err(Error::IllegalDistrib);
}
}};
//TODO: label the install with the distrib, component and category so searching installs is simple
if ns == my_ns && self.spec.distrib == "core" && self.spec.component == "vynil" && self.spec.category == "core" {
Expand Down Expand Up @@ -119,12 +126,10 @@ impl Reconciler for Install {
}
}
// Validate that the dependencies are actually installed
//TODO: support for only current namespace
let mut found = false;
let mut found_ns = String::new();
let mut found_name = String::new();
let namespaces: Api<Namespace> = Api::all(client.clone());
// TODO: sort the namespace with name close to the current namespace first so we found the right one
for ns in namespaces.list(&ListParams::default()).await.unwrap() {
let installs: Api<Install> = Api::namespaced(client.clone(), ns.metadata.name.clone().unwrap().as_str());
for install in installs.list(&ListParams::default()).await.unwrap() {
Expand Down Expand Up @@ -164,7 +169,7 @@ impl Reconciler for Install {
}
}
// Use provided check script
if comp.check.is_some() {
let conditions = if comp.check.is_some() {
let check = comp.check.clone().unwrap();
let mut script = script::Script::from_str(&check, script::new_base_context(
self.spec.category.clone(),
Expand Down Expand Up @@ -194,8 +199,17 @@ impl Reconciler for Install {
events::from_check("Install", &name, "Validation succeed".to_string(), None)
).await.map_err(Error::KubeError)?;
}
}
let hashedself = crate::jobs::HashedSelf::new(ns.as_str(), name.as_str(), self.options_digest().as_str(), self.spec.distrib.as_str(), &comp.commit_id);
match script.get_string_result("conditions") {
Ok(val) => val,
Err(e) => {
let mut errors: Vec<String> = Vec::new();
errors.push(format!("{e}"));
self.update_status_conditions_failed(client, OPERATOR, errors).await.map_err(Error::KubeError)?;
return Ok(Action::requeue(Duration::from_secs(5*60)))
}
}
} else {"{}".to_string()};
let hashedself = crate::jobs::HashedSelf::new(ns.as_str(), name.as_str(), self.options_digest().as_str(), self.spec.distrib.as_str(), &comp.commit_id, &conditions);
let agent_job = if self.should_plan() {
jobs.get_installs_plan(&hashedself, self.spec.category.as_str(), self.spec.component.as_str())
} else {
Expand Down Expand Up @@ -230,9 +244,9 @@ impl Reconciler for Install {
}};
if let Some(status) = job.status {
if status.completion_time.is_none() {
info!("Waiting after {agent_name} Job");
debug!("Waiting after {agent_name} Job");
recorder.publish(
events::from_check("Install", &name, "Bootstrap in progress, requeue".to_string(), None)
events::from_check("Install", &name, "Agent is progressing, waiting for its end".to_string(), None)
).await.map_err(Error::KubeError)?;
jobs.wait_max(agent_name.as_str(),2*60).await.map_err(Error::WaitError)?.map_err(Error::JobError)?;
}
Expand Down Expand Up @@ -269,7 +283,7 @@ impl Reconciler for Install {
jobs.delete(agent_name.as_str()).await.unwrap();
}
// Create the delete job
let hashedself = crate::jobs::HashedSelf::new(ns.as_str(), name.as_str(), self.options_digest().as_str(), self.spec.distrib.as_str(), "");
let hashedself = crate::jobs::HashedSelf::new(ns.as_str(), name.as_str(), self.options_digest().as_str(), self.spec.distrib.as_str(), "","");
let destroyer_job = jobs.get_installs_destroy(&hashedself, self.spec.category.as_str(), self.spec.component.as_str());

info!("Creating {deletor_name} Job");
Expand Down
11 changes: 8 additions & 3 deletions operator/src/jobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ pub struct HashedSelf {
name: String,
hash: String,
distrib: String,
commit_id: String
commit_id: String,
conditions: String
}

impl HashedSelf {
#[must_use] pub fn new(ns: &str, name: &str, hash: &str, distrib: &str, commit_id: &str) -> HashedSelf {
#[must_use] pub fn new(ns: &str, name: &str, hash: &str, distrib: &str, commit_id: &str, conditions: &str) -> HashedSelf {
HashedSelf {
ns: ns.to_string(),
name: name.to_string(),
hash: hash.to_string(),
distrib: distrib.to_string(),
commit_id: commit_id.to_string()
commit_id: commit_id.to_string(),
conditions: conditions.to_string()
}
}
}
Expand All @@ -50,6 +52,9 @@ fn install_container(hself: &HashedSelf) -> serde_json::Value {
},{
"name": "COMMIT_ID",
"value": hself.commit_id
},{
"name": "CONDITIONS",
"value": hself.conditions
},{
"name": "LOG_LEVEL",
"value": level
Expand Down
2 changes: 1 addition & 1 deletion package/src/pkg_script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ fn explode_to_tf(src: &str, dest: &str, base: &str) -> Result<()> {
values["metadata"]["namespace"] = serde_yaml::Value::from("${var.namespace}");
}
values["metadata"]["ownerReferences"] = serde_yaml::Value::from("${jsonencode(var.install_owner)}");
values["metadata"]["labels"] = serde_yaml::Value::from("${jsonencode(local.common-labels)}");
values["metadata"]["labels"] = serde_yaml::Value::from("${jsonencode(local.common_labels)}");
let str = serde_yaml::to_string(&values).unwrap();
content.push_str(&format!("resource \"kubectl_manifest\" \"{}\" {{
yaml_body = <<-EOF
Expand Down
9 changes: 8 additions & 1 deletion package/src/script.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use rhai::{Engine, Scope, Module, ImmutableString};
use std::{process, path::{PathBuf, Path}};
use anyhow::{Result, bail};
use anyhow::{Result, bail, anyhow};
use crate::pkg_script::add_pkg_to_engine;
use crate::k8s_script::add_k8s_to_engine;
use k8s::{Client, get_client};
Expand Down Expand Up @@ -129,4 +129,11 @@ impl Script {
}
Ok(())
}
pub fn get_string_result(&mut self, fnct: &str) -> Result<String> {
if self.have_fn(fnct) {
self.engine.eval_with_scope::<String>(&mut self.ctx, format!("{fnct}()").as_str()).map_err(|e| anyhow!("{e}"))
} else {
Ok("{}".to_string())
}
}
}
19 changes: 10 additions & 9 deletions package/src/terraform.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::path::{Path, PathBuf};
use anyhow::{Result, bail};
use k8s::yaml::{Providers, Component};
use serde::{Deserialize, Serialize};
use crate::shell;

pub fn gen_file(dest:&PathBuf, content: &String, force: bool) -> Result<()> {
Expand Down Expand Up @@ -338,12 +337,15 @@ pub fn gen_variables(dest_dir: &PathBuf, yaml: &Component,config:&serde_json::Ma
let mut content = format!("
variable \"category\" {{
default = \"{}\"
type = string
}}
variable \"component\" {{
default = \"{}\"
type = string
}}
variable \"instance\" {{
default = \"{}\"
type = string
}}
variable \"install_owner\" {{
default = null
Expand All @@ -352,8 +354,8 @@ variable \"install_owner\" {{
for (name,value) in config {
let str = serde_json::to_string(value).unwrap();
let output = match shell::get_output(&format!("echo 'jsondecode({:?})'|terraform console",str)) {Ok(d) => d, Err(e) => {bail!("{e}")}};
if yaml.tfaddtype.is_some() && *yaml.tfaddtype.as_ref().unwrap() {
let typed = yaml.get_tf_type(name);
if ! yaml.tfaddtype.is_some() || ! *yaml.tfaddtype.as_ref().unwrap() {
let typed = if name=="name"||name=="namespace" {"string".to_string()} else {yaml.get_tf_type(name)};
tracing::debug!("{}({})={}", name, typed, output);
content += format!("variable \"{}\" {{
default = {}
Expand All @@ -370,7 +372,6 @@ variable \"install_owner\" {{
gen_file(&file, &content, false)
}

#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct InstallOwner {
pub namespace: String,
pub name: String,
Expand All @@ -385,14 +386,14 @@ impl InstallOwner {
}
}
pub fn to_string(&self) -> String {
format!("[
format!("[{{
\"apiVersion\": \"vynil.solidite.fr/v1\",
\"kind\": \"Install\",
\"blockOwnerDeletion\": true,
\"namespace\": \"{}\",
\"name\": \"{}\",
\"uid\": \"{}\",
]", self.namespace, self.name, self.uid)
}}]", self.namespace, self.name, self.uid)
}
}

Expand All @@ -410,7 +411,7 @@ pub fn gen_tfvars(dest_dir: &PathBuf, config:&serde_json::Map<String, serde_json
", name, output).as_str();
}
if let Some(ownref) = owner {
let str = serde_json::to_string(&ownref).unwrap();
let str = ownref.to_string();
let output = match shell::get_output(&format!("echo 'jsondecode({:?})'|terraform console",str)) {Ok(d) => d, Err(e) => {bail!("{e}")}};

content += format!("install_owner = [{}]
Expand All @@ -437,7 +438,7 @@ locals {
# gitea_host = \"http://gitea-http.${var.domain}-ci.svc:3000/\"
# gitea_username = data.kubernetes_secret_v1.gitea.data[\"username\"]
# gitea_password = data.kubernetes_secret_v1.gitea.data[\"password\"]
common-labels = {
common_labels = {
\"vynil.solidite.fr/owner-name\" = var.instance
\"vynil.solidite.fr/owner-namespace\" = var.namespace
\"vynil.solidite.fr/owner-category\" = var.category
Expand Down Expand Up @@ -491,7 +492,7 @@ locals {
# }
data \"kustomization_overlay\" \"data\" {
common_labels = local.common-labels
common_labels = local.common_labels
namespace = var.namespace
resources = [for file in fileset(path.module, \"*.yaml\"): file if file != \"index.yaml\"]
}
Expand Down

0 comments on commit 7c4f35a

Please sign in to comment.