make serviceType configurable, fixes #928

Signed-off-by: Arnold Bechtoldt <arnold.bechtoldt@inovex.de>
This commit is contained in:
Arnold Bechtoldt 2018-09-27 15:27:30 +02:00
parent d374e33b71
commit d261e1f3f1
11 changed files with 85 additions and 17 deletions

View File

@ -405,7 +405,15 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1.ACMEIssuerHTTP01Config": {
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Properties: map[string]spec.Schema{},
Description: "ACMEIssuerHTTP01Config is a structure containing the ACME HTTP configuration options",
Properties: map[string]spec.Schema{
"solverServiceType": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
},
},
Dependencies: []string{},

View File

@ -871,6 +871,7 @@ Appears In:
</tr>
</tbody>
</table>
<p>ACMEIssuerHTTP01Config is a structure containing the ACME HTTP configuration options</p>
<aside class="notice">
Appears In:
@ -887,6 +888,7 @@ Appears In:
</thead>
<tbody>
<tr>
<td><code>solverServiceType</code><br /> <em>string</em></td>
<td></td>
</tr>
</tbody>

View File

@ -26,6 +26,15 @@ using Ingress resources
http01: {}
To define which Kubernetes service type to use during challenge response specify the following http01 config
.. code-block:: yaml
http01:
# Valid values are ClusterIP and NodePort
solverServiceType: ClusterIP
By default type NodePort will be used when you don't set http01 or when you set solverServiceType to an empty string. This may change in future.
.. note::
Let's Encrypt does not support issuing wildcard certificates with HTTP-01 challenges.
To issue wildcard certificates, you must use the DNS-01 challenge.

View File

@ -19,6 +19,7 @@ go_library(
deps = [
"//pkg/apis/certmanager:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",

View File

@ -17,6 +17,7 @@ limitations under the License.
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -134,7 +135,9 @@ type ACMEIssuer struct {
DNS01 *ACMEIssuerDNS01Config `json:"dns01,omitempty"`
}
// ACMEIssuerHTTP01Config is a structure containing the ACME HTTP configuration options
type ACMEIssuerHTTP01Config struct {
SolverServiceType corev1.ServiceType `json:"solverServiceType,omitempty"`
}
// ACMEIssuerDNS01Config is a structure containing the ACME DNS configuration

View File

@ -14,6 +14,7 @@ go_library(
"//pkg/apis/certmanager/v1alpha1:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/issuer/acme/dns/rfc2136:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
],
)
@ -30,6 +31,7 @@ go_test(
"//pkg/apis/certmanager/v1alpha1:go_default_library",
"//pkg/issuer/acme/dns/rfc2136:go_default_library",
"//test/util/generate:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
],
)

View File

