diff --git a/api/v1beta1/stepclusterissuer_types.go b/api/v1beta1/stepclusterissuer_types.go index 9ae71741eb7e0d4474306e6ed6ec9b768300eea1..d59f78e77a77ace0314027bad3923adb19587d6a 100644 --- a/api/v1beta1/stepclusterissuer_types.go +++ b/api/v1beta1/stepclusterissuer_types.go @@ -48,13 +48,15 @@ type StepClusterIssuerSpec struct { type StepClusterIssuerStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file + + // +optional Conditions []StepClusterIssuerCondition `json:"conditions,omitempty"` } // +kubebuilder:object:root=true -// +kubebuilder:resource:scope=Cluster // StepClusterIssuer is the Schema for the stepclusterissuers API +// +kubebuilder:subresource:status type StepClusterIssuer struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -72,59 +74,6 @@ type StepClusterIssuerList struct { Items []StepClusterIssuer `json:"items"` } -// 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'). @@ -148,4 +97,4 @@ type StepClusterIssuerCondition struct { // transition, complementing reason. // +optional Message string `json:"message,omitempty"` -} \ No newline at end of file +} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 64ea5cd46bf7704e7375ba6e2ad5314b40f1ec7a..5808ce0316ef80d1303f3c95f5a8c77ba649386b 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -43,8 +43,8 @@ func (in *StepClusterIssuer) DeepCopyInto(out *StepClusterIssuer) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StepClusterIssuer. @@ -65,6 +65,25 @@ func (in *StepClusterIssuer) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StepClusterIssuerCondition) DeepCopyInto(out *StepClusterIssuerCondition) { + *out = *in + if in.LastTransitionTime != nil { + in, out := &in.LastTransitionTime, &out.LastTransitionTime + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StepClusterIssuerCondition. +func (in *StepClusterIssuerCondition) DeepCopy() *StepClusterIssuerCondition { + if in == nil { + return nil + } + out := new(StepClusterIssuerCondition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StepClusterIssuerList) DeepCopyInto(out *StepClusterIssuerList) { *out = *in @@ -100,6 +119,12 @@ func (in *StepClusterIssuerList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StepClusterIssuerSpec) DeepCopyInto(out *StepClusterIssuerSpec) { *out = *in + out.Provisioner = in.Provisioner + if in.CABundle != nil { + in, out := &in.CABundle, &out.CABundle + *out = make([]byte, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StepClusterIssuerSpec. @@ -115,6 +140,13 @@ func (in *StepClusterIssuerSpec) DeepCopy() *StepClusterIssuerSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StepClusterIssuerStatus) DeepCopyInto(out *StepClusterIssuerStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]StepClusterIssuerCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StepClusterIssuerStatus. diff --git a/config/crd/bases/certmanager.step.sm_stepclusterissuers.yaml b/config/crd/bases/certmanager.step.sm_stepclusterissuers.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1c592d6f061f0ef234b5ca21d7564110ab46f5eb --- /dev/null +++ b/config/crd/bases/certmanager.step.sm_stepclusterissuers.yaml @@ -0,0 +1,122 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.5.0 + creationTimestamp: null + name: stepclusterissuers.certmanager.step.sm +spec: + group: certmanager.step.sm + names: + kind: StepClusterIssuer + listKind: StepClusterIssuerList + plural: stepclusterissuers + singular: stepclusterissuer + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: StepClusterIssuer is the Schema for the stepclusterissuers 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: StepClusterIssuerSpec defines the desired state of StepClusterIssuer + properties: + caBundle: + description: 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. + format: byte + type: string + provisioner: + description: Provisioner contains the step certificates provisioner configuration. + properties: + kid: + description: KeyID is the kid property of the JWK provisioner. + type: string + name: + description: Names is the name of the JWK provisioner. + type: string + passwordRef: + description: PasswordRef is a reference to a Secret containing the provisioner password used to decrypt the provisioner private key. + properties: + key: + description: The key of the secret to select from. Must be a valid secret key. + type: string + name: + description: The name of the secret in the pod's namespace to select from. + type: string + required: + - name + type: object + required: + - kid + - name + - passwordRef + type: object + url: + description: URL is the base URL for the step certificates instance. + type: string + required: + - provisioner + - url + type: object + status: + description: StepClusterIssuerStatus defines the observed state of StepClusterIssuer + properties: + conditions: + items: + description: StepClusterIssuerCondition contains condition information for the step issuer. + properties: + lastTransitionTime: + description: LastTransitionTime is the timestamp corresponding to the last status change of this condition. + format: date-time + type: string + message: + description: Message is a human readable description of the details of the last transition, complementing reason. + type: string + reason: + description: Reason is a brief machine readable explanation for the condition's last transition. + type: string + status: + allOf: + - enum: + - "True" + - "False" + - Unknown + - enum: + - "True" + - "False" + - Unknown + description: Status of the condition, one of ('True', 'False', 'Unknown'). + type: string + type: + description: Type of the condition, currently ('Ready'). + enum: + - Ready + type: string + required: + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/bases/certmanager.step.sm_stepissuers.yaml b/config/crd/bases/certmanager.step.sm_stepissuers.yaml index 3d883661b62115f265fc35cc415ee8b7896c8487..3c7f1ff3b94b7cf2bcccbc3233ebd286c099b79b 100644 --- a/config/crd/bases/certmanager.step.sm_stepissuers.yaml +++ b/config/crd/bases/certmanager.step.sm_stepissuers.yaml @@ -22,14 +22,10 @@ spec: description: StepIssuer is the Schema for the stepissuers 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' + 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' + 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 @@ -37,14 +33,11 @@ spec: description: StepIssuerSpec defines the desired state of StepIssuer properties: caBundle: - description: 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. + description: 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. format: byte type: string provisioner: - description: Provisioner contains the step certificates provisioner - configuration. + description: Provisioner contains the step certificates provisioner configuration. properties: kid: description: KeyID is the kid property of the JWK provisioner. @@ -53,17 +46,13 @@ spec: description: Names is the name of the JWK provisioner. type: string passwordRef: - description: PasswordRef is a reference to a Secret containing - the provisioner password used to decrypt the provisioner private - key. + description: PasswordRef is a reference to a Secret containing the provisioner password used to decrypt the provisioner private key. properties: key: - description: The key of the secret to select from. Must be - a valid secret key. + description: The key of the secret to select from. Must be a valid secret key. type: string name: - description: The name of the secret in the pod's namespace - to select from. + description: The name of the secret in the pod's namespace to select from. type: string required: - name @@ -85,21 +74,17 @@ spec: properties: conditions: items: - description: StepIssuerCondition contains condition information - for the step issuer. + description: StepIssuerCondition contains condition information for the step issuer. properties: lastTransitionTime: - description: LastTransitionTime is the timestamp corresponding - to the last status change of this condition. + description: LastTransitionTime is the timestamp corresponding to the last status change of this condition. format: date-time type: string message: - description: Message is a human readable description of the - details of the last transition, complementing reason. + description: Message is a human readable description of the details of the last transition, complementing reason. type: string reason: - description: Reason is a brief machine readable explanation - for the condition's last transition. + description: Reason is a brief machine readable explanation for the condition's last transition. type: string status: allOf: @@ -111,8 +96,7 @@ spec: - "True" - "False" - Unknown - description: Status of the condition, one of ('True', 'False', - 'Unknown'). + description: Status of the condition, one of ('True', 'False', 'Unknown'). type: string type: description: Type of the condition, currently ('Ready'). diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index edff2da4120587951b158fc820dca855c6265ea0..305ff4d83687a06e9e2c3a3300e03d4761e7afb1 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -41,7 +41,6 @@ rules: - apiGroups: - certmanager.step.sm resources: - - stepissuers - stepclusterissuers verbs: - create @@ -54,7 +53,6 @@ rules: - apiGroups: - certmanager.step.sm resources: - - stepissuers/status - stepclusterissuers/status verbs: - get diff --git a/controllers/step_status_cluster_reconciler.go b/controllers/step_status_cluster_reconciler.go new file mode 100644 index 0000000000000000000000000000000000000000..86b93e0e2ddaf3cf88623c4d371dafaa8e42e136 --- /dev/null +++ b/controllers/step_status_cluster_reconciler.go @@ -0,0 +1,90 @@ +package controllers + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + api "github.com/smallstep/step-issuer/api/v1beta1" + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type stepStatusClusterReconciler struct { + *StepClusterIssuerReconciler + issuer *api.StepClusterIssuer + logger logr.Logger +} + +func newStepStatusClusterReconciler(r *StepClusterIssuerReconciler, iss *api.StepClusterIssuer, log logr.Logger) *stepStatusClusterReconciler { + return &stepStatusClusterReconciler{ + StepClusterIssuerReconciler: r, + issuer: iss, + logger: log, + } +} + +func (r *stepStatusClusterReconciler) Update(ctx context.Context, status api.ConditionStatus, reason, message string, args ...interface{}) error { + completeMessage := fmt.Sprintf(message, args...) + r.setCondition(status, reason, completeMessage) + + // Fire an Event to additionally inform users of the change + eventType := core.EventTypeNormal + if status == api.ConditionFalse { + eventType = core.EventTypeWarning + } + r.Recorder.Event(r.issuer, eventType, reason, completeMessage) + + return r.Client.Status().Update(ctx, r.issuer) +} + +func (r *stepStatusClusterReconciler) UpdateNoError(ctx context.Context, status api.ConditionStatus, reason, message string, args ...interface{}) { + if err := r.Update(ctx, status, reason, message, args...); err != nil { + r.logger.Error(err, "failed to update", "status", status, "reason", reason) + } +} + +// setCondition will set a 'condition' on the given api.StepClusterIssuer resource. +// +// - If no condition of the same type already exists, the condition will be +// inserted with the LastTransitionTime set to the current time. +// - If a condition of the same type and state already exists, the condition +// will be updated but the LastTransitionTime will not be modified. +// - If a condition of the same type and different state already exists, the +// condition will be updated and the LastTransitionTime set to the current +// time. +func (r *stepStatusClusterReconciler) setCondition(status api.ConditionStatus, reason, message string) { + now := meta.NewTime(r.Clock.Now()) + c := api.StepClusterIssuerCondition{ + Type: api.ConditionReady, + Status: status, + Reason: reason, + Message: message, + LastTransitionTime: &now, + } + + // Search through existing conditions + for idx, cond := range r.issuer.Status.Conditions { + // Skip unrelated conditions + if cond.Type != api.ConditionReady { + continue + } + + // If this update doesn't contain a state transition, we don't update + // the conditions LastTransitionTime to Now() + if cond.Status == status { + c.LastTransitionTime = cond.LastTransitionTime + } else { + r.logger.Info("found status change for StepIssuer condition; setting lastTransitionTime", "condition", cond.Type, "old_status", cond.Status, "new_status", status, "time", now.Time) + } + + // Overwrite the existing condition + r.issuer.Status.Conditions[idx] = c + return + } + + // If we've not found an existing condition of this type, we simply insert + // the new condition into the slice. + r.issuer.Status.Conditions = append(r.issuer.Status.Conditions, c) + r.logger.Info("setting lastTransitionTime for StepIssuer condition", "condition", api.ConditionReady, "time", now.Time) +} diff --git a/controllers/stepclusterissuer_controller.go b/controllers/stepclusterissuer_controller.go index 50bc59d40b45476cbde905ca9101980c2ee6c2ee..8fca0d8e20980b4f6e9f9955d532bb7a3525ee5e 100644 --- a/controllers/stepclusterissuer_controller.go +++ b/controllers/stepclusterissuer_controller.go @@ -46,7 +46,7 @@ type StepClusterIssuerReconciler struct { // 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) { +func (r *StepClusterIssuerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("stepclusterissuer", req.NamespacedName) iss := new(api.StepClusterIssuer) @@ -55,7 +55,7 @@ func (r *StepClusterIssuerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, return ctrl.Result{}, client.IgnoreNotFound(err) } - statusReconciler := newStepStatusReconciler(r, iss, log) + statusReconciler := newStepStatusClusterReconciler(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) @@ -86,7 +86,7 @@ func (r *StepClusterIssuerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, } // Initialize and store the provisioner - p, err := provisioners.New(iss, password) + p, err := provisioners.NewFromStepClusterIssuer(iss, password) if err != nil { log.Error(err, "failed to initialize provisioner") statusReconciler.UpdateNoError(ctx, api.ConditionFalse, "Error", "failed initialize provisioner") @@ -97,6 +97,8 @@ func (r *StepClusterIssuerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, return ctrl.Result{}, statusReconciler.Update(ctx, api.ConditionTrue, "Verified", "StepClusterIssuer verified and ready to sign certificates") } +// SetupWithManager initializes the StepClusterIssuer controller into the controller +// runtime. func (r *StepClusterIssuerReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&api.StepClusterIssuer{}). @@ -118,4 +120,4 @@ func validateStepClusterIssuerSpec(s api.StepClusterIssuerSpec) error { default: return nil } -} \ No newline at end of file +} diff --git a/controllers/stepissuer_controller.go b/controllers/stepissuer_controller.go index cdbe4dc6ded7aafef44cf0f9525676b8020320ca..e935b3e48c353eb13302eafc24c71cb72444ab50 100644 --- a/controllers/stepissuer_controller.go +++ b/controllers/stepissuer_controller.go @@ -87,7 +87,7 @@ func (r *StepIssuerReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // Initialize and store the provisioner - p, err := provisioners.New(iss, password) + p, err := provisioners.NewFromStepIssuer(iss, password) if err != nil { log.Error(err, "failed to initialize provisioner") statusReconciler.UpdateNoError(ctx, api.ConditionFalse, "Error", "failed initialize provisioner") diff --git a/controllers/suite_test.go b/controllers/suite_test.go index d8d6b31e862a838e71c8aae8d6136c9dddd9faff..6fb9465713bbb4387256a5f8b591acce5b9b2dbb 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -30,7 +30,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/envtest/printer" logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/log/zap" // +kubebuilder:scaffold:imports ) diff --git a/main.go b/main.go index 0a9ad46c250f79a31e40142334f315ddeeb08b02..593a4c13a9da6e327f23ed335db5aed3b8a5ac35 100644 --- a/main.go +++ b/main.go @@ -89,9 +89,9 @@ func main() { } if err = (&controllers.StepClusterIssuerReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("StepClusterIssuer"), - Clock: clock.RealClock{}, + 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") diff --git a/provisioners/step.go b/provisioners/step.go index 440ed34ca484a3f8f803efca726474f10efcaaa9..32d20b3377a17122c1e8894eaf1c2f18bbe572ee 100644 --- a/provisioners/step.go +++ b/provisioners/step.go @@ -26,7 +26,34 @@ type Step struct { // New returns a new Step provisioner, configured with the information in the // given issuer. -func New(iss *api.StepIssuer, password []byte) (*Step, error) { +func NewFromStepIssuer(iss *api.StepIssuer, password []byte) (*Step, error) { + var options []ca.ClientOption + if len(iss.Spec.CABundle) > 0 { + options = append(options, ca.WithCABundle(iss.Spec.CABundle)) + } + provisioner, err := ca.NewProvisioner(iss.Spec.Provisioner.Name, iss.Spec.Provisioner.KeyID, iss.Spec.URL, password, options...) + if err != nil { + return nil, err + } + + p := &Step{ + name: iss.Name + "." + iss.Namespace, + provisioner: provisioner, + } + + // Request identity certificate if required. + if version, err := provisioner.Version(); err == nil { + if version.RequireClientAuthentication { + if err := p.createIdentityCertificate(); err != nil { + return nil, err + } + } + } + + return p, nil +} + +func NewFromStepClusterIssuer(iss *api.StepClusterIssuer, password []byte) (*Step, error) { var options []ca.ClientOption if len(iss.Spec.CABundle) > 0 { options = append(options, ca.WithCABundle(iss.Spec.CABundle))