394 lines
14 KiB
Python
394 lines
14 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2023 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.
|
|
"""Upgrade a 1st gen Cloud Function to the Cloud Run function."""
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import collections
|
|
|
|
from googlecloudsdk.api_lib.functions import api_enablement
|
|
from googlecloudsdk.api_lib.functions.v2 import client as client_v2
|
|
from googlecloudsdk.api_lib.functions.v2 import exceptions
|
|
from googlecloudsdk.api_lib.functions.v2 import util as api_util
|
|
from googlecloudsdk.calliope import base
|
|
from googlecloudsdk.calliope import exceptions as calliope_exceptions
|
|
from googlecloudsdk.command_lib.eventarc import types as trigger_types
|
|
from googlecloudsdk.command_lib.functions import flags
|
|
from googlecloudsdk.command_lib.functions import run_util
|
|
from googlecloudsdk.command_lib.functions import service_account_util
|
|
from googlecloudsdk.command_lib.functions.v2 import deploy_util
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core.console import console_io
|
|
import six
|
|
|
|
SUPPORTED_EVENT_TYPES = (
|
|
'google.pubsub.topic.publish',
|
|
'providers/cloud.pubsub/eventTypes/topic.publish',
|
|
)
|
|
|
|
UpgradeAction = collections.namedtuple(
|
|
'UpgradeAction',
|
|
[
|
|
'target_state',
|
|
'prompt_msg',
|
|
'op_description',
|
|
'success_msg',
|
|
],
|
|
)
|
|
_ABORT_GUIDANCE_MSG = (
|
|
'You can abort the upgrade process at any time by rerunning this command'
|
|
' with the --abort flag.'
|
|
)
|
|
|
|
_SETUP_CONFIG_ACTION = UpgradeAction(
|
|
target_state='SETUP_FUNCTION_UPGRADE_CONFIG_SUCCESSFUL',
|
|
prompt_msg=(
|
|
'This creates a Cloud Run function with the same name [{}], code, and'
|
|
' configuration as the 1st gen function. The 1st gen function will'
|
|
' continue to serve traffic until you redirect traffic to the Cloud Run'
|
|
' function in the next step.\n\nTo learn more about the differences'
|
|
' between 1st gen and Cloud Run functions, visit:'
|
|
' https://cloud.google.com/functions/docs/concepts/version-comparison'
|
|
),
|
|
op_description=(
|
|
'Setting up the upgrade for function. Please wait while we'
|
|
' duplicate the 1st gen function configuration and code to a Cloud Run'
|
|
' function.'
|
|
),
|
|
success_msg=(
|
|
'The Cloud Run function is now ready for testing:\n {}\nView the'
|
|
' function upgrade testing guide for steps on how to test the function'
|
|
' before redirecting traffic to it.\n\nOnce you are ready to redirect'
|
|
' traffic, rerun this command with the --redirect-traffic flag.'
|
|
)
|
|
+ '\n\n'
|
|
+ _ABORT_GUIDANCE_MSG,
|
|
)
|
|
|
|
_REDIRECT_TRAFFIC_ACTION = UpgradeAction(
|
|
target_state='REDIRECT_FUNCTION_UPGRADE_TRAFFIC_SUCCESSFUL',
|
|
prompt_msg=(
|
|
'This will redirect all traffic from the 1st gen function [{}] to its'
|
|
' Cloud Run function copy. Please ensure that you have tested the Cloud'
|
|
' Run function before proceeding.'
|
|
),
|
|
op_description='Redirecting traffic to the Cloud Run function.',
|
|
success_msg=(
|
|
'The Cloud Run function is now serving all traffic.'
|
|
' If you experience issues, rerun this command with the'
|
|
' --rollback-traffic flag. Otherwise, once you are ready to finalize'
|
|
' the upgrade, rerun this command with the --commit flag.'
|
|
)
|
|
+ '\n\n'
|
|
+ _ABORT_GUIDANCE_MSG,
|
|
)
|
|
|
|
_ROLLBACK_TRAFFIC_ACTION = UpgradeAction(
|
|
target_state='SETUP_FUNCTION_UPGRADE_CONFIG_SUCCESSFUL',
|
|
prompt_msg=(
|
|
'This will rollback all traffic from the Cloud Run function copy [{}]'
|
|
' to the original 1st gen function. The Cloud Run function is still'
|
|
' available for testing.'
|
|
),
|
|
op_description='Rolling back traffic to the 1st gen function.',
|
|
success_msg=(
|
|
'The 1st gen function is now serving all traffic. The Cloud Run'
|
|
' function is still available for testing.'
|
|
)
|
|
+ '\n\n'
|
|
+ _ABORT_GUIDANCE_MSG,
|
|
)
|
|
|
|
_ABORT_ACTION = UpgradeAction(
|
|
target_state='ELIGIBLE_FOR_2ND_GEN_UPGRADE',
|
|
prompt_msg=(
|
|
'This will abort the upgrade process and delete the Cloud Run function'
|
|
' copy of the 1st gen function [{}].'
|
|
),
|
|
op_description='Aborting the upgrade for function.',
|
|
success_msg=(
|
|
'Upgrade aborted and the Cloud Run function was successfully deleted.'
|
|
),
|
|
)
|
|
|
|
_COMMIT_ACTION = UpgradeAction(
|
|
target_state=None,
|
|
prompt_msg=(
|
|
'This will complete the upgrade process for function [{}] and delete'
|
|
' the 1st gen copy.\n\nThis action cannot be undone.'
|
|
),
|
|
op_description=(
|
|
'Completing the upgrade and deleting the 1st gen copy for function.'
|
|
),
|
|
success_msg=(
|
|
'Upgrade completed and the 1st gen copy was successfully'
|
|
' deleted.\n\nYour function will continue to be available at the'
|
|
' following endpoints:\n{}\nReminder, your function can now be managed'
|
|
' through the Cloud Run API. Any event triggers are now Eventarc'
|
|
' triggers and can be managed through Eventarc API.'
|
|
),
|
|
)
|
|
|
|
# Source: http://cs/f:UpgradeStateMachine.java
|
|
_VALID_TRANSITION_ACTIONS = {
|
|
'ELIGIBLE_FOR_2ND_GEN_UPGRADE': [_SETUP_CONFIG_ACTION],
|
|
'UPGRADE_OPERATION_IN_PROGRESS': [],
|
|
'SETUP_FUNCTION_UPGRADE_CONFIG_SUCCESSFUL': [
|
|
_REDIRECT_TRAFFIC_ACTION,
|
|
_ABORT_ACTION,
|
|
],
|
|
'SETUP_FUNCTION_UPGRADE_CONFIG_ERROR': [
|
|
_SETUP_CONFIG_ACTION,
|
|
_ABORT_ACTION,
|
|
],
|
|
'ABORT_FUNCTION_UPGRADE_ERROR': [_ABORT_ACTION],
|
|
'REDIRECT_FUNCTION_UPGRADE_TRAFFIC_SUCCESSFUL': [
|
|
_COMMIT_ACTION,
|
|
_ROLLBACK_TRAFFIC_ACTION,
|
|
_ABORT_ACTION,
|
|
],
|
|
'REDIRECT_FUNCTION_UPGRADE_TRAFFIC_ERROR': [
|
|
_REDIRECT_TRAFFIC_ACTION,
|
|
_ABORT_ACTION,
|
|
],
|
|
'ROLLBACK_FUNCTION_UPGRADE_TRAFFIC_ERROR': [
|
|
_ROLLBACK_TRAFFIC_ACTION,
|
|
_ABORT_ACTION,
|
|
],
|
|
'COMMIT_FUNCTION_UPGRADE_SUCCESSFUL': [],
|
|
'COMMIT_FUNCTION_UPGRADE_ERROR_ROLLBACK_SAFE': [
|
|
_COMMIT_ACTION,
|
|
_ROLLBACK_TRAFFIC_ACTION,
|
|
_ABORT_ACTION,
|
|
],
|
|
'COMMIT_FUNCTION_UPGRADE_ERROR': [_COMMIT_ACTION],
|
|
} # type: dict[str, list[UpgradeAction]]
|
|
|
|
|
|
def _ValidateStateTransition(upgrade_state, action):
|
|
# type: (_,UpgradeAction) -> None
|
|
"""Validates whether the action is a valid action for the given upgrade state."""
|
|
upgrade_state_str = six.text_type(upgrade_state)
|
|
if upgrade_state_str == 'UPGRADE_OPERATION_IN_PROGRESS':
|
|
raise exceptions.FunctionsError(
|
|
'An upgrade operation is already in progress for this function.'
|
|
' Please try again later.'
|
|
)
|
|
|
|
if upgrade_state_str == action.target_state:
|
|
raise exceptions.FunctionsError(
|
|
'This function is already in the desired upgrade state: {}'.format(
|
|
upgrade_state
|
|
)
|
|
)
|
|
|
|
if action not in _VALID_TRANSITION_ACTIONS[upgrade_state_str]:
|
|
raise exceptions.FunctionsError(
|
|
'This function is not eligible for this operation. Its current upgrade'
|
|
" state is '{}'.".format(upgrade_state)
|
|
)
|
|
|
|
|
|
# Source: http://cs/f:Gen1UpgradeEligibilityValidator.java
|
|
def _RaiseNotEligibleForUpgradeError(function):
|
|
"""Raises an error when the function is not eligible for upgrade."""
|
|
if six.text_type(function.environment) == 'GEN_2':
|
|
raise exceptions.FunctionsError(
|
|
f'Function [{function.name}] is not eligible for Upgrade. To migrate to'
|
|
' Cloud Run function, please detach the function using `gcloud'
|
|
' functions detach` instead.'
|
|
)
|
|
if ':' in api_util.GetProject():
|
|
raise exceptions.FunctionsError(
|
|
f'Function [{function.name}] is not eligible for Cloud Run function'
|
|
' upgrade. It is in domain-scoped project that Cloud Run does not'
|
|
' support.'
|
|
)
|
|
if six.text_type(function.state) != 'ACTIVE':
|
|
raise exceptions.FunctionsError(
|
|
f'Function [{function.name}] is not eligible for Cloud Run function'
|
|
f' upgrade. It is in state [{function.state}].'
|
|
)
|
|
if (
|
|
not function.url
|
|
and function.eventTrigger.eventType not in SUPPORTED_EVENT_TYPES
|
|
):
|
|
raise exceptions.FunctionsError(
|
|
f'Function [{function.name}] is not eligible for Cloud Run function'
|
|
' upgrade. Only HTTP functions and Pub/Sub triggered functions are'
|
|
' supported.'
|
|
)
|
|
raise exceptions.FunctionsError(
|
|
f'Function [{function.name}] is not eligible for Cloud Run function'
|
|
' upgrade.'
|
|
)
|
|
|
|
|
|
@base.DefaultUniverseOnly
|
|
@base.ReleaseTracks(base.ReleaseTrack.BETA)
|
|
class UpgradeBeta(base.Command):
|
|
"""Upgrade a 1st gen Cloud Function to the Cloud Run function."""
|
|
|
|
detailed_help = {
|
|
'DESCRIPTION': '{description}',
|
|
'EXAMPLES': """\
|
|
To start the upgrade process for a 1st gen function `foo` and create a Cloud Run function copy, run:
|
|
|
|
$ {command} foo --setup-config
|
|
|
|
Once you are ready to redirect traffic to the Cloud Run function copy, run:
|
|
|
|
$ {command} foo --redirect-traffic
|
|
|
|
If you find you need to do more local testing you can rollback traffic to the 1st gen copy:
|
|
|
|
$ {command} foo --rollback-traffic
|
|
|
|
Once you're ready to finish upgrading and delete the 1st gen copy, run:
|
|
|
|
$ {command} foo --commit
|
|
|
|
You can abort the upgrade process at any time by running:
|
|
|
|
$ {command} foo --abort
|
|
""",
|
|
}
|
|
|
|
@staticmethod
|
|
def Args(parser):
|
|
flags.AddFunctionResourceArg(parser, 'to upgrade')
|
|
flags.AddUpgradeFlags(parser)
|
|
|
|
def Run(self, args):
|
|
client = client_v2.FunctionsClient(self.ReleaseTrack())
|
|
function_ref = args.CONCEPTS.name.Parse()
|
|
function_name = function_ref.RelativeName()
|
|
|
|
function = client.GetFunction(function_name)
|
|
|
|
if not function:
|
|
raise exceptions.FunctionsError(
|
|
'Function [{}] does not exist.'.format(function_name)
|
|
)
|
|
|
|
if not function.upgradeInfo:
|
|
_RaiseNotEligibleForUpgradeError(function)
|
|
|
|
upgrade_state = function.upgradeInfo.upgradeState
|
|
|
|
if (
|
|
six.text_type(upgrade_state)
|
|
== 'INELIGIBLE_FOR_UPGRADE_UNTIL_REDEPLOYMENT'
|
|
):
|
|
raise exceptions.FunctionsError(
|
|
f'Function [{function.name}] is not eligible for Cloud Run function'
|
|
f' upgrade. The runtime [{function.buildConfig.runtime}] is not'
|
|
' supported. Please update to a supported runtime instead and try'
|
|
' again. Use `gcloud functions runtimes list` to get a list of'
|
|
' available runtimes.'
|
|
)
|
|
|
|
action = None
|
|
action_fn = None
|
|
if args.redirect_traffic:
|
|
action = _REDIRECT_TRAFFIC_ACTION
|
|
action_fn = client.RedirectFunctionUpgradeTraffic
|
|
elif args.rollback_traffic:
|
|
action = _ROLLBACK_TRAFFIC_ACTION
|
|
action_fn = client.RollbackFunctionUpgradeTraffic
|
|
elif args.commit:
|
|
action = _COMMIT_ACTION
|
|
action_fn = client.CommitFunctionUpgrade
|
|
elif args.abort:
|
|
action = _ABORT_ACTION
|
|
action_fn = client.AbortFunctionUpgrade
|
|
elif args.setup_config:
|
|
action = _SETUP_CONFIG_ACTION
|
|
action_fn = client.SetupFunctionUpgradeConfig
|
|
else:
|
|
raise calliope_exceptions.OneOfArgumentsRequiredException(
|
|
[
|
|
'--abort',
|
|
'--commit',
|
|
'--redirect-traffic',
|
|
'--rollback-traffic',
|
|
'--setup-config',
|
|
],
|
|
'One of the upgrade step must be specified.',
|
|
)
|
|
|
|
_ValidateStateTransition(upgrade_state, action)
|
|
|
|
message = action.prompt_msg.format(function_name)
|
|
if not console_io.PromptContinue(message, default=True):
|
|
return
|
|
|
|
if action == _SETUP_CONFIG_ACTION:
|
|
# Preliminary checks to ensure APIs and permissions are set up in case
|
|
# this is the user's first time deploying a Cloud Run function.
|
|
api_enablement.PromptToEnableApiIfDisabled('cloudbuild.googleapis.com')
|
|
api_enablement.PromptToEnableApiIfDisabled(
|
|
'artifactregistry.googleapis.com'
|
|
)
|
|
trigger = function.eventTrigger
|
|
if not trigger and args.trigger_service_account:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--trigger-service-account',
|
|
'Trigger service account can only be specified for'
|
|
' event-triggered functions.',
|
|
)
|
|
if trigger and trigger_types.IsPubsubType(trigger.eventType):
|
|
deploy_util.ensure_pubsub_sa_has_token_creator_role()
|
|
if trigger and trigger_types.IsAuditLogType(trigger.eventType):
|
|
deploy_util.ensure_data_access_logs_are_enabled(trigger.eventFilters)
|
|
operation = action_fn(function_name, args.trigger_service_account)
|
|
else:
|
|
operation = action_fn(function_name)
|
|
|
|
description = action.op_description
|
|
api_util.WaitForOperation(
|
|
client.client, client.messages, operation, description
|
|
)
|
|
|
|
log.status.Print()
|
|
|
|
if action == _SETUP_CONFIG_ACTION:
|
|
function = client.GetFunction(function_name)
|
|
if function.eventTrigger:
|
|
# Checks trigger service account has route.invoker permission on the
|
|
# project. If not, prompts to add the run invoker role to the function.
|
|
service_account_util.ValidateAndBindTriggerServiceAccount(
|
|
function,
|
|
api_util.GetProject(),
|
|
args.trigger_service_account,
|
|
is_gen2=False,
|
|
)
|
|
log.status.Print(
|
|
action.success_msg.format(function.upgradeInfo.serviceConfig.uri)
|
|
)
|
|
elif action == _COMMIT_ACTION:
|
|
service = run_util.GetService(function)
|
|
urls_strings = ''.join(f'* {url}\n' for url in service.urls)
|
|
log.status.Print(action.success_msg.format(urls_strings))
|
|
else:
|
|
log.status.Print(action.success_msg)
|
|
|
|
|
|
@base.DefaultUniverseOnly
|
|
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
|
|
class UpgradeAlpha(UpgradeBeta):
|
|
"""Upgrade a 1st gen Cloud Function to the Cloud Run function."""
|