diff --git a/test/config-transport-encryption/features.yaml b/test/config-transport-encryption/features.yaml new file mode 100644 index 00000000000..f3485538a8c --- /dev/null +++ b/test/config-transport-encryption/features.yaml @@ -0,0 +1,24 @@ +# 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 +# +# https://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. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-features + namespace: knative-eventing + labels: + knative.dev/config-propagation: original + knative.dev/config-category: eventing +data: + transport-encryption: "strict" diff --git a/test/e2e-common.sh b/test/e2e-common.sh index 3dba143d1b6..687b3c15471 100755 --- a/test/e2e-common.sh +++ b/test/e2e-common.sh @@ -49,7 +49,7 @@ readonly CONFIG_TRACING_CONFIG="test/config/config-tracing.yaml" readonly KNATIVE_EVENTING_MONITORING_YAML="test/config/monitoring.yaml" # The number of controlplane replicas to run. -readonly REPLICAS=3 +readonly REPLICAS=${REPLICAS:-3} # Should deploy a Knative Monitoring as well readonly DEPLOY_KNATIVE_MONITORING="${DEPLOY_KNATIVE_MONITORING:-1}" @@ -76,6 +76,8 @@ UNINSTALL_LIST=() # Setup the Knative environment for running tests. function knative_setup() { + install_cert_manager || fail_test "Could not install Cert Manager" + install_knative_eventing "HEAD" install_mt_broker || fail_test "Could not install MT Channel Based Broker" @@ -83,8 +85,6 @@ function knative_setup() { enable_sugar || fail_test "Could not enable Sugar Controller Injection" unleash_duck || fail_test "Could not unleash the chaos duck" - - install_cert_manager || fail_test "Could not install Cert Manager" } function scale_controlplane() { @@ -147,6 +147,12 @@ function install_knative_eventing() { -f "${EVENTING_CORE_NAME}" || return 1 UNINSTALL_LIST+=( "${EVENTING_CORE_NAME}" ) + local EVENTING_TLS_NAME=${TMP_DIR}/${EVENTING_TLS_YAML##*/} + sed "s/namespace: ${KNATIVE_DEFAULT_NAMESPACE}/namespace: ${SYSTEM_NAMESPACE}/g" ${EVENTING_TLS_YAML} > ${EVENTING_TLS_NAME} + kubectl apply \ + -f "${EVENTING_TLS_NAME}" || return 1 + UNINSTALL_LIST+=( "${EVENTING_TLS_NAME}" ) + kubectl patch horizontalpodautoscalers.autoscaling -n ${SYSTEM_NAMESPACE} eventing-webhook -p '{"spec": {"minReplicas": '${REPLICAS}'}}' || return 1 else diff --git a/test/e2e-rekt-tests.sh b/test/e2e-rekt-tests.sh index 1826ef4f115..b35b2a7bddb 100755 --- a/test/e2e-rekt-tests.sh +++ b/test/e2e-rekt-tests.sh @@ -38,4 +38,10 @@ echo "Running E2E Reconciler Tests" go_test_e2e -timeout=1h ./test/rekt || fail_test +echo "Running E2E Reconciler Tests with strict transport encryption" + +kubectl apply -Rf "$(dirname "$0")/config-transport-encryption" + +go_test_e2e -timeout=1h ./test/rekt -run TLS || fail_test + success diff --git a/test/e2e-upgrade-tests.sh b/test/e2e-upgrade-tests.sh index 8af0a4ad3bc..af9fdecbac9 100755 --- a/test/e2e-upgrade-tests.sh +++ b/test/e2e-upgrade-tests.sh @@ -26,8 +26,7 @@ source "$(dirname "${BASH_SOURCE[0]}")/e2e-common.sh" # Overrides function knative_setup { - # Nothing to do at setup - true + install_cert_manager || return $? } function install_test_resources { diff --git a/test/rekt/broker_test.go b/test/rekt/broker_test.go index 39ac3270a69..1152f9bb639 100644 --- a/test/rekt/broker_test.go +++ b/test/rekt/broker_test.go @@ -26,6 +26,7 @@ import ( "knative.dev/pkg/system" _ "knative.dev/pkg/system/testing" "knative.dev/reconciler-test/pkg/environment" + "knative.dev/reconciler-test/pkg/eventshub" "knative.dev/reconciler-test/pkg/k8s" "knative.dev/reconciler-test/pkg/knative" "knative.dev/reconciler-test/pkg/tracing" @@ -230,3 +231,19 @@ func TestBrokerDeliverLongResponseMessage(t *testing.T) { env.TestSet(ctx, t, broker.BrokerDeliverLongResponseMessage()) } + +func TestMTChannelBrokerRotateTLSCertificates(t *testing.T) { + t.Parallel() + + ctx, env := global.Environment( + knative.WithKnativeNamespace(system.Namespace()), + knative.WithLoggingConfig, + knative.WithTracingConfig, + k8s.WithEventListener, + environment.Managed(t), + eventshub.WithTLS(t), + environment.WithPollTimings(5*time.Second, 4*time.Minute), + ) + + env.Test(ctx, t, broker.RotateMTChannelBrokerTLSCertificates()) +} diff --git a/test/rekt/channel_test.go b/test/rekt/channel_test.go index 96ccfdb875d..9eca8d9771c 100644 --- a/test/rekt/channel_test.go +++ b/test/rekt/channel_test.go @@ -21,12 +21,14 @@ package rekt import ( "testing" + "time" "github.com/cloudevents/sdk-go/v2/binding" duckv1 "knative.dev/pkg/apis/duck/v1" "knative.dev/pkg/system" _ "knative.dev/pkg/system/testing" "knative.dev/reconciler-test/pkg/environment" + "knative.dev/reconciler-test/pkg/eventshub" "knative.dev/reconciler-test/pkg/k8s" "knative.dev/reconciler-test/pkg/knative" "knative.dev/reconciler-test/pkg/manifest" @@ -318,3 +320,19 @@ func TestChannelDeadLetterSinkExtensions(t *testing.T) { env.TestSet(ctx, t, channel.ChannelDeadLetterSinkExtensions(createSubscriberFn)) } + +func TestInMemoryChannelRotateIngressTLSCertificate(t *testing.T) { + t.Parallel() + + ctx, env := global.Environment( + knative.WithKnativeNamespace(system.Namespace()), + knative.WithLoggingConfig, + knative.WithTracingConfig, + k8s.WithEventListener, + environment.Managed(t), + eventshub.WithTLS(t), + environment.WithPollTimings(5*time.Second, 4*time.Minute), + ) + + env.Test(ctx, t, channel.RotateDispatcherTLSCertificate()) +} diff --git a/test/rekt/features/broker/eventing_tls_feature.go b/test/rekt/features/broker/eventing_tls_feature.go new file mode 100644 index 00000000000..a1700550605 --- /dev/null +++ b/test/rekt/features/broker/eventing_tls_feature.go @@ -0,0 +1,105 @@ +/* +Copyright 2023 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 broker + +import ( + "context" + "time" + + cetest "github.com/cloudevents/sdk-go/v2/test" + "github.com/google/uuid" + "k8s.io/apimachinery/pkg/types" + "knative.dev/pkg/system" + "knative.dev/reconciler-test/pkg/eventshub" + "knative.dev/reconciler-test/pkg/eventshub/assert" + "knative.dev/reconciler-test/pkg/feature" + "knative.dev/reconciler-test/pkg/resources/service" + "knative.dev/reconciler-test/resources/certificate" + + "knative.dev/eventing/test/rekt/features/featureflags" + "knative.dev/eventing/test/rekt/resources/addressable" + "knative.dev/eventing/test/rekt/resources/broker" + "knative.dev/eventing/test/rekt/resources/trigger" +) + +func RotateMTChannelBrokerTLSCertificates() *feature.Feature { + ingressCertificateName := "mt-broker-ingress-server-tls" + ingressSecretName := "mt-broker-ingress-server-tls" + + filterCertificateName := "mt-broker-filter-server-tls" + + brokerName := feature.MakeRandomK8sName("broker") + triggerName := feature.MakeRandomK8sName("trigger") + sink := feature.MakeRandomK8sName("sink") + source := feature.MakeRandomK8sName("source") + + f := feature.NewFeatureNamed("Rotate MTChannelBroker TLS certificate") + + f.Prerequisite("transport encryption is strict", featureflags.TransportEncryptionStrict()) + f.Prerequisite("should not run when Istio is enabled", featureflags.IstioDisabled()) + + f.Setup("Rotate ingress certificate", certificate.Rotate(certificate.RotateCertificate{ + Certificate: types.NamespacedName{ + Namespace: system.Namespace(), + Name: ingressCertificateName, + }, + })) + // We cannot externally verify this certificate rotation + f.Setup("Rotate filter certificate", certificate.Rotate(certificate.RotateCertificate{ + Certificate: types.NamespacedName{ + Namespace: system.Namespace(), + Name: filterCertificateName, + }, + })) + + f.Setup("install sink", eventshub.Install(sink, eventshub.StartReceiverTLS)) + f.Setup("install broker", broker.Install(brokerName, broker.WithEnvConfig()...)) + f.Setup("Broker is ready", broker.IsReady(brokerName)) + f.Setup("install trigger", func(ctx context.Context, t feature.T) { + d := service.AsDestinationRef(sink) + d.CACerts = eventshub.GetCaCerts(ctx) + trigger.Install(triggerName, brokerName, trigger.WithSubscriberFromDestination(d))(ctx, t) + }) + f.Setup("trigger is ready", trigger.IsReady(triggerName)) + f.Setup("Broker has HTTPS address", broker.ValidateAddress(brokerName, addressable.AssertHTTPSAddress)) + + event := cetest.FullEvent() + event.SetID(uuid.New().String()) + + f.Requirement("install source", eventshub.Install(source, + eventshub.StartSenderToResourceTLS(broker.GVR(), brokerName, nil), + eventshub.InputEvent(event), + // Send multiple events so that we take into account that the certificate rotation might + // be detected by the server after some time. + eventshub.SendMultipleEvents(100, 3*time.Second), + )) + + f.Assert("Event sent", assert.OnStore(source). + MatchSentEvent(cetest.HasId(event.ID())). + AtLeast(1), + ) + f.Assert("Event received", assert.OnStore(sink). + MatchReceivedEvent(cetest.HasId(event.ID())). + AtLeast(1), + ) + f.Assert("Source match updated peer certificate", assert.OnStore(source). + MatchPeerCertificatesReceived(assert.MatchPeerCertificatesFromSecret(system.Namespace(), ingressSecretName, "tls.crt")). + AtLeast(1), + ) + + return f +} diff --git a/test/rekt/features/channel/eventing_tls_feature.go b/test/rekt/features/channel/eventing_tls_feature.go new file mode 100644 index 00000000000..1f4268c1473 --- /dev/null +++ b/test/rekt/features/channel/eventing_tls_feature.go @@ -0,0 +1,98 @@ +/* +Copyright 2023 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 channel + +import ( + "context" + "time" + + cetest "github.com/cloudevents/sdk-go/v2/test" + "github.com/google/uuid" + "k8s.io/apimachinery/pkg/types" + "knative.dev/pkg/system" + "knative.dev/reconciler-test/pkg/eventshub" + "knative.dev/reconciler-test/pkg/eventshub/assert" + "knative.dev/reconciler-test/pkg/feature" + "knative.dev/reconciler-test/pkg/resources/service" + "knative.dev/reconciler-test/resources/certificate" + + "knative.dev/eventing/test/rekt/features/featureflags" + "knative.dev/eventing/test/rekt/resources/addressable" + "knative.dev/eventing/test/rekt/resources/channel_impl" + "knative.dev/eventing/test/rekt/resources/subscription" +) + +func RotateDispatcherTLSCertificate() *feature.Feature { + certificateName := "imc-dispatcher-server-tls" + secretName := "imc-dispatcher-server-tls" + + channelName := feature.MakeRandomK8sName("channel") + subscriptionName := feature.MakeRandomK8sName("sub") + sink := feature.MakeRandomK8sName("sink") + source := feature.MakeRandomK8sName("source") + + f := feature.NewFeatureNamed("Rotate " + certificateName + " certificate") + + f.Prerequisite("transport encryption is strict", featureflags.TransportEncryptionStrict()) + f.Prerequisite("should not run when Istio is enabled", featureflags.IstioDisabled()) + + f.Setup("Rotate certificate", certificate.Rotate(certificate.RotateCertificate{ + Certificate: types.NamespacedName{ + Namespace: system.Namespace(), + Name: certificateName, + }, + })) + + f.Setup("install sink", eventshub.Install(sink, eventshub.StartReceiverTLS)) + f.Setup("install channel", channel_impl.Install(channelName)) + f.Setup("channel is ready", channel_impl.IsReady(channelName)) + f.Setup("install subscription", func(ctx context.Context, t feature.T) { + d := service.AsDestinationRef(sink) + d.CACerts = eventshub.GetCaCerts(ctx) + subscription.Install(subscriptionName, + subscription.WithChannel(channel_impl.AsRef(channelName)), + subscription.WithSubscriberFromDestination(d))(ctx, t) + }) + f.Setup("subscription is ready", subscription.IsReady(subscriptionName)) + f.Setup("Channel has HTTPS address", channel_impl.ValidateAddress(channelName, addressable.AssertHTTPSAddress)) + + event := cetest.FullEvent() + event.SetID(uuid.New().String()) + + f.Requirement("install source", eventshub.Install(source, + eventshub.StartSenderToResourceTLS(channel_impl.GVR(), channelName, nil), + eventshub.InputEvent(event), + // Send multiple events so that we take into account that the certificate rotation might + // be detected by the server after some time. + eventshub.SendMultipleEvents(100, 3*time.Second), + )) + + f.Assert("Event sent", assert.OnStore(source). + MatchSentEvent(cetest.HasId(event.ID())). + AtLeast(1), + ) + f.Assert("Event received", assert.OnStore(sink). + MatchReceivedEvent(cetest.HasId(event.ID())). + AtLeast(1), + ) + f.Assert("Source match updated peer certificate", assert.OnStore(source). + MatchPeerCertificatesReceived(assert.MatchPeerCertificatesFromSecret(system.Namespace(), secretName, "tls.crt")). + AtLeast(1), + ) + + return f +} diff --git a/test/rekt/features/featureflags/featureflags.go b/test/rekt/features/featureflags/featureflags.go index 0ba4a2c7c85..8d007d96fea 100644 --- a/test/rekt/features/featureflags/featureflags.go +++ b/test/rekt/features/featureflags/featureflags.go @@ -46,6 +46,20 @@ func TransportEncryptionPermissiveOrStrict() feature.ShouldRun { } } +func TransportEncryptionStrict() feature.ShouldRun { + return func(ctx context.Context, t feature.T) (feature.PrerequisiteResult, error) { + flags, err := getFeatureFlags(ctx, "config-features") + if err != nil { + return feature.PrerequisiteResult{}, err + } + + return feature.PrerequisiteResult{ + ShouldRun: flags.IsStrictTransportEncryption(), + Reason: flags.String(), + }, nil + } +} + func IstioDisabled() feature.ShouldRun { return func(ctx context.Context, t feature.T) (feature.PrerequisiteResult, error) { flags, err := getFeatureFlags(ctx, "config-features") diff --git a/test/rekt/resources/addressable/addressable.go b/test/rekt/resources/addressable/addressable.go index d8c566186bb..df3997e261d 100644 --- a/test/rekt/resources/addressable/addressable.go +++ b/test/rekt/resources/addressable/addressable.go @@ -18,6 +18,7 @@ package addressable import ( "context" + "fmt" "time" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -27,6 +28,8 @@ import ( "knative.dev/reconciler-test/pkg/k8s" ) +type ValidateAddress func(addressable *duckv1.Addressable) error + // Address returns a broker's address. func Address(ctx context.Context, gvr schema.GroupVersionResource, name string, timings ...time.Duration) (*duckv1.Addressable, error) { interval, timeout := k8s.PollTimings(ctx, timings) @@ -51,3 +54,10 @@ func Address(ctx context.Context, gvr schema.GroupVersionResource, name string, }) return addr, err } + +func AssertHTTPSAddress(addr *duckv1.Addressable) error { + if addr.URL.Scheme != "https" { + return fmt.Errorf("address is not HTTPS: %#v", addr) + } + return nil +} diff --git a/test/rekt/resources/broker/broker.go b/test/rekt/resources/broker/broker.go index cdabc65815e..27d7adde922 100644 --- a/test/rekt/resources/broker/broker.go +++ b/test/rekt/resources/broker/broker.go @@ -158,6 +158,21 @@ func IsAddressable(name string, timings ...time.Duration) feature.StepFn { return k8s.IsAddressable(GVR(), name, timings...) } +// ValidateAddress validates the address retured by Address +func ValidateAddress(name string, validate addressable.ValidateAddress, timings ...time.Duration) feature.StepFn { + return func(ctx context.Context, t feature.T) { + addr, err := Address(ctx, name, timings...) + if err != nil { + t.Error(err) + return + } + if err := validate(addr); err != nil { + t.Error(err) + return + } + } +} + // Address returns a broker's address. func Address(ctx context.Context, name string, timings ...time.Duration) (*duckv1.Addressable, error) { return addressable.Address(ctx, GVR(), name, timings...) diff --git a/test/rekt/resources/channel_impl/channel_impl.go b/test/rekt/resources/channel_impl/channel_impl.go index 428df32d6a8..b7a08d2b9b4 100644 --- a/test/rekt/resources/channel_impl/channel_impl.go +++ b/test/rekt/resources/channel_impl/channel_impl.go @@ -172,3 +172,18 @@ func AsDestinationRef(name string) *duckv1.Destination { // WithDeadLetterSink adds the dead letter sink related config to a Subscription spec. var WithDeadLetterSink = delivery.WithDeadLetterSink + +// ValidateAddress validates the address retured by Address +func ValidateAddress(name string, validate addressable.ValidateAddress, timings ...time.Duration) feature.StepFn { + return func(ctx context.Context, t feature.T) { + addr, err := Address(ctx, name, timings...) + if err != nil { + t.Error(err) + return + } + if err := validate(addr); err != nil { + t.Error(err) + return + } + } +} diff --git a/test/rekt/resources/subscription/subscription.go b/test/rekt/resources/subscription/subscription.go index fa9b8e22982..a2b26c34375 100644 --- a/test/rekt/resources/subscription/subscription.go +++ b/test/rekt/resources/subscription/subscription.go @@ -19,14 +19,16 @@ package subscription import ( "context" "embed" + "strings" "time" "k8s.io/apimachinery/pkg/runtime/schema" - "knative.dev/eventing/test/rekt/resources/delivery" duckv1 "knative.dev/pkg/apis/duck/v1" "knative.dev/reconciler-test/pkg/feature" "knative.dev/reconciler-test/pkg/k8s" "knative.dev/reconciler-test/pkg/manifest" + + "knative.dev/eventing/test/rekt/resources/delivery" ) //go:embed *.yaml @@ -128,3 +130,36 @@ func Install(name string, opts ...manifest.CfgFn) feature.StepFn { func IsReady(name string, timing ...time.Duration) feature.StepFn { return k8s.IsReady(gvr(), name, timing...) } + +// WithSubscriberFromDestination adds the subscriber related config to a Trigger spec. +func WithSubscriberFromDestination(dest *duckv1.Destination) manifest.CfgFn { + return func(cfg map[string]interface{}) { + if _, set := cfg["subscriber"]; !set { + cfg["subscriber"] = map[string]interface{}{} + } + subscriber := cfg["subscriber"].(map[string]interface{}) + + uri := dest.URI + ref := dest.Ref + + if dest.CACerts != nil { + // This is a multi-line string and should be indented accordingly. + // Replace "new line" with "new line + spaces". + subscriber["CACerts"] = strings.ReplaceAll(*dest.CACerts, "\n", "\n ") + } + + if uri != nil { + subscriber["uri"] = uri.String() + } + if ref != nil { + if _, set := subscriber["ref"]; !set { + subscriber["ref"] = map[string]interface{}{} + } + sref := subscriber["ref"].(map[string]interface{}) + sref["apiVersion"] = ref.APIVersion + sref["kind"] = ref.Kind + // skip namespace + sref["name"] = ref.Name + } + } +} diff --git a/test/rekt/resources/subscription/subscription.yaml b/test/rekt/resources/subscription/subscription.yaml index 345a3c8a590..82db40579ab 100644 --- a/test/rekt/resources/subscription/subscription.yaml +++ b/test/rekt/resources/subscription/subscription.yaml @@ -36,6 +36,10 @@ spec: {{ if .subscriber.uri }} uri: {{ .subscriber.uri }} {{ end }} + {{ if .subscriber.CACerts }} + CACerts: |- + {{ .subscriber.CACerts }} + {{ end }} {{ end }} {{if .reply }} reply: @@ -53,6 +57,10 @@ spec: {{ if .reply.uri }} uri: {{ .reply.uri }} {{ end }} + {{ if .reply.CACerts }} + CACerts: |- + {{ .reply.CACerts }} + {{ end }} {{ end }} {{ if .delivery }} delivery: @@ -72,6 +80,10 @@ spec: {{ if .delivery.deadLetterSink.uri }} uri: {{ .delivery.deadLetterSink.uri }} {{ end }} + {{ if .delivery.deadLetterSink.CACerts }} + CACerts: |- + {{ .delivery.deadLetterSink.CACerts }} + {{ end }} {{ end }} {{ if .delivery.retry }} retry: {{ .delivery.retry}} diff --git a/third_party/VENDOR-LICENSE/knative.dev/reconciler-test/pkg/LICENSE b/third_party/VENDOR-LICENSE/knative.dev/reconciler-test/LICENSE similarity index 100% rename from third_party/VENDOR-LICENSE/knative.dev/reconciler-test/pkg/LICENSE rename to third_party/VENDOR-LICENSE/knative.dev/reconciler-test/LICENSE diff --git a/vendor/knative.dev/reconciler-test/resources/certificate/certificate.go b/vendor/knative.dev/reconciler-test/resources/certificate/certificate.go new file mode 100644 index 00000000000..e3e80768e15 --- /dev/null +++ b/vendor/knative.dev/reconciler-test/resources/certificate/certificate.go @@ -0,0 +1,218 @@ +/* +Copyright 2023 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 certificate + +import ( + "bytes" + "context" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "knative.dev/pkg/apis" + duckv1 "knative.dev/pkg/apis/duck/v1" + kubeclient "knative.dev/pkg/client/injection/kube/client" + "knative.dev/pkg/injection/clients/dynamicclient" + + "knative.dev/reconciler-test/pkg/feature" +) + +var ( + certificateGVR = schema.GroupVersionResource{ + Group: "cert-manager.io", + Version: "v1", + Resource: "certificates", + } +) + +type RotateCertificate struct { + Certificate types.NamespacedName +} + +// Rotate rotates a cert-manager issued certificate. +// The procedure follows the same process as the cert-manager command `cmctl renew ` +// See also https://cert-manager.io/docs/usage/certificate/#actions-triggering-private-key-rotation +func Rotate(rotate RotateCertificate) feature.StepFn { + return func(ctx context.Context, t feature.T) { + before := getSecret(ctx, t, rotate) + issueRotation(ctx, t, rotate) + waitForRotation(ctx, t, rotate, before) + } + +} + +func issueRotation(ctx context.Context, t feature.T, rotate RotateCertificate) { + var lastErr error + err := wait.PollImmediate(time.Second, time.Minute, func() (bool, error) { + err := rotateCertificate(ctx, rotate) + if err == nil { + return true, nil + } + lastErr = err + + // Retry on conflicts + if apierrors.IsConflict(err) { + return false, nil + } + + return false, err + }) + if err != nil { + t.Fatal(err, lastErr) + } +} + +type Certificate struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec Spec `json:"spec"` + + Status Status `json:"status"` +} + +type Spec struct { + SecretName string `json:"secretName"` +} + +// Status defines the observed state of Certificate +type Status struct { + duckv1.Status `json:",inline"` + // Copied from https://github.com/cert-manager/cert-manager/blob/master/pkg/apis/certmanager/v1/types_certificate.go + LastFailureTime *metav1.Time `json:"lastFailureTime,omitempty"` + NotBefore *metav1.Time `json:"notBefore,omitempty"` + NotAfter *metav1.Time `json:"notAfter,omitempty"` + RenewalTime *metav1.Time `json:"renewalTime,omitempty"` + Revision *int `json:"revision,omitempty"` + NextPrivateKeySecretName *string `json:"nextPrivateKeySecretName,omitempty"` + FailedIssuanceAttempts *int `json:"failedIssuanceAttempts,omitempty"` +} + +func rotateCertificate(ctx context.Context, rotate RotateCertificate) error { + dc := dynamicclient.Get(ctx).Resource(certificateGVR) + + obj, err := dc. + Namespace(rotate.Certificate.Namespace). + Get(ctx, rotate.Certificate.Name, metav1.GetOptions{}) + if err != nil { + return err + } + + cert := &Certificate{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, cert); err != nil { + return err + } + + renewCertificate(cert) + + obj.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(cert) + if err != nil { + return err + } + + _, err = dc. + Namespace(rotate.Certificate.Namespace). + UpdateStatus(ctx, obj, metav1.UpdateOptions{}) + if err != nil { + return err + } + + return nil +} + +func waitForRotation(ctx context.Context, t feature.T, rotate RotateCertificate, before *corev1.Secret) { + keys := []string{"tls.key", "tls.crt"} + err := wait.PollImmediate(time.Second, time.Minute, func() (bool, error) { + current := getSecret(ctx, t, rotate) + for _, key := range keys { + if bytes.Equal(before.Data[key], current.Data[key]) { + t.Logf("Value for key %s is equal", key) + return false, nil + } + } + return true, nil + }) + if err != nil { + t.Errorf("Failed while waiting for Certificate rotation to happen: %v", err) + } +} + +func getSecret(ctx context.Context, t feature.T, rotate RotateCertificate) *corev1.Secret { + obj, err := dynamicclient.Get(ctx).Resource(certificateGVR). + Namespace(rotate.Certificate.Namespace). + Get(ctx, rotate.Certificate.Name, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Failed to get certificate %s/%s: %v", rotate.Certificate.Namespace, rotate.Certificate.Name, err) + } + + cert := &Certificate{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, cert); err != nil { + t.Fatal(err) + } + + secret, err := kubeclient.Get(ctx). + CoreV1(). + Secrets(rotate.Certificate.Namespace). + Get(ctx, cert.Spec.SecretName, metav1.GetOptions{}) + if err != nil { + t.Fatalf("Failed to get secret %s/%s: %v", rotate.Certificate.Namespace, cert.Spec.SecretName, err) + } + + return secret +} + +// Adapted from: +// - https://github.com/cert-manager/cert-manager/blob/843deed22f563dbdcbbf71a9fc478609ee90cb8e/pkg/api/util/conditions.go#L165-L204 +// - https://github.com/cert-manager/cert-manager/blob/843deed22f563dbdcbbf71a9fc478609ee90cb8e/cmd/ctl/pkg/renew/renew.go#L206-L214 +func renewCertificate(c *Certificate) { + + newCondition := apis.Condition{ + Type: apis.ConditionType("Issuing"), + Status: corev1.ConditionTrue, + Reason: "ManuallyTriggered", + Message: "Certificate re-issuance manually triggered", + } + + nowTime := metav1.NewTime(time.Now()) + newCondition.LastTransitionTime = apis.VolatileTime{Inner: nowTime} + + // Search through existing conditions + for idx, cond := range c.Status.GetConditions() { + // Skip unrelated conditions + if cond.Type != newCondition.Type { + continue + } + + // If this update doesn't contain a state transition, we don't update + // the conditions LastTransitionTime to Now() + if cond.Status == newCondition.Status { + newCondition.LastTransitionTime = cond.LastTransitionTime + } + + // Overwrite the existing condition + c.Status.Conditions[idx] = newCondition + return + } + + c.Status.SetConditions(append(c.Status.GetConditions(), newCondition)) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index fdcf95ce077..6213b5f6409 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1348,6 +1348,7 @@ knative.dev/reconciler-test/pkg/resources/secret knative.dev/reconciler-test/pkg/resources/service knative.dev/reconciler-test/pkg/state knative.dev/reconciler-test/pkg/tracing +knative.dev/reconciler-test/resources/certificate # sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 ## explicit; go 1.18 sigs.k8s.io/json