diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index 0ee558e844e..1d7a3437e9e 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -4347,6 +4347,10 @@ }, "io.k8s.api.certificates.v1beta1.CertificateSigningRequestCondition": { "properties": { + "lastTransitionTime": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Time", + "description": "lastTransitionTime is the time the condition last transitioned from one status to another. If unset, when a new condition type is added or an existing condition's status is changed, the server defaults this to the current time." + }, "lastUpdateTime": { "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Time", "description": "timestamp for the last update to this condition" @@ -4359,8 +4363,12 @@ "description": "brief reason for the request state", "type": "string" }, + "status": { + "description": "Status of the condition, one of True, False, Unknown. Approved, Denied, and Failed conditions may not be \"False\" or \"Unknown\". Defaults to \"True\". If unset, should be treated as \"True\".", + "type": "string" + }, "type": { - "description": "request approval state, currently Approved or Denied.", + "description": "type of the condition. Known conditions include \"Approved\", \"Denied\", and \"Failed\".", "type": "string" } }, diff --git a/pkg/apis/certificates/BUILD b/pkg/apis/certificates/BUILD index a826a7e2266..a789aa93ab3 100644 --- a/pkg/apis/certificates/BUILD +++ b/pkg/apis/certificates/BUILD @@ -16,6 +16,7 @@ go_library( ], importpath = "k8s.io/kubernetes/pkg/apis/certificates", deps = [ + "//pkg/apis/core:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", diff --git a/pkg/apis/certificates/types.go b/pkg/apis/certificates/types.go index b3a07f8d7cd..b616c000113 100644 --- a/pkg/apis/certificates/types.go +++ b/pkg/apis/certificates/types.go @@ -16,7 +16,10 @@ limitations under the License. package certificates -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + api "k8s.io/kubernetes/pkg/apis/core" +) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -72,6 +75,28 @@ type CertificateSigningRequestSpec struct { Extra map[string]ExtraValue } +// Built in signerName values that are honoured by kube-controller-manager. +// None of these usages are related to ServiceAccount token secrets +// `.data[ca.crt]` in any way. +const ( + // Signs certificates that will be honored as client-certs by the + // kube-apiserver. Never auto-approved by kube-controller-manager. + KubeAPIServerClientSignerName = "kubernetes.io/kube-apiserver-client" + + // Signs client certificates that will be honored as client-certs by the + // kube-apiserver for a kubelet. + // May be auto-approved by kube-controller-manager. + KubeAPIServerClientKubeletSignerName = "kubernetes.io/kube-apiserver-client-kubelet" + + // Signs serving certificates that are honored as a valid kubelet serving + // certificate by the kube-apiserver, but has no other guarantees. + KubeletServingSignerName = "kubernetes.io/kubelet-serving" + + // Has no guarantees for trust at all. Some distributions may honor these + // as client certs, but that behavior is not standard kubernetes behavior. + LegacyUnknownSignerName = "kubernetes.io/legacy-unknown" +) + // ExtraValue masks the value so protobuf can generate type ExtraValue []string @@ -91,11 +116,17 @@ type RequestConditionType string const ( CertificateApproved RequestConditionType = "Approved" CertificateDenied RequestConditionType = "Denied" + CertificateFailed RequestConditionType = "Failed" ) type CertificateSigningRequestCondition struct { - // request approval state, currently Approved or Denied. + // type of the condition. Known conditions include "Approved", "Denied", and "Failed". Type RequestConditionType + // Status of the condition, one of True, False, Unknown. + // Approved, Denied, and Failed conditions may not be "False" or "Unknown". + // If unset, should be treated as "True". + // +optional + Status api.ConditionStatus // brief reason for the request state // +optional Reason string @@ -105,6 +136,9 @@ type CertificateSigningRequestCondition struct { // timestamp for the last update to this condition // +optional LastUpdateTime metav1.Time + // lastTransitionTime is the time the condition last transitioned from one status to another. + // +optional + LastTransitionTime metav1.Time } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/certificates/v1beta1/BUILD b/pkg/apis/certificates/v1beta1/BUILD index 057b22b2ade..d7349d8ea67 100644 --- a/pkg/apis/certificates/v1beta1/BUILD +++ b/pkg/apis/certificates/v1beta1/BUILD @@ -20,7 +20,9 @@ go_library( importpath = "k8s.io/kubernetes/pkg/apis/certificates/v1beta1", deps = [ "//pkg/apis/certificates:go_default_library", + "//pkg/apis/core:go_default_library", "//staging/src/k8s.io/api/certificates/v1beta1:go_default_library", + "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/conversion:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", diff --git a/pkg/apis/certificates/v1beta1/defaults.go b/pkg/apis/certificates/v1beta1/defaults.go index ec11d3ab111..83a43876b43 100644 --- a/pkg/apis/certificates/v1beta1/defaults.go +++ b/pkg/apis/certificates/v1beta1/defaults.go @@ -18,10 +18,12 @@ package v1beta1 import ( "crypto/x509" + "fmt" "reflect" "strings" certificatesv1beta1 "k8s.io/api/certificates/v1beta1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" ) @@ -41,6 +43,12 @@ func SetDefaults_CertificateSigningRequestSpec(obj *certificatesv1beta1.Certific } } +func SetDefaults_CertificateSigningRequestCondition(obj *certificatesv1beta1.CertificateSigningRequestCondition) { + if len(obj.Status) == 0 { + obj.Status = v1.ConditionTrue + } +} + // DefaultSignerNameFromSpec will determine the signerName that should be set // by attempting to inspect the 'request' content and the spec options. func DefaultSignerNameFromSpec(obj *certificatesv1beta1.CertificateSigningRequestSpec) string { @@ -59,18 +67,34 @@ func DefaultSignerNameFromSpec(obj *certificatesv1beta1.CertificateSigningReques } } +var ( + organizationNotSystemNodesErr = fmt.Errorf("subject organization is not system:nodes") + commonNameNotSystemNode = fmt.Errorf("subject common name does not begin with system:node:") + dnsOrIPSANRequiredErr = fmt.Errorf("DNS or IP subjectAltName is required") + dnsSANNotAllowedErr = fmt.Errorf("DNS subjectAltNames are not allowed") + emailSANNotAllowedErr = fmt.Errorf("Email subjectAltNames are not allowed") + ipSANNotAllowedErr = fmt.Errorf("IP subjectAltNames are not allowed") + uriSANNotAllowedErr = fmt.Errorf("URI subjectAltNames are not allowed") +) + func IsKubeletServingCSR(req *x509.CertificateRequest, usages []certificatesv1beta1.KeyUsage) bool { + return ValidateKubeletServingCSR(req, usages) == nil +} +func ValidateKubeletServingCSR(req *x509.CertificateRequest, usages []certificatesv1beta1.KeyUsage) error { if !reflect.DeepEqual([]string{"system:nodes"}, req.Subject.Organization) { - return false + return organizationNotSystemNodesErr } // at least one of dnsNames or ipAddresses must be specified if len(req.DNSNames) == 0 && len(req.IPAddresses) == 0 { - return false + return dnsOrIPSANRequiredErr } - if len(req.EmailAddresses) > 0 || len(req.URIs) > 0 { - return false + if len(req.EmailAddresses) > 0 { + return emailSANNotAllowedErr + } + if len(req.URIs) > 0 { + return uriSANNotAllowedErr } requiredUsages := []certificatesv1beta1.KeyUsage{ @@ -79,27 +103,39 @@ func IsKubeletServingCSR(req *x509.CertificateRequest, usages []certificatesv1be certificatesv1beta1.UsageServerAuth, } if !equalUnsorted(requiredUsages, usages) { - return false + return fmt.Errorf("usages did not match %v", requiredUsages) } if !strings.HasPrefix(req.Subject.CommonName, "system:node:") { - return false + return commonNameNotSystemNode } - return true + return nil } func IsKubeletClientCSR(req *x509.CertificateRequest, usages []certificatesv1beta1.KeyUsage) bool { + return ValidateKubeletClientCSR(req, usages) == nil +} +func ValidateKubeletClientCSR(req *x509.CertificateRequest, usages []certificatesv1beta1.KeyUsage) error { if !reflect.DeepEqual([]string{"system:nodes"}, req.Subject.Organization) { - return false + return organizationNotSystemNodesErr } - if len(req.DNSNames) > 0 || len(req.EmailAddresses) > 0 || len(req.IPAddresses) > 0 || len(req.URIs) > 0 { - return false + if len(req.DNSNames) > 0 { + return dnsSANNotAllowedErr + } + if len(req.EmailAddresses) > 0 { + return emailSANNotAllowedErr + } + if len(req.IPAddresses) > 0 { + return ipSANNotAllowedErr + } + if len(req.URIs) > 0 { + return uriSANNotAllowedErr } if !strings.HasPrefix(req.Subject.CommonName, "system:node:") { - return false + return commonNameNotSystemNode } requiredUsages := []certificatesv1beta1.KeyUsage{ @@ -108,10 +144,10 @@ func IsKubeletClientCSR(req *x509.CertificateRequest, usages []certificatesv1bet certificatesv1beta1.UsageClientAuth, } if !equalUnsorted(requiredUsages, usages) { - return false + return fmt.Errorf("usages did not match %v", requiredUsages) } - return true + return nil } // equalUnsorted compares two []string for equality of contents regardless of diff --git a/pkg/apis/certificates/v1beta1/zz_generated.conversion.go b/pkg/apis/certificates/v1beta1/zz_generated.conversion.go index e8c87716e59..fbe73f01137 100644 --- a/pkg/apis/certificates/v1beta1/zz_generated.conversion.go +++ b/pkg/apis/certificates/v1beta1/zz_generated.conversion.go @@ -24,10 +24,12 @@ import ( unsafe "unsafe" v1beta1 "k8s.io/api/certificates/v1beta1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" conversion "k8s.io/apimachinery/pkg/conversion" runtime "k8s.io/apimachinery/pkg/runtime" certificates "k8s.io/kubernetes/pkg/apis/certificates" + core "k8s.io/kubernetes/pkg/apis/core" ) func init() { @@ -124,9 +126,11 @@ func Convert_certificates_CertificateSigningRequest_To_v1beta1_CertificateSignin func autoConvert_v1beta1_CertificateSigningRequestCondition_To_certificates_CertificateSigningRequestCondition(in *v1beta1.CertificateSigningRequestCondition, out *certificates.CertificateSigningRequestCondition, s conversion.Scope) error { out.Type = certificates.RequestConditionType(in.Type) + out.Status = core.ConditionStatus(in.Status) out.Reason = in.Reason out.Message = in.Message out.LastUpdateTime = in.LastUpdateTime + out.LastTransitionTime = in.LastTransitionTime return nil } @@ -137,9 +141,11 @@ func Convert_v1beta1_CertificateSigningRequestCondition_To_certificates_Certific func autoConvert_certificates_CertificateSigningRequestCondition_To_v1beta1_CertificateSigningRequestCondition(in *certificates.CertificateSigningRequestCondition, out *v1beta1.CertificateSigningRequestCondition, s conversion.Scope) error { out.Type = v1beta1.RequestConditionType(in.Type) + out.Status = v1.ConditionStatus(in.Status) out.Reason = in.Reason out.Message = in.Message out.LastUpdateTime = in.LastUpdateTime + out.LastTransitionTime = in.LastTransitionTime return nil } @@ -192,7 +198,7 @@ func Convert_certificates_CertificateSigningRequestList_To_v1beta1_CertificateSi func autoConvert_v1beta1_CertificateSigningRequestSpec_To_certificates_CertificateSigningRequestSpec(in *v1beta1.CertificateSigningRequestSpec, out *certificates.CertificateSigningRequestSpec, s conversion.Scope) error { out.Request = *(*[]byte)(unsafe.Pointer(&in.Request)) - if err := v1.Convert_Pointer_string_To_string(&in.SignerName, &out.SignerName, s); err != nil { + if err := metav1.Convert_Pointer_string_To_string(&in.SignerName, &out.SignerName, s); err != nil { return err } out.Usages = *(*[]certificates.KeyUsage)(unsafe.Pointer(&in.Usages)) @@ -210,7 +216,7 @@ func Convert_v1beta1_CertificateSigningRequestSpec_To_certificates_CertificateSi func autoConvert_certificates_CertificateSigningRequestSpec_To_v1beta1_CertificateSigningRequestSpec(in *certificates.CertificateSigningRequestSpec, out *v1beta1.CertificateSigningRequestSpec, s conversion.Scope) error { out.Request = *(*[]byte)(unsafe.Pointer(&in.Request)) - if err := v1.Convert_string_To_Pointer_string(&in.SignerName, &out.SignerName, s); err != nil { + if err := metav1.Convert_string_To_Pointer_string(&in.SignerName, &out.SignerName, s); err != nil { return err } out.Usages = *(*[]v1beta1.KeyUsage)(unsafe.Pointer(&in.Usages)) diff --git a/pkg/apis/certificates/v1beta1/zz_generated.defaults.go b/pkg/apis/certificates/v1beta1/zz_generated.defaults.go index beb1ee2d125..b3ff787cc3f 100644 --- a/pkg/apis/certificates/v1beta1/zz_generated.defaults.go +++ b/pkg/apis/certificates/v1beta1/zz_generated.defaults.go @@ -40,6 +40,10 @@ func RegisterDefaults(scheme *runtime.Scheme) error { func SetObjectDefaults_CertificateSigningRequest(in *v1beta1.CertificateSigningRequest) { SetDefaults_CertificateSigningRequestSpec(&in.Spec) + for i := range in.Status.Conditions { + a := &in.Status.Conditions[i] + SetDefaults_CertificateSigningRequestCondition(a) + } } func SetObjectDefaults_CertificateSigningRequestList(in *v1beta1.CertificateSigningRequestList) { diff --git a/pkg/apis/certificates/validation/BUILD b/pkg/apis/certificates/validation/BUILD index 93e1542f2b8..690d2ff56bd 100644 --- a/pkg/apis/certificates/validation/BUILD +++ b/pkg/apis/certificates/validation/BUILD @@ -12,9 +12,16 @@ go_library( importpath = "k8s.io/kubernetes/pkg/apis/certificates/validation", deps = [ "//pkg/apis/certificates:go_default_library", + "//pkg/apis/certificates/v1beta1:go_default_library", "//pkg/apis/core/validation:go_default_library", + "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + "//staging/src/k8s.io/client-go/util/cert:go_default_library", ], ) @@ -37,7 +44,11 @@ go_test( embed = [":go_default_library"], deps = [ "//pkg/apis/certificates:go_default_library", + "//pkg/apis/certificates/v1beta1:go_default_library", + "//pkg/apis/core:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", ], ) diff --git a/pkg/apis/certificates/validation/validation.go b/pkg/apis/certificates/validation/validation.go index d24446355af..20d84a3395a 100644 --- a/pkg/apis/certificates/validation/validation.go +++ b/pkg/apis/certificates/validation/validation.go @@ -17,16 +17,65 @@ limitations under the License. package validation import ( + "bytes" + "crypto/x509" + "encoding/pem" "fmt" "strings" + v1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/diff" + "k8s.io/apimachinery/pkg/util/sets" utilvalidation "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" - + utilcert "k8s.io/client-go/util/cert" "k8s.io/kubernetes/pkg/apis/certificates" + certificatesv1beta1 "k8s.io/kubernetes/pkg/apis/certificates/v1beta1" apivalidation "k8s.io/kubernetes/pkg/apis/core/validation" ) +var ( + // trueConditionTypes is the set of condition types which may only have a status of True if present + trueConditionTypes = sets.NewString( + string(certificates.CertificateApproved), + string(certificates.CertificateDenied), + string(certificates.CertificateFailed), + ) + + trueStatusOnly = sets.NewString(string(v1.ConditionTrue)) + allStatusValues = sets.NewString(string(v1.ConditionTrue), string(v1.ConditionFalse), string(v1.ConditionUnknown)) +) + +type certificateValidationOptions struct { + // The following allow modifications only permitted via certain update paths + + // allow populating/modifying Approved/Denied conditions + allowSettingApprovalConditions bool + // allow populating status.certificate + allowSettingCertificate bool + + // allow Approved and Denied conditions to be exist. + // we tolerate this when the problem is already present in the persisted object for compatibility. + allowBothApprovedAndDenied bool + + // The following are bad things we tolerate for compatibility reasons: + // * in requests made via the v1beta1 API + // * in update requests where the problem is already present in the persisted object + + // allow modifying status.certificate on an update where the old object has a different certificate + allowResettingCertificate bool + // allow the legacy-unknown signerName + allowLegacySignerName bool + // allow conditions with duplicate types + allowDuplicateConditionTypes bool + // allow conditions with "" types + allowEmptyConditionType bool + // allow arbitrary content in status.certificate + allowArbitraryCertificate bool +} + // validateCSR validates the signature and formatting of a base64-wrapped, // PEM-encoded PKCS#10 certificate signing request. If this is invalid, we must // not accept the CSR for further processing. @@ -43,12 +92,55 @@ func validateCSR(obj *certificates.CertificateSigningRequest) error { return nil } +func validateCertificate(pemData []byte) error { + if len(pemData) == 0 { + return nil + } + + blocks := 0 + for { + block, remainingData := pem.Decode(pemData) + if block == nil { + break + } + + if block.Type != utilcert.CertificateBlockType { + return fmt.Errorf("only CERTIFICATE PEM blocks are allowed, found %q", block.Type) + } + if len(block.Headers) != 0 { + return fmt.Errorf("no PEM block headers are permitted") + } + blocks++ + + certs, err := x509.ParseCertificates(block.Bytes) + if err != nil { + return err + } + if len(certs) == 0 { + return fmt.Errorf("found CERTIFICATE PEM block containing 0 certificates") + } + + pemData = remainingData + } + + if blocks == 0 { + return fmt.Errorf("must contain at least one CERTIFICATE PEM block") + } + + return nil +} + // We don't care what you call your certificate requests. func ValidateCertificateRequestName(name string, prefix bool) []string { return nil } -func ValidateCertificateSigningRequest(csr *certificates.CertificateSigningRequest) field.ErrorList { +func ValidateCertificateSigningRequestCreate(csr *certificates.CertificateSigningRequest, version schema.GroupVersion) field.ErrorList { + opts := getValidationOptions(version, csr, nil) + return validateCertificateSigningRequest(csr, opts) +} + +func validateCertificateSigningRequest(csr *certificates.CertificateSigningRequest, opts certificateValidationOptions) field.ErrorList { isNamespaced := false allErrs := apivalidation.ValidateObjectMeta(&csr.ObjectMeta, isNamespaced, ValidateCertificateRequestName, field.NewPath("metadata")) @@ -60,7 +152,71 @@ func ValidateCertificateSigningRequest(csr *certificates.CertificateSigningReque if len(csr.Spec.Usages) == 0 { allErrs = append(allErrs, field.Required(specPath.Child("usages"), "usages must be provided")) } - allErrs = append(allErrs, ValidateCertificateSigningRequestSignerName(specPath.Child("signerName"), csr.Spec.SignerName)...) + if !opts.allowLegacySignerName && csr.Spec.SignerName == certificates.LegacyUnknownSignerName { + allErrs = append(allErrs, field.Invalid(specPath.Child("signerName"), csr.Spec.SignerName, "the legacy signerName is not allowed via this API version")) + } else { + allErrs = append(allErrs, ValidateCertificateSigningRequestSignerName(specPath.Child("signerName"), csr.Spec.SignerName)...) + } + allErrs = append(allErrs, validateConditions(field.NewPath("status", "conditions"), csr, opts)...) + + if !opts.allowArbitraryCertificate { + if err := validateCertificate(csr.Status.Certificate); err != nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("status", "certificate"), "", err.Error())) + } + } + + return allErrs +} + +func validateConditions(fldPath *field.Path, csr *certificates.CertificateSigningRequest, opts certificateValidationOptions) field.ErrorList { + allErrs := field.ErrorList{} + + seenTypes := map[certificates.RequestConditionType]bool{} + hasApproved := false + hasDenied := false + + for i, c := range csr.Status.Conditions { + + if !opts.allowEmptyConditionType { + if len(c.Type) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Index(i).Child("type"), "")) + } + } + + allowedStatusValues := allStatusValues + if trueConditionTypes.Has(string(c.Type)) { + allowedStatusValues = trueStatusOnly + } + switch { + case c.Status == "": + allErrs = append(allErrs, field.Required(fldPath.Index(i).Child("status"), "")) + case !allowedStatusValues.Has(string(c.Status)): + allErrs = append(allErrs, field.NotSupported(fldPath.Index(i).Child("status"), c.Status, trueConditionTypes.List())) + } + + if !opts.allowBothApprovedAndDenied { + switch c.Type { + case certificates.CertificateApproved: + hasApproved = true + if hasDenied { + allErrs = append(allErrs, field.Invalid(fldPath.Index(i).Child("type"), c.Type, "Approved and Denied conditions are mutually exclusive")) + } + case certificates.CertificateDenied: + hasDenied = true + if hasApproved { + allErrs = append(allErrs, field.Invalid(fldPath.Index(i).Child("type"), c.Type, "Approved and Denied conditions are mutually exclusive")) + } + } + } + + if !opts.allowDuplicateConditionTypes { + if seenTypes[c.Type] { + allErrs = append(allErrs, field.Duplicate(fldPath.Index(i).Child("type"), c.Type)) + } + seenTypes[c.Type] = true + } + } + return allErrs } @@ -140,8 +296,172 @@ func ValidateCertificateSigningRequestSignerName(fldPath *field.Path, signerName return el } -func ValidateCertificateSigningRequestUpdate(newCSR, oldCSR *certificates.CertificateSigningRequest) field.ErrorList { - validationErrorList := ValidateCertificateSigningRequest(newCSR) +func ValidateCertificateSigningRequestUpdate(newCSR, oldCSR *certificates.CertificateSigningRequest, version schema.GroupVersion) field.ErrorList { + opts := getValidationOptions(version, newCSR, oldCSR) + return validateCertificateSigningRequestUpdate(newCSR, oldCSR, opts) +} + +func ValidateCertificateSigningRequestStatusUpdate(newCSR, oldCSR *certificates.CertificateSigningRequest, version schema.GroupVersion) field.ErrorList { + opts := getValidationOptions(version, newCSR, oldCSR) + opts.allowSettingCertificate = true + return validateCertificateSigningRequestUpdate(newCSR, oldCSR, opts) +} + +func ValidateCertificateSigningRequestApprovalUpdate(newCSR, oldCSR *certificates.CertificateSigningRequest, version schema.GroupVersion) field.ErrorList { + opts := getValidationOptions(version, newCSR, oldCSR) + opts.allowSettingApprovalConditions = true + return validateCertificateSigningRequestUpdate(newCSR, oldCSR, opts) +} + +func validateCertificateSigningRequestUpdate(newCSR, oldCSR *certificates.CertificateSigningRequest, opts certificateValidationOptions) field.ErrorList { + validationErrorList := validateCertificateSigningRequest(newCSR, opts) metaUpdateErrorList := apivalidation.ValidateObjectMetaUpdate(&newCSR.ObjectMeta, &oldCSR.ObjectMeta, field.NewPath("metadata")) + + // prevent removal of existing Approved/Denied/Failed conditions + for _, t := range []certificates.RequestConditionType{certificates.CertificateApproved, certificates.CertificateDenied, certificates.CertificateFailed} { + oldConditions := findConditions(oldCSR, t) + newConditions := findConditions(newCSR, t) + if len(newConditions) < len(oldConditions) { + validationErrorList = append(validationErrorList, field.Forbidden(field.NewPath("status", "conditions"), fmt.Sprintf("updates may not remove a condition of type %q", t))) + } + } + + if !opts.allowSettingApprovalConditions { + // prevent addition/removal/modification of Approved/Denied conditions + for _, t := range []certificates.RequestConditionType{certificates.CertificateApproved, certificates.CertificateDenied} { + oldConditions := findConditions(oldCSR, t) + newConditions := findConditions(newCSR, t) + switch { + case len(newConditions) < len(oldConditions): + // removals are prevented above + case len(newConditions) > len(oldConditions): + validationErrorList = append(validationErrorList, field.Forbidden(field.NewPath("status", "conditions"), fmt.Sprintf("updates may not add a condition of type %q", t))) + case !apiequality.Semantic.DeepEqual(oldConditions, newConditions): + conditionDiff := diff.ObjectDiff(oldConditions, newConditions) + validationErrorList = append(validationErrorList, field.Forbidden(field.NewPath("status", "conditions"), fmt.Sprintf("updates may not modify a condition of type %q\n%v", t, conditionDiff))) + } + } + } + + if !bytes.Equal(newCSR.Status.Certificate, oldCSR.Status.Certificate) { + if !opts.allowSettingCertificate { + validationErrorList = append(validationErrorList, field.Forbidden(field.NewPath("status", "certificate"), "updates may not set certificate content")) + } else if !opts.allowResettingCertificate && len(oldCSR.Status.Certificate) > 0 { + validationErrorList = append(validationErrorList, field.Forbidden(field.NewPath("status", "certificate"), "updates may not modify existing certificate content")) + } + } + return append(validationErrorList, metaUpdateErrorList...) } + +// findConditions returns all instances of conditions of the specified type +func findConditions(csr *certificates.CertificateSigningRequest, conditionType certificates.RequestConditionType) []certificates.CertificateSigningRequestCondition { + var retval []certificates.CertificateSigningRequestCondition + for i, c := range csr.Status.Conditions { + if c.Type == conditionType { + retval = append(retval, csr.Status.Conditions[i]) + } + } + return retval +} + +// getValidationOptions returns the validation options to be +// compatible with the specified version and existing CSR. +// oldCSR may be nil if this is a create request. +// validation options related to subresource-specific capabilities are set to false. +func getValidationOptions(version schema.GroupVersion, newCSR, oldCSR *certificates.CertificateSigningRequest) certificateValidationOptions { + return certificateValidationOptions{ + allowResettingCertificate: allowResettingCertificate(version), + allowBothApprovedAndDenied: allowBothApprovedAndDenied(oldCSR), + allowLegacySignerName: allowLegacySignerName(version, oldCSR), + allowDuplicateConditionTypes: allowDuplicateConditionTypes(version, oldCSR), + allowEmptyConditionType: allowEmptyConditionType(version, oldCSR), + allowArbitraryCertificate: allowArbitraryCertificate(version, newCSR, oldCSR), + } +} + +func allowResettingCertificate(version schema.GroupVersion) bool { + // compatibility with v1beta1 + return version == certificatesv1beta1.SchemeGroupVersion +} + +func allowBothApprovedAndDenied(oldCSR *certificates.CertificateSigningRequest) bool { + if oldCSR == nil { + return false + } + approved := false + denied := false + for _, c := range oldCSR.Status.Conditions { + if c.Type == certificates.CertificateApproved { + approved = true + } else if c.Type == certificates.CertificateDenied { + denied = true + } + } + // compatibility with existing data + return approved && denied +} + +func allowLegacySignerName(version schema.GroupVersion, oldCSR *certificates.CertificateSigningRequest) bool { + switch { + case version == certificatesv1beta1.SchemeGroupVersion: + return true // compatibility with v1beta1 + case oldCSR != nil && oldCSR.Spec.SignerName == certificates.LegacyUnknownSignerName: + return true // compatibility with existing data + default: + return false + } +} + +func allowDuplicateConditionTypes(version schema.GroupVersion, oldCSR *certificates.CertificateSigningRequest) bool { + switch { + case version == certificatesv1beta1.SchemeGroupVersion: + return true // compatibility with v1beta1 + case oldCSR != nil && hasDuplicateConditionTypes(oldCSR): + return true // compatibility with existing data + default: + return false + } +} +func hasDuplicateConditionTypes(csr *certificates.CertificateSigningRequest) bool { + seen := map[certificates.RequestConditionType]bool{} + for _, c := range csr.Status.Conditions { + if seen[c.Type] { + return true + } + seen[c.Type] = true + } + return false +} + +func allowEmptyConditionType(version schema.GroupVersion, oldCSR *certificates.CertificateSigningRequest) bool { + switch { + case version == certificatesv1beta1.SchemeGroupVersion: + return true // compatibility with v1beta1 + case oldCSR != nil && hasEmptyConditionType(oldCSR): + return true // compatibility with existing data + default: + return false + } +} +func hasEmptyConditionType(csr *certificates.CertificateSigningRequest) bool { + for _, c := range csr.Status.Conditions { + if len(c.Type) == 0 { + return true + } + } + return false +} + +func allowArbitraryCertificate(version schema.GroupVersion, newCSR, oldCSR *certificates.CertificateSigningRequest) bool { + switch { + case version == certificatesv1beta1.SchemeGroupVersion: + return true // compatibility with v1beta1 + case newCSR != nil && oldCSR != nil && bytes.Equal(newCSR.Status.Certificate, oldCSR.Status.Certificate): + return true // tolerate updates that don't touch status.certificate + case oldCSR != nil && validateCertificate(oldCSR.Status.Certificate) != nil: + return true // compatibility with existing data + default: + return false + } +} diff --git a/pkg/apis/certificates/validation/validation_test.go b/pkg/apis/certificates/validation/validation_test.go index 12857c3fbd0..78675b664e9 100644 --- a/pkg/apis/certificates/validation/validation_test.go +++ b/pkg/apis/certificates/validation/validation_test.go @@ -28,9 +28,13 @@ import ( "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" - + "k8s.io/kubernetes/pkg/apis/certificates" capi "k8s.io/kubernetes/pkg/apis/certificates" + capiv1beta1 "k8s.io/kubernetes/pkg/apis/certificates/v1beta1" + "k8s.io/kubernetes/pkg/apis/core" ) var ( @@ -39,7 +43,7 @@ var ( validUsages = []capi.KeyUsage{capi.UsageKeyEncipherment} ) -func TestValidateCertificateSigningRequest(t *testing.T) { +func TestValidateCertificateSigningRequestCreate(t *testing.T) { specPath := field.NewPath("spec") // maxLengthSignerName is a signerName that is of maximum length, utilising // the max length specifications defined in validation.go. @@ -48,6 +52,7 @@ func TestValidateCertificateSigningRequest(t *testing.T) { maxLengthSignerName := fmt.Sprintf("%s/%s.%s", maxLengthFQDN, repeatString("a", 63), repeatString("a", 253)) tests := map[string]struct { csr capi.CertificateSigningRequest + gv schema.GroupVersion errs field.ErrorList }{ "CSR with empty request data should fail": { @@ -261,7 +266,7 @@ func TestValidateCertificateSigningRequest(t *testing.T) { } for name, test := range tests { t.Run(name, func(t *testing.T) { - el := ValidateCertificateSigningRequest(&test.csr) + el := ValidateCertificateSigningRequestCreate(&test.csr, test.gv) if !reflect.DeepEqual(el, test.errs) { t.Errorf("returned and expected errors did not match - expected %v but got %v", test.errs.ToAggregate(), el.ToAggregate()) } @@ -306,3 +311,812 @@ func newCSRPEM(t *testing.T) []byte { return p } + +func Test_getValidationOptions(t *testing.T) { + tests := []struct { + name string + version schema.GroupVersion + newCSR *certificates.CertificateSigningRequest + oldCSR *certificates.CertificateSigningRequest + want certificateValidationOptions + }{ + { + name: "v1beta1 compatible create", + version: capiv1beta1.SchemeGroupVersion, + oldCSR: nil, + want: certificateValidationOptions{ + allowResettingCertificate: true, + allowBothApprovedAndDenied: false, + allowLegacySignerName: true, + allowDuplicateConditionTypes: true, + allowEmptyConditionType: true, + allowArbitraryCertificate: true, + }, + }, + { + name: "v1 strict create", + version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"}, + oldCSR: nil, + want: certificateValidationOptions{}, + }, + { + name: "v1beta1 compatible update", + version: capiv1beta1.SchemeGroupVersion, + oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved}, {Type: capi.CertificateDenied}}, + }}, + want: certificateValidationOptions{ + allowResettingCertificate: true, + allowBothApprovedAndDenied: true, // existing object has both approved and denied + allowLegacySignerName: true, + allowDuplicateConditionTypes: true, + allowEmptyConditionType: true, + allowArbitraryCertificate: true, + }, + }, + { + name: "v1 strict update", + version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"}, + oldCSR: &capi.CertificateSigningRequest{}, + want: certificateValidationOptions{}, + }, + { + name: "v1 compatible update, approved+denied", + version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"}, + oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved}, {Type: capi.CertificateDenied}}, + }}, + want: certificateValidationOptions{ + allowBothApprovedAndDenied: true, + }, + }, + { + name: "v1 compatible update, legacy signerName", + version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"}, + oldCSR: &capi.CertificateSigningRequest{Spec: capi.CertificateSigningRequestSpec{SignerName: capi.LegacyUnknownSignerName}}, + want: certificateValidationOptions{ + allowLegacySignerName: true, + }, + }, + { + name: "v1 compatible update, duplicate condition types", + version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"}, + oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved}, {Type: capi.CertificateApproved}}, + }}, + want: certificateValidationOptions{ + allowDuplicateConditionTypes: true, + }, + }, + { + name: "v1 compatible update, empty condition types", + version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"}, + oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{}}, + }}, + want: certificateValidationOptions{ + allowEmptyConditionType: true, + }, + }, + { + name: "v1 compatible update, no diff to certificate", + version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"}, + newCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{ + Certificate: validCertificate, + }}, + oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{ + Certificate: validCertificate, + }}, + want: certificateValidationOptions{ + allowArbitraryCertificate: true, + }, + }, + { + name: "v1 compatible update, existing invalid certificate", + version: schema.GroupVersion{Group: "certificates.k8s.io", Version: "v1"}, + newCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{ + Certificate: []byte(`new - no PEM blocks`), + }}, + oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{ + Certificate: []byte(`old - no PEM blocks`), + }}, + want: certificateValidationOptions{ + allowArbitraryCertificate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getValidationOptions(tt.version, tt.newCSR, tt.oldCSR); !reflect.DeepEqual(got, tt.want) { + t.Errorf("got %#v\nwant %#v", got, tt.want) + } + }) + } +} + +func TestValidateCertificateSigningRequestUpdate(t *testing.T) { + validUpdateMeta := validObjectMeta + validUpdateMeta.ResourceVersion = "1" + + validUpdateMetaWithFinalizers := validUpdateMeta + validUpdateMetaWithFinalizers.Finalizers = []string{"foo"} + + validSpec := capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + SignerName: "example.com/something", + } + + tests := []struct { + name string + newCSR *certificates.CertificateSigningRequest + oldCSR *certificates.CertificateSigningRequest + versionErrs map[string][]string + }{ + { + name: "no-op", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec}, + }, + { + name: "finalizer change with invalid status", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}}, + }, + { + name: "add Approved condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}}, + }}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec}, + versionErrs: map[string][]string{ + "v1": {`status.conditions: Forbidden: updates may not add a condition of type "Approved"`}, + "v1beta1": {`status.conditions: Forbidden: updates may not add a condition of type "Approved"`}, + }, + }, + { + name: "remove Approved condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}}, + }}, + versionErrs: map[string][]string{ + "v1": {`status.conditions: Forbidden: updates may not remove a condition of type "Approved"`}, + "v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Approved"`}, + }, + }, + { + name: "add Denied condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}}, + }}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec}, + versionErrs: map[string][]string{ + "v1": {`status.conditions: Forbidden: updates may not add a condition of type "Denied"`}, + "v1beta1": {`status.conditions: Forbidden: updates may not add a condition of type "Denied"`}, + }, + }, + { + name: "remove Denied condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}}, + }}, + versionErrs: map[string][]string{ + "v1": {`status.conditions: Forbidden: updates may not remove a condition of type "Denied"`}, + "v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Denied"`}, + }, + }, + { + name: "add Failed condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}}, + }}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec}, + versionErrs: map[string][]string{}, + }, + { + name: "remove Failed condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}}, + }}, + versionErrs: map[string][]string{ + "v1": {`status.conditions: Forbidden: updates may not remove a condition of type "Failed"`}, + "v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Failed"`}, + }, + }, + { + name: "set certificate", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Certificate: validCertificate, + }}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec}, + versionErrs: map[string][]string{ + "v1": {`status.certificate: Forbidden: updates may not set certificate content`}, + "v1beta1": {`status.certificate: Forbidden: updates may not set certificate content`}, + }, + }, + } + + for _, tt := range tests { + for _, version := range []string{"v1", "v1beta1"} { + t.Run(tt.name+"_"+version, func(t *testing.T) { + gotErrs := sets.NewString() + for _, err := range ValidateCertificateSigningRequestUpdate(tt.newCSR, tt.oldCSR, schema.GroupVersion{Group: certificates.GroupName, Version: version}) { + gotErrs.Insert(err.Error()) + } + wantErrs := sets.NewString(tt.versionErrs[version]...) + for _, missing := range wantErrs.Difference(gotErrs).List() { + t.Errorf("missing expected error: %s", missing) + } + for _, unexpected := range gotErrs.Difference(wantErrs).List() { + t.Errorf("unexpected error: %s", unexpected) + } + }) + } + } +} + +func TestValidateCertificateSigningRequestStatusUpdate(t *testing.T) { + validUpdateMeta := validObjectMeta + validUpdateMeta.ResourceVersion = "1" + + validUpdateMetaWithFinalizers := validUpdateMeta + validUpdateMetaWithFinalizers.Finalizers = []string{"foo"} + + validSpec := capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + SignerName: "example.com/something", + } + + tests := []struct { + name string + newCSR *certificates.CertificateSigningRequest + oldCSR *certificates.CertificateSigningRequest + versionErrs map[string][]string + }{ + { + name: "no-op", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec}, + }, + { + name: "finalizer change with invalid status", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}}, + }, + { + name: "add Approved condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}}, + }}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec}, + versionErrs: map[string][]string{ + "v1": {`status.conditions: Forbidden: updates may not add a condition of type "Approved"`}, + "v1beta1": {`status.conditions: Forbidden: updates may not add a condition of type "Approved"`}, + }, + }, + { + name: "remove Approved condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}}, + }}, + versionErrs: map[string][]string{ + "v1": {`status.conditions: Forbidden: updates may not remove a condition of type "Approved"`}, + "v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Approved"`}, + }, + }, + { + name: "add Denied condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}}, + }}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec}, + versionErrs: map[string][]string{ + "v1": {`status.conditions: Forbidden: updates may not add a condition of type "Denied"`}, + "v1beta1": {`status.conditions: Forbidden: updates may not add a condition of type "Denied"`}, + }, + }, + { + name: "remove Denied condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}}, + }}, + versionErrs: map[string][]string{ + "v1": {`status.conditions: Forbidden: updates may not remove a condition of type "Denied"`}, + "v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Denied"`}, + }, + }, + { + name: "add Failed condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}}, + }}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec}, + versionErrs: map[string][]string{}, + }, + { + name: "remove Failed condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}}, + }}, + versionErrs: map[string][]string{ + "v1": {`status.conditions: Forbidden: updates may not remove a condition of type "Failed"`}, + "v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Failed"`}, + }, + }, + { + name: "set valid certificate", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Certificate: validCertificate, + }}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec}, + versionErrs: map[string][]string{}, + }, + { + name: "set invalid certificate", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Certificate: invalidCertificateNoPEM, + }}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec}, + versionErrs: map[string][]string{ + "v1": {`status.certificate: Invalid value: "": must contain at least one CERTIFICATE PEM block`}, + }, + }, + { + name: "reset certificate", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Certificate: invalidCertificateNonCertificatePEM, + }}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Certificate: invalidCertificateNoPEM, + }}, + versionErrs: map[string][]string{ + "v1": {`status.certificate: Forbidden: updates may not modify existing certificate content`}, + }, + }, + } + + for _, tt := range tests { + for _, version := range []string{"v1", "v1beta1"} { + t.Run(tt.name+"_"+version, func(t *testing.T) { + gotErrs := sets.NewString() + for _, err := range ValidateCertificateSigningRequestStatusUpdate(tt.newCSR, tt.oldCSR, schema.GroupVersion{Group: certificates.GroupName, Version: version}) { + gotErrs.Insert(err.Error()) + } + wantErrs := sets.NewString(tt.versionErrs[version]...) + for _, missing := range wantErrs.Difference(gotErrs).List() { + t.Errorf("missing expected error: %s", missing) + } + for _, unexpected := range gotErrs.Difference(wantErrs).List() { + t.Errorf("unexpected error: %s", unexpected) + } + }) + } + } +} + +func TestValidateCertificateSigningRequestApprovalUpdate(t *testing.T) { + validUpdateMeta := validObjectMeta + validUpdateMeta.ResourceVersion = "1" + + validUpdateMetaWithFinalizers := validUpdateMeta + validUpdateMetaWithFinalizers.Finalizers = []string{"foo"} + + validSpec := capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + SignerName: "example.com/something", + } + + tests := []struct { + name string + newCSR *certificates.CertificateSigningRequest + oldCSR *certificates.CertificateSigningRequest + versionErrs map[string][]string + }{ + { + name: "no-op", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec}, + }, + { + name: "finalizer change with invalid certificate", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}}, + }, + { + name: "add Approved condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}}, + }}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec}, + }, + { + name: "remove Approved condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}}, + }}, + versionErrs: map[string][]string{ + "v1": {`status.conditions: Forbidden: updates may not remove a condition of type "Approved"`}, + "v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Approved"`}, + }, + }, + { + name: "add Denied condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}}, + }}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec}, + }, + { + name: "remove Denied condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}}, + }}, + versionErrs: map[string][]string{ + "v1": {`status.conditions: Forbidden: updates may not remove a condition of type "Denied"`}, + "v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Denied"`}, + }, + }, + { + name: "add Failed condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}}, + }}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec}, + versionErrs: map[string][]string{}, + }, + { + name: "remove Failed condition", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}}, + }}, + versionErrs: map[string][]string{ + "v1": {`status.conditions: Forbidden: updates may not remove a condition of type "Failed"`}, + "v1beta1": {`status.conditions: Forbidden: updates may not remove a condition of type "Failed"`}, + }, + }, + { + name: "set certificate", + newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{ + Certificate: validCertificate, + }}, + oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec}, + versionErrs: map[string][]string{ + "v1": {`status.certificate: Forbidden: updates may not set certificate content`}, + "v1beta1": {`status.certificate: Forbidden: updates may not set certificate content`}, + }, + }, + } + + for _, tt := range tests { + for _, version := range []string{"v1", "v1beta1"} { + t.Run(tt.name+"_"+version, func(t *testing.T) { + gotErrs := sets.NewString() + for _, err := range ValidateCertificateSigningRequestApprovalUpdate(tt.newCSR, tt.oldCSR, schema.GroupVersion{Group: certificates.GroupName, Version: version}) { + gotErrs.Insert(err.Error()) + } + wantErrs := sets.NewString(tt.versionErrs[version]...) + for _, missing := range wantErrs.Difference(gotErrs).List() { + t.Errorf("missing expected error: %s", missing) + } + for _, unexpected := range gotErrs.Difference(wantErrs).List() { + t.Errorf("unexpected error: %s", unexpected) + } + }) + } + } +} + +// Test_validateCertificateSigningRequestOptions verifies validation options are effective in tolerating specific aspects of CSRs +func Test_validateCertificateSigningRequestOptions(t *testing.T) { + validSpec := capi.CertificateSigningRequestSpec{ + Usages: validUsages, + Request: newCSRPEM(t), + SignerName: "example.com/something", + } + + tests := []struct { + // testcase name + name string + + // csr being validated + csr *certificates.CertificateSigningRequest + + // options that allow the csr to pass validation + lenientOpts certificateValidationOptions + + // expected errors when validating strictly + strictErrs []string + }{ + // valid strict cases + { + name: "no status", + csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec}, + }, + { + name: "approved condition", + csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec, + Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}}, + }, + }, + }, + { + name: "denied condition", + csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec, + Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}}, + }, + }, + }, + { + name: "failed condition", + csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec, + Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}}, + }, + }, + }, + { + name: "approved+issued", + csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec, + Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}}, + Certificate: validCertificate, + }, + }, + }, + + // legacy signer + { + name: "legacy signer", + csr: &capi.CertificateSigningRequest{ + ObjectMeta: validObjectMeta, + Spec: func() capi.CertificateSigningRequestSpec { + specCopy := validSpec + specCopy.SignerName = capi.LegacyUnknownSignerName + return specCopy + }(), + }, + lenientOpts: certificateValidationOptions{allowLegacySignerName: true}, + strictErrs: []string{`spec.signerName: Invalid value: "kubernetes.io/legacy-unknown": the legacy signerName is not allowed via this API version`}, + }, + + // invalid condition cases + { + name: "empty condition type", + csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec, + Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Status: core.ConditionTrue}}, + }, + }, + lenientOpts: certificateValidationOptions{allowEmptyConditionType: true}, + strictErrs: []string{`status.conditions[0].type: Required value`}, + }, + { + name: "approved and denied", + csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec, + Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}, {Type: capi.CertificateDenied, Status: core.ConditionTrue}}, + }, + }, + lenientOpts: certificateValidationOptions{allowBothApprovedAndDenied: true}, + strictErrs: []string{`status.conditions[1].type: Invalid value: "Denied": Approved and Denied conditions are mutually exclusive`}, + }, + { + name: "duplicate condition", + csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec, + Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}, {Type: capi.CertificateApproved, Status: core.ConditionTrue}}, + }, + }, + lenientOpts: certificateValidationOptions{allowDuplicateConditionTypes: true}, + strictErrs: []string{`status.conditions[1].type: Duplicate value: "Approved"`}, + }, + + // invalid allowArbitraryCertificate cases + { + name: "status.certificate, no PEM", + csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec, + Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}}, + Certificate: invalidCertificateNoPEM, + }, + }, + lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true}, + strictErrs: []string{`status.certificate: Invalid value: "": must contain at least one CERTIFICATE PEM block`}, + }, + { + name: "status.certificate, non-CERTIFICATE PEM", + csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec, + Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}}, + Certificate: invalidCertificateNonCertificatePEM, + }, + }, + lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true}, + strictErrs: []string{`status.certificate: Invalid value: "": only CERTIFICATE PEM blocks are allowed, found "CERTIFICATE1"`}, + }, + { + name: "status.certificate, PEM headers", + csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec, + Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}}, + Certificate: invalidCertificatePEMHeaders, + }, + }, + lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true}, + strictErrs: []string{`status.certificate: Invalid value: "": no PEM block headers are permitted`}, + }, + { + name: "status.certificate, non-base64 PEM", + csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec, + Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}}, + Certificate: invalidCertificateNonBase64PEM, + }, + }, + lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true}, + strictErrs: []string{`status.certificate: Invalid value: "": must contain at least one CERTIFICATE PEM block`}, + }, + { + name: "status.certificate, empty PEM block", + csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec, + Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}}, + Certificate: invalidCertificateEmptyPEM, + }, + }, + lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true}, + strictErrs: []string{`status.certificate: Invalid value: "": found CERTIFICATE PEM block containing 0 certificates`}, + }, + { + name: "status.certificate, non-ASN1 data", + csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec, + Status: capi.CertificateSigningRequestStatus{ + Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}}, + Certificate: invalidCertificateNonASN1Data, + }, + }, + lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true}, + strictErrs: []string{`status.certificate: Invalid value: "": asn1: structure error: sequence tag mismatch`}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // make sure the lenient options validate with no errors + for _, err := range validateCertificateSigningRequest(tt.csr, tt.lenientOpts) { + t.Errorf("unexpected error with lenient options: %s", err.Error()) + } + + // make sure the strict options produce the expected errors + gotErrs := sets.NewString() + for _, err := range validateCertificateSigningRequest(tt.csr, certificateValidationOptions{}) { + gotErrs.Insert(err.Error()) + } + wantErrs := sets.NewString(tt.strictErrs...) + for _, missing := range wantErrs.Difference(gotErrs).List() { + t.Errorf("missing expected strict error: %s", missing) + } + for _, unexpected := range gotErrs.Difference(wantErrs).List() { + t.Errorf("unexpected strict error: %s", unexpected) + } + }) + } +} + +var ( + validCertificate = []byte(` +Leading non-PEM content +-----BEGIN CERTIFICATE----- +MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw +GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx +MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb +KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC +BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU +K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q +a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5 +MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps= +-----END CERTIFICATE----- +Intermediate non-PEM content +-----BEGIN CERTIFICATE----- +MIIBqDCCAU6gAwIBAgIUfqZtjoFgczZ+oQZbEC/BDSS2J6wwCgYIKoZIzj0EAwIw +EjEQMA4GA1UEAxMHUm9vdC1DQTAgFw0xNjEwMTEwNTA2MDBaGA8yMTE2MDkxNzA1 +MDYwMFowGjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEyWHEMMCctJg8Xa5YWLqaCPbk3MjB+uvXac42JM9pj4k9jedD +kpUJRkWIPzgJI8Zk/3cSzluUTixP6JBSDKtwwaN4MHYwDgYDVR0PAQH/BAQDAgGm +MBMGA1UdJQQMMAoGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FF+p0JcY31pz+mjNZnjv0Gum92vZMB8GA1UdIwQYMBaAFB7P6+i4/pfNjqZgJv/b +dgA7Fe4tMAoGCCqGSM49BAMCA0gAMEUCIQCTT1YWQZaAqfQ2oBxzOkJE2BqLFxhz +3smQlrZ5gCHddwIgcvT7puhYOzAgcvMn9+SZ1JOyZ7edODjshCVCRnuHK2c= +-----END CERTIFICATE----- +Trailing non-PEM content +`) + + invalidCertificateNoPEM = []byte(`no PEM content`) + + invalidCertificateNonCertificatePEM = []byte(` +Leading non-PEM content +-----BEGIN CERTIFICATE1----- +MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw +GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx +MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb +KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC +BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU +K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q +a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5 +MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps= +-----END CERTIFICATE1----- +Trailing non-PEM content +`) + + invalidCertificatePEMHeaders = []byte(` +Leading non-PEM content +-----BEGIN CERTIFICATE----- +Some-Header: Some-Value +MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw +GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx +MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb +KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC +BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU +K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q +a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5 +MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps= +-----END CERTIFICATE----- +Trailing non-PEM content +`) + + invalidCertificateNonBase64PEM = []byte(` +Leading non-PEM content +-----BEGIN CERTIFICATE----- +MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw +GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx +MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb +KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC +BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU +K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q +a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5 +MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1d????????? +-----END CERTIFICATE----- +Trailing non-PEM content +`) + + invalidCertificateEmptyPEM = []byte(` +Leading non-PEM content +-----BEGIN CERTIFICATE----- +-----END CERTIFICATE----- +Trailing non-PEM content +`) + + // first character is invalid + invalidCertificateNonASN1Data = []byte(` +Leading non-PEM content +-----BEGIN CERTIFICATE----- +MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw +GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx +MTYwOTE3MDUwNjAwWjAUNRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb +KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC +BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU +K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q +a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5 +MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps= +-----END CERTIFICATE----- +Trailing non-PEM content +`) +) diff --git a/pkg/apis/certificates/zz_generated.deepcopy.go b/pkg/apis/certificates/zz_generated.deepcopy.go index 23636909de0..b0e748b6c11 100644 --- a/pkg/apis/certificates/zz_generated.deepcopy.go +++ b/pkg/apis/certificates/zz_generated.deepcopy.go @@ -56,6 +56,7 @@ func (in *CertificateSigningRequest) DeepCopyObject() runtime.Object { func (in *CertificateSigningRequestCondition) DeepCopyInto(out *CertificateSigningRequestCondition) { *out = *in in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime) + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) return } diff --git a/pkg/controller/certificates/BUILD b/pkg/controller/certificates/BUILD index 8e45c7dd1e3..3e9133793a1 100644 --- a/pkg/controller/certificates/BUILD +++ b/pkg/controller/certificates/BUILD @@ -16,6 +16,7 @@ go_library( "//pkg/apis/certificates/v1beta1:go_default_library", "//pkg/controller:go_default_library", "//staging/src/k8s.io/api/certificates/v1beta1:go_default_library", + "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", diff --git a/pkg/controller/certificates/certificate_controller.go b/pkg/controller/certificates/certificate_controller.go index e2dc80e8ac2..e030a945407 100644 --- a/pkg/controller/certificates/certificate_controller.go +++ b/pkg/controller/certificates/certificate_controller.go @@ -186,7 +186,7 @@ func (cc *CertificateController) syncFunc(key string) error { return err } - if csr.Status.Certificate != nil { + if len(csr.Status.Certificate) > 0 { // no need to do anything because it already has a cert return nil } diff --git a/pkg/controller/certificates/certificate_controller_utils.go b/pkg/controller/certificates/certificate_controller_utils.go index 4a5d0d4f2bb..941a72dca23 100644 --- a/pkg/controller/certificates/certificate_controller_utils.go +++ b/pkg/controller/certificates/certificate_controller_utils.go @@ -16,7 +16,10 @@ limitations under the License. package certificates -import certificates "k8s.io/api/certificates/v1beta1" +import ( + certificates "k8s.io/api/certificates/v1beta1" + v1 "k8s.io/api/core/v1" +) // IsCertificateRequestApproved returns true if a certificate request has the // "Approved" condition and no "Denied" conditions; false otherwise. @@ -25,6 +28,16 @@ func IsCertificateRequestApproved(csr *certificates.CertificateSigningRequest) b return approved && !denied } +// HasCondition returns true if the csr contains a condition of the specified type with a status that is set to True or is empty +func HasTrueCondition(csr *certificates.CertificateSigningRequest, conditionType certificates.RequestConditionType) bool { + for _, c := range csr.Status.Conditions { + if c.Type == conditionType && (len(c.Status) == 0 || c.Status == v1.ConditionTrue) { + return true + } + } + return false +} + func GetCertApprovalCondition(status *certificates.CertificateSigningRequestStatus) (approved bool, denied bool) { for _, c := range status.Conditions { if c.Type == certificates.CertificateApproved { diff --git a/pkg/controller/certificates/cleaner/cleaner.go b/pkg/controller/certificates/cleaner/cleaner.go index 22e38936065..b62d5510743 100644 --- a/pkg/controller/certificates/cleaner/cleaner.go +++ b/pkg/controller/certificates/cleaner/cleaner.go @@ -47,6 +47,7 @@ const ( // cleaned up. approvedExpiration = 1 * time.Hour deniedExpiration = 1 * time.Hour + failedExpiration = 1 * time.Hour pendingExpiration = 24 * time.Hour ) @@ -108,7 +109,7 @@ func (ccc *CSRCleanerController) handle(csr *capi.CertificateSigningRequest) err if err != nil { return err } - if isIssuedPastDeadline(csr) || isDeniedPastDeadline(csr) || isPendingPastDeadline(csr) || isIssuedExpired { + if isIssuedPastDeadline(csr) || isDeniedPastDeadline(csr) || isFailedPastDeadline(csr) || isPendingPastDeadline(csr) || isIssuedExpired { if err := ccc.csrClient.Delete(context.TODO(), csr.Name, metav1.DeleteOptions{}); err != nil { return fmt.Errorf("unable to delete CSR %q: %v", csr.Name, err) } @@ -158,6 +159,19 @@ func isDeniedPastDeadline(csr *capi.CertificateSigningRequest) bool { return false } +// isFailedPastDeadline checks if the certificate has a Failed status and the +// creation time of the CSR is passed the deadline that pending requests are +// maintained for. +func isFailedPastDeadline(csr *capi.CertificateSigningRequest) bool { + for _, c := range csr.Status.Conditions { + if c.Type == capi.CertificateFailed && isOlderThan(c.LastUpdateTime, deniedExpiration) { + klog.Infof("Cleaning CSR %q as it is more than %v old and failed.", csr.Name, deniedExpiration) + return true + } + } + return false +} + // isIssuedPastDeadline checks if the certificate has an Issued status and the // creation time of the CSR is passed the deadline that issued requests are // maintained for. @@ -180,13 +194,13 @@ func isOlderThan(t metav1.Time, d time.Duration) bool { // 'Issued' status. Implicitly, if there is a certificate associated with the // CSR, the CSR statuses that are visible via `kubectl` will include 'Issued'. func isIssued(csr *capi.CertificateSigningRequest) bool { - return csr.Status.Certificate != nil + return len(csr.Status.Certificate) > 0 } // isExpired checks if the CSR has a certificate and the date in the `NotAfter` // field has gone by. func isExpired(csr *capi.CertificateSigningRequest) (bool, error) { - if csr.Status.Certificate == nil { + if len(csr.Status.Certificate) == 0 { return false, nil } block, _ := pem.Decode(csr.Status.Certificate) @@ -197,5 +211,8 @@ func isExpired(csr *capi.CertificateSigningRequest) (bool, error) { if err != nil { return false, fmt.Errorf("unable to parse certificate data: %v", err) } + if len(certs) == 0 { + return false, fmt.Errorf("no certificates found") + } return time.Now().After(certs[0].NotAfter), nil } diff --git a/pkg/controller/certificates/cleaner/cleaner_test.go b/pkg/controller/certificates/cleaner/cleaner_test.go index 7a8d8f6e1d3..4d9303d0c11 100644 --- a/pkg/controller/certificates/cleaner/cleaner_test.go +++ b/pkg/controller/certificates/cleaner/cleaner_test.go @@ -124,6 +124,38 @@ func TestCleanerWithApprovedExpiredCSR(t *testing.T) { }, []string{"delete"}, }, + { + "no delete failed not passed deadline", + metav1.NewTime(time.Now().Add(-1 * time.Minute)), + nil, + []capi.CertificateSigningRequestCondition{ + { + Type: capi.CertificateApproved, + LastUpdateTime: metav1.NewTime(time.Now().Add(-2 * time.Hour)), + }, + { + Type: capi.CertificateFailed, + LastUpdateTime: metav1.NewTime(time.Now().Add(-50 * time.Minute)), + }, + }, + []string{}, + }, + { + "delete failed passed deadline", + metav1.NewTime(time.Now().Add(-1 * time.Minute)), + nil, + []capi.CertificateSigningRequestCondition{ + { + Type: capi.CertificateApproved, + LastUpdateTime: metav1.NewTime(time.Now().Add(-2 * time.Hour)), + }, + { + Type: capi.CertificateFailed, + LastUpdateTime: metav1.NewTime(time.Now().Add(-2 * time.Hour)), + }, + }, + []string{"delete"}, + }, { "no delete pending not passed deadline", metav1.NewTime(time.Now().Add(-5 * time.Hour)), diff --git a/pkg/controller/certificates/signer/BUILD b/pkg/controller/certificates/signer/BUILD index c66c4024264..46ca96560c4 100644 --- a/pkg/controller/certificates/signer/BUILD +++ b/pkg/controller/certificates/signer/BUILD @@ -17,6 +17,7 @@ go_test( embed = [":go_default_library"], deps = [ "//pkg/apis/certificates/v1beta1:go_default_library", + "//pkg/controller/certificates:go_default_library", "//staging/src/k8s.io/api/certificates/v1beta1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/clock:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", @@ -39,6 +40,7 @@ go_library( "//pkg/controller/certificates:go_default_library", "//pkg/controller/certificates/authority:go_default_library", "//staging/src/k8s.io/api/certificates/v1beta1:go_default_library", + "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apiserver/pkg/server/dynamiccertificates:go_default_library", "//staging/src/k8s.io/client-go/informers/certificates/v1beta1:go_default_library", diff --git a/pkg/controller/certificates/signer/signer.go b/pkg/controller/certificates/signer/signer.go index 452e727b08a..986a95f94c8 100644 --- a/pkg/controller/certificates/signer/signer.go +++ b/pkg/controller/certificates/signer/signer.go @@ -25,6 +25,7 @@ import ( "time" capi "k8s.io/api/certificates/v1beta1" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apiserver/pkg/server/dynamiccertificates" certificatesinformers "k8s.io/client-go/informers/certificates/v1beta1" @@ -106,7 +107,7 @@ func (c *CSRSigningController) Run(workers int, stopCh <-chan struct{}) { c.certificateController.Run(workers, stopCh) } -type isRequestForSignerFunc func(req *x509.CertificateRequest, usages []capi.KeyUsage, signerName string) bool +type isRequestForSignerFunc func(req *x509.CertificateRequest, usages []capi.KeyUsage, signerName string) (bool, error) type signer struct { caProvider *caProvider @@ -139,8 +140,8 @@ func newSigner(signerName, caFile, caKeyFile string, client clientset.Interface, } func (s *signer) handle(csr *capi.CertificateSigningRequest) error { - // Ignore unapproved requests - if !certificates.IsCertificateRequestApproved(csr) { + // Ignore unapproved or failed requests + if !certificates.IsCertificateRequestApproved(csr) || certificates.HasTrueCondition(csr, capi.CertificateFailed) { return nil } @@ -153,9 +154,21 @@ func (s *signer) handle(csr *capi.CertificateSigningRequest) error { if err != nil { return fmt.Errorf("unable to parse csr %q: %v", csr.Name, err) } - if !s.isRequestForSignerFn(x509cr, csr.Spec.Usages, *csr.Spec.SignerName) { - // TODO: mark the CertificateRequest as being in a terminal state and - // communicate to the user why the request has been refused. + if recognized, err := s.isRequestForSignerFn(x509cr, csr.Spec.Usages, *csr.Spec.SignerName); err != nil { + csr.Status.Conditions = append(csr.Status.Conditions, capi.CertificateSigningRequestCondition{ + Type: capi.CertificateFailed, + Status: v1.ConditionTrue, + Reason: "SignerValidationFailure", + Message: err.Error(), + LastUpdateTime: metav1.Now(), + }) + _, err = s.client.CertificatesV1beta1().CertificateSigningRequests().UpdateStatus(context.TODO(), csr, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("error adding failure condition for csr: %v", err) + } + return nil + } else if !recognized { + // Ignore requests for kubernetes.io signerNames we don't recognize return nil } cert, err := s.sign(x509cr, csr.Spec.Usages) @@ -201,41 +214,41 @@ func getCSRVerificationFuncForSignerName(signerName string) (isRequestForSignerF // TODO type this error so that a different reporting loop (one without a signing cert), can mark // CSRs with unknown kube signers as terminal if we wish. This largely depends on how tightly we want to control // our signerNames. - return nil, fmt.Errorf("unrecongized signerName: %q", signerName) + return nil, fmt.Errorf("unrecognized signerName: %q", signerName) } } -func isKubeletServing(req *x509.CertificateRequest, usages []capi.KeyUsage, signerName string) bool { +func isKubeletServing(req *x509.CertificateRequest, usages []capi.KeyUsage, signerName string) (bool, error) { if signerName != capi.KubeletServingSignerName { - return false + return false, nil } - return capihelper.IsKubeletServingCSR(req, usages) + return true, capihelper.ValidateKubeletServingCSR(req, usages) } -func isKubeletClient(req *x509.CertificateRequest, usages []capi.KeyUsage, signerName string) bool { +func isKubeletClient(req *x509.CertificateRequest, usages []capi.KeyUsage, signerName string) (bool, error) { if signerName != capi.KubeAPIServerClientKubeletSignerName { - return false + return false, nil } - return capihelper.IsKubeletClientCSR(req, usages) + return true, capihelper.ValidateKubeletClientCSR(req, usages) } -func isKubeAPIServerClient(req *x509.CertificateRequest, usages []capi.KeyUsage, signerName string) bool { +func isKubeAPIServerClient(req *x509.CertificateRequest, usages []capi.KeyUsage, signerName string) (bool, error) { if signerName != capi.KubeAPIServerClientSignerName { - return false + return false, nil } - return validAPIServerClientUsages(usages) + return true, validAPIServerClientUsages(usages) } -func isLegacyUnknown(req *x509.CertificateRequest, usages []capi.KeyUsage, signerName string) bool { +func isLegacyUnknown(req *x509.CertificateRequest, usages []capi.KeyUsage, signerName string) (bool, error) { if signerName != capi.LegacyUnknownSignerName { - return false + return false, nil } // No restrictions are applied to the legacy-unknown signerName to // maintain backward compatibility in v1beta1. - return true + return true, nil } -func validAPIServerClientUsages(usages []capi.KeyUsage) bool { +func validAPIServerClientUsages(usages []capi.KeyUsage) error { hasClientAuth := false for _, u := range usages { switch u { @@ -244,8 +257,11 @@ func validAPIServerClientUsages(usages []capi.KeyUsage) bool { case capi.UsageClientAuth: hasClientAuth = true default: - return false + return fmt.Errorf("invalid usage for client certificate: %s", u) } } - return hasClientAuth + if !hasClientAuth { + return fmt.Errorf("missing required usage for client certificate: %s", capi.UsageClientAuth) + } + return nil } diff --git a/pkg/controller/certificates/signer/signer_test.go b/pkg/controller/certificates/signer/signer_test.go index baeeb1420a3..b44c3cb4772 100644 --- a/pkg/controller/certificates/signer/signer_test.go +++ b/pkg/controller/certificates/signer/signer_test.go @@ -35,6 +35,7 @@ import ( "k8s.io/client-go/kubernetes/fake" testclient "k8s.io/client-go/testing" "k8s.io/client-go/util/cert" + "k8s.io/kubernetes/pkg/controller/certificates" capihelper "k8s.io/kubernetes/pkg/apis/certificates/v1beta1" ) @@ -114,6 +115,8 @@ func TestHandle(t *testing.T) { usages []capi.KeyUsage // whether the generated CSR should be marked as approved approved bool + // whether the generated CSR should be marked as failed + failed bool // the signerName to be set on the generated CSR signerName string // if true, expect an error to be returned @@ -149,10 +152,17 @@ func TestHandle(t *testing.T) { usages: []capi.KeyUsage{capi.UsageServerAuth, capi.UsageClientAuth, capi.UsageDigitalSignature, capi.UsageKeyEncipherment}, approved: true, verify: func(t *testing.T, as []testclient.Action) { - if len(as) != 0 { - t.Errorf("expected no Update action but got %d", len(as)) + if len(as) != 1 { + t.Errorf("expected one Update action but got %d", len(as)) return } + csr := as[0].(testclient.UpdateAction).GetObject().(*capi.CertificateSigningRequest) + if len(csr.Status.Certificate) != 0 { + t.Errorf("expected no certificate to be issued") + } + if !certificates.HasTrueCondition(csr, capi.CertificateFailed) { + t.Errorf("expected Failed condition") + } }, }, { @@ -207,6 +217,21 @@ func TestHandle(t *testing.T) { } }, }, + { + name: "should do nothing if failed", + signerName: "kubernetes.io/kubelet-serving", + commonName: "system:node:testnode", + org: []string{"system:nodes"}, + usages: []capi.KeyUsage{capi.UsageServerAuth, capi.UsageDigitalSignature, capi.UsageKeyEncipherment}, + dnsNames: []string{"example.com"}, + approved: true, + failed: true, + verify: func(t *testing.T, as []testclient.Action) { + if len(as) != 0 { + t.Errorf("expected no action to be taken") + } + }, + }, { name: "should do nothing if an unrecognised signerName is used", signerName: "kubernetes.io/not-recognised", @@ -267,7 +292,7 @@ func TestHandle(t *testing.T) { // continue with rest of test } - csr := makeTestCSR(csrBuilder{cn: c.commonName, signerName: c.signerName, approved: c.approved, usages: c.usages, org: c.org, dnsNames: c.dnsNames}) + csr := makeTestCSR(csrBuilder{cn: c.commonName, signerName: c.signerName, approved: c.approved, failed: c.failed, usages: c.usages, org: c.org, dnsNames: c.dnsNames}) if err := s.handle(csr); err != nil && !c.err { t.Errorf("unexpected err: %v", err) } @@ -286,6 +311,7 @@ type csrBuilder struct { org []string signerName string approved bool + failed bool usages []capi.KeyUsage } @@ -318,5 +344,10 @@ func makeTestCSR(b csrBuilder) *capi.CertificateSigningRequest { Type: capi.CertificateApproved, }) } + if b.failed { + csr.Status.Conditions = append(csr.Status.Conditions, capi.CertificateSigningRequestCondition{ + Type: capi.CertificateFailed, + }) + } return csr } diff --git a/pkg/kubelet/certificate/bootstrap/bootstrap_test.go b/pkg/kubelet/certificate/bootstrap/bootstrap_test.go index bbfa0e9aa8c..8ba3daeb77a 100644 --- a/pkg/kubelet/certificate/bootstrap/bootstrap_test.go +++ b/pkg/kubelet/certificate/bootstrap/bootstrap_test.go @@ -174,6 +174,7 @@ func (c *fakeClient) Watch(_ context.Context, opts metav1.ListOptions) (watch.In func (c *fakeClient) generateCSR() *certificates.CertificateSigningRequest { var condition certificates.CertificateSigningRequestCondition + var certificateData []byte if c.failureType == certificateSigningRequestDenied { condition = certificates.CertificateSigningRequestCondition{ Type: certificates.CertificateDenied, @@ -182,6 +183,7 @@ func (c *fakeClient) generateCSR() *certificates.CertificateSigningRequest { condition = certificates.CertificateSigningRequestCondition{ Type: certificates.CertificateApproved, } + certificateData = []byte(`issued certificate`) } csr := certificates.CertificateSigningRequest{ @@ -192,7 +194,7 @@ func (c *fakeClient) generateCSR() *certificates.CertificateSigningRequest { Conditions: []certificates.CertificateSigningRequestCondition{ condition, }, - Certificate: []byte{}, + Certificate: certificateData, }, } return &csr diff --git a/pkg/printers/internalversion/printers.go b/pkg/printers/internalversion/printers.go index 1707a46e3f1..ac58d853bbb 100644 --- a/pkg/printers/internalversion/printers.go +++ b/pkg/printers/internalversion/printers.go @@ -1781,10 +1781,7 @@ func printCertificateSigningRequest(obj *certificates.CertificateSigningRequest, row := metav1.TableRow{ Object: runtime.RawExtension{Object: obj}, } - status, err := extractCSRStatus(obj) - if err != nil { - return nil, err - } + status := extractCSRStatus(obj) signerName := "" if obj.Spec.SignerName != "" { signerName = obj.Spec.SignerName @@ -1793,16 +1790,16 @@ func printCertificateSigningRequest(obj *certificates.CertificateSigningRequest, return []metav1.TableRow{row}, nil } -func extractCSRStatus(csr *certificates.CertificateSigningRequest) (string, error) { - var approved, denied bool +func extractCSRStatus(csr *certificates.CertificateSigningRequest) string { + var approved, denied, failed bool for _, c := range csr.Status.Conditions { switch c.Type { case certificates.CertificateApproved: approved = true case certificates.CertificateDenied: denied = true - default: - return "", fmt.Errorf("unknown csr condition %q", c) + case certificates.CertificateFailed: + failed = true } } var status string @@ -1814,10 +1811,13 @@ func extractCSRStatus(csr *certificates.CertificateSigningRequest) (string, erro } else { status += "Pending" } + if failed { + status += ",Failed" + } if len(csr.Status.Certificate) > 0 { status += ",Issued" } - return status, nil + return status } func printCertificateSigningRequestList(list *certificates.CertificateSigningRequestList, options printers.GenerateOptions) ([]metav1.TableRow, error) { diff --git a/pkg/registry/certificates/certificates/BUILD b/pkg/registry/certificates/certificates/BUILD index 7c4fc9febbc..094ffb1dfa7 100644 --- a/pkg/registry/certificates/certificates/BUILD +++ b/pkg/registry/certificates/certificates/BUILD @@ -17,10 +17,12 @@ go_library( "//pkg/api/legacyscheme:go_default_library", "//pkg/apis/certificates:go_default_library", "//pkg/apis/certificates/validation:go_default_library", + "//staging/src/k8s.io/api/certificates/v1beta1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library", "//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library", @@ -34,6 +36,7 @@ go_test( embed = [":go_default_library"], deps = [ "//pkg/apis/certificates:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", "//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library", diff --git a/pkg/registry/certificates/certificates/strategy.go b/pkg/registry/certificates/certificates/strategy.go index 1bbebdedde9..fc35c33acfa 100644 --- a/pkg/registry/certificates/certificates/strategy.go +++ b/pkg/registry/certificates/certificates/strategy.go @@ -20,10 +20,12 @@ import ( "context" "fmt" + certificatesv1beta1 "k8s.io/api/certificates/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/generic" @@ -94,7 +96,7 @@ func (csrStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object // Validate validates a new CSR. Validation must check for a correct signature. func (csrStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { csr := obj.(*certificates.CertificateSigningRequest) - return validation.ValidateCertificateSigningRequest(csr) + return validation.ValidateCertificateSigningRequestCreate(csr, requestGroupVersion(ctx)) } // Canonicalize normalizes the object after validation (which includes a signature check). @@ -104,7 +106,7 @@ func (csrStrategy) Canonicalize(obj runtime.Object) {} func (csrStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { oldCSR := old.(*certificates.CertificateSigningRequest) newCSR := obj.(*certificates.CertificateSigningRequest) - return validation.ValidateCertificateSigningRequestUpdate(newCSR, oldCSR) + return validation.ValidateCertificateSigningRequestUpdate(newCSR, oldCSR, requestGroupVersion(ctx)) } // If AllowUnconditionalUpdate() is true and the object specified by @@ -142,20 +144,84 @@ func (csrStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime. newCSR := obj.(*certificates.CertificateSigningRequest) oldCSR := old.(*certificates.CertificateSigningRequest) - // Updating the Status should only update the Status and not the spec - // or approval conditions. The intent is to separate the concerns of - // approval and certificate issuance. + // Updating /status should not modify spec newCSR.Spec = oldCSR.Spec - newCSR.Status.Conditions = oldCSR.Status.Conditions + + switch requestGroupVersion(ctx) { + case certificatesv1beta1.SchemeGroupVersion: + // Specifically preserve existing Approved/Denied conditions. + // If we cannot (if the status update attempted to add/remove Approved/Denied conditions), revert to old conditions for backwards compatibility. + if !preserveConditionInstances(newCSR, oldCSR, certificates.CertificateApproved) || !preserveConditionInstances(newCSR, oldCSR, certificates.CertificateDenied) { + newCSR.Status.Conditions = oldCSR.Status.Conditions + } + default: + // Specifically preserve existing Approved/Denied conditions. + // Adding/removing Approved/Denied conditions will cause these to fail, + // and the change in Approved/Denied conditions will produce a validation error + preserveConditionInstances(newCSR, oldCSR, certificates.CertificateApproved) + preserveConditionInstances(newCSR, oldCSR, certificates.CertificateDenied) + } + + populateConditionTimestamps(newCSR, oldCSR) +} + +// preserveConditionInstances copies instances of the the specified condition type from oldCSR to newCSR. +// or returns false if the newCSR added or removed instances +func preserveConditionInstances(newCSR, oldCSR *certificates.CertificateSigningRequest, conditionType certificates.RequestConditionType) bool { + oldIndices := findConditionIndices(oldCSR, conditionType) + newIndices := findConditionIndices(newCSR, conditionType) + if len(oldIndices) != len(newIndices) { + // instances were added or removed, we cannot preserve the existing values + return false + } + // preserve the old condition values + for i, oldIndex := range oldIndices { + newCSR.Status.Conditions[newIndices[i]] = oldCSR.Status.Conditions[oldIndex] + } + return true +} + +// findConditionIndices returns the indices of instances of the specified condition type +func findConditionIndices(csr *certificates.CertificateSigningRequest, conditionType certificates.RequestConditionType) []int { + var retval []int + for i, c := range csr.Status.Conditions { + if c.Type == conditionType { + retval = append(retval, i) + } + } + return retval +} + +// nowFunc allows overriding for unit tests +var nowFunc = metav1.Now + +// populateConditionTimestamps sets LastUpdateTime and LastTransitionTime in newCSR if missing +func populateConditionTimestamps(newCSR, oldCSR *certificates.CertificateSigningRequest) { + now := nowFunc() for i := range newCSR.Status.Conditions { if newCSR.Status.Conditions[i].LastUpdateTime.IsZero() { - newCSR.Status.Conditions[i].LastUpdateTime = metav1.Now() + newCSR.Status.Conditions[i].LastUpdateTime = now + } + + // preserve existing lastTransitionTime if the condition with this type/status already exists, + // otherwise set to now. + if newCSR.Status.Conditions[i].LastTransitionTime.IsZero() { + lastTransition := now + for _, oldCondition := range oldCSR.Status.Conditions { + if oldCondition.Type == newCSR.Status.Conditions[i].Type && + oldCondition.Status == newCSR.Status.Conditions[i].Status && + !oldCondition.LastTransitionTime.IsZero() { + lastTransition = oldCondition.LastTransitionTime + break + } + } + newCSR.Status.Conditions[i].LastTransitionTime = lastTransition } } } func (csrStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { - return validation.ValidateCertificateSigningRequestUpdate(obj.(*certificates.CertificateSigningRequest), old.(*certificates.CertificateSigningRequest)) + return validation.ValidateCertificateSigningRequestStatusUpdate(obj.(*certificates.CertificateSigningRequest), old.(*certificates.CertificateSigningRequest), requestGroupVersion(ctx)) } // Canonicalize normalizes the object after validation. @@ -170,28 +236,22 @@ type csrApprovalStrategy struct { var ApprovalStrategy = csrApprovalStrategy{Strategy} // PrepareForUpdate prepares the new certificate signing request by limiting -// the data that is updated to only the conditions. Also, if there is no -// existing LastUpdateTime on a condition, the current date/time will be set. +// the data that is updated to only the conditions and populating condition timestamps func (csrApprovalStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { newCSR := obj.(*certificates.CertificateSigningRequest) oldCSR := old.(*certificates.CertificateSigningRequest) + populateConditionTimestamps(newCSR, oldCSR) + newConditions := newCSR.Status.Conditions + // Updating the approval should only update the conditions. newCSR.Spec = oldCSR.Spec - oldCSR.Status.Conditions = newCSR.Status.Conditions - for i := range newCSR.Status.Conditions { - // The Conditions are an array of values, some of which may be - // pre-existing and unaltered by this update, so a LastUpdateTime is - // added only if one isn't already set. - if newCSR.Status.Conditions[i].LastUpdateTime.IsZero() { - newCSR.Status.Conditions[i].LastUpdateTime = metav1.Now() - } - } newCSR.Status = oldCSR.Status + newCSR.Status.Conditions = newConditions } func (csrApprovalStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { - return validation.ValidateCertificateSigningRequestUpdate(obj.(*certificates.CertificateSigningRequest), old.(*certificates.CertificateSigningRequest)) + return validation.ValidateCertificateSigningRequestApprovalUpdate(obj.(*certificates.CertificateSigningRequest), old.(*certificates.CertificateSigningRequest), requestGroupVersion(ctx)) } // GetAttrs returns labels and fields of a given object for filtering purposes. @@ -211,3 +271,11 @@ func SelectableFields(obj *certificates.CertificateSigningRequest) fields.Set { } return generic.MergeFieldsSets(objectMetaFieldsSet, csrSpecificFieldsSet) } + +// requestGroupVersion returns the group/version associated with the given context, or a zero-value group/version +func requestGroupVersion(ctx context.Context) schema.GroupVersion { + if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found { + return schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion} + } + return schema.GroupVersion{} +} diff --git a/pkg/registry/certificates/certificates/strategy_test.go b/pkg/registry/certificates/certificates/strategy_test.go index b1b660ba068..0c940b296f2 100644 --- a/pkg/registry/certificates/certificates/strategy_test.go +++ b/pkg/registry/certificates/certificates/strategy_test.go @@ -20,7 +20,9 @@ import ( "context" "reflect" "testing" + "time" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/diff" "k8s.io/apiserver/pkg/authentication/user" @@ -121,3 +123,117 @@ func TestStrategyCreate(t *testing.T) { } } } + +func TestStatusUpdate(t *testing.T) { + now := metav1.Now() + later := metav1.NewTime(now.Add(time.Hour)) + nowFunc = func() metav1.Time { return now } + defer func() { + nowFunc = metav1.Now + }() + + tests := []struct { + name string + newObj *certapi.CertificateSigningRequest + oldObj *certapi.CertificateSigningRequest + expectedObjs map[string]*certapi.CertificateSigningRequest + }{ + { + name: "no-op", + newObj: &certapi.CertificateSigningRequest{}, + oldObj: &certapi.CertificateSigningRequest{}, + expectedObjs: map[string]*certapi.CertificateSigningRequest{ + "v1": {}, + "v1beta1": {}, + }, + }, + { + name: "adding failed condition to existing approved/denied conditions", + newObj: &certapi.CertificateSigningRequest{ + Status: certapi.CertificateSigningRequestStatus{ + Conditions: []certapi.CertificateSigningRequestCondition{ + {Type: certapi.CertificateFailed}, + {Type: certapi.CertificateDenied}, + {Type: certapi.CertificateApproved}, + {Type: certapi.CertificateDenied}, + {Type: certapi.CertificateApproved}, + }, + }, + }, + oldObj: &certapi.CertificateSigningRequest{ + Status: certapi.CertificateSigningRequestStatus{ + Conditions: []certapi.CertificateSigningRequestCondition{ + {Type: certapi.CertificateDenied, Reason: "because1"}, + {Type: certapi.CertificateApproved, Reason: "because2"}, + {Type: certapi.CertificateDenied, Reason: "because3", LastUpdateTime: later, LastTransitionTime: later}, + {Type: certapi.CertificateApproved, Reason: "because4", LastUpdateTime: later, LastTransitionTime: later}, + }, + }, + }, + expectedObjs: map[string]*certapi.CertificateSigningRequest{ + // preserve existing Approved/Denied conditions + "v1": { + Status: certapi.CertificateSigningRequestStatus{ + Conditions: []certapi.CertificateSigningRequestCondition{ + {Type: certapi.CertificateFailed, LastUpdateTime: now, LastTransitionTime: now}, + {Type: certapi.CertificateDenied, LastUpdateTime: now, LastTransitionTime: later, Reason: "because1"}, + {Type: certapi.CertificateApproved, LastUpdateTime: now, LastTransitionTime: later, Reason: "because2"}, + {Type: certapi.CertificateDenied, LastUpdateTime: later, LastTransitionTime: later, Reason: "because3"}, + {Type: certapi.CertificateApproved, LastUpdateTime: later, LastTransitionTime: later, Reason: "because4"}, + }, + }, + }, + // preserve existing Approved/Denied conditions + "v1beta1": { + Status: certapi.CertificateSigningRequestStatus{ + Conditions: []certapi.CertificateSigningRequestCondition{ + {Type: certapi.CertificateFailed, LastUpdateTime: now, LastTransitionTime: now}, + {Type: certapi.CertificateDenied, LastUpdateTime: now, LastTransitionTime: later, Reason: "because1"}, + {Type: certapi.CertificateApproved, LastUpdateTime: now, LastTransitionTime: later, Reason: "because2"}, + {Type: certapi.CertificateDenied, LastUpdateTime: later, LastTransitionTime: later, Reason: "because3"}, + {Type: certapi.CertificateApproved, LastUpdateTime: later, LastTransitionTime: later, Reason: "because4"}, + }, + }, + }, + }, + }, + { + name: "add approved condition", + newObj: &certapi.CertificateSigningRequest{ + Status: certapi.CertificateSigningRequestStatus{ + Conditions: []certapi.CertificateSigningRequestCondition{ + {Type: certapi.CertificateApproved}, + }, + }, + }, + oldObj: &certapi.CertificateSigningRequest{}, + expectedObjs: map[string]*certapi.CertificateSigningRequest{ + // preserved submitted conditions if existing Approved/Denied conditions could not be copied over (will fail validation) + "v1": { + Status: certapi.CertificateSigningRequestStatus{ + Conditions: []certapi.CertificateSigningRequestCondition{ + {Type: certapi.CertificateApproved, LastUpdateTime: now, LastTransitionTime: now}, + }, + }, + }, + // reset conditions to existing conditions if Approved/Denied conditions could not be copied over + "v1beta1": { + Status: certapi.CertificateSigningRequestStatus{}, + }, + }, + }, + } + + for _, tt := range tests { + for _, version := range []string{"v1", "v1beta1"} { + t.Run(tt.name+"_"+version, func(t *testing.T) { + ctx := genericapirequest.WithRequestInfo(context.TODO(), &genericapirequest.RequestInfo{APIGroup: "certificates.k8s.io", APIVersion: version}) + obj := tt.newObj.DeepCopy() + StatusStrategy.PrepareForUpdate(ctx, obj, tt.oldObj.DeepCopy()) + if !reflect.DeepEqual(obj, tt.expectedObjs[version]) { + t.Errorf("object diff: %s", diff.ObjectDiff(obj, tt.expectedObjs[version])) + } + }) + } + } +} diff --git a/plugin/pkg/admission/certificates/signing/BUILD b/plugin/pkg/admission/certificates/signing/BUILD index 7c6303d0c8c..175ef63cb82 100644 --- a/plugin/pkg/admission/certificates/signing/BUILD +++ b/plugin/pkg/admission/certificates/signing/BUILD @@ -8,6 +8,7 @@ go_library( deps = [ "//pkg/apis/certificates:go_default_library", "//plugin/pkg/admission/certificates:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library", "//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", diff --git a/plugin/pkg/admission/certificates/signing/admission.go b/plugin/pkg/admission/certificates/signing/admission.go index 4f072f803f9..2cd2b2e2740 100644 --- a/plugin/pkg/admission/certificates/signing/admission.go +++ b/plugin/pkg/admission/certificates/signing/admission.go @@ -24,10 +24,10 @@ import ( "k8s.io/klog/v2" + apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apiserver/pkg/admission" genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer" "k8s.io/apiserver/pkg/authorization/authorizer" - api "k8s.io/kubernetes/pkg/apis/certificates" "k8s.io/kubernetes/plugin/pkg/admission/certificates" ) @@ -73,10 +73,10 @@ func NewPlugin() *Plugin { var csrGroupResource = api.Resource("certificatesigningrequests") -// Validate verifies that the requesting user has permission to approve +// Validate verifies that the requesting user has permission to sign // CertificateSigningRequests for the specified signerName. func (p *Plugin) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { - // Ignore all calls to anything other than 'certificatesigningrequests/approval'. + // Ignore all calls to anything other than 'certificatesigningrequests/status'. // Ignore all operations other than UPDATE. if a.GetSubresource() != "status" || a.GetResource().GroupResource() != csrGroupResource { @@ -92,8 +92,8 @@ func (p *Plugin) Validate(ctx context.Context, a admission.Attributes, o admissi return admission.NewForbidden(a, fmt.Errorf("expected type CertificateSigningRequest, got: %T", a.GetObject())) } - // only run if the status.certificate field has been changed - if reflect.DeepEqual(oldCSR.Status.Certificate, csr.Status.Certificate) { + // only run if the status.certificate or status.conditions field has been changed + if reflect.DeepEqual(oldCSR.Status.Certificate, csr.Status.Certificate) && apiequality.Semantic.DeepEqual(oldCSR.Status.Conditions, csr.Status.Conditions) { return nil } diff --git a/plugin/pkg/admission/certificates/signing/admission_test.go b/plugin/pkg/admission/certificates/signing/admission_test.go index 724c3e4dad3..ac2f114d602 100644 --- a/plugin/pkg/admission/certificates/signing/admission_test.go +++ b/plugin/pkg/admission/certificates/signing/admission_test.go @@ -48,7 +48,7 @@ func TestPlugin_Validate(t *testing.T) { }, allowed: false, }, - "allowed if the 'certificate' field has not changed": { + "allowed if the 'certificate' and conditions field has not changed": { attributes: &testAttributes{ resource: certificatesapi.Resource("certificatesigningrequests"), subresource: "status", @@ -63,7 +63,7 @@ func TestPlugin_Validate(t *testing.T) { allowed: true, authzErr: errors.New("faked error"), }, - "deny request if authz lookup fails": { + "deny request if authz lookup fails on certificate change": { allowedName: "abc.com/xyz", attributes: &testAttributes{ resource: certificatesapi.Resource("certificatesigningrequests"), @@ -84,6 +84,27 @@ func TestPlugin_Validate(t *testing.T) { authzErr: errors.New("test"), allowed: false, }, + "deny request if authz lookup fails on condition change": { + allowedName: "abc.com/xyz", + attributes: &testAttributes{ + resource: certificatesapi.Resource("certificatesigningrequests"), + subresource: "status", + oldObj: &certificatesapi.CertificateSigningRequest{Spec: certificatesapi.CertificateSigningRequestSpec{ + SignerName: "abc.com/xyz", + }}, + obj: &certificatesapi.CertificateSigningRequest{ + Spec: certificatesapi.CertificateSigningRequestSpec{ + SignerName: "abc.com/xyz", + }, + Status: certificatesapi.CertificateSigningRequestStatus{ + Conditions: []certificatesapi.CertificateSigningRequestCondition{{Type: certificatesapi.CertificateFailed}}, + }, + }, + operation: admission.Update, + }, + authzErr: errors.New("test"), + allowed: false, + }, "allow request if user is authorized for specific signerName": { allowedName: "abc.com/xyz", attributes: &testAttributes{ diff --git a/staging/src/k8s.io/api/certificates/v1beta1/BUILD b/staging/src/k8s.io/api/certificates/v1beta1/BUILD index b1d4e3903e6..f731ec6b4b3 100644 --- a/staging/src/k8s.io/api/certificates/v1beta1/BUILD +++ b/staging/src/k8s.io/api/certificates/v1beta1/BUILD @@ -19,6 +19,7 @@ go_library( importmap = "k8s.io/kubernetes/vendor/k8s.io/api/certificates/v1beta1", importpath = "k8s.io/api/certificates/v1beta1", deps = [ + "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", diff --git a/staging/src/k8s.io/api/certificates/v1beta1/generated.pb.go b/staging/src/k8s.io/api/certificates/v1beta1/generated.pb.go index 24fa4bf8103..1729931b82d 100644 --- a/staging/src/k8s.io/api/certificates/v1beta1/generated.pb.go +++ b/staging/src/k8s.io/api/certificates/v1beta1/generated.pb.go @@ -27,6 +27,8 @@ import ( proto "github.com/gogo/protobuf/proto" github_com_gogo_protobuf_sortkeys "github.com/gogo/protobuf/sortkeys" + k8s_io_api_core_v1 "k8s.io/api/core/v1" + math "math" math_bits "math/bits" reflect "reflect" @@ -227,59 +229,62 @@ func init() { } var fileDescriptor_09d156762b8218ef = []byte{ - // 824 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x54, 0x4d, 0x6f, 0x1b, 0x45, - 0x18, 0xf6, 0xfa, 0xdb, 0xe3, 0x90, 0x56, 0x23, 0x54, 0x2d, 0x91, 0xba, 0x1b, 0xad, 0x00, 0x85, - 0x8f, 0xce, 0x92, 0x0a, 0x41, 0x94, 0x03, 0x82, 0x0d, 0x15, 0x44, 0xb4, 0x20, 0x4d, 0x1a, 0x0e, - 0x08, 0x89, 0x8e, 0xd7, 0x6f, 0x37, 0x53, 0x77, 0x3f, 0xd8, 0x99, 0x35, 0xf8, 0xd6, 0x9f, 0xc0, - 0x91, 0x0b, 0x12, 0x3f, 0x27, 0x1c, 0x90, 0x7a, 0xec, 0x01, 0x59, 0xc4, 0xdc, 0xf9, 0x01, 0x3d, - 0xa1, 0x99, 0x1d, 0x7b, 0x8d, 0x23, 0xd7, 0x55, 0x73, 0xdb, 0xf7, 0x79, 0xdf, 0xe7, 0x79, 0x3f, - 0x67, 0xd1, 0x97, 0xa3, 0x03, 0x41, 0x78, 0xea, 0x8f, 0x8a, 0x01, 0xe4, 0x09, 0x48, 0x10, 0xfe, - 0x18, 0x92, 0x61, 0x9a, 0xfb, 0xc6, 0xc1, 0x32, 0xee, 0x87, 0x90, 0x4b, 0xfe, 0x90, 0x87, 0x4c, - 0xbb, 0xf7, 0x07, 0x20, 0xd9, 0xbe, 0x1f, 0x41, 0x02, 0x39, 0x93, 0x30, 0x24, 0x59, 0x9e, 0xca, - 0x14, 0xbb, 0x25, 0x81, 0xb0, 0x8c, 0x93, 0x65, 0x02, 0x31, 0x84, 0x9d, 0x5b, 0x11, 0x97, 0x67, - 0xc5, 0x80, 0x84, 0x69, 0xec, 0x47, 0x69, 0x94, 0xfa, 0x9a, 0x37, 0x28, 0x1e, 0x6a, 0x4b, 0x1b, - 0xfa, 0xab, 0xd4, 0xdb, 0xf9, 0xb0, 0x2a, 0x20, 0x66, 0xe1, 0x19, 0x4f, 0x20, 0x9f, 0xf8, 0xd9, - 0x28, 0x52, 0x80, 0xf0, 0x63, 0x90, 0xcc, 0x1f, 0x5f, 0xaa, 0x62, 0xc7, 0x5f, 0xc7, 0xca, 0x8b, - 0x44, 0xf2, 0x18, 0x2e, 0x11, 0x3e, 0xda, 0x44, 0x10, 0xe1, 0x19, 0xc4, 0x6c, 0x95, 0xe7, 0xfd, - 0x51, 0x47, 0x6f, 0x1c, 0x55, 0x6d, 0x9e, 0xf0, 0x28, 0xe1, 0x49, 0x44, 0xe1, 0xc7, 0x02, 0x84, - 0xc4, 0x0f, 0x50, 0x57, 0x55, 0x38, 0x64, 0x92, 0xd9, 0xd6, 0xae, 0xb5, 0xd7, 0xbf, 0xfd, 0x01, - 0xa9, 0xe6, 0xb3, 0x48, 0x44, 0xb2, 0x51, 0xa4, 0x00, 0x41, 0x54, 0x34, 0x19, 0xef, 0x93, 0x6f, - 0x06, 0x8f, 0x20, 0x94, 0xf7, 0x40, 0xb2, 0x00, 0x9f, 0x4f, 0xdd, 0xda, 0x6c, 0xea, 0xa2, 0x0a, - 0xa3, 0x0b, 0x55, 0xfc, 0x00, 0x35, 0x45, 0x06, 0xa1, 0x5d, 0xd7, 0xea, 0x9f, 0x90, 0x0d, 0xd3, - 0x27, 0x6b, 0x6b, 0x3d, 0xc9, 0x20, 0x0c, 0xb6, 0x4c, 0xae, 0xa6, 0xb2, 0xa8, 0x56, 0xc6, 0x67, - 0xa8, 0x2d, 0x24, 0x93, 0x85, 0xb0, 0x1b, 0x3a, 0xc7, 0xa7, 0x57, 0xc8, 0xa1, 0x75, 0x82, 0x6d, - 0x93, 0xa5, 0x5d, 0xda, 0xd4, 0xe8, 0x7b, 0xbf, 0xd5, 0x91, 0xb7, 0x96, 0x7b, 0x94, 0x26, 0x43, - 0x2e, 0x79, 0x9a, 0xe0, 0x03, 0xd4, 0x94, 0x93, 0x0c, 0xf4, 0x40, 0x7b, 0xc1, 0x9b, 0xf3, 0x92, - 0xef, 0x4f, 0x32, 0x78, 0x3e, 0x75, 0x5f, 0x5f, 0x8d, 0x57, 0x38, 0xd5, 0x0c, 0xfc, 0x36, 0x6a, - 0xe7, 0xc0, 0x44, 0x9a, 0xe8, 0x71, 0xf5, 0xaa, 0x42, 0xa8, 0x46, 0xa9, 0xf1, 0xe2, 0x77, 0x50, - 0x27, 0x06, 0x21, 0x58, 0x04, 0xba, 0xe7, 0x5e, 0x70, 0xcd, 0x04, 0x76, 0xee, 0x95, 0x30, 0x9d, - 0xfb, 0xf1, 0x23, 0xb4, 0xfd, 0x98, 0x09, 0x79, 0x9a, 0x0d, 0x99, 0x84, 0xfb, 0x3c, 0x06, 0xbb, - 0xa9, 0xa7, 0xf4, 0xee, 0xcb, 0xed, 0x59, 0x31, 0x82, 0x1b, 0x46, 0x7d, 0xfb, 0xee, 0xff, 0x94, - 0xe8, 0x8a, 0xb2, 0x37, 0xb5, 0xd0, 0xcd, 0xb5, 0xf3, 0xb9, 0xcb, 0x85, 0xc4, 0xdf, 0x5f, 0xba, - 0x37, 0xf2, 0x72, 0x75, 0x28, 0xb6, 0xbe, 0xb6, 0xeb, 0xa6, 0x96, 0xee, 0x1c, 0x59, 0xba, 0xb5, - 0x1f, 0x50, 0x8b, 0x4b, 0x88, 0x85, 0x5d, 0xdf, 0x6d, 0xec, 0xf5, 0x6f, 0x1f, 0xbe, 0xfa, 0x21, - 0x04, 0xaf, 0x99, 0x34, 0xad, 0x63, 0x25, 0x48, 0x4b, 0x5d, 0xef, 0xdf, 0xc6, 0x0b, 0x1a, 0x54, - 0x27, 0x89, 0xdf, 0x42, 0x9d, 0xbc, 0x34, 0x75, 0x7f, 0x5b, 0x41, 0x5f, 0x6d, 0xc5, 0x44, 0xd0, - 0xb9, 0x0f, 0x13, 0x84, 0x04, 0x8f, 0x12, 0xc8, 0xbf, 0x66, 0x31, 0xd8, 0x9d, 0x72, 0xd9, 0xea, - 0x0d, 0x9d, 0x2c, 0x50, 0xba, 0x14, 0x81, 0x09, 0x6a, 0x17, 0x6a, 0x9d, 0xc2, 0x6e, 0xed, 0x36, - 0xf6, 0x7a, 0xc1, 0x0d, 0x75, 0x14, 0xa7, 0x1a, 0x79, 0x3e, 0x75, 0xbb, 0x5f, 0xc1, 0x44, 0x1b, - 0xd4, 0x44, 0xe1, 0xf7, 0x51, 0xb7, 0x10, 0x90, 0x27, 0x4a, 0xbd, 0x3c, 0xa5, 0xc5, 0xdc, 0x4e, - 0x0d, 0x4e, 0x17, 0x11, 0xf8, 0x26, 0x6a, 0x14, 0x7c, 0x68, 0x4e, 0xa9, 0x6f, 0x02, 0x1b, 0xa7, - 0xc7, 0x9f, 0x53, 0x85, 0x63, 0x0f, 0xb5, 0xa3, 0x3c, 0x2d, 0x32, 0x61, 0x37, 0x75, 0x72, 0xa4, - 0x92, 0x7f, 0xa1, 0x11, 0x6a, 0x3c, 0x38, 0x41, 0x2d, 0xf8, 0x59, 0xe6, 0xcc, 0x6e, 0xeb, 0xd1, - 0x1f, 0x5f, 0xed, 0x9d, 0x93, 0x3b, 0x4a, 0xeb, 0x4e, 0x22, 0xf3, 0x49, 0xb5, 0x09, 0x8d, 0xd1, - 0x32, 0xcd, 0x0e, 0x20, 0x54, 0xc5, 0xe0, 0xeb, 0xa8, 0x31, 0x82, 0x49, 0xf9, 0xe0, 0xa8, 0xfa, - 0xc4, 0x9f, 0xa1, 0xd6, 0x98, 0x3d, 0x2e, 0xc0, 0xfc, 0x77, 0xde, 0xdb, 0x58, 0x8f, 0x56, 0xfb, - 0x56, 0x51, 0x68, 0xc9, 0x3c, 0xac, 0x1f, 0x58, 0xde, 0x9f, 0x16, 0x72, 0x37, 0xfc, 0x2d, 0xf0, - 0x4f, 0x08, 0x85, 0xf3, 0xb7, 0x2c, 0x6c, 0x4b, 0xf7, 0x7f, 0xf4, 0xea, 0xfd, 0x2f, 0xfe, 0x0b, - 0xd5, 0x8f, 0x75, 0x01, 0x09, 0xba, 0x94, 0x0a, 0xef, 0xa3, 0xfe, 0x92, 0xb4, 0xee, 0x74, 0x2b, - 0xb8, 0x36, 0x9b, 0xba, 0xfd, 0x25, 0x71, 0xba, 0x1c, 0xe3, 0x7d, 0x6c, 0xc6, 0xa6, 0x1b, 0xc5, - 0xee, 0xfc, 0xbd, 0x58, 0x7a, 0xaf, 0xbd, 0xd5, 0x7b, 0x3f, 0xec, 0xfe, 0xfa, 0xbb, 0x5b, 0x7b, - 0xf2, 0xd7, 0x6e, 0x2d, 0xb8, 0x75, 0x7e, 0xe1, 0xd4, 0x9e, 0x5e, 0x38, 0xb5, 0x67, 0x17, 0x4e, - 0xed, 0xc9, 0xcc, 0xb1, 0xce, 0x67, 0x8e, 0xf5, 0x74, 0xe6, 0x58, 0xcf, 0x66, 0x8e, 0xf5, 0xf7, - 0xcc, 0xb1, 0x7e, 0xf9, 0xc7, 0xa9, 0x7d, 0xd7, 0x31, 0xdd, 0xfd, 0x17, 0x00, 0x00, 0xff, 0xff, - 0x69, 0x8d, 0xc8, 0xd3, 0xaf, 0x07, 0x00, 0x00, + // 878 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x55, 0x4b, 0x6f, 0x1c, 0x45, + 0x10, 0xde, 0xf1, 0xbe, 0x7b, 0x8d, 0x13, 0xb5, 0x50, 0x34, 0xac, 0x94, 0x19, 0x6b, 0x04, 0xc8, + 0x3c, 0xd2, 0x83, 0xa3, 0x08, 0x2c, 0x1f, 0x10, 0x8c, 0x89, 0xc0, 0xc2, 0x01, 0xa9, 0x6d, 0x73, + 0x40, 0x48, 0xa4, 0x77, 0xb6, 0x32, 0xee, 0x6c, 0xe6, 0xc1, 0x74, 0xcf, 0xc2, 0xde, 0xf2, 0x13, + 0x38, 0x72, 0xe4, 0xe7, 0x98, 0x03, 0x52, 0x8e, 0x39, 0xa0, 0x15, 0xde, 0xdc, 0xf9, 0x01, 0x3e, + 0xa1, 0xee, 0xe9, 0x9d, 0x5d, 0xbf, 0x70, 0x48, 0x6e, 0xdb, 0x5f, 0xd7, 0xf7, 0x7d, 0x55, 0x35, + 0xd5, 0xb5, 0xe8, 0xab, 0xd1, 0x96, 0x20, 0x3c, 0xf5, 0x47, 0xc5, 0x00, 0xf2, 0x04, 0x24, 0x08, + 0x7f, 0x0c, 0xc9, 0x30, 0xcd, 0x7d, 0x73, 0xc1, 0x32, 0xee, 0x87, 0x90, 0x4b, 0xfe, 0x88, 0x87, + 0x4c, 0x5f, 0x6f, 0x0e, 0x40, 0xb2, 0x4d, 0x3f, 0x82, 0x04, 0x72, 0x26, 0x61, 0x48, 0xb2, 0x3c, + 0x95, 0x29, 0x76, 0x4b, 0x02, 0x61, 0x19, 0x27, 0xcb, 0x04, 0x62, 0x08, 0xfd, 0x3b, 0x11, 0x97, + 0x47, 0xc5, 0x80, 0x84, 0x69, 0xec, 0x47, 0x69, 0x94, 0xfa, 0x9a, 0x37, 0x28, 0x1e, 0xe9, 0x93, + 0x3e, 0xe8, 0x5f, 0xa5, 0x5e, 0xdf, 0x5b, 0x4e, 0x20, 0xcd, 0xc1, 0x1f, 0x5f, 0xf0, 0xec, 0xdf, + 0x5b, 0xc4, 0xc4, 0x2c, 0x3c, 0xe2, 0x09, 0xe4, 0x13, 0x3f, 0x1b, 0x45, 0x0a, 0x10, 0x7e, 0x0c, + 0x92, 0x5d, 0xc6, 0xf2, 0xaf, 0x62, 0xe5, 0x45, 0x22, 0x79, 0x0c, 0x17, 0x08, 0x1f, 0x5f, 0x47, + 0x10, 0xe1, 0x11, 0xc4, 0xec, 0x3c, 0xcf, 0xfb, 0x63, 0x05, 0xbd, 0xb5, 0xb3, 0x68, 0xc5, 0x3e, + 0x8f, 0x12, 0x9e, 0x44, 0x14, 0x7e, 0x2a, 0x40, 0x48, 0xfc, 0x10, 0x75, 0x54, 0x86, 0x43, 0x26, + 0x99, 0x6d, 0xad, 0x5b, 0x1b, 0xbd, 0xbb, 0x1f, 0x91, 0x45, 0x0f, 0x2b, 0x23, 0x92, 0x8d, 0x22, + 0x05, 0x08, 0xa2, 0xa2, 0xc9, 0x78, 0x93, 0x7c, 0x3b, 0x78, 0x0c, 0xa1, 0x7c, 0x00, 0x92, 0x05, + 0xf8, 0x78, 0xea, 0xd6, 0x66, 0x53, 0x17, 0x2d, 0x30, 0x5a, 0xa9, 0xe2, 0x87, 0xa8, 0x21, 0x32, + 0x08, 0xed, 0x15, 0xad, 0xfe, 0x29, 0xb9, 0xe6, 0x0b, 0x91, 0x2b, 0x73, 0xdd, 0xcf, 0x20, 0x0c, + 0x56, 0x8d, 0x57, 0x43, 0x9d, 0xa8, 0x56, 0xc6, 0x47, 0xa8, 0x25, 0x24, 0x93, 0x85, 0xb0, 0xeb, + 0xda, 0xe3, 0xb3, 0xd7, 0xf0, 0xd0, 0x3a, 0xc1, 0x9a, 0x71, 0x69, 0x95, 0x67, 0x6a, 0xf4, 0xbd, + 0x17, 0x75, 0xe4, 0x5d, 0xc9, 0xdd, 0x49, 0x93, 0x21, 0x97, 0x3c, 0x4d, 0xf0, 0x16, 0x6a, 0xc8, + 0x49, 0x06, 0xba, 0xa1, 0xdd, 0xe0, 0xed, 0x79, 0xca, 0x07, 0x93, 0x0c, 0x4e, 0xa7, 0xee, 0x9b, + 0xe7, 0xe3, 0x15, 0x4e, 0x35, 0x03, 0xef, 0x55, 0xa5, 0xb4, 0x34, 0xf7, 0xde, 0xd9, 0x44, 0x4e, + 0xa7, 0xee, 0x25, 0x13, 0x49, 0x2a, 0xa5, 0xb3, 0xe9, 0xe2, 0x77, 0x51, 0x2b, 0x07, 0x26, 0xd2, + 0x44, 0x37, 0xbf, 0xbb, 0x28, 0x8b, 0x6a, 0x94, 0x9a, 0x5b, 0xfc, 0x1e, 0x6a, 0xc7, 0x20, 0x04, + 0x8b, 0x40, 0x77, 0xb0, 0x1b, 0xdc, 0x30, 0x81, 0xed, 0x07, 0x25, 0x4c, 0xe7, 0xf7, 0xf8, 0x31, + 0x5a, 0x7b, 0xc2, 0x84, 0x3c, 0xcc, 0x86, 0x4c, 0xc2, 0x01, 0x8f, 0xc1, 0x6e, 0xe8, 0x9e, 0xbf, + 0xff, 0x72, 0x53, 0xa3, 0x18, 0xc1, 0x2d, 0xa3, 0xbe, 0xb6, 0x77, 0x46, 0x89, 0x9e, 0x53, 0xc6, + 0x63, 0x84, 0x15, 0x72, 0x90, 0xb3, 0x44, 0x94, 0x8d, 0x52, 0x7e, 0xcd, 0xff, 0xed, 0xd7, 0x37, + 0x7e, 0x78, 0xef, 0x82, 0x1a, 0xbd, 0xc4, 0xc1, 0x9b, 0x5a, 0xe8, 0xf6, 0x95, 0x5f, 0x79, 0x8f, + 0x0b, 0x89, 0x7f, 0xb8, 0xf0, 0x6a, 0xc8, 0xcb, 0xe5, 0xa3, 0xd8, 0xfa, 0xcd, 0xdc, 0x34, 0x39, + 0x75, 0xe6, 0xc8, 0xd2, 0x8b, 0xf9, 0x11, 0x35, 0xb9, 0x84, 0x58, 0xd8, 0x2b, 0xeb, 0xf5, 0x8d, + 0xde, 0xdd, 0xed, 0x57, 0x1f, 0xe7, 0xe0, 0x0d, 0x63, 0xd3, 0xdc, 0x55, 0x82, 0xb4, 0xd4, 0xf5, + 0xfe, 0xa9, 0xff, 0x47, 0x81, 0xea, 0x61, 0xe1, 0x77, 0x50, 0x3b, 0x2f, 0x8f, 0xba, 0xbe, 0xd5, + 0xa0, 0xa7, 0xa6, 0xc1, 0x44, 0xd0, 0xf9, 0x1d, 0x26, 0x08, 0x09, 0x1e, 0x25, 0x90, 0x7f, 0xc3, + 0x62, 0xb0, 0xdb, 0xe5, 0x90, 0xa9, 0x4d, 0xb0, 0x5f, 0xa1, 0x74, 0x29, 0x02, 0x13, 0xd4, 0x2a, + 0xd4, 0x18, 0x09, 0xbb, 0xb9, 0x5e, 0xdf, 0xe8, 0x06, 0xb7, 0xd4, 0x30, 0x1e, 0x6a, 0xe4, 0x74, + 0xea, 0x76, 0xbe, 0x86, 0x89, 0x3e, 0x50, 0x13, 0x85, 0x3f, 0x44, 0x9d, 0x42, 0x40, 0x9e, 0x28, + 0xf5, 0x72, 0x84, 0xab, 0xbe, 0x1d, 0x1a, 0x9c, 0x56, 0x11, 0xf8, 0x36, 0xaa, 0x17, 0x7c, 0x68, + 0x46, 0xb8, 0x67, 0x02, 0xeb, 0x87, 0xbb, 0x5f, 0x50, 0x85, 0x63, 0x0f, 0xb5, 0xa2, 0x3c, 0x2d, + 0x32, 0x61, 0x37, 0xb4, 0x39, 0x52, 0xe6, 0x5f, 0x6a, 0x84, 0x9a, 0x1b, 0x9c, 0xa0, 0x26, 0xfc, + 0x22, 0x73, 0x66, 0xb7, 0x74, 0xeb, 0x77, 0x5f, 0x6f, 0x5b, 0x91, 0xfb, 0x4a, 0xeb, 0x7e, 0x22, + 0xf3, 0xc9, 0xe2, 0x4b, 0x68, 0x8c, 0x96, 0x36, 0x7d, 0x40, 0x68, 0x11, 0x83, 0x6f, 0xa2, 0xfa, + 0x08, 0x26, 0xe5, 0xda, 0xa0, 0xea, 0x27, 0xfe, 0x1c, 0x35, 0xc7, 0xec, 0x49, 0x01, 0x66, 0x7b, + 0x7e, 0x70, 0x6d, 0x3e, 0x5a, 0xed, 0x3b, 0x45, 0xa1, 0x25, 0x73, 0x7b, 0x65, 0xcb, 0xf2, 0xfe, + 0xb4, 0x90, 0x7b, 0xcd, 0xce, 0xc3, 0x3f, 0x23, 0x14, 0xce, 0xf7, 0x88, 0xb0, 0x2d, 0x5d, 0xff, + 0xce, 0xab, 0xd7, 0x5f, 0xed, 0xa4, 0xc5, 0xdf, 0x43, 0x05, 0x09, 0xba, 0x64, 0x85, 0x37, 0x51, + 0x6f, 0x49, 0x5a, 0x57, 0xba, 0x1a, 0xdc, 0x98, 0x4d, 0xdd, 0xde, 0x92, 0x38, 0x5d, 0x8e, 0xf1, + 0x3e, 0x31, 0x6d, 0xd3, 0x85, 0x62, 0x77, 0xfe, 0x5e, 0x2c, 0xfd, 0x5d, 0xbb, 0xe7, 0xe7, 0x7d, + 0xbb, 0xf3, 0xdb, 0xef, 0x6e, 0xed, 0xe9, 0x5f, 0xeb, 0xb5, 0xe0, 0xce, 0xf1, 0x89, 0x53, 0x7b, + 0x76, 0xe2, 0xd4, 0x9e, 0x9f, 0x38, 0xb5, 0xa7, 0x33, 0xc7, 0x3a, 0x9e, 0x39, 0xd6, 0xb3, 0x99, + 0x63, 0x3d, 0x9f, 0x39, 0xd6, 0xdf, 0x33, 0xc7, 0xfa, 0xf5, 0x85, 0x53, 0xfb, 0xbe, 0x6d, 0xaa, + 0xfb, 0x37, 0x00, 0x00, 0xff, 0xff, 0x21, 0x97, 0x54, 0xe9, 0x99, 0x08, 0x00, 0x00, } func (m *CertificateSigningRequest) Marshal() (dAtA []byte, err error) { @@ -355,6 +360,21 @@ func (m *CertificateSigningRequestCondition) MarshalToSizedBuffer(dAtA []byte) ( _ = i var l int _ = l + i -= len(m.Status) + copy(dAtA[i:], m.Status) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.Status))) + i-- + dAtA[i] = 0x32 + { + size, err := m.LastTransitionTime.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x2a { size, err := m.LastUpdateTime.MarshalToSizedBuffer(dAtA[:i]) if err != nil { @@ -640,6 +660,10 @@ func (m *CertificateSigningRequestCondition) Size() (n int) { n += 1 + l + sovGenerated(uint64(l)) l = m.LastUpdateTime.Size() n += 1 + l + sovGenerated(uint64(l)) + l = m.LastTransitionTime.Size() + n += 1 + l + sovGenerated(uint64(l)) + l = len(m.Status) + n += 1 + l + sovGenerated(uint64(l)) return n } @@ -763,6 +787,8 @@ func (this *CertificateSigningRequestCondition) String() string { `Reason:` + fmt.Sprintf("%v", this.Reason) + `,`, `Message:` + fmt.Sprintf("%v", this.Message) + `,`, `LastUpdateTime:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.LastUpdateTime), "Time", "v1.Time", 1), `&`, ``, 1) + `,`, + `LastTransitionTime:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.LastTransitionTime), "Time", "v1.Time", 1), `&`, ``, 1) + `,`, + `Status:` + fmt.Sprintf("%v", this.Status) + `,`, `}`, }, "") return s @@ -1143,6 +1169,71 @@ func (m *CertificateSigningRequestCondition) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LastTransitionTime", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.LastTransitionTime.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Status", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Status = k8s_io_api_core_v1.ConditionStatus(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) diff --git a/staging/src/k8s.io/api/certificates/v1beta1/generated.proto b/staging/src/k8s.io/api/certificates/v1beta1/generated.proto index 78d2dbc78fb..4d581d62bef 100644 --- a/staging/src/k8s.io/api/certificates/v1beta1/generated.proto +++ b/staging/src/k8s.io/api/certificates/v1beta1/generated.proto @@ -21,6 +21,7 @@ syntax = 'proto2'; package k8s.io.api.certificates.v1beta1; +import "k8s.io/api/core/v1/generated.proto"; import "k8s.io/apimachinery/pkg/apis/meta/v1/generated.proto"; import "k8s.io/apimachinery/pkg/runtime/generated.proto"; import "k8s.io/apimachinery/pkg/runtime/schema/generated.proto"; @@ -43,9 +44,16 @@ message CertificateSigningRequest { } message CertificateSigningRequestCondition { - // request approval state, currently Approved or Denied. + // type of the condition. Known conditions include "Approved", "Denied", and "Failed". optional string type = 1; + // Status of the condition, one of True, False, Unknown. + // Approved, Denied, and Failed conditions may not be "False" or "Unknown". + // Defaults to "True". + // If unset, should be treated as "True". + // +optional + optional string status = 6; + // brief reason for the request state // +optional optional string reason = 2; @@ -57,6 +65,12 @@ message CertificateSigningRequestCondition { // timestamp for the last update to this condition // +optional optional k8s.io.apimachinery.pkg.apis.meta.v1.Time lastUpdateTime = 4; + + // lastTransitionTime is the time the condition last transitioned from one status to another. + // If unset, when a new condition type is added or an existing condition's status is changed, + // the server defaults this to the current time. + // +optional + optional k8s.io.apimachinery.pkg.apis.meta.v1.Time lastTransitionTime = 5; } message CertificateSigningRequestList { diff --git a/staging/src/k8s.io/api/certificates/v1beta1/types.go b/staging/src/k8s.io/api/certificates/v1beta1/types.go index f5d6b8acd24..340cf994b99 100644 --- a/staging/src/k8s.io/api/certificates/v1beta1/types.go +++ b/staging/src/k8s.io/api/certificates/v1beta1/types.go @@ -19,6 +19,7 @@ package v1beta1 import ( "fmt" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -134,11 +135,18 @@ type RequestConditionType string const ( CertificateApproved RequestConditionType = "Approved" CertificateDenied RequestConditionType = "Denied" + CertificateFailed RequestConditionType = "Failed" ) type CertificateSigningRequestCondition struct { - // request approval state, currently Approved or Denied. + // type of the condition. Known conditions include "Approved", "Denied", and "Failed". Type RequestConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=RequestConditionType"` + // Status of the condition, one of True, False, Unknown. + // Approved, Denied, and Failed conditions may not be "False" or "Unknown". + // Defaults to "True". + // If unset, should be treated as "True". + // +optional + Status v1.ConditionStatus `json:"status" protobuf:"bytes,6,opt,name=status,casttype=k8s.io/api/core/v1.ConditionStatus"` // brief reason for the request state // +optional Reason string `json:"reason,omitempty" protobuf:"bytes,2,opt,name=reason"` @@ -148,6 +156,11 @@ type CertificateSigningRequestCondition struct { // timestamp for the last update to this condition // +optional LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty" protobuf:"bytes,4,opt,name=lastUpdateTime"` + // lastTransitionTime is the time the condition last transitioned from one status to another. + // If unset, when a new condition type is added or an existing condition's status is changed, + // the server defaults this to the current time. + // +optional + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,5,opt,name=lastTransitionTime"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/staging/src/k8s.io/api/certificates/v1beta1/types_swagger_doc_generated.go b/staging/src/k8s.io/api/certificates/v1beta1/types_swagger_doc_generated.go index a2edb45a815..4526747d670 100644 --- a/staging/src/k8s.io/api/certificates/v1beta1/types_swagger_doc_generated.go +++ b/staging/src/k8s.io/api/certificates/v1beta1/types_swagger_doc_generated.go @@ -38,10 +38,12 @@ func (CertificateSigningRequest) SwaggerDoc() map[string]string { } var map_CertificateSigningRequestCondition = map[string]string{ - "type": "request approval state, currently Approved or Denied.", - "reason": "brief reason for the request state", - "message": "human readable message with details about the request state", - "lastUpdateTime": "timestamp for the last update to this condition", + "type": "type of the condition. Known conditions include \"Approved\", \"Denied\", and \"Failed\".", + "status": "Status of the condition, one of True, False, Unknown. Approved, Denied, and Failed conditions may not be \"False\" or \"Unknown\". Defaults to \"True\". If unset, should be treated as \"True\".", + "reason": "brief reason for the request state", + "message": "human readable message with details about the request state", + "lastUpdateTime": "timestamp for the last update to this condition", + "lastTransitionTime": "lastTransitionTime is the time the condition last transitioned from one status to another. If unset, when a new condition type is added or an existing condition's status is changed, the server defaults this to the current time.", } func (CertificateSigningRequestCondition) SwaggerDoc() map[string]string { diff --git a/staging/src/k8s.io/api/certificates/v1beta1/zz_generated.deepcopy.go b/staging/src/k8s.io/api/certificates/v1beta1/zz_generated.deepcopy.go index 11d0f77dd91..0463f5bb0c4 100644 --- a/staging/src/k8s.io/api/certificates/v1beta1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/api/certificates/v1beta1/zz_generated.deepcopy.go @@ -56,6 +56,7 @@ func (in *CertificateSigningRequest) DeepCopyObject() runtime.Object { func (in *CertificateSigningRequestCondition) DeepCopyInto(out *CertificateSigningRequestCondition) { *out = *in in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime) + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) return } diff --git a/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.json b/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.json index a801b472921..e7f6035933c 100644 --- a/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.json +++ b/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.json @@ -61,11 +61,13 @@ "conditions": [ { "type": "o,c鮽ort昍řČ扷5Ɨ", + "status": "ěĂ凗蓏Ŋ蛊ĉy緅縕", "reason": "25", "message": "26", - "lastUpdateTime": "2901-11-14T22:54:07Z" + "lastUpdateTime": "1985-03-23T14:10:57Z", + "lastTransitionTime": "2352-05-22T04:29:36Z" } ], - "certificate": "9Q==" + "certificate": "cw==" } } \ No newline at end of file diff --git a/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.pb b/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.pb index 4ee509f46f5..762cee2ec2b 100644 Binary files a/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.pb and b/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.pb differ diff --git a/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.yaml b/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.yaml index c24c1ff0c80..3207d6282d9 100644 --- a/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.yaml +++ b/staging/src/k8s.io/api/testdata/HEAD/certificates.k8s.io.v1beta1.CertificateSigningRequest.yaml @@ -42,9 +42,11 @@ spec: - J枊a username: "20" status: - certificate: 9Q== + certificate: cw== conditions: - - lastUpdateTime: "2901-11-14T22:54:07Z" + - lastTransitionTime: "2352-05-22T04:29:36Z" + lastUpdateTime: "1985-03-23T14:10:57Z" message: "26" reason: "25" + status: ěĂ凗蓏Ŋ蛊ĉy緅縕 type: o,c鮽ort昍řČ扷5Ɨ diff --git a/staging/src/k8s.io/api/testdata/v1.17.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.json b/staging/src/k8s.io/api/testdata/v1.17.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.json new file mode 100644 index 00000000000..92439f872a8 --- /dev/null +++ b/staging/src/k8s.io/api/testdata/v1.17.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.json @@ -0,0 +1,72 @@ +{ + "kind": "CertificateSigningRequest", + "apiVersion": "certificates.k8s.io/v1beta1", + "metadata": { + "name": "2", + "generateName": "3", + "namespace": "4", + "selfLink": "5", + "uid": "7", + "resourceVersion": "11042405498087606203", + "generation": 8071137005907523419, + "creationTimestamp": null, + "deletionGracePeriodSeconds": -4955867275792137171, + "labels": { + "7": "8" + }, + "annotations": { + "9": "10" + }, + "ownerReferences": [ + { + "apiVersion": "11", + "kind": "12", + "name": "13", + "uid": "Dz廔ȇ{sŊƏp", + "controller": false, + "blockOwnerDeletion": true + } + ], + "finalizers": [ + "14" + ], + "clusterName": "15", + "managedFields": [ + { + "manager": "16", + "operation": "鐊唊飙Ş-U圴÷a/ɔ}摁(湗Ć]", + "apiVersion": "17", + "fieldsType": "18" + } + ] + }, + "spec": { + "request": "OA==", + "usages": [ + "J枊a" + ], + "username": "19", + "uid": "20", + "groups": [ + "21" + ], + "extra": { + "22": [ + "23" +] + } + }, + "status": { + "conditions": [ + { + "type": "o,c鮽ort昍řČ扷5Ɨ", + "status": "", + "reason": "24", + "message": "25", + "lastUpdateTime": "2901-11-14T22:54:07Z", + "lastTransitionTime": null + } + ], + "certificate": "9Q==" + } +} \ No newline at end of file diff --git a/staging/src/k8s.io/api/testdata/v1.17.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.pb b/staging/src/k8s.io/api/testdata/v1.17.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.pb new file mode 100644 index 00000000000..5e865cf1125 Binary files /dev/null and b/staging/src/k8s.io/api/testdata/v1.17.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.pb differ diff --git a/staging/src/k8s.io/api/testdata/v1.17.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.yaml b/staging/src/k8s.io/api/testdata/v1.17.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.yaml new file mode 100644 index 00000000000..0d98daa5bda --- /dev/null +++ b/staging/src/k8s.io/api/testdata/v1.17.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.yaml @@ -0,0 +1,51 @@ +apiVersion: certificates.k8s.io/v1beta1 +kind: CertificateSigningRequest +metadata: + annotations: + "9": "10" + clusterName: "15" + creationTimestamp: null + deletionGracePeriodSeconds: -4955867275792137171 + finalizers: + - "14" + generateName: "3" + generation: 8071137005907523419 + labels: + "7": "8" + managedFields: + - apiVersion: "17" + fieldsType: "18" + manager: "16" + operation: 鐊唊飙Ş-U圴÷a/ɔ}摁(湗Ć] + name: "2" + namespace: "4" + ownerReferences: + - apiVersion: "11" + blockOwnerDeletion: true + controller: false + kind: "12" + name: "13" + uid: Dz廔ȇ{sŊƏp + resourceVersion: "11042405498087606203" + selfLink: "5" + uid: "7" +spec: + extra: + "22": + - "23" + groups: + - "21" + request: OA== + uid: "20" + usages: + - J枊a + username: "19" +status: + certificate: 9Q== + conditions: + - lastTransitionTime: null + lastUpdateTime: "2901-11-14T22:54:07Z" + message: "25" + reason: "24" + status: "" + type: o,c鮽ort昍řČ扷5Ɨ diff --git a/staging/src/k8s.io/api/testdata/v1.18.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.json b/staging/src/k8s.io/api/testdata/v1.18.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.json new file mode 100644 index 00000000000..da9095e7ff3 --- /dev/null +++ b/staging/src/k8s.io/api/testdata/v1.18.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.json @@ -0,0 +1,73 @@ +{ + "kind": "CertificateSigningRequest", + "apiVersion": "certificates.k8s.io/v1beta1", + "metadata": { + "name": "2", + "generateName": "3", + "namespace": "4", + "selfLink": "5", + "uid": "7", + "resourceVersion": "11042405498087606203", + "generation": 8071137005907523419, + "creationTimestamp": null, + "deletionGracePeriodSeconds": -4955867275792137171, + "labels": { + "7": "8" + }, + "annotations": { + "9": "10" + }, + "ownerReferences": [ + { + "apiVersion": "11", + "kind": "12", + "name": "13", + "uid": "Dz廔ȇ{sŊƏp", + "controller": false, + "blockOwnerDeletion": true + } + ], + "finalizers": [ + "14" + ], + "clusterName": "15", + "managedFields": [ + { + "manager": "16", + "operation": "鐊唊飙Ş-U圴÷a/ɔ}摁(湗Ć]", + "apiVersion": "17", + "fieldsType": "18" + } + ] + }, + "spec": { + "request": "OA==", + "signerName": "19", + "usages": [ + "J枊a" + ], + "username": "20", + "uid": "21", + "groups": [ + "22" + ], + "extra": { + "23": [ + "24" +] + } + }, + "status": { + "conditions": [ + { + "type": "o,c鮽ort昍řČ扷5Ɨ", + "status": "", + "reason": "25", + "message": "26", + "lastUpdateTime": "2901-11-14T22:54:07Z", + "lastTransitionTime": null + } + ], + "certificate": "9Q==" + } +} \ No newline at end of file diff --git a/staging/src/k8s.io/api/testdata/v1.18.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.pb b/staging/src/k8s.io/api/testdata/v1.18.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.pb new file mode 100644 index 00000000000..581777549f5 Binary files /dev/null and b/staging/src/k8s.io/api/testdata/v1.18.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.pb differ diff --git a/staging/src/k8s.io/api/testdata/v1.18.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.yaml b/staging/src/k8s.io/api/testdata/v1.18.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.yaml new file mode 100644 index 00000000000..08e9efa5142 --- /dev/null +++ b/staging/src/k8s.io/api/testdata/v1.18.0/certificates.k8s.io.v1beta1.CertificateSigningRequest.after_roundtrip.yaml @@ -0,0 +1,52 @@ +apiVersion: certificates.k8s.io/v1beta1 +kind: CertificateSigningRequest +metadata: + annotations: + "9": "10" + clusterName: "15" + creationTimestamp: null + deletionGracePeriodSeconds: -4955867275792137171 + finalizers: + - "14" + generateName: "3" + generation: 8071137005907523419 + labels: + "7": "8" + managedFields: + - apiVersion: "17" + fieldsType: "18" + manager: "16" + operation: 鐊唊飙Ş-U圴÷a/ɔ}摁(湗Ć] + name: "2" + namespace: "4" + ownerReferences: + - apiVersion: "11" + blockOwnerDeletion: true + controller: false + kind: "12" + name: "13" + uid: Dz廔ȇ{sŊƏp + resourceVersion: "11042405498087606203" + selfLink: "5" + uid: "7" +spec: + extra: + "23": + - "24" + groups: + - "22" + request: OA== + signerName: "19" + uid: "21" + usages: + - J枊a + username: "20" +status: + certificate: 9Q== + conditions: + - lastTransitionTime: null + lastUpdateTime: "2901-11-14T22:54:07Z" + message: "26" + reason: "25" + status: "" + type: o,c鮽ort昍řČ扷5Ɨ diff --git a/staging/src/k8s.io/client-go/util/certificate/certificate_manager.go b/staging/src/k8s.io/client-go/util/certificate/certificate_manager.go index 3cadebc6921..9df414abc6f 100644 --- a/staging/src/k8s.io/client-go/util/certificate/certificate_manager.go +++ b/staging/src/k8s.io/client-go/util/certificate/certificate_manager.go @@ -374,6 +374,9 @@ func getCurrentCertificateOrBootstrap( if err != nil { return nil, false, fmt.Errorf("unable to parse certificate data: %v", err) } + if len(certs) < 1 { + return nil, false, fmt.Errorf("no cert data found") + } bootstrapCert.Leaf = certs[0] if _, err := store.Update(bootstrapCertificatePEM, bootstrapKeyPEM); err != nil { diff --git a/staging/src/k8s.io/client-go/util/certificate/csr/csr.go b/staging/src/k8s.io/client-go/util/certificate/csr/csr.go index 13e6cf3e106..c763f31c20b 100644 --- a/staging/src/k8s.io/client-go/util/certificate/csr/csr.go +++ b/staging/src/k8s.io/client-go/util/certificate/csr/csr.go @@ -112,18 +112,25 @@ func WaitForCertificate(ctx context.Context, client certificatesclient.Certifica if csr.UID != req.UID { return false, fmt.Errorf("csr %q changed UIDs", csr.Name) } + approved := false for _, c := range csr.Status.Conditions { if c.Type == certificates.CertificateDenied { - return false, fmt.Errorf("certificate signing request is not approved, reason: %v, message: %v", c.Reason, c.Message) + return false, fmt.Errorf("certificate signing request is denied, reason: %v, message: %v", c.Reason, c.Message) + } + if c.Type == certificates.CertificateFailed { + return false, fmt.Errorf("certificate signing request failed, reason: %v, message: %v", c.Reason, c.Message) } if c.Type == certificates.CertificateApproved { - if csr.Status.Certificate != nil { - klog.V(2).Infof("certificate signing request %s is issued", csr.Name) - return true, nil - } - klog.V(2).Infof("certificate signing request %s is approved, waiting to be issued", csr.Name) + approved = true } } + if approved { + if len(csr.Status.Certificate) > 0 { + klog.V(2).Infof("certificate signing request %s is issued", csr.Name) + return true, nil + } + klog.V(2).Infof("certificate signing request %s is approved, waiting to be issued", csr.Name) + } return false, nil }, ) diff --git a/staging/src/k8s.io/kubectl/pkg/describe/describe.go b/staging/src/k8s.io/kubectl/pkg/describe/describe.go index cbeca846716..d58df99cd1f 100644 --- a/staging/src/k8s.io/kubectl/pkg/describe/describe.go +++ b/staging/src/k8s.io/kubectl/pkg/describe/describe.go @@ -3261,10 +3261,7 @@ func (p *CertificateSigningRequestDescriber) Describe(namespace, name string, de if err != nil { return "", fmt.Errorf("Error parsing CSR: %v", err) } - status, err := extractCSRStatus(csr) - if err != nil { - return "", err - } + status := extractCSRStatus(csr) var events *corev1.EventList if describerSettings.ShowEvents { @@ -4843,16 +4840,16 @@ func formatEndpoints(endpoints *corev1.Endpoints, ports sets.String) string { return ret } -func extractCSRStatus(csr *certificatesv1beta1.CertificateSigningRequest) (string, error) { - var approved, denied bool +func extractCSRStatus(csr *certificatesv1beta1.CertificateSigningRequest) string { + var approved, denied, failed bool for _, c := range csr.Status.Conditions { switch c.Type { case certificatesv1beta1.CertificateApproved: approved = true case certificatesv1beta1.CertificateDenied: denied = true - default: - return "", fmt.Errorf("unknown csr condition %q", c) + case certificatesv1beta1.CertificateFailed: + failed = true } } var status string @@ -4864,10 +4861,13 @@ func extractCSRStatus(csr *certificatesv1beta1.CertificateSigningRequest) (strin } else { status += "Pending" } + if failed { + status += ",Failed" + } if len(csr.Status.Certificate) > 0 { status += ",Issued" } - return status, nil + return status } // backendStringer behaves just like a string interface and converts the given backend to a string.