2 Commits

Author SHA1 Message Date
84f338426e first controller implementation 2025-03-19 22:36:42 +01:00
ce3a6dfc6d crd matching for bitwardenSecret 2025-03-19 22:27:44 +01:00
7 changed files with 180 additions and 13 deletions

View File

@ -28,11 +28,19 @@ import (
// BitwardenSecretSpec defines the desired state of BitwardenSecret. // BitwardenSecretSpec defines the desired state of BitwardenSecret.
type BitwardenSecretSpec struct { type BitwardenSecretSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Content []Element `json:"content"`
// Important: Run "make" to regenerate code after modifying this file ID string `json:"id"`
Name string `json:"name"`
Namespace string `json:"namespace,omitempty"`
SecretType string `json:"secretType,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
}
// Foo is an example field of BitwardenSecret. Edit bitwardensecret_types.go to remove/update type Element struct {
Foo string `json:"foo,omitempty"` SecretName string `json:"secretName"`
SecretRef string `json:"secretRef"`
SecretScope string `json:"secretScope"`
} }
// BitwardenSecretStatus defines the observed state of BitwardenSecret. // BitwardenSecretStatus defines the observed state of BitwardenSecret.

View File

@ -33,7 +33,7 @@ func (in *BitwardenSecret) DeepCopyInto(out *BitwardenSecret) {
*out = *in *out = *in
out.TypeMeta = in.TypeMeta out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec in.Spec.DeepCopyInto(&out.Spec)
out.Status = in.Status out.Status = in.Status
} }
@ -90,6 +90,25 @@ func (in *BitwardenSecretList) DeepCopyObject() runtime.Object {
// 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 *BitwardenSecretSpec) DeepCopyInto(out *BitwardenSecretSpec) { func (in *BitwardenSecretSpec) DeepCopyInto(out *BitwardenSecretSpec) {
*out = *in *out = *in
if in.Content != nil {
in, out := &in.Content, &out.Content
*out = make([]Element, len(*in))
copy(*out, *in)
}
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BitwardenSecretSpec. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BitwardenSecretSpec.
@ -206,6 +225,21 @@ func (in *BitwardenTemplateStatus) DeepCopy() *BitwardenTemplateStatus {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Element) DeepCopyInto(out *Element) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Element.
func (in *Element) DeepCopy() *Element {
if in == nil {
return nil
}
out := new(Element)
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 *RegistryCredential) DeepCopyInto(out *RegistryCredential) { func (in *RegistryCredential) DeepCopyInto(out *RegistryCredential) {
*out = *in *out = *in

View File

@ -39,10 +39,41 @@ spec:
spec: spec:
description: BitwardenSecretSpec defines the desired state of BitwardenSecret. description: BitwardenSecretSpec defines the desired state of BitwardenSecret.
properties: properties:
foo: annotations:
description: Foo is an example field of BitwardenSecret. Edit bitwardensecret_types.go additionalProperties:
to remove/update type: string
type: object
content:
items:
properties:
secretName:
type: string
secretRef:
type: string
secretScope:
type: string
required:
- secretName
- secretRef
- secretScope
type: object
type: array
id:
type: string type: string
labels:
additionalProperties:
type: string
type: object
name:
type: string
namespace:
type: string
secretType:
type: string
required:
- content
- id
- name
type: object type: object
status: status:
description: BitwardenSecretStatus defines the observed state of BitwardenSecret. description: BitwardenSecretStatus defines the observed state of BitwardenSecret.

View File

@ -7,7 +7,6 @@ rules:
- apiGroups: - apiGroups:
- lerentis.uploadfilter24.eu.lerentis.uploadfilter24.eu - lerentis.uploadfilter24.eu.lerentis.uploadfilter24.eu
resources: resources:
- bitwardensecrets
- bitwardentemplates - bitwardentemplates
- registrycredentials - registrycredentials
verbs: verbs:
@ -21,7 +20,6 @@ rules:
- apiGroups: - apiGroups:
- lerentis.uploadfilter24.eu.lerentis.uploadfilter24.eu - lerentis.uploadfilter24.eu.lerentis.uploadfilter24.eu
resources: resources:
- bitwardensecrets/finalizers
- bitwardentemplates/finalizers - bitwardentemplates/finalizers
- registrycredentials/finalizers - registrycredentials/finalizers
verbs: verbs:
@ -29,7 +27,6 @@ rules:
- apiGroups: - apiGroups:
- lerentis.uploadfilter24.eu.lerentis.uploadfilter24.eu - lerentis.uploadfilter24.eu.lerentis.uploadfilter24.eu
resources: resources:
- bitwardensecrets/status
- bitwardentemplates/status - bitwardentemplates/status
- registrycredentials/status - registrycredentials/status
verbs: verbs:

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.23.0
godebug default=go1.23 godebug default=go1.23
require ( require (
github.com/bitwarden/sdk-go v1.0.2
github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/ginkgo/v2 v2.22.0
github.com/onsi/gomega v1.36.1 github.com/onsi/gomega v1.36.1
k8s.io/apimachinery v0.32.1 k8s.io/apimachinery v0.32.1

2
go.sum
View File

@ -6,6 +6,8 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bitwarden/sdk-go v1.0.2 h1:krk5et4sfksLDDcrYHcs8f3jL/TGcQ1EShw4CG21JSI=
github.com/bitwarden/sdk-go v1.0.2/go.mod h1:RuYh+gqffp3h8wNUVWz1bvp2Pho10AFz+WIlI26iWY4=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=

View File

@ -21,13 +21,19 @@ package controller
import ( import (
"context" "context"
"encoding/base64"
"os"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log"
"github.com/bitwarden/sdk-go"
lerentisuploadfilter24euv1 "github.com/lerentis/bitwarden-crd-operator/api/v1" lerentisuploadfilter24euv1 "github.com/lerentis/bitwarden-crd-operator/api/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
// BitwardenSecretReconciler reconciles a BitwardenSecret object // BitwardenSecretReconciler reconciles a BitwardenSecret object
@ -50,9 +56,97 @@ type BitwardenSecretReconciler struct {
// For more details, check Reconcile and its Result here: // For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/reconcile // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/reconcile
func (r *BitwardenSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { func (r *BitwardenSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx) olog := log.FromContext(ctx)
// TODO(user): your logic here // Fetch the BitwardenSecret instance
var bitwardenSecret lerentisuploadfilter24euv1.BitwardenSecret
if err := r.Get(ctx, req.NamespacedName, &bitwardenSecret); err != nil {
if errors.IsNotFound(err) {
olog.Info("BitwardenSecret resource not found. Ignoring since object must be deleted.")
return ctrl.Result{}, nil
}
olog.Error(err, "Failed to get BitwardenSecret.")
return ctrl.Result{}, err
}
// Fetch secrets from Bitwarden
apiURL := os.Getenv("API_URL")
identityURL := os.Getenv("IDENTITY_URL")
client, _ := sdk.NewBitwardenClient(&apiURL, &identityURL)
accessToken := os.Getenv("ACCESS_TOKEN")
stateFile := os.Getenv("STATE_FILE")
err := client.AccessTokenLogin(accessToken, &stateFile)
if err != nil {
olog.Error(err, "Failed to authenticate with Bitwarden.")
return ctrl.Result{}, err
}
secretData := make(map[string][]byte)
for _, element := range bitwardenSecret.Spec.Content {
resp, err := client.Secrets().Get(bitwardenSecret.Spec.ID)
if err != nil {
olog.Error(err, "Failed to fetch item from Bitwarden.")
return ctrl.Result{}, err
}
item := resp.Value
var secretValue string
switch element.SecretScope {
case "login":
if element.SecretName == "username" {
secretValue = item.Login.Username
} else if element.SecretName == "password" {
secretValue = item.Login.Password
}
case "fields":
for _, field := range item.Fields {
if field.Name == element.SecretName {
secretValue = field.Value
break
}
}
case "attachment":
for _, attachment := range item.Attachments {
if attachment.FileName == element.SecretName {
secretValue = attachment.File
break
}
}
}
if secretValue != "" {
secretData[element.SecretRef] = []byte(base64.StdEncoding.EncodeToString([]byte(secretValue)))
}
}
// Create or update Kubernetes secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: bitwardenSecret.Spec.Name,
Namespace: bitwardenSecret.Spec.Namespace,
Labels: bitwardenSecret.Spec.Labels,
Annotations: bitwardenSecret.Spec.Annotations,
},
Data: secretData,
Type: corev1.SecretType(bitwardenSecret.Spec.SecretType),
}
if err := r.Client.Create(ctx, secret); err != nil && !errors.IsAlreadyExists(err) {
olog.Error(err, "Failed to create Secret.")
return ctrl.Result{}, err
}
if errors.IsAlreadyExists(err) {
if err := r.Client.Update(ctx, secret); err != nil {
olog.Error(err, "Failed to update Secret.")
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }