Compare commits
54 Commits
Author | SHA1 | Date | |
---|---|---|---|
1af78da2a2 | |||
5ca9997068 | |||
8ae66f1469 | |||
84a2a07173 | |||
634dcda630 | |||
f5013373ec | |||
f08acd5bab | |||
2724e43237 | |||
82173faf0f | |||
eaa51611d9 | |||
fde0c09a1b | |||
4457b6fc02 | |||
38e5919ea0 | |||
42219228ed | |||
e19bea34c3 | |||
19390304d8 | |||
16a1b5eb50 | |||
e5db5d8a0f | |||
dfc4a4d0b6 | |||
35d3168ae2 | |||
ad9ca49f7c | |||
bb6331c5e5 | |||
36aea1c05c | |||
c08dcb049d | |||
284648fe79 | |||
77c6c96fb0 | |||
b8fde5644c | |||
8f806474d1 | |||
252d0852ae | |||
c2b13dab9f | |||
b24056354f | |||
f24f2706ae | |||
c609dc4dd5 | |||
57137daa4e | |||
2f74098afe | |||
616d23c550 | |||
4bbca20797 | |||
6bb28cbfe3 | |||
c4c71766d9 | |||
22efa0fbe6 | |||
984d1fd259 | |||
74569c3b15 | |||
22527aadc6 | |||
d9ce9748b4 | |||
c76601a695 | |||
cdae730c6b | |||
b980ac949e | |||
6e8e63e5ee | |||
3e06479cda | |||
52007c82e0 | |||
1aa8562509 | |||
545439923f | |||
843139591c | |||
af57ef6cc8 |
3559
Cargo.lock
generated
3559
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -11,5 +11,7 @@ tracing-subscriber = { version = "0.3.18" }
|
||||
clap = { version = "4", features = ["derive", "env"] }
|
||||
dotenv = { version = "0.15" }
|
||||
|
||||
flux-releaser = { git = "https://git.front.kjuulh.io/kjuulh/flux-releaser", branch = "main" }
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.1"
|
||||
|
@ -17,9 +17,11 @@ uuid = { version = "1.7.0", features = ["v4"] }
|
||||
serde_yaml = "0.9.34"
|
||||
tokio-stream = { version = "0.1.15", features = ["full"] }
|
||||
walkdir = "2.5.0"
|
||||
minijinja = "2.0.1"
|
||||
minijinja = { version = "2.0.1", features = ["custom_syntax"] }
|
||||
futures = "0.3.30"
|
||||
|
||||
flux-releaser.workspace = true
|
||||
|
||||
[[test]]
|
||||
name = "integration"
|
||||
path = "tests/tests.rs"
|
||||
|
@ -1,3 +1,5 @@
|
||||
pub mod cluster_vars;
|
||||
pub mod crdb_database;
|
||||
pub mod cuddle_vars;
|
||||
pub mod ingress;
|
||||
pub mod vault_secret;
|
||||
|
131
crates/cuddle-clusters/src/catalog/crdb_database.rs
Normal file
131
crates/cuddle-clusters/src/catalog/crdb_database.rs
Normal file
@ -0,0 +1,131 @@
|
||||
use std::path::Path;
|
||||
|
||||
use minijinja::{value::Object, Value};
|
||||
|
||||
use crate::{catalog::cuddle_vars::CuddleVariable, Component};
|
||||
|
||||
use super::cuddle_vars::{load_cuddle_file, CuddleVariables};
|
||||
|
||||
pub struct CockroachDB {
|
||||
variables: CuddleVariables,
|
||||
}
|
||||
|
||||
impl CockroachDB {
|
||||
pub async fn new(path: &Path) -> anyhow::Result<Self> {
|
||||
let variables = load_cuddle_file(path).await?;
|
||||
|
||||
Ok(Self { variables })
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for CockroachDB {
|
||||
fn name(&self) -> String {
|
||||
"cuddle/crdb".into()
|
||||
}
|
||||
|
||||
fn render_value(
|
||||
&self,
|
||||
_environment: &str,
|
||||
_value: &serde_yaml::Value,
|
||||
) -> Option<anyhow::Result<minijinja::Value>> {
|
||||
if let Some(true) = self
|
||||
.variables
|
||||
.0
|
||||
.get("database")
|
||||
.and_then(|v| match v {
|
||||
CuddleVariable::Object(o) => Some(o),
|
||||
_ => None,
|
||||
})
|
||||
.and_then(|o| o.0.get("crdb"))
|
||||
.and_then(|o| match o {
|
||||
CuddleVariable::String(o) => {
|
||||
if o == "true" {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
return Some(Ok(minijinja::Value::from_object(CockroachDBValues {
|
||||
name: self.name(),
|
||||
enabled: true,
|
||||
})));
|
||||
}
|
||||
|
||||
Some(Ok(minijinja::Value::from_object(CockroachDBValues {
|
||||
name: self.name(),
|
||||
enabled: false,
|
||||
})))
|
||||
}
|
||||
|
||||
fn render(
|
||||
&self,
|
||||
_environment: &str,
|
||||
_value: &serde_yaml::Value,
|
||||
) -> Option<anyhow::Result<(String, String)>> {
|
||||
if let Some(true) = self
|
||||
.variables
|
||||
.0
|
||||
.get("database")
|
||||
.and_then(|v| match v {
|
||||
CuddleVariable::Object(o) => Some(o),
|
||||
_ => None,
|
||||
})
|
||||
.and_then(|o| o.0.get("crdb"))
|
||||
.and_then(|o| match o {
|
||||
CuddleVariable::String(o) => {
|
||||
if o == "true" {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
return Some(Ok((
|
||||
format!("{}.yaml", self.name().replace("/", "-")),
|
||||
r#"
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ vars.cuddle_crdb.file_name(vars.cuddle_vars.service) }}
|
||||
namespace: {{ vars.cluster_vars.namespace }}
|
||||
data:
|
||||
DATABASE_URL: postgresql://root@{{environment}}-cluster:26257/{{ vars.cuddle_vars.service | replace("-", "_") }}
|
||||
"#
|
||||
.into(),
|
||||
)));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CockroachDBValues {
|
||||
name: String,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl Object for CockroachDBValues {
|
||||
fn get_value(self: &std::sync::Arc<Self>, key: &minijinja::Value) -> Option<minijinja::Value> {
|
||||
let name = self.name.clone();
|
||||
match key.as_str()? {
|
||||
"has_values" => {
|
||||
if self.enabled {
|
||||
Some(minijinja::Value::from_serialize(true))
|
||||
} else {
|
||||
Some(minijinja::Value::from_serialize(false))
|
||||
}
|
||||
}
|
||||
"file_name" => Some(Value::from_function(move |file_name: String| {
|
||||
format!("{}-{}", file_name, name.replace("/", "-"))
|
||||
})),
|
||||
"env" => Some(Value::from_serialize("DATABASE_URL")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
@ -84,7 +84,7 @@ pub struct CuddleVars {
|
||||
pub variables: CuddleVariables,
|
||||
}
|
||||
|
||||
const PARENT_PLAN_PREFIX: &str = ".cuddle/plan";
|
||||
const PARENT_PLAN_PREFIX: &str = ".cuddle/base";
|
||||
const CUDDLE_FILE: &str = "cuddle.yaml";
|
||||
|
||||
impl CuddleVars {
|
||||
@ -95,7 +95,7 @@ impl CuddleVars {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_cuddle_file(path: &Path) -> BoxFuture<'static, anyhow::Result<CuddleVariables>> {
|
||||
pub fn load_cuddle_file(path: &Path) -> BoxFuture<'static, anyhow::Result<CuddleVariables>> {
|
||||
let path = path.to_path_buf();
|
||||
|
||||
async move {
|
||||
|
172
crates/cuddle-clusters/src/catalog/ingress.rs
Normal file
172
crates/cuddle-clusters/src/catalog/ingress.rs
Normal file
@ -0,0 +1,172 @@
|
||||
use std::path::Path;
|
||||
|
||||
use minijinja::{context, syntax::SyntaxConfig};
|
||||
|
||||
use crate::Component;
|
||||
|
||||
use super::cuddle_vars::{load_cuddle_file, CuddleVariable, CuddleVariables};
|
||||
|
||||
pub enum IngressType {
|
||||
External,
|
||||
Internal,
|
||||
ExternalGrpc,
|
||||
InternalGrpc,
|
||||
}
|
||||
|
||||
pub struct Ingress {
|
||||
variables: CuddleVariables,
|
||||
}
|
||||
|
||||
impl Ingress {
|
||||
pub async fn new(path: &Path) -> anyhow::Result<Self> {
|
||||
let variables = load_cuddle_file(path).await?;
|
||||
|
||||
Ok(Self { variables })
|
||||
}
|
||||
|
||||
fn render_ingress_types(
|
||||
&self,
|
||||
ingress_types: Vec<IngressType>,
|
||||
) -> anyhow::Result<(String, String)> {
|
||||
let mut templates = Vec::new();
|
||||
|
||||
let internal_template = r#"
|
||||
{%- set service_name = vars.cuddle_vars.service %}
|
||||
{%- set host_name = vars.cuddle_vars.service | replace("_", "-") | replace(".", "-") %}
|
||||
<%- macro host() -%>
|
||||
<% if connection_type is defined %><<connection_type>>.<% endif %>{{ host_name }}.{{ environment }}.<< base_host >>
|
||||
<%- endmacro %>
|
||||
|
||||
<%- macro k8s_service() -%>
|
||||
<%- if connection_type == "grpc" -%>
|
||||
{{ service_name }}-grpc
|
||||
<%- else -%>
|
||||
{{ service_name }}
|
||||
<%- endif -%>
|
||||
<%- endmacro %>
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/issuer: << issuer >>
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
labels:
|
||||
app: {{ service_name }}
|
||||
cluster: {{ vars.cluster_vars.name }}
|
||||
name: {{ service_name }}-<< name >>
|
||||
namespace: {{ vars.cluster_vars.namespace }}
|
||||
spec:
|
||||
rules:
|
||||
- host: << host() >>
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: << k8s_service() >>
|
||||
port:
|
||||
name: << name >>
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- << host() >>
|
||||
secretName: tls-{{ service_name }}-<< issuer >>-<< name >>-ingress-dns
|
||||
"#;
|
||||
|
||||
let get_template = |name, base_host, connection_type| {
|
||||
let mut env = minijinja::Environment::new();
|
||||
env.set_syntax(
|
||||
SyntaxConfig::builder()
|
||||
.block_delimiters("<%", "%>")
|
||||
.variable_delimiters("<<", ">>")
|
||||
.comment_delimiters("<#", "#>")
|
||||
.build()
|
||||
.expect("to be able to build minijinja syntax"),
|
||||
);
|
||||
|
||||
env.add_global("name", name);
|
||||
env.add_global("base_host", base_host);
|
||||
if let Some(connection_type) = connection_type {
|
||||
env.add_global("connection_type", connection_type);
|
||||
}
|
||||
|
||||
env.add_global("issuer", "kjuulh-app");
|
||||
|
||||
env.render_named_str("ingress.yaml", internal_template, context! {})
|
||||
};
|
||||
|
||||
for ingress_type in ingress_types {
|
||||
match ingress_type {
|
||||
IngressType::External => {
|
||||
templates.push(get_template("external-http", "kjuulh.app", None)?)
|
||||
}
|
||||
IngressType::Internal => {
|
||||
templates.push(get_template("internal-http", "internal.kjuulh.app", None)?)
|
||||
}
|
||||
IngressType::ExternalGrpc => {
|
||||
templates.push(get_template("external-grpc", "kjuulh.app", Some("grpc"))?)
|
||||
}
|
||||
IngressType::InternalGrpc => templates.push(get_template(
|
||||
"internal-grpc",
|
||||
"internal.kjuulh.app",
|
||||
Some("grpc"),
|
||||
)?),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(("ingress.yaml".into(), templates.join("\n")))
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Ingress {
|
||||
fn name(&self) -> String {
|
||||
"cuddle/ingress".to_string()
|
||||
}
|
||||
|
||||
fn render(
|
||||
&self,
|
||||
_environment: &str,
|
||||
_value: &serde_yaml::Value,
|
||||
) -> Option<anyhow::Result<(String, String)>> {
|
||||
if let Some(ingress_types) = self
|
||||
.variables
|
||||
.0
|
||||
.get("ingress")
|
||||
.and_then(|v| match v {
|
||||
CuddleVariable::Array(a) => Some(a),
|
||||
_ => None,
|
||||
})
|
||||
.map(|o| {
|
||||
let mut types = Vec::new();
|
||||
|
||||
for value in o {
|
||||
match value {
|
||||
CuddleVariable::Object(o) => {
|
||||
if o.0.contains_key("external") {
|
||||
types.push(IngressType::External)
|
||||
} else if o.0.contains_key("internal") {
|
||||
types.push(IngressType::Internal)
|
||||
} else if o.0.contains_key("external_grpc") {
|
||||
types.push(IngressType::ExternalGrpc)
|
||||
} else if o.0.contains_key("internal_grpc") {
|
||||
types.push(IngressType::InternalGrpc)
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
types
|
||||
})
|
||||
{
|
||||
return Some(self.render_ingress_types(ingress_types));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
@ -68,17 +68,42 @@ impl Component for VaultSecret {
|
||||
return Some(Ok(minijinja::Value::from_object(vault_values)));
|
||||
}
|
||||
|
||||
None
|
||||
Some(Ok(minijinja::Value::from_object(VaultSecretValues {
|
||||
name: self.name().replace("/", "-"),
|
||||
secrets: VaultSecretsLookup {
|
||||
secrets: Vec::default(),
|
||||
},
|
||||
})))
|
||||
}
|
||||
|
||||
fn render(
|
||||
&self,
|
||||
_environment: &str,
|
||||
_value: &serde_yaml::Value,
|
||||
value: &serde_yaml::Value,
|
||||
) -> Option<anyhow::Result<(String, String)>> {
|
||||
Some(Ok((
|
||||
format!("{}.yaml", self.name().replace("/", "_")),
|
||||
r#"apiVersion: secrets.hashicorp.com/v1beta1
|
||||
value
|
||||
.as_mapping()
|
||||
.and_then(|map| map.get("env"))
|
||||
.and_then(|v| v.as_mapping())
|
||||
.map(|v| {
|
||||
v.iter()
|
||||
.filter_map(|(k, v)| {
|
||||
if v.as_mapping()
|
||||
.map(|m| m.get("vault").filter(|v| v.as_bool() == Some(true)))
|
||||
.is_some()
|
||||
{
|
||||
Some(k)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter_map(|k| k.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.map(|_| {
|
||||
Ok((
|
||||
format!("{}.yaml", self.name().replace("/", "_")),
|
||||
r#"apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: {{ vars.vault_secret.file_name(vars.cuddle_vars.service) }}
|
||||
@ -92,8 +117,9 @@ spec:
|
||||
refreshAfter: 30s
|
||||
type: kv-v2
|
||||
"#
|
||||
.into(),
|
||||
)))
|
||||
.into(),
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub trait Component {
|
||||
fn name(&self) -> String;
|
||||
@ -27,11 +27,11 @@ pub trait Component {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConcreteComponent {
|
||||
inner: Rc<dyn Component + 'static>,
|
||||
inner: Arc<dyn Component + Sync + Send + 'static>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for ConcreteComponent {
|
||||
type Target = Rc<dyn Component + 'static>;
|
||||
type Target = Arc<dyn Component + Sync + Send + 'static>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
@ -39,8 +39,8 @@ impl std::ops::Deref for ConcreteComponent {
|
||||
}
|
||||
|
||||
impl ConcreteComponent {
|
||||
pub fn new<T: Component + 'static>(t: T) -> Self {
|
||||
Self { inner: Rc::new(t) }
|
||||
pub fn new<T: Component + Sync + Send + 'static>(t: T) -> Self {
|
||||
Self { inner: Arc::new(t) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,7 +54,7 @@ impl IntoComponent for ConcreteComponent {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Component + 'static> IntoComponent for T {
|
||||
impl<T: Component + Sync + Send + 'static> IntoComponent for T {
|
||||
fn into_component(self) -> ConcreteComponent {
|
||||
ConcreteComponent::new(self)
|
||||
}
|
||||
|
@ -7,3 +7,5 @@ pub mod catalog;
|
||||
|
||||
pub mod process;
|
||||
pub use process::{process, process_opts};
|
||||
|
||||
pub mod releaser;
|
||||
|
@ -13,12 +13,32 @@ use tokio_stream::{wrappers::ReadDirStream, StreamExt};
|
||||
use crate::components::{ConcreteComponent, IntoComponent};
|
||||
|
||||
pub async fn process() -> anyhow::Result<()> {
|
||||
process_opts(Vec::<ConcreteComponent>::new(), ProcessOpts::default()).await
|
||||
process_opts(
|
||||
Vec::<ConcreteComponent>::new(),
|
||||
ProcessOpts::default(),
|
||||
None::<NoUploadStrategy>,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub trait UploadStrategy {
|
||||
fn upload(&self, input_path: &Path) -> BoxFuture<'_, anyhow::Result<()>>;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NoUploadStrategy {}
|
||||
|
||||
impl UploadStrategy for NoUploadStrategy {
|
||||
fn upload(&self, _input_path: &Path) -> BoxFuture<'_, anyhow::Result<()>> {
|
||||
async move { Ok(()) }.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProcessOpts {
|
||||
pub path: PathBuf,
|
||||
pub output: PathBuf,
|
||||
|
||||
pub variables: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for ProcessOpts {
|
||||
@ -29,16 +49,18 @@ impl Default for ProcessOpts {
|
||||
.expect("to be able to get current dir")
|
||||
.join("cuddle-clusters")
|
||||
.join("k8s"),
|
||||
variables: HashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TEMPLATES_PATH_PREFIX: &str = "templates/clusters";
|
||||
const CUDDLE_PLAN_PATH_PREFIX: &str = ".cuddle/plan";
|
||||
const CUDDLE_PLAN_PATH_PREFIX: &str = ".cuddle/base";
|
||||
|
||||
pub async fn process_opts(
|
||||
components: impl IntoIterator<Item = impl IntoComponent>,
|
||||
opts: ProcessOpts,
|
||||
upload_strategy: Option<impl UploadStrategy>,
|
||||
) -> anyhow::Result<()> {
|
||||
let components = components
|
||||
.into_iter()
|
||||
@ -61,10 +83,21 @@ pub async fn process_opts(
|
||||
let template_files = load_template_files(&path).await?;
|
||||
tracing::debug!("found files: {:?}", template_files);
|
||||
|
||||
tokio::fs::remove_dir_all(&opts.output).await?;
|
||||
let _ = tokio::fs::remove_dir_all(&opts.output).await;
|
||||
tokio::fs::create_dir_all(&opts.output).await?;
|
||||
|
||||
process_templates(&components, &clusters, &template_files, &opts.output).await?;
|
||||
process_templates(
|
||||
&components,
|
||||
&clusters,
|
||||
&template_files,
|
||||
&opts.output,
|
||||
&opts.variables,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(upload_strategy) = upload_strategy {
|
||||
upload_strategy.upload(&opts.output).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -184,6 +217,7 @@ async fn process_templates(
|
||||
clusters: &CuddleClusters,
|
||||
template_files: &TemplateFiles,
|
||||
dest: &Path,
|
||||
variables: &HashMap<String, String>,
|
||||
) -> anyhow::Result<()> {
|
||||
for (environment, value) in clusters.iter() {
|
||||
process_cluster(
|
||||
@ -192,6 +226,7 @@ async fn process_templates(
|
||||
environment,
|
||||
template_files,
|
||||
&dest.join(environment),
|
||||
variables,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@ -205,9 +240,18 @@ async fn process_cluster(
|
||||
environment: &str,
|
||||
template_files: &TemplateFiles,
|
||||
dest: &Path,
|
||||
variables: &HashMap<String, String>,
|
||||
) -> anyhow::Result<()> {
|
||||
for (_, template_file) in &template_files.templates {
|
||||
process_template_file(components, value, environment, template_file, dest).await?;
|
||||
process_template_file(
|
||||
components,
|
||||
value,
|
||||
environment,
|
||||
template_file,
|
||||
dest,
|
||||
variables,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
for (template_file_name, template_content) in components
|
||||
@ -228,6 +272,7 @@ async fn process_cluster(
|
||||
&template_file_name,
|
||||
&template_content,
|
||||
dest,
|
||||
variables,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@ -245,6 +290,7 @@ async fn process_template_file(
|
||||
environment: &str,
|
||||
template_file: &PathBuf,
|
||||
dest: &Path,
|
||||
variables: &HashMap<String, String>,
|
||||
) -> anyhow::Result<()> {
|
||||
let file = tokio::fs::read_to_string(template_file)
|
||||
.await
|
||||
@ -260,6 +306,7 @@ async fn process_template_file(
|
||||
&file_name.to_string_lossy(),
|
||||
&file,
|
||||
dest,
|
||||
variables,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@ -273,6 +320,7 @@ async fn process_render_template(
|
||||
file_name: &str,
|
||||
file_content: &str,
|
||||
dest: &Path,
|
||||
user_vars: &HashMap<String, String>,
|
||||
) -> anyhow::Result<()> {
|
||||
if !dest.exists() {
|
||||
tokio::fs::create_dir_all(dest).await?;
|
||||
@ -286,6 +334,7 @@ async fn process_render_template(
|
||||
env.add_global("environment", environment);
|
||||
|
||||
let mut variables = HashMap::new();
|
||||
|
||||
for component in components {
|
||||
let name = component.name();
|
||||
|
||||
@ -295,6 +344,10 @@ async fn process_render_template(
|
||||
variables.insert(name.replace("/", "_"), value);
|
||||
}
|
||||
}
|
||||
variables.insert(
|
||||
"user_vars".into(),
|
||||
minijinja::Value::from_serialize(user_vars),
|
||||
);
|
||||
|
||||
let tmpl = env.get_template(file_name)?;
|
||||
let rendered = tmpl.render(context! {
|
||||
|
108
crates/cuddle-clusters/src/releaser.rs
Normal file
108
crates/cuddle-clusters/src/releaser.rs
Normal file
@ -0,0 +1,108 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use flux_releaser::{
|
||||
app::{LocalApp, SharedLocalApp},
|
||||
services::flux_local_cluster::extensions::FluxLocalClusterManagerExt,
|
||||
};
|
||||
use futures::{future::BoxFuture, FutureExt};
|
||||
|
||||
use crate::process::UploadStrategy;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Releaser {
|
||||
registry: String,
|
||||
service: String,
|
||||
}
|
||||
|
||||
impl Releaser {
|
||||
pub fn with_registry(&mut self, registry: impl Into<String>) -> &mut Self {
|
||||
self.registry = registry.into();
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_service(&mut self, service: impl Into<String>) -> &mut Self {
|
||||
self.service = service.into();
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn release(&self, input_path: impl Into<PathBuf>) -> anyhow::Result<()> {
|
||||
let input_path = input_path.into();
|
||||
let branch = self.get_branch()?.ok_or(anyhow::anyhow!(
|
||||
"failed to find branch, required for triggering release"
|
||||
))?;
|
||||
|
||||
tracing::trace!("triggering release for: {}", input_path.display());
|
||||
|
||||
let local_app =
|
||||
SharedLocalApp::new(LocalApp::new(&self.registry).await?).flux_local_cluster_manager();
|
||||
|
||||
let upload_id = local_app
|
||||
.package_clusters(input_path)
|
||||
.await
|
||||
.context("failed to package clusters")?;
|
||||
|
||||
local_app
|
||||
.commit_artifact(&self.service, &branch, upload_id)
|
||||
.await
|
||||
.context("failed to commit artifact")?;
|
||||
|
||||
local_app
|
||||
.trigger_release(&self.service, &branch)
|
||||
.await
|
||||
.context("failed to trigger release")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_branch(&self) -> anyhow::Result<Option<String>> {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "--abbrev-ref", "HEAD"])
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
let branch = std::str::from_utf8(&output.stdout)?.trim().to_string();
|
||||
Ok(Some(branch))
|
||||
} else {
|
||||
let err = std::str::from_utf8(&output.stderr)?;
|
||||
anyhow::bail!("Failed to get branch name: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UploadStrategy for Releaser {
|
||||
fn upload(&self, input_path: &Path) -> BoxFuture<'_, anyhow::Result<()>> {
|
||||
let input_path = input_path.to_path_buf();
|
||||
|
||||
async move {
|
||||
self.release(input_path).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::releaser::Releaser;
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_upload_test() -> anyhow::Result<()> {
|
||||
return Ok(());
|
||||
let releaser = Releaser::default();
|
||||
|
||||
releaser
|
||||
.release(
|
||||
"/home/kjuulh/git/git.front.kjuulh.io/kjuulh/cuddle-rust-service-plan/.cuddle/tmp/cuddle-clusters",
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
use std::{cmp::Ordering, path::Path};
|
||||
use std::{cmp::Ordering, collections::HashMap, path::Path};
|
||||
|
||||
use cuddle_clusters::{process::ProcessOpts, ConcreteComponent, IntoComponent};
|
||||
use cuddle_clusters::{
|
||||
process::{NoUploadStrategy, ProcessOpts},
|
||||
ConcreteComponent, IntoComponent,
|
||||
};
|
||||
use walkdir::DirEntry;
|
||||
|
||||
pub(crate) async fn run_test_with_components(
|
||||
@ -27,7 +30,9 @@ pub(crate) async fn run_test_with_components(
|
||||
ProcessOpts {
|
||||
path: test_folder.clone(),
|
||||
output: actual.clone(),
|
||||
variables: HashMap::default(),
|
||||
},
|
||||
None::<NoUploadStrategy>,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@ -37,7 +42,9 @@ pub(crate) async fn run_test_with_components(
|
||||
ProcessOpts {
|
||||
path: test_folder,
|
||||
output: expected.clone(),
|
||||
variables: HashMap::default(),
|
||||
},
|
||||
None::<NoUploadStrategy>,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
@ -4,7 +4,10 @@ mod can_run_for_env;
|
||||
mod cuddle_vars;
|
||||
|
||||
use cuddle_clusters::{
|
||||
catalog::{cluster_vars::ClusterVars, cuddle_vars::CuddleVars, vault_secret::VaultSecret},
|
||||
catalog::{
|
||||
cluster_vars::ClusterVars, crdb_database::CockroachDB, cuddle_vars::CuddleVars,
|
||||
ingress::Ingress, vault_secret::VaultSecret,
|
||||
},
|
||||
IntoComponent,
|
||||
};
|
||||
|
||||
@ -96,3 +99,38 @@ async fn with_vault_secrets() -> anyhow::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn with_crdb() -> anyhow::Result<()> {
|
||||
let current_dir = std::env::current_dir()?.join("tests/with_crdb");
|
||||
|
||||
run_test_with_components(
|
||||
"with_crdb",
|
||||
vec![
|
||||
CuddleVars::new(¤t_dir).await?.into_component(),
|
||||
ClusterVars::default().into_component(),
|
||||
VaultSecret::default().into_component(),
|
||||
CockroachDB::new(¤t_dir).await?.into_component(),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn with_ingress() -> anyhow::Result<()> {
|
||||
let current_dir = std::env::current_dir()?.join("tests/with_ingress");
|
||||
|
||||
run_test_with_components(
|
||||
"with_ingress",
|
||||
vec![
|
||||
CuddleVars::new(¤t_dir).await?.into_component(),
|
||||
ClusterVars::default().into_component(),
|
||||
Ingress::new(¤t_dir).await?.into_component(),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ vars.cuddle_vars.service }}-config
|
||||
data:
|
||||
{%- if (vars.cluster_vars.env | items | length) > 0 %}
|
||||
environment:
|
||||
{%- for (name, value) in vars.cluster_vars.env | dictsort %}
|
||||
{{name | upper | replace(".", "_") | replace("-", "_") }}: {{value}}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
7
crates/cuddle-clusters/tests/with_crdb/cuddle.yaml
Normal file
7
crates/cuddle-clusters/tests/with_crdb/cuddle.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
vars:
|
||||
service: service
|
||||
database:
|
||||
crdb: "true"
|
||||
|
||||
cuddle/clusters:
|
||||
dev:
|
@ -0,0 +1,9 @@
|
||||
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: service-cuddle-crdb
|
||||
namespace: dev
|
||||
data:
|
||||
DATABASE_URL: postgresql://root@dev-cluster:26257/service
|
||||
|
@ -0,0 +1,39 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: service
|
||||
name: service
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: service
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: service
|
||||
spec:
|
||||
containers:
|
||||
- args:
|
||||
- serve
|
||||
command:
|
||||
- service
|
||||
image: kasperhermansen/service:main-1715336504
|
||||
name: service
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: service-config
|
||||
env:
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: service-cuddle-crdb
|
||||
key: DATABASE_URL
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
name: external-http
|
||||
- containerPort: 3001
|
||||
name: internal-http
|
||||
- containerPort: 3002
|
||||
name: internal-grpc
|
@ -0,0 +1,54 @@
|
||||
{%- set service_name = vars.cuddle_vars.service -%}
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ service_name }}
|
||||
name: {{ service_name }}
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ service_name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ service_name }}
|
||||
spec:
|
||||
containers:
|
||||
- args:
|
||||
- serve
|
||||
command:
|
||||
- {{ service_name }}
|
||||
image: kasperhermansen/{{ service_name }}:main-1715336504
|
||||
name: {{ service_name }}
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{service_name}}-config
|
||||
{%- if vars.vault_secret.has_values or vars.cuddle_crdb.has_values %}
|
||||
env:
|
||||
{%- if vars.vault_secret.has_values %}
|
||||
{%- for secret in vars.vault_secret.secrets %}
|
||||
- name: {{secret | upper | replace(".", "_") | replace("-", "_") }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ vars.vault_secret.file_name(service_name) }}
|
||||
key: {{ secret }}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
{%- if vars.cuddle_crdb.has_values %}
|
||||
- name: {{vars.cuddle_crdb.env }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ vars.cuddle_crdb.file_name(service_name) }}
|
||||
key: {{ vars.cuddle_crdb.env }}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
name: external-http
|
||||
- containerPort: 3001
|
||||
name: internal-http
|
||||
- containerPort: 3002
|
||||
name: internal-grpc
|
11
crates/cuddle-clusters/tests/with_ingress/cuddle.yaml
Normal file
11
crates/cuddle-clusters/tests/with_ingress/cuddle.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
vars:
|
||||
service: service
|
||||
ingress:
|
||||
- external: "true"
|
||||
- internal: "true"
|
||||
- external_grpc: "true"
|
||||
- internal_grpc: "true"
|
||||
|
||||
cuddle/clusters:
|
||||
dev:
|
||||
prod:
|
@ -0,0 +1,121 @@
|
||||
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/issuer: kjuulh-app
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
labels:
|
||||
app: service
|
||||
cluster: dev
|
||||
name: service-external-http
|
||||
namespace: dev
|
||||
spec:
|
||||
rules:
|
||||
- host: service.dev.kjuulh.app
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: service
|
||||
port:
|
||||
name: external-http
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- service.dev.kjuulh.app
|
||||
secretName: tls-service-kjuulh-app-external-http-ingress-dns
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/issuer: kjuulh-app
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
labels:
|
||||
app: service
|
||||
cluster: dev
|
||||
name: service-internal-http
|
||||
namespace: dev
|
||||
spec:
|
||||
rules:
|
||||
- host: service.dev.internal.kjuulh.app
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: service
|
||||
port:
|
||||
name: internal-http
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- service.dev.internal.kjuulh.app
|
||||
secretName: tls-service-kjuulh-app-internal-http-ingress-dns
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/issuer: kjuulh-app
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
labels:
|
||||
app: service
|
||||
cluster: dev
|
||||
name: service-external-grpc
|
||||
namespace: dev
|
||||
spec:
|
||||
rules:
|
||||
- host: grpc.service.dev.kjuulh.app
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: service-grpc
|
||||
port:
|
||||
name: external-grpc
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- grpc.service.dev.kjuulh.app
|
||||
secretName: tls-service-kjuulh-app-external-grpc-ingress-dns
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/issuer: kjuulh-app
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
labels:
|
||||
app: service
|
||||
cluster: dev
|
||||
name: service-internal-grpc
|
||||
namespace: dev
|
||||
spec:
|
||||
rules:
|
||||
- host: grpc.service.dev.internal.kjuulh.app
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: service-grpc
|
||||
port:
|
||||
name: internal-grpc
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- grpc.service.dev.internal.kjuulh.app
|
||||
secretName: tls-service-kjuulh-app-internal-grpc-ingress-dns
|
@ -0,0 +1,30 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: service
|
||||
name: service
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: service
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: service
|
||||
spec:
|
||||
containers:
|
||||
- args:
|
||||
- serve
|
||||
command:
|
||||
- service
|
||||
image: kasperhermansen/service:main-1715336504
|
||||
name: service
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
name: external-http
|
||||
- containerPort: 3001
|
||||
name: internal-http
|
||||
- containerPort: 3002
|
||||
name: internal-grpc
|
@ -0,0 +1,121 @@
|
||||
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/issuer: kjuulh-app
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
labels:
|
||||
app: service
|
||||
cluster: prod
|
||||
name: service-external-http
|
||||
namespace: prod
|
||||
spec:
|
||||
rules:
|
||||
- host: service.prod.kjuulh.app
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: service
|
||||
port:
|
||||
name: external-http
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- service.prod.kjuulh.app
|
||||
secretName: tls-service-kjuulh-app-external-http-ingress-dns
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/issuer: kjuulh-app
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
labels:
|
||||
app: service
|
||||
cluster: prod
|
||||
name: service-internal-http
|
||||
namespace: prod
|
||||
spec:
|
||||
rules:
|
||||
- host: service.prod.internal.kjuulh.app
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: service
|
||||
port:
|
||||
name: internal-http
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- service.prod.internal.kjuulh.app
|
||||
secretName: tls-service-kjuulh-app-internal-http-ingress-dns
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/issuer: kjuulh-app
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
labels:
|
||||
app: service
|
||||
cluster: prod
|
||||
name: service-external-grpc
|
||||
namespace: prod
|
||||
spec:
|
||||
rules:
|
||||
- host: grpc.service.prod.kjuulh.app
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: service-grpc
|
||||
port:
|
||||
name: external-grpc
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- grpc.service.prod.kjuulh.app
|
||||
secretName: tls-service-kjuulh-app-external-grpc-ingress-dns
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/issuer: kjuulh-app
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
labels:
|
||||
app: service
|
||||
cluster: prod
|
||||
name: service-internal-grpc
|
||||
namespace: prod
|
||||
spec:
|
||||
rules:
|
||||
- host: grpc.service.prod.internal.kjuulh.app
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: service-grpc
|
||||
port:
|
||||
name: internal-grpc
|
||||
path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- grpc.service.prod.internal.kjuulh.app
|
||||
secretName: tls-service-kjuulh-app-internal-grpc-ingress-dns
|
@ -0,0 +1,30 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: service
|
||||
name: service
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: service
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: service
|
||||
spec:
|
||||
containers:
|
||||
- args:
|
||||
- serve
|
||||
command:
|
||||
- service
|
||||
image: kasperhermansen/service:main-1715336504
|
||||
name: service
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
name: external-http
|
||||
- containerPort: 3001
|
||||
name: internal-http
|
||||
- containerPort: 3002
|
||||
name: internal-grpc
|
@ -1,26 +1,20 @@
|
||||
{%- set service_name = vars.cuddle_vars.service -%}
|
||||
{%- set cluster_name = vars.cluster_vars.name -%}
|
||||
{%- set cluster_namespace = vars.cluster_vars.namespace -%}
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ service_name }}
|
||||
cluster: {{ cluster_name }}
|
||||
name: {{ service_name }}
|
||||
namespace: {{ cluster_namespace }}
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ service_name }}
|
||||
cluster: {{ cluster_name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ service_name }}
|
||||
cluster: {{ cluster_name }}
|
||||
spec:
|
||||
containers:
|
||||
- args:
|
||||
@ -29,9 +23,6 @@ spec:
|
||||
- {{ service_name }}
|
||||
image: kasperhermansen/{{ service_name }}:main-1715336504
|
||||
name: {{ service_name }}
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{service_name}}-config
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
name: external-http
|
Loading…
Reference in New Issue
Block a user