6 Commits

Author SHA1 Message Date
3c29377013 Trigger reconcile webhook once build is done
All checks were successful
Build and deploy / build (push) Successful in 7m18s
2025-04-23 21:00:02 +02:00
07123a92a9 Use reusable workflow
All checks were successful
Build and deploy / build (push) Successful in 5m30s
2025-04-23 14:08:42 +02:00
ac15ce1d38 Clarified password env variable missing error
All checks were successful
kustomization/siranga/3850ce12 reconciliation succeeded
Build and deploy / Build container and manifests (push) Successful in 5m49s
2025-04-22 10:47:18 +02:00
5a7652f3a4 Make ldap search filter configurable
All checks were successful
Build and deploy / Build container and manifests (push) Successful in 6m8s
kustomization/siranga/3850ce12 reconciliation succeeded
2025-04-22 00:42:56 +02:00
95ad229077 Get bind_dn from secret 2025-04-22 00:42:33 +02:00
e9673211c1 Added liveness probe
All checks were successful
Build and deploy / Build container and manifests (push) Successful in 5m57s
2025-04-21 03:22:34 +02:00
6 changed files with 80 additions and 90 deletions

View File

@@ -7,83 +7,9 @@ on:
tags: tags:
- v*.*.* - v*.*.*
env:
OCI_REPO: git.huizinga.dev/dreaded_x/${{ gitea.event.repository.name}}
jobs: jobs:
build: build:
name: Build container and manifests uses: dreaded_x/workflows/.gitea/workflows/rust-kubernetes.yaml@66ab50c3ac239dbdd1e42e6276ec2e65b6a79379
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set timestamp and release version
run: |
echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV
git fetch --prune --unshallow --tags --force
echo "RELEASE_VERSION=$(git describe --always --dirty='--modified')" >> $GITHUB_ENV
cat $GITHUB_ENV
- name: Login to registry
uses: docker/login-action@v3
with: with:
registry: git.huizinga.dev webhook_url: ${{ secrets.WEBHOOK_URL }}
username: ${{ gitea.actor }} secrets: inherit
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Install kustomize
run: |
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
- name: Setup Flux CLI
uses: https://github.com/fluxcd/flux2/action@main
with:
version: v2.5.0
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.OCI_REPO }}
tags: |
type=edge
type=ref,event=branch
type=semver,pattern=v{{version}}
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{major}}
- name: Build container
id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
sbom: true
provenance: mode=max
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
"RELEASE_VERSION=${{ env.RELEASE_VERSION }}"
env:
SOURCE_DATE_EPOCH: ${{ env.TIMESTAMP }}
- name: Kustomize manifests
run: |
./kustomize build ./manifests | sed "s/\${DIGEST}/${{ steps.build.outputs.digest }}/" > ./manifests.yaml
- name: Push manifests
run: |
flux push artifact oci://${{ env.OCI_REPO }}/manifests:${{ gitea.head_ref || gitea.ref_name }} \
--path="./manifests.yaml" \
--source="$(git config --get remote.origin.url)" \
--revision="$(git rev-parse HEAD)" \
$(echo "${{ steps.meta.outputs.labels }}" | sed -e 's/^/-a /')
flux tag artifact oci://${{ env.OCI_REPO }}/manifests:${{ gitea.head_ref || gitea.ref_name }} \
$(echo "${{ steps.meta.outputs.tags }}" | sed -e 's/^.*:/--tag /')

34
Cargo.lock generated
View File

@@ -1684,6 +1684,16 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "leon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42a865ffec5587961f5afc6d365bccb304f4feaa1928f4fe94c91c9d210d7310"
dependencies = [
"miette",
"thiserror 2.0.12",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.172" version = "0.2.172"
@@ -1772,6 +1782,29 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "miette"
version = "7.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484"
dependencies = [
"cfg-if",
"miette-derive",
"thiserror 1.0.69",
"unicode-width 0.1.14",
]
[[package]]
name = "miette-derive"
version = "7.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@@ -2945,6 +2978,7 @@ dependencies = [
"hyper", "hyper",
"hyper-util", "hyper-util",
"ldap3", "ldap3",
"leon",
"pin-project-lite", "pin-project-lite",
"rand 0.8.5", "rand 0.8.5",
"ratatui", "ratatui",

View File

@@ -18,6 +18,7 @@ http-body-util = { version = "0.1.3", features = ["full"] }
hyper = { version = "1.6.0", features = ["full"] } hyper = { version = "1.6.0", features = ["full"] }
hyper-util = { version = "0.1.11", features = ["full"] } hyper-util = { version = "0.1.11", features = ["full"] }
ldap3 = "0.11.5" ldap3 = "0.11.5"
leon = "3.0.2"
pin-project-lite = "0.2.16" pin-project-lite = "0.2.16"
rand = "0.8.5" rand = "0.8.5"
ratatui = { version = "0.29.0", features = ["unstable-backend-writer"] } ratatui = { version = "0.29.0", features = ["unstable-backend-writer"] }

View File

@@ -55,15 +55,20 @@ spec:
value: ldap://lldap.lldap.svc.cluster.local:3890 value: ldap://lldap.lldap.svc.cluster.local:3890
- name: LDAP_BASE - name: LDAP_BASE
value: ou=people,dc=huizinga,dc=dev value: ou=people,dc=huizinga,dc=dev
- name: LDAP_SEARCH_FILTER
value: (uid={username})
- name: LDAP_BIND_DN - name: LDAP_BIND_DN
value: uid=siranga.siranga,ou=people,dc=huizinga,dc=dev valueFrom:
secretKeyRef:
name: siranga-lldap-credentials
key: bind_dn
- name: LDAP_PASSWORD_FILE - name: LDAP_PASSWORD_FILE
value: /secrets/credentials/password value: /secrets/credentials/password
- name: PRIVATE_KEY_FILE - name: PRIVATE_KEY_FILE
value: /secrets/key/private.pem value: /secrets/key/private.pem
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /healthz path: /health
port: metrics port: metrics
initialDelaySeconds: 3 initialDelaySeconds: 3
periodSeconds: 3 periodSeconds: 3

View File

@@ -1,4 +1,5 @@
use ldap3::{LdapConnAsync, SearchEntry}; use ldap3::{LdapConnAsync, SearchEntry};
use leon::{Template, vals};
use russh::keys::PublicKey; use russh::keys::PublicKey;
use tokio::select; use tokio::select;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
@@ -9,6 +10,7 @@ use tracing::{debug, error};
pub struct Ldap { pub struct Ldap {
base: String, base: String,
ldap: ldap3::Ldap, ldap: ldap3::Ldap,
search_filter: String,
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@@ -21,6 +23,10 @@ pub enum LdapError {
MissingEnvironmentVariable(&'static str), MissingEnvironmentVariable(&'static str),
#[error("Could not read password file: {0}")] #[error("Could not read password file: {0}")]
CouldNotReadPasswordFile(#[from] std::io::Error), CouldNotReadPasswordFile(#[from] std::io::Error),
#[error("Failed to parse search filter: {0}")]
FailedToParseSearchFilter(#[from] leon::ParseError),
#[error("Failed to render search filter: {0}")]
FailedToRenderSearchFilter(#[from] leon::RenderError),
} }
impl Ldap { impl Ldap {
@@ -33,11 +39,14 @@ impl Ldap {
.map_err(|_| LdapError::MissingEnvironmentVariable("LDAP_BASE"))?; .map_err(|_| LdapError::MissingEnvironmentVariable("LDAP_BASE"))?;
let bind_dn = std::env::var("LDAP_BIND_DN") let bind_dn = std::env::var("LDAP_BIND_DN")
.map_err(|_| LdapError::MissingEnvironmentVariable("LDAP_BIND_DN"))?; .map_err(|_| LdapError::MissingEnvironmentVariable("LDAP_BIND_DN"))?;
let search_filter = std::env::var("LDAP_SEARCH_FILTER")
.map_err(|_| LdapError::MissingEnvironmentVariable("LDAP_SEARCH_FILTER"))?;
let password = std::env::var("LDAP_PASSWORD_FILE").map_or_else( let password = std::env::var("LDAP_PASSWORD_FILE").map_or_else(
|_| { |_| {
std::env::var("LDAP_PASSWORD") std::env::var("LDAP_PASSWORD").map_err(|_| {
.map_err(|_| LdapError::MissingEnvironmentVariable("LDAP_PASSWORD")) LdapError::MissingEnvironmentVariable("LDAP_PASSWORD or LDAP_PASSWORD_FILE")
})
}, },
|path| { |path| {
std::fs::read_to_string(path) std::fs::read_to_string(path)
@@ -65,20 +74,39 @@ impl Ldap {
ldap.simple_bind(&bind_dn, &password).await?.success()?; ldap.simple_bind(&bind_dn, &password).await?.success()?;
Ok((Self { base, ldap }, handle)) Ok((
Self {
base,
ldap,
search_filter,
},
handle,
))
} }
pub async fn get_ssh_keys( pub async fn get_ssh_keys(
&mut self, &mut self,
user: impl AsRef<str>, user: impl AsRef<str>,
) -> Result<Vec<PublicKey>, LdapError> { ) -> Result<Vec<PublicKey>, LdapError> {
let search_filter = Template::parse(&self.search_filter)?;
let search_filter = search_filter.render(&&vals(|key| {
if key == "username" {
Some(user.as_ref().to_string().into())
} else {
None
}
}))?;
debug!("search_filter = {search_filter}");
Ok(self Ok(self
.ldap .ldap
.search( .search(
&self.base, &self.base,
ldap3::Scope::Subtree, ldap3::Scope::Subtree,
// TODO: Make this not hardcoded // TODO: Make this not hardcoded
&format!("(uid={})", user.as_ref()), &search_filter,
vec!["sshkeys"], vec!["sshkeys"],
) )
.await? .await?

View File

@@ -4,8 +4,8 @@ use std::net::SocketAddr;
use std::path::Path; use std::path::Path;
use std::time::Duration; use std::time::Duration;
use axum::Router;
use axum::routing::get; use axum::routing::get;
use axum::{Json, Router};
use color_eyre::eyre::Context; use color_eyre::eyre::Context;
use dotenvy::dotenv; use dotenvy::dotenv;
use rand::rngs::OsRng; use rand::rngs::OsRng;
@@ -126,7 +126,7 @@ async fn main() -> color_eyre::Result<()> {
let http_task = service.serve(http_listener, token.clone()); let http_task = service.serve(http_listener, token.clone());
info!("HTTP is available on {http_addr}"); info!("HTTP is available on {http_addr}");
let metrics_app = Router::new().route("/health", get(async || "UP")); let metrics_app = Router::new().route("/health", get(async || Json("healthy")));
let metrics_addr = SocketAddr::from(([0, 0, 0, 0], metrics_port)); let metrics_addr = SocketAddr::from(([0, 0, 0, 0], metrics_port));
let metrics_listener = TcpListener::bind(metrics_addr).await?; let metrics_listener = TcpListener::bind(metrics_addr).await?;
let metrics = axum::serve(metrics_listener, metrics_app) let metrics = axum::serve(metrics_listener, metrics_app)
@@ -140,10 +140,6 @@ async fn main() -> color_eyre::Result<()> {
_ = shutdown_task(token.clone()) => { _ = shutdown_task(token.clone()) => {
error!("Failed to shutdown gracefully"); error!("Failed to shutdown gracefully");
} }
_ = tokio::time::sleep(std::time::Duration::from_secs(30)) => {
debug!("Simulating issue");
std::future::pending::<()>().await;
}
}; };
Ok(()) Ok(())