Compare commits

...

51 Commits
v0.1.0 ... main

Author SHA1 Message Date
f08acd5bab chore(deps): update rust crate anyhow to v1.0.89
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2024-09-15 04:20:50 +00:00
2724e43237 chore(deps): update rust crate anyhow to v1.0.88
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2024-09-12 00:22:44 +00:00
82173faf0f
feat: set interval down to a minute
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-09-08 12:44:21 +02:00
eaa51611d9
feat: with native roots
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-09-08 00:38:00 +02:00
fde0c09a1b
feat: with explicit install
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-09-07 23:49:19 +02:00
4457b6fc02
feat: with install
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-09-07 23:43:24 +02:00
38e5919ea0
feat: with ring
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-09-07 23:39:31 +02:00
42219228ed
feat: enable stuff
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-09-07 23:33:32 +02:00
e19bea34c3
feat: update packages
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-09-07 23:24:49 +02:00
19390304d8
feat: update packages
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-09-07 23:16:20 +02:00
16a1b5eb50 chore(deps): update all dependencies
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2024-07-06 13:30:20 +00:00
e5db5d8a0f
feat: add actual grpc service
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-29 23:04:52 +02:00
dfc4a4d0b6
feat: fix typo
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-29 22:18:06 +02:00
35d3168ae2
feat: revert to actual label
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-29 22:13:57 +02:00
ad9ca49f7c
feat: fix scheme
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-29 22:05:10 +02:00
bb6331c5e5
feat: trying with h2c service
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-29 22:01:36 +02:00
36aea1c05c
feat: add grpc ports as well
Some checks failed
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-29 21:21:34 +02:00
c08dcb049d
feat: fix name
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-27 22:43:21 +02:00
284648fe79
feat: rename
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-27 22:40:40 +02:00
77c6c96fb0
feat: remove suffix
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-27 22:40:23 +02:00
b8fde5644c
feat: add ingress
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-27 22:13:18 +02:00
8f806474d1
feat: add ingress wip
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-27 21:39:29 +02:00
252d0852ae
refactor: move crdb to own file
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-27 20:57:15 +02:00
c2b13dab9f
feat: use lower case
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-26 23:38:19 +02:00
b24056354f
feat: use actual service name
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-26 23:32:57 +02:00
f24f2706ae
feat: cannot use lowe case
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-26 23:28:56 +02:00
c609dc4dd5
feat: update filename
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-26 23:27:55 +02:00
57137daa4e
feat: add namespace
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-26 23:22:38 +02:00
2f74098afe
feat: update env
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-26 23:17:44 +02:00
616d23c550
feat: add crdb
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-26 22:51:56 +02:00
4bbca20797
feat: add webroots
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-26 20:46:46 +02:00
6bb28cbfe3
feat: with tls
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-26 17:24:18 +02:00
c4c71766d9
feat: use new tokio stream
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-26 15:46:28 +02:00
22efa0fbe6
feat: update client
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-26 15:20:14 +02:00
984d1fd259
feat: more debugging
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-26 15:12:29 +02:00
74569c3b15
feat: update
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-26 14:45:35 +02:00
22527aadc6
feat: update flux releaser
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-25 23:36:07 +02:00
d9ce9748b4
feat: actually set registry
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-25 23:11:41 +02:00
c76601a695
feat: add option for releaser
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-25 23:03:07 +02:00
cdae730c6b
feat: add slot for upload strategy
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-25 21:34:15 +02:00
b980ac949e fix(deps): update rust crate serde to v1.0.203
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2024-05-25 18:35:45 +00:00
6e8e63e5ee
feat: add user variables to input
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-25 14:47:18 +02:00
3e06479cda
fix: only create vault secret template if actual secret found
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-25 14:37:13 +02:00
52007c82e0
feat: without remove all
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-25 14:27:26 +02:00
1aa8562509
feat: plan -> base
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-25 14:21:11 +02:00
545439923f
feat: add sync
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-25 14:04:54 +02:00
843139591c
feat: add send
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-25 14:03:00 +02:00
af57ef6cc8
feat: use arc instead
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-25 13:58:19 +02:00
10a56ad690 Merge pull request 'chore(release): v0.1.1' (#3) from cuddle-please/release into main
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #3
2024-05-25 13:46:44 +02:00
cuddle-please
6e6ce96855 chore(release): 0.1.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-25 11:46:15 +00:00
32281c02bb
chore: remove deps
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-05-25 13:44:34 +02:00
26 changed files with 3142 additions and 373 deletions

View File

@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.1.1] - 2024-05-25
### Other
- remove deps
## [0.1.0] - 2024-05-25
### Added

2463
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,8 @@ tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3.18" }
clap = { version = "4", features = ["derive", "env"] }
dotenv = { version = "0.15" }
axum = { version = "0.7" }
flux-releaser = { git = "https://git.front.kjuulh.io/kjuulh/flux-releaser", branch = "main" }
[workspace.package]
version = "0.1.0"
version = "0.1.1"

View File

@ -11,24 +11,17 @@ tracing.workspace = true
tracing-subscriber.workspace = true
clap.workspace = true
dotenv.workspace = true
axum.workspace = true
serde = { version = "1.0.197", features = ["derive"] }
sqlx = { version = "0.7.3", features = [
"runtime-tokio",
"tls-rustls",
"postgres",
"uuid",
"time",
] }
uuid = { version = "1.7.0", features = ["v4"] }
tower-http = { version = "0.5.2", features = ["cors", "trace"] }
serde_yaml = "0.9.34"
tokio-stream = "0.1.15"
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"

View File

@ -1,3 +1,5 @@
pub mod cluster_vars;
pub mod crdb_database;
pub mod cuddle_vars;
pub mod ingress;
pub mod vault_secret;

View 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,
}
}
}

View File

@ -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 {

View 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
}
}

View File

@ -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(),
))
})
}
}

View File

@ -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)
}

View File

@ -7,3 +7,5 @@ pub mod catalog;
pub mod process;
pub use process::{process, process_opts};
pub mod releaser;

View File

@ -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! {

View 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(())
}
}

View File

@ -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?;
}

View File

@ -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(&current_dir).await?.into_component(),
ClusterVars::default().into_component(),
VaultSecret::default().into_component(),
CockroachDB::new(&current_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(&current_dir).await?.into_component(),
ClusterVars::default().into_component(),
Ingress::new(&current_dir).await?.into_component(),
],
)
.await?;
Ok(())
}

View File

@ -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 %}

View File

@ -0,0 +1,7 @@
vars:
service: service
database:
crdb: "true"
cuddle/clusters:
dev:

View File

@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: service-cuddle-crdb
namespace: dev
data:
DATABASE_URL: postgresql://root@dev-cluster:26257/service

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,11 @@
vars:
service: service
ingress:
- external: "true"
- internal: "true"
- external_grpc: "true"
- internal_grpc: "true"
cuddle/clusters:
dev:
prod:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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