Merge pull request #2092 from flant/kubernetes-fallback-to-namespace-file
fix: read namespace from file for Kubernetes storage client
This commit is contained in:
		| @@ -343,10 +343,10 @@ func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string, l | ||||
| 	var t http.RoundTripper | ||||
| 	httpTransport := &http.Transport{ | ||||
| 		Proxy: http.ProxyFromEnvironment, | ||||
| 		Dial: (&net.Dialer{ | ||||
| 		DialContext: (&net.Dialer{ | ||||
| 			Timeout:   30 * time.Second, | ||||
| 			KeepAlive: 30 * time.Second, | ||||
| 		}).Dial, | ||||
| 		}).DialContext, | ||||
| 		TLSClientConfig:       tlsConfig, | ||||
| 		TLSHandshakeTimeout:   10 * time.Second, | ||||
| 		ExpectContinueTimeout: 1 * time.Second, | ||||
| @@ -460,34 +460,76 @@ func namespaceFromServiceAccountJWT(s string) (string, error) { | ||||
| 	return data.Namespace, nil | ||||
| } | ||||
|  | ||||
| func inClusterConfig() (cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string, err error) { | ||||
| 	host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT") | ||||
| func namespaceFromFile(path string) (string, error) { | ||||
| 	data, err := ioutil.ReadFile(path) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	return string(data), nil | ||||
| } | ||||
|  | ||||
| func getInClusterConfigNamespace(token, namespaceENV, namespacePath string) (string, error) { | ||||
| 	namespace := os.Getenv(namespaceENV) | ||||
| 	if namespace != "" { | ||||
| 		return namespace, nil | ||||
| 	} | ||||
|  | ||||
| 	namespace, err := namespaceFromServiceAccountJWT(token) | ||||
| 	if err == nil { | ||||
| 		return namespace, nil | ||||
| 	} | ||||
|  | ||||
| 	err = fmt.Errorf("inspect service account token: %v", err) | ||||
| 	namespace, fileErr := namespaceFromFile(namespacePath) | ||||
| 	if fileErr == nil { | ||||
| 		return namespace, nil | ||||
| 	} | ||||
|  | ||||
| 	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) | ||||
| 	if len(host) == 0 || len(port) == 0 { | ||||
| 		err = fmt.Errorf("unable to load in-cluster configuration, KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined") | ||||
| 		return | ||||
| 		return k8sapi.Cluster{}, k8sapi.AuthInfo{}, "", 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 + "]" | ||||
| 	cluster = k8sapi.Cluster{ | ||||
| 	cluster := k8sapi.Cluster{ | ||||
| 		Server:               "https://" + host + ":" + port, | ||||
| 		CertificateAuthority: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", | ||||
| 	} | ||||
| 	token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	user = k8sapi.AuthInfo{Token: string(token)} | ||||
|  | ||||
| 	if namespace = os.Getenv("KUBERNETES_POD_NAMESPACE"); namespace == "" { | ||||
| 		namespace, err = namespaceFromServiceAccountJWT(user.Token) | ||||
| 		if err != nil { | ||||
| 			err = fmt.Errorf("failed to inspect service account token: %v", err) | ||||
| 			return | ||||
| 		} | ||||
| 		CertificateAuthority: serviceAccountCAPath, | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| 	token, err := ioutil.ReadFile(serviceAccountTokenPath) | ||||
| 	if err != nil { | ||||
| 		return cluster, k8sapi.AuthInfo{}, "", err | ||||
| 	} | ||||
|  | ||||
| 	user := k8sapi.AuthInfo{Token: string(token)} | ||||
|  | ||||
| 	namespace, err := getInClusterConfigNamespace(user.Token, kubernetesPodNamespaceENV, serviceAccountNamespacePath) | ||||
| 	if err != nil { | ||||
| 		return cluster, user, "", err | ||||
| 	} | ||||
|  | ||||
| 	return cluster, user, namespace, nil | ||||
| } | ||||
|  | ||||
| func currentContext(config *k8sapi.Config) (cluster k8sapi.Cluster, user k8sapi.AuthInfo, ns string, err error) { | ||||
| @@ -498,7 +540,7 @@ func currentContext(config *k8sapi.Config) (cluster k8sapi.Cluster, user k8sapi. | ||||
| 			return cluster, user, "", errors.New("kubeconfig has no current context") | ||||
| 		} | ||||
| 	} | ||||
| 	context, ok := func() (k8sapi.Context, bool) { | ||||
| 	k8sContext, ok := func() (k8sapi.Context, bool) { | ||||
| 		for _, namedContext := range config.Contexts { | ||||
| 			if namedContext.Name == config.CurrentContext { | ||||
| 				return namedContext.Context, true | ||||
| @@ -512,26 +554,26 @@ func currentContext(config *k8sapi.Config) (cluster k8sapi.Cluster, user k8sapi. | ||||
|  | ||||
| 	cluster, ok = func() (k8sapi.Cluster, bool) { | ||||
| 		for _, namedCluster := range config.Clusters { | ||||
| 			if namedCluster.Name == context.Cluster { | ||||
| 			if namedCluster.Name == k8sContext.Cluster { | ||||
| 				return namedCluster.Cluster, true | ||||
| 			} | ||||
| 		} | ||||
| 		return k8sapi.Cluster{}, false | ||||
| 	}() | ||||
| 	if !ok { | ||||
| 		return cluster, user, "", fmt.Errorf("no cluster named %q found", context.Cluster) | ||||
| 		return cluster, user, "", fmt.Errorf("no cluster named %q found", k8sContext.Cluster) | ||||
| 	} | ||||
|  | ||||
| 	user, ok = func() (k8sapi.AuthInfo, bool) { | ||||
| 		for _, namedAuthInfo := range config.AuthInfos { | ||||
| 			if namedAuthInfo.Name == context.AuthInfo { | ||||
| 			if namedAuthInfo.Name == k8sContext.AuthInfo { | ||||
| 				return namedAuthInfo.AuthInfo, true | ||||
| 			} | ||||
| 		} | ||||
| 		return k8sapi.AuthInfo{}, false | ||||
| 	}() | ||||
| 	if !ok { | ||||
| 		return cluster, user, "", fmt.Errorf("no user named %q found", context.AuthInfo) | ||||
| 		return cluster, user, "", fmt.Errorf("no user named %q found", k8sContext.AuthInfo) | ||||
| 	} | ||||
| 	return cluster, user, context.Namespace, nil | ||||
| 	return cluster, user, k8sContext.Namespace, nil | ||||
| } | ||||
|   | ||||
| @@ -3,8 +3,12 @@ package kubernetes | ||||
| import ( | ||||
| 	"hash" | ||||
| 	"hash/fnv" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"sync" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| // This test does not have an explicit error condition but is used | ||||
| @@ -42,18 +46,101 @@ func TestOfflineTokenName(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestNamespaceFromServiceAccountJWT(t *testing.T) { | ||||
| 	namespace, err := namespaceFromServiceAccountJWT(serviceAccountToken) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| func TestGetClusterConfigNamespace(t *testing.T) { | ||||
| 	const namespaceENVVariableName = "TEST_GET_CLUSTER_CONFIG_NAMESPACE" | ||||
| 	{ | ||||
| 		os.Setenv(namespaceENVVariableName, "namespace-from-env") | ||||
| 		defer os.Unsetenv(namespaceENVVariableName) | ||||
| 	} | ||||
| 	wantNamespace := "dex-test-namespace" | ||||
| 	if namespace != wantNamespace { | ||||
| 		t.Errorf("expected namespace %q got %q", wantNamespace, namespace) | ||||
|  | ||||
| 	var namespaceFile string | ||||
| 	{ | ||||
| 		tmpfile, err := ioutil.TempFile(os.TempDir(), "test-get-cluster-config-namespace") | ||||
| 		require.NoError(t, err) | ||||
|  | ||||
| 		_, err = tmpfile.Write([]byte("namespace-from-file")) | ||||
| 		require.NoError(t, err) | ||||
|  | ||||
| 		namespaceFile = tmpfile.Name() | ||||
| 		defer os.Remove(namespaceFile) | ||||
| 	} | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		name        string | ||||
| 		token       string | ||||
| 		fileName    string | ||||
| 		envVariable string | ||||
|  | ||||
| 		expectedError     bool | ||||
| 		expectedNamespace string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:        "With env variable", | ||||
| 			envVariable: "TEST_GET_CLUSTER_CONFIG_NAMESPACE", | ||||
|  | ||||
| 			expectedNamespace: "namespace-from-env", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:  "With token", | ||||
| 			token: serviceAccountToken, | ||||
|  | ||||
| 			expectedNamespace: "dex-test-namespace", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "With namespace file", | ||||
| 			fileName: namespaceFile, | ||||
|  | ||||
| 			expectedNamespace: "namespace-from-file", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "With file and token", | ||||
| 			fileName: namespaceFile, | ||||
| 			token:    serviceAccountToken, | ||||
|  | ||||
| 			expectedNamespace: "dex-test-namespace", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "With file and env", | ||||
| 			fileName:    namespaceFile, | ||||
| 			envVariable: "TEST_GET_CLUSTER_CONFIG_NAMESPACE", | ||||
|  | ||||
| 			expectedNamespace: "namespace-from-env", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "With token and env", | ||||
| 			envVariable: "TEST_GET_CLUSTER_CONFIG_NAMESPACE", | ||||
| 			token:       serviceAccountToken, | ||||
|  | ||||
| 			expectedNamespace: "namespace-from-env", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "With file, token and env", | ||||
| 			fileName:    namespaceFile, | ||||
| 			token:       serviceAccountToken, | ||||
| 			envVariable: "TEST_GET_CLUSTER_CONFIG_NAMESPACE", | ||||
|  | ||||
| 			expectedNamespace: "namespace-from-env", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:          "Without anything", | ||||
| 			expectedError: true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range tests { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			namespace, err := getInClusterConfigNamespace(tc.token, tc.envVariable, tc.fileName) | ||||
| 			if tc.expectedError { | ||||
| 				require.Error(t, err) | ||||
| 			} else { | ||||
| 				require.NoError(t, err) | ||||
| 			} | ||||
| 			require.Equal(t, namespace, tc.expectedNamespace) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var serviceAccountToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZXgtdGVzdC1uYW1lc3BhY2UiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoiZG90aGVyb2JvdC1zZWNyZXQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZG90aGVyb2JvdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjQyYjJhOTRmLTk4MjAtMTFlNi1iZDc0LTJlZmQzOGYxMjYxYyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZXgtdGVzdC1uYW1lc3BhY2U6ZG90aGVyb2JvdCJ9.KViBpPwCiBwxDvAjYUUXoVvLVwqV011aLlYQpNtX12Bh8M-QAFch-3RWlo_SR00bcdFg_nZo9JKACYlF_jHMEsf__PaYms9r7vEaSg0jPfkqnL2WXZktzQRyLBr0n-bxeUrbwIWsKOAC0DfFB5nM8XoXljRmq8yAx8BAdmQp7MIFb4EOV9nYthhua6pjzYyaFSiDiYTjw7HtXOvoL8oepodJ3-37pUKS8vdBvnvUoqC4M1YAhkO5L36JF6KV_RfmG8GPEdNQfXotHcsR-3jKi1n8S5l7Xd-rhrGOhSGQizH3dORzo9GvBAhYeqbq1O-NLzm2EQUiMQayIUx7o4g3Kw" | ||||
| 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 | ||||
| // import Kubernetes, just leave it as a comment. | ||||
|   | ||||
		Reference in New Issue
	
	Block a user