diff --git a/storage/kubernetes/client.go b/storage/kubernetes/client.go index 1a1653b3..679bdfda 100644 --- a/storage/kubernetes/client.go +++ b/storage/kubernetes/client.go @@ -32,6 +32,17 @@ import ( "github.com/dexidp/dex/storage/kubernetes/k8sapi" ) +const ( + serviceAccountPath = "/var/run/secrets/kubernetes.io/serviceaccount/" + serviceAccountTokenPath = serviceAccountPath + "token" + serviceAccountCAPath = serviceAccountPath + "ca.crt" + serviceAccountNamespacePath = serviceAccountPath + "namespace" + + kubernetesServiceHostENV = "KUBERNETES_SERVICE_HOST" + kubernetesServicePortENV = "KUBERNETES_SERVICE_PORT" + kubernetesPodNamespaceENV = "KUBERNETES_POD_NAMESPACE" +) + type client struct { client *http.Client baseURL string @@ -508,33 +519,35 @@ func getInClusterConfigNamespace(token, namespaceENV, namespacePath string) (str return "", fmt.Errorf("%v: trying to get namespace from file: %v", err, fileErr) } -func inClusterConfig() (k8sapi.Cluster, k8sapi.AuthInfo, string, error) { - const ( - serviceAccountPath = "/var/run/secrets/kubernetes.io/serviceaccount/" - serviceAccountTokenPath = serviceAccountPath + "token" - serviceAccountCAPath = serviceAccountPath + "ca.crt" - serviceAccountNamespacePath = serviceAccountPath + "namespace" - - kubernetesServiceHostENV = "KUBERNETES_SERVICE_HOST" - kubernetesServicePortENV = "KUBERNETES_SERVICE_PORT" - kubernetesPodNamespaceENV = "KUBERNETES_POD_NAMESPACE" - ) - - host, port := os.Getenv(kubernetesServiceHostENV), os.Getenv(kubernetesServicePortENV) +func getInClusterConnectOptions(host, port string) (k8sapi.Cluster, error) { if len(host) == 0 || len(port) == 0 { - return k8sapi.Cluster{}, k8sapi.AuthInfo{}, "", fmt.Errorf( + return k8sapi.Cluster{}, fmt.Errorf( "unable to load in-cluster configuration, %s and %s must be defined", kubernetesServiceHostENV, kubernetesServicePortENV, ) } + // we need to wrap IPv6 addresses in square brackets - // IPv4 also works with square brackets - host = "[" + host + "]" + // IPv4 used to work with square brackets, but it was fixed in the latest Go versions + // https://github.com/golang/go/issues/75712 + ipAddr := net.ParseIP(host) + if ipAddr != nil && ipAddr.To4() == nil { + host = "[" + host + "]" + } + cluster := k8sapi.Cluster{ Server: "https://" + host + ":" + port, CertificateAuthority: serviceAccountCAPath, } + return cluster, nil +} + +func inClusterConfig() (k8sapi.Cluster, k8sapi.AuthInfo, string, error) { + cluster, err := getInClusterConnectOptions(os.Getenv(kubernetesServiceHostENV), os.Getenv(kubernetesServicePortENV)) + if err != nil { + return cluster, k8sapi.AuthInfo{}, "", err + } token, err := os.ReadFile(serviceAccountTokenPath) if err != nil { diff --git a/storage/kubernetes/client_test.go b/storage/kubernetes/client_test.go index 564333d2..e57992f7 100644 --- a/storage/kubernetes/client_test.go +++ b/storage/kubernetes/client_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/dexidp/dex/storage/kubernetes/k8sapi" @@ -216,6 +217,63 @@ func TestGetClusterConfigNamespace(t *testing.T) { } } +func TestGetInClusterConnectOptions(t *testing.T) { + type testCase struct { + name string + host string + port string + expectedURL string + expectError bool + } + + testCases := []testCase{ + { + name: "valid IPv4", + host: "10.1.1.1", + port: "443", + expectedURL: "https://10.1.1.1:443", + }, + { + name: "valid IPv6", + host: "fd00::1", + port: "8443", + expectedURL: "https://[fd00::1]:8443", + }, + { + name: "valid DNS name", + host: "kubernetes.default.svc", + port: "443", + expectedURL: "https://kubernetes.default.svc:443", + }, + { + name: "empty host", + host: "", + port: "443", + expectError: true, + }, + { + name: "empty port", + host: "127.0.0.1", + port: "", + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cluster, err := getInClusterConnectOptions(tc.host, tc.port) + + if tc.expectError { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tc.expectedURL, cluster.Server) + assert.Equal(t, "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", cluster.CertificateAuthority) + }) + } +} + const serviceAccountToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZXgtdGVzdC1uYW1lc3BhY2UiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoiZG90aGVyb2JvdC1zZWNyZXQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZG90aGVyb2JvdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjQyYjJhOTRmLTk4MjAtMTFlNi1iZDc0LTJlZmQzOGYxMjYxYyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZXgtdGVzdC1uYW1lc3BhY2U6ZG90aGVyb2JvdCJ9.KViBpPwCiBwxDvAjYUUXoVvLVwqV011aLlYQpNtX12Bh8M-QAFch-3RWlo_SR00bcdFg_nZo9JKACYlF_jHMEsf__PaYms9r7vEaSg0jPfkqnL2WXZktzQRyLBr0n-bxeUrbwIWsKOAC0DfFB5nM8XoXljRmq8yAx8BAdmQp7MIFb4EOV9nYthhua6pjzYyaFSiDiYTjw7HtXOvoL8oepodJ3-37pUKS8vdBvnvUoqC4M1YAhkO5L36JF6KV_RfmG8GPEdNQfXotHcsR-3jKi1n8S5l7Xd-rhrGOhSGQizH3dORzo9GvBAhYeqbq1O-NLzm2EQUiMQayIUx7o4g3Kw" // The following program was used to generate the example token. Since we don't want to