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,56 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""The command group for cloud container images."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA,
base.ReleaseTrack.GA)
class Images(base.Group):
"""List and manipulate Google Container Registry images."""
category = base.COMPUTE_CATEGORY
@staticmethod
def Args(parser):
"""Add arguments to the parser.
Args:
parser: argparse.ArgumentParser, This is a standard argparser parser with
which you can register arguments. See the public argparse documentation
for its capabilities.
"""
pass
def Filter(self, context, args):
"""Modify the context that will be given to this group's commands when run.
Args:
context: {str:object}, A set of key-value pairs that can be used for
common initialization among commands.
args: argparse.Namespace: The same namespace given to the corresponding
.Run() invocation.
Returns:
The refined command context.
"""
base.RequireProjectID(args)
return context

View File

@@ -0,0 +1,135 @@
# -*- coding: utf-8 -*- #
# Copyright 2016 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.
"""Add tag command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from containerregistry.client import docker_name
from containerregistry.client.v2 import docker_image as v2_image
from containerregistry.client.v2 import docker_session as v2_session
from containerregistry.client.v2_2 import docker_http
from containerregistry.client.v2_2 import docker_image as v2_2_image
from containerregistry.client.v2_2 import docker_image_list
from containerregistry.client.v2_2 import docker_session as v2_2_session
from googlecloudsdk.api_lib.container.images import util
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container import flags
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core.console import console_io
import six
class Create(base.CreateCommand):
"""Adds tags to existing image."""
detailed_help = {
'DESCRIPTION':
"""\
The container images add-tag command adds the tag(s) specified in
the second (and following) tag parameter(s) to the image referenced
in the first tag parameter. Repositories must be hosted by the
Google Container Registry.
""",
'EXAMPLES':
"""\
Add a tag to another tag:
$ {command} gcr.io/myproject/myimage:mytag1
gcr.io/myproject/myimage:mytag2
Add a tag to a digest
$ {command} gcr.io/myproject/myimage@sha256:digest
gcr.io/myproject/myimage:mytag2
Add a tag to latest
$ {command} gcr.io/myproject/myimage
gcr.io/myproject/myimage:mytag2
Promote a tag to latest
$ {command} gcr.io/myproject/myimage:mytag1
gcr.io/myproject/myimage:latest
""",
}
@staticmethod
def Args(parser):
flags.AddTagOrDigestPositional(
parser, arg_name='src_image', verb='add tags for', repeated=False)
flags.AddTagOrDigestPositional(
parser,
arg_name='dest_image',
verb='be the new tags',
repeated=True,
tags_only=True)
def Run(self, args):
# pylint: disable=missing-docstring
def Push(image, dest_names, creds, http_obj, session_push_type):
for dest_name in dest_names:
with session_push_type(dest_name, creds, http_obj) as push:
push.upload(image)
log.CreatedResource(dest_name)
http_obj = util.Http()
src_name = util.GetDockerImageFromTagOrDigest(args.src_image)
dest_names = []
for dest_image in args.dest_image:
try:
dest_name = docker_name.Tag(dest_image)
except docker_name.BadNameException as e:
raise util.InvalidImageNameError(six.text_type(e))
if '/' not in dest_name.repository:
raise exceptions.Error(
'Pushing to project root-level images is disabled. '
'Please designate an image within a project, '
'e.g. gcr.io/project-id/my-image:tag')
dest_names.append(dest_name)
console_io.PromptContinue(
'This will tag {} with:\n{}'.format(
src_name,
'\n'.join(six.text_type(dest_name) for dest_name in dest_names)),
default=True,
cancel_on_no=True)
creds = util.CredentialProvider()
with util.WrapExpectedDockerlessErrors():
with docker_image_list.FromRegistry(src_name, creds,
http_obj) as manifest_list:
if manifest_list.exists():
Push(manifest_list, dest_names, creds, http_obj, v2_2_session.Push)
return
with v2_2_image.FromRegistry(
src_name,
creds,
http_obj,
accepted_mimes=docker_http.SUPPORTED_MANIFEST_MIMES) as v2_2_img:
if v2_2_img.exists():
Push(v2_2_img, dest_names, creds, http_obj, v2_2_session.Push)
return
with v2_image.FromRegistry(src_name, creds, http_obj) as v2_img:
Push(v2_img, dest_names, creds, http_obj, v2_session.Push)

View File

@@ -0,0 +1,195 @@
# -*- coding: utf-8 -*- #
# Copyright 2016 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.
"""Delete images command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from containerregistry.client import docker_name
from containerregistry.client.v2_2 import docker_session
from googlecloudsdk.api_lib.container.images import util
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container import flags
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.resource import resource_printer
import six
class Delete(base.DeleteCommand):
"""Delete existing images.
The container images delete command of gcloud deletes a specified
image and tags in a specified repository. Repositories
must be hosted by the Google Container Registry.
"""
detailed_help = {
'DESCRIPTION':
"""\
The container images delete command deletes the specified image from
the registry. All associated tags are also deleted.
""",
'EXAMPLES':
"""\
Deletes the image as long as there aren't additional, unspecified tags
referencing it:
$ {command} <IMAGE_NAME>
Deletes the image (and tags) from the input IMAGE_NAME:
$ {command} <IMAGE_NAME> --force-delete-tags
Deletes the image (and tags) from the input IMAGE_NAME, without
additional prompting:
$ {command} <IMAGE_NAME> --force-delete-tags --quiet
To easily identify and delete untagged images in a project, first
filter digests that lack tags:
$ gcloud container images list-tags [HOSTNAME]/[PROJECT-ID]/[IMAGE]\
--filter='-tags:*' --format="get(digest)" --limit=$BIG_NUMBER
Then, delete these tagless images without prompting by running:
$ {command} [HOSTNAME]/[PROJECT-ID]/[IMAGE]@DIGEST --quiet
""",
}
@staticmethod
def Args(parser):
"""Register flags for this command.
Args:
parser: An argparse.ArgumentParser-like object. It is mocked out in order
to capture some information, but behaves like an ArgumentParser.
"""
flags.AddTagOrDigestPositional(parser, verb='delete')
parser.add_argument(
'--force-delete-tags',
action='store_true',
default=False,
help=(
'If there are tags pointing to an image to be deleted then they '
'must all be specified explicitly, or this flag must be specified, '
'for the command to succeed.'))
def Run(self, args):
"""This is what ts called when the user runs this command.
Args:
args: an argparse namespace. All the arguments that were provided to this
command invocation.
Raises:
InvalidImageNameError: If the user specified an invalid image name.
Returns:
A list of the deleted docker_name.Tag and docker_name.Digest objects
"""
# IMAGE_NAME: The fully-qualified image name to delete (with a digest).
# Deletes the layers. Ex. gcr.io/google-appengine/java(@DIGEST|:TAG).
http_obj = util.Http()
with util.WrapExpectedDockerlessErrors():
# collect input/validate
digests, explicit_tags = self._ProcessImageNames(args.image_names)
# Resolve tags to digests.
for tag in explicit_tags:
digests.add(util.GetDigestFromName(six.text_type(tag)))
# Find all the tags that reference digests to be deleted.
all_tags = set()
for digest in digests:
all_tags.update(util.GetDockerTagsForDigest(digest, http_obj))
# Find all the tags that weren't specified explicitly.
implicit_tags = all_tags.difference(explicit_tags)
if implicit_tags and not args.force_delete_tags:
log.error('Tags:')
for tag in explicit_tags:
log.error('- ' + six.text_type(tag))
raise exceptions.Error(
'This operation will implicitly delete the tags listed above. '
'Please manually remove with the `untag` command or re-run with '
'--force-delete-tags to confirm.')
# Print the digests to be deleted.
if digests:
log.status.Print('Digests:')
for digest in digests:
self._PrintDigest(digest, http_obj)
# Print the tags to be deleted.
if explicit_tags:
log.status.Print('Tags:')
for tag in explicit_tags:
log.status.Print('- ' + six.text_type(tag))
# Prompt the user for consent to delete all the above.
console_io.PromptContinue(
'This operation will delete the tags and images identified by the '
'digests above.',
default=True,
cancel_on_no=True)
# The user has given explicit consent, merge the tags.
explicit_tags.update(implicit_tags)
# delete and collect output
result = []
for tag in explicit_tags: # tags must be deleted before digests
self._DeleteDockerTagOrDigest(tag, http_obj)
result.append({'name': six.text_type(tag)})
for digest in digests:
self._DeleteDockerTagOrDigest(digest, http_obj)
result.append({'name': six.text_type(digest)})
return result
def _ProcessImageNames(self, image_names):
digests = set()
tags = set()
for image_name in image_names:
docker_obj = util.GetDockerImageFromTagOrDigest(image_name)
if isinstance(docker_obj, docker_name.Digest):
digests.add(docker_obj)
elif isinstance(docker_obj, docker_name.Tag):
if not util.IsFullySpecified(image_name):
log.warning('Implicit ":latest" tag specified: ' + image_name)
tags.add(docker_obj)
return [digests, tags]
def _DeleteDockerTagOrDigest(self, tag_or_digest, http_obj):
docker_session.Delete(
creds=util.CredentialProvider(), name=tag_or_digest, transport=http_obj)
log.DeletedResource(tag_or_digest)
def _PrintDigest(self, digest, http_obj):
log.status.Print('- ' + six.text_type(digest))
self._DisplayDigestTags(digest, http_obj)
def _DisplayDigestTags(self, digest, http_obj):
tag_list = util.GetTagNamesForDigest(digest, http_obj)
if not tag_list: # no tags on this digest, skip delete prompt
return
fmt = ('list[title=" Associated tags:"]')
resource_printer.Print(tag_list, fmt, out=log.status)

