mirror of https://github.com/k3s-io/k3s
Check if service accounts exist in gcr credential provider
Signed-off-by: Vishnu kannan <vishnuk@google.com>pull/6/head
parent
ee9cded79a
commit
ea1a459a80
|
@ -32,15 +32,18 @@ const (
|
|||
metadataAttributes = metadataUrl + "instance/attributes/"
|
||||
dockerConfigKey = metadataAttributes + "google-dockercfg"
|
||||
dockerConfigUrlKey = metadataAttributes + "google-dockercfg-url"
|
||||
serviceAccounts = metadataUrl + "instance/service-accounts/"
|
||||
metadataScopes = metadataUrl + "instance/service-accounts/default/scopes"
|
||||
metadataToken = metadataUrl + "instance/service-accounts/default/token"
|
||||
metadataEmail = metadataUrl + "instance/service-accounts/default/email"
|
||||
storageScopePrefix = "https://www.googleapis.com/auth/devstorage"
|
||||
cloudPlatformScopePrefix = "https://www.googleapis.com/auth/cloud-platform"
|
||||
googleProductName = "Google"
|
||||
defaultServiceAccount = "default/"
|
||||
)
|
||||
|
||||
// A variable to enable testing
|
||||
// Product file path that contains the cloud service name.
|
||||
// This is a variable instead of a const to enable testing.
|
||||
var gceProductNameFile = "/sys/class/dmi/id/product_name"
|
||||
|
||||
// For these urls, the parts of the host name can be glob, for example '*.gcr.io" will match
|
||||
|
@ -103,7 +106,8 @@ func init() {
|
|||
})
|
||||
}
|
||||
|
||||
// Returns true if it finds a local GCE VM
|
||||
// Returns true if it finds a local GCE VM.
|
||||
// Looks at a product file that is an undocumented API.
|
||||
func onGCEVM() bool {
|
||||
data, err := ioutil.ReadFile(gceProductNameFile)
|
||||
if err != nil {
|
||||
|
@ -162,36 +166,74 @@ func (g *dockerConfigUrlKeyProvider) Provide() credentialprovider.DockerConfig {
|
|||
return credentialprovider.DockerConfig{}
|
||||
}
|
||||
|
||||
// Enabled implements a special metadata-based check, which verifies the
|
||||
// storage scope is available on the GCE VM.
|
||||
func (g *containerRegistryProvider) Enabled() bool {
|
||||
if !onGCEVM() {
|
||||
return false
|
||||
}
|
||||
// Given that we are on GCE, we should keep retrying until the metadata server responds.
|
||||
var (
|
||||
value []byte
|
||||
err error
|
||||
backoff = time.Millisecond
|
||||
)
|
||||
// runcWithBackoff runs input function `f` with an exponential backoff.
|
||||
// Note that this method can block indefinitely.
|
||||
func runWithBackoff(f func() ([]byte, error)) []byte {
|
||||
var backoff = 100 * time.Millisecond
|
||||
const maxBackoff = time.Minute
|
||||
for {
|
||||
value, err = credentialprovider.ReadUrl(metadataScopes+"?alt=json", g.Client, metadataHeader)
|
||||
value, err := f()
|
||||
if err == nil {
|
||||
break
|
||||
return value
|
||||
}
|
||||
glog.Errorf("failed to Get %q: %v", metadataScopes+"?alt=json", err)
|
||||
time.Sleep(backoff)
|
||||
backoff = backoff * 2
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
}
|
||||
var scopes []string
|
||||
if err := json.Unmarshal(value, &scopes); err != nil {
|
||||
}
|
||||
|
||||
// Enabled implements a special metadata-based check, which verifies the
|
||||
// storage scope is available on the GCE VM.
|
||||
// If running on a GCE VM, check if 'default' service account exists.
|
||||
// If it does not exist, assume that registry is not enabled.
|
||||
// If default service account exists, check if relevant scopes exist in the default service account.
|
||||
// The metadata service can become temporarily inaccesible. Hence all requests to the metadata
|
||||
// service will be retried until the metadata server returns a `200`.
|
||||
// It is expected that "http://metadata.google.internal./computeMetadata/v1/instance/service-accounts/" will return a `200`
|
||||
// and "http://metadata.google.internal./computeMetadata/v1/instance/service-accounts/default/scopes" will also return `200`.
|
||||
// More information on metadata service can be found here - https://cloud.google.com/compute/docs/storing-retrieving-metadata
|
||||
func (g *containerRegistryProvider) Enabled() bool {
|
||||
if !onGCEVM() {
|
||||
return false
|
||||
}
|
||||
// Given that we are on GCE, we should keep retrying until the metadata server responds.
|
||||
value := runWithBackoff(func() ([]byte, error) {
|
||||
value, err := credentialprovider.ReadUrl(serviceAccounts, g.Client, metadataHeader)
|
||||
if err != nil {
|
||||
glog.V(2).Infof("Failed to Get service accounts from gce metadata server: %v", err)
|
||||
}
|
||||
return value, err
|
||||
})
|
||||
// We expect the service account to return a list of account directories separated by newlines, e.g.,
|
||||
// sv-account-name1/
|
||||
// sv-account-name2/
|
||||
// ref: https://cloud.google.com/compute/docs/storing-retrieving-metadata
|
||||
defaultServiceAccountExists := false
|
||||
for _, sa := range strings.Split(string(value), "\n") {
|
||||
if strings.TrimSpace(sa) == defaultServiceAccount {
|
||||
defaultServiceAccountExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !defaultServiceAccountExists {
|
||||
glog.V(2).Infof("'default' service account does not exist. Found following service accounts: %q", string(value))
|
||||
return false
|
||||
}
|
||||
url := metadataScopes + "?alt=json"
|
||||
value = runWithBackoff(func() ([]byte, error) {
|
||||
value, err := credentialprovider.ReadUrl(url, g.Client, metadataHeader)
|
||||
if err != nil {
|
||||
glog.V(2).Infof("Failed to Get scopes in default service account from gce metadata server: %v", err)
|
||||
}
|
||||
return value, err
|
||||
})
|
||||
var scopes []string
|
||||
if err := json.Unmarshal(value, &scopes); err != nil {
|
||||
glog.Errorf("Failed to unmarshal scopes: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
for _, v := range scopes {
|
||||
// cloudPlatformScope implies storage scope.
|
||||
if strings.HasPrefix(v, storageScopePrefix) || strings.HasPrefix(v, cloudPlatformScopePrefix) {
|
||||
|
|
|
@ -198,10 +198,11 @@ func TestContainerRegistryBasics(t *testing.T) {
|
|||
token := &tokenBlob{AccessToken: "ya26.lots-of-indiscernible-garbage"}
|
||||
|
||||
const (
|
||||
defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/"
|
||||
scopeEndpoint = defaultEndpoint + "scopes"
|
||||
emailEndpoint = defaultEndpoint + "email"
|
||||
tokenEndpoint = defaultEndpoint + "token"
|
||||
serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/"
|
||||
defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/"
|
||||
scopeEndpoint = defaultEndpoint + "scopes"
|
||||
emailEndpoint = defaultEndpoint + "email"
|
||||
tokenEndpoint = defaultEndpoint + "token"
|
||||
)
|
||||
var err error
|
||||
gceProductNameFile, err = createProductNameFile()
|
||||
|
@ -227,6 +228,9 @@ func TestContainerRegistryBasics(t *testing.T) {
|
|||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
fmt.Fprintln(w, string(bytes))
|
||||
} else if serviceAccountsEndpoint == r.URL.Path {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, "default/\ncustom")
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
@ -272,10 +276,54 @@ func TestContainerRegistryBasics(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestContainerRegistryNoServiceAccount(t *testing.T) {
|
||||
const (
|
||||
serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/"
|
||||
)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Only serve the URL key and the value endpoint
|
||||
if serviceAccountsEndpoint == r.URL.Path {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
bytes, err := json.Marshal([]string{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
fmt.Fprintln(w, string(bytes))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
var err error
|
||||
gceProductNameFile, err = createProductNameFile()
|
||||
if err != nil {
|
||||
t.Errorf("failed to create gce product name file: %v", err)
|
||||
}
|
||||
defer os.Remove(gceProductNameFile)
|
||||
|
||||
// Make a transport that reroutes all traffic to the example server
|
||||
transport := utilnet.SetTransportDefaults(&http.Transport{
|
||||
Proxy: func(req *http.Request) (*url.URL, error) {
|
||||
return url.Parse(server.URL + req.URL.Path)
|
||||
},
|
||||
})
|
||||
|
||||
provider := &containerRegistryProvider{
|
||||
metadataProvider{Client: &http.Client{Transport: transport}},
|
||||
}
|
||||
|
||||
if provider.Enabled() {
|
||||
t.Errorf("Provider is unexpectedly enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerRegistryNoStorageScope(t *testing.T) {
|
||||
const (
|
||||
defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/"
|
||||
scopeEndpoint = defaultEndpoint + "scopes"
|
||||
serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/"
|
||||
defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/"
|
||||
scopeEndpoint = defaultEndpoint + "scopes"
|
||||
)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Only serve the URL key and the value endpoint
|
||||
|
@ -283,6 +331,9 @@ func TestContainerRegistryNoStorageScope(t *testing.T) {
|
|||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `["https://www.googleapis.com/auth/compute.read_write"]`)
|
||||
} else if serviceAccountsEndpoint == r.URL.Path {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, "default/\ncustom")
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
@ -314,8 +365,9 @@ func TestContainerRegistryNoStorageScope(t *testing.T) {
|
|||
|
||||
func TestComputePlatformScopeSubstitutesStorageScope(t *testing.T) {
|
||||
const (
|
||||
defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/"
|
||||
scopeEndpoint = defaultEndpoint + "scopes"
|
||||
serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/"
|
||||
defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/"
|
||||
scopeEndpoint = defaultEndpoint + "scopes"
|
||||
)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Only serve the URL key and the value endpoint
|
||||
|
@ -323,6 +375,10 @@ func TestComputePlatformScopeSubstitutesStorageScope(t *testing.T) {
|
|||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `["https://www.googleapis.com/auth/compute.read_write","https://www.googleapis.com/auth/cloud-platform.read-only"]`)
|
||||
} else if serviceAccountsEndpoint == r.URL.Path {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintln(w, "default/\ncustom")
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
|
|
@ -29,7 +29,11 @@ import (
|
|||
// DockerConfigProvider is the interface that registered extensions implement
|
||||
// to materialize 'dockercfg' credentials.
|
||||
type DockerConfigProvider interface {
|
||||
// Enabled returns true if the config provider is enabled.
|
||||
// Implementations can be blocking - e.g. metadata server unavailable.
|
||||
Enabled() bool
|
||||
// Provide returns docker configuration.
|
||||
// Implementations can be blocking - e.g. metadata server unavailable.
|
||||
Provide() DockerConfig
|
||||
// LazyProvide() gets called after URL matches have been performed, so the
|
||||
// location used as the key in DockerConfig would be redundant.
|
||||
|
|
Loading…
Reference in New Issue