mirror of
https://forge.liiib.re/indiehost/libre.sh/libre.sh.git
synced 2025-01-14 22:43:08 +00:00
This commit is contained in:
parent
d2452df888
commit
a590a16f2b
19 changed files with 922 additions and 1 deletions
9
PROJECT
9
PROJECT
|
@ -304,4 +304,13 @@ resources:
|
|||
kind: Wikijs
|
||||
path: libre.sh/api/apps/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"
|
||||
|
|
67
api/apps/v1alpha1/vaultwarden_types.go
Normal file
67
api/apps/v1alpha1/vaultwarden_types.go
Normal 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{})
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
//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
|
||||
|
||||
|
@ -1265,6 +1265,103 @@ func (in *StaticWebsiteStatus) DeepCopy() *StaticWebsiteStatus {
|
|||
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.
|
||||
func (in *Wikijs) DeepCopyInto(out *Wikijs) {
|
||||
*out = *in
|
||||
|
|
39
api/apps/v1alpha1/zz_generated.vaultwarden_lsh.go
Normal file
39
api/apps/v1alpha1/zz_generated.vaultwarden_lsh.go
Normal 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
|
||||
}
|
|
@ -354,6 +354,12 @@ func main() {
|
|||
setupLog.Error(err, "unable to create controller", "controller", "Wikijs")
|
||||
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
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
|
|
159
config/crd/bases/apps.libre.sh_vaultwardens.yaml
Normal file
159
config/crd/bases/apps.libre.sh_vaultwardens.yaml
Normal 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: {}
|
|
@ -35,6 +35,7 @@ resources:
|
|||
- bases/apps.libre.sh_listmonks.yaml
|
||||
- bases/apps.libre.sh_staticwebsites.yaml
|
||||
- bases/apps.libre.sh_wikijs.yaml
|
||||
- bases/apps.libre.sh_vaultwardens.yaml
|
||||
#+kubebuilder:scaffold:crdkustomizeresource
|
||||
patches:
|
||||
|
||||
|
|
27
config/rbac/apps_vaultwarden_editor_role.yaml
Normal file
27
config/rbac/apps_vaultwarden_editor_role.yaml
Normal 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
|
23
config/rbac/apps_vaultwarden_viewer_role.yaml
Normal file
23
config/rbac/apps_vaultwarden_viewer_role.yaml
Normal 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
|
|
@ -30,3 +30,5 @@ resources:
|
|||
- matrix_elementcall_viewer_role.yaml
|
||||
- voip_livekitserver_editor_role.yaml
|
||||
- voip_livekitserver_viewer_role.yaml
|
||||
- apps_vaultwarden_viewer_role.yaml
|
||||
- apps_vaultwarden_editor_role.yaml
|
||||
|
|
|
@ -404,6 +404,32 @@ rules:
|
|||
- get
|
||||
- patch
|
||||
- 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:
|
||||
- apps.libre.sh
|
||||
resources:
|
||||
|
|
13
config/samples/apps_v1alpha1_vaultwarden.yaml
Normal file
13
config/samples/apps_v1alpha1_vaultwarden.yaml
Normal 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: {}
|
|
@ -33,4 +33,5 @@ resources:
|
|||
- apps_v1alpha1_listmonk.yaml
|
||||
- apps_v1alpha1_staticwebsite.yaml
|
||||
- apps_v1alpha1_wikijs.yaml
|
||||
- apps_v1alpha1_vaultwarden.yaml
|
||||
#+kubebuilder:scaffold:manifestskustomizesamples
|
||||
|
|
128
internal/controller/apps/vaultwarden_controller.go
Normal file
128
internal/controller/apps/vaultwarden_controller.go
Normal 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,
|
||||
}
|
||||
}
|
116
internal/controller/apps/vaultwarden_controller_deploy.go
Normal file
116
internal/controller/apps/vaultwarden_controller_deploy.go
Normal 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())
|
||||
})
|
||||
}
|
36
internal/controller/apps/vaultwarden_controller_pvc.go
Normal file
36
internal/controller/apps/vaultwarden_controller_pvc.go
Normal 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())
|
||||
})
|
||||
}
|
30
internal/controller/apps/vaultwarden_controller_secret.cue
Normal file
30
internal/controller/apps/vaultwarden_controller_secret.cue
Normal 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
|
||||
}
|
||||
}
|
70
internal/controller/apps/vaultwarden_controller_secret.go
Normal file
70
internal/controller/apps/vaultwarden_controller_secret.go
Normal 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())
|
||||
})
|
||||
}
|
71
internal/controller/apps/vaultwarden_controller_test.go
Normal file
71
internal/controller/apps/vaultwarden_controller_test.go
Normal 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.
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue