package actionsgithubcom import ( "context" "crypto/tls" "encoding/base64" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "time" "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1" "github.com/actions/actions-runner-controller/github/actions" "github.com/go-logr/logr" "github.com/actions/actions-runner-controller/github/actions/fake" "github.com/actions/actions-runner-controller/github/actions/testserver" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" ) const ( timeout = time.Second * 10 interval = time.Millisecond * 250 runnerImage = "ghcr.io/actions/actions-runner:latest" ) func newExampleRunner(name, namespace, configSecretName string) *v1alpha1.EphemeralRunner { return &v1alpha1.EphemeralRunner{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, Spec: v1alpha1.EphemeralRunnerSpec{ GitHubConfigUrl: "https://github.com/owner/repo", GitHubConfigSecret: configSecretName, RunnerScaleSetId: 1, PodTemplateSpec: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: EphemeralRunnerContainerName, Image: runnerImage, Command: []string{"/runner/run.sh"}, VolumeMounts: []corev1.VolumeMount{ { Name: "runner", MountPath: "/runner", }, }, }, }, InitContainers: []corev1.Container{ { Name: "setup", Image: runnerImage, Command: []string{"sh", "-c", "cp -r /home/runner/* /runner/"}, VolumeMounts: []corev1.VolumeMount{ { Name: "runner", MountPath: "/runner", }, }, }, }, Volumes: []corev1.Volume{ { Name: "runner", VolumeSource: corev1.VolumeSource{ EmptyDir: &corev1.EmptyDirVolumeSource{}, }, }, }, }, }, }, } } var _ = Describe("EphemeralRunner", func() { Describe("Resource manipulation", func() { var ctx context.Context var mgr ctrl.Manager var autoscalingNS *corev1.Namespace var configSecret *corev1.Secret var controller *EphemeralRunnerReconciler var ephemeralRunner *v1alpha1.EphemeralRunner BeforeEach(func() { ctx = context.Background() autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name) controller = &EphemeralRunnerReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Log: logf.Log, ActionsClient: fake.NewMultiClient(), } err := controller.SetupWithManager(mgr) Expect(err).To(BeNil(), "failed to setup controller") ephemeralRunner = newExampleRunner("test-runner", autoscalingNS.Name, configSecret.Name) err = k8sClient.Create(ctx, ephemeralRunner) Expect(err).To(BeNil(), "failed to create ephemeral runner") startManagers(GinkgoT(), mgr) }) It("It should create/add all required resources for EphemeralRunner (finalizer, jit secret)", func() { created := new(v1alpha1.EphemeralRunner) // Check if finalizer is added Eventually( func() ([]string, error) { err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, created) if err != nil { return nil, err } if len(created.Finalizers) == 0 { return nil, nil } n := len(created.Finalizers) // avoid capacity mismatch return created.Finalizers[:n:n], nil }, timeout, interval, ).Should(BeEquivalentTo([]string{ephemeralRunnerActionsFinalizerName, ephemeralRunnerFinalizerName})) Eventually( func() (bool, error) { secret := new(corev1.Secret) if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, secret); err != nil { return false, err } _, ok := secret.Data[jitTokenKey] return ok, nil }, timeout, interval, ).Should(BeEquivalentTo(true)) Eventually( func() (string, error) { pod := new(corev1.Pod) if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil { return "", err } return pod.Name, nil }, timeout, interval, ).Should(BeEquivalentTo(ephemeralRunner.Name)) }) It("It should re-create pod on failure", func() { pod := new(corev1.Pod) Eventually(func() (bool, error) { if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil { return false, err } return true, nil }).Should(BeEquivalentTo(true)) err := k8sClient.Delete(ctx, pod) Expect(err).To(BeNil(), "failed to delete pod") pod = new(corev1.Pod) Eventually(func() (bool, error) { if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil { return false, err } return true, nil }, timeout, interval, ).Should(BeEquivalentTo(true)) }) It("It should clean up resources when deleted", func() { // wait for pod to be created pod := new(corev1.Pod) Eventually(func() (bool, error) { if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil { return false, err } return true, nil }).Should(BeEquivalentTo(true)) // create runner-linked pod runnerLinkedPod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "test-runner-linked-pod", Namespace: ephemeralRunner.Namespace, Labels: map[string]string{ "runner-pod": ephemeralRunner.Name, }, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "runner-linked-container", Image: "ubuntu:latest", }, }, }, } err := k8sClient.Create(ctx, runnerLinkedPod) Expect(err).To(BeNil(), "failed to create runner linked pod") Eventually( func() (bool, error) { pod := new(corev1.Pod) if err := k8sClient.Get(ctx, client.ObjectKey{Name: runnerLinkedPod.Name, Namespace: runnerLinkedPod.Namespace}, pod); err != nil { return false, nil } return true, nil }, timeout, interval, ).Should(BeEquivalentTo(true)) // create runner linked secret runnerLinkedSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "test-runner-linked-secret", Namespace: ephemeralRunner.Namespace, Labels: map[string]string{ "runner-pod": ephemeralRunner.Name, }, }, Data: map[string][]byte{"test": []byte("test")}, } err = k8sClient.Create(ctx, runnerLinkedSecret) Expect(err).To(BeNil(), "failed to create runner linked secret") Eventually( func() (bool, error) { secret := new(corev1.Secret) if err := k8sClient.Get(ctx, client.ObjectKey{Name: runnerLinkedSecret.Name, Namespace: runnerLinkedSecret.Namespace}, secret); err != nil { return false, nil } return true, nil }, timeout, interval, ).Should(BeEquivalentTo(true)) err = k8sClient.Delete(ctx, ephemeralRunner) Expect(err).To(BeNil(), "failed to delete ephemeral runner") Eventually( func() (bool, error) { pod := new(corev1.Pod) err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod) if err == nil { return false, nil } return kerrors.IsNotFound(err), nil }, timeout, interval, ).Should(BeEquivalentTo(true)) Eventually( func() (bool, error) { secret := new(corev1.Secret) err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, secret) if err == nil { return false, nil } return kerrors.IsNotFound(err), nil }, timeout, interval, ).Should(BeEquivalentTo(true)) Eventually( func() (bool, error) { pod := new(corev1.Pod) err = k8sClient.Get(ctx, client.ObjectKey{Name: runnerLinkedPod.Name, Namespace: runnerLinkedPod.Namespace}, pod) if err == nil { return false, nil } return kerrors.IsNotFound(err), nil }, timeout, interval, ).Should(BeEquivalentTo(true)) Eventually( func() (bool, error) { secret := new(corev1.Secret) err = k8sClient.Get(ctx, client.ObjectKey{Name: runnerLinkedSecret.Name, Namespace: runnerLinkedSecret.Namespace}, secret) if err == nil { return false, nil } return kerrors.IsNotFound(err), nil }, timeout, interval, ).Should(BeEquivalentTo(true)) Eventually( func() (bool, error) { updated := new(v1alpha1.EphemeralRunner) err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated) if err == nil { return false, nil } return kerrors.IsNotFound(err), nil }, timeout, interval, ).Should(BeEquivalentTo(true)) }) It("It should eventually have runner id set", func() { Eventually( func() (int, error) { updatedEphemeralRunner := new(v1alpha1.EphemeralRunner) err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updatedEphemeralRunner) if err != nil { return 0, err } return updatedEphemeralRunner.Status.RunnerId, nil }, timeout, interval, ).Should(BeNumerically(">", 0)) }) It("It should patch the ephemeral runner non terminating status", func() { pod := new(corev1.Pod) Eventually( func() (bool, error) { err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod) if err != nil { return false, err } return true, nil }, timeout, interval, ).Should(BeEquivalentTo(true)) for _, phase := range []corev1.PodPhase{corev1.PodRunning, corev1.PodPending} { podCopy := pod.DeepCopy() pod.Status.Phase = phase // set container state to force status update pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{ Name: EphemeralRunnerContainerName, State: corev1.ContainerState{}, }) err := k8sClient.Status().Patch(ctx, pod, client.MergeFrom(podCopy)) Expect(err).To(BeNil(), "failed to patch pod status") Eventually( func() (corev1.PodPhase, error) { updated := new(v1alpha1.EphemeralRunner) err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated) if err != nil { return "", err } return updated.Status.Phase, nil }, timeout, interval, ).Should(BeEquivalentTo(phase)) } }) It("It should not update phase if container state does not exist", func() { pod := new(corev1.Pod) Eventually( func() (bool, error) { err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod) if err != nil { return false, err } return true, nil }, timeout, interval, ).Should(BeEquivalentTo(true)) pod.Status.Phase = corev1.PodRunning err := k8sClient.Status().Update(ctx, pod) Expect(err).To(BeNil(), "failed to patch pod status") Consistently( func() (corev1.PodPhase, error) { updated := new(v1alpha1.EphemeralRunner) if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated); err != nil { return corev1.PodUnknown, err } return updated.Status.Phase, nil }, timeout, ).Should(BeEquivalentTo("")) }) It("It should not re-create pod indefinitely", func() { updated := new(v1alpha1.EphemeralRunner) pod := new(corev1.Pod) Eventually( func() (bool, error) { err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated) if err != nil { return false, err } err = k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod) if err != nil { if kerrors.IsNotFound(err) && len(updated.Status.Failures) > 5 { return true, nil } return false, err } pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{ Name: EphemeralRunnerContainerName, State: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{ ExitCode: 1, }, }, }) err = k8sClient.Status().Update(ctx, pod) Expect(err).To(BeNil(), "Failed to update pod status") return false, fmt.Errorf("pod haven't failed for 5 times.") }, timeout, interval, ).Should(BeEquivalentTo(true), "we should stop creating pod after 5 failures") // In case we still have pod created due to controller-runtime cache delay, mark the container as exited err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod) if err == nil { pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{ Name: EphemeralRunnerContainerName, State: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{ ExitCode: 1, }, }, }) err := k8sClient.Status().Update(ctx, pod) Expect(err).To(BeNil(), "Failed to update pod status") } // EphemeralRunner should failed with reason TooManyPodFailures Eventually(func() (string, error) { err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated) if err != nil { return "", err } return updated.Status.Reason, nil }, timeout, interval).Should(BeEquivalentTo("TooManyPodFailures"), "Reason should be TooManyPodFailures") // EphemeralRunner should not have any pod Eventually(func() (bool, error) { err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod) if err == nil { return false, nil } return kerrors.IsNotFound(err), nil }, timeout, interval).Should(BeEquivalentTo(true)) }) It("It should re-create pod on eviction", func() { pod := new(corev1.Pod) Eventually( func() (bool, error) { err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod) if err != nil { return false, err } return true, nil }, timeout, interval, ).Should(BeEquivalentTo(true)) pod.Status.Phase = corev1.PodFailed pod.Status.Reason = "Evicted" pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{ Name: EphemeralRunnerContainerName, State: corev1.ContainerState{}, }) err := k8sClient.Status().Update(ctx, pod) Expect(err).To(BeNil(), "failed to patch pod status") updated := new(v1alpha1.EphemeralRunner) Eventually(func() (bool, error) { err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated) if err != nil { return false, err } return len(updated.Status.Failures) == 1, nil }, timeout, interval).Should(BeEquivalentTo(true)) // should re-create after failure Eventually( func() (bool, error) { pod := new(corev1.Pod) if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil { return false, err } return true, nil }, timeout, interval, ).Should(BeEquivalentTo(true)) }) It("It should re-create pod on exit status 0, but runner exists within the service", func() { pod := new(corev1.Pod) Eventually( func() (bool, error) { if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil { return false, err } return true, nil }, timeout, interval, ).Should(BeEquivalentTo(true)) pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{ Name: EphemeralRunnerContainerName, State: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{ ExitCode: 0, }, }, }) err := k8sClient.Status().Update(ctx, pod) Expect(err).To(BeNil(), "failed to update pod status") updated := new(v1alpha1.EphemeralRunner) Eventually(func() (bool, error) { err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated) if err != nil { return false, err } return len(updated.Status.Failures) == 1, nil }, timeout, interval).Should(BeEquivalentTo(true)) // should re-create after failure Eventually( func() (bool, error) { pod := new(corev1.Pod) if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil { return false, err } return true, nil }, timeout, interval, ).Should(BeEquivalentTo(true)) }) It("It should not set the phase to succeeded without pod termination status", func() { pod := new(corev1.Pod) Eventually( func() (bool, error) { if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil { return false, err } return true, nil }, timeout, interval, ).Should(BeEquivalentTo(true)) // first set phase to running pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{ Name: EphemeralRunnerContainerName, State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.Now(), }, }, }) pod.Status.Phase = corev1.PodRunning err := k8sClient.Status().Update(ctx, pod) Expect(err).To(BeNil()) Eventually( func() (corev1.PodPhase, error) { updated := new(v1alpha1.EphemeralRunner) if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated); err != nil { return "", err } return updated.Status.Phase, nil }, timeout, interval, ).Should(BeEquivalentTo(corev1.PodRunning)) // set phase to succeeded pod.Status.Phase = corev1.PodSucceeded err = k8sClient.Status().Update(ctx, pod) Expect(err).To(BeNil()) Consistently( func() (corev1.PodPhase, error) { updated := new(v1alpha1.EphemeralRunner) if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated); err != nil { return "", err } return updated.Status.Phase, nil }, timeout, ).Should(BeEquivalentTo(corev1.PodRunning)) }) }) Describe("Checking the API", func() { var ctx context.Context var autoscalingNS *corev1.Namespace var configSecret *corev1.Secret var controller *EphemeralRunnerReconciler var mgr ctrl.Manager BeforeEach(func() { ctx = context.Background() autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name) controller = &EphemeralRunnerReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Log: logf.Log, ActionsClient: fake.NewMultiClient( fake.WithDefaultClient( fake.NewFakeClient( fake.WithGetRunner( nil, &actions.ActionsError{ StatusCode: http.StatusNotFound, ExceptionName: "AgentNotFoundException", }, ), ), nil, ), ), } err := controller.SetupWithManager(mgr) Expect(err).To(BeNil(), "failed to setup controller") startManagers(GinkgoT(), mgr) }) It("It should set the Phase to Succeeded", func() { ephemeralRunner := newExampleRunner("test-runner", autoscalingNS.Name, configSecret.Name) err := k8sClient.Create(ctx, ephemeralRunner) Expect(err).To(BeNil()) pod := new(corev1.Pod) Eventually(func() (bool, error) { if err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod); err != nil { return false, err } return true, nil }, timeout, interval).Should(BeEquivalentTo(true)) pod.Status.ContainerStatuses = append(pod.Status.ContainerStatuses, corev1.ContainerStatus{ Name: EphemeralRunnerContainerName, State: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{ ExitCode: 0, }, }, }) err = k8sClient.Status().Update(ctx, pod) Expect(err).To(BeNil(), "failed to update pod status") updated := new(v1alpha1.EphemeralRunner) Eventually(func() (corev1.PodPhase, error) { err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, updated) if err != nil { return "", nil } return updated.Status.Phase, nil }, timeout, interval).Should(BeEquivalentTo(corev1.PodSucceeded)) }) }) Describe("Pod proxy config", func() { var ctx context.Context var mgr ctrl.Manager var autoScalingNS *corev1.Namespace var configSecret *corev1.Secret var controller *EphemeralRunnerReconciler BeforeEach(func() { ctx = context.Background() autoScalingNS, mgr = createNamespace(GinkgoT(), k8sClient) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoScalingNS.Name) controller = &EphemeralRunnerReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Log: logf.Log, ActionsClient: fake.NewMultiClient(), } err := controller.SetupWithManager(mgr) Expect(err).To(BeNil(), "failed to setup controller") startManagers(GinkgoT(), mgr) }) It("uses an actions client with proxy transport", func() { // Use an actual client controller.ActionsClient = actions.NewMultiClient(logr.Discard()) proxySuccessfulllyCalled := false proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { header := r.Header.Get("Proxy-Authorization") Expect(header).NotTo(BeEmpty()) header = strings.TrimPrefix(header, "Basic ") decoded, err := base64.StdEncoding.DecodeString(header) Expect(err).NotTo(HaveOccurred()) Expect(string(decoded)).To(Equal("test:password")) proxySuccessfulllyCalled = true w.WriteHeader(http.StatusOK) })) GinkgoT().Cleanup(func() { proxy.Close() }) secretCredentials := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "proxy-credentials", Namespace: autoScalingNS.Name, }, Data: map[string][]byte{ "username": []byte("test"), "password": []byte("password"), }, } err := k8sClient.Create(ctx, secretCredentials) Expect(err).NotTo(HaveOccurred(), "failed to create secret credentials") ephemeralRunner := newExampleRunner("test-runner", autoScalingNS.Name, configSecret.Name) ephemeralRunner.Spec.GitHubConfigUrl = "http://example.com/org/repo" ephemeralRunner.Spec.Proxy = &v1alpha1.ProxyConfig{ HTTP: &v1alpha1.ProxyServerConfig{ Url: proxy.URL, CredentialSecretRef: "proxy-credentials", }, } err = k8sClient.Create(ctx, ephemeralRunner) Expect(err).To(BeNil(), "failed to create ephemeral runner") Eventually( func() bool { return proxySuccessfulllyCalled }, 2*time.Second, interval, ).Should(BeEquivalentTo(true)) }) It("It should create EphemeralRunner with proxy environment variables using ProxySecretRef", func() { ephemeralRunner := newExampleRunner("test-runner", autoScalingNS.Name, configSecret.Name) ephemeralRunner.Spec.Proxy = &v1alpha1.ProxyConfig{ HTTP: &v1alpha1.ProxyServerConfig{ Url: "http://proxy.example.com:8080", }, HTTPS: &v1alpha1.ProxyServerConfig{ Url: "http://proxy.example.com:8080", }, NoProxy: []string{"example.com"}, } ephemeralRunner.Spec.ProxySecretRef = "proxy-secret" err := k8sClient.Create(ctx, ephemeralRunner) Expect(err).To(BeNil(), "failed to create ephemeral runner") pod := new(corev1.Pod) Eventually( func(g Gomega) { err := k8sClient.Get(ctx, client.ObjectKey{Name: ephemeralRunner.Name, Namespace: ephemeralRunner.Namespace}, pod) g.Expect(err).To(BeNil(), "failed to get ephemeral runner pod") }, timeout, interval, ).Should(Succeed(), "failed to get ephemeral runner pod") Expect(pod.Spec.Containers[0].Env).To(ContainElement(corev1.EnvVar{ Name: EnvVarHTTPProxy, ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ Name: ephemeralRunner.Spec.ProxySecretRef, }, Key: "http_proxy", }, }, })) Expect(pod.Spec.Containers[0].Env).To(ContainElement(corev1.EnvVar{ Name: EnvVarHTTPSProxy, ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ Name: ephemeralRunner.Spec.ProxySecretRef, }, Key: "https_proxy", }, }, })) Expect(pod.Spec.Containers[0].Env).To(ContainElement(corev1.EnvVar{ Name: EnvVarNoProxy, ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ Name: ephemeralRunner.Spec.ProxySecretRef, }, Key: "no_proxy", }, }, })) }) }) Describe("TLS config", func() { var ctx context.Context var mgr ctrl.Manager var autoScalingNS *corev1.Namespace var configSecret *corev1.Secret var controller *EphemeralRunnerReconciler var rootCAConfigMap *corev1.ConfigMap BeforeEach(func() { ctx = context.Background() autoScalingNS, mgr = createNamespace(GinkgoT(), k8sClient) configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoScalingNS.Name) cert, err := os.ReadFile(filepath.Join( "../../", "github", "actions", "testdata", "rootCA.crt", )) Expect(err).NotTo(HaveOccurred(), "failed to read root CA cert") rootCAConfigMap = &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "root-ca-configmap", Namespace: autoScalingNS.Name, }, Data: map[string]string{ "rootCA.crt": string(cert), }, } err = k8sClient.Create(ctx, rootCAConfigMap) Expect(err).NotTo(HaveOccurred(), "failed to create configmap with root CAs") controller = &EphemeralRunnerReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Log: logf.Log, ActionsClient: fake.NewMultiClient(), } err = controller.SetupWithManager(mgr) Expect(err).To(BeNil(), "failed to setup controller") startManagers(GinkgoT(), mgr) }) It("should be able to make requests to a server using root CAs", func() { certsFolder := filepath.Join( "../../", "github", "actions", "testdata", ) certPath := filepath.Join(certsFolder, "server.crt") keyPath := filepath.Join(certsFolder, "server.key") serverSuccessfullyCalled := false server := testserver.NewUnstarted(GinkgoT(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { serverSuccessfullyCalled = true w.WriteHeader(http.StatusOK) })) cert, err := tls.LoadX509KeyPair(certPath, keyPath) Expect(err).NotTo(HaveOccurred(), "failed to load server cert") server.TLS = &tls.Config{Certificates: []tls.Certificate{cert}} server.StartTLS() // Use an actual client controller.ActionsClient = actions.NewMultiClient(logr.Discard()) ephemeralRunner := newExampleRunner("test-runner", autoScalingNS.Name, configSecret.Name) ephemeralRunner.Spec.GitHubConfigUrl = server.ConfigURLForOrg("my-org") ephemeralRunner.Spec.GitHubServerTLS = &v1alpha1.GitHubServerTLSConfig{ CertificateFrom: &v1alpha1.TLSCertificateSource{ ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ Name: rootCAConfigMap.Name, }, Key: "rootCA.crt", }, }, } err = k8sClient.Create(ctx, ephemeralRunner) Expect(err).To(BeNil(), "failed to create ephemeral runner") Eventually( func() bool { return serverSuccessfullyCalled }, 2*time.Second, interval, ).Should(BeTrue(), "failed to contact server") }) }) })