View File

@@ -0,0 +1,273 @@
# -*- coding: utf-8 -*- #
# Copyright 2016 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.
"""Command to show Container Analysis Data for a specified image."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import re
from containerregistry.client import docker_name
from googlecloudsdk.api_lib.container.images import container_data_util
from googlecloudsdk.api_lib.container.images import util
from googlecloudsdk.api_lib.containeranalysis import filter_util
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.artifacts import requests as ar_requests
from googlecloudsdk.command_lib.container import flags
from googlecloudsdk.core import log
# Add to this as we add more container analysis data.
_DEFAULT_KINDS = [
'BUILD',
'VULNERABILITY',
'IMAGE',
'DEPLOYMENT',
'DISCOVERY',
]
# Includes support for domain scoped projects like
# google.com/project/gcr.io/image
GCR_REPO_REGEX = r'^(?P<project>([^\/]+\.[^\/]+\/)?([^\/\.]+))\/(?P<repo>(us\.|eu\.|asia\.)?gcr.io)\/(?P<image>.*)'
def MaybeConvertToGCR(image_name):
"""Converts gcr.io repos on AR from pkg.dev->gcr.io.
Args:
image_name: Image to convert to GCR.
Returns:
The same image_name, but maybe in GCR format.
"""
if 'pkg.dev' not in image_name.registry:
return image_name
# "repository" here refers to the docker definition, which would be called
# "package" in AR
matches = re.match(GCR_REPO_REGEX, image_name.repository)
if not matches:
return image_name
messages = ar_requests.GetMessages()
settings = ar_requests.GetProjectSettings(matches.group('project'))
if (
settings.legacyRedirectionState
== messages.ProjectSettings.LegacyRedirectionStateValueValuesEnum.REDIRECTION_FROM_GCR_IO_DISABLED
):
log.warning(
'gcr.io repositories in Artifact Registry are only scanned if'
' redirected. Redirect this project before checking scanning results'
)
return image_name
log.warning(
'Container Analysis API uses the gcr.io hostname for scanning results of'
' gcr.io repositories. Using https://{}/{} instead...'.format(
matches.group('repo'), matches.group('project')
)
)
return docker_name.Digest(
'{registry}/{repository}@{sha256}'.format(
registry=matches.group('repo'),
repository='{}/{}'.format(
matches.group('project'), matches.group('image')
),
sha256=image_name.digest,
)
)
def _CommonArgs(parser):
flags.AddTagOrDigestPositional(parser, verb='describe', repeated=False)
# pylint: disable=line-too-long
@base.ReleaseTracks(base.ReleaseTrack.GA)
class Describe(base.DescribeCommand):
r"""Lists information about the specified image.
## EXAMPLES
Describe the specified image:
$ {command} gcr.io/myproject/myimage@digest
Or:
$ {command} gcr.io/myproject/myimage:tag
Find the digest for a tag:
$ {command} gcr.io/myproject/myimage:tag \
--format="value(image_summary.digest)"
Or:
$ {command} gcr.io/myproject/myimage:tag \
--format="value(image_summary.fully_qualified_digest)"
"""
@staticmethod
def Args(parser):
_CommonArgs(parser)
def Run(self, args):
"""This is what gets called when the user runs this command.
Args:
args: an argparse namespace. All the arguments that were provided to this
command invocation.
Raises:
InvalidImageNameError: If the user specified an invalid image name.
Returns:
Some value that we want to have printed later.
"""
with util.WrapExpectedDockerlessErrors(args.image_name):
img_name = MaybeConvertToGCR(util.GetDigestFromName(args.image_name))
return container_data_util.ContainerData(
registry=img_name.registry,
repository=img_name.repository,
digest=img_name.digest)
@base.ReleaseTracks(base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA)
class DescribeAlphaAndBeta(Describe):
r"""Lists container analysis data for a given image.
Lists container analysis data for a valid image.
## EXAMPLES
Describe the specified image:
$ {command} gcr.io/myproject/myimage@digest
Or:
$ {command} gcr.io/myproject/myimage:tag
Find the digest for a tag:
$ {command} gcr.io/myproject/myimage:tag \
--format="value(image_summary.digest)"
Or:
$ {command} gcr.io/myproject/myimage:tag \
--format="value(image_summary.fully_qualified_digest)"
See package vulnerabilities found by the Container Analysis API for the
specified image:
$ {command} gcr.io/myproject/myimage@digest --show-package-vulnerability
"""
@staticmethod
def Args(parser):
_CommonArgs(parser)
# TODO(b/116048537): Refactor these flags to comply with gcloud style.
parser.add_argument(
'--metadata-filter',
default='',
help=('Additional filter to fetch metadata for '
'a given fully qualified image reference.'))
parser.add_argument(
'--show-build-details',
action='store_true',
help='Include build metadata in the output.')
parser.add_argument(
'--show-package-vulnerability',
action='store_true',
help='Include vulnerability metadata in the output.')
parser.add_argument(
'--show-image-basis',
action='store_true',
help='Include base image metadata in the output.')
parser.add_argument(
'--show-deployment',
action='store_true',
help='Include deployment metadata in the output.')
parser.add_argument(
'--show-all-metadata',
action='store_true',
help='Include all metadata in the output.')
def Run(self, args):
"""This is what gets called when the user runs this command.
Args:
args: an argparse namespace. All the arguments that were provided to this
command invocation.
Raises:
InvalidImageNameError: If the user specified an invalid image name.
Returns:
Some value that we want to have printed later.
"""
filter_kinds = []
if args.show_build_details:
filter_kinds.append('BUILD')
if args.show_package_vulnerability:
filter_kinds.append('VULNERABILITY')
filter_kinds.append('DISCOVERY')
if args.show_image_basis:
filter_kinds.append('IMAGE')
if args.show_deployment:
filter_kinds.append('DEPLOYMENT')
if args.show_all_metadata:
filter_kinds = _DEFAULT_KINDS
if filter_kinds or args.metadata_filter:
f = filter_util.ContainerAnalysisFilter()
f.WithKinds(filter_kinds)
f.WithCustomFilter(args.metadata_filter)
with util.WrapExpectedDockerlessErrors(args.image_name):
img_name = MaybeConvertToGCR(util.GetDigestFromName(args.image_name))
# The filter needs the image name with the digest, because that's
# what it matches against in the API.
f.WithResources(['https://{}'.format(img_name)])
data = util.TransformContainerAnalysisData(img_name, f)
# Clear out fields that weren't asked for and have no data.
if (not data.build_details_summary.build_details and
not args.show_build_details and not args.show_all_metadata):
del data.build_details_summary
if (not data.package_vulnerability_summary.vulnerabilities and
not args.show_package_vulnerability and not args.show_all_metadata):
del data.package_vulnerability_summary
if (not data.discovery_summary.discovery and
not args.show_package_vulnerability and not args.show_all_metadata):
del data.discovery_summary
if (not data.image_basis_summary.base_images and
not args.show_image_basis and not args.show_all_metadata):
del data.image_basis_summary
if (not data.deployment_summary.deployments and
not args.show_deployment and not args.show_all_metadata):
del data.deployment_summary
return data
else:
with util.WrapExpectedDockerlessErrors(args.image_name):
img_name = MaybeConvertToGCR(util.GetDigestFromName(args.image_name))
return container_data_util.ContainerData(
registry=img_name.registry,
repository=img_name.repository,
digest=img_name.digest)

View File

@@ -0,0 +1,116 @@
# -*- coding: utf-8 -*- #
# Copyright 2016 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.
"""List images command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from containerregistry.client.v2_2 import docker_image
from googlecloudsdk.api_lib.container.images import util
from googlecloudsdk.calliope import base
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
class List(base.ListCommand):
"""List existing images."""
detailed_help = {
'DESCRIPTION':
"""\
The container images list command of gcloud lists metadata about
existing container images in a specified repository. Repositories
must be hosted by the Google Container Registry.
""",
'EXAMPLES':
"""\
List the images in a specified repository:
$ {command} --repository=gcr.io/myproject
List the images in the default repository:
$ {command}
List images with names prefixed with 'test-project':
$ {command} --filter="name:test-project"
""",
}
@staticmethod
def Args(parser):
"""Register flags for this command.
Args:
parser: An argparse.ArgumentParser-like object. It is mocked out in order
to capture some information, but behaves like an ArgumentParser.
"""
parser.add_argument(
'--repository',
required=False,
help=('The name of the repository. Format: *.gcr.io/repository. '
'Defaults to gcr.io/<project>, for the active project.'))
parser.display_info.AddFormat('table(name)')
def Run(self, args):
"""This is what gets called when the user runs this command.
Args:
args: an argparse namespace. All the arguments that were provided to this
command invocation.
Returns:
Some value that we want to have printed later.
Raises:
exceptions.Error: If the repository could not be found, or access was
denied.
docker_http.V2DiagnosticException: Any other error occurred while
accessing GCR.
"""
repository_arg = args.repository
self._epilog = None
if not repository_arg:
project_id = properties.VALUES.core.project.Get(required=True)
# Handle domain-scoped projects...
project_id = project_id.replace(':', '/', 1)
repository_arg = 'gcr.io/{0}'.format(project_id)
self._epilog = 'Only listing images in {0}. '.format(repository_arg)
self._epilog += 'Use --repository to list images in other repositories.'
# Throws if invalid.
repository = util.ValidateRepositoryPath(repository_arg)
def _DisplayName(c):
"""Display the fully-qualified name."""
return '{0}/{1}'.format(repository, c)
http_obj = util.Http()
with util.WrapExpectedDockerlessErrors(repository):
with docker_image.FromRegistry(
basic_creds=util.CredentialProvider(),
name=repository,
transport=http_obj) as r:
images = [{'name': _DisplayName(c)} for c in r.children()]
return images
def Epilog(self, resources_were_displayed=True):
super(List, self).Epilog(resources_were_displayed)
if self._epilog:
log.status.Print(self._epilog)

