feat(vaultwarden): initial packaging
All checks were successful
/ publish (push) Successful in 17s

This commit is contained in:
Hugo Renard 2025-01-10 18:26:51 +01:00
parent d2452df888
commit a590a16f2b
Signed by: hougo
GPG key ID: 3A285FD470209C59
19 changed files with 922 additions and 1 deletions

View file

@ -304,4 +304,13 @@ resources:
kind: Wikijs kind: Wikijs
path: libre.sh/api/apps/v1alpha1 path: libre.sh/api/apps/v1alpha1
version: v1alpha1 version: v1alpha1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: libre.sh
group: apps
kind: Vaultwarden
path: libre.sh/api/apps/v1alpha1
version: v1alpha1
version: "3" version: "3"

View file

@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: 2025 IndieHosters <contact@indiehosters.net>
//
// SPDX-License-Identifier: EUPL-1.2
package v1alpha1
import (
"encoding/json"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
lshmeta "libre.sh/api/meta/v1alpha1"
)
// VaultwardenSpec defines the desired state of Vaultwarden.
type VaultwardenSpec struct {
lshmeta.Spec `json:",inline"`
//+kubebuilder:validation:Required
//+kubebuilder:validation:MinLength=3
Host string `json:"host"`
//+kubebuilder:validation:Required
//+kubebuilder:validation:MinLength=3
Image string `json:"image"`
//+kubebuilder:validation:Required
MailboxRef corev1.LocalObjectReference `json:"mailboxRef"`
//+kubebuilder:validation:Type=object
//+kubebuilder:validation:Schemaless
//+kubebuilder:pruning:PreserveUnknownFields
Config json.RawMessage `json:"config,omitempty"`
}
// VaultwardenStatus defines the observed state of Vaultwarden.
type VaultwardenStatus struct {
lshmeta.Status `json:",inline"`
}
//go:generate lsh-gen v1alpha1 Vaultwarden
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description=""
//+kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description=""
//+kubebuilder:printcolumn:name="Version",type="string",JSONPath=".status.version",description=""
//+kubebuilder:printcolumn:name="Suspended",type="boolean",JSONPath=".spec.suspend",description=""
// Vaultwarden is the Schema for the vaultwardens API.
type Vaultwarden struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec VaultwardenSpec `json:"spec,omitempty"`
Status VaultwardenStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// VaultwardenList contains a list of Vaultwarden.
type VaultwardenList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Vaultwarden `json:"items"`
}
func init() {
SchemeBuilder.Register(&Vaultwarden{}, &VaultwardenList{})
}

View file

