diff --git a/Makefile b/Makefile index 1e0e902..3914c63 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Image URL to use all building/pushing image targets -IMG ?= erwinvaneyk/ca-completer:latest +IMG ?= erwinvaneyk/cert-completer:latest # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) CRD_OPTIONS ?= "crd:trivialVersions=true" @@ -45,6 +45,7 @@ vet: # Generate code generate: controller-gen $(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths="./..." + ./hack/generate-k8s-resources.sh # Build the docker image docker-build: test diff --git a/PROJECT b/PROJECT index c5ecbf5..35c8b93 100644 --- a/PROJECT +++ b/PROJECT @@ -1,3 +1,3 @@ version: "2" domain: erwinvaneyk.nl -repo: github.com/erwinvaneyk/ca-completer +repo: github.com/erwinvaneyk/cert-completer diff --git a/cert-completer.yaml b/cert-completer.yaml new file mode 100644 index 0000000..f9c4f4a --- /dev/null +++ b/cert-completer.yaml @@ -0,0 +1,155 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: cert-completer-leader-election-role + namespace: default +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: cert-completer-manager-role +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - patch + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cert-completer-proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: cert-completer-leader-election-rolebinding + namespace: default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: cert-completer-leader-election-role +subjects: +- kind: ServiceAccount + name: default + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: cert-completer-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cert-completer-manager-role +subjects: +- kind: ServiceAccount + name: default + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: cert-completer-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cert-completer-proxy-role +subjects: +- kind: ServiceAccount + name: default + namespace: default +--- +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: cert-completer-controller-manager-metrics-service + namespace: default +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + control-plane: controller-manager + name: cert-completer-controller-manager + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --enable-leader-election + command: + - /manager + image: erwinvaneyk/cert-completer:latest + name: manager + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + terminationGracePeriodSeconds: 10 diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 9483710..bae8da0 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -1,12 +1,12 @@ # Adds namespace to all resources. -namespace: ca-completer-system +namespace: default # Value of this field is prepended to the # names of all resources, e.g. a deployment named # "wordpress" becomes "alices-wordpress". # Note that it should also match with the prefix (text before '-') of the namespace # field above. -namePrefix: ca-completer- +namePrefix: cert-completer- # Labels to add to all resources and selectors. #commonLabels: diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 2599c61..2cb959a 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -4,5 +4,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller - newName: erwinvaneyk/ca-completer + newName: erwinvaneyk/cert-completer newTag: latest diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index b6c85a5..0e095ec 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -1,10 +1,3 @@ -apiVersion: v1 -kind: Namespace -metadata: - labels: - control-plane: controller-manager - name: system ---- apiVersion: apps/v1 kind: Deployment metadata: diff --git a/controllers/controller.go b/controllers/controller.go index 785a946..fa8ce76 100644 --- a/controllers/controller.go +++ b/controllers/controller.go @@ -17,7 +17,13 @@ import ( const ErrInvalidCertChain = "failed to parse certificate chain in tls.crt" -type CACompleter struct { +// CertCompleter parses the TLS certificate chain in a secret with an empty +// ca.tls, and updates the secret with the last (top-most) certificate in this +// chain as the ca.crt. +// +// Although this does not guarantee that ca.crt contains a root CA, it does +// guarantee that the CA present is valid for the TLS secret. +type CertCompleter struct { client.Client Log logr.Logger Scheme *runtime.Scheme @@ -25,7 +31,7 @@ type CACompleter struct { // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;update;patch -func (c *CACompleter) Reconcile(req reconcile.Request) (reconcile.Result, error) { +func (c *CertCompleter) Reconcile(req reconcile.Request) (reconcile.Result, error) { ctx := context.Background() log := c.Log.WithValues("secret", req.NamespacedName.String()) @@ -50,11 +56,10 @@ func (c *CACompleter) Reconcile(req reconcile.Request) (reconcile.Result, error) log.Info("Updated the ca.crt of the TLS secret.") } - return reconcile.Result{}, nil } -func (c *CACompleter) SetupWithManager(mgr ctrl.Manager) error { +func (c *CertCompleter) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&corev1.Secret{}). Complete(c) @@ -64,7 +69,7 @@ func (c *CACompleter) SetupWithManager(mgr ctrl.Manager) error { // // If the secret was updated, the updated result is returned. Otherwise, if // the secret was not updated, the return value is nil. -func (c *CACompleter) reconcileSecret(secret *corev1.Secret) (updated *corev1.Secret, err error){ +func (c *CertCompleter) reconcileSecret(secret *corev1.Secret) (updated *corev1.Secret, err error) { log := c.Log.WithValues("secret", fmt.Sprintf("%s/%s", secret.Namespace, secret.Name)) log.Info("Evaluating secret...") diff --git a/controllers/controller_test.go b/controllers/controller_test.go index cedf14f..1fd4012 100644 --- a/controllers/controller_test.go +++ b/controllers/controller_test.go @@ -10,8 +10,8 @@ import ( ) // It should reconcile valid TLS secrets. -func TestCACompleter_reconcileSecret_certChain(t *testing.T) { - ctrl := CACompleter{ +func TestCertCompleter_reconcileSecret_certChain(t *testing.T) { + ctrl := CertCompleter{ Log: zap.New(), } @@ -21,13 +21,13 @@ func TestCACompleter_reconcileSecret_certChain(t *testing.T) { APIVersion: corev1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", + Name: "test-secret", Namespace: "test-namespace", }, - Type: corev1.SecretTypeTLS, - Data: map[string][]byte{ - "ca.crt": nil, - "tls.crt": []byte(strings.Join(certs, "")), + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + "ca.crt": nil, + "tls.crt": []byte(strings.Join(certs, "")), }, } @@ -39,8 +39,8 @@ func TestCACompleter_reconcileSecret_certChain(t *testing.T) { } // It should reconcile valid TLS secrets with just one certificate in the chain. -func TestCACompleter_reconcileSecret_singleCert(t *testing.T) { - ctrl := CACompleter{ +func TestCertCompleter_reconcileSecret_singleCert(t *testing.T) { + ctrl := CertCompleter{ Log: zap.New(), } @@ -50,12 +50,12 @@ func TestCACompleter_reconcileSecret_singleCert(t *testing.T) { APIVersion: corev1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", + Name: "test-secret", Namespace: "test-namespace", }, - Type: corev1.SecretTypeTLS, - Data: map[string][]byte{ - "ca.crt": nil, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + "ca.crt": nil, "tls.crt": []byte(certs[0]), }, } @@ -68,8 +68,8 @@ func TestCACompleter_reconcileSecret_singleCert(t *testing.T) { } // It should ignore non-TLS secrets. -func TestCACompleter_reconcileSecret_nonTLS(t *testing.T) { - ctrl := CACompleter{ +func TestCertCompleter_reconcileSecret_nonTLS(t *testing.T) { + ctrl := CertCompleter{ Log: zap.New(), } @@ -79,12 +79,12 @@ func TestCACompleter_reconcileSecret_nonTLS(t *testing.T) { APIVersion: corev1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", + Name: "test-secret", Namespace: "test-namespace", }, - Type: corev1.SecretTypeOpaque, - Data: map[string][]byte{ - "ca.crt": nil, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "ca.crt": nil, "tls.crt": []byte(strings.Join(certs, "")), }, } @@ -95,8 +95,8 @@ func TestCACompleter_reconcileSecret_nonTLS(t *testing.T) { } // It should ignore complete TLS secrets (that already have a CA). -func TestCACompleter_reconcileSecret_completeSecret(t *testing.T) { - ctrl := CACompleter{ +func TestCertCompleter_reconcileSecret_completeSecret(t *testing.T) { + ctrl := CertCompleter{ Log: zap.New(), } @@ -106,12 +106,12 @@ func TestCACompleter_reconcileSecret_completeSecret(t *testing.T) { APIVersion: corev1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", + Name: "test-secret", Namespace: "test-namespace", }, - Type: corev1.SecretTypeTLS, - Data: map[string][]byte{ - "ca.crt": []byte(certs[len(certs) - 1]), + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + "ca.crt": []byte(certs[len(certs)-1]), "tls.crt": []byte(strings.Join(certs, "")), }, } @@ -121,10 +121,9 @@ func TestCACompleter_reconcileSecret_completeSecret(t *testing.T) { assert.Empty(t, updatedSecret) } - // It should ignore empty TLS secrets. -func TestCACompleter_reconcileSecret_emptySecret(t *testing.T) { - ctrl := CACompleter{ +func TestCertCompleter_reconcileSecret_emptySecret(t *testing.T) { + ctrl := CertCompleter{ Log: zap.New(), } @@ -134,12 +133,12 @@ func TestCACompleter_reconcileSecret_emptySecret(t *testing.T) { APIVersion: corev1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", + Name: "test-secret", Namespace: "test-namespace", }, - Type: corev1.SecretTypeTLS, - Data: map[string][]byte{ - "ca.crt": nil, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + "ca.crt": nil, "tls.crt": nil, }, } @@ -150,8 +149,8 @@ func TestCACompleter_reconcileSecret_emptySecret(t *testing.T) { } // It should error on an invalid TLS secret. -func TestCACompleter_reconcileSecret_invalidCert(t *testing.T) { - ctrl := CACompleter{ +func TestCertCompleter_reconcileSecret_invalidCert(t *testing.T) { + ctrl := CertCompleter{ Log: zap.New(), } @@ -161,12 +160,12 @@ func TestCACompleter_reconcileSecret_invalidCert(t *testing.T) { APIVersion: corev1.SchemeGroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ - Name: "test-secret", + Name: "test-secret", Namespace: "test-namespace", }, - Type: corev1.SecretTypeTLS, - Data: map[string][]byte{ - "ca.crt": nil, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + "ca.crt": nil, "tls.crt": []byte(certs[0][:len(certs[0])-100]), // partial cert }, } @@ -237,4 +236,4 @@ X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6 KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg== -----END CERTIFICATE----- -`} \ No newline at end of file +`} diff --git a/go.mod b/go.mod index 2cb210e..958505c 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/erwinvaneyk/ca-completer +module github.com/erwinvaneyk/cert-completer go 1.13 diff --git a/hack/generate-k8s-resources.sh b/hack/generate-k8s-resources.sh new file mode 100755 index 0000000..945d306 --- /dev/null +++ b/hack/generate-k8s-resources.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +# generate-k8s-resources.sh - prebuild k8s resources + +set -o errexit +set -o nounset +set -o pipefail + +ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"; + +kustomize build "${ROOT}/../config/default" > "${ROOT}/../cert-completer.yaml" \ No newline at end of file diff --git a/main.go b/main.go index c07dccb..5d5e8af 100644 --- a/main.go +++ b/main.go @@ -17,7 +17,7 @@ package main import ( "flag" - "github.com/erwinvaneyk/ca-completer/controllers" + "github.com/erwinvaneyk/cert-completer/controllers" "os" "k8s.io/apimachinery/pkg/runtime" @@ -62,11 +62,11 @@ func main() { os.Exit(1) } - if err = (&controllers.CACompleter{ + if err = (&controllers.CertCompleter{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers"), }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "CACompleter") + setupLog.Error(err, "unable to create controller", "controller", "CertCompleter") os.Exit(1) } // +kubebuilder:scaffold:builder