File: //snap/google-cloud-cli/current/lib/googlecloudsdk/command_lib/init_util.py
# -*- 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.
"""Contains utilities to support the `gcloud init` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.cloudresourcemanager import projects_api
from googlecloudsdk.api_lib.cloudresourcemanager import projects_util
from googlecloudsdk.api_lib.resource_manager import operations
from googlecloudsdk.calliope import usage_text
from googlecloudsdk.command_lib.projects import util as projects_command_util
from googlecloudsdk.core import log
from googlecloudsdk.core import resources
from googlecloudsdk.core.console import console_io
import six
_ENTER_PROJECT_ID_MESSAGE = """\
Enter a Project ID. Note that a Project ID CANNOT be changed later.
Project IDs must be 6-30 characters (lowercase ASCII, digits, or
hyphens) in length and start with a lowercase letter. \
"""
_CREATE_PROJECT_SENTINEL = object()
# At around 200 projects, being able to list them out becomes less useful.
_PROJECT_LIST_LIMIT = 200
def _GetProjectIds(limit=None):
"""Returns a list of project IDs the current user can list.
Args:
limit: int, the maximum number of project ids to return.
Returns:
list of str, project IDs, or None (if the command fails).
"""
try:
projects = projects_api.List(limit=limit)
return sorted([project.projectId for project in projects])
except Exception as err: # pylint: disable=broad-except
log.warning('Listing available projects failed: %s', six.text_type(err))
return None
def _IsExistingProject(project_id):
project_ref = projects_command_util.ParseProject(project_id)
try:
project = projects_api.Get(project_ref)
return projects_util.IsActive(project)
except Exception: # pylint: disable=broad-except
# Yeah, this isn't great, but there isn't a perfect exception super class
# that covers both API related errors and network errors.
return False
def _PromptForProjectId(project_ids, limit_exceeded):
"""Prompt the user for a project ID, based on the list of available IDs.
Also allows an option to create a project.
Args:
project_ids: list of str or None, the project IDs to prompt for. If this
value is None, the listing was unsuccessful and we prompt the user
free-form (and do not validate the input). If it's empty, we offer to
create a project for the user.
limit_exceeded: bool, whether or not the project list limit was reached. If
this limit is reached, then user will be prompted with a choice to
manually enter a project id, create a new project, or list all projects.
Returns:
str, the project ID to use, or _CREATE_PROJECT_SENTINEL (if a project should
be created), or None
"""
if project_ids is None:
return console_io.PromptResponse(
'Enter project ID you would like to use: ') or None
elif not project_ids:
if not console_io.PromptContinue(
'This account has no projects.',
prompt_string='Would you like to create one?'):
return None
return _CREATE_PROJECT_SENTINEL
elif limit_exceeded:
idx = console_io.PromptChoice(
['Enter a project ID', 'Create a new project', 'List projects'],
message=('This account has a lot of projects! Listing them all can '
'take a while.'))
if idx is None:
return None
elif idx == 0:
return console_io.PromptWithValidator(
_IsExistingProject,
'Project ID does not exist or is not active.',
'Enter project ID you would like to use: ',
allow_invalid=True)
elif idx == 1:
return _CREATE_PROJECT_SENTINEL
else:
project_ids = _GetProjectIds()
idx = console_io.PromptChoice(
project_ids + ['Enter a project ID', 'Create a new project'],
message='Pick cloud project to use: ',
allow_freeform=True,
freeform_suggester=usage_text.TextChoiceSuggester())
if idx is None:
return None
elif idx == len(project_ids):
return console_io.PromptWithValidator(
_IsExistingProject,
'Project ID does not exist or is not active.',
'Enter project ID you would like to use: ',
allow_invalid=True)
elif idx == len(project_ids) + 1:
return _CREATE_PROJECT_SENTINEL
return project_ids[idx]
def _CreateProject(project_id, project_ids):
"""Create a project and check that it isn't in the known project IDs."""
if project_ids and project_id in project_ids:
raise ValueError('Attempting to create a project that already exists.')
project_ref = resources.REGISTRY.Create(
'cloudresourcemanager.projects', projectId=project_id)
try:
create_op = projects_api.Create(project_ref)
except Exception as err: # pylint: disable=broad-except
log.warning('Project creation failed: {err}\n'
'Please make sure to create the project [{project}] using\n'
' $ gcloud projects create {project}\n'
'or change to another project using\n'
' $ gcloud config set project <PROJECT ID>'.format(
err=six.text_type(err), project=project_id))
return None
# wait for async Create operation to complete and check the status.
try:
create_op = operations.WaitForOperation(create_op)
except operations.OperationFailedException as err:
log.warning(
'Project creation for project [{project}] failed:\n {err}'.format(
err=six.text_type(err), project=project_id))
return None
return project_id
def PickProject(preselected=None):
"""Allows user to select a project.
Args:
preselected: str, use this value if not None
Returns:
str, project_id or None if was not selected.
"""
project_ids = _GetProjectIds(limit=_PROJECT_LIST_LIMIT + 1)
limit_exceeded = False
if project_ids is not None and len(project_ids) > _PROJECT_LIST_LIMIT:
limit_exceeded = True
selected = None
if preselected:
project_id = preselected
else:
project_id = _PromptForProjectId(project_ids, limit_exceeded)
if project_id is not _CREATE_PROJECT_SENTINEL:
selected = project_id
if not limit_exceeded:
if (project_ids is None or project_id in project_ids or
project_id is None or selected):
return project_id
else:
# If we fall into limit_exceeded logic and preselected was None, then
# as long as project_id is not _CREATE_PROJECT_SENTINEL, then we know the
# project_id is valid.
if ((preselected and _IsExistingProject(preselected)) or
project_id is not _CREATE_PROJECT_SENTINEL):
return project_id
if project_id is _CREATE_PROJECT_SENTINEL:
project_id = console_io.PromptResponse(_ENTER_PROJECT_ID_MESSAGE)
if not project_id:
return None
else:
if project_ids:
message = '[{0}] is not one of your projects [{1}]. '.format(
project_id, ','.join(project_ids))
else:
message = 'This account has no projects.'
if not console_io.PromptContinue(
message=message, prompt_string='Would you like to create it?'):
return None
return _CreateProject(project_id, project_ids)