Compare commits

..

No commits in common. "main" and "v0.1.1" have entirely different histories.
main ... v0.1.1

47 changed files with 319 additions and 5271 deletions

View File

@ -6,87 +6,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.2.0] - 2024-11-29
### Added
- feat/add-postgres-database (#20)
feat: add postgres database
Signed-off-by: kjuulh <contact@kjuulh.io>
feat: add postgres and more templates
Signed-off-by: kjuulh <contact@kjuulh.io>
Reviewed-on: https://git.front.kjuulh.io/kjuulh/cuddle-clusters/pulls/20
Co-authored-by: kjuulh <contact@kjuulh.io>
Co-committed-by: kjuulh <contact@kjuulh.io>
- add support for raw reading of variables
- add replicas
- set interval down to a minute
- with native roots
- with explicit install
- with install
- with ring
- enable stuff
- update packages
- update packages
- add actual grpc service
- fix typo
- revert to actual label
- fix scheme
- trying with h2c service
- add grpc ports as well
- fix name
- rename
- remove suffix
- add ingress
- add ingress wip
- use lower case
- use actual service name
- cannot use lowe case
- update filename
- add namespace
- update env
- add crdb
- add webroots
- with tls
- use new tokio stream
- update client
- more debugging
- update
- update flux releaser
- actually set registry
- add option for releaser
- add slot for upload strategy
- add user variables to input
- without remove all
- plan -> base
- add sync
- add send
- use arc instead
### Fixed
- *(deps)* update rust crate serde to v1.0.215
- *(deps)* update rust crate futures to v0.3.31
- *(deps)* update rust crate minijinja to v2.3.1
- *(deps)* update rust crate minijinja to v2.3.0
- *(deps)* update rust crate serde to v1.0.203
- only create vault secret template if actual secret found
### Other
- *(deps)* update rust crate tracing to v0.1.41
- *(deps)* update all dependencies
- *(deps)* update rust crate clap to v4.5.20
- *(deps)* update rust crate clap to v4.5.19
- *(deps)* update rust crate clap to v4.5.18
- *(deps)* update rust crate anyhow to v1.0.89
- *(deps)* update rust crate anyhow to v1.0.88
- *(deps)* update all dependencies
- move crdb to own file
## [0.1.1] - 2024-05-25
### Other

3961
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,5 @@ 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.2.0"
version = "0.1.1"

View File

@ -17,11 +17,9 @@ 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 = { version = "2.0.1", features = ["custom_syntax"] }
minijinja = "2.0.1"
futures = "0.3.30"
flux-releaser.workspace = true
[[test]]
name = "integration"
path = "tests/tests.rs"

View File

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

View File

@ -1,24 +1,7 @@
use std::collections::{BTreeMap, HashMap};
use serde_yaml::Value;
use std::collections::HashMap;
use crate::Component;
use super::cuddle_vars::CuddleVariables;
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum RawClusterVariable {
Map(RawClusterVariables),
List(RawClusterVariableList),
String(String),
Bool(bool),
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct RawClusterVariables(BTreeMap<String, RawClusterVariable>);
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct RawClusterVariableList(Vec<RawClusterVariable>);
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum ClusterVariable {
String(String),
@ -34,10 +17,6 @@ pub struct ClusterVariables {
env: ClusterEnv,
name: String,
namespace: String,
replicas: u64,
raw: RawClusterVariables,
// raw: CuddleVariables,
}
#[derive(Default)]
@ -57,10 +36,9 @@ impl Component for ClusterVars {
environment: &str,
value: &serde_yaml::Value,
) -> Option<anyhow::Result<minijinja::Value>> {
let mut vars = ClusterVariables {
replicas: 3,
..Default::default()
};
let mut vars = ClusterVariables::default();
// TODO: actually extract values
if let Some(mapping) = value.as_mapping() {
if let Some(env) = mapping.get("env") {
@ -74,14 +52,7 @@ impl Component for ClusterVars {
}
}
}
if let Some(replicas) = mapping.get("replicas") {
if let Some(replicas) = replicas.as_u64() {
vars.replicas = replicas;
}
}
}
vars.raw = value.into();
vars.name = environment.into();
vars.namespace = environment.into();
@ -96,11 +67,6 @@ impl minijinja::value::Object for ClusterVariables {
"env" => minijinja::Value::from_object(self.env.clone()),
"name" => minijinja::Value::from_safe_string(self.name.clone()),
"namespace" => minijinja::Value::from_safe_string(self.namespace.clone()),
"replicas" => minijinja::Value::from_safe_string(self.replicas.to_string()),
"raw" => {
tracing::info!("returning raw: {:?}", self.raw);
minijinja::Value::from_object(self.raw.clone())
}
_ => return None,
};
@ -126,95 +92,3 @@ impl minijinja::value::Object for ClusterEnv {
)
}
}
impl From<&Value> for RawClusterVariables {
fn from(value: &Value) -> Self {
match value {
Value::Mapping(mapping) => RawClusterVariables(
mapping
.into_iter()
.map(|(k, v)| {
(
k.as_str()
.expect("keys to always be valid strings")
.to_string(),
v.into(),
)
})
.collect(),
),
Value::Null => RawClusterVariables::default(),
_ => todo!(),
}
}
}
impl From<&Value> for RawClusterVariable {
fn from(value: &Value) -> Self {
match value {
Value::Null => Self::Map(RawClusterVariables::default()),
Value::Bool(b) => Self::Bool(*b),
Value::Number(number) => Self::String(number.to_string()),
Value::String(s) => Self::String(s.into()),
Value::Sequence(vec) => Self::List(RawClusterVariableList(
vec.iter().map(|i| i.into()).collect(),
)),
Value::Mapping(mapping) => Self::Map(RawClusterVariables(
mapping
.into_iter()
.map(|(k, v)| {
(
k.as_str()
.expect("keys to always be valid strings")
.to_string(),
v.into(),
)
})
.collect(),
)),
Value::Tagged(_tagged_value) => todo!(),
}
}
}
impl minijinja::value::Object for RawClusterVariables {
fn get_value(self: &std::sync::Arc<Self>, key: &minijinja::Value) -> Option<minijinja::Value> {
self.0.get(key.as_str()?).map(|o| match o {
RawClusterVariable::Map(raw_cluster_variables) => {
minijinja::Value::from_object(raw_cluster_variables.clone())
}
RawClusterVariable::List(list) => minijinja::Value::from_object(list.clone()),
RawClusterVariable::String(s) => minijinja::Value::from_safe_string(s.clone()),
RawClusterVariable::Bool(b) => minijinja::Value::from_safe_string(b.to_string()),
})
}
fn enumerate(self: &std::sync::Arc<Self>) -> minijinja::value::Enumerator {
minijinja::value::Enumerator::Values(
self.0
.keys()
.map(|key| minijinja::Value::from_safe_string(key.clone()))
.collect::<Vec<_>>(),
)
}
}
impl minijinja::value::Object for RawClusterVariableList {
fn enumerate(self: &std::sync::Arc<Self>) -> minijinja::value::Enumerator {
minijinja::value::Enumerator::Values(
self.0
.iter()
.map(|i| match i {
RawClusterVariable::Map(raw_cluster_variables) => {
minijinja::Value::from_object(raw_cluster_variables.clone())
}
RawClusterVariable::List(list) => minijinja::Value::from_object(list.clone()),
RawClusterVariable::String(s) => minijinja::Value::from_safe_string(s.clone()),
RawClusterVariable::Bool(b) => {
minijinja::Value::from_safe_string(b.to_string())
}
})
.collect(),
)
}
}

View File

@ -1,131 +0,0 @@
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

@ -34,13 +34,12 @@ impl TryFrom<serde_yaml::Value> for CuddleVariable {
serde_yaml::Value::String(s) => Ok(Self::String(s)),
serde_yaml::Value::Number(num) => Ok(Self::String(num.to_string())),
serde_yaml::Value::Bool(bool) => Ok(Self::String(bool.to_string())),
serde_yaml::Value::Null => Ok(Self::Object(Box::default())),
_ => Err(anyhow::anyhow!("cannot handle type of serde value")),
}
}
}
#[derive(PartialEq, Eq, Debug, Clone, Default)]
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct CuddleVariables(pub HashMap<String, CuddleVariable>);
impl TryFrom<serde_yaml::Mapping> for CuddleVariables {
@ -69,7 +68,7 @@ impl TryFrom<serde_yaml::Value> for CuddleVariables {
fn try_from(value: serde_yaml::Value) -> Result<Self, Self::Error> {
match value {
serde_yaml::Value::Null => Ok(Self::default()),
serde_yaml::Value::Null => anyhow::bail!("cannot handle null"),
serde_yaml::Value::Bool(_) => anyhow::bail!("cannot handle bool"),
serde_yaml::Value::Number(_) => anyhow::bail!("cannot handle number"),
serde_yaml::Value::String(_) => anyhow::bail!("cannot handle string"),
@ -85,7 +84,7 @@ pub struct CuddleVars {
pub variables: CuddleVariables,
}
const PARENT_PLAN_PREFIX: &str = ".cuddle/base";
const PARENT_PLAN_PREFIX: &str = ".cuddle/plan";
const CUDDLE_FILE: &str = "cuddle.yaml";
impl CuddleVars {
@ -96,7 +95,7 @@ impl CuddleVars {
}
}
pub fn load_cuddle_file(path: &Path) -> BoxFuture<'static, anyhow::Result<CuddleVariables>> {
fn load_cuddle_file(path: &Path) -> BoxFuture<'static, anyhow::Result<CuddleVariables>> {
let path = path.to_path_buf();
async move {

View File

@ -1,172 +0,0 @@
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

@ -1,146 +0,0 @@
use std::path::Path;
use minijinja::{value::Object, Value};
use crate::Component;
use super::cuddle_vars::{load_cuddle_file, CuddleVariable, CuddleVariables};
pub struct PostgresDatabase {
variables: CuddleVariables,
}
impl PostgresDatabase {
pub async fn new(path: &Path) -> anyhow::Result<Self> {
let variables = load_cuddle_file(path).await?;
Ok(Self { variables })
}
}
impl Component for PostgresDatabase {
fn name(&self) -> String {
"cuddle/postgres".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("postgres"))
.and_then(|o| match o {
CuddleVariable::String(o) => {
if o == "true" {
Some(true)
} else {
None
}
}
_ => None,
})
{
return Some(Ok(minijinja::Value::from_object(PostgresDatabaseValues {
name: self.name(),
enabled: true,
})));
}
Some(Ok(minijinja::Value::from_object(PostgresDatabaseValues {
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("postgres"))
.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#"
{%- if environment == "dev" %}
{%- set port = 5433 %}
{%- else %}
{%- set port = 5432 %}
{%- endif %}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ vars.cuddle_postgres.file_name(vars.cuddle_vars.service) }}
namespace: {{ vars.cluster_vars.namespace }}
data:
DATABASE_TYPE: postgresql
DATABASE_HOST: {{ environment }}.postgresql.kjuulh.app
DATABASE_PORT: "{{ port }}"
DATABASE_USER: {{ vars.cuddle_vars.service | replace("-", "_") }}
DATABASE_DB: {{ vars.cuddle_vars.service | replace("-", "_") }}
"#
.into(),
)));
}
None
}
}
#[derive(Debug)]
struct PostgresDatabaseValues {
name: String,
enabled: bool,
}
impl Object for PostgresDatabaseValues {
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(vec![
"DATABASE_HOST",
"DATABASE_PORT",
"DATABASE_USER",
"DATABASE_DB",
])),
_ => None,
}
}
}

