// Copyright 2024-2025 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package integration import ( "context" "crypto/tls" "strconv" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" "go.pinniped.dev/internal/config/concierge" "go.pinniped.dev/internal/config/supervisor" "go.pinniped.dev/test/testlib" ) type stringEditorFunc func(t *testing.T, in string) string func performLimitedCiphersTest( t *testing.T, allowedCiphersConfig []string, expectedConfigForSupervisorOIDCEndpoints *tls.Config, expectedConfigForAggregatedAPIEndpoints *tls.Config, ) { env := testEnvForPodShutdownTests(t) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) editSupervisorAllowedCiphersConfig := func(t *testing.T, configMapData string) string { t.Helper() var config supervisor.Config err := yaml.Unmarshal([]byte(configMapData), &config) require.NoError(t, err) require.Empty(t, config.TLS.OneDotTwo.AllowedCiphers) // precondition config.TLS.OneDotTwo.AllowedCiphers = allowedCiphersConfig updatedConfig, err := yaml.Marshal(config) require.NoError(t, err) return string(updatedConfig) } editConciergeAllowedCiphersConfig := func(t *testing.T, configMapData string) string { t.Helper() var config concierge.Config err := yaml.Unmarshal([]byte(configMapData), &config) require.NoError(t, err) require.Empty(t, config.TLS.OneDotTwo.AllowedCiphers) // precondition config.TLS.OneDotTwo.AllowedCiphers = allowedCiphersConfig updatedConfig, err := yaml.Marshal(config) require.NoError(t, err) return string(updatedConfig) } // Update Supervisor's allowed ciphers in its static configmap and restart pods. updateStaticConfigMapAndRestartApp(t, ctx, env.SupervisorNamespace, env.SupervisorAppName+"-static-config", env.SupervisorAppName, false, editSupervisorAllowedCiphersConfig, ) // Update Concierge's allowed ciphers in its static configmap and restart pods. updateStaticConfigMapAndRestartApp(t, ctx, env.ConciergeNamespace, env.ConciergeAppName+"-config", env.ConciergeAppName, true, editConciergeAllowedCiphersConfig, ) // Probe TLS config of Supervisor's OIDC endpoints. expectTLSConfigForServicePort(t, ctx, env.SupervisorAppName+"-nodeport", env.SupervisorNamespace, "10509", expectedConfigForSupervisorOIDCEndpoints, ) // Probe TLS config of Supervisor's aggregated endpoints. expectTLSConfigForServicePort(t, ctx, env.SupervisorAppName+"-api", env.SupervisorNamespace, "10510", expectedConfigForAggregatedAPIEndpoints, ) // Probe TLS config of Concierge's aggregated endpoints. expectTLSConfigForServicePort(t, ctx, env.ConciergeAppName+"-api", env.ConciergeNamespace, "10511", expectedConfigForAggregatedAPIEndpoints, ) } func expectTLSConfigForServicePort( t *testing.T, ctx context.Context, serviceName string, serviceNamespace string, localPortAsStr string, expectedConfig *tls.Config, ) { portAsInt, err := strconv.Atoi(localPortAsStr) require.NoError(t, err) portAsUint := uint16(portAsInt) //nolint:gosec // okay to cast because it will only be legal port numbers startKubectlPortForward(ctx, t, localPortAsStr, "443", serviceName, serviceNamespace) stdout, stderr := testlib.RunNmapSSLEnum(t, "127.0.0.1", portAsUint) require.Empty(t, stderr) expectedNMapOutput := testlib.GetExpectedCiphers(expectedConfig, "server") assert.Contains(t, stdout, expectedNMapOutput, "actual nmap output:\n%s", stdout, "but was expected to contain:\n%s", expectedNMapOutput, ) } func updateStaticConfigMapAndRestartApp( t *testing.T, ctx context.Context, namespace string, staticConfigMapName string, appName string, isConcierge bool, editConfigMapFunc stringEditorFunc, ) { configMapClient := testlib.NewKubernetesClientset(t).CoreV1().ConfigMaps(namespace) staticConfigMap, err := configMapClient.Get(ctx, staticConfigMapName, metav1.GetOptions{}) require.NoError(t, err) originalConfig := staticConfigMap.Data["pinniped.yaml"] require.NotEmpty(t, originalConfig) t.Cleanup(func() { cleanupCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) staticConfigMapForCleanup, err := configMapClient.Get(cleanupCtx, staticConfigMapName, metav1.GetOptions{}) require.NoError(t, err) staticConfigMapForCleanup.Data = make(map[string]string) staticConfigMapForCleanup.Data["pinniped.yaml"] = originalConfig _, err = configMapClient.Update(cleanupCtx, staticConfigMapForCleanup, metav1.UpdateOptions{}) require.NoError(t, err) restartAllPodsOfApp(t, namespace, appName, isConcierge) }) staticConfigMap.Data = make(map[string]string) staticConfigMap.Data["pinniped.yaml"] = editConfigMapFunc(t, originalConfig) _, err = configMapClient.Update(ctx, staticConfigMap, metav1.UpdateOptions{}) require.NoError(t, err) restartAllPodsOfApp(t, namespace, appName, isConcierge) } // restartAllPodsOfApp will immediately scale to 0 and then scale back. func restartAllPodsOfApp( t *testing.T, namespace string, appName string, isConcierge bool, ) { t.Helper() ignorePodsWithNameSubstring := "" if isConcierge { ignorePodsWithNameSubstring = "-kube-cert-agent-" } // Precondition: the app should have some pods running initially. initialPods := getRunningPodsByNamePrefix(t, namespace, appName+"-", ignorePodsWithNameSubstring) require.Greater(t, len(initialPods), 0) // Scale down the deployment's number of replicas to 0, which will shut down all the pods. originalScale := updateDeploymentScale(t, namespace, appName, 0) require.Greater(t, int(originalScale), 0) scaleDeploymentBackToOriginalScale := func() { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() client := testlib.NewKubernetesClientset(t) currentScale, err := client.AppsV1().Deployments(namespace).GetScale(ctx, appName, metav1.GetOptions{}) require.NoError(t, err) if currentScale.Spec.Replicas == originalScale { // Already scaled appropriately. No need to change the scale. return } updateDeploymentScale(t, namespace, appName, originalScale) // Wait for all the new pods to be running and ready. var newPods []corev1.Pod testlib.RequireEventually(t, func(requireEventually *require.Assertions) { newPods = getRunningPodsByNamePrefix(t, namespace, appName+"-", ignorePodsWithNameSubstring) requireEventually.Equal(len(newPods), int(originalScale), "wanted pods to return to original scale") requireEventually.True(allPodsReady(newPods), "wanted all new pods to be ready") }, 2*time.Minute, 200*time.Millisecond) } // Even if the test fails due to the below assertions, still try to scale back to original scale, // to avoid polluting other tests. t.Cleanup(scaleDeploymentBackToOriginalScale) // Now that we have adjusted the scale to 0, the pods should go away. testlib.RequireEventually(t, func(requireEventually *require.Assertions) { newPods := getRunningPodsByNamePrefix(t, namespace, appName+"-", ignorePodsWithNameSubstring) requireEventually.Len(newPods, 0, "wanted zero pods") }, 2*time.Minute, 200*time.Millisecond) // Scale back to original scale immediately. scaleDeploymentBackToOriginalScale() }