diff --git a/pkg/apis/certmanager/validation/certificate_for_issuer.go b/pkg/apis/certmanager/validation/certificate_for_issuer.go new file mode 100644 index 000000000..792b54466 --- /dev/null +++ b/pkg/apis/certmanager/validation/certificate_for_issuer.go @@ -0,0 +1,61 @@ +package validation + +import ( + "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" + "github.com/jetstack/cert-manager/pkg/issuer" +) + +func ValidateCertificateForIssuer(crt *v1alpha1.Certificate, issuerObj v1alpha1.GenericIssuer) field.ErrorList { + el := field.ErrorList{} + + path := field.NewPath("spec") + + issuerType, err := issuer.NameForIssuer(issuerObj) + if err != nil { + el = append(el, field.Invalid(path, err.Error(), err.Error())) + return el + } + + switch issuerType { + case issuer.IssuerACME: + el = append(el, ValidateCertificateForACMEIssuer(&crt.Spec, issuerObj.GetSpec(), path)...) + case issuer.IssuerCA: + el = append(el, ValidateCertificateForCAIssuer(&crt.Spec, issuerObj.GetSpec(), path)...) + case issuer.IssuerVault: + el = append(el, ValidateCertificateForVaultIssuer(&crt.Spec, issuerObj.GetSpec(), path)...) + case issuer.IssuerSelfSigned: + el = append(el, ValidateCertificateForSelfSignedIssuer(&crt.Spec, issuerObj.GetSpec(), path)...) + } + + return el +} + +func ValidateCertificateForACMEIssuer(crt *v1alpha1.CertificateSpec, issuer *v1alpha1.IssuerSpec, specPath *field.Path) field.ErrorList { + el := field.ErrorList{} + + if crt.KeyAlgorithm != v1alpha1.KeyAlgorithm("") && crt.KeyAlgorithm != v1alpha1.RSAKeyAlgorithm { + el = append(el, field.Invalid(specPath.Child("keyAlgorithm"), crt.KeyAlgorithm, "ACME key algorithm must be RSA")) + } + + return el +} + +func ValidateCertificateForCAIssuer(crt *v1alpha1.CertificateSpec, issuer *v1alpha1.IssuerSpec, specPath *field.Path) field.ErrorList { + el := field.ErrorList{} + + return el +} + +func ValidateCertificateForVaultIssuer(crt *v1alpha1.CertificateSpec, issuer *v1alpha1.IssuerSpec, specPath *field.Path) field.ErrorList { + el := field.ErrorList{} + + return el +} + +func ValidateCertificateForSelfSignedIssuer(crt *v1alpha1.CertificateSpec, issuer *v1alpha1.IssuerSpec, specPath *field.Path) field.ErrorList { + el := field.ErrorList{} + + return el +} diff --git a/pkg/apis/certmanager/validation/certificate_for_issuer_test.go b/pkg/apis/certmanager/validation/certificate_for_issuer_test.go new file mode 100644 index 000000000..cc7c6e9df --- /dev/null +++ b/pkg/apis/certmanager/validation/certificate_for_issuer_test.go @@ -0,0 +1,158 @@ +package validation + +import ( + "reflect" + "testing" + + "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" + "github.com/jetstack/cert-manager/test/util/generate" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +const ( + defaultTestIssuerName = "test-issuer" + defaultTestCrtName = "test-crt" + defaultTestNamespace = "default" +) + +func TestValidateCertificateForIssuer(t *testing.T) { + fldPath := field.NewPath("spec") + + scenarios := map[string]struct { + crt *v1alpha1.Certificate + issuer *v1alpha1.Issuer + errs []*field.Error + }{ + "valid basic certificate": { + crt: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + IssuerRef: validIssuerRef, + ACME: &v1alpha1.ACMECertificateConfig{ + Config: []v1alpha1.ACMECertificateDomainConfig{ + { + Domains: []string{"example.com"}, + ACMESolverConfig: v1alpha1.ACMESolverConfig{ + HTTP01: &v1alpha1.ACMECertificateHTTP01Config{}, + }, + }, + }, + }, + }, + }, + + issuer: generate.Issuer(generate.IssuerConfig{ + Name: defaultTestIssuerName, + Namespace: defaultTestNamespace, + }), + }, + "certificate with invalid keyAlgorithm": { + crt: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + KeyAlgorithm: v1alpha1.KeyAlgorithm("blah"), + IssuerRef: validIssuerRef, + ACME: &v1alpha1.ACMECertificateConfig{ + Config: []v1alpha1.ACMECertificateDomainConfig{ + { + Domains: []string{"example.com"}, + ACMESolverConfig: v1alpha1.ACMESolverConfig{ + HTTP01: &v1alpha1.ACMECertificateHTTP01Config{}, + }, + }, + }, + }, + }, + }, + issuer: generate.Issuer(generate.IssuerConfig{ + Name: defaultTestIssuerName, + Namespace: defaultTestNamespace, + }), + errs: []*field.Error{ + field.Invalid(fldPath.Child("keyAlgorithm"), v1alpha1.KeyAlgorithm("blah"), "ACME key algorithm must be RSA"), + }, + }, + "certificate with correct keyAlgorithm for ACME": { + crt: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + KeyAlgorithm: v1alpha1.RSAKeyAlgorithm, + IssuerRef: validIssuerRef, + ACME: &v1alpha1.ACMECertificateConfig{ + Config: []v1alpha1.ACMECertificateDomainConfig{ + { + Domains: []string{"example.com"}, + ACMESolverConfig: v1alpha1.ACMESolverConfig{ + HTTP01: &v1alpha1.ACMECertificateHTTP01Config{}, + }, + }, + }, + }, + }, + }, + issuer: generate.Issuer(generate.IssuerConfig{ + Name: defaultTestIssuerName, + Namespace: defaultTestNamespace, + }), + }, + "certificate with incorrect keyAlgorithm for ACME": { + crt: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + KeyAlgorithm: v1alpha1.ECDSAKeyAlgorithm, + IssuerRef: validIssuerRef, + ACME: &v1alpha1.ACMECertificateConfig{ + Config: []v1alpha1.ACMECertificateDomainConfig{ + { + Domains: []string{"example.com"}, + ACMESolverConfig: v1alpha1.ACMESolverConfig{ + HTTP01: &v1alpha1.ACMECertificateHTTP01Config{}, + }, + }, + }, + }, + }, + }, + issuer: generate.Issuer(generate.IssuerConfig{ + Name: defaultTestIssuerName, + Namespace: defaultTestNamespace, + }), + errs: []*field.Error{ + field.Invalid(fldPath.Child("keyAlgorithm"), v1alpha1.ECDSAKeyAlgorithm, "ACME key algorithm must be RSA"), + }, + }, + "certificate with unspecified issuer type": { + crt: &v1alpha1.Certificate{ + Spec: v1alpha1.CertificateSpec{ + KeyAlgorithm: v1alpha1.ECDSAKeyAlgorithm, + IssuerRef: validIssuerRef, + ACME: &v1alpha1.ACMECertificateConfig{ + Config: []v1alpha1.ACMECertificateDomainConfig{ + { + Domains: []string{"example.com"}, + ACMESolverConfig: v1alpha1.ACMESolverConfig{ + HTTP01: &v1alpha1.ACMECertificateHTTP01Config{}, + }, + }, + }, + }, + }, + }, + issuer: &v1alpha1.Issuer{}, + errs: []*field.Error{ + field.Invalid(fldPath, "no issuer specified for Issuer '/'", "no issuer specified for Issuer '/'"), + }, + }, + } + for n, s := range scenarios { + t.Run(n, func(t *testing.T) { + errs := ValidateCertificateForIssuer(s.crt, s.issuer) + if len(errs) != len(s.errs) { + t.Errorf("Expected %v but got %v", s.errs, errs) + return + } + for i, e := range errs { + expectedErr := s.errs[i] + if !reflect.DeepEqual(e, expectedErr) { + t.Errorf("Expected %v but got %v", expectedErr, e) + } + } + }) + } +} diff --git a/pkg/controller/certificates/sync.go b/pkg/controller/certificates/sync.go index aa98aa8e3..e02ee4f4f 100644 --- a/pkg/controller/certificates/sync.go +++ b/pkg/controller/certificates/sync.go @@ -82,6 +82,22 @@ func (c *Controller) Sync(ctx context.Context, crt *v1alpha1.Certificate) (err e return err } + el = validation.ValidateCertificateForIssuer(crtCopy, issuerObj) + if len(el) > 0 { + msg := fmt.Sprintf("Resource validation failed: %v", el.ToAggregate()) + crtCopy.UpdateStatusCondition(v1alpha1.CertificateConditionReady, v1alpha1.ConditionFalse, errorConfig, msg, false) + return + } else { + for i, c := range crtCopy.Status.Conditions { + if c.Type == v1alpha1.CertificateConditionReady { + if c.Reason == errorConfig && c.Status == v1alpha1.ConditionFalse { + crtCopy.Status.Conditions = append(crtCopy.Status.Conditions[:i], crtCopy.Status.Conditions[i+1:]...) + break + } + } + } + } + issuerReady := issuerObj.HasCondition(v1alpha1.IssuerCondition{ Type: v1alpha1.IssuerConditionReady, Status: v1alpha1.ConditionTrue, diff --git a/pkg/issuer/const.go b/pkg/issuer/const.go index 7b36fa95d..6bffcbb38 100644 --- a/pkg/issuer/const.go +++ b/pkg/issuer/const.go @@ -17,9 +17,9 @@ const ( IssuerSelfSigned string = "selfsigned" ) -// nameForIssuer determines the name of the issuer implementation given an +// NameForIssuer determines the name of the issuer implementation given an // Issuer resource. -func nameForIssuer(i v1alpha1.GenericIssuer) (string, error) { +func NameForIssuer(i v1alpha1.GenericIssuer) (string, error) { switch { case i.GetSpec().ACME != nil: return IssuerACME, nil diff --git a/pkg/issuer/factory.go b/pkg/issuer/factory.go index 9f6abcf7d..02d7d1a27 100644 --- a/pkg/issuer/factory.go +++ b/pkg/issuer/factory.go @@ -30,7 +30,7 @@ type factory struct { // however this is an inexpensive operation and so, Issuers should not need // to be cached and reused. func (f *factory) IssuerFor(issuer v1alpha1.GenericIssuer) (Interface, error) { - issuerType, err := nameForIssuer(issuer) + issuerType, err := NameForIssuer(issuer) if err != nil { return nil, fmt.Errorf("could not get issuer type: %s", err.Error()) }