View File

@ -68,42 +68,17 @@ impl Component for VaultSecret {
return Some(Ok(minijinja::Value::from_object(vault_values)));
}
Some(Ok(minijinja::Value::from_object(VaultSecretValues {
name: self.name().replace("/", "-"),
secrets: VaultSecretsLookup {
secrets: Vec::default(),
},
})))
None
}
fn render(
&self,
_environment: &str,
value: &serde_yaml::Value,
_value: &serde_yaml::Value,
) -> Option<anyhow::Result<(String, String)>> {
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
Some(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) }}
@ -117,9 +92,8 @@ spec:
refreshAfter: 30s
type: kv-v2
"#
.into(),
))
})
.into(),
)))
}
}

View File

@ -1,4 +1,4 @@
use std::sync::Arc;
use std::rc::Rc;
pub trait Component {
fn name(&self) -> String;
@ -27,11 +27,11 @@ pub trait Component {
#[derive(Clone)]
pub struct ConcreteComponent {
inner: Arc<dyn Component + Sync + Send + 'static>,
inner: Rc<dyn Component + 'static>,
}
impl std::ops::Deref for ConcreteComponent {
type Target = Arc<dyn Component + Sync + Send + 'static>;
type Target = Rc<dyn Component + '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 + Sync + Send + 'static>(t: T) -> Self {
Self { inner: Arc::new(t) }
pub fn new<T: Component + 'static>(t: T) -> Self {
Self { inner: Rc::new(t) }
}
}
@ -54,7 +54,7 @@ impl IntoComponent for ConcreteComponent {
}
}
impl<T: Component + Sync + Send + 'static> IntoComponent for T {
impl<T: Component + 'static> IntoComponent for T {
fn into_component(self) -> ConcreteComponent {
ConcreteComponent::new(self)
}

View File

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

View File

@ -13,32 +13,12 @@ use tokio_stream::{wrappers::ReadDirStream, StreamExt};
use crate::components::{ConcreteComponent, IntoComponent};
pub async fn process() -> anyhow::Result<()> {
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()
}
process_opts(Vec::<ConcreteComponent>::new(), ProcessOpts::default()).await
}
pub struct ProcessOpts {
pub path: PathBuf,
pub output: PathBuf,
pub variables: HashMap<String, String>,
}
impl Default for ProcessOpts {
@ -49,18 +29,16 @@ 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/base";
const CUDDLE_PLAN_PATH_PREFIX: &str = ".cuddle/plan";
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()
@ -83,21 +61,10 @@ pub async fn process_opts(
let template_files = load_template_files(&path).await?;
tracing::debug!("found files: {:?}", template_files);
let _ = tokio::fs::remove_dir_all(&opts.output).await;
tokio::fs::remove_dir_all(&opts.output).await?;
tokio::fs::create_dir_all(&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?;
}
process_templates(&components, &clusters, &template_files, &opts.output).await?;
Ok(())
}
@ -217,7 +184,6 @@ 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(
@ -226,7 +192,6 @@ async fn process_templates(
environment,
template_files,
&dest.join(environment),
variables,
)
.await?;
}
@ -240,18 +205,9 @@ 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,
variables,
)
.await?;
process_template_file(components, value, environment, template_file, dest).await?;
}
for (template_file_name, template_content) in components
@ -272,7 +228,6 @@ async fn process_cluster(
&template_file_name,
&template_content,
dest,
variables,
)
.await?;
}
@ -290,7 +245,6 @@ 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
@ -306,7 +260,6 @@ async fn process_template_file(
&file_name.to_string_lossy(),
&file,
dest,
variables,
)
.await?;
@ -320,7 +273,6 @@ 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?;
@ -334,7 +286,6 @@ async fn process_render_template(
env.add_global("environment", environment);
let mut variables = HashMap::new();
for component in components {
let name = component.name();
@ -344,22 +295,12 @@ 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! {
vars => variables
})?;
let rendered = if rendered.is_empty() || rendered.ends_with("\n") {
rendered
} else {
format!("{rendered}\n")
};
dest_file.write_all(rendered.as_bytes()).await?;
Ok(())

View File

@ -1,108 +0,0 @@
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

@ -3,4 +3,4 @@
some = {
thing = "some"
}
}
}