@ -1,6 +1,6 @@
//go:build !ignore_autogenerated //go:build !ignore_autogenerated
// SPDX-FileCopyrightText: 2024 - 2025 IndieHosters <contact@indiehosters.net> // SPDX-FileCopyrightText: 2025 IndieHosters <contact@indiehosters.net>
// //
// SPDX-License-Identifier: EUPL-1.2 // SPDX-License-Identifier: EUPL-1.2
@ -1265,6 +1265,103 @@ func (in *StaticWebsiteStatus) DeepCopy() *StaticWebsiteStatus {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Vaultwarden) DeepCopyInto(out *Vaultwarden) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Vaultwarden.
func (in *Vaultwarden) DeepCopy() *Vaultwarden {
if in == nil {
return nil
}
out := new(Vaultwarden)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Vaultwarden) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VaultwardenList) DeepCopyInto(out *VaultwardenList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Vaultwarden, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultwardenList.
func (in *VaultwardenList) DeepCopy() *VaultwardenList {
if in == nil {
return nil
}
out := new(VaultwardenList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *VaultwardenList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VaultwardenSpec) DeepCopyInto(out *VaultwardenSpec) {
*out = *in
out.Spec = in.Spec
out.MailboxRef = in.MailboxRef
if in.Config != nil {
in, out := &in.Config, &out.Config
*out = make(json.RawMessage, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultwardenSpec.
func (in *VaultwardenSpec) DeepCopy() *VaultwardenSpec {
if in == nil {
return nil
}
out := new(VaultwardenSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VaultwardenStatus) DeepCopyInto(out *VaultwardenStatus) {
*out = *in
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultwardenStatus.
func (in *VaultwardenStatus) DeepCopy() *VaultwardenStatus {
if in == nil {
return nil
}
out := new(VaultwardenStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Wikijs) DeepCopyInto(out *Wikijs) { func (in *Wikijs) DeepCopyInto(out *Wikijs) {
*out = *in *out = *in

View file

@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2025 IndieHosters <contact@indiehosters.net>
//
// SPDX-License-Identifier: EUPL-1.2
// Code generated by lsh-gen; DO NOT EDIT.
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (o *Vaultwarden) GetSuspend() bool {
return o.Spec.Suspend
}
func (o *Vaultwarden) SetSuspend(value bool) {
o.Spec.Suspend = value
}
func (o *Vaultwarden) GetVersion() string {
return o.Status.Version
}
func (o *Vaultwarden) SetVersion(value string) {
o.Status.Version = value
}
func (o *Vaultwarden) GetImage() string {
return o.Spec.Image
}
func (o *Vaultwarden) GetConditions() []metav1.Condition {
return o.Status.Conditions
}
func (o *Vaultwarden) SetConditions(conditions []metav1.Condition) {
o.Status.Conditions = conditions
}

View file

@ -354,6 +354,12 @@ func main() {
setupLog.Error(err, "unable to create controller", "controller", "Wikijs") setupLog.Error(err, "unable to create controller", "controller", "Wikijs")
os.Exit(1) os.Exit(1)
} }
if err = (&appscontroller.VaultwardenReconciler{
Client: mgr.GetClient(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Vaultwarden")
os.Exit(1)
}
//+kubebuilder:scaffold:builder //+kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {

View file

@ -0,0 +1,159 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.14.0
name: vaultwardens.apps.libre.sh
spec:
group: apps.libre.sh
names:
kind: Vaultwarden
listKind: VaultwardenList
plural: vaultwardens
singular: vaultwarden
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].message
name: Status
type: string
- jsonPath: .status.version
name: Version
type: string
- jsonPath: .spec.suspend
name: Suspended
type: boolean
name: v1alpha1
schema:
openAPIV3Schema:
description: Vaultwarden is the Schema for the vaultwardens API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: VaultwardenSpec defines the desired state of Vaultwarden.
properties:
config:
type: object
x-kubernetes-preserve-unknown-fields: true
host:
minLength: 3
type: string
image:
minLength: 3
type: string
mailboxRef:
description: |-
LocalObjectReference contains enough information to let you locate the
referenced object inside the same namespace.
properties:
name:
description: |-
Name of the referent.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?
type: string
type: object
x-kubernetes-map-type: atomic
suspend:
type: boolean
required:
- host
- image
- mailboxRef
type: object
status:
description: VaultwardenStatus defines the observed state of Vaultwarden.
properties:
conditions:
items:
description: "Condition contains details for one aspect of the current state of this API Resource.\n---\nThis struct is intended for direct use as an array at the field path .status.conditions. For example,\n\n\n\ttype FooStatus struct{\n\t // Represents the observations of a foo's current state.\n\t // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t // other fields\n\t}"
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: |-
type of condition in CamelCase or in foo.example.com/CamelCase.
---
Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
useful (see .node.status.conditions), the ability to deconflict is important.
The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
version:
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}

View file

@ -35,6 +35,7 @@ resources:
- bases/apps.libre.sh_listmonks.yaml - bases/apps.libre.sh_listmonks.yaml
- bases/apps.libre.sh_staticwebsites.yaml - bases/apps.libre.sh_staticwebsites.yaml
- bases/apps.libre.sh_wikijs.yaml - bases/apps.libre.sh_wikijs.yaml
- bases/apps.libre.sh_vaultwardens.yaml
#+kubebuilder:scaffold:crdkustomizeresource #+kubebuilder:scaffold:crdkustomizeresource
patches: patches:

View file

@ -0,0 +1,27 @@
# permissions for end users to edit vaultwardens.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: libre-sh
app.kubernetes.io/managed-by: kustomize
name: apps-vaultwarden-editor-role
rules:
- apiGroups:
- apps.libre.sh
resources:
- vaultwardens
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- apps.libre.sh
resources:
- vaultwardens/status
verbs:
- get

View file

@ -0,0 +1,23 @@
# permissions for end users to view vaultwardens.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: libre-sh
app.kubernetes.io/managed-by: kustomize
name: apps-vaultwarden-viewer-role
rules:
- apiGroups:
- apps.libre.sh
resources:
- vaultwardens
verbs:
- get
- list
- watch
- apiGroups:
- apps.libre.sh
resources:
- vaultwardens/status
verbs:
- get

View file

@ -30,3 +30,5 @@ resources:
- matrix_elementcall_viewer_role.yaml - matrix_elementcall_viewer_role.yaml
- voip_livekitserver_editor_role.yaml - voip_livekitserver_editor_role.yaml
- voip_livekitserver_viewer_role.yaml - voip_livekitserver_viewer_role.yaml
- apps_vaultwarden_viewer_role.yaml
- apps_vaultwarden_editor_role.yaml

View file

@ -404,6 +404,32 @@ rules:
- get - get
- patch - patch
- update - update
- apiGroups:
- apps.libre.sh
resources:
- vaultwardens
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- apps.libre.sh
resources:
- vaultwardens/finalizers
verbs:
- update
- apiGroups:
- apps.libre.sh
resources:
- vaultwardens/status
verbs:
- get
- patch
- update
- apiGroups: - apiGroups:
- apps.libre.sh - apps.libre.sh
resources: resources:

View file

@ -0,0 +1,13 @@
apiVersion: apps.libre.sh/v1alpha1
kind: Vaultwarden
metadata:
labels:
app.kubernetes.io/name: libre-sh
app.kubernetes.io/managed-by: kustomize
name: vaultwarden-sample
spec:
host: vault.dev.local
image: docker.io/vaultwarden/server:latest
mailboxRef:
name: mailbox-sample
config: {}

View file

@ -33,4 +33,5 @@ resources:
- apps_v1alpha1_listmonk.yaml - apps_v1alpha1_listmonk.yaml
- apps_v1alpha1_staticwebsite.yaml - apps_v1alpha1_staticwebsite.yaml
- apps_v1alpha1_wikijs.yaml - apps_v1alpha1_wikijs.yaml
- apps_v1alpha1_vaultwarden.yaml
#+kubebuilder:scaffold:manifestskustomizesamples #+kubebuilder:scaffold:manifestskustomizesamples

View file

@ -0,0 +1,128 @@
// SPDX-FileCopyrightText: 2025 IndieHosters <contact@indiehosters.net>
//
// SPDX-License-Identifier: EUPL-1.2
package apps
import (
"context"
"github.com/fluxcd/pkg/apis/meta"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
appsv1alpha1 "libre.sh/api/apps/v1alpha1"
lshcore "libre.sh/api/core/v1alpha1"
lshmeta "libre.sh/api/meta/v1alpha1"
lshr "libre.sh/pkg/controller-runtime"
)
type vaultwardenResources struct {
postgres *lshcore.Postgres
pvc *corev1.PersistentVolumeClaim
mailbox *lshcore.Mailbox
secret *corev1.Secret
}
// VaultwardenReconciler reconciles a Vaultwarden object
type VaultwardenReconciler struct {
client.Client
}
// +kubebuilder:rbac:groups=apps.libre.sh,resources=vaultwardens,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps.libre.sh,resources=vaultwardens/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps.libre.sh,resources=vaultwardens/finalizers,verbs=update
func (r *VaultwardenReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
var vault appsv1alpha1.Vaultwarden
patcher, result := lshr.Initialize(ctx, r, req, &vault)
if result != nil {
return result.Unwrap()
}
if lshr.IsFinalizing(&vault) {
return lshr.Finalize(ctx, r, patcher, &vault, func() error {
return nil
})
}
resources := vaultwardenResources{}
_, err := lshr.ReconcileGenericIngress(ctx, r, &vault, vault.Spec.Host, nil)
if err != nil {
return ctrl.Result{}, err
}
resources.postgres, err = lshr.ReconcileGenericPostgres(ctx, r, &vault, "vaultwarden")
if err != nil {
return ctrl.Result{}, err
}
err = r.reconcilePVC(ctx, &vault, &resources)
if err != nil {
return ctrl.Result{}, err
}
resources.mailbox = &lshcore.Mailbox{}
resources.mailbox.Namespace = vault.Namespace
resources.mailbox.Name = vault.Spec.MailboxRef.Name
err = r.Get(ctx, client.ObjectKeyFromObject(resources.mailbox), resources.mailbox)
if err != nil {
return ctrl.Result{}, err
}
lshr.SetDependencyCondition(&vault, resources.postgres, resources.mailbox)
if err := lshr.Patch(ctx, r, patcher, &vault, lshr.PatchOpts{}); err != nil {
return ctrl.Result{}, err
}
if lshr.IsDependencyNotReady(&vault) || lshr.IsImporting(&vault) {
log.Info("Waiting for dependencies or importation")
return ctrl.Result{}, nil
}
_, err = lshr.ReconcileGenericService(ctx, r, &vault)
if err != nil {
return ctrl.Result{}, err
}
err = r.reconcileSecret(ctx, &vault, &resources)
if err != nil {
return ctrl.Result{}, err
}
err = r.reconcileDeployment(ctx, &vault, &resources)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, lshr.Complete(ctx, r, patcher, &vault, lshr.PatchOpts{})
}
// SetupWithManager sets up the controller with the Manager.
func (r *VaultwardenReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&appsv1alpha1.Vaultwarden{}).
Named(r.Name()).
Owns(&lshcore.Postgres{}).
Owns(&appsv1.Deployment{}).
Complete(r)
}
func (r *VaultwardenReconciler) Name() string {
return "vaultwarden-controller"
}
func (r *VaultwardenReconciler) OwnedConditions() []string {
return []string{
meta.ReconcilingCondition,
meta.StalledCondition,
lshmeta.DependenciesNotReady,
lshmeta.HookJobNotComleted,
}
}

View file

@ -0,0 +1,116 @@
// Copyright 2024 IndieHosters.
// SPDX-FileCopyrightText: 2025 IndieHosters <contact@indiehosters.net>
//
// SPDX-License-Identifier: EUPL-1.2
package apps
import (
"context"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
appsv1alpha1 "libre.sh/api/apps/v1alpha1"
lshr "libre.sh/pkg/controller-runtime"
)
func (r *VaultwardenReconciler) reconcileDeployment(ctx context.Context, vault *appsv1alpha1.Vaultwarden, resources *vaultwardenResources) error {
var deploy appsv1.Deployment
lshr.SetResourceNamespacedName(vault, &deploy)
return lshr.CreateOrPatch(ctx, r, &deploy, func() error {
lshr.ApplyLabels(vault, &deploy, nil)
deploy.Spec.Selector = &metav1.LabelSelector{
MatchLabels: lshr.ExtractLabelSelector(&deploy),
}
deploy.Spec.Template.ObjectMeta.Labels = deploy.Labels
deploy.Spec.Template.Spec.TopologySpreadConstraints = []corev1.TopologySpreadConstraint{
{
MaxSkew: 1,
TopologyKey: "kubernetes.io/hostname",
WhenUnsatisfiable: corev1.ScheduleAnyway,
LabelSelector: deploy.Spec.Selector,
},
}
deploy.Spec.Strategy.Type = appsv1.RollingUpdateDeploymentStrategyType
if deploy.Spec.Replicas == nil {
replicas := int32(2)
deploy.Spec.Replicas = &replicas
}
deploy.Spec.Template.Spec.Volumes = []corev1.Volume{
{
Name: "data",
VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: resources.pvc.GetName(),
},
},
},
{
Name: "config",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: lshr.GetResourceName(vault),
},
},
},
}
probeHandler := corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/alive",
Port: intstr.FromString("http"),
},
}
mountPropagation := corev1.MountPropagationHostToContainer
deploy.Spec.Template.Spec.Containers = []corev1.Container{
{
Image: vault.Spec.Image,
ImagePullPolicy: corev1.PullAlways,
Name: "app",
Ports: []corev1.ContainerPort{
{
ContainerPort: 80,
Name: "http",
},
},
ReadinessProbe: &corev1.Probe{
ProbeHandler: probeHandler,
},
LivenessProbe: &corev1.Probe{
ProbeHandler: probeHandler,
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "data",
MountPath: "/data",
MountPropagation: &mountPropagation,
},
{
Name: "config",
MountPath: "/data/config.json",
SubPath: "config.json",
},
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
corev1.ResourceMemory: resource.MustParse("100Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("500Mi"),
},
},
},
}
return controllerutil.SetControllerReference(vault, &deploy, r.Scheme())
})
}

View file

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2023 - 2025 IndieHosters <contact@indiehosters.net>
//
// SPDX-License-Identifier: EUPL-1.2
package apps
import (
"context"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
appsv1alpha1 "libre.sh/api/apps/v1alpha1"
lshr "libre.sh/pkg/controller-runtime"
)
func (r *VaultwardenReconciler) reconcilePVC(ctx context.Context, vault *appsv1alpha1.Vaultwarden, resources *vaultwardenResources) error {
var pvc corev1.PersistentVolumeClaim
resources.pvc = &pvc
lshr.SetResourceNamespacedName(vault, &pvc)
return lshr.CreateOrPatch(ctx, r, &pvc, func() error {
lshr.ApplyLabels(vault, &pvc, nil)
pvc.Spec.AccessModes = []corev1.PersistentVolumeAccessMode{
corev1.ReadWriteMany,
}
storageClassName := "juicefs.libre.sh"
pvc.Spec.StorageClassName = &storageClassName
pvc.Spec.Resources = corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: resource.MustParse("10Gi"),
},
}
return controllerutil.SetControllerReference(vault, &pvc, r.Scheme())
})
}

