diff --git a/pkg/api/types.go b/pkg/api/types.go index ab854971590..b9e4301b028 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -2218,6 +2218,8 @@ const ( ServiceAccountKubeconfigKey = "kubernetes.kubeconfig" // ServiceAccountRootCAKey is the key of the optional root certificate authority for SecretTypeServiceAccountToken secrets ServiceAccountRootCAKey = "ca.crt" + // ServiceAccountNamespaceKey is the key of the optional namespace to use as the default for namespaced API calls + ServiceAccountNamespaceKey = "namespace" // SecretTypeDockercfg contains a dockercfg file that follows the same format rules as ~/.dockercfg // diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index 9ea96df62e1..3f6c4fcd24b 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -2685,6 +2685,8 @@ const ( ServiceAccountKubeconfigKey = "kubernetes.kubeconfig" // ServiceAccountRootCAKey is the key of the optional root certificate authority for SecretTypeServiceAccountToken secrets ServiceAccountRootCAKey = "ca.crt" + // ServiceAccountNamespaceKey is the key of the optional namespace to use as the default for namespaced API calls + ServiceAccountNamespaceKey = "namespace" // SecretTypeDockercfg contains a dockercfg file that follows the same format rules as ~/.dockercfg // diff --git a/pkg/client/unversioned/clientcmd/client_config.go b/pkg/client/unversioned/clientcmd/client_config.go index a0a323fe4d5..ce94a128bb0 100644 --- a/pkg/client/unversioned/clientcmd/client_config.go +++ b/pkg/client/unversioned/clientcmd/client_config.go @@ -19,8 +19,10 @@ package clientcmd import ( "fmt" "io" + "io/ioutil" "net/url" "os" + "strings" "github.com/golang/glog" "github.com/imdario/mergo" @@ -325,12 +327,19 @@ func (inClusterClientConfig) ClientConfig() (*client.Config, error) { } func (inClusterClientConfig) Namespace() (string, error) { - // TODO: generic way to figure out what namespace you are running in? - // This way assumes you've set the POD_NAMESPACE environment variable - // using the downward API. + // This way assumes you've set the POD_NAMESPACE environment variable using the downward API. + // This check has to be done first for backwards compatibility with the way InClusterConfig was originally set up if ns := os.Getenv("POD_NAMESPACE"); ns != "" { return ns, nil } + + // Fall back to the namespace associated with the service account token, if available + if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil { + if ns := strings.TrimSpace(string(data)); len(ns) > 0 { + return ns, nil + } + } + return "default", nil } diff --git a/pkg/controller/serviceaccount/tokens_controller.go b/pkg/controller/serviceaccount/tokens_controller.go index 59ea09a7081..253c1efe2f3 100644 --- a/pkg/controller/serviceaccount/tokens_controller.go +++ b/pkg/controller/serviceaccount/tokens_controller.go @@ -325,6 +325,7 @@ func (e *TokensController) createSecret(serviceAccount *api.ServiceAccount) erro return err } secret.Data[api.ServiceAccountTokenKey] = []byte(token) + secret.Data[api.ServiceAccountNamespaceKey] = []byte(serviceAccount.Namespace) if e.rootCA != nil && len(e.rootCA) > 0 { secret.Data[api.ServiceAccountRootCAKey] = e.rootCA } @@ -364,10 +365,12 @@ func (e *TokensController) generateTokenIfNeeded(serviceAccount *api.ServiceAcco caData := secret.Data[api.ServiceAccountRootCAKey] needsCA := len(e.rootCA) > 0 && bytes.Compare(caData, e.rootCA) != 0 + needsNamespace := len(secret.Data[api.ServiceAccountNamespaceKey]) == 0 + tokenData := secret.Data[api.ServiceAccountTokenKey] needsToken := len(tokenData) == 0 - if !needsCA && !needsToken { + if !needsCA && !needsToken && !needsNamespace { return nil } @@ -375,6 +378,10 @@ func (e *TokensController) generateTokenIfNeeded(serviceAccount *api.ServiceAcco if needsCA { secret.Data[api.ServiceAccountRootCAKey] = e.rootCA } + // Set the namespace + if needsNamespace { + secret.Data[api.ServiceAccountNamespaceKey] = []byte(secret.Namespace) + } // Generate the token if needsToken { diff --git a/pkg/controller/serviceaccount/tokens_controller_test.go b/pkg/controller/serviceaccount/tokens_controller_test.go index e39e752d1c2..7a527ff4c7f 100644 --- a/pkg/controller/serviceaccount/tokens_controller_test.go +++ b/pkg/controller/serviceaccount/tokens_controller_test.go @@ -115,8 +115,9 @@ func createdTokenSecret() *api.Secret { }, Type: api.SecretTypeServiceAccountToken, Data: map[string][]byte{ - "token": []byte("ABC"), - "ca.crt": []byte("CA Data"), + "token": []byte("ABC"), + "ca.crt": []byte("CA Data"), + "namespace": []byte("default"), }, } } @@ -136,8 +137,9 @@ func serviceAccountTokenSecret() *api.Secret { }, Type: api.SecretTypeServiceAccountToken, Data: map[string][]byte{ - "token": []byte("ABC"), - "ca.crt": []byte("CA Data"), + "token": []byte("ABC"), + "ca.crt": []byte("CA Data"), + "namespace": []byte("default"), }, } } @@ -163,6 +165,20 @@ func serviceAccountTokenSecretWithCAData(data []byte) *api.Secret { return secret } +// serviceAccountTokenSecretWithoutNamespaceData returns an existing ServiceAccountToken secret that lacks namespace data +func serviceAccountTokenSecretWithoutNamespaceData() *api.Secret { + secret := serviceAccountTokenSecret() + delete(secret.Data, api.ServiceAccountNamespaceKey) + return secret +} + +// serviceAccountTokenSecretWithNamespaceData returns an existing ServiceAccountToken secret with the specified namespace data +func serviceAccountTokenSecretWithNamespaceData(data []byte) *api.Secret { + secret := serviceAccountTokenSecret() + secret.Data[api.ServiceAccountNamespaceKey] = data + return secret +} + func TestTokenCreation(t *testing.T) { testcases := map[string]struct { ClientObjects []runtime.Object @@ -379,6 +395,24 @@ func TestTokenCreation(t *testing.T) { core.NewUpdateAction("secrets", api.NamespaceDefault, serviceAccountTokenSecret()), }, }, + "added token secret without namespace data": { + ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutNamespaceData()}, + ExistingServiceAccount: serviceAccount(tokenSecretReferences()), + + AddedSecret: serviceAccountTokenSecretWithoutNamespaceData(), + ExpectedActions: []core.Action{ + core.NewUpdateAction("secrets", api.NamespaceDefault, serviceAccountTokenSecret()), + }, + }, + "added token secret with custom namespace data": { + ClientObjects: []runtime.Object{serviceAccountTokenSecretWithNamespaceData([]byte("custom"))}, + ExistingServiceAccount: serviceAccount(tokenSecretReferences()), + + AddedSecret: serviceAccountTokenSecretWithNamespaceData([]byte("custom")), + ExpectedActions: []core.Action{ + // no update is performed... the custom namespace is preserved + }, + }, "updated secret without serviceaccount": { ClientObjects: []runtime.Object{serviceAccountTokenSecret()}, @@ -422,6 +456,24 @@ func TestTokenCreation(t *testing.T) { core.NewUpdateAction("secrets", api.NamespaceDefault, serviceAccountTokenSecret()), }, }, + "updated token secret without namespace data": { + ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutNamespaceData()}, + ExistingServiceAccount: serviceAccount(tokenSecretReferences()), + + UpdatedSecret: serviceAccountTokenSecretWithoutNamespaceData(), + ExpectedActions: []core.Action{ + core.NewUpdateAction("secrets", api.NamespaceDefault, serviceAccountTokenSecret()), + }, + }, + "updated token secret with custom namespace data": { + ClientObjects: []runtime.Object{serviceAccountTokenSecretWithNamespaceData([]byte("custom"))}, + ExistingServiceAccount: serviceAccount(tokenSecretReferences()), + + UpdatedSecret: serviceAccountTokenSecretWithNamespaceData([]byte("custom")), + ExpectedActions: []core.Action{ + // no update is performed... the custom namespace is preserved + }, + }, "deleted secret without serviceaccount": { DeletedSecret: serviceAccountTokenSecret(), diff --git a/test/e2e/service_accounts.go b/test/e2e/service_accounts.go index 8900a702406..f1a9a8e994c 100644 --- a/test/e2e/service_accounts.go +++ b/test/e2e/service_accounts.go @@ -24,11 +24,14 @@ import ( apierrors "k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/util" "k8s.io/kubernetes/pkg/util/wait" + "k8s.io/kubernetes/pkg/version" "k8s.io/kubernetes/plugin/pkg/admission/serviceaccount" . "github.com/onsi/ginkgo" ) +var serviceAccountTokenNamespaceVersion = version.MustParse("v1.2.0") + var _ = Describe("ServiceAccounts", func() { f := NewFramework("svcaccounts") @@ -94,11 +97,28 @@ var _ = Describe("ServiceAccounts", func() { }, } + supportsTokenNamespace, _ := serverVersionGTE(serviceAccountTokenNamespaceVersion, f.Client) + if supportsTokenNamespace { + pod.Spec.Containers = append(pod.Spec.Containers, api.Container{ + Name: "namespace-test", + Image: "gcr.io/google_containers/mounttest:0.2", + Args: []string{ + fmt.Sprintf("--file_content=%s/%s", serviceaccount.DefaultAPITokenMountPath, api.ServiceAccountNamespaceKey), + }, + }) + } + f.TestContainerOutput("consume service account token", pod, 0, []string{ fmt.Sprintf(`content of file "%s/%s": %s`, serviceaccount.DefaultAPITokenMountPath, api.ServiceAccountTokenKey, tokenContent), }) f.TestContainerOutput("consume service account root CA", pod, 1, []string{ fmt.Sprintf(`content of file "%s/%s": %s`, serviceaccount.DefaultAPITokenMountPath, api.ServiceAccountRootCAKey, rootCAContent), }) + + if supportsTokenNamespace { + f.TestContainerOutput("consume service account namespace", pod, 2, []string{ + fmt.Sprintf(`content of file "%s/%s": %s`, serviceaccount.DefaultAPITokenMountPath, api.ServiceAccountNamespaceKey, f.Namespace.Name), + }) + } }) })