View File

@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Command to list Container Registry usage."""
import frozendict
from googlecloudsdk.api_lib.container.images import gcr_utils
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.asset import flags as asset_flags
from googlecloudsdk.command_lib.asset import utils as asset_utils
_DETAILED_HELP = frozendict.frozendict({
'DESCRIPTION': '{description}',
'EXAMPLES': """ \
To list Container Registry usage in a project:
$ {command} --project=my-proj
To list Container Registry usage in an organization:
$ {command} --organization=my-org
To list Container Registry usage in a folder:
$ {command} --folder=my-folder
To list all active Container Registry usage in an organization:
$ {command} --organization=my-org --filter="usage=ACTIVE"
To list all projects that aren't redirected yet:
$ {command} --organization=my-org --filter="usage!=REDIRECTED"
""",
})
@base.ReleaseTracks(base.ReleaseTrack.GA)
class ListGCRUsage(base.ListCommand):
"""List Container Registry usage.
List Container Registry usage for all projects in the specified scope
(project/organization/folder). Caller must have
`cloudasset.assets.searchAllResources` permission on the requested scope and
`storage.objects.list permission` on the Cloud Storage buckets used by
Container Registry.
The tool returns the following lists of usage states:
ACTIVE: Container Registry usage has occurred in the last 30 days. The host
location and project are not redirected.
INACTIVE: No Container Registry usage has occurred in the last 30 days. The
host location and project are not redirected.
REDIRECTED: The project has been redirected to Artifact Registry but still has
Container Registry Cloud Storage buckets. This project will continue to
function after Container Registry is turned down and no further action is
required. You can reduce costs by deleting the Container Registry Cloud
Storage buckets.
REDIRECTION_INCOMPLETE: Requests are redirected to Artifact Registry, but data
is still being copied from Container Registry.
LEGACY: Container Registry usage is unknown. This state is caused by legacy
Container Registry projects that store container image metadata files in Cloud
Storage buckets. For more information on legacy Container Registry projects,
see
https://cloud.google.com/container-registry/docs/deprecations/feature-deprecations#container_image_metadata_storage_change.
"""
detailed_help = _DETAILED_HELP
@staticmethod
def Args(parser):
asset_flags.AddParentArgs(
parser,
'Project ID.',
'Organization ID.',
'Folder ID.',
)
base.URI_FLAG.RemoveFromParser(parser)
def Run(self, args):
parent = asset_utils.GetParentNameForExport(
args.organization,
args.project,
args.folder,
)
gcr_repos = gcr_utils.ListGCRRepos(parent)
for gcr_repo in gcr_repos:
yield gcr_utils.CheckGCRUsage(gcr_repo)

View File

@@ -0,0 +1,219 @@
# -*- coding: utf-8 -*- #
# Copyright 2016 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.
"""List tags command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import heapq
import sys
from containerregistry.client.v2_2 import docker_image
from googlecloudsdk.api_lib.container.images import util
from googlecloudsdk.api_lib.containeranalysis import filter_util
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container import flags
from googlecloudsdk.core import exceptions
# Add to this as we add columns.
_DEFAULT_KINDS = [
'BUILD',
'IMAGE',
'DISCOVERY',
]
# How many images for which to report vulnerabilities, by default. These are
# always the most recent images, regardless of sorting.
_DEFAULT_SHOW_OCCURRENCES_FROM = 10
# By default return the most recent timestamps.
# (The --sort-by flag uses syntax `~X` to mean "sort descending on field X.")
_DEFAULT_SORT_BY = '~timestamp'
_TAGS_FORMAT = """
table(
digest.slice(7:19).join(''),
tags.list(),
timestamp.date():optional,
BUILD.build.provenance.sourceProvenance.context.cloudRepo.revisionId.notnull().list().slice(:8).join(''):optional:label=GIT_SHA,
vuln_counts.list():optional:label=VULNERABILITIES,
IMAGE.image.sort(distance).map().extract(baseResourceUrl).slice(:1).map().list().list().split('//').slice(1:).list().split('@').slice(:1).list():optional:label=FROM,
BUILD.build.provenance.id.notnull().list():optional:label=BUILD,
DISCOVERY[0].discovered.analysisStatus:optional:label=VULNERABILITY_SCAN_STATUS
)
"""
class ArgumentError(exceptions.Error):
"""For missing required mutually inclusive flags."""
pass
@base.ReleaseTracks(base.ReleaseTrack.GA)
class ListTagsGA(base.ListCommand):
"""List tags and digests for the specified image."""
detailed_help = {
'DESCRIPTION':
"""\
The container images list-tags command of gcloud lists metadata about
tags and digests for the specified container image. Images must be
hosted by the Google Container Registry.
""",
'EXAMPLES':
"""\
List the tags in a specified image:
$ {command} gcr.io/myproject/myimage
To receive the full, JSON-formatted output (with untruncated digests):
$ {command} gcr.io/myproject/myimage --format=json
To list digests without corresponding tags:
$ {command} gcr.io/myproject/myimage --filter="NOT tags:*"
To list images that have a tag with the value '30e5504145':
$ gcloud container images list-tags --filter="'tags:30e5504145'"
The last example encloses the filter expression in single quotes
because the value '30e5504145' could be interpreted as a number in
scientific notation.
""",
}
@staticmethod
def Args(parser):
"""Register flags for this command.
Args:
parser: An argparse.ArgumentParser-like object. It is mocked out in order
to capture some information, but behaves like an ArgumentParser.
"""
flags.AddImagePositional(parser, verb='list tags for')
base.SORT_BY_FLAG.SetDefault(parser, _DEFAULT_SORT_BY)
# Does nothing for us, included in base.ListCommand
base.URI_FLAG.RemoveFromParser(parser)
parser.display_info.AddFormat(_TAGS_FORMAT)
def Run(self, args):
"""This is what gets called when the user runs this command.
Args:
args: an argparse namespace. All the arguments that were provided to this
command invocation.
Raises:
InvalidImageNameError: If the user specified an invalid image name.
Returns:
Some value that we want to have printed later.
"""
repository = util.ValidateRepositoryPath(args.image_name)
http_obj = util.Http()
with util.WrapExpectedDockerlessErrors(repository):
with docker_image.FromRegistry(
basic_creds=util.CredentialProvider(),
name=repository,
transport=http_obj) as image:
manifests = image.manifests()
return util.TransformManifests(manifests, repository)
@base.ReleaseTracks(base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA)
class ListTagsALPHAandBETA(ListTagsGA, base.ListCommand):
"""List tags and digests for the specified image."""
@staticmethod
def Args(parser):
"""Register flags for this command.
Args:
parser: An argparse.ArgumentParser-like object. It is mocked out in order
to capture some information, but behaves like an ArgumentParser.
"""
# Weird syntax, but this is how to call a static base method from the
# derived method in Python.
super(ListTagsALPHAandBETA, ListTagsALPHAandBETA).Args(parser)
# TODO(b/116048537): Refactor these flags to comply with gcloud style.
parser.add_argument(
'--show-occurrences',
action='store_true',
default=True,
help='Whether to show summaries of the various Occurrence types.')
parser.add_argument(
'--occurrence-filter',
default=' OR '.join(
['kind = "{kind}"'.format(kind=x) for x in _DEFAULT_KINDS]),
help='A filter for the Occurrences which will be summarized.')
parser.add_argument(
'--show-occurrences-from',
type=arg_parsers.BoundedInt(1, sys.maxsize, unlimited=True),
default=_DEFAULT_SHOW_OCCURRENCES_FROM,
help=('How many of the most recent images for which to summarize '
'Occurences.'))
def Run(self, args):
"""This is what gets called when the user runs this command.
Args:
args: an argparse namespace. All the arguments that were provided to this
command invocation.
Raises:
ArgumentError: If the user provided the flag --show-occurrences-from but
--show-occurrences=False.
InvalidImageNameError: If the user specified an invalid image name.
Returns:
Some value that we want to have printed later.
"""
# Verify that --show-occurrences-from is set iff --show-occurrences=True.
if args.IsSpecified('show_occurrences_from') and not args.show_occurrences:
raise ArgumentError(
'--show-occurrences-from may only be set if --show-occurrences=True')
repository = util.ValidateRepositoryPath(args.image_name)
http_obj = util.Http()
with util.WrapExpectedDockerlessErrors(repository):
with docker_image.FromRegistry(
basic_creds=util.CredentialProvider(),
name=repository,
transport=http_obj) as image:
manifests = image.manifests()
# Only consider the top _DEFAULT_SHOW_OCCURRENCES_FROM images
# to reduce computation time.
most_recent_resource_urls = None
occ_filter = filter_util.ContainerAnalysisFilter()
occ_filter.WithCustomFilter(args.occurrence_filter)
occ_filter.WithResourcePrefixes(['https://{}'.format(repository)])
if args.show_occurrences_from:
# This block is skipped when the user provided
# --show-occurrences-from=unlimited on the CLI.
most_recent_resource_urls = [
'https://%s@%s' % (args.image_name, k) for k in heapq.nlargest(
args.show_occurrences_from,
manifests, key=lambda k: manifests[k]['timeCreatedMs'])
]
occ_filter.WithResources(most_recent_resource_urls)
return util.TransformManifests(
manifests,
repository,
show_occurrences=args.show_occurrences,
occurrence_filter=occ_filter)

