Skip to content

Commit b31436d

Browse files
Merge pull request #19 from PillaiManish/eso-48
ESO-48: Implement the functionality to ensure Certificate, Secret resources stay in desired state
2 parents a6fc76a + 13acfa4 commit b31436d

File tree

13 files changed

+1169
-1
lines changed

13 files changed

+1169
-1
lines changed

api/v1alpha1/external_secrets_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ type BitwardenSecretManagerProvider struct {
143143
// +kubebuilder:validation:Enum:="true";"false"
144144
// +kubebuilder:validation:Optional
145145
Enabled string `json:"enabled,omitempty"`
146+
147+
// SecretRef is the kubernetes secret containing the TLS key pair to be used for the bitwarden server.
148+
// The issuer in CertManagerConfig will be utilized to generate the required certificate if the secret
149+
// reference is not provided and CertManagerConfig is configured. The key names in secret for certificate
150+
// must be `tls.crt`, for private key must be `tls.key` and for CA certificate key name must be `ca.crt`.
151+
// +kubebuilder:validation:Optional
152+
SecretRef SecretReference `json:"secretRef,omitempty"`
146153
}
147154

148155
// WebhookConfig is for configuring external-secrets webhook specifics.

api/v1alpha1/meta.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,10 @@ type ObjectReference struct {
2424
// +optional
2525
Group string `json:"group,omitempty"`
2626
}
27+
28+
// SecretReference is a reference to the secret with the given name, which should exist
29+
// in the same namespace where it will be utilized.
30+
type SecretReference struct {
31+
// Name of the secret resource being referred to.
32+
Name string `json:"name"`
33+
}

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
apiVersion: cert-manager.io/v1
2+
kind: Certificate
3+
metadata:
4+
name: bitwarden-tls-certs
5+
namespace: external-secrets
6+
labels:
7+
app.kubernetes.io/name: bitwarden-tls-certs
8+
app.kubernetes.io/instance: external-secrets
9+
app.kubernetes.io/version: "v0.14.0"
10+
app.kubernetes.io/managed-by: external-secrets-operator
11+
spec:
12+
secretName: bitwarden-tls-certs
13+
dnsNames:
14+
- bitwarden-sdk-server.external-secrets.svc.cluster.local
15+
- external-secrets-bitwarden-sdk-server.external-secrets.svc.cluster.local
16+
- localhost
17+
ipAddresses:
18+
- 127.0.0.1
19+
- ::1
20+
privateKey:
21+
algorithm: RSA
22+
encoding: PKCS8
23+
size: 2048
24+
issuerRef:
25+
group: cert-manager.io
26+
kind: Issuer
27+
name: my-issuer
28+
duration: "8760h"

config/crd/bases/operator.openshift.io_externalsecrets.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,20 @@ spec:
10181018
- "true"
10191019
- "false"
10201020
type: string
1021+
secretRef:
1022+
description: |-
1023+
SecretRef is the kubernetes secret containing the TLS key pair to be used for the bitwarden server.
1024+
The issuer in CertManagerConfig will be utilized to generate the required certificate if the secret
1025+
reference is not provided and CertManagerConfig is configured. The key names in secret for certificate
1026+
must be `tls.crt`, for private key must be `tls.key` and for CA certificate key name must be `ca.crt`.
1027+
properties:
1028+
name:
1029+
description: Name of the secret resource being referred
1030+
to.
1031+
type: string
1032+
required:
1033+
- name
1034+
type: object
10211035
type: object
10221036
logLevel:
10231037
default: 1