View file

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2025 IndieHosters <contact@indiehosters.net>
//
// SPDX-License-Identifier: EUPL-1.2
import (
"strings"
"strconv"
)
_vault: {}
_postgres: {}
_mailbox: {}
domain: "https://\(_vault.spec.host)"
database_url: strings.Replace("\(_postgres.url)", "postgres://", "postgresql://", 1)
smtp_from: "\(_mailbox.address)"
smtp_host: "\(_mailbox.host)"
smtp_port: strconv.Atoi("\(_mailbox.port)")
smtp_username: "\(_mailbox.username)"
smtp_password: "\(_mailbox.password)"
smtp_security: string | *"off"
if strconv.ParseBool("\(_mailbox.tls)") {
smtp_security: "starttls"
}
if _vault.spec.config != _|_ {
for k, v in _vault.spec.config {
"\(k)": v
}
}

View file

@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2023 - 2025 IndieHosters <contact@indiehosters.net>
//
// SPDX-License-Identifier: EUPL-1.2
package apps
import (
"context"
_ "embed"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
appsv1alpha1 "libre.sh/api/apps/v1alpha1"
"libre.sh/internal/cue"
lshr "libre.sh/pkg/controller-runtime"
)
//go:embed vaultwarden_controller_secret.cue
var vaultwardenConfigSchema string
func (r *VaultwardenReconciler) reconcileSecret(ctx context.Context, vault *appsv1alpha1.Vaultwarden, resources *vaultwardenResources) error {
var secret corev1.Secret
resources.secret = &secret
lshr.SetResourceNamespacedName(vault, &secret)
return lshr.CreateOrPatch(ctx, r, &secret, func() error {
if secret.Data == nil {
secret.Data = make(map[string][]byte)
}
var pgSecret corev1.Secret
err := r.Get(ctx, types.NamespacedName{
Namespace: vault.Namespace,
Name: resources.postgres.SecretName(),
}, &pgSecret)
if err != nil {
return err
}
var mailboxSecret corev1.Secret
err = r.Get(ctx, types.NamespacedName{
Namespace: vault.Namespace,
Name: resources.mailbox.SecretName(),
}, &mailboxSecret)
if err != nil {
return err
}
config, _, err := cue.GenerateConfig(vaultwardenConfigSchema, cue.FromJSON(secret.Data["config.json"]), cue.ToJSON(),
cue.WithValue("_vault", vault),
cue.WithValue("_postgres", pgSecret.Data),
cue.WithValue("_mailbox", mailboxSecret.Data),
// cue.WithGenerator("forgejo-internal-token", func() interface{} {
// value, _ := generate.NewInternalToken()
// return value
// }),
// cue.WithGenerator("forgejo-jwt-secret", func() interface{} {
// _, value, _ := generate.NewJwtSecret()
// return value
// }),
)
if err != nil {
return err
}
secret.Data["config.json"] = []byte(config)
return controllerutil.SetControllerReference(vault, &secret, r.Scheme())
})
}

