feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,238 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility library for working with docker clients."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import errno
import json
import os
import subprocess
import sys
from googlecloudsdk.core import exceptions
from googlecloudsdk.core.util import encoding
from googlecloudsdk.core.util import files
from googlecloudsdk.core.util import platforms
from googlecloudsdk.core.util import semver
import six
from six.moves import urllib
DOCKER_NOT_FOUND_ERROR = 'Docker is not installed.'
class DockerError(exceptions.Error):
"""Base class for docker errors."""
class DockerConfigUpdateError(DockerError):
"""There was an error updating the docker configuration file."""
class InvalidDockerConfigError(DockerError):
"""The docker configuration file could not be read."""
def _GetUserHomeDir():
if platforms.OperatingSystem.Current() == platforms.OperatingSystem.WINDOWS:
# %HOME% has precedence over %USERPROFILE% for files.GetHomeDir().
# The Docker config resides under %USERPROFILE% on Windows
return encoding.Decode(os.path.expandvars('%USERPROFILE%'))
else:
return files.GetHomeDir()
def _GetNewConfigDirectory():
# Return the value of $DOCKER_CONFIG, if it exists, otherwise ~/.docker
# see https://github.com/moby/moby/blob/master/cli/config/configdir.go
# NOTE: The preceding link is not owned by Google and cannot yet be updated to
# address disrespectful term.
docker_config = encoding.GetEncodedValue(os.environ, 'DOCKER_CONFIG')
if docker_config is not None:
return docker_config
else:
return os.path.join(_GetUserHomeDir(), '.docker')
# Other tools like the python docker library (used by gcloud app)
# also rely on Docker's authorization configuration (in addition
# to the docker CLI client)
# NOTE: Lazy for manipulation of HOME / mocking.
def GetDockerConfigPath(force_new=False):
"""Retrieve the path to Docker's configuration file, noting its format.
Args:
force_new: bool, whether to force usage of the new config file regardless
of whether it exists (for testing).
Returns:
A tuple containing:
-The path to Docker's configuration file, and
-A boolean indicating whether it is in the new (1.7.0+) configuration format
"""
# Starting in Docker 1.7.0, the Docker client moved where it writes
# credentials to ~/.docker/config.json. It is half backwards-compatible,
# if the new file doesn't exist, it falls back on the old file.
# if the new file exists, it IGNORES the old file.
# This is a problem when a user has logged into another registry on 1.7.0
# and then uses 'gcloud docker'.
# This must remain compatible with: https://github.com/docker/docker-py
new_path = os.path.join(_GetNewConfigDirectory(), 'config.json')
if os.path.exists(new_path) or force_new:
return new_path, True
# Only one location will be probed to locate the new config.
# This is consistent with the Docker client's behavior:
# https://github.com/moby/moby/blob/master/cli/config/configdir.go
# NOTE: The preceding link is not owned by Google and cannot yet be updated to
# address disrespectful term.
old_path = os.path.join(_GetUserHomeDir(), '.dockercfg')
return old_path, False
def EnsureDocker(func):
"""Wraps a function that uses subprocess to invoke docker.
Rewrites OS Exceptions when not installed.
Args:
func: A function that uses subprocess to invoke docker.
Returns:
The decorated function.
Raises:
DockerError: Docker cannot be run.
"""
def DockerFunc(*args, **kwargs):
try:
return func(*args, **kwargs)
except OSError as e:
if e.errno == errno.ENOENT:
raise DockerError(DOCKER_NOT_FOUND_ERROR)
else:
raise
return DockerFunc
@EnsureDocker
def Execute(args):
"""Wraps an invocation of the docker client with the specified CLI arguments.
Args:
args: The list of command-line arguments to docker.
Returns:
The exit code from Docker.
"""
return subprocess.call(
['docker'] + args, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr)
@EnsureDocker
def GetDockerProcess(docker_args, stdin_file, stdout_file, stderr_file):
# Wraps the construction of a docker subprocess object with the specified
# arguments and I/O files.
return subprocess.Popen(
['docker'] + docker_args,
stdin=stdin_file,
stdout=stdout_file,
stderr=stderr_file)
def GetDockerVersion():
"""Returns the installed Docker client version.
Returns:
The installed Docker client version.
Raises:
DockerError: Docker cannot be run or does not accept 'docker version
--format '{{.Client.Version}}''.
"""
docker_args = "version --format '{{.Client.Version}}'".split()
docker_p = GetDockerProcess(
docker_args,
stdin_file=sys.stdin,
stdout_file=subprocess.PIPE,
stderr_file=subprocess.PIPE)
# Wait for docker to finished executing and retrieve its stdout/stderr.
stdoutdata, _ = docker_p.communicate()
if docker_p.returncode != 0 or not stdoutdata:
raise DockerError('could not retrieve Docker client version')
# Remove ' from beginning and end of line.
return semver.LooseVersion(stdoutdata.strip("'"))
def GetNormalizedURL(server):
"""Sanitize and normalize the server input."""
parsed_url = urllib.parse.urlparse(server)
# Work around the fact that Python 2.6 does not properly
# look for :// and simply splits on colon, so something
# like 'gcr.io:1234' returns the scheme 'gcr.io'.
if '://' not in server:
# Server doesn't have a scheme, set it to HTTPS.
parsed_url = urllib.parse.urlparse('https://' + server)
if parsed_url.hostname == 'localhost':
# Now that it parses, if the hostname is localhost switch to HTTP.
parsed_url = urllib.parse.urlparse('http://' + server)
return parsed_url
def ReadConfigurationFile(path):
"""Retrieve the full contents of the Docker configuration file.
Args:
path: string, path to configuration file
Returns:
The full contents of the configuration file as parsed JSON dict.
Raises:
ValueError: path is not set.
InvalidDockerConfigError: config file could not be read as JSON.
"""
if not path:
raise ValueError('Docker configuration file path is empty')
# In many cases file might not exist even though Docker is installed,
# so do not treat that as an error, just return empty contents.
if not os.path.exists(path):
return {}
contents = files.ReadFileContents(path)
# If the file is empty, return empty JSON.
# This helps if someone 'touched' the file or manually
# deleted the contents.
if not contents or contents.isspace():
return {}
try:
return json.loads(contents)
except ValueError as err:
raise InvalidDockerConfigError(
('Docker configuration file [{}] could not be read as JSON: '
'{}').format(path, six.text_type(err)))

View File

@@ -0,0 +1,141 @@
# -*- coding: utf-8 -*- #
# Copyright 2014 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Default value constants exposed by core utilities."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
DEFAULT_REGISTRY = 'gcr.io'
REGIONAL_GCR_REGISTRIES = ['us.gcr.io', 'eu.gcr.io', 'asia.gcr.io']
REGIONAL_AR_REGISTRIES = [
'africa-south1-docker.pkg.dev',
'docker.africa-south1.rep.pkg.dev',
'asia-docker.pkg.dev',
'asia-east1-docker.pkg.dev',
'docker.asia-east1.rep.pkg.dev',
'asia-east2-docker.pkg.dev',
'docker.asia-east2.rep.pkg.dev',
'asia-northeast1-docker.pkg.dev',
'docker.asia-northeast1.rep.pkg.dev',
'asia-northeast2-docker.pkg.dev',
'docker.asia-northeast2.rep.pkg.dev',
'asia-northeast3-docker.pkg.dev',
'docker.asia-northeast3.rep.pkg.dev',
'asia-south1-docker.pkg.dev',
'docker.asia-south1.rep.pkg.dev',
'asia-south2-docker.pkg.dev',
'docker.asia-south2.rep.pkg.dev',
'asia-southeast1-docker.pkg.dev',
'docker.asia-southeast1.rep.pkg.dev',
'asia-southeast2-docker.pkg.dev',
'docker.asia-southeast2.rep.pkg.dev',
'australia-southeast1-docker.pkg.dev',
'docker.australia-southeast1.rep.pkg.dev',
'australia-southeast2-docker.pkg.dev',
'docker.australia-southeast2.rep.pkg.dev',
'europe-docker.pkg.dev',
'europe-central2-docker.pkg.dev',
'docker.europe-central2.rep.pkg.dev',
'europe-north1-docker.pkg.dev',
'docker.europe-north1.rep.pkg.dev',
'europe-north2-docker.pkg.dev',
'europe-southwest1-docker.pkg.dev',
'docker.europe-southwest1.rep.pkg.dev',
'europe-west1-docker.pkg.dev',
'docker.europe-west1.rep.pkg.dev',
'europe-west10-docker.pkg.dev',
'docker.europe-west10.rep.pkg.dev',
'europe-west12-docker.pkg.dev',
'docker.europe-west12.rep.pkg.dev',
'europe-west2-docker.pkg.dev',
'docker.europe-west2.rep.pkg.dev',
'europe-west3-docker.pkg.dev',
'docker.europe-west3.rep.pkg.dev',
'europe-west4-docker.pkg.dev',
'docker.europe-west4.rep.pkg.dev',
'europe-west6-docker.pkg.dev',
'docker.europe-west6.rep.pkg.dev',
'europe-west8-docker.pkg.dev',
'docker.europe-west8.rep.pkg.dev',
'europe-west9-docker.pkg.dev',
'docker.europe-west9.rep.pkg.dev',
'me-central1-docker.pkg.dev',
'docker.me-central1.rep.pkg.dev',
'me-central2-docker.pkg.dev',
'docker.me-central2.rep.pkg.dev',
'me-west1-docker.pkg.dev',
'docker.me-west1.rep.pkg.dev',
'northamerica-northeast1-docker.pkg.dev',
'docker.northamerica-northeast1.rep.pkg.dev',
'northamerica-northeast2-docker.pkg.dev',
'docker.northamerica-northeast2.rep.pkg.dev',
'northamerica-south1-docker.pkg.dev',
'southamerica-east1-docker.pkg.dev',
'docker.southamerica-east1.rep.pkg.dev',
'southamerica-west1-docker.pkg.dev',
'docker.southamerica-west1.rep.pkg.dev',
'us-docker.pkg.dev',
'us-central1-docker.pkg.dev',
'docker.us-central1.rep.pkg.dev',
'us-central2-docker.pkg.dev',
'docker.us-central2.rep.pkg.dev',
'us-east1-docker.pkg.dev',
'docker.us-east1.rep.pkg.dev',
'us-east4-docker.pkg.dev',
'docker.us-east4.rep.pkg.dev',
'us-east5-docker.pkg.dev',
'docker.us-east5.rep.pkg.dev',
'us-east7-docker.pkg.dev',
'docker.us-east7.rep.pkg.dev',
'us-south1-docker.pkg.dev',
'docker.us-south1.rep.pkg.dev',
'us-west1-docker.pkg.dev',
'docker.us-west1.rep.pkg.dev',
'us-west2-docker.pkg.dev',
'docker.us-west2.rep.pkg.dev',
'us-west3-docker.pkg.dev',
'docker.us-west3.rep.pkg.dev',
'us-west4-docker.pkg.dev',
'docker.us-west4.rep.pkg.dev',
'us-west8-docker.pkg.dev',
]
AUTHENTICATED_LAUNCHER_REGISTRIES = ['marketplace.gcr.io']
LAUNCHER_REGISTRIES = AUTHENTICATED_LAUNCHER_REGISTRIES + [
'l.gcr.io', 'launcher.gcr.io'
]
LAUNCHER_PROJECT = 'cloud-marketplace'
KUBERNETES_PUSH = 'staging-k8s.gcr.io'
KUBERNETES_READ_ONLY = 'k8s.gcr.io'
# GCR's regional demand-based mirrors of DockerHub.
# These are intended for use with the daemon flag, e.g.
# --registry-mirror=https://mirror.gcr.io
MIRROR_REGISTRIES = [
'us-mirror.gcr.io', 'eu-mirror.gcr.io', 'asia-mirror.gcr.io',
'mirror.gcr.io'
]
MIRROR_PROJECT = 'cloud-containers-mirror'
# These are the registries to authenticatefor by default, during
# `gcloud docker` and `gcloud auth configure-docker`
DEFAULT_REGISTRIES_TO_AUTHENTICATE = ([DEFAULT_REGISTRY] +
REGIONAL_GCR_REGISTRIES +
[KUBERNETES_PUSH] +
AUTHENTICATED_LAUNCHER_REGISTRIES)
ALL_SUPPORTED_REGISTRIES = (
DEFAULT_REGISTRIES_TO_AUTHENTICATE + REGIONAL_AR_REGISTRIES +
LAUNCHER_REGISTRIES + MIRROR_REGISTRIES + [KUBERNETES_READ_ONLY])
DEFAULT_DEVSHELL_IMAGE = (DEFAULT_REGISTRY + '/dev_con/cloud-dev-common:prod')
METADATA_IMAGE = DEFAULT_REGISTRY + '/google_appengine/faux-metadata:latest'

View File

@@ -0,0 +1,225 @@
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility library for configuring docker credential helpers."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import collections
import json
from googlecloudsdk.core.docker import client_lib as client_utils
from googlecloudsdk.core.docker import constants
from googlecloudsdk.core.util import files
from googlecloudsdk.core.util import semver
import six
MIN_DOCKER_CONFIG_HELPER_VERSION = semver.LooseVersion('1.13')
CREDENTIAL_HELPER_KEY = 'credHelpers'
class DockerConfigUpdateError(client_utils.DockerError):
"""Error thrown for issues updating Docker configuration file updates."""
class Configuration(object):
"""Full Docker configuration configuration file and related meta-data."""
def __init__(self, config_data, path=None):
self.contents = config_data
self.path = path
self._version = None # Evaluated lazily.
def __eq__(self, other):
return (self.contents == other.contents and
self.path == other.path)
@classmethod
def FromJson(cls, json_string, path):
"""Build a Configuration object from a JSON string.
Args:
json_string: string, json content for Configuration
path: string, file path to Docker Configuation File
Returns:
a Configuration object
"""
if not json_string or json_string.isspace():
config_dict = {}
else:
config_dict = json.loads(json_string)
return Configuration(config_dict, path)
def ToJson(self):
"""Get this Configuration objects contents as a JSON string."""
return json.dumps(self.contents, indent=2)
def DockerVersion(self):
if not self._version:
version_str = six.text_type(client_utils.GetDockerVersion())
self._version = semver.LooseVersion(version_str)
return self._version
def SupportsRegistryHelpers(self):
"""Returns True unless Docker is confirmed to not support helpers."""
try:
return self.DockerVersion() >= MIN_DOCKER_CONFIG_HELPER_VERSION
except: # pylint: disable=bare-except
# Always fail open.
return True
def GetRegisteredCredentialHelpers(self):
"""Returns credential helpers entry from the Docker config file.
Returns:
'credHelpers' entry if it is specified in the Docker configuration or
empty dict if the config does not contain a 'credHelpers' key.
"""
if self.contents and CREDENTIAL_HELPER_KEY in self.contents:
return {CREDENTIAL_HELPER_KEY: self.contents[CREDENTIAL_HELPER_KEY]}
return {}
def RegisterCredentialHelpers(self, mappings_dict=None):
"""Adds Docker 'credHelpers' entry to this configuration.
Adds Docker 'credHelpers' entry to this configuration and writes updated
configuration to disk.
Args:
mappings_dict: The dict of 'credHelpers' mappings ({registry: handler}) to
add to the Docker configuration. If not set, use the values from
BuildOrderedCredentialHelperRegistries(DefaultAuthenticatedRegistries())
Raises:
ValueError: mappings are not a valid dict.
DockerConfigUpdateError: Configuration does not support 'credHelpers'.
"""
mappings_dict = mappings_dict or BuildOrderedCredentialHelperRegistries(
DefaultAuthenticatedRegistries())
if not isinstance(mappings_dict, dict):
raise ValueError(
'Invalid Docker credential helpers mappings {}'.format(mappings_dict))
if not self.SupportsRegistryHelpers():
raise DockerConfigUpdateError('Credential Helpers not supported for this '
'Docker client version {}'.format(
self.DockerVersion()))
self.contents[CREDENTIAL_HELPER_KEY] = mappings_dict
self.WriteToDisk()
def WriteToDisk(self):
"""Writes Conifguration object to disk."""
try:
files.WriteFileAtomically(self.path, self.ToJson())
except (TypeError, ValueError, OSError, IOError) as err:
raise DockerConfigUpdateError('Error writing Docker configuration '
'to disk: {}'.format(six.text_type(err)))
# Defaulting to new config location since we know minimum version
# for supporting credential helpers is > 1.7.
@classmethod
def ReadFromDisk(cls, path=None):
"""Reads configuration file and meta-data from default Docker location.
Reads configuration file and meta-data from default Docker location. Returns
a Configuration object containing the full contents of the configuration
file, and the configuration file path.
Args:
path: string, path to look for the Docker config file. If empty will
attempt to read from the new config location (default).
Returns:
A Configuration object
Raises:
ValueError: path or is_new_format are not set.
InvalidDockerConfigError: config file could not be read as JSON.
"""
path = path or client_utils.GetDockerConfigPath(True)[0]
try:
content = client_utils.ReadConfigurationFile(path)
except (ValueError, client_utils.DockerError) as err:
raise client_utils.InvalidDockerConfigError(
('Docker configuration file [{}] could not be read as JSON: {}'
).format(path, six.text_type(err)))
return cls(content, path)
def DefaultAuthenticatedRegistries(include_artifact_registry=False):
"""Return list of default gcloud credential helper registires."""
if include_artifact_registry:
return constants.DEFAULT_REGISTRIES_TO_AUTHENTICATE + constants.REGIONAL_AR_REGISTRIES
else:
return constants.DEFAULT_REGISTRIES_TO_AUTHENTICATE
def SupportedRegistries():
"""Return list of gcloud credential helper supported Docker registires."""
return constants.ALL_SUPPORTED_REGISTRIES
def BuildOrderedCredentialHelperRegistries(registries):
"""Returns dict of gcloud helper mappings for the supplied repositories.
Returns ordered dict of Docker registry to gcloud helper mappings for the
supplied list of registries.
Ensures that the order in which credential helper registry entries are
processed is consistent.
Args:
registries: list, the registries to create the mappings for.
Returns:
OrderedDict of Docker registry to gcloud helper mappings.
"""
# Based on Docker credHelper docs this should work on Windows transparently
# so we do not need to register .exe files seperately, see
# https://docs.docker.com/engine/reference/commandline/login/#credential-helpers
return collections.OrderedDict([
(registry, 'gcloud') for registry in registries
])
def GetGcloudCredentialHelperConfig(registries=None,
include_artifact_registry=False):
"""Gets the credHelpers Docker config entry for gcloud supported registries.
Returns a Docker configuration JSON entry that will register gcloud as the
credential helper for all Google supported Docker registries.
Args:
registries: list, the registries to create the mappings for. If not
supplied, will use DefaultAuthenticatedRegistries().
include_artifact_registry: bool, whether to include all Artifact Registry
domains as well as GCR domains registries when called with no list of
registries to add.
Returns:
The config used to register gcloud as the credential helper for all
supported Docker registries.
"""
registered_helpers = BuildOrderedCredentialHelperRegistries(
registries or DefaultAuthenticatedRegistries(include_artifact_registry))
return {CREDENTIAL_HELPER_KEY: registered_helpers}

View File

@@ -0,0 +1,278 @@
# -*- coding: utf-8 -*- #
# Copyright 2014 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility library for configuring access to the Google Container Registry.
Sets docker up to authenticate with the Google Container Registry using the
active gcloud credential.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import base64
import json
import os
import subprocess
import sys
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core.credentials import store
from googlecloudsdk.core.docker import client_lib
from googlecloudsdk.core.docker import constants
from googlecloudsdk.core.util import files
import six
_USERNAME = 'gclouddockertoken'
_EMAIL = 'not@val.id'
_CREDENTIAL_STORE_KEY = 'credsStore'
class UnsupportedRegistryError(client_lib.DockerError):
"""Indicates an attempt to use an unsupported registry."""
def __init__(self, image_url):
self.image_url = image_url
def __str__(self):
return ('{0} is not in a supported registry. Supported registries are '
'{1}'.format(self.image_url, constants.ALL_SUPPORTED_REGISTRIES))
def DockerLogin(server, username, access_token):
"""Register the username / token for the given server on Docker's keyring."""
# Sanitize and normalize the server input.
parsed_url = client_lib.GetNormalizedURL(server)
server = parsed_url.geturl()
# 'docker login' must be used due to the change introduced in
# https://github.com/docker/docker/pull/20107 .
docker_args = ['login']
docker_args.append('--username=' + username)
docker_args.append('--password=' + access_token)
docker_args.append(server) # The auth endpoint must be the last argument.
docker_p = client_lib.GetDockerProcess(
docker_args,
stdin_file=sys.stdin,
stdout_file=subprocess.PIPE,
stderr_file=subprocess.PIPE)
# Wait for docker to finished executing and retrieve its stdout/stderr.
stdoutdata, stderrdata = docker_p.communicate()
if docker_p.returncode == 0:
# If the login was successful, print only unexpected info.
_SurfaceUnexpectedInfo(stdoutdata, stderrdata)
else:
# If the login failed, print everything.
log.error('Docker CLI operation failed:')
log.out.Print(stdoutdata)
log.status.Print(stderrdata)
raise client_lib.DockerError('Docker login failed.')
def _SurfaceUnexpectedInfo(stdoutdata, stderrdata):
"""Reads docker's output and surfaces unexpected lines.
Docker's CLI has a certain amount of chattiness, even on successes.
Args:
stdoutdata: The raw data output from the pipe given to Popen as stdout.
stderrdata: The raw data output from the pipe given to Popen as stderr.
"""
# Split the outputs by lines.
stdout = [s.strip() for s in stdoutdata.splitlines()]
stderr = [s.strip() for s in stderrdata.splitlines()]
for line in stdout:
# Swallow 'Login Succeeded' and 'saved in,' surface any other std output.
if (line != 'Login Succeeded') and (
'login credentials saved in' not in line):
line = '%s%s' % (line, os.linesep)
log.out.Print(line) # log.out => stdout
for line in stderr:
if not _IsExpectedErrorLine(line):
line = '%s%s' % (line, os.linesep)
log.status.Print(line) # log.status => stderr
def _CredentialStoreConfigured():
"""Returns True if a credential store is specified in the docker config.
Returns:
True if a credential store is specified in the docker config.
False if the config file does not exist or does not contain a
'credsStore' key.
"""
try:
# Not Using DockerConfigInfo here to be backward compatiable with
# UpdateDockerCredentials which should still work if Docker is not installed
path, is_new_format = client_lib.GetDockerConfigPath()
contents = client_lib.ReadConfigurationFile(path)
if is_new_format:
return _CREDENTIAL_STORE_KEY in contents
else:
# The old format is for Docker <1.7.0.
# Older Docker clients (<1.11.0) don't support credential helpers.
return False
except IOError:
# Config file doesn't exist.
return False
def ReadDockerAuthConfig():
"""Retrieve the contents of the Docker authorization entry.
NOTE: This is public only to facilitate testing.
Returns:
The map of authorizations used by docker.
"""
# Not using DockerConfigInfo here to be backward compatible with
# UpdateDockerCredentials which should still work if Docker is not installed
path, new_format = client_lib.GetDockerConfigPath()
structure = client_lib.ReadConfigurationFile(path)
if new_format:
return structure['auths'] if 'auths' in structure else {}
else:
return structure
def WriteDockerAuthConfig(structure):
"""Write out a complete set of Docker authorization entries.
This is public only to facilitate testing.
Args:
structure: The dict of authorization mappings to write to the
Docker configuration file.
"""
# Not using DockerConfigInfo here to be backward compatible with
# UpdateDockerCredentials which should still work if Docker is not installed
path, is_new_format = client_lib.GetDockerConfigPath()
contents = client_lib.ReadConfigurationFile(path)
if is_new_format:
full_cfg = contents
full_cfg['auths'] = structure
file_contents = json.dumps(full_cfg, indent=2)
else:
file_contents = json.dumps(structure, indent=2)
files.WriteFileAtomically(path, file_contents)
def UpdateDockerCredentials(server, refresh=True):
"""Updates the docker config to have fresh credentials.
This reads the current contents of Docker's keyring, and extends it with
a fresh entry for the provided 'server', based on the active gcloud
credential. If a credential exists for 'server' this replaces it.
Args:
server: The hostname of the registry for which we're freshening
the credential.
refresh: Whether to force a token refresh on the active credential.
Raises:
core.credentials.exceptions.Error: There was an error loading the
credentials.
"""
if refresh:
access_token = store.GetFreshAccessToken()
else:
access_token = store.GetAccessToken()
if not access_token:
raise exceptions.Error(
'No access token could be obtained from the current credentials.')
if _CredentialStoreConfigured():
try:
# Update the credentials stored by docker, passing the sentinel username
# and access token.
DockerLogin(server, _USERNAME, access_token)
except client_lib.DockerError as e:
# Only catch docker-not-found error
if six.text_type(e) != client_lib.DOCKER_NOT_FOUND_ERROR:
raise
# Fall back to the previous manual .dockercfg manipulation
# in order to support gcloud app's docker-binaryless use case.
_UpdateDockerConfig(server, _USERNAME, access_token)
log.warning(
"'docker' was not discovered on the path. Credentials have been "
'stored, but are not guaranteed to work with the Docker client '
' if an external credential store is configured.')
else:
_UpdateDockerConfig(server, _USERNAME, access_token)
def _UpdateDockerConfig(server, username, access_token):
"""Register the username / token for the given server on Docker's keyring."""
# NOTE: using "docker login" doesn't work as they're quite strict on what
# is allowed in username/password.
try:
dockercfg_contents = ReadDockerAuthConfig()
except (IOError, client_lib.InvalidDockerConfigError):
# If the file doesn't exist, start with an empty map.
dockercfg_contents = {}
# Add the entry for our server.
auth = username + ':' + access_token
auth = base64.b64encode(auth.encode('ascii')).decode('ascii')
# Sanitize and normalize the server input.
parsed_url = client_lib.GetNormalizedURL(server)
server = parsed_url.geturl()
server_unqualified = parsed_url.hostname
# Clear out any unqualified stale entry for this server
if server_unqualified in dockercfg_contents:
del dockercfg_contents[server_unqualified]
dockercfg_contents[server] = {'auth': auth, 'email': _EMAIL}
WriteDockerAuthConfig(dockercfg_contents)
def _IsExpectedErrorLine(line):
"""Returns whether or not the given line was expected from the Docker client.
Args:
line: The line received in stderr from Docker
Returns:
True if the line was expected, False otherwise.
"""
expected_line_substrs = [
# --email is deprecated
'--email',
# login success
'login credentials saved in',
# Use stdin for passwords
'WARNING! Using --password via the CLI is insecure. Use --password-stdin.'
]
for expected_line_substr in expected_line_substrs:
if expected_line_substr in line:
return True
return False