pkg/controller/certificate.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package controller
2+
3+
import (
4+
"fmt"
5+
"sigs.k8s.io/controller-runtime/pkg/client"
6+
"strings"
7+
8+
corev1 "k8s.io/api/core/v1"
9+
"k8s.io/apimachinery/pkg/types"
10+
11+
certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
12+
v1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
13+
14+
operatorv1alpha1 "github.com/openshift/external-secrets-operator/api/v1alpha1"
15+
"github.com/openshift/external-secrets-operator/pkg/operator/assets"
16+
)
17+
18+
var (
19+
serviceExternalSecretWebhookName string = "external-secrets-webhook"
20+
)
21+
22+
func (r *ExternalSecretsReconciler) createOrApplyCertificates(es *operatorv1alpha1.ExternalSecrets, resourceLabels map[string]string, recon bool) error {
23+
if isCertManagerConfigEnabled(es) {
24+
if err := r.createOrApplyCertificate(es, resourceLabels, webhookCertificateAssetName, recon); err != nil {
25+
return err
26+
}
27+
}
28+
29+
if isBitwardenConfigEnabled(es) {
30+
bitwardenConfig := es.Spec.ExternalSecretsConfig.BitwardenSecretManagerProvider
31+
if bitwardenConfig.SecretRef.Name != "" {
32+
return r.assertSecretRefExists(es, es.Spec.ExternalSecretsConfig.BitwardenSecretManagerProvider)
33+
}
34+
if err := r.createOrApplyCertificate(es, resourceLabels, bitwardenCertificateAssetName, recon); err != nil {
35+
return err
36+
}
37+
}
38+
return nil
39+
}
40+
41+
func (r *ExternalSecretsReconciler) createOrApplyCertificate(es *operatorv1alpha1.ExternalSecrets, resourceLabels map[string]string, fileName string, recon bool) error {
42+
desired, err := r.getCertificateObject(es, resourceLabels, fileName)
43+
if err != nil {
44+
return err
45+
}
46+
47+
certificateName := fmt.Sprintf("%s/%s", desired.GetNamespace(), desired.GetName())
48+
r.log.V(4).Info("reconciling certificate resource", "name", certificateName)
49+
fetched := &certmanagerv1.Certificate{}
50+
key := types.NamespacedName{
51+
Name: desired.GetName(),
52+
Namespace: desired.GetNamespace(),
53+
}
54+
exist, err := r.Exists(r.ctx, key, fetched)
55+
if err != nil {
56+
return FromClientError(err, "failed to check %s certificate resource already exists", certificateName)
57+
}
58+
59+
if exist && recon {
60+
r.eventRecorder.Eventf(es, corev1.EventTypeWarning, "ResourceAlreadyExists", "%s certificate resource already exists, maybe from previous installation", certificateName)
61+
}
62+
if exist && hasObjectChanged(desired, fetched) {
63+
r.log.V(1).Info("certificate has been modified, updating to desired state", "name", certificateName)
64+
if err := r.UpdateWithRetry(r.ctx, desired); err != nil {
65+
return FromClientError(err, "failed to update %s certificate resource", certificateName)
66+
}
67+
r.eventRecorder.Eventf(es, corev1.EventTypeNormal, "Reconciled", "certificate resource %s reconciled back to desired state", certificateName)
68+
} else {
69+
r.log.V(4).Info("certificate resource already exists and is in expected state", "name", certificateName)
70+
}
71+
if !exist {
72+
if err := r.Create(r.ctx, desired); err != nil {
73+
return FromClientError(err, "failed to create %s certificate resource", certificateName)
74+
}
75+
r.eventRecorder.Eventf(es, corev1.EventTypeNormal, "Reconciled", "certificate resource %s created", certificateName)
76+
}
77+
78+
return nil
79+
}
80+
81+
func (r *ExternalSecretsReconciler) getCertificateObject(es *operatorv1alpha1.ExternalSecrets, resourceLabels map[string]string, fileName string) (*certmanagerv1.Certificate, error) {
82+
certificate := decodeCertificateObjBytes(assets.MustAsset(fileName))
83+
84+
updateNamespace(certificate, es)
85+
updateResourceLabels(certificate, resourceLabels)
86+
87+
if err := r.updateCertificateParams(es, certificate); err != nil {
88+
return nil, NewIrrecoverableError(err, "failed to update certificate resource for %s/%s deployment", getNamespace(es), es.GetName())
89+
}
90+
91+
return certificate, nil
92+
}
93+
94+
func (r *ExternalSecretsReconciler) updateCertificateParams(es *operatorv1alpha1.ExternalSecrets, certificate *certmanagerv1.Certificate) error {
95+
certManageConfig := es.Spec.ExternalSecretsConfig.WebhookConfig.CertManagerConfig
96+
externalSecretsNamespace := getNamespace(es)
97+
98+
if certManageConfig.IssuerRef.Name == "" {
99+
return fmt.Errorf("issuerRef.Name not present")
100+
}
101+
102+
certificate.Spec.IssuerRef = v1.ObjectReference{
103+
Name: certManageConfig.IssuerRef.Name,
104+
Kind: certManageConfig.IssuerRef.Kind,
105+
Group: certManageConfig.IssuerRef.Group,
106+
}
107+
108+
// Since Kind and Group configs are optional. certmanagerv1.IssuerKind will
109+
// be used as default for Kind and certmanagerapi.GroupName as default for
110+
// Group.
111+
if certificate.Spec.IssuerRef.Kind == "" {
112+
certificate.Spec.IssuerRef.Kind = issuerKind
113+
}
114+
if certificate.Spec.IssuerRef.Group == "" {
115+
certificate.Spec.IssuerRef.Group = issuerGroup
116+
}
117+
118+
if err := r.assertIssuerRefExists(certificate.Spec.IssuerRef, externalSecretsNamespace); err != nil {
119+
return err
120+
}
121+
122+
certificate.Spec.DNSNames = updateNamespaceForFQDN(certificate.Spec.DNSNames, externalSecretsNamespace)
123+
124+
if certManageConfig.CertificateRenewBefore != nil {
125+
certificate.Spec.RenewBefore = certManageConfig.CertificateRenewBefore
126+
}
127+
128+
if certManageConfig.CertificateDuration != nil {
129+
certificate.Spec.Duration = certManageConfig.CertificateDuration
130+
}
131+
132+
return nil
133+
}
134+
135+
func (r *ExternalSecretsReconciler) assertIssuerRefExists(issueRef v1.ObjectReference, namespace string) error {
136+
ifExists, err := r.getIssuer(issueRef, namespace)
137+
if err != nil || !ifExists {
138+
return FromClientError(err, "failed to fetch issuer")
139+
}
140+
return nil
141+
}
142+
143+
func (r *ExternalSecretsReconciler) assertSecretRefExists(es *operatorv1alpha1.ExternalSecrets, bitwardenConfig *operatorv1alpha1.BitwardenSecretManagerProvider) error {
144+
namespacedName := types.NamespacedName{
145+
Name: bitwardenConfig.SecretRef.Name,
146+
Namespace: getNamespace(es),
147+
}
148+
object := &corev1.Secret{}
149+
150+
if err := r.Get(r.ctx, namespacedName, object); err != nil {
151+
return fmt.Errorf("failed to fetch %q secret: %w", namespacedName, err)
152+
}
153+
154+
return nil
155+
}
156+
157+
func (r *ExternalSecretsReconciler) getIssuer(issuerRef v1.ObjectReference, namespace string) (bool, error) {
158+
namespacedName := types.NamespacedName{
159+
Name: issuerRef.Name,
160+
Namespace: namespace,
161+
}
162+
163+
var object client.Object
164+
switch strings.ToLower(issuerRef.Kind) {
165+
case clusterIssuerKind:
166+
object = &certmanagerv1.ClusterIssuer{}
167+
case issuerKind:
168+
object = &certmanagerv1.Issuer{}
169+
}
170+
171+
if ifExists, err := r.Exists(r.ctx, namespacedName, object); err != nil {
172+
return ifExists, fmt.Errorf("failed to fetch %q issuer: %w", namespacedName, err)
173+
} else {
174+
return ifExists, nil
175+
}
176+
}
177+
178+
func updateNamespaceForFQDN(fqdns []string, namespace string) []string {
179+
updated := make([]string, 0, len(fqdns))
180+
for _, fqdn := range fqdns {
181+
parts := strings.Split(fqdn, ".")
182+
// DNSNames for kubernetes service will be of the form
183+
// <service-name>.<service-namespace>.svc.<cluster-domain>
184+
if len(parts) >= 2 {
185+
parts[1] = namespace
186+
updated = append(updated, strings.Join(parts, "."))
187+
} else {
188+
updated = append(updated, fqdn)
189+
}
190+
}
191+
return updated
192+
}

0 commit comments

Comments
 (0)