mirror of
https://github.com/actions/actions-runner-controller.git
synced 2025-12-11 03:57:01 +00:00
Add support for self-signed CA certificates (#2268)
Co-authored-by: Bassem Dghaidi <568794+Link-@users.noreply.github.com> Co-authored-by: Nikola Jokic <jokicnikola07@gmail.com> Co-authored-by: Tingluo Huang <tingluohuang@github.com>
This commit is contained in:
@@ -423,6 +423,15 @@ func (r *AutoscalingListenerReconciler) createListenerPod(ctx context.Context, a
|
||||
}
|
||||
}
|
||||
|
||||
if autoscalingListener.Spec.GitHubServerTLS != nil {
|
||||
env, err := r.certificateEnvVarForListener(ctx, autoscalingRunnerSet, autoscalingListener)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("failed to create certificate env var for listener: %v", err)
|
||||
}
|
||||
|
||||
envs = append(envs, env)
|
||||
}
|
||||
|
||||
newPod := r.resourceBuilder.newScaleSetListenerPod(autoscalingListener, serviceAccount, secret, envs...)
|
||||
|
||||
if err := ctrl.SetControllerReference(autoscalingListener, newPod, r.Scheme); err != nil {
|
||||
@@ -439,6 +448,47 @@ func (r *AutoscalingListenerReconciler) createListenerPod(ctx context.Context, a
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *AutoscalingListenerReconciler) certificateEnvVarForListener(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet, autoscalingListener *v1alpha1.AutoscalingListener) (corev1.EnvVar, error) {
|
||||
if autoscalingListener.Spec.GitHubServerTLS.CertificateFrom == nil {
|
||||
return corev1.EnvVar{}, fmt.Errorf("githubServerTLS.certificateFrom is not specified")
|
||||
}
|
||||
|
||||
if autoscalingListener.Spec.GitHubServerTLS.CertificateFrom.ConfigMapKeyRef == nil {
|
||||
return corev1.EnvVar{}, fmt.Errorf("githubServerTLS.certificateFrom.configMapKeyRef is not specified")
|
||||
}
|
||||
|
||||
var configmap corev1.ConfigMap
|
||||
err := r.Get(
|
||||
ctx,
|
||||
types.NamespacedName{
|
||||
Namespace: autoscalingRunnerSet.Namespace,
|
||||
Name: autoscalingListener.Spec.GitHubServerTLS.CertificateFrom.ConfigMapKeyRef.Name,
|
||||
},
|
||||
&configmap,
|
||||
)
|
||||
if err != nil {
|
||||
return corev1.EnvVar{}, fmt.Errorf(
|
||||
"failed to get configmap %s: %w",
|
||||
autoscalingListener.Spec.GitHubServerTLS.CertificateFrom.ConfigMapKeyRef.Name,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
certificate, ok := configmap.Data[autoscalingListener.Spec.GitHubServerTLS.CertificateFrom.ConfigMapKeyRef.Key]
|
||||
if !ok {
|
||||
return corev1.EnvVar{}, fmt.Errorf(
|
||||
"key %s is not found in configmap %s",
|
||||
autoscalingListener.Spec.GitHubServerTLS.CertificateFrom.ConfigMapKeyRef.Key,
|
||||
autoscalingListener.Spec.GitHubServerTLS.CertificateFrom.ConfigMapKeyRef.Name,
|
||||
)
|
||||
}
|
||||
|
||||
return corev1.EnvVar{
|
||||
Name: "GITHUB_SERVER_ROOT_CA",
|
||||
Value: certificate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *AutoscalingListenerReconciler) createSecretsForListener(ctx context.Context, autoscalingListener *v1alpha1.AutoscalingListener, secret *corev1.Secret, logger logr.Logger) (ctrl.Result, error) {
|
||||
newListenerSecret := r.resourceBuilder.newScaleSetListenerSecretMirror(autoscalingListener, secret)
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package actionsgithubcom
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -554,3 +556,165 @@ var _ = Describe("Test AutoScalingListener controller with proxy", func() {
|
||||
autoscalingListenerTestInterval).Should(Succeed(), "failed to delete secret with proxy details")
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Test GitHub Server TLS configuration", func() {
|
||||
var ctx context.Context
|
||||
var mgr ctrl.Manager
|
||||
var autoscalingNS *corev1.Namespace
|
||||
var autoscalingRunnerSet *actionsv1alpha1.AutoscalingRunnerSet
|
||||
var configSecret *corev1.Secret
|
||||
var autoscalingListener *actionsv1alpha1.AutoscalingListener
|
||||
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 := &AutoscalingListenerReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
Log: logf.Log,
|
||||
}
|
||||
err = controller.SetupWithManager(mgr)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
||||
|
||||
min := 1
|
||||
max := 10
|
||||
autoscalingRunnerSet = &actionsv1alpha1.AutoscalingRunnerSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-asrs",
|
||||
Namespace: autoscalingNS.Name,
|
||||
},
|
||||
Spec: actionsv1alpha1.AutoscalingRunnerSetSpec{
|
||||
GitHubConfigUrl: "https://github.com/owner/repo",
|
||||
GitHubConfigSecret: configSecret.Name,
|
||||
GitHubServerTLS: &actionsv1alpha1.GitHubServerTLSConfig{
|
||||
CertificateFrom: &actionsv1alpha1.TLSCertificateSource{
|
||||
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: rootCAConfigMap.Name,
|
||||
},
|
||||
Key: "rootCA.crt",
|
||||
},
|
||||
},
|
||||
},
|
||||
MaxRunners: &max,
|
||||
MinRunners: &min,
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "runner",
|
||||
Image: "ghcr.io/actions/runner",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = k8sClient.Create(ctx, autoscalingRunnerSet)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")
|
||||
|
||||
autoscalingListener = &actionsv1alpha1.AutoscalingListener{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-asl",
|
||||
Namespace: autoscalingNS.Name,
|
||||
},
|
||||
Spec: actionsv1alpha1.AutoscalingListenerSpec{
|
||||
GitHubConfigUrl: "https://github.com/owner/repo",
|
||||
GitHubConfigSecret: configSecret.Name,
|
||||
GitHubServerTLS: &actionsv1alpha1.GitHubServerTLSConfig{
|
||||
CertificateFrom: &actionsv1alpha1.TLSCertificateSource{
|
||||
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: rootCAConfigMap.Name,
|
||||
},
|
||||
Key: "rootCA.crt",
|
||||
},
|
||||
},
|
||||
},
|
||||
RunnerScaleSetId: 1,
|
||||
AutoscalingRunnerSetNamespace: autoscalingRunnerSet.Namespace,
|
||||
AutoscalingRunnerSetName: autoscalingRunnerSet.Name,
|
||||
EphemeralRunnerSetName: "test-ers",
|
||||
MaxRunners: 10,
|
||||
MinRunners: 1,
|
||||
Image: "ghcr.io/owner/repo",
|
||||
},
|
||||
}
|
||||
|
||||
err = k8sClient.Create(ctx, autoscalingListener)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingListener")
|
||||
|
||||
startManagers(GinkgoT(), mgr)
|
||||
})
|
||||
|
||||
Context("When creating a new AutoScalingListener", func() {
|
||||
It("It should set the certificates as an environment variable on the pod", func() {
|
||||
pod := new(corev1.Pod)
|
||||
Eventually(
|
||||
func(g Gomega) {
|
||||
err := k8sClient.Get(
|
||||
ctx,
|
||||
client.ObjectKey{
|
||||
Name: autoscalingListener.Name,
|
||||
Namespace: autoscalingListener.Namespace,
|
||||
},
|
||||
pod,
|
||||
)
|
||||
|
||||
g.Expect(err).NotTo(HaveOccurred(), "failed to get pod")
|
||||
g.Expect(pod.Spec.Containers).NotTo(BeEmpty(), "pod should have containers")
|
||||
g.Expect(pod.Spec.Containers[0].Env).NotTo(BeEmpty(), "pod should have env variables")
|
||||
|
||||
var env *corev1.EnvVar
|
||||
for _, e := range pod.Spec.Containers[0].Env {
|
||||
if e.Name == "GITHUB_SERVER_ROOT_CA" {
|
||||
env = &e
|
||||
break
|
||||
}
|
||||
}
|
||||
g.Expect(env).NotTo(BeNil(), "pod should have an env variable named GITHUB_SERVER_ROOT_CA_PATH")
|
||||
|
||||
cert, err := os.ReadFile(filepath.Join(
|
||||
"../../",
|
||||
"github",
|
||||
"actions",
|
||||
"testdata",
|
||||
"rootCA.crt",
|
||||
))
|
||||
g.Expect(err).NotTo(HaveOccurred(), "failed to read rootCA.crt")
|
||||
|
||||
g.Expect(env.Value).To(
|
||||
BeEquivalentTo(string(cert)),
|
||||
"GITHUB_SERVER_ROOT_CA should be the rootCA.crt",
|
||||
)
|
||||
}).
|
||||
WithTimeout(autoscalingRunnerSetTestTimeout).
|
||||
WithPolling(autoscalingListenerTestInterval).
|
||||
Should(Succeed(), "failed to create pod with volume and env variable")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -541,7 +541,23 @@ func (r *AutoscalingRunnerSetReconciler) actionsClientFor(ctx context.Context, a
|
||||
return nil, fmt.Errorf("failed to find GitHub config secret: %w", err)
|
||||
}
|
||||
|
||||
var opts []actions.ClientOption
|
||||
opts, err := r.actionsClientOptionsFor(ctx, autoscalingRunnerSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get actions client options: %w", err)
|
||||
}
|
||||
|
||||
return r.ActionsClient.GetClientFromSecret(
|
||||
ctx,
|
||||
autoscalingRunnerSet.Spec.GitHubConfigUrl,
|
||||
autoscalingRunnerSet.Namespace,
|
||||
configSecret.Data,
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *AutoscalingRunnerSetReconciler) actionsClientOptionsFor(ctx context.Context, autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet) ([]actions.ClientOption, error) {
|
||||
var options []actions.ClientOption
|
||||
|
||||
if autoscalingRunnerSet.Spec.Proxy != nil {
|
||||
proxyFunc, err := autoscalingRunnerSet.Spec.Proxy.ProxyFunc(func(s string) (*corev1.Secret, error) {
|
||||
var secret corev1.Secret
|
||||
@@ -556,16 +572,35 @@ func (r *AutoscalingRunnerSetReconciler) actionsClientFor(ctx context.Context, a
|
||||
return nil, fmt.Errorf("failed to get proxy func: %w", err)
|
||||
}
|
||||
|
||||
opts = append(opts, actions.WithProxy(proxyFunc))
|
||||
options = append(options, actions.WithProxy(proxyFunc))
|
||||
}
|
||||
|
||||
return r.ActionsClient.GetClientFromSecret(
|
||||
ctx,
|
||||
autoscalingRunnerSet.Spec.GitHubConfigUrl,
|
||||
autoscalingRunnerSet.Namespace,
|
||||
configSecret.Data,
|
||||
opts...,
|
||||
)
|
||||
tlsConfig := autoscalingRunnerSet.Spec.GitHubServerTLS
|
||||
if tlsConfig != nil {
|
||||
pool, err := tlsConfig.ToCertPool(func(name, key string) ([]byte, error) {
|
||||
var configmap corev1.ConfigMap
|
||||
err := r.Get(
|
||||
ctx,
|
||||
types.NamespacedName{
|
||||
Namespace: autoscalingRunnerSet.Namespace,
|
||||
Name: name,
|
||||
},
|
||||
&configmap,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get configmap %s: %w", name, err)
|
||||
}
|
||||
|
||||
return []byte(configmap.Data[key]), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tls config: %w", err)
|
||||
}
|
||||
|
||||
options = append(options, actions.WithRootCAs(pool))
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
|
||||
@@ -2,10 +2,13 @@ package actionsgithubcom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -787,4 +790,242 @@ var _ = Describe("Test Client optional configuration", func() {
|
||||
).Should(BeTrue(), "server was not called")
|
||||
})
|
||||
})
|
||||
|
||||
Context("When specifying a configmap for root CAs", func() {
|
||||
var ctx context.Context
|
||||
var mgr ctrl.Manager
|
||||
var autoscalingNS *corev1.Namespace
|
||||
var configSecret *corev1.Secret
|
||||
var rootCAConfigMap *corev1.ConfigMap
|
||||
var controller *AutoscalingRunnerSetReconciler
|
||||
|
||||
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 = &AutoscalingRunnerSetReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
Log: logf.Log,
|
||||
ControllerNamespace: autoscalingNS.Name,
|
||||
DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
|
||||
ActionsClient: fake.NewMultiClient(),
|
||||
}
|
||||
err = controller.SetupWithManager(mgr)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to setup controller")
|
||||
|
||||
startManagers(GinkgoT(), mgr)
|
||||
})
|
||||
|
||||
It("should be able to make requests to a server using root CAs", func() {
|
||||
controller.ActionsClient = actions.NewMultiClient("test", logr.Discard())
|
||||
|
||||
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()
|
||||
|
||||
min := 1
|
||||
max := 10
|
||||
autoscalingRunnerSet := &v1alpha1.AutoscalingRunnerSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-asrs",
|
||||
Namespace: autoscalingNS.Name,
|
||||
},
|
||||
Spec: v1alpha1.AutoscalingRunnerSetSpec{
|
||||
GitHubConfigUrl: server.ConfigURLForOrg("my-org"),
|
||||
GitHubConfigSecret: configSecret.Name,
|
||||
GitHubServerTLS: &v1alpha1.GitHubServerTLSConfig{
|
||||
CertificateFrom: &v1alpha1.TLSCertificateSource{
|
||||
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: rootCAConfigMap.Name,
|
||||
},
|
||||
Key: "rootCA.crt",
|
||||
},
|
||||
},
|
||||
},
|
||||
MaxRunners: &max,
|
||||
MinRunners: &min,
|
||||
RunnerGroup: "testgroup",
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "runner",
|
||||
Image: "ghcr.io/actions/runner",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = k8sClient.Create(ctx, autoscalingRunnerSet)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")
|
||||
|
||||
// wait for server to be called
|
||||
Eventually(
|
||||
func() (bool, error) {
|
||||
return serverSuccessfullyCalled, nil
|
||||
},
|
||||
autoscalingRunnerSetTestTimeout,
|
||||
1*time.Nanosecond,
|
||||
).Should(BeTrue(), "server was not called")
|
||||
})
|
||||
|
||||
It("it creates a listener referencing the right configmap for TLS", func() {
|
||||
min := 1
|
||||
max := 10
|
||||
autoscalingRunnerSet := &v1alpha1.AutoscalingRunnerSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-asrs",
|
||||
Namespace: autoscalingNS.Name,
|
||||
},
|
||||
Spec: v1alpha1.AutoscalingRunnerSetSpec{
|
||||
GitHubConfigUrl: "https://github.com/owner/repo",
|
||||
GitHubConfigSecret: configSecret.Name,
|
||||
GitHubServerTLS: &v1alpha1.GitHubServerTLSConfig{
|
||||
CertificateFrom: &v1alpha1.TLSCertificateSource{
|
||||
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: rootCAConfigMap.Name,
|
||||
},
|
||||
Key: "rootCA.crt",
|
||||
},
|
||||
},
|
||||
},
|
||||
MaxRunners: &max,
|
||||
MinRunners: &min,
|
||||
RunnerGroup: "testgroup",
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "runner",
|
||||
Image: "ghcr.io/actions/runner",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := k8sClient.Create(ctx, autoscalingRunnerSet)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")
|
||||
|
||||
Eventually(
|
||||
func(g Gomega) {
|
||||
listener := new(v1alpha1.AutoscalingListener)
|
||||
err := k8sClient.Get(
|
||||
ctx,
|
||||
client.ObjectKey{
|
||||
Name: scaleSetListenerName(autoscalingRunnerSet),
|
||||
Namespace: autoscalingRunnerSet.Namespace,
|
||||
},
|
||||
listener,
|
||||
)
|
||||
g.Expect(err).NotTo(HaveOccurred(), "failed to get listener")
|
||||
|
||||
g.Expect(listener.Spec.GitHubServerTLS).NotTo(BeNil(), "listener does not have TLS config")
|
||||
g.Expect(listener.Spec.GitHubServerTLS).To(BeEquivalentTo(autoscalingRunnerSet.Spec.GitHubServerTLS), "listener does not have TLS config")
|
||||
},
|
||||
autoscalingRunnerSetTestTimeout,
|
||||
autoscalingListenerTestInterval,
|
||||
).Should(Succeed(), "tls config is incorrect")
|
||||
})
|
||||
|
||||
It("it creates an ephemeral runner set referencing the right configmap for TLS", func() {
|
||||
min := 1
|
||||
max := 10
|
||||
autoscalingRunnerSet := &v1alpha1.AutoscalingRunnerSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-asrs",
|
||||
Namespace: autoscalingNS.Name,
|
||||
},
|
||||
Spec: v1alpha1.AutoscalingRunnerSetSpec{
|
||||
GitHubConfigUrl: "https://github.com/owner/repo",
|
||||
GitHubConfigSecret: configSecret.Name,
|
||||
GitHubServerTLS: &v1alpha1.GitHubServerTLSConfig{
|
||||
CertificateFrom: &v1alpha1.TLSCertificateSource{
|
||||
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: rootCAConfigMap.Name,
|
||||
},
|
||||
Key: "rootCA.crt",
|
||||
},
|
||||
},
|
||||
},
|
||||
MaxRunners: &max,
|
||||
MinRunners: &min,
|
||||
RunnerGroup: "testgroup",
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "runner",
|
||||
Image: "ghcr.io/actions/runner",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := k8sClient.Create(ctx, autoscalingRunnerSet)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")
|
||||
|
||||
Eventually(
|
||||
func(g Gomega) {
|
||||
runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
|
||||
err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
|
||||
g.Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunnerSet")
|
||||
g.Expect(runnerSetList.Items).To(HaveLen(1), "expected 1 EphemeralRunnerSet to be created")
|
||||
|
||||
runnerSet := &runnerSetList.Items[0]
|
||||
|
||||
g.Expect(runnerSet.Spec.EphemeralRunnerSpec.GitHubServerTLS).NotTo(BeNil(), "expected EphemeralRunnerSpec.GitHubServerTLS to be set")
|
||||
g.Expect(runnerSet.Spec.EphemeralRunnerSpec.GitHubServerTLS).To(BeEquivalentTo(autoscalingRunnerSet.Spec.GitHubServerTLS), "EphemeralRunnerSpec does not have TLS config")
|
||||
},
|
||||
autoscalingRunnerSetTestTimeout,
|
||||
autoscalingListenerTestInterval,
|
||||
).Should(Succeed())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -680,6 +680,21 @@ func (r *EphemeralRunnerReconciler) actionsClientFor(ctx context.Context, runner
|
||||
return nil, fmt.Errorf("failed to get secret: %w", err)
|
||||
}
|
||||
|
||||
opts, err := r.actionsClientOptionsFor(ctx, runner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get actions client options: %w", err)
|
||||
}
|
||||
|
||||
return r.ActionsClient.GetClientFromSecret(
|
||||
ctx,
|
||||
runner.Spec.GitHubConfigUrl,
|
||||
runner.Namespace,
|
||||
secret.Data,
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *EphemeralRunnerReconciler) actionsClientOptionsFor(ctx context.Context, runner *v1alpha1.EphemeralRunner) ([]actions.ClientOption, error) {
|
||||
var opts []actions.ClientOption
|
||||
if runner.Spec.Proxy != nil {
|
||||
proxyFunc, err := runner.Spec.Proxy.ProxyFunc(func(s string) (*corev1.Secret, error) {
|
||||
@@ -698,13 +713,32 @@ func (r *EphemeralRunnerReconciler) actionsClientFor(ctx context.Context, runner
|
||||
opts = append(opts, actions.WithProxy(proxyFunc))
|
||||
}
|
||||
|
||||
return r.ActionsClient.GetClientFromSecret(
|
||||
ctx,
|
||||
runner.Spec.GitHubConfigUrl,
|
||||
runner.Namespace,
|
||||
secret.Data,
|
||||
opts...,
|
||||
)
|
||||
tlsConfig := runner.Spec.GitHubServerTLS
|
||||
if tlsConfig != nil {
|
||||
pool, err := tlsConfig.ToCertPool(func(name, key string) ([]byte, error) {
|
||||
var configmap corev1.ConfigMap
|
||||
err := r.Get(
|
||||
ctx,
|
||||
types.NamespacedName{
|
||||
Namespace: runner.Namespace,
|
||||
Name: name,
|
||||
},
|
||||
&configmap,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get configmap %s: %w", name, err)
|
||||
}
|
||||
|
||||
return []byte(configmap.Data[key]), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tls config: %w", err)
|
||||
}
|
||||
|
||||
opts = append(opts, actions.WithRootCAs(pool))
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// runnerRegisteredWithService checks if the runner is still registered with the service
|
||||
|
||||
@@ -2,10 +2,13 @@ package actionsgithubcom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +17,7 @@ import (
|
||||
"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"
|
||||
@@ -841,4 +845,100 @@ var _ = Describe("EphemeralRunner", func() {
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
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("test", 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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -450,6 +450,22 @@ func (r *EphemeralRunnerSetReconciler) actionsClientFor(ctx context.Context, rs
|
||||
if err := r.Get(ctx, types.NamespacedName{Namespace: rs.Namespace, Name: rs.Spec.EphemeralRunnerSpec.GitHubConfigSecret}, secret); err != nil {
|
||||
return nil, fmt.Errorf("failed to get secret: %w", err)
|
||||
}
|
||||
|
||||
opts, err := r.actionsClientOptionsFor(ctx, rs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get actions client options: %w", err)
|
||||
}
|
||||
|
||||
return r.ActionsClient.GetClientFromSecret(
|
||||
ctx,
|
||||
rs.Spec.EphemeralRunnerSpec.GitHubConfigUrl,
|
||||
rs.Namespace,
|
||||
secret.Data,
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *EphemeralRunnerSetReconciler) actionsClientOptionsFor(ctx context.Context, rs *v1alpha1.EphemeralRunnerSet) ([]actions.ClientOption, error) {
|
||||
var opts []actions.ClientOption
|
||||
if rs.Spec.EphemeralRunnerSpec.Proxy != nil {
|
||||
proxyFunc, err := rs.Spec.EphemeralRunnerSpec.Proxy.ProxyFunc(func(s string) (*corev1.Secret, error) {
|
||||
@@ -468,13 +484,32 @@ func (r *EphemeralRunnerSetReconciler) actionsClientFor(ctx context.Context, rs
|
||||
opts = append(opts, actions.WithProxy(proxyFunc))
|
||||
}
|
||||
|
||||
return r.ActionsClient.GetClientFromSecret(
|
||||
ctx,
|
||||
rs.Spec.EphemeralRunnerSpec.GitHubConfigUrl,
|
||||
rs.Namespace,
|
||||
secret.Data,
|
||||
opts...,
|
||||
)
|
||||
tlsConfig := rs.Spec.EphemeralRunnerSpec.GitHubServerTLS
|
||||
if tlsConfig != nil {
|
||||
pool, err := tlsConfig.ToCertPool(func(name, key string) ([]byte, error) {
|
||||
var configmap corev1.ConfigMap
|
||||
err := r.Get(
|
||||
ctx,
|
||||
types.NamespacedName{
|
||||
Namespace: rs.Namespace,
|
||||
Name: name,
|
||||
},
|
||||
&configmap,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get configmap %s: %w", name, err)
|
||||
}
|
||||
|
||||
return []byte(configmap.Data[key]), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tls config: %w", err)
|
||||
}
|
||||
|
||||
opts = append(opts, actions.WithRootCAs(pool))
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
|
||||
@@ -2,10 +2,13 @@ package actionsgithubcom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -24,6 +27,7 @@ import (
|
||||
v1alpha1 "github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
|
||||
"github.com/actions/actions-runner-controller/github/actions"
|
||||
"github.com/actions/actions-runner-controller/github/actions/fake"
|
||||
"github.com/actions/actions-runner-controller/github/actions/testserver"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -834,3 +838,148 @@ var _ = Describe("Test EphemeralRunnerSet controller with proxy settings", func(
|
||||
).Should(BeEquivalentTo(true))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Test EphemeralRunnerSet controller with custom root CA", func() {
|
||||
var ctx context.Context
|
||||
var mgr ctrl.Manager
|
||||
var autoscalingNS *corev1.Namespace
|
||||
var ephemeralRunnerSet *actionsv1alpha1.EphemeralRunnerSet
|
||||
var configSecret *corev1.Secret
|
||||
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 := &EphemeralRunnerSetReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
Log: logf.Log,
|
||||
ActionsClient: actions.NewMultiClient("test", logr.Discard()),
|
||||
}
|
||||
err = controller.SetupWithManager(mgr)
|
||||
Expect(err).NotTo(HaveOccurred(), "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()
|
||||
|
||||
ephemeralRunnerSet = &actionsv1alpha1.EphemeralRunnerSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-asrs",
|
||||
Namespace: autoscalingNS.Name,
|
||||
},
|
||||
Spec: actionsv1alpha1.EphemeralRunnerSetSpec{
|
||||
Replicas: 1,
|
||||
EphemeralRunnerSpec: actionsv1alpha1.EphemeralRunnerSpec{
|
||||
GitHubConfigUrl: server.ConfigURLForOrg("my-org"),
|
||||
GitHubConfigSecret: configSecret.Name,
|
||||
GitHubServerTLS: &actionsv1alpha1.GitHubServerTLSConfig{
|
||||
CertificateFrom: &v1alpha1.TLSCertificateSource{
|
||||
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: rootCAConfigMap.Name,
|
||||
},
|
||||
Key: "rootCA.crt",
|
||||
},
|
||||
},
|
||||
},
|
||||
RunnerScaleSetId: 100,
|
||||
PodTemplateSpec: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "runner",
|
||||
Image: "ghcr.io/actions/runner",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = k8sClient.Create(ctx, ephemeralRunnerSet)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to create EphemeralRunnerSet")
|
||||
|
||||
runnerList := new(actionsv1alpha1.EphemeralRunnerList)
|
||||
Eventually(func() (int, error) {
|
||||
err := k8sClient.List(ctx, runnerList, client.InNamespace(ephemeralRunnerSet.Namespace))
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
return len(runnerList.Items), nil
|
||||
},
|
||||
ephemeralRunnerSetTestTimeout,
|
||||
ephemeralRunnerSetTestInterval,
|
||||
).Should(BeEquivalentTo(1), "failed to create ephemeral runner")
|
||||
|
||||
runner := runnerList.Items[0].DeepCopy()
|
||||
Expect(runner.Spec.GitHubServerTLS).NotTo(BeNil(), "runner tls config should not be nil")
|
||||
Expect(runner.Spec.GitHubServerTLS).To(BeEquivalentTo(ephemeralRunnerSet.Spec.EphemeralRunnerSpec.GitHubServerTLS), "runner tls config should be correct")
|
||||
|
||||
runner.Status.Phase = corev1.PodRunning
|
||||
runner.Status.RunnerId = 100
|
||||
err = k8sClient.Status().Patch(ctx, runner, client.MergeFrom(&runnerList.Items[0]))
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to update ephemeral runner status")
|
||||
|
||||
updatedRunnerSet := new(actionsv1alpha1.EphemeralRunnerSet)
|
||||
err = k8sClient.Get(ctx, client.ObjectKey{Namespace: ephemeralRunnerSet.Namespace, Name: ephemeralRunnerSet.Name}, updatedRunnerSet)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to get EphemeralRunnerSet")
|
||||
|
||||
updatedRunnerSet.Spec.Replicas = 0
|
||||
err = k8sClient.Update(ctx, updatedRunnerSet)
|
||||
Expect(err).NotTo(HaveOccurred(), "failed to update EphemeralRunnerSet")
|
||||
|
||||
// wait for server to be called
|
||||
Eventually(
|
||||
func() bool {
|
||||
return serverSuccessfullyCalled
|
||||
},
|
||||
autoscalingRunnerSetTestTimeout,
|
||||
1*time.Nanosecond,
|
||||
).Should(BeTrue(), "server was not called")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -307,6 +307,7 @@ func (b *resourceBuilder) newAutoScalingListener(autoscalingRunnerSet *v1alpha1.
|
||||
Image: image,
|
||||
ImagePullSecrets: imagePullSecrets,
|
||||
Proxy: autoscalingRunnerSet.Spec.Proxy,
|
||||
GitHubServerTLS: autoscalingRunnerSet.Spec.GitHubServerTLS,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -39,9 +39,11 @@ import (
|
||||
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
|
||||
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
|
||||
|
||||
var cfg *rest.Config
|
||||
var k8sClient client.Client
|
||||
var testEnv *envtest.Environment
|
||||
var (
|
||||
cfg *rest.Config
|
||||
k8sClient client.Client
|
||||
testEnv *envtest.Environment
|
||||
)
|
||||
|
||||
func TestAPIs(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
Reference in New Issue
Block a user