View File

@ -1,9 +1,6 @@
use std::{cmp::Ordering, collections::HashMap, path::Path};
use std::{cmp::Ordering, path::Path};
use cuddle_clusters::{
process::{NoUploadStrategy, ProcessOpts},
ConcreteComponent, IntoComponent,
};
use cuddle_clusters::{process::ProcessOpts, ConcreteComponent, IntoComponent};
use walkdir::DirEntry;
pub(crate) async fn run_test_with_components(
@ -30,9 +27,7 @@ pub(crate) async fn run_test_with_components(
ProcessOpts {
path: test_folder.clone(),
output: actual.clone(),
variables: HashMap::default(),
},
None::<NoUploadStrategy>,
)
.await?;
@ -42,9 +37,7 @@ pub(crate) async fn run_test_with_components(
ProcessOpts {
path: test_folder,
output: expected.clone(),
variables: HashMap::default(),
},
None::<NoUploadStrategy>,
)
.await?;
}

View File

@ -1 +1 @@
env: dev
env: dev

View File

@ -1 +1 @@
env: prod
env: prod

View File

@ -1 +1 @@
some_file: 4
some_file: 4

View File

@ -1 +1 @@
service
service

View File

@ -4,10 +4,7 @@ mod can_run_for_env;
mod cuddle_vars;
use cuddle_clusters::{
catalog::{
cluster_vars::ClusterVars, crdb_database::CockroachDB, cuddle_vars::CuddleVars,
ingress::Ingress, postgres_database::PostgresDatabase, vault_secret::VaultSecret,
},
catalog::{cluster_vars::ClusterVars, cuddle_vars::CuddleVars, vault_secret::VaultSecret},
IntoComponent,
};
@ -68,8 +65,17 @@ async fn with_cuddle_vars() -> anyhow::Result<()> {
}
#[tokio::test]
async fn with_cluster_vars() -> anyhow::Result<()> {
run_test_with_components("with_cluster_vars", vec![ClusterVars::default()]).await?;
async fn with_actual_deployment() -> anyhow::Result<()> {
let current_dir = std::env::current_dir()?.join("tests/with_cuddle_vars");
run_test_with_components(
"with_actual_deployment",
vec![
CuddleVars::new(&current_dir).await?.into_component(),
ClusterVars::default().into_component(),
],
)
.await?;
Ok(())
}
@ -90,56 +96,3 @@ 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(())
}
#[tokio::test]
async fn with_postgres_databse() -> anyhow::Result<()> {
let current_dir = std::env::current_dir()?.join("tests/with_postgres_database");
run_test_with_components(
"with_postgres_database",
vec![
CuddleVars::new(&current_dir).await?.into_component(),
ClusterVars::default().into_component(),
VaultSecret::default().into_component(),
PostgresDatabase::new(&current_dir).await?.into_component(),
],
)
.await?;
Ok(())
}