@ -17,10 +17,12 @@ limitations under the License.
package validation
import (
"fmt"
"strings"
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/rfc2136"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
@ -125,7 +127,26 @@ func ValidateVaultIssuerConfig(iss *v1alpha1.VaultIssuer, fldPath *field.Path) f
}
func ValidateACMEIssuerHTTP01Config(iss *v1alpha1.ACMEIssuerHTTP01Config, fldPath *field.Path) field.ErrorList {
return nil
el := field.ErrorList{}
if len(iss.SolverServiceType) > 0 {
validTypes := []corev1.ServiceType{
corev1.ServiceTypeClusterIP,
corev1.ServiceTypeNodePort,
}
validType := false
for _, validTypeName := range validTypes {
if iss.SolverServiceType == validTypeName {
validType = true
break
}
}
if !validType {
el = append(el, field.Invalid(fldPath.Child("solverServiceType"), iss.SolverServiceType, fmt.Sprintf("optional field solverServiceType must be one of %q", validTypes)))
}
}
return el
}
func ValidateACMEIssuerDNS01Config(iss *v1alpha1.ACMEIssuerDNS01Config, fldPath *field.Path) field.ErrorList {

View File

@ -20,6 +20,7 @@ import (
"reflect"
"testing"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
@ -146,6 +147,19 @@ func TestValidateACMEIssuerConfig(t *testing.T) {
HTTP01: &v1alpha1.ACMEIssuerHTTP01Config{},
},
},
"acme issue with invalid http01 service config": {
spec: &v1alpha1.ACMEIssuer{
Email: "valid-email",
Server: "valid-server",
PrivateKey: validSecretKeyRef,
HTTP01: &v1alpha1.ACMEIssuerHTTP01Config{
SolverServiceType: corev1.ServiceType("InvalidServiceType"),
},
},
errs: []*field.Error{
field.Invalid(fldPath.Child("http01", "solverServiceType"), corev1.ServiceType("InvalidServiceType"), "optional field solverServiceType must be one of [\"ClusterIP\" \"NodePort\"]"),
},
},
}
for n, s := range scenarios {
t.Run(n, func(t *testing.T) {

View File

@ -81,7 +81,7 @@ func NewSolver(ctx *controller.Context) *Solver {
// will return nil (i.e. this function is idempotent).
func (s *Solver) Present(ctx context.Context, issuer v1alpha1.GenericIssuer, crt *v1alpha1.Certificate, ch v1alpha1.ACMEOrderChallenge) error {
_, podErr := s.ensurePod(crt, ch)
svc, svcErr := s.ensureService(crt, ch)
svc, svcErr := s.ensureService(issuer, crt, ch)
if svcErr != nil {
return utilerrors.NewAggregate([]error{podErr, svcErr})
}

View File

@ -30,7 +30,7 @@ import (
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
)
func (s *Solver) ensureService(crt *v1alpha1.Certificate, ch v1alpha1.ACMEOrderChallenge) (*corev1.Service, error) {
func (s *Solver) ensureService(issuer v1alpha1.GenericIssuer, crt *v1alpha1.Certificate, ch v1alpha1.ACMEOrderChallenge) (*corev1.Service, error) {
existingServices, err := s.getServicesForChallenge(crt, ch)
if err != nil {
return nil, err
@ -49,7 +49,7 @@ func (s *Solver) ensureService(crt *v1alpha1.Certificate, ch v1alpha1.ACMEOrderC
}
glog.Infof("No existing HTTP01 challenge solver service found for Certificate %q. One will be created.", crt.Namespace+"/"+crt.Name)
return s.createService(crt, ch)
return s.createService(issuer, crt, ch)
}
// getServicesForChallenge returns a list of services that were created to solve
@ -85,13 +85,13 @@ func (s *Solver) getServicesForChallenge(crt *v1alpha1.Certificate, ch v1alpha1.
// createService will create the service required to solve this challenge
// in the target API server.
func (s *Solver) createService(crt *v1alpha1.Certificate, ch v1alpha1.ACMEOrderChallenge) (*corev1.Service, error) {
return s.Client.CoreV1().Services(crt.Namespace).Create(buildService(crt, ch))
func (s *Solver) createService(issuer v1alpha1.GenericIssuer, crt *v1alpha1.Certificate, ch v1alpha1.ACMEOrderChallenge) (*corev1.Service, error) {
return s.Client.CoreV1().Services(crt.Namespace).Create(buildService(issuer, crt, ch))
}
func buildService(crt *v1alpha1.Certificate, ch v1alpha1.ACMEOrderChallenge) *corev1.Service {
func buildService(issuer v1alpha1.GenericIssuer, crt *v1alpha1.Certificate, ch v1alpha1.ACMEOrderChallenge) *corev1.Service {
podLabels := podLabels(ch)
return &corev1.Service{
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "cm-acme-http-solver-",
Namespace: crt.Namespace,
@ -102,7 +102,6 @@ func buildService(crt *v1alpha1.Certificate, ch v1alpha1.ACMEOrderChallenge) *co
OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(crt, certificateGvk)},
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeNodePort,
Ports: []corev1.ServicePort{
{
Name: "http",
@ -113,6 +112,15 @@ func buildService(crt *v1alpha1.Certificate, ch v1alpha1.ACMEOrderChallenge) *co
Selector: podLabels,
},
}
// http01 config is still optional, thus checking for presence and setting previous default
if issuer.GetSpec().ACME.HTTP01 == nil {
service.Spec.Type = corev1.ServiceTypeNodePort //TODO drop this line in future to use Kubernetes' default
} else if issuer.GetSpec().ACME.HTTP01.SolverServiceType != "" {
service.Spec.Type = issuer.GetSpec().ACME.HTTP01.SolverServiceType
}
return service
}
func (s *Solver) cleanupServices(crt *v1alpha1.Certificate, ch v1alpha1.ACMEOrderChallenge) error {

View File

@ -46,7 +46,7 @@ func TestEnsureService(t *testing.T) {
Domain: "example.com",
},
PreFn: func(t *testing.T, s *solverFixture) {
svc, err := s.Solver.createService(s.Certificate, s.Challenge)
svc, err := s.Solver.createService(s.Issuer, s.Certificate, s.Challenge)
if err != nil {
t.Errorf("error preparing test: %v", err)
}
@ -89,7 +89,7 @@ func TestEnsureService(t *testing.T) {
Domain: "example.com",
},
PreFn: func(t *testing.T, s *solverFixture) {
expectedService := buildService(s.Certificate, s.Challenge)
expectedService := buildService(s.Issuer, s.Certificate, s.Challenge)
// create a reactor that fails the test if a service is created
s.Builder.FakeKubeClient().PrependReactor("create", "services", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) {
service := action.(coretesting.CreateAction).GetObject().(*v1.Service)
@ -142,11 +142,11 @@ func TestEnsureService(t *testing.T) {
},
Err: true,
PreFn: func(t *testing.T, s *solverFixture) {
_, err := s.Solver.createService(s.Certificate, s.Challenge)
_, err := s.Solver.createService(s.Issuer, s.Certificate, s.Challenge)
if err != nil {
t.Errorf("error preparing test: %v", err)
}
_, err = s.Solver.createService(s.Certificate, s.Challenge)
_, err = s.Solver.createService(s.Issuer, s.Certificate, s.Challenge)
if err != nil {
t.Errorf("error preparing test: %v", err)
}
@ -169,7 +169,7 @@ func TestEnsureService(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
test.Setup(t)
resp, err := test.Solver.ensureService(test.Certificate, test.Challenge)
resp, err := test.Solver.ensureService(test.Issuer, test.Certificate, test.Challenge)
if err != nil && !test.Err {
t.Errorf("Expected function to not error, but got: %v", err)
}
@ -198,7 +198,7 @@ func TestGetServicesForCertificate(t *testing.T) {
Domain: "example.com",
},
PreFn: func(t *testing.T, s *solverFixture) {
ing, err := s.Solver.createService(s.Certificate, s.Challenge)
ing, err := s.Solver.createService(s.Issuer, s.Certificate, s.Challenge)
if err != nil {
t.Errorf("error preparing test: %v", err)
}
@ -232,7 +232,7 @@ func TestGetServicesForCertificate(t *testing.T) {
Domain: "example.com",
},
PreFn: func(t *testing.T, s *solverFixture) {
_, err := s.Solver.createService(s.Certificate, v1alpha1.ACMEOrderChallenge{
_, err := s.Solver.createService(s.Issuer, s.Certificate, v1alpha1.ACMEOrderChallenge{
Domain: "invaliddomain",
})
if err != nil {