diff --git a/api/v1beta1/stepclusterissuer_types.go b/api/v1beta1/stepclusterissuer_types.go index 5f3e56145876d964525bc28101d108ebc3c05db7..9ae71741eb7e0d4474306e6ed6ec9b768300eea1 100644 --- a/api/v1beta1/stepclusterissuer_types.go +++ b/api/v1beta1/stepclusterissuer_types.go @@ -22,19 +22,33 @@ import ( // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +func init() { + SchemeBuilder.Register(&StepClusterIssuer{}, &StepClusterIssuerList{}) +} + // StepClusterIssuerSpec defines the desired state of StepClusterIssuer type StepClusterIssuerSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file - // Foo is an example field of StepClusterIssuer. Edit StepClusterIssuer_types.go to remove/update - Foo string `json:"foo,omitempty"` + // URL is the base URL for the step certificates instance. + URL string `json:"url"` + + // Provisioner contains the step certificates provisioner configuration. + Provisioner StepProvisioner `json:"provisioner"` + + // CABundle is a base64 encoded TLS certificate used to verify connections + // to the step certificates server. If not set the system root certificates + // are used to validate the TLS connection. + // +optional + CABundle []byte `json:"caBundle,omitempty"` } // StepClusterIssuerStatus defines the observed state of StepClusterIssuer type StepClusterIssuerStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file + Conditions []StepClusterIssuerCondition `json:"conditions,omitempty"` } // +kubebuilder:object:root=true @@ -58,6 +72,80 @@ type StepClusterIssuerList struct { Items []StepClusterIssuer `json:"items"` } -func init() { - SchemeBuilder.Register(&StepClusterIssuer{}, &StepClusterIssuerList{}) +// SecretKeySelector contains the reference to a secret. +type SecretKeySelector struct { + // The name of the secret in the pod's namespace to select from. + Name string `json:"name"` + + // The key of the secret to select from. Must be a valid secret key. + // +optional + Key string `json:"key,omitempty"` +} + +// StepProvisioner contains the configuration used to create step certificate +// tokens used to grant certificates. +type StepProvisioner struct { + // Names is the name of the JWK provisioner. + Name string `json:"name"` + + // KeyID is the kid property of the JWK provisioner. + KeyID string `json:"kid"` + + // PasswordRef is a reference to a Secret containing the provisioner + // password used to decrypt the provisioner private key. + PasswordRef SecretKeySelector `json:"passwordRef"` } + +// ConditionType represents a StepClusterIssuer condition type. +// +kubebuilder:validation:Enum=Ready +type ConditionType string + +const ( + // ConditionReady indicates that a StepClusterIssuer is ready for use. + ConditionReady ConditionType = "Ready" +) + +// ConditionStatus represents a condition's status. +// +kubebuilder:validation:Enum=True;False;Unknown +type ConditionStatus string + +// These are valid condition statuses. "ConditionTrue" means a resource is in +// the condition; "ConditionFalse" means a resource is not in the condition; +// "ConditionUnknown" means kubernetes can't decide if a resource is in the +// condition or not. In the future, we could add other intermediate +// conditions, e.g. ConditionDegraded. +const ( + // ConditionTrue represents the fact that a given condition is true + ConditionTrue ConditionStatus = "True" + + // ConditionFalse represents the fact that a given condition is false + ConditionFalse ConditionStatus = "False" + + // ConditionUnknown represents the fact that a given condition is unknown + ConditionUnknown ConditionStatus = "Unknown" +) + +// StepClusterIssuerCondition contains condition information for the step issuer. +type StepClusterIssuerCondition struct { + // Type of the condition, currently ('Ready'). + Type ConditionType `json:"type"` + + // Status of the condition, one of ('True', 'False', 'Unknown'). + // +kubebuilder:validation:Enum=True;False;Unknown + Status ConditionStatus `json:"status"` + + // LastTransitionTime is the timestamp corresponding to the last status + // change of this condition. + // +optional + LastTransitionTime *metav1.Time `json:"lastTransitionTime,omitempty"` + + // Reason is a brief machine readable explanation for the condition's last + // transition. + // +optional + Reason string `json:"reason,omitempty"` + + // Message is a human readable description of the details of the last + // transition, complementing reason. + // +optional + Message string `json:"message,omitempty"` +} \ No newline at end of file diff --git a/config/rbac/cert_manager_controller_approver_clusterrole.yaml b/config/rbac/cert_manager_controller_approver_clusterrole.yaml index c8ab809b568db3837a63ac80e7538e9bc19cf449..3c1f04468801c1c5738913d48f71addff29f52de 100644 --- a/config/rbac/cert_manager_controller_approver_clusterrole.yaml +++ b/config/rbac/cert_manager_controller_approver_clusterrole.yaml @@ -12,3 +12,4 @@ rules: - approve resourceNames: - stepissuers.certmanager.step.sm/* + - stepclusterissuers.certmanager.step.sm/* diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 7fd8af725d82118603f00fdf4506c2963d4e15c4..edff2da4120587951b158fc820dca855c6265ea0 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -42,6 +42,7 @@ rules: - certmanager.step.sm resources: - stepissuers + - stepclusterissuers verbs: - create - delete @@ -54,6 +55,7 @@ rules: - certmanager.step.sm resources: - stepissuers/status + - stepclusterissuers/status verbs: - get - patch diff --git a/config/rbac/stepclusterissuer_editor_role.yaml b/config/rbac/stepclusterissuer_editor_role.yaml deleted file mode 100644 index 2117244eeb4e166cf3265658a2a8b97f1f975c78..0000000000000000000000000000000000000000 --- a/config/rbac/stepclusterissuer_editor_role.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# permissions for end users to edit stepclusterissuers. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: stepclusterissuer-editor-role -rules: -- apiGroups: - - certmanager.step.sm - resources: - - stepclusterissuers - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - certmanager.step.sm - resources: - - stepclusterissuers/status - verbs: - - get diff --git a/config/rbac/stepclusterissuer_viewer_role.yaml b/config/rbac/stepclusterissuer_viewer_role.yaml deleted file mode 100644 index 629d62e6dc5d84951d0539440f624ffa6c973df8..0000000000000000000000000000000000000000 --- a/config/rbac/stepclusterissuer_viewer_role.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# permissions for end users to view stepclusterissuers. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: stepclusterissuer-viewer-role -rules: -- apiGroups: - - certmanager.step.sm - resources: - - stepclusterissuers - verbs: - - get - - list - - watch -- apiGroups: - - certmanager.step.sm - resources: - - stepclusterissuers/status - verbs: - - get diff --git a/config/samples/certmanager_v1beta1_stepclusterissuer.yaml b/config/samples/certmanager_v1beta1_stepclusterissuer.yaml deleted file mode 100644 index 228375e2d371c931297fb093562dbcd531ec8ef2..0000000000000000000000000000000000000000 --- a/config/samples/certmanager_v1beta1_stepclusterissuer.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: certmanager.step.sm/v1beta1 -kind: StepClusterIssuer -metadata: - name: stepclusterissuer-sample -spec: - # Add fields here - foo: bar diff --git a/config/samples/stepclusterissuer.yaml b/config/samples/stepclusterissuer.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9e814275d54438d12d5d6588c51aa0359731a7e5 --- /dev/null +++ b/config/samples/stepclusterissuer.yaml @@ -0,0 +1,16 @@ +apiVersion: certmanager.step.sm/v1beta1 +kind: StepClusterIssuer +metadata: + name: step-cluster-issuer +spec: + # The CA URL. + url: https://step-certificates.default.svc.cluster.local + # The base64 encoded version of the CA root certificate in PEM format. + caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJpekNDQVRHZ0F3SUJBZ0lRTytFQWg4eS8wVjlQMFhwSHJWajVOVEFLQmdncWhrak9QUVFEQWpBa01TSXcKSUFZRFZRUURFeGxUZEdWd0lFTmxjblJwWm1sallYUmxjeUJTYjI5MElFTkJNQjRYRFRFNU1EZ3hNekU1TVRVdwpNbG9YRFRJNU1EZ3hNREU1TVRVd01sb3dKREVpTUNBR0ExVUVBeE1aVTNSbGNDQkRaWEowYVdacFkyRjBaWE1nClVtOXZkQ0JEUVRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQkFNVkw3VzBQbTNvSlVmSTR3WGQKa2xERW5uNVhTbWo4NlgwYW1DQTBnY08xdElUUG1DVzNCcGU0cE9vV1V2WlZlUWRvU2NxN3pua1V0Mi9HMnQxTgo3MWlqUlRCRE1BNEdBMVVkRHdFQi93UUVBd0lCQmpBU0JnTlZIUk1CQWY4RUNEQUdBUUgvQWdFQk1CMEdBMVVkCkRnUVdCQlJ1Y1ByVm5QdlpOMHI0QVU5TGcyL2VCcng3a2pBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlCUlJBdGsKNXpMY0doQ2FobVBuVzIwZExpdEMzRVdNaVE0bERwN2FFeitFUEFJaEFJOWZWczVxb0l0bVQ4anA2WktVNVEydQphRFBrOGsyQ25OMjdyRnNZV3VwTAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + # The provisioner name, kid, and a reference to the provisioner password secret. + provisioner: + name: admin + kid: N6I99Yuk7iGDMk_eW3QaN2admCsrC9UuDN27dlFXUOs + passwordRef: + name: step-certificates-provisioner-password + key: password \ No newline at end of file diff --git a/controllers/stepclusterissuer_controller.go b/controllers/stepclusterissuer_controller.go index 9ec339f91077072c042d4a37966a479fdb0f4f87..50bc59d40b45476cbde905ca9101980c2ee6c2ee 100644 --- a/controllers/stepclusterissuer_controller.go +++ b/controllers/stepclusterissuer_controller.go @@ -17,36 +17,105 @@ package controllers import ( "context" + "fmt" "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/runtime" + api "github.com/smallstep/step-issuer/api/v1beta1" + "github.com/smallstep/step-issuer/provisioners" + core "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "k8s.io/utils/clock" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - - certmanagerv1beta1 "github.com/smallstep/step-issuer/api/v1beta1" ) // StepClusterIssuerReconciler reconciles a StepClusterIssuer object type StepClusterIssuerReconciler struct { client.Client - Log logr.Logger - Scheme *runtime.Scheme + Log logr.Logger + Clock clock.Clock + Recorder record.EventRecorder } // +kubebuilder:rbac:groups=certmanager.step.sm,resources=stepclusterissuers,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=certmanager.step.sm,resources=stepclusterissuers/status,verbs=get;update;patch +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch +// Reconcile will read and validate the StepClusterIssuer resources, it will set the +// status condition ready to true if everything is right. func (r *StepClusterIssuerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { - _ = context.Background() - _ = r.Log.WithValues("stepclusterissuer", req.NamespacedName) + log := r.Log.WithValues("stepclusterissuer", req.NamespacedName) + + iss := new(api.StepClusterIssuer) + if err := r.Client.Get(ctx, req.NamespacedName, iss); err != nil { + log.Error(err, "failed to retrieve StepClusterIssuer resource") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + statusReconciler := newStepStatusReconciler(r, iss, log) + if err := validateStepClusterIssuerSpec(iss.Spec); err != nil { + log.Error(err, "failed to validate StepClusterIssuer resource") + statusReconciler.UpdateNoError(ctx, api.ConditionFalse, "Validation", "Failed to validate resource: %v", err) + return ctrl.Result{}, err + } - // your logic here + // Fetch the provisioner password + var secret core.Secret + secretNamespaceName := types.NamespacedName{ + Namespace: req.Namespace, + Name: iss.Spec.Provisioner.PasswordRef.Name, + } + if err := r.Client.Get(ctx, secretNamespaceName, &secret); err != nil { + log.Error(err, "failed to retrieve StepClusterIssuer provisioner secret", "namespace", secretNamespaceName.Namespace, "name", secretNamespaceName.Name) + if apierrors.IsNotFound(err) { + statusReconciler.UpdateNoError(ctx, api.ConditionFalse, "NotFound", "Failed to retrieve provisioner secret: %v", err) + } else { + statusReconciler.UpdateNoError(ctx, api.ConditionFalse, "Error", "Failed to retrieve provisioner secret: %v", err) + } + return ctrl.Result{}, err + } + password, ok := secret.Data[iss.Spec.Provisioner.PasswordRef.Key] + if !ok { + err := fmt.Errorf("secret %s does not contain key %s", secret.Name, iss.Spec.Provisioner.PasswordRef.Key) + log.Error(err, "failed to retrieve StepClusterIssuer provisioner secret", "namespace", secretNamespaceName.Namespace, "name", secretNamespaceName.Name) + statusReconciler.UpdateNoError(ctx, api.ConditionFalse, "NotFound", "Failed to retrieve provisioner secret: %v", err) + return ctrl.Result{}, err + } - return ctrl.Result{}, nil + // Initialize and store the provisioner + p, err := provisioners.New(iss, password) + if err != nil { + log.Error(err, "failed to initialize provisioner") + statusReconciler.UpdateNoError(ctx, api.ConditionFalse, "Error", "failed initialize provisioner") + return ctrl.Result{}, err + } + provisioners.Store(req.NamespacedName, p) + + return ctrl.Result{}, statusReconciler.Update(ctx, api.ConditionTrue, "Verified", "StepClusterIssuer verified and ready to sign certificates") } func (r *StepClusterIssuerReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&certmanagerv1beta1.StepClusterIssuer{}). + For(&api.StepClusterIssuer{}). Complete(r) } + +func validateStepClusterIssuerSpec(s api.StepClusterIssuerSpec) error { + switch { + case s.URL == "": + return fmt.Errorf("spec.url cannot be empty") + case s.Provisioner.Name == "": + return fmt.Errorf("spec.provisioner.name cannot be empty") + case s.Provisioner.KeyID == "": + return fmt.Errorf("spec.provisioner.kid cannot be empty") + case s.Provisioner.PasswordRef.Name == "": + return fmt.Errorf("spec.provisioner.passwordRef.name cannot be empty") + case s.Provisioner.PasswordRef.Key == "": + return fmt.Errorf("spec.provisioner.passwordRef.key cannot be empty") + default: + return nil + } +} \ No newline at end of file diff --git a/main.go b/main.go index f0fbced07e708f1d721f2450f1be5343764ebbcb..0a9ad46c250f79a31e40142334f315ddeeb08b02 100644 --- a/main.go +++ b/main.go @@ -88,6 +88,16 @@ func main() { os.Exit(1) } + if err = (&controllers.StepClusterIssuerReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("StepClusterIssuer"), + Clock: clock.RealClock{}, + Recorder: mgr.GetEventRecorderFor("stepclusterissuer-controller"), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "StepClusterIssuer") + os.Exit(1) + } + if err = (&controllers.CertificateRequestReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("CertificateRequest"), @@ -99,14 +109,6 @@ func main() { os.Exit(1) } - if err = (&controllers.StepClusterIssuerReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("StepClusterIssuer"), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "StepClusterIssuer") - os.Exit(1) - } // +kubebuilder:scaffold:builder setupLog.Info("starting manager")