View File

@@ -0,0 +1,138 @@
# -*- 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.
"""Untag images command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from containerregistry.client import docker_name
from containerregistry.client.v2_2 import docker_session
from googlecloudsdk.api_lib.container.images import util
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.container import flags
from googlecloudsdk.core import log
from googlecloudsdk.core.console import console_io
import six
class Untag(base.DeleteCommand):
"""Remove existing image tags.
The container images untag command of gcloud deletes a specified
tag on a specified image. Repositories must be hosted by the
Google Container Registry.
"""
detailed_help = {
'DESCRIPTION':
"""\
The container images untag command removes the specified tag
from the image.
""",
'EXAMPLES':
"""\
Removes the tag from the input IMAGE_NAME:
$ {command} <IMAGE_NAME>
""",
}
@staticmethod
def Args(parser):
"""Register flags for this command.
Args:
parser: An argparse.ArgumentParser-like object. It is mocked out in order
to capture some information, but behaves like an ArgumentParser.
"""
flags.AddTagOrDigestPositional(parser, verb='untag', tags_only=True)
def Run(self, args):
"""This is what is called when the user runs this command.
Args:
args: an argparse namespace. All the arguments that were provided to this
command invocation.
Raises:
util.InvalidImageNameError: If the user specified an invalid
(or non-existent) image name.
Returns:
A list of the deleted docker_name.Tag objects
"""
# IMAGE_NAME: The fully-qualified image name to delete (with a tag).
# Removes the tag from the image. Ex. gcr.io/google-appengine/java:TAG.
http_obj = util.Http()
# collect input/validate
tags = self._ParseArgs(args.image_names)
digests = dict()
with util.WrapExpectedDockerlessErrors():
for tag in tags:
try:
# Resolve tags to digests. Throws InvalidImageNameError on 404.
digests[tag] = util.GetDigestFromName(six.text_type(tag))
except util.InvalidImageNameError:
# We already validated the image string in _ParseArgs, this is a 404
raise util.InvalidImageNameError(
'Image could not be found: [{}]'.format(six.text_type(tag)))
if not tags:
log.warning('No tags found matching image names [%s].',
', '.join(args.image_names))
return
for tag, digest in six.iteritems(digests):
log.status.Print('Tag: [{}]'.format(six.text_type(tag)))
log.status.Print('- referencing digest: [{}]'.format(
six.text_type(digest)))
log.status.Print('')
console_io.PromptContinue(
'This operation will remove the above tags. '
'Tag removals only delete the tags; '
'The underlying image layers (referenced by the above digests) will '
'continue to exist.',
cancel_on_no=True)
# delete and collect output
result = []
for tag in tags:
self._DeleteDockerTag(tag, digests, http_obj)
result.append({'name': six.text_type(tag)})
return result
def _ParseArgs(self, image_names):
tags = set()
for image_name in image_names:
docker_obj = util.GetDockerImageFromTagOrDigest(image_name)
if (isinstance(docker_obj, docker_name.Tag) and
util.IsFullySpecified(image_name)):
# Only accept explicitly named tags for removal.
tags.add(docker_obj)
else:
raise util.InvalidImageNameError(
'IMAGE_NAME must be of the form [*.gcr.io/repository:<tag>]: '
'[{}]'.format(image_name))
return tags
def _DeleteDockerTag(self, tag, digests, http_obj):
# calling Delete on an image referenced by tag only deletes the tag
docker_session.Delete(
creds=util.CredentialProvider(), name=tag, transport=http_obj)
log.DeletedResource('[{tag}] (referencing [{digest}])'.format(
tag=tag, digest=digests[tag]))