diff --git a/docs/serving-api.md b/docs/serving-api.md index 08fe4a2fb611..db83df6bfa04 100644 --- a/docs/serving-api.md +++ b/docs/serving-api.md @@ -2110,6 +2110,20 @@ Service.
Knative Routes, and by Kubernetes Services. +tlsTLS allows the DomainMapping to terminate TLS traffic with an existing secret.
+tlsTLS allows the DomainMapping to terminate TLS traffic with an existing secret.
++(Appears on:DomainMappingSpec) +
++
SecretTLS wrapper for TLS SecretName.
+ +| Field | +Description | +
|---|---|
+secretName+ +string + + |
+
+ SecretName is the name of the existing secret used to terminate TLS traffic. + |
+
Generated with gen-crd-api-reference-docs
diff --git a/pkg/apis/serving/v1alpha1/domainmapping_lifecycle.go b/pkg/apis/serving/v1alpha1/domainmapping_lifecycle.go
index 4ab88d4396f2..ed189c3dd66a 100644
--- a/pkg/apis/serving/v1alpha1/domainmapping_lifecycle.go
+++ b/pkg/apis/serving/v1alpha1/domainmapping_lifecycle.go
@@ -63,6 +63,9 @@ const (
// DomainMappingConditionCertificateProvisioned condition when it is set to True
// because AutoTLS was not enabled.
AutoTLSNotEnabledMessage = "autoTLS is not enabled"
+ // TLSCertificateProvidedExternally indicates that a TLS secret won't be created or managed
+ // instead a reference to an existing TLS secret should have been provided in the DomainMapping spec
+ TLSCertificateProvidedExternally = "TLS certificate was provided externally"
)
// MarkTLSNotEnabled sets DomainMappingConditionCertificateProvisioned to true when
@@ -72,6 +75,11 @@ func (dms *DomainMappingStatus) MarkTLSNotEnabled(msg string) {
"TLSNotEnabled", msg)
}
+func (dms *DomainMappingStatus) MarkCertificateNotRequired(msg string) {
+ domainMappingCondSet.Manage(dms).MarkTrueWithReason(DomainMappingConditionCertificateProvisioned,
+ "CertificateExternallyProvided", msg)
+}
+
// MarkCertificateReady marks the DomainMappingConditionCertificateProvisioned
// condition to indicate that the Certificate is ready.
func (dms *DomainMappingStatus) MarkCertificateReady(name string) {
diff --git a/pkg/apis/serving/v1alpha1/domainmapping_types.go b/pkg/apis/serving/v1alpha1/domainmapping_types.go
index bcbbf9204961..10d30a6b017f 100644
--- a/pkg/apis/serving/v1alpha1/domainmapping_types.go
+++ b/pkg/apis/serving/v1alpha1/domainmapping_types.go
@@ -70,6 +70,12 @@ type DomainMappingList struct {
Items []DomainMapping `json:"items"`
}
+// SecretTLS wrapper for TLS SecretName.
+type SecretTLS struct {
+ // SecretName is the name of the existing secret used to terminate TLS traffic.
+ SecretName string `json:"secretName"`
+}
+
// DomainMappingSpec describes the DomainMapping the user wishes to exist.
type DomainMappingSpec struct {
// Ref specifies the target of the Domain Mapping.
@@ -82,6 +88,10 @@ type DomainMappingSpec struct {
// This contract is satisfied by Knative types such as Knative Services and
// Knative Routes, and by Kubernetes Services.
Ref duckv1.KReference `json:"ref"`
+
+ // TLS allows the DomainMapping to terminate TLS traffic with an existing secret.
+ // +optional
+ TLS *SecretTLS `json:"tls,omitempty"`
}
// DomainMappingStatus describes the current state of the DomainMapping.
diff --git a/pkg/apis/serving/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/serving/v1alpha1/zz_generated.deepcopy.go
index 843511cdf1e5..8565d4ef31f4 100644
--- a/pkg/apis/serving/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/serving/v1alpha1/zz_generated.deepcopy.go
@@ -47,7 +47,7 @@ func (in *DomainMapping) DeepCopyInto(out *DomainMapping) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
- out.Spec = in.Spec
+ in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
return
}
@@ -107,6 +107,11 @@ func (in *DomainMappingList) DeepCopyObject() runtime.Object {
func (in *DomainMappingSpec) DeepCopyInto(out *DomainMappingSpec) {
*out = *in
out.Ref = in.Ref
+ if in.TLS != nil {
+ in, out := &in.TLS, &out.TLS
+ *out = new(SecretTLS)
+ **out = **in
+ }
return
}
@@ -146,3 +151,19 @@ func (in *DomainMappingStatus) DeepCopy() *DomainMappingStatus {
in.DeepCopyInto(out)
return out
}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SecretTLS) DeepCopyInto(out *SecretTLS) {
+ *out = *in
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretTLS.
+func (in *SecretTLS) DeepCopy() *SecretTLS {
+ if in == nil {
+ return nil
+ }
+ out := new(SecretTLS)
+ in.DeepCopyInto(out)
+ return out
+}
diff --git a/pkg/reconciler/domainmapping/reconciler.go b/pkg/reconciler/domainmapping/reconciler.go
index f832195ab417..cc6fe1e22f9a 100644
--- a/pkg/reconciler/domainmapping/reconciler.go
+++ b/pkg/reconciler/domainmapping/reconciler.go
@@ -199,6 +199,16 @@ func certClass(ctx context.Context) string {
}
func (r *Reconciler) tls(ctx context.Context, dm *v1alpha1.DomainMapping) ([]netv1alpha1.IngressTLS, []netv1alpha1.HTTP01Challenge, error) {
+ if dm.Spec.TLS != nil {
+ dm.Status.MarkCertificateNotRequired(v1alpha1.TLSCertificateProvidedExternally)
+ dm.Status.URL.Scheme = "https"
+ return []netv1alpha1.IngressTLS{{
+ Hosts: []string{dm.Name},
+ SecretName: dm.Spec.TLS.SecretName,
+ SecretNamespace: dm.Namespace,
+ }}, nil, nil
+ }
+
if !autoTLSEnabled(ctx, dm) {
dm.Status.MarkTLSNotEnabled(v1.AutoTLSNotEnabledMessage)
return nil, nil, nil
diff --git a/pkg/reconciler/domainmapping/table_test.go b/pkg/reconciler/domainmapping/table_test.go
index 0d4c42d275f6..4ebd729017f9 100644
--- a/pkg/reconciler/domainmapping/table_test.go
+++ b/pkg/reconciler/domainmapping/table_test.go
@@ -1108,6 +1108,49 @@ func TestReconcileTLSEnabled(t *testing.T) {
WantEvents: []string{
Eventf(corev1.EventTypeNormal, "FinalizerUpdate", "Updated %q finalizers", "challenged.com"),
},
+ }, {
+ Name: "TLS secret provided",
+ Key: "default/certificateless.com",
+ Objects: []runtime.Object{
+ ksvc("default", "ready", "ready.default.svc.cluster.local", ""),
+ domainMapping("default", "certificateless.com",
+ withTLSSecret("tls-secret"),
+ withRef("default", "ready"),
+ withURL("https", "certificateless.com"),
+ withAddress("https", "certificateless.com"),
+ ),
+ resources.MakeDomainClaim(domainMapping("default", "certificateless.com", withRef("default", "ready"))),
+ },
+ WantCreates: []runtime.Object{
+ ingress(domainMapping("default", "certificateless.com", withRef("default", "ready")), "the-ingress-class",
+ withIngressHTTPOption(netv1alpha1.HTTPOptionRedirected),
+ withIngressTLS(netv1alpha1.IngressTLS{
+ Hosts: []string{"certificateless.com"},
+ SecretName: "tls-secret",
+ SecretNamespace: "default",
+ })),
+ },
+ WantStatusUpdates: []clientgotesting.UpdateActionImpl{{
+ Object: domainMapping("default", "certificateless.com",
+ withRef("default", "ready"),
+ withURL("https", "certificateless.com"),
+ withAddress("https", "certificateless.com"),
+ withTLSSecret("tls-secret"),
+ withInitDomainMappingConditions,
+ withCertificateReady,
+ withDomainClaimed,
+ withReferenceResolved,
+ withCertificateNotRequired,
+ withIngressNotConfigured,
+ ),
+ }},
+ WantPatches: []clientgotesting.PatchActionImpl{
+ patchAddFinalizerAction("default", "certificateless.com"),
+ },
+ WantEvents: []string{
+ Eventf(corev1.EventTypeNormal, "FinalizerUpdate", "Updated %q finalizers", "certificateless.com"),
+ Eventf(corev1.EventTypeNormal, "Created", "Created Ingress %q", "certificateless.com"),
+ },
}}
table.Test(t, MakeFactory(func(ctx context.Context, listers *Listers, cmw configmap.Watcher) controller.Reconciler {
@@ -1276,6 +1319,18 @@ func withPropagatedStatus(status netv1alpha1.IngressStatus) domainMappingOption
}
}
+func withTLSSecret(secretName string) domainMappingOption {
+ return func(r *v1alpha1.DomainMapping) {
+ r.Spec.TLS = &v1alpha1.SecretTLS{
+ SecretName: secretName,
+ }
+ }
+}
+
+func withCertificateNotRequired(dm *v1alpha1.DomainMapping) {
+ dm.Status.MarkCertificateNotRequired(v1alpha1.TLSCertificateProvidedExternally)
+}
+
func withInitDomainMappingConditions(dm *v1alpha1.DomainMapping) {
dm.Status.InitializeConditions()
}
diff --git a/test/e2e/domain_mapping_test.go b/test/e2e/domain_mapping_test.go
new file mode 100644
index 000000000000..2a2b74ce0cdd
--- /dev/null
+++ b/test/e2e/domain_mapping_test.go
@@ -0,0 +1,209 @@
+// +build e2e
+
+/*
+Copyright 2021 The Knative Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package e2e
+
+import (
+ "bytes"
+ "context"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "math/big"
+ "net/http"
+ "net/url"
+ "testing"
+ "time"
+
+ "knative.dev/pkg/test/spoof"
+
+ corev1 "k8s.io/api/core/v1"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/util/wait"
+ duckv1 "knative.dev/pkg/apis/duck/v1"
+ pkgTest "knative.dev/pkg/test"
+ "knative.dev/serving/pkg/apis/serving/v1alpha1"
+ "knative.dev/serving/test"
+ v1test "knative.dev/serving/test/v1"
+)
+
+func TestBYOCertificate(t *testing.T) {
+ if !test.ServingFlags.EnableAlphaFeatures {
+ t.Skip("Alpha features not enabled")
+ }
+ t.Parallel()
+
+ clients := test.Setup(t)
+
+ names := test.ResourceNames{
+ Service: test.ObjectNameForTest(t),
+ Image: test.PizzaPlanet1,
+ }
+
+ test.EnsureTearDown(t, clients, &names)
+
+ ctx := context.Background()
+
+ ksvc, err := v1test.CreateServiceReady(t, clients, &names)
+ if err != nil {
+ t.Fatalf("Failed to create initial Service %v: %v", names.Service, err)
+ }
+
+ host := ksvc.Service.Name + ".example.org"
+
+ resolvableCustomDomain := false
+
+ if test.ServingFlags.CustomDomain != "" {
+ host = ksvc.Service.Name + "." + test.ServingFlags.CustomDomain
+ resolvableCustomDomain = true
+ }
+
+ cert, key := makeCertificateForDomain(t, host)
+ secret, err := clients.KubeClient.CoreV1().Secrets(test.ServingNamespace).Create(ctx, &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: test.AppendRandomString("byocert-secret"),
+ },
+ Type: corev1.SecretTypeTLS,
+ Data: map[string][]byte{
+ corev1.TLSCertKey: cert,
+ corev1.TLSPrivateKeyKey: key,
+ },
+ }, metav1.CreateOptions{})
+
+ if err != nil {
+ t.Fatalf("Secret creation could not be completed: %v", err)
+ }
+
+ t.Cleanup(func() {
+ err = clients.KubeClient.CoreV1().Secrets(secret.Namespace).Delete(ctx, secret.Name, metav1.DeleteOptions{})
+ if err != nil {
+ t.Fatalf("Secret %s/%s could not be deleted: %v", secret.Namespace, secret.Name, err)
+ }
+ })
+
+ dm := v1alpha1.DomainMapping{
+ TypeMeta: metav1.TypeMeta{},
+ ObjectMeta: metav1.ObjectMeta{
+ Name: host,
+ Namespace: ksvc.Service.Namespace,
+ },
+ Spec: v1alpha1.DomainMappingSpec{
+ Ref: duckv1.KReference{
+ APIVersion: "serving.knative.dev/v1",
+ Name: ksvc.Service.Name,
+ Namespace: ksvc.Service.Namespace,
+ Kind: "Service",
+ },
+ TLS: &v1alpha1.SecretTLS{
+ SecretName: secret.Name,
+ }},
+ Status: v1alpha1.DomainMappingStatus{},
+ }
+
+ _, err = clients.ServingAlphaClient.DomainMappings.Create(ctx, &dm, metav1.CreateOptions{})
+ if err != nil {
+ t.Fatalf("Problem creating DomainMapping %q: %v", host, err)
+ }
+ t.Cleanup(func() {
+ clients.ServingAlphaClient.DomainMappings.Delete(ctx, dm.Name, metav1.DeleteOptions{})
+ })
+
+ err = wait.PollImmediate(test.PollInterval, test.PollTimeout, func() (bool, error) {
+ dm, err := clients.ServingAlphaClient.DomainMappings.Get(ctx, dm.Name, metav1.GetOptions{})
+ if err != nil {
+ return false, err
+ }
+ ready := dm.Status.IsReady() && dm.Status.URL != nil && dm.Status.URL.Scheme == "https"
+ return ready, nil
+ })
+ if err != nil {
+ t.Fatalf("Polling for DomainMapping state produced an error: %v", err)
+ }
+
+ pemData := test.PemDataFromSecret(ctx, t.Logf, clients, secret.Namespace, secret.Name)
+ rootCAs := x509.NewCertPool()
+ if !rootCAs.AppendCertsFromPEM(pemData) {
+ t.Fatalf("Failed to add the certificate to the root CA")
+ }
+
+ var trustSelfSigned spoof.TransportOption = func(transport *http.Transport) *http.Transport {
+ transport.TLSClientConfig = &tls.Config{
+ RootCAs: rootCAs,
+ }
+ return transport
+ }
+
+ _, err = pkgTest.WaitForEndpointState(ctx,
+ clients.KubeClient,
+ t.Logf,
+ &url.URL{Scheme: "HTTPS", Host: host},
+ pkgTest.EventuallyMatchesBody(test.PizzaPlanetText1),
+ "DomainMappingBYOSSLCert",
+ resolvableCustomDomain,
+ trustSelfSigned,
+ )
+ if err != nil {
+ t.Fatalf("Service response unavailable: %v", err)
+ }
+}
+
+func makeCertificateForDomain(t *testing.T, domainName string) (cert []byte, key []byte) {
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+ serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+ if err != nil {
+ t.Fatalf("Failed to generate serial number: %v", err)
+ }
+ priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatalf("Failed to generate private key: %v", err)
+ }
+ public := &priv.PublicKey
+ now := time.Now()
+ template := x509.Certificate{
+ SerialNumber: serialNumber,
+ Subject: pkix.Name{
+ Organization: []string{"Acme Co"},
+ },
+ NotBefore: now,
+ NotAfter: now.Add(time.Hour * 12),
+
+ KeyUsage: x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+ BasicConstraintsValid: true,
+ DNSNames: []string{domainName},
+ }
+ derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, public, priv)
+ if err != nil {
+ t.Fatalf("Failed to create certificate: %v", err)
+ }
+ var certOut bytes.Buffer
+ pem.Encode(&certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
+
+ privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
+ if err != nil {
+ t.Fatalf("Failed to marshal private key: %v", err)
+ }
+ var keyOut bytes.Buffer
+ pem.Encode(&keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
+ return certOut.Bytes(), keyOut.Bytes()
+}
diff --git a/test/e2e_flags.go b/test/e2e_flags.go
index 83975c9d6113..a804840ce71c 100644
--- a/test/e2e_flags.go
+++ b/test/e2e_flags.go
@@ -32,7 +32,7 @@ var ServingFlags = initializeServingFlags()
// ServingEnvironmentFlags holds the e2e flags needed only by the serving repo.
type ServingEnvironmentFlags struct {
ResolvableDomain bool // Resolve Route controller's `domainSuffix`
- CustomDomain string // Indicaates the `domainSuffix` for custom domain test.
+ CustomDomain string // Indicates the `domainSuffix` for custom domain test.
HTTPS bool // Indicates where the test service will be created with https
Buckets int // The number of reconciler buckets configured.
Replicas int // The number of controlplane replicas being run.