View File

@ -0,0 +1,11 @@
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

@ -1,20 +1,26 @@
{%- 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:
@ -23,6 +29,9 @@ spec:
- {{ service_name }}
image: kasperhermansen/{{ service_name }}:main-1715336504
name: {{ service_name }}
envFrom:
- configMapRef:
name: {{service_name}}-config
ports:
- containerPort: 3000
name: external-http

View File

@ -0,0 +1,15 @@
vars:
service: service
some:
nested:
item: something
array:
- item: item
cuddle/clusters:
dev:
env:
something: thing
nested.item: item

View File

@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: service-config
data:
environment:
NESTED_ITEM: item
SOMETHING: thing

View File

@ -3,16 +3,20 @@ kind: Deployment
metadata:
labels:
app: service
cluster: dev
name: service
namespace: dev
spec:
replicas: 3
selector:
matchLabels:
app: service
cluster: dev
template:
metadata:
labels:
app: service
cluster: dev
spec:
containers:
- args:
@ -24,16 +28,10 @@ spec:
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
name: internal-grpc

View File

@ -1,16 +0,0 @@
vars:
cuddle/clusters:
dev:
replicas: 1
list:
- listItem: listValue
env:
vault: true
something.something: something
something:
something: "something"
vault: true
prod:
env:

View File

@ -1,9 +0,0 @@
name: dev
namespace: dev
replicas: 1
items:
- something.something
listValue
something: something

View File

@ -1,5 +0,0 @@
name: prod
namespace: prod
replicas: 3
items:

View File

@ -1,17 +0,0 @@
name: {{ vars.cluster_vars.name }}
namespace: {{ vars.cluster_vars.namespace }}
replicas: {{ vars.cluster_vars.replicas }}
items:
{%- for val in vars.cluster_vars.env %}
- {{val}}
{%- endfor %}
{%- if vars.cluster_vars.raw.list -%}
{%- for val in vars.cluster_vars.raw.list %}
{{ val.listItem }}
{%- endfor -%}
{%- endif %}
{% if vars.cluster_vars.raw.env.something %}
something: {{ vars.cluster_vars.raw.env.something.something }}
{% endif %}

View File

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

View File

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

View File

@ -1,54 +0,0 @@
{%- 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

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

View File

@ -1,121 +0,0 @@
---
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

@ -1,30 +0,0 @@
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,121 +0,0 @@
---
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

@ -1,30 +0,0 @@
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,7 +0,0 @@
vars:
service: service
database:
postgres: "true"
cuddle/clusters:
dev:

View File

@ -1,12 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: service-cuddle-postgres
namespace: dev
data:
DATABASE_TYPE: postgresql
DATABASE_HOST: dev.postgresql.kjuulh.app
DATABASE_PORT: "5433"
DATABASE_USER: service
DATABASE_DB: service

View File

@ -1,54 +0,0 @@
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_HOST
valueFrom:
secretKeyRef:
name: service-cuddle-postgres
key: DATABASE_HOST
- name: DATABASE_PORT
valueFrom:
secretKeyRef:
name: service-cuddle-postgres
key: DATABASE_PORT
- name: DATABASE_USER
valueFrom:
secretKeyRef:
name: service-cuddle-postgres
key: DATABASE_USER
- name: DATABASE_DB
valueFrom:
secretKeyRef:
name: service-cuddle-postgres
key: DATABASE_DB
ports:
- containerPort: 3000
name: external-http
- containerPort: 3001
name: internal-http
- containerPort: 3002
name: internal-grpc

View File

@ -1,56 +0,0 @@
{%- 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_postgres.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_postgres.has_values %}
{%- for env in vars.cuddle_postgres.env %}
- name: {{ env }}
valueFrom:
secretKeyRef:
name: {{ vars.cuddle_postgres.file_name(service_name) }}
key: {{ env }}
{%- endfor %}
{%- endif %}
{%- endif %}
ports:
- containerPort: 3000
name: external-http
- containerPort: 3001
name: internal-http
- containerPort: 3002
name: internal-grpc

View File

@ -36,4 +36,4 @@ spec:
- containerPort: 3001
name: internal-http
- containerPort: 3002
name: internal-grpc
name: internal-grpc

View File

@ -10,4 +10,4 @@ spec:
mount: kvv2
path: service/dev
refreshAfter: 30s
type: kv-v2
type: kv-v2