Compare commits

..

3 Commits

Author SHA1 Message Date
379ae5e80f Use compact formatting for tracing if running through cargo
Some checks failed
Build and deploy / Build container and manifests (push) Has been cancelled
2025-03-22 04:54:39 +01:00
77331c5080 Added demo yaml 2025-03-22 04:48:49 +01:00
197b718b85 Add Group controller (#3) 2025-03-22 04:43:57 +01:00
27 changed files with 945 additions and 1529 deletions

View File

@@ -1,2 +1,2 @@
[advisories] [advisories]
ignore = ["RUSTSEC-2024-0344", "RUSTSEC-2025-0023"] ignore = ["RUSTSEC-2024-0344"]

View File

@@ -2,4 +2,3 @@
!queries !queries
!src !src
!Cargo.* !Cargo.*
!.cargo/config.toml

View File

@@ -7,9 +7,89 @@ on:
tags: tags:
- v*.*.* - v*.*.*
env:
OCI_REPO: git.huizinga.dev/dreaded_x/${{ gitea.event.repository.name}}
jobs: jobs:
build: build:
uses: dreaded_x/workflows/.gitea/workflows/docker-kubernetes.yaml@ef78704b98c72e4a6b8340f9bff7b085a7bdd95c name: Build container and manifests
secrets: inherit runs-on: ubuntu-latest
with: steps:
webhook_url: ${{ secrets.WEBHOOK_URL }} - name: Checkout
uses: actions/checkout@v4
- name: Get Git commit timestamps
run: echo "TIMESTAMP=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV
- name: Login to registry
uses: docker/login-action@v3
with:
registry: git.huizinga.dev
username: ${{ gitea.actor }}
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 and export to docker
id: build
uses: docker/build-push-action@v6
with:
context: .
load: true
annotations: ${{ steps.meta.outputs.annotations }}
env:
SOURCE_DATE_EPOCH: ${{ env.TIMESTAMP }}
- name: Generate CRDs
run: |
docker run --rm ${{ steps.build.outputs.imageid }} /crdgen > ./manifests/crds.yaml
- name: Push container
uses: docker/build-push-action@v6
id: push
with:
context: .
push: true
sbom: true
provenance: mode=max
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
env:
SOURCE_DATE_EPOCH: ${{ env.TIMESTAMP }}
- name: Kustomize manifests
run: |
./kustomize build ./manifests | sed "s/\${DIGEST}/${{ steps.push.outputs.digest }}/" > ./manifests.yaml
- name: Push manifests
run: |
flux push artifact oci://$OCI_REPO/manifests:latest \
--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://$OCI_REPO/manifests:latest \
$(echo "${{ steps.meta.outputs.tags }}" | sed -e 's/^.*:/--tag /')

View File

@@ -2,19 +2,17 @@ fail_fast: true
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0 rev: v4.6.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
- id: check-yaml - id: check-yaml
args:
- --allow-multiple-documents
- id: check-toml - id: check-toml
- id: check-added-large-files - id: check-added-large-files
- id: check-merge-conflict - id: check-merge-conflict
- repo: https://github.com/crate-ci/typos - repo: https://github.com/crate-ci/typos
rev: typos-dict-v0.13.13 rev: v1.21.0
hooks: hooks:
- id: typos - id: typos
args: ["--force-exclude"] args: ["--force-exclude"]
@@ -45,17 +43,6 @@ repos:
name: audit name: audit
description: Audit packages description: Audit packages
entry: cargo audit entry: cargo audit
args: ["--deny", "warnings"]
language: system
pass_filenames: false
verbose: true
always_run: true
- id: udeps
name: unused
description: Check for unused crates
entry: cargo +nightly udeps
args: ["--workspace"]
language: system language: system
types: [file] types: [file]
files: (\.rs|Cargo.lock)$ files: (\.rs|Cargo.lock)$
@@ -72,6 +59,6 @@ repos:
pass_filenames: false pass_filenames: false
- repo: https://github.com/hadolint/hadolint - repo: https://github.com/hadolint/hadolint
rev: v2.14.0 rev: v2.12.0
hooks: hooks:
- id: hadolint - id: hadolint

1577
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,38 +8,32 @@ default-run = "lldap-controller"
members = ["queries"] members = ["queries"]
[workspace.dependencies] [workspace.dependencies]
cynic = "3.12.0" cynic = "3.10.0"
cynic-codegen = "3.12.0" insta = { version = "1.42.2", features = ["yaml"] }
insta = { version = "1.45.0", features = ["yaml"] }
[dependencies] [dependencies]
queries = { path = "./queries" } queries = { path = "./queries" }
anyhow = "1.0.97"
lldap_auth = { git = "https://github.com/lldap/lldap" } lldap_auth = { git = "https://github.com/lldap/lldap" }
# Purposefully kept at 0.8.x for compatibility with lldap
rand = { version = "0.8.5" } rand = { version = "0.8.5" }
serde_json = "1.0.145" serde_json = "1.0.140"
cynic = { workspace = true, features = ["http-reqwest"] } cynic = { workspace = true, features = ["http-reqwest"] }
tokio = { version = "1.48.0", features = ["full"] } tokio = { version = "1.44.0", features = ["full"] }
kube = { version = "2.0.1", features = ["derive", "runtime"] } kube = { version = "0.99.0", features = ["derive", "runtime"] }
k8s-openapi = { version = "0.26.1", features = ["v1_34"] } k8s-openapi = { version = "0.24.0", features = ["v1_31"] }
schemars = { version = "1.1.0", features = ["chrono04"] } schemars = { version = "0.8.22", features = ["chrono"] }
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_yaml = "0.9.34" serde_yaml = "0.9.34"
futures = "0.3.31" futures = "0.3.31"
tracing-subscriber = { version = "0.3.22", features = ["json", "env-filter"] } tracing-subscriber = { version = "0.3.19", features = ["json", "env-filter"] }
tracing = "0.1.44" tracing = "0.1.41"
thiserror = "2.0.17" thiserror = "2.0.12"
chrono = "0.4.42" chrono = "0.4.40"
passwords = "3.1.16" passwords = "3.1.16"
reqwest = { version = "0.12.26", default-features = false, features = [ reqwest = { version = "0.12.14", default-features = false, features = [
"json", "json",
"rustls-tls", "rustls-tls",
] } ] }
git-version = "0.3.9"
color-eyre = "0.6.5"
dotenvy = "0.15.7"
leon = "3.0.2"
async-trait = "0.1.89"
[dev-dependencies] [dev-dependencies]
insta = { workspace = true } insta = { workspace = true }

View File

@@ -1,4 +1,4 @@
FROM rust:1.92 AS base FROM rust:1.85 AS base
ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
RUN cargo install cargo-chef --locked --version 0.1.71 && \ RUN cargo install cargo-chef --locked --version 0.1.71 && \
cargo install cargo-auditable --locked --version 0.6.6 cargo install cargo-auditable --locked --version 0.6.6
@@ -13,13 +13,9 @@ COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json RUN cargo chef cook --release --recipe-path recipe.json
COPY . . COPY . .
ARG RELEASE_VERSION RUN cargo auditable build --release
ENV RELEASE_VERSION=${RELEASE_VERSION}
RUN cargo auditable build --release && /app/target/release/crdgen > /crds.yaml
FROM scratch AS manifests
COPY --from=builder /crds.yaml /
FROM gcr.io/distroless/cc-debian12:nonroot AS runtime FROM gcr.io/distroless/cc-debian12:nonroot AS runtime
COPY --from=builder /app/target/release/lldap-controller /lldap-controller COPY --from=builder /app/target/release/lldap-controller /lldap-controller
COPY --from=builder /app/target/release/crdgen /crdgen
CMD ["/lldap-controller"] CMD ["/lldap-controller"]

View File

@@ -1,23 +0,0 @@
variable "TAG_BASE" {}
variable "RELEASE_VERSION" {}
group "default" {
targets = ["lldap-controller", "manifests"]
}
target "docker-metadata-action" {}
target "lldap-controller" {
inherits = ["docker-metadata-action"]
context = "./"
dockerfile = "Dockerfile"
tags = [for tag in target.docker-metadata-action.tags : "${TAG_BASE}:${tag}"]
target = "runtime"
}
target "manifests" {
context = "./"
dockerfile = "Dockerfile"
target = "manifests"
output = [{ type = "cacheonly" }, "manifests"]
}

View File

@@ -9,12 +9,6 @@ rules:
- serviceusers - serviceusers
- serviceusers/status - serviceusers/status
- serviceusers/finalizers - serviceusers/finalizers
- groups
- grours/status
- grours/finalizers
- userattributes
- userattributes/status
- userattributes/finalizers
verbs: verbs:
- "*" - "*"
- apiGroups: - apiGroups:

View File

@@ -31,10 +31,6 @@ spec:
requests: requests:
cpu: 50m cpu: 50m
memory: 100Mi memory: 100Mi
volumeMounts:
- name: credentials
readOnly: true
mountPath: "/secrets/credentials"
env: env:
- name: RUST_LOG - name: RUST_LOG
value: info,lldap_controller=debug value: info,lldap_controller=debug
@@ -42,11 +38,8 @@ spec:
value: "http://lldap:17170" value: "http://lldap:17170"
- name: LLDAP_USERNAME - name: LLDAP_USERNAME
value: admin value: admin
- name: LLDAP_PASSWORD_FILE - name: LLDAP_PASSWORD
value: /secrets/credentials/lldap-ldap-user-pass valueFrom:
- name: LLDAP_BIND_DN secretKeyRef:
value: uid={username},ou=people,dc=huizinga,dc=dev name: lldap-credentials
volumes: key: lldap-ldap-user-pass
- name: credentials
secret:
secretName: lldap-credentials

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "queries" name = "queries"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
[dependencies] [dependencies]
cynic = { workspace = true } cynic = { workspace = true }
@@ -10,4 +10,4 @@ cynic = { workspace = true }
insta = { workspace = true } insta = { workspace = true }
[build-dependencies] [build-dependencies]
cynic-codegen = { workspace = true } cynic-codegen = "3.10.0"

View File

@@ -110,68 +110,6 @@ pub struct DeleteGroup {
pub delete_group: Success, pub delete_group: Success,
} }
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query")]
pub struct GetUserAttributes {
pub schema: Schema,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct Schema {
pub user_schema: AttributeList,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct AttributeList {
pub attributes: Vec<AttributeSchema>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct AttributeSchema {
pub name: String,
pub is_visible: bool,
pub is_list: bool,
pub is_editable: bool,
pub attribute_type: AttributeType,
}
#[derive(cynic::Enum, Clone, Copy, Debug, PartialEq, Eq)]
pub enum AttributeType {
String,
Integer,
JpegPhoto,
DateTime,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct CreateUserAttributeVariables<'a> {
pub editable: bool,
pub list: bool,
pub name: &'a str,
#[cynic(rename = "type")]
pub r#type: AttributeType,
pub visible: bool,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "CreateUserAttributeVariables")]
pub struct CreateUserAttribute {
#[arguments(attributeType: $r#type, isEditable: $editable, isList: $list, isVisible: $visible, name: $name)]
pub add_user_attribute: Success,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct DeleteUserAttributeVariables<'a> {
pub name: &'a str,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "DeleteUserAttributeVariables")]
pub struct DeleteUserAttribute {
#[arguments(name: $name)]
pub delete_user_attribute: Success,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use cynic::{MutationBuilder, QueryBuilder}; use cynic::{MutationBuilder, QueryBuilder};
@@ -239,31 +177,4 @@ mod tests {
insta::assert_snapshot!(operation.query); insta::assert_snapshot!(operation.query);
} }
#[test]
fn get_user_attributes_gql_output() {
let operation = GetUserAttributes::build(());
insta::assert_snapshot!(operation.query);
}
#[test]
fn create_user_attribute_gql_output() {
let operation = CreateUserAttribute::build(CreateUserAttributeVariables {
r#type: AttributeType::String,
list: true,
editable: true,
visible: true,
name: "attr",
});
insta::assert_snapshot!(operation.query);
}
#[test]
fn delete_user_attribute_gql_output() {
let operation = DeleteUserAttribute::build(DeleteUserAttributeVariables { name: "attr" });
insta::assert_snapshot!(operation.query);
}
} }

View File

@@ -1,9 +0,0 @@
---
source: queries/src/lib.rs
expression: operation.query
---
mutation CreateUserAttribute($editable: Boolean!, $list: Boolean!, $name: String!, $type: AttributeType!, $visible: Boolean!) {
addUserAttribute(attributeType: $type, isEditable: $editable, isList: $list, isVisible: $visible, name: $name) {
ok
}
}

View File

@@ -1,9 +0,0 @@
---
source: queries/src/lib.rs
expression: operation.query
---
mutation DeleteUserAttribute($name: String!) {
deleteUserAttribute(name: $name) {
ok
}
}

View File

@@ -1,17 +0,0 @@
---
source: queries/src/lib.rs
expression: operation.query
---
query GetUserAttributes {
schema {
userSchema {
attributes {
name
isVisible
isList
isEditable
attributeType
}
}
}
}

View File

@@ -1,4 +1,4 @@
[toolchain] [toolchain]
channel = "1.92" channel = "1.85"
profile = "default" profile = "default"
components = ["rust-analyzer"] components = ["rust-analyzer"]

View File

@@ -1,11 +1,9 @@
use kube::CustomResourceExt; use kube::CustomResourceExt;
fn main() { fn main() {
let resources = [ print!(
"{}---\n{}",
serde_yaml::to_string(&lldap_controller::resources::ServiceUser::crd()).unwrap(), serde_yaml::to_string(&lldap_controller::resources::ServiceUser::crd()).unwrap(),
serde_yaml::to_string(&lldap_controller::resources::Group::crd()).unwrap(), serde_yaml::to_string(&lldap_controller::resources::Group::crd()).unwrap()
serde_yaml::to_string(&lldap_controller::resources::UserAttribute::crd()).unwrap(), )
]
.join("---\n");
print!("{resources}")
} }

View File

@@ -10,16 +10,10 @@ pub struct Context {
pub lldap_config: LldapConfig, pub lldap_config: LldapConfig,
pub controller_name: String, pub controller_name: String,
pub recorder: Recorder, pub recorder: Recorder,
pub bind_dn_template: String,
} }
impl Context { impl Context {
pub fn new( pub fn new(controller_name: &str, client: kube::Client, lldap_config: LldapConfig) -> Self {
controller_name: &str,
client: kube::Client,
lldap_config: LldapConfig,
bind_dn_template: impl Into<String>,
) -> Self {
let reporter: Reporter = controller_name.into(); let reporter: Reporter = controller_name.into();
let recorder = Recorder::new(client.clone(), reporter); let recorder = Recorder::new(client.clone(), reporter);
@@ -28,7 +22,6 @@ impl Context {
lldap_config, lldap_config,
controller_name: controller_name.into(), controller_name: controller_name.into(),
recorder, recorder,
bind_dn_template: bind_dn_template.into(),
} }
} }
} }
@@ -41,31 +34,23 @@ pub trait ControllerEvents {
where where
T: Resource<DynamicType = ()> + Sync; T: Resource<DynamicType = ()> + Sync;
async fn user_created<T>(&self, obj: &T) -> Result<(), Self::Error> async fn user_created<T>(&self, obj: &T, username: &str) -> Result<(), Self::Error>
where where
T: Resource<DynamicType = ()> + Sync; T: Resource<DynamicType = ()> + Sync;
async fn group_created<T>(&self, obj: &T) -> Result<(), Self::Error> async fn group_created<T>(&self, obj: &T, name: &str) -> Result<(), Self::Error>
where where
T: Resource<DynamicType = ()> + Sync; T: Resource<DynamicType = ()> + Sync;
async fn user_deleted<T>(&self, obj: &T) -> Result<(), Self::Error> async fn user_deleted<T>(&self, obj: &T, username: &str) -> Result<(), Self::Error>
where where
T: Resource<DynamicType = ()> + Sync; T: Resource<DynamicType = ()> + Sync;
async fn group_deleted<T>(&self, obj: &T) -> Result<(), Self::Error> async fn group_deleted<T>(&self, obj: &T, name: &str) -> Result<(), Self::Error>
where where
T: Resource<DynamicType = ()> + Sync; T: Resource<DynamicType = ()> + Sync;
async fn user_attribute_created<T>(&self, obj: &T) -> Result<(), Self::Error> async fn user_not_found<T>(&self, obj: &T, username: &str) -> Result<(), Self::Error>
where
T: Resource<DynamicType = ()> + Sync;
async fn user_attribute_desync<T>(&self, obj: &T, fields: &[String]) -> Result<(), Self::Error>
where
T: Resource<DynamicType = ()> + Sync;
async fn user_attribute_deleted<T>(&self, obj: &T) -> Result<(), Self::Error>
where where
T: Resource<DynamicType = ()> + Sync; T: Resource<DynamicType = ()> + Sync;
} }
@@ -90,16 +75,16 @@ impl ControllerEvents for Recorder {
.await .await
} }
async fn user_created<T>(&self, obj: &T) -> Result<(), Self::Error> async fn user_created<T>(&self, obj: &T, username: &str) -> Result<(), Self::Error>
where where
T: Resource<DynamicType = ()> + Sync, T: Resource<DynamicType = ()> + Sync,
{ {
self.publish( self.publish(
&Event { &Event {
type_: EventType::Normal, type_: EventType::Normal,
reason: "Created".into(), reason: "UserCreated".into(),
note: Some("Created user".into()), note: Some(format!("Created user '{username}'")),
action: "Created".into(), action: "UserCreated".into(),
secondary: None, secondary: None,
}, },
&obj.object_ref(&()), &obj.object_ref(&()),
@@ -107,16 +92,16 @@ impl ControllerEvents for Recorder {
.await .await
} }
async fn group_created<T>(&self, obj: &T) -> Result<(), Self::Error> async fn group_created<T>(&self, obj: &T, name: &str) -> Result<(), Self::Error>
where where
T: Resource<DynamicType = ()> + Sync, T: Resource<DynamicType = ()> + Sync,
{ {
self.publish( self.publish(
&Event { &Event {
type_: EventType::Normal, type_: EventType::Normal,
reason: "Created".into(), reason: "GroupCreated".into(),
note: Some("Created group".into()), note: Some(format!("Created group '{name}'")),
action: "Created".into(), action: "GroupCreated".into(),
secondary: None, secondary: None,
}, },
&obj.object_ref(&()), &obj.object_ref(&()),
@@ -124,16 +109,16 @@ impl ControllerEvents for Recorder {
.await .await
} }
async fn user_deleted<T>(&self, obj: &T) -> Result<(), Self::Error> async fn user_deleted<T>(&self, obj: &T, username: &str) -> Result<(), Self::Error>
where where
T: Resource<DynamicType = ()> + Sync, T: Resource<DynamicType = ()> + Sync,
{ {
self.publish( self.publish(
&Event { &Event {
type_: EventType::Normal, type_: EventType::Normal,
reason: "Deleted".into(), reason: "UserDeleted".into(),
note: Some("Deleted user".into()), note: Some(format!("Deleted user '{username}'")),
action: "Deleted".into(), action: "UserDeleted".into(),
secondary: None, secondary: None,
}, },
&obj.object_ref(&()), &obj.object_ref(&()),
@@ -141,16 +126,16 @@ impl ControllerEvents for Recorder {
.await .await
} }
async fn group_deleted<T>(&self, obj: &T) -> Result<(), Self::Error> async fn group_deleted<T>(&self, obj: &T, name: &str) -> Result<(), Self::Error>
where where
T: Resource<DynamicType = ()> + Sync, T: Resource<DynamicType = ()> + Sync,
{ {
self.publish( self.publish(
&Event { &Event {
type_: EventType::Normal, type_: EventType::Normal,
reason: "Deleted".into(), reason: "GroupDeleted".into(),
note: Some("Deleted group".into()), note: Some(format!("Deleted group '{name}'")),
action: "Deleted".into(), action: "GroupDeleted".into(),
secondary: None, secondary: None,
}, },
&obj.object_ref(&()), &obj.object_ref(&()),
@@ -158,52 +143,16 @@ impl ControllerEvents for Recorder {
.await .await
} }
async fn user_attribute_created<T>(&self, obj: &T) -> Result<(), Self::Error> async fn user_not_found<T>(&self, obj: &T, username: &str) -> Result<(), Self::Error>
where where
T: Resource<DynamicType = ()> + Sync, T: Resource<DynamicType = ()> + Sync,
{ {
self.publish( self.publish(
&Event { &Event {
type_: EventType::Warning, type_: EventType::Warning,
reason: "Created".into(), reason: "UserNotFound".into(),
note: Some("Created user attribute".into()), note: Some(format!("User '{username}' not found")),
action: "Created".into(), action: "UserNotFound".into(),
secondary: None,
},
&obj.object_ref(&()),
)
.await
}
async fn user_attribute_desync<T>(&self, obj: &T, fields: &[String]) -> Result<(), Self::Error>
where
T: Resource<DynamicType = ()> + Sync,
{
self.publish(
&Event {
type_: EventType::Warning,
reason: "Desync".into(),
note: Some(format!(
"User attribute fields '{fields:?}' are out of sync"
)),
action: "Desync".into(),
secondary: None,
},
&obj.object_ref(&()),
)
.await
}
async fn user_attribute_deleted<T>(&self, obj: &T) -> Result<(), Self::Error>
where
T: Resource<DynamicType = ()> + Sync,
{
self.publish(
&Event {
type_: EventType::Warning,
reason: "Deleted".into(),
note: Some("Deleted user attribute'".into()),
action: "Deleted".into(),
secondary: None, secondary: None,
}, },
&obj.object_ref(&()), &obj.object_ref(&()),

View File

@@ -1,6 +1,3 @@
pub mod context; pub mod context;
pub mod lldap; pub mod lldap;
pub mod resources; pub mod resources;
mod version;
pub use version::VERSION;

View File

@@ -1,5 +1,6 @@
use std::time::Duration; use std::time::Duration;
use anyhow::Context;
use cynic::http::{CynicReqwestError, ReqwestExt}; use cynic::http::{CynicReqwestError, ReqwestExt};
use cynic::{GraphQlError, GraphQlResponse, MutationBuilder, QueryBuilder}; use cynic::{GraphQlError, GraphQlResponse, MutationBuilder, QueryBuilder};
use lldap_auth::login::{ClientSimpleLoginRequest, ServerLoginResponse}; use lldap_auth::login::{ClientSimpleLoginRequest, ServerLoginResponse};
@@ -7,17 +8,14 @@ use lldap_auth::opaque::AuthenticationError;
use lldap_auth::registration::ServerRegistrationStartResponse; use lldap_auth::registration::ServerRegistrationStartResponse;
use lldap_auth::{opaque, registration}; use lldap_auth::{opaque, registration};
use queries::{ use queries::{
AddUserToGroup, AddUserToGroupVariables, AttributeSchema, CreateGroup, CreateGroupVariables, AddUserToGroup, AddUserToGroupVariables, CreateGroup, CreateGroupVariables, CreateUser,
CreateUser, CreateUserAttribute, CreateUserAttributeVariables, CreateUserVariables, CreateUserVariables, DeleteGroup, DeleteGroupVariables, DeleteUser, DeleteUserVariables,
DeleteGroup, DeleteGroupVariables, DeleteUser, DeleteUserAttribute, GetGroups, GetUser, GetUserVariables, Group, RemoveUserFromGroup, RemoveUserFromGroupVariables,
DeleteUserAttributeVariables, DeleteUserVariables, GetGroups, GetUser, GetUserAttributes, User,
GetUserVariables, Group, RemoveUserFromGroup, RemoveUserFromGroupVariables, User,
}; };
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue}; use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
use tracing::{debug, trace}; use tracing::{debug, trace};
use crate::resources::AttributeType;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum Error { pub enum Error {
#[error("Cynic error: {0}")] #[error("Cynic error: {0}")]
@@ -28,19 +26,15 @@ pub enum Error {
Authentication(#[from] AuthenticationError), Authentication(#[from] AuthenticationError),
#[error("GraphQL error: {0}")] #[error("GraphQL error: {0}")]
GraphQl(#[from] GraphQlError), GraphQl(#[from] GraphQlError),
#[error("Missing environment variable: {0}")]
MissingEnvironmentVariable(&'static str),
#[error("Could not read password file: {0}")]
CouldNotReadPasswordFile(#[from] std::io::Error),
} }
pub type Result<T, E = Error> = std::result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;
fn check_graphql_errors<T>(response: GraphQlResponse<T>) -> Result<T> { fn check_graphql_errors<T>(response: GraphQlResponse<T>) -> Result<T> {
if let Some(errors) = &response.errors if let Some(errors) = &response.errors {
&& !errors.is_empty() if !errors.is_empty() {
{ Err(errors.first().expect("Should not be empty").clone())?;
Err(errors.first().expect("Should not be empty").clone())?; }
} }
Ok(response Ok(response
@@ -56,26 +50,14 @@ pub struct LldapConfig {
} }
impl LldapConfig { impl LldapConfig {
pub fn try_from_env() -> Result<Self> { pub fn try_from_env() -> anyhow::Result<Self> {
let password = std::env::var("LLDAP_PASSWORD_FILE").map_or_else(
|_| {
std::env::var("LLDAP_PASSWORD").map_err(|_| {
Error::MissingEnvironmentVariable("LLDAP_PASSWORD or LLDAP_PASSWORD_FILE")
})
},
|path| {
std::fs::read_to_string(path)
.map(|v| v.trim().into())
.map_err(|err| err.into())
},
)?;
Ok(Self { Ok(Self {
username: std::env::var("LLDAP_USERNAME") username: std::env::var("LLDAP_USERNAME")
.map_err(|_| Error::MissingEnvironmentVariable("LLDAP_USERNAME"))?, .context("Variable 'LLDAP_USERNAME' is not set or invalid")?,
password, password: std::env::var("LLDAP_PASSWORD")
.context("Variable 'LLDAP_PASSWORD' is not set or invalid")?,
url: std::env::var("LLDAP_URL") url: std::env::var("LLDAP_URL")
.map_err(|_| Error::MissingEnvironmentVariable("LLDAP_URL"))?, .context("Variable 'LLDAP_URL' is not set or invalid")?,
}) })
} }
@@ -303,60 +285,4 @@ impl LldapClient {
Ok(()) Ok(())
} }
pub async fn get_user_attributes(&self) -> Result<Vec<AttributeSchema>> {
let operation = GetUserAttributes::build(());
let response = self
.client
.post(format!("{}/api/graphql", self.url))
.run_graphql(operation)
.await?;
Ok(check_graphql_errors(response)?
.schema
.user_schema
.attributes)
}
pub async fn create_user_attribute(
&self,
name: &str,
r#type: AttributeType,
list: bool,
visible: bool,
editable: bool,
) -> Result<()> {
let operation = CreateUserAttribute::build(CreateUserAttributeVariables {
name,
r#type: r#type.into(),
list,
visible,
editable,
});
let response = self
.client
.post(format!("{}/api/graphql", self.url))
.run_graphql(operation)
.await?;
check_graphql_errors(response)?;
Ok(())
}
pub async fn delete_user_attribute(&self, name: &str) -> Result<()> {
let operation = DeleteUserAttribute::build(DeleteUserAttributeVariables { name });
let response = self
.client
.post(format!("{}/api/graphql", self.url))
.run_graphql(operation)
.await?;
check_graphql_errors(response)?;
Ok(())
}
} }

View File

@@ -1,20 +1,15 @@
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use color_eyre::eyre::Context as _;
use dotenvy::dotenv;
use futures::StreamExt; use futures::StreamExt;
use k8s_openapi::api::core::v1::Secret; use k8s_openapi::api::core::v1::Secret;
use kube::runtime::controller::{self, Action}; use kube::runtime::controller::{self, Action};
use kube::runtime::reflector::ObjectRef; use kube::runtime::reflector::ObjectRef;
use kube::runtime::{Controller, watcher}; use kube::runtime::{Controller, watcher};
use kube::{Api, Client as KubeClient, Resource}; use kube::{Api, Client as KubeClient, Resource};
use lldap_controller::VERSION;
use lldap_controller::context::Context; use lldap_controller::context::Context;
use lldap_controller::lldap::LldapConfig; use lldap_controller::lldap::LldapConfig;
use lldap_controller::resources::{ use lldap_controller::resources::{self, Error, Group, ServiceUser, reconcile};
self, Error, Group, ServiceUser, UserAttribute, reconcile, reconcile_namespaced,
};
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::util::SubscriberInitExt;
@@ -37,10 +32,7 @@ async fn log_status<T>(
} }
#[tokio::main] #[tokio::main]
async fn main() -> color_eyre::Result<()> { async fn main() -> anyhow::Result<()> {
color_eyre::install()?;
dotenv().ok();
let env_filter = EnvFilter::try_from_default_env() let env_filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info")) .or_else(|_| EnvFilter::try_new("info"))
.expect("Fallback should be valid"); .expect("Fallback should be valid");
@@ -53,9 +45,7 @@ async fn main() -> color_eyre::Result<()> {
Registry::default().with(logger).with(env_filter).init(); Registry::default().with(logger).with(env_filter).init();
} }
info!(version = VERSION, "Starting"); info!("Starting controller");
let bind_dn_template = std::env::var("LLDAP_BIND_DN").wrap_err("LLDAP_BIND_DN is not set")?;
let client = KubeClient::try_default().await?; let client = KubeClient::try_default().await?;
@@ -63,35 +53,25 @@ async fn main() -> color_eyre::Result<()> {
"lldap.huizinga.dev", "lldap.huizinga.dev",
client.clone(), client.clone(),
LldapConfig::try_from_env()?, LldapConfig::try_from_env()?,
bind_dn_template,
); );
let service_users = Api::<ServiceUser>::all(client.clone());
let secrets = Api::<Secret>::all(client.clone()); let secrets = Api::<Secret>::all(client.clone());
let service_users = Api::<ServiceUser>::all(client.clone());
let service_user_controller = Controller::new(service_users, Default::default()) let service_user_controller = Controller::new(service_users, Default::default())
.owns(secrets, Default::default()) .owns(secrets, Default::default())
.shutdown_on_signal()
.run(reconcile_namespaced, error_policy, Arc::new(data.clone()))
.for_each(log_status);
let groups = Api::<Group>::all(client.clone());
let group_controller = Controller::new(groups, Default::default())
.shutdown_on_signal() .shutdown_on_signal()
.run(reconcile, error_policy, Arc::new(data.clone())) .run(reconcile, error_policy, Arc::new(data.clone()))
.for_each(log_status); .for_each(log_status);
let user_attributes = Api::<UserAttribute>::all(client.clone()); let groups = Api::<Group>::all(client.clone());
let user_attribute_controller = Controller::new(user_attributes, Default::default())
let group_controller = Controller::new(groups, Default::default())
.shutdown_on_signal() .shutdown_on_signal()
.run(reconcile, error_policy, Arc::new(data)) .run(reconcile, error_policy, Arc::new(data))
.for_each(log_status); .for_each(log_status);
tokio::join!( tokio::join!(service_user_controller, group_controller);
service_user_controller,
group_controller,
user_attribute_controller
);
Ok(()) Ok(())
} }

View File

@@ -1,7 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use async_trait::async_trait;
use kube::CustomResource; use kube::CustomResource;
use kube::runtime::controller::Action; use kube::runtime::controller::Action;
use schemars::JsonSchema; use schemars::JsonSchema;
@@ -20,7 +19,6 @@ use crate::context::{Context, ControllerEvents};
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GroupSpec {} pub struct GroupSpec {}
#[async_trait]
impl Reconcile for Group { impl Reconcile for Group {
async fn reconcile(self: Arc<Self>, ctx: Arc<Context>) -> Result<Action> { async fn reconcile(self: Arc<Self>, ctx: Arc<Context>) -> Result<Action> {
let name = self let name = self
@@ -41,7 +39,7 @@ impl Reconcile for Group {
lldap_client.create_group(&name).await?; lldap_client.create_group(&name).await?;
ctx.recorder.group_created(self.as_ref()).await?; ctx.recorder.group_created(self.as_ref(), &name).await?;
} else { } else {
trace!("Group already exists"); trace!("Group already exists");
} }
@@ -68,7 +66,7 @@ impl Reconcile for Group {
lldap_client.delete_group(group.id).await?; lldap_client.delete_group(group.id).await?;
ctx.recorder.group_deleted(self.as_ref()).await?; ctx.recorder.group_deleted(self.as_ref(), &name).await?;
} else { } else {
trace!(name, "Group does not exist") trace!(name, "Group does not exist")
} }

View File

@@ -1,12 +1,9 @@
mod group; mod group;
mod service_user; mod service_user;
mod user_attribute;
use core::fmt; use core::fmt;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait;
use k8s_openapi::{ClusterResourceScope, NamespaceResourceScope};
use kube::runtime::controller::Action; use kube::runtime::controller::Action;
use kube::runtime::finalizer; use kube::runtime::finalizer;
use kube::{Api, Resource, ResourceExt}; use kube::{Api, Resource, ResourceExt};
@@ -16,7 +13,6 @@ use tracing::{debug, instrument};
pub use self::group::Group; pub use self::group::Group;
pub use self::service_user::ServiceUser; pub use self::service_user::ServiceUser;
pub use self::user_attribute::{Type as AttributeType, UserAttribute};
use crate::context::Context; use crate::context::Context;
use crate::lldap; use crate::lldap;
@@ -32,8 +28,6 @@ pub enum Error {
Finalizer(#[source] Box<finalizer::Error<Self>>), Finalizer(#[source] Box<finalizer::Error<Self>>),
#[error("MissingObjectKey: {0}")] #[error("MissingObjectKey: {0}")]
MissingObjectKey(&'static str), MissingObjectKey(&'static str),
#[error("UserAttributeDesync: {0:?}")]
UserAttributeDesync(Vec<String>),
} }
impl From<finalizer::Error<Self>> for Error { impl From<finalizer::Error<Self>> for Error {
@@ -44,60 +38,29 @@ impl From<finalizer::Error<Self>> for Error {
type Result<T, E = Error> = std::result::Result<T, E>; type Result<T, E = Error> = std::result::Result<T, E>;
#[async_trait] trait Reconcile {
pub trait Reconcile {
async fn reconcile(self: Arc<Self>, ctx: Arc<Context>) -> Result<Action>; async fn reconcile(self: Arc<Self>, ctx: Arc<Context>) -> Result<Action>;
async fn cleanup(self: Arc<Self>, ctx: Arc<Context>) -> Result<Action>; async fn cleanup(self: Arc<Self>, ctx: Arc<Context>) -> Result<Action>;
} }
#[instrument(skip(obj, ctx))]
pub async fn reconcile_namespaced<T>(obj: Arc<T>, ctx: Arc<Context>) -> Result<Action>
where
T: Resource<Scope = NamespaceResourceScope>
+ ResourceExt
+ Clone
+ Serialize
+ DeserializeOwned
+ fmt::Debug
+ Reconcile,
<T as Resource>::DynamicType: Default,
{
debug!(name = obj.name_any(), "Reconcile");
let namespace = obj.namespace().expect("resource should be namespaced");
let api = Api::<T>::namespaced(ctx.client.clone(), &namespace);
Ok(finalizer(&api, &ctx.controller_name, obj, |event| async {
match event {
finalizer::Event::Apply(obj) => obj.reconcile(ctx.clone()).await,
finalizer::Event::Cleanup(obj) => obj.cleanup(ctx.clone()).await,
}
})
.await?)
}
#[instrument(skip(obj, ctx))] #[instrument(skip(obj, ctx))]
pub async fn reconcile<T>(obj: Arc<T>, ctx: Arc<Context>) -> Result<Action> pub async fn reconcile<T>(obj: Arc<T>, ctx: Arc<Context>) -> Result<Action>
where where
T: Resource<Scope = ClusterResourceScope> T: Resource + ResourceExt + Clone + Serialize + DeserializeOwned + fmt::Debug + Reconcile,
+ ResourceExt
+ Clone
+ Serialize
+ DeserializeOwned
+ fmt::Debug
+ Reconcile,
<T as Resource>::DynamicType: Default, <T as Resource>::DynamicType: Default,
{ {
debug!(name = obj.name_any(), "Reconcile"); debug!(name = obj.name_any(), "Reconcile");
let api = Api::<T>::all(ctx.client.clone()); let service_users = Api::<T>::all(ctx.client.clone());
Ok(finalizer(&api, &ctx.controller_name, obj, |event| async { Ok(
match event { finalizer(&service_users, &ctx.controller_name, obj, |event| async {
finalizer::Event::Apply(obj) => obj.reconcile(ctx.clone()).await, match event {
finalizer::Event::Cleanup(obj) => obj.cleanup(ctx.clone()).await, finalizer::Event::Apply(obj) => obj.reconcile(ctx.clone()).await,
} finalizer::Event::Cleanup(obj) => obj.cleanup(ctx.clone()).await,
}) }
.await?) })
.await?,
)
} }

View File

@@ -3,14 +3,12 @@ use std::str::from_utf8;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use async_trait::async_trait;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use k8s_openapi::api::core::v1::Secret; use k8s_openapi::api::core::v1::Secret;
use k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference; use k8s_openapi::apimachinery::pkg::apis::meta::v1::OwnerReference;
use kube::api::{ObjectMeta, Patch, PatchParams, PostParams}; use kube::api::{ObjectMeta, Patch, PatchParams, PostParams};
use kube::runtime::controller::Action; use kube::runtime::controller::Action;
use kube::{Api, CustomResource, Resource}; use kube::{Api, CustomResource, Resource};
use leon::{Template, vals};
use passwords::PasswordGenerator; use passwords::PasswordGenerator;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -77,7 +75,6 @@ fn format_username(name: &str, namespace: &str) -> String {
format!("{name}.{namespace}") format!("{name}.{namespace}")
} }
#[async_trait]
impl Reconcile for ServiceUser { impl Reconcile for ServiceUser {
async fn reconcile(self: Arc<Self>, ctx: Arc<Context>) -> Result<Action> { async fn reconcile(self: Arc<Self>, ctx: Arc<Context>) -> Result<Action> {
let name = self let name = self
@@ -116,31 +113,6 @@ impl Reconcile for ServiceUser {
debug!(name, secret_name, "Generating new secret"); debug!(name, secret_name, "Generating new secret");
new_secret(&username, oref) new_secret(&username, oref)
})
.and_modify(|secret| {
let bind_dn_template = match Template::parse(&ctx.bind_dn_template) {
Ok(template) => template,
Err(err) => {
warn!("Invalid bind_dn template: {err}");
return;
}
};
let bind_dn = match bind_dn_template.render(&&vals(|key| match key {
"username" => Some(username.clone().into()),
_ => None,
})) {
Ok(bind_dn) => bind_dn,
Err(err) => {
warn!("Failed to render bind_dn template: {err}");
return;
}
};
secret
.string_data
.get_or_insert_default()
.insert("bind_dn".into(), bind_dn);
}); });
trace!(name, "Committing secret"); trace!(name, "Committing secret");
@@ -172,7 +144,7 @@ impl Reconcile for ServiceUser {
debug!(name, username, "Creating new user"); debug!(name, username, "Creating new user");
let user = lldap_client.create_user(&username).await?; let user = lldap_client.create_user(&username).await?;
ctx.recorder.user_created(self.as_ref()).await?; ctx.recorder.user_created(self.as_ref(), &username).await?;
Ok(user) Ok(user)
} }
@@ -236,11 +208,14 @@ impl Reconcile for ServiceUser {
Err(lldap::Error::GraphQl(err)) Err(lldap::Error::GraphQl(err))
if err.message == format!("Entity not found: `No such user: '{username}'`") => if err.message == format!("Entity not found: `No such user: '{username}'`") =>
{ {
ctx.recorder
.user_not_found(self.as_ref(), &username)
.await?;
warn!(name, username, "User not found"); warn!(name, username, "User not found");
Ok(()) Ok(())
} }
Ok(_) => { Ok(_) => {
ctx.recorder.user_deleted(self.as_ref()).await?; ctx.recorder.user_deleted(self.as_ref(), &username).await?;
Ok(()) Ok(())
} }
Err(err) => Err(err), Err(err) => Err(err),

View File

@@ -1,191 +0,0 @@
use std::time::Duration;
use async_trait::async_trait;
use kube::api::{Patch, PatchParams};
use kube::runtime::controller::Action;
use kube::{Api, CustomResource, KubeSchema};
use queries::AttributeType;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{debug, trace, warn};
use super::Reconcile;
use crate::context::ControllerEvents;
use crate::lldap;
use crate::resources::Error;
#[derive(Deserialize, Serialize, Clone, Copy, Debug, JsonSchema)]
pub enum Type {
String,
Integer,
Jpeg,
DateTime,
}
impl From<Type> for AttributeType {
fn from(t: Type) -> Self {
match t {
Type::String => Self::String,
Type::Integer => Self::Integer,
Type::Jpeg => Self::JpegPhoto,
Type::DateTime => Self::DateTime,
}
}
}
#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, KubeSchema)]
#[kube(
kind = "UserAttribute",
group = "lldap.huizinga.dev",
version = "v1",
status = "UserAttributesStatus"
)]
#[kube(
shortname = "lua",
doc = "Custom resource for managing custom User Attributes inside of LLDAP",
printcolumn = r#"{"name":"Type", "type":"string", "description":"Type of attribute", "jsonPath":".spec.type"}"#,
printcolumn = r#"{"name":"List", "type":"boolean", "description":"Can the attribute contain multiple values", "jsonPath":".spec.list"}"#,
printcolumn = r#"{"name":"Visible", "type":"boolean", "description":"Can users see the value", "jsonPath":".spec.userVisible"}"#,
printcolumn = r#"{"name":"Editable", "type":"boolean", "description":"Can users edit the value", "jsonPath":".spec.userEditable"}"#,
printcolumn = r#"{"name":"Synced", "type":"boolean", "jsonPath":".status.synced"}"#,
printcolumn = r#"{"name":"Age", "type":"date", "jsonPath":".metadata.creationTimestamp"}"#
)]
#[kube(
validation = Rule::new("self.spec == oldSelf.spec").message("User attributes are immutable"),
validation = Rule::new("!self.spec.userEditable || self.spec.userVisible && self.spec.userEditable").message("Editable attribute must also be visible")
)]
#[serde(rename_all = "camelCase")]
pub struct UserAttributeSpec {
r#type: Type,
#[serde(default)]
list: bool,
#[serde(default)]
user_visible: bool,
#[serde(default)]
user_editable: bool,
}
#[derive(Deserialize, Serialize, Clone, Default, Debug, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct UserAttributesStatus {
pub synced: bool,
}
#[async_trait]
impl Reconcile for UserAttribute {
async fn reconcile(
self: std::sync::Arc<Self>,
ctx: std::sync::Arc<crate::context::Context>,
) -> super::Result<kube::runtime::controller::Action> {
let name = self
.metadata
.name
.clone()
.ok_or(Error::MissingObjectKey(".metadata.name"))?;
debug!(name, "Apply");
trace!(name, "Get existing attributes");
let lldap_client = ctx.lldap_config.build_client().await?;
let user_attributes = lldap_client.get_user_attributes().await?;
trace!("{user_attributes:?}");
let client = &ctx.client;
let api = Api::<UserAttribute>::all(client.clone());
if let Some(attribute) = user_attributes
.iter()
.find(|attribute| attribute.name == name)
{
trace!("User attribute already exists: {attribute:?}");
let mut desynced: Vec<String> = Vec::new();
if attribute.attribute_type != self.spec.r#type.into() {
desynced.push("type".into());
}
if attribute.is_list != self.spec.list {
desynced.push("list".into());
}
if attribute.is_visible != self.spec.user_visible {
desynced.push("userVisible".into());
}
if attribute.is_editable != self.spec.user_editable {
desynced.push("userEditable".into());
}
if !desynced.is_empty() {
set_status(&api, &name, false).await?;
ctx.recorder
.user_attribute_desync(self.as_ref(), &desynced)
.await?;
return Err(Error::UserAttributeDesync(desynced));
}
trace!("User attribute matches with spec");
} else {
trace!("User attribute does not exist yet");
lldap_client
.create_user_attribute(
&name,
self.spec.r#type,
self.spec.list,
self.spec.user_visible,
self.spec.user_editable,
)
.await?;
ctx.recorder.user_attribute_created(self.as_ref()).await?;
}
set_status(&api, &name, true).await?;
Ok(Action::requeue(Duration::from_secs(3600)))
}
async fn cleanup(
self: std::sync::Arc<Self>,
ctx: std::sync::Arc<crate::context::Context>,
) -> super::Result<kube::runtime::controller::Action> {
let name = self
.metadata
.name
.clone()
.ok_or(Error::MissingObjectKey(".metadata.name"))?;
debug!(name, "Cleanup");
let lldap_client = ctx.lldap_config.build_client().await?;
trace!(name, "Deleting user attribute");
match lldap_client.delete_user_attribute(&name).await {
Err(lldap::Error::GraphQl(err))
if err.message == format!("Attribute {name} is not defined in the schema") =>
{
warn!(name, "User attribute not found");
Ok(())
}
Ok(_) => {
ctx.recorder.user_attribute_deleted(self.as_ref()).await?;
Ok(())
}
Err(err) => Err(err),
}?;
Ok(Action::await_change())
}
}
async fn set_status(
api: &Api<UserAttribute>,
name: &str,
synced: bool,
) -> Result<UserAttribute, kube::Error> {
trace!(name, "Updating status");
let status = json!({
"status": UserAttributesStatus { synced }
});
api.patch_status(name, &PatchParams::default(), &Patch::Merge(&status))
.await
}

View File

@@ -1,11 +0,0 @@
pub const VERSION: &str = get_version();
const fn get_version() -> &'static str {
if let Some(version) = std::option_env!("RELEASE_VERSION")
&& !version.is_empty()
{
version
} else {
git_version::git_version!(fallback = "unknown")
}
}

View File

@@ -1,9 +0,0 @@
apiVersion: lldap.huizinga.dev/v1
kind: UserAttribute
metadata:
name: test-attribute
spec:
type: String
list: true
userVisible: true
userEditable: true