Skip to content

Commit

Permalink
Allow for multiple namespaces in a single tenant using a namespace la…
Browse files Browse the repository at this point in the history
…bel key
  • Loading branch information
sebt3 committed Nov 6, 2024
1 parent 6f09ab6 commit f0a964c
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 81 deletions.
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "kuberest"
version = "1.0.0"
version = "1.1.0"
authors = ["Sébastien Huss <[email protected]>"]
edition = "2021"
default-run = "controller"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ kubectl wait -n kuberest --for=condition=available deploy/kuberest --timeout=30s

### Tenant aware

The controller can either function per-namespace (refuse to read secrets from other namespace mostly) or behave globally. The default behaviour is to limit to current namespace, to activate, set the environement variable MULTI_TENANT to false (or tenants.enabled=false for the chart)
The controller can either function per-tenant (refuse to read secrets from namespace that doesnt share the same label mostly) or behave globally. The default behaviour is to limit to current tenant, to activate, set the environement variable MULTI_TENANT to false (or tenants.enabled=false for the chart). You can also select the common label value using the variable TENANT_LABEL (or tenants.label for the chart).

## Usage
Please see the [documentation](https://sebt3.github.io/kuberest/docs/)
4 changes: 2 additions & 2 deletions charts/kuberest/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ apiVersion: v2
name: kuberest
description: Allow to Control remote REST api endpoints from the confort of your cluster
type: application
version: "1.0.0"
appVersion: "1.0.0"
version: "1.1.0"
appVersion: "1.1.0"
2 changes: 2 additions & 0 deletions charts/kuberest/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ spec:
{{- end }}
- name: MULTI_TENANT
value: "{{ .Values.tenants.enabled }}"
- name: TENANT_LABEL
value: "{{ .Values.tenants.label }}"
{{- with .Values.env }}
{{- toYaml . | nindent 8 }}
{{- end }}
Expand Down
3 changes: 3 additions & 0 deletions charts/kuberest/templates/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ rules:
- apiGroups: [""]
resources: ["secrets", "configmaps"]
verbs: ["*"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list"]
- apiGroups: ["events.k8s.io"]
resources: ["events"]
verbs: ["create"]
Expand Down
1 change: 1 addition & 0 deletions charts/kuberest/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ logging:
# set to false if you want to use Secrets across-namespaces (unsafe in a multi-tenants cluster)
tenants:
enabled: true
label: "kuberest.solidite.fr/tenant"

env: []

Expand Down
5 changes: 5 additions & 0 deletions deploy/operator/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ rules:
- apiGroups: [""]
resources: ["secrets", "configmaps"]
verbs: ["*"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list"]
- apiGroups: ["events.k8s.io"]
resources: ["events"]
verbs: ["create"]
Expand Down Expand Up @@ -112,6 +115,8 @@ spec:
value: info,kube=debug,controller=debug
- name: MULTI_TENANT
value: "true"
- name: TENANT_LABEL
value: "kuberest.solidite.fr/tenant"
readinessProbe:
httpGet:
path: /health
Expand Down
196 changes: 124 additions & 72 deletions src/restendpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
};
use chrono::{self, DateTime, Utc};
use futures::StreamExt;
use k8s_openapi::api::core::v1::Namespace;
use kube::{
api::{Api, ListParams, Patch, PatchParams, ResourceExt},
client::Client,
Expand Down Expand Up @@ -528,6 +529,29 @@ impl RestEndPoint {
}
}

async fn get_tenant_namespaces(&self, client: Client) -> Result<Vec<String>> {
let my_ns = self.metadata.namespace.clone().unwrap();
let ns_api: Api<Namespace> = Api::all(client);
let my_ns_meta = ns_api.get_metadata(&my_ns).await.map_err(Error::KubeError)?;
let label_key =
std::env::var("TENANT_LABEL").unwrap_or_else(|_| "kuberest.solidite.fr/tenant".to_string());
let res = vec![my_ns];
if let Some(labels) = my_ns_meta.metadata.labels.clone() {
if labels.clone().keys().into_iter().any(|k| k == &label_key) {
let tenant_name = &labels[&label_key];
let mut lp = ListParams::default();
lp = lp.labels(format!("{}=={}", label_key, tenant_name).as_str());
let my_nss = ns_api.list_metadata(&lp).await.map_err(Error::KubeError)?;
return Ok(my_nss
.items
.into_iter()
.map(|n| n.metadata.name.unwrap())
.collect());
}
}
Ok(res)
}

// Reconcile (for non-finalizer related changes)
async fn reconcile(&self, ctx: Arc<Context>) -> Result<Action> {
// Before everything : is this reconcillation because of our last status update ?
Expand All @@ -550,7 +574,7 @@ impl RestEndPoint {
let recorder = ctx.diagnostics.read().await.recorder(client.clone(), self);
let ns = self.namespace().unwrap();
let name = self.name_any();
let restendpoints: Api<RestEndPoint> = Api::namespaced(client, &ns);
let restendpoints: Api<RestEndPoint> = Api::namespaced(client.clone(), &ns);
let mut conditions: Vec<ApplicationCondition> = Vec::new();
let mut values = serde_json::json!({"input":{},"pre":{},"read":{},"write":{},"post":{}});
let mut hbs = HandleBars::new();
Expand Down Expand Up @@ -582,26 +606,37 @@ impl RestEndPoint {
}
let mut rhai = Script::new();
rhai.ctx.set_or_push("hbs", hbs.clone());
let use_multi = std::env::var("MULTI_TENANT")
.unwrap_or_else(|_| "true".to_string())
.to_lowercase()
== "true".to_string();
let allowed = if use_multi && (self.spec.inputs.is_some() || self.spec.outputs.is_some()) {
self.get_tenant_namespaces(client.clone()).await?
} else {
Vec::new()
};

// Read the inputs first
if let Some(inputs) = self.spec.clone().inputs {
for input in inputs {
if let Some(secret) = input.secret_ref {
let my_ns = if std::env::var("MULTI_TENANT")
.unwrap_or_else(|_| "true".to_string())
.to_lowercase()
== "true".to_string()
{
if secret.namespace.is_some() {
recorder.publish(Event {
type_: EventType::Warning,
reason: "IgnoredNamespace".into(),
note: Some(format!("Ignoring namespace from Input '{}' as the operator run in multi-tenant mode",input.name)),
action: "readingInput".into(),
secondary: None,
}).await.map_err(Error::KubeError)?;
let my_ns = if use_multi {
if let Some(o_ns) = secret.namespace.clone() {
if allowed.contains(&o_ns) {
o_ns
} else {
recorder.publish(Event {
type_: EventType::Warning,
reason: "IgnoredNamespace".into(),
note: Some(format!("Ignoring namespace from Input '{}' as the operator run in multi-tenant mode",input.name)),
action: "readingInput".into(),
secondary: None,
}).await.map_err(Error::KubeError)?;
ns.clone()
}
} else {
ns.clone()
}
ns.clone()
} else if let Some(local_ns) = secret.namespace {
local_ns
} else {
Expand Down Expand Up @@ -650,21 +685,23 @@ impl RestEndPoint {
)));
}
} else if let Some(cfgmap) = input.config_map_ref {
let my_ns = if std::env::var("MULTI_TENANT")
.unwrap_or_else(|_| "true".to_string())
.to_lowercase()
== "true".to_string()
{
if cfgmap.namespace.is_some() {
recorder.publish(Event {
type_: EventType::Warning,
reason: "IgnoredNamespace".into(),
note: Some(format!("Ignoring namespace from Input '{}' as the operator run in multi-tenant mode",input.name)),
action: "readingInput".into(),
secondary: None,
}).await.map_err(Error::KubeError)?;
let my_ns = if use_multi {
if let Some(o_ns) = cfgmap.namespace.clone() {
if allowed.contains(&o_ns) {
o_ns
} else {
recorder.publish(Event {
type_: EventType::Warning,
reason: "IgnoredNamespace".into(),
note: Some(format!("Ignoring namespace from Input '{}' as the operator run in multi-tenant mode",input.name)),
action: "readingInput".into(),
secondary: None,
}).await.map_err(Error::KubeError)?;
ns.clone()
}
} else {
ns.clone()
}
ns.clone()
} else if let Some(local_ns) = cfgmap.namespace {
local_ns
} else {
Expand Down Expand Up @@ -1328,21 +1365,23 @@ impl RestEndPoint {
let mut owned_new: Vec<OwnedObjects> = Vec::new();
if let Some(outputs) = self.spec.outputs.clone() {
for output in outputs {
let my_ns = if std::env::var("MULTI_TENANT")
.unwrap_or_else(|_| "true".to_string())
.to_lowercase()
== "true".to_string()
{
if output.metadata.namespace.is_some() {
recorder.publish(Event {
type_: EventType::Warning,
reason: "IgnoredNamespace".into(),
note: Some(format!("Ignoring namespace from Output '{}' as the operator run in multi-tenant mode",output.metadata.name)),
action: "readingInput".into(),
secondary: None,
}).await.map_err(Error::KubeError)?;
let my_ns = if use_multi {
if let Some(o_ns) = output.metadata.namespace.clone() {
if allowed.contains(&o_ns) {
o_ns
} else {
recorder.publish(Event {
type_: EventType::Warning,
reason: "IgnoredNamespace".into(),
note: Some(format!("Ignoring namespace from Input '{}' as the operator run in multi-tenant mode",output.metadata.name)),
action: "readingInput".into(),
secondary: None,
}).await.map_err(Error::KubeError)?;
ns.clone()
}
} else {
ns.clone()
}
ns.clone()
} else if let Some(local_ns) = output.clone().metadata.namespace {
local_ns
} else {
Expand Down Expand Up @@ -1703,7 +1742,7 @@ impl RestEndPoint {
let recorder = Recorder::new(client.clone(), reporter, self.object_ref(&()));
let ns = self.namespace().unwrap();
let name = self.name_any();
let restendpoints: Api<RestEndPoint> = Api::namespaced(client, &ns);
let restendpoints: Api<RestEndPoint> = Api::namespaced(client.clone(), &ns);
let mut conditions: Vec<ApplicationCondition> = Vec::new();
let mut values = serde_json::json!({"input":{},"pre":{}});
let mut hbs = HandleBars::new();
Expand Down Expand Up @@ -1733,27 +1772,38 @@ impl RestEndPoint {
});
}
}
let use_multi = std::env::var("MULTI_TENANT")
.unwrap_or_else(|_| "true".to_string())
.to_lowercase()
== "true".to_string();
let allowed = if use_multi && (self.spec.inputs.is_some() || self.spec.outputs.is_some()) {
self.get_tenant_namespaces(client.clone()).await?
} else {
Vec::new()
};
let mut rhai = Script::new();
rhai.ctx.set_or_push("hbs", hbs.clone());
// Read the inputs first
if let Some(inputs) = self.spec.clone().inputs {
for input in inputs {
if let Some(secret) = input.secret_ref {
let my_ns = if std::env::var("MULTI_TENANT")
.unwrap_or_else(|_| "true".to_string())
.to_lowercase()
== "true".to_string()
{
if secret.namespace.is_some() {
recorder.publish(Event {
type_: EventType::Warning,
reason: "IgnoredNamespace".into(),
note: Some(format!("Ignoring namespace from Input '{}' as the operator run in multi-tenant mode",input.name)),
action: "readingInput".into(),
secondary: None,
}).await.map_err(Error::KubeError)?;
let my_ns = if use_multi {
if let Some(o_ns) = secret.namespace.clone() {
if allowed.contains(&o_ns) {
o_ns
} else {
recorder.publish(Event {
type_: EventType::Warning,
reason: "IgnoredNamespace".into(),
note: Some(format!("Ignoring namespace from Input '{}' as the operator run in multi-tenant mode",input.name)),
action: "readingInput".into(),
secondary: None,
}).await.map_err(Error::KubeError)?;
ns.clone()
}
} else {
ns.clone()
}
ns.clone()
} else if let Some(local_ns) = secret.namespace {
local_ns
} else {
Expand Down Expand Up @@ -1802,21 +1852,23 @@ impl RestEndPoint {
)));
}
} else if let Some(cfgmap) = input.config_map_ref {
let my_ns = if std::env::var("MULTI_TENANT")
.unwrap_or_else(|_| "true".to_string())
.to_lowercase()
== "true".to_string()
{
if cfgmap.namespace.is_some() {
recorder.publish(Event {
type_: EventType::Warning,
reason: "IgnoredNamespace".into(),
note: Some(format!("Ignoring namespace from Input '{}' as the operator run in multi-tenant mode",input.name)),
action: "readingInput".into(),
secondary: None,
}).await.map_err(Error::KubeError)?;
let my_ns = if use_multi {
if let Some(o_ns) = cfgmap.namespace.clone() {
if allowed.contains(&o_ns) {
o_ns
} else {
recorder.publish(Event {
type_: EventType::Warning,
reason: "IgnoredNamespace".into(),
note: Some(format!("Ignoring namespace from Input '{}' as the operator run in multi-tenant mode",input.name)),
action: "readingInput".into(),
secondary: None,
}).await.map_err(Error::KubeError)?;
ns.clone()
}
} else {
ns.clone()
}
ns.clone()
} else if let Some(local_ns) = cfgmap.namespace {
local_ns
} else {
Expand Down

0 comments on commit f0a964c

Please sign in to comment.