View file

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2025 IndieHosters <contact@indiehosters.net>
//
// SPDX-License-Identifier: EUPL-1.2
package apps
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
appsv1alpha1 "libre.sh/api/apps/v1alpha1"
)
var _ = Describe("Vaultwarden Controller", func() {
Context("When reconciling a resource", func() {
const resourceName = "test-resource"
ctx := context.Background()
typeNamespacedName := types.NamespacedName{
Name: resourceName,
Namespace: "default", // TODO(user):Modify as needed
}
vaultwarden := &appsv1alpha1.Vaultwarden{}
BeforeEach(func() {
By("creating the custom resource for the Kind Vaultwarden")
err := k8sClient.Get(ctx, typeNamespacedName, vaultwarden)
if err != nil && errors.IsNotFound(err) {
resource := &appsv1alpha1.Vaultwarden{
ObjectMeta: metav1.ObjectMeta{
Name: resourceName,
Namespace: "default",
},
// TODO(user): Specify other spec details if needed.
}
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
}
})
AfterEach(func() {
// TODO(user): Cleanup logic after each test, like removing the resource instance.
resource := &appsv1alpha1.Vaultwarden{}
err := k8sClient.Get(ctx, typeNamespacedName, resource)
Expect(err).NotTo(HaveOccurred())
By("Cleanup the specific resource instance Vaultwarden")
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
})
It("should successfully reconcile the resource", func() {
By("Reconciling the created resource")
controllerReconciler := &VaultwardenReconciler{
Client: k8sClient,
}
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.
})
})
})