1# -*- coding: utf-8 -*- #
2# Copyright 2015 Google LLC. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#    http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""Update cluster command."""
16
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import unicode_literals
20
21from apitools.base.py import exceptions as apitools_exceptions
22
23from googlecloudsdk.api_lib.container import api_adapter
24from googlecloudsdk.api_lib.container import kubeconfig as kconfig
25from googlecloudsdk.api_lib.container import util
26from googlecloudsdk.calliope import actions
27from googlecloudsdk.calliope import arg_parsers
28from googlecloudsdk.calliope import base
29from googlecloudsdk.calliope import exceptions
30from googlecloudsdk.command_lib.container import container_command_util
31from googlecloudsdk.command_lib.container import flags
32from googlecloudsdk.core import log
33from googlecloudsdk.core.console import console_attr
34from googlecloudsdk.core.console import console_io
35from six.moves import input  # pylint: disable=redefined-builtin
36
37
38class InvalidAddonValueError(util.Error):
39  """A class for invalid --update-addons input."""
40
41  def __init__(self, value):
42    message = ('invalid --update-addons value {0}; '
43               'must be ENABLED or DISABLED.'.format(value))
44    super(InvalidAddonValueError, self).__init__(message)
45
46
47class InvalidPasswordError(util.Error):
48  """A class for invalid password input."""
49
50  def __init__(self, value, error):
51    message = 'invalid password value "{0}"; {1}'.format(value, error)
52    super(InvalidPasswordError, self).__init__(message)
53
54
55def _ParseAddonDisabled(val):
56  if val == 'ENABLED':
57    return False
58  if val == 'DISABLED':
59    return True
60  raise InvalidAddonValueError(val)
61
62
63def _AddCommonArgs(parser):
64  """Register common flags for this command.
65
66  Args:
67    parser: An argparse.ArgumentParser-like object. It is mocked out in order to
68      capture some information, but behaves like an ArgumentParser.
69  """
70  parser.add_argument(
71      'name', metavar='NAME', help='The name of the cluster to update.')
72  parser.add_argument('--node-pool', help='Node pool to be updated.')
73  # Timeout in seconds for the operation, default 3600 seconds (60 minutes)
74  parser.add_argument(
75      '--timeout',
76      type=int,
77      default=3600,
78      hidden=True,
79      help='Timeout (seconds) for waiting on the operation to complete.')
80  flags.AddAsyncFlag(parser)
81
82
83def _AddMutuallyExclusiveArgs(mutex_group, release_track):
84  """Add all arguments that need to be mutually exclusive from each other."""
85  if release_track == base.ReleaseTrack.ALPHA:
86    mutex_group.add_argument(
87        '--update-addons',
88        type=arg_parsers.ArgDict(
89            spec=dict(
90                {
91                    api_adapter.INGRESS: _ParseAddonDisabled,
92                    api_adapter.HPA: _ParseAddonDisabled,
93                    api_adapter.DASHBOARD: _ParseAddonDisabled,
94                    api_adapter.NETWORK_POLICY: _ParseAddonDisabled,
95                    api_adapter.ISTIO: _ParseAddonDisabled,
96                    api_adapter.APPLICATIONMANAGER: _ParseAddonDisabled,
97                    api_adapter.CLOUDBUILD: _ParseAddonDisabled,
98                    api_adapter.NODELOCALDNS: _ParseAddonDisabled,
99                    api_adapter.GCEPDCSIDRIVER: _ParseAddonDisabled,
100                    api_adapter.CONFIGCONNECTOR: _ParseAddonDisabled,
101                },
102                **{k: _ParseAddonDisabled for k in api_adapter.CLOUDRUN_ADDONS
103                  }),),
104        dest='disable_addons',
105        metavar='ADDON=ENABLED|DISABLED',
106        help="""Cluster addons to enable or disable. Options are
107{hpa}=ENABLED|DISABLED
108{ingress}=ENABLED|DISABLED
109{dashboard}=ENABLED|DISABLED
110{istio}=ENABLED|DISABLED
111{application_manager}=ENABLED|DISABLED
112{network_policy}=ENABLED|DISABLED
113{cloudrun}=ENABLED|DISABLED
114{cloudbuild}=ENABLED|DISABLED
115{configconnector}=ENABLED|DISABLED
116{nodelocaldns}=ENABLED|DISABLED
117{gcepdcsidriver}=ENABLED|DISABLED""".format(
118    hpa=api_adapter.HPA,
119    ingress=api_adapter.INGRESS,
120    dashboard=api_adapter.DASHBOARD,
121    network_policy=api_adapter.NETWORK_POLICY,
122    istio=api_adapter.ISTIO,
123    application_manager=api_adapter.APPLICATIONMANAGER,
124    cloudrun=api_adapter.CLOUDRUN_ADDONS[0],
125    cloudbuild=api_adapter.CLOUDBUILD,
126    configconnector=api_adapter.CONFIGCONNECTOR,
127    nodelocaldns=api_adapter.NODELOCALDNS,
128    gcepdcsidriver=api_adapter.GCEPDCSIDRIVER,
129    ))
130
131  elif release_track == base.ReleaseTrack.BETA:
132    mutex_group.add_argument(
133        '--update-addons',
134        type=arg_parsers.ArgDict(
135            spec=dict(
136                {
137                    api_adapter.INGRESS: _ParseAddonDisabled,
138                    api_adapter.HPA: _ParseAddonDisabled,
139                    api_adapter.DASHBOARD: _ParseAddonDisabled,
140                    api_adapter.NETWORK_POLICY: _ParseAddonDisabled,
141                    api_adapter.ISTIO: _ParseAddonDisabled,
142                    api_adapter.APPLICATIONMANAGER: _ParseAddonDisabled,
143                    api_adapter.NODELOCALDNS: _ParseAddonDisabled,
144                    api_adapter.GCEPDCSIDRIVER: _ParseAddonDisabled,
145                    api_adapter.CONFIGCONNECTOR: _ParseAddonDisabled,
146                },
147                **{k: _ParseAddonDisabled for k in api_adapter.CLOUDRUN_ADDONS
148                  }),),
149        dest='disable_addons',
150        metavar='ADDON=ENABLED|DISABLED',
151        help="""Cluster addons to enable or disable. Options are
152{hpa}=ENABLED|DISABLED
153{ingress}=ENABLED|DISABLED
154{dashboard}=ENABLED|DISABLED
155{istio}=ENABLED|DISABLED
156{application_manager}=ENABLED|DISABLED
157{network_policy}=ENABLED|DISABLED
158{cloudrun}=ENABLED|DISABLED
159{configconnector}=ENABLED|DISABLED
160{nodelocaldns}=ENABLED|DISABLED
161{gcepdcsidriver}=ENABLED|DISABLED""".format(
162    hpa=api_adapter.HPA,
163    ingress=api_adapter.INGRESS,
164    dashboard=api_adapter.DASHBOARD,
165    network_policy=api_adapter.NETWORK_POLICY,
166    istio=api_adapter.ISTIO,
167    application_manager=api_adapter.APPLICATIONMANAGER,
168    cloudrun=api_adapter.CLOUDRUN_ADDONS[0],
169    configconnector=api_adapter.CONFIGCONNECTOR,
170    nodelocaldns=api_adapter.NODELOCALDNS,
171    gcepdcsidriver=api_adapter.GCEPDCSIDRIVER,
172    ))
173
174  else:
175    mutex_group.add_argument(
176        '--update-addons',
177        type=arg_parsers.ArgDict(
178            spec=dict(
179                {
180                    api_adapter.INGRESS: _ParseAddonDisabled,
181                    api_adapter.HPA: _ParseAddonDisabled,
182                    api_adapter.DASHBOARD: _ParseAddonDisabled,
183                    api_adapter.NETWORK_POLICY: _ParseAddonDisabled,
184                    api_adapter.NODELOCALDNS: _ParseAddonDisabled,
185                    api_adapter.CONFIGCONNECTOR: _ParseAddonDisabled,
186                    api_adapter.GCEPDCSIDRIVER: _ParseAddonDisabled,
187                },
188                **{k: _ParseAddonDisabled for k in api_adapter.CLOUDRUN_ADDONS
189                  }),),
190        dest='disable_addons',
191        metavar='ADDON=ENABLED|DISABLED',
192        help="""Cluster addons to enable or disable. Options are
193{hpa}=ENABLED|DISABLED
194{ingress}=ENABLED|DISABLED
195{dashboard}=ENABLED|DISABLED
196{network_policy}=ENABLED|DISABLED
197{cloudrun}=ENABLED|DISABLED
198{configconnector}=ENABLED|DISABLED
199{nodelocaldns}=ENABLED|DISABLED
200{gcepdcsidriver}=ENABLED|DISABLED""".format(
201    hpa=api_adapter.HPA,
202    ingress=api_adapter.INGRESS,
203    dashboard=api_adapter.DASHBOARD,
204    network_policy=api_adapter.NETWORK_POLICY,
205    cloudrun=api_adapter.CLOUDRUN_ADDONS[0],
206    configconnector=api_adapter.CONFIGCONNECTOR,
207    nodelocaldns=api_adapter.NODELOCALDNS,
208    gcepdcsidriver=api_adapter.GCEPDCSIDRIVER,
209    ))
210
211  mutex_group.add_argument(
212      '--generate-password',
213      action='store_true',
214      default=None,
215      help='Ask the server to generate a secure password and use that as the '
216      'basic auth password, keeping the existing username.')
217  mutex_group.add_argument(
218      '--set-password',
219      action='store_true',
220      default=None,
221      help='Set the basic auth password to the specified value, keeping the '
222      'existing username.')
223
224  flags.AddBasicAuthFlags(mutex_group)
225
226
227def _AddAdditionalZonesArg(mutex_group, deprecated=True):
228  action = None
229  if deprecated:
230    action = actions.DeprecationAction(
231        'additional-zones',
232        warn='This flag is deprecated. '
233        'Use --node-locations=PRIMARY_ZONE,[ZONE,...] instead.')
234  mutex_group.add_argument(
235      '--additional-zones',
236      type=arg_parsers.ArgList(),
237      action=action,
238      metavar='ZONE',
239      help="""\
240The set of additional zones in which the cluster's node footprint should be
241replicated. All zones must be in the same region as the cluster's primary zone.
242
243Note that the exact same footprint will be replicated in all zones, such that
244if you created a cluster with 4 nodes in a single zone and then use this option
245to spread across 2 more zones, 8 additional nodes will be created.
246
247Multiple locations can be specified, separated by commas. For example:
248
249  $ {command} example-cluster --zone us-central1-a --additional-zones us-central1-b,us-central1-c
250
251To remove all zones other than the cluster's primary zone, pass the empty string
252to the flag. For example:
253
254  $ {command} example-cluster --zone us-central1-a --additional-zones ""
255""")
256
257
258@base.ReleaseTracks(base.ReleaseTrack.GA)
259class Update(base.UpdateCommand):
260  """Update cluster settings for an existing container cluster."""
261
262  detailed_help = {
263      'DESCRIPTION':
264          '{description}',
265      'EXAMPLES':
266          """\
267          To enable autoscaling for an existing cluster, run:
268
269            $ {command} sample-cluster --enable-autoscaling
270          """,
271  }
272
273  @staticmethod
274  def Args(parser):
275    """Register flags for this command.
276
277    Args:
278      parser: An argparse.ArgumentParser-like object. It is mocked out in order
279        to capture some information, but behaves like an ArgumentParser.
280    """
281    _AddCommonArgs(parser)
282    group = parser.add_mutually_exclusive_group(required=True)
283    group_locations = group.add_mutually_exclusive_group()
284    _AddMutuallyExclusiveArgs(group, base.ReleaseTrack.GA)
285    flags.AddNodeLocationsFlag(group_locations)
286    flags.AddClusterAutoscalingFlags(parser, group)
287    flags.AddMasterAuthorizedNetworksFlags(
288        parser, enable_group_for_update=group)
289    flags.AddEnableLegacyAuthorizationFlag(group)
290    flags.AddStartIpRotationFlag(group)
291    flags.AddStartCredentialRotationFlag(group)
292    flags.AddCompleteIpRotationFlag(group)
293    flags.AddCompleteCredentialRotationFlag(group)
294    flags.AddCloudRunConfigFlag(parser)
295    flags.AddUpdateLabelsFlag(group)
296    flags.AddRemoveLabelsFlag(group)
297    flags.AddNetworkPolicyFlags(group)
298    flags.AddEnableIntraNodeVisibilityFlag(group)
299    group_logging_monitoring = group.add_group()
300    flags.AddLoggingServiceFlag(group_logging_monitoring)
301    flags.AddMonitoringServiceFlag(group_logging_monitoring)
302    flags.AddEnableBinAuthzFlag(group)
303    flags.AddEnableStackdriverKubernetesFlag(group)
304    flags.AddDailyMaintenanceWindowFlag(group, add_unset_text=True)
305    flags.AddRecurringMaintenanceWindowFlags(group, is_update=True)
306    flags.AddResourceUsageExportFlags(group, is_update=True)
307    flags.AddReleaseChannelFlag(group, is_update=True, hidden=False)
308    flags.AddWorkloadIdentityFlags(group)
309    flags.AddWorkloadIdentityUpdateFlags(group)
310    flags.AddDatabaseEncryptionFlag(group)
311    flags.AddDisableDatabaseEncryptionFlag(group)
312    flags.AddDisableDefaultSnatFlag(group, for_cluster_create=False)
313    flags.AddVerticalPodAutoscalingFlag(group)
314    flags.AddAutoprovisioningFlags(group)
315    flags.AddEnableShieldedNodesFlags(group)
316    flags.AddMasterGlobalAccessFlag(group, is_update=True)
317    flags.AddPrivateIpv6GoogleAccessTypeFlag('v1', group, hidden=False)
318    flags.AddNotificationConfigFlag(group)
319
320  def ParseUpdateOptions(self, args, locations):
321    get_default = lambda key: getattr(args, key)
322    flags.ValidateNotificationConfigFlag(args)
323    opts = container_command_util.ParseUpdateOptionsBase(args, locations)
324    opts.resource_usage_bigquery_dataset = args.resource_usage_bigquery_dataset
325    opts.clear_resource_usage_bigquery_dataset = \
326        args.clear_resource_usage_bigquery_dataset
327    opts.enable_network_egress_metering = args.enable_network_egress_metering
328    opts.enable_resource_consumption_metering = \
329        args.enable_resource_consumption_metering
330    opts.enable_intra_node_visibility = args.enable_intra_node_visibility
331    opts.enable_master_global_access = args.enable_master_global_access
332    opts.enable_shielded_nodes = args.enable_shielded_nodes
333    opts.release_channel = args.release_channel
334    opts.cloud_run_config = flags.GetLegacyCloudRunFlag('{}_config', args,
335                                                        get_default)
336    flags.ValidateCloudRunConfigUpdateArgs(opts.cloud_run_config,
337                                           args.disable_addons)
338    if args.disable_addons and api_adapter.NODELOCALDNS in args.disable_addons:
339      # NodeLocalDNS is being enabled or disabled
340      console_io.PromptContinue(
341          message='Enabling/Disabling NodeLocal DNSCache causes a re-creation '
342          'of all cluster nodes at versions 1.15 or above. '
343          'This operation is long-running and will block other '
344          'operations on the cluster (including delete) until it has run '
345          'to completion.',
346          cancel_on_no=True)
347    opts.disable_default_snat = args.disable_default_snat
348    opts.notification_config = args.notification_config
349    return opts
350
351  def Run(self, args):
352    """This is what gets called when the user runs this command.
353
354    Args:
355      args: an argparse namespace. All the arguments that were provided to this
356        command invocation.
357
358    Returns:
359      Some value that we want to have printed later.
360    """
361    adapter = self.context['api_adapter']
362    location_get = self.context['location_get']
363    location = location_get(args)
364    cluster_ref = adapter.ParseCluster(args.name, location)
365    cluster_name = args.name
366    cluster_node_count = None
367    cluster_zone = cluster_ref.zone
368    cluster_is_required = self.IsClusterRequired(args)
369    try:
370      # Attempt to get cluster for better prompts and to validate args.
371      # Error is a warning but not fatal. Should only exit with a failure on
372      # the actual update API calls below.
373      cluster = adapter.GetCluster(cluster_ref)
374      cluster_name = cluster.name
375      cluster_node_count = cluster.currentNodeCount
376      cluster_zone = cluster.zone
377    except (exceptions.HttpException, apitools_exceptions.HttpForbiddenError,
378            util.Error) as error:
379      if cluster_is_required:
380        raise
381      log.warning(('Problem loading details of cluster to update:\n\n{}\n\n'
382                   'You can still attempt updates to the cluster.\n').format(
383                       console_attr.SafeText(error)))
384
385    # locations will be None if additional-zones was specified, an empty list
386    # if it was specified with no argument, or a populated list if zones were
387    # provided. We want to distinguish between the case where it isn't
388    # specified (and thus shouldn't be passed on to the API) and the case where
389    # it's specified as wanting no additional zones, in which case we must pass
390    # the cluster's primary zone to the API.
391    # TODO(b/29578401): Remove the hasattr once the flag is GA.
392    locations = None
393    if hasattr(args, 'additional_zones') and args.additional_zones is not None:
394      locations = sorted([cluster_ref.zone] + args.additional_zones)
395    if hasattr(args, 'node_locations') and args.node_locations is not None:
396      locations = sorted(args.node_locations)
397
398    flags.LogBasicAuthDeprecationWarning(args)
399    if args.IsSpecified('username') or args.IsSpecified('enable_basic_auth'):
400      flags.MungeBasicAuthFlags(args)
401      options = api_adapter.SetMasterAuthOptions(
402          action=api_adapter.SetMasterAuthOptions.SET_USERNAME,
403          username=args.username,
404          password=args.password)
405
406      try:
407        op_ref = adapter.SetMasterAuth(cluster_ref, options)
408      except apitools_exceptions.HttpError as error:
409        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
410    elif (args.generate_password or args.set_password or
411          args.IsSpecified('password')):
412      if args.generate_password:
413        password = ''
414        options = api_adapter.SetMasterAuthOptions(
415            action=api_adapter.SetMasterAuthOptions.GENERATE_PASSWORD,
416            password=password)
417      else:
418        password = args.password
419        if not args.IsSpecified('password'):
420          password = input('Please enter the new password:')
421        options = api_adapter.SetMasterAuthOptions(
422            action=api_adapter.SetMasterAuthOptions.SET_PASSWORD,
423            password=password)
424
425      try:
426        op_ref = adapter.SetMasterAuth(cluster_ref, options)
427        del password
428        del options
429      except apitools_exceptions.HttpError as error:
430        del password
431        del options
432        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
433    elif args.enable_network_policy is not None:
434      console_io.PromptContinue(
435          message='Enabling/Disabling Network Policy causes a rolling '
436          'update of all cluster nodes, similar to performing a cluster '
437          'upgrade.  This operation is long-running and will block other '
438          'operations on the cluster (including delete) until it has run '
439          'to completion.',
440          cancel_on_no=True)
441      options = api_adapter.SetNetworkPolicyOptions(
442          enabled=args.enable_network_policy)
443      try:
444        op_ref = adapter.SetNetworkPolicy(cluster_ref, options)
445      except apitools_exceptions.HttpError as error:
446        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
447    elif args.start_ip_rotation or args.start_credential_rotation:
448      if args.start_ip_rotation:
449        msg_tmpl = """This will start an IP Rotation on cluster [{name}]. The \
450master will be updated to serve on a new IP address in addition to the current \
451IP address. Kubernetes Engine will then recreate all nodes ({num_nodes} nodes) \
452to point to the new IP address. This operation is long-running and will block \
453other operations on the cluster (including delete) until it has run to \
454completion."""
455        rotate_credentials = False
456      elif args.start_credential_rotation:
457        msg_tmpl = """This will start an IP and Credentials Rotation on cluster\
458 [{name}]. The master will be updated to serve on a new IP address in addition \
459to the current IP address, and cluster credentials will be rotated. Kubernetes \
460Engine will then recreate all nodes ({num_nodes} nodes) to point to the new IP \
461address. This operation is long-running and will block other operations on the \
462cluster (including delete) until it has run to completion."""
463        rotate_credentials = True
464      console_io.PromptContinue(
465          message=msg_tmpl.format(
466              name=cluster_name,
467              num_nodes=cluster_node_count if cluster_node_count else '?'),
468          cancel_on_no=True)
469      try:
470        op_ref = adapter.StartIpRotation(
471            cluster_ref, rotate_credentials=rotate_credentials)
472      except apitools_exceptions.HttpError as error:
473        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
474    elif args.complete_ip_rotation or args.complete_credential_rotation:
475      if args.complete_ip_rotation:
476        msg_tmpl = """This will complete the in-progress IP Rotation on \
477cluster [{name}]. The master will be updated to stop serving on the old IP \
478address and only serve on the new IP address. Make sure all API clients have \
479been updated to communicate with the new IP address (e.g. by running `gcloud \
480container clusters get-credentials --project {project} --zone {zone} {name}`). \
481This operation is long-running and will block other operations on the cluster \
482(including delete) until it has run to completion."""
483      elif args.complete_credential_rotation:
484        msg_tmpl = """This will complete the in-progress Credential Rotation on\
485 cluster [{name}]. The master will be updated to stop serving on the old IP \
486address and only serve on the new IP address. Old cluster credentials will be \
487invalidated. Make sure all API clients have been updated to communicate with \
488the new IP address (e.g. by running `gcloud container clusters get-credentials \
489--project {project} --zone {zone} {name}`). This operation is long-running and \
490will block other operations on the cluster (including delete) until it has run \
491to completion."""
492      console_io.PromptContinue(
493          message=msg_tmpl.format(
494              name=cluster_name,
495              project=cluster_ref.projectId,
496              zone=cluster_zone),
497          cancel_on_no=True)
498      try:
499        op_ref = adapter.CompleteIpRotation(cluster_ref)
500      except apitools_exceptions.HttpError as error:
501        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
502    elif args.update_labels is not None:
503      try:
504        op_ref = adapter.UpdateLabels(cluster_ref, args.update_labels)
505      except apitools_exceptions.HttpError as error:
506        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
507    elif args.remove_labels is not None:
508      try:
509        op_ref = adapter.RemoveLabels(cluster_ref, args.remove_labels)
510      except apitools_exceptions.HttpError as error:
511        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
512    elif args.logging_service is not None and args.monitoring_service is None:
513      try:
514        op_ref = adapter.SetLoggingService(cluster_ref, args.logging_service)
515      except apitools_exceptions.HttpError as error:
516        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
517    elif args.maintenance_window is not None:
518      try:
519        op_ref = adapter.SetDailyMaintenanceWindow(cluster_ref,
520                                                   cluster.maintenancePolicy,
521                                                   args.maintenance_window)
522      except apitools_exceptions.HttpError as error:
523        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
524    elif getattr(args, 'maintenance_window_start', None) is not None:
525      try:
526        op_ref = adapter.SetRecurringMaintenanceWindow(
527            cluster_ref, cluster.maintenancePolicy,
528            args.maintenance_window_start, args.maintenance_window_end,
529            args.maintenance_window_recurrence)
530      except apitools_exceptions.HttpError as error:
531        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
532    elif getattr(args, 'clear_maintenance_window', None):
533      try:
534        op_ref = adapter.RemoveMaintenanceWindow(cluster_ref,
535                                                 cluster.maintenancePolicy)
536      except apitools_exceptions.HttpError as error:
537        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
538    elif getattr(args, 'add_maintenance_exclusion_end', None) is not None:
539      try:
540        op_ref = adapter.AddMaintenanceExclusion(
541            cluster_ref, cluster.maintenancePolicy,
542            args.add_maintenance_exclusion_name,
543            args.add_maintenance_exclusion_start,
544            args.add_maintenance_exclusion_end)
545      except apitools_exceptions.HttpError as error:
546        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
547    elif getattr(args, 'remove_maintenance_exclusion', None) is not None:
548      try:
549        op_ref = adapter.RemoveMaintenanceExclusion(
550            cluster_ref, cluster.maintenancePolicy,
551            args.remove_maintenance_exclusion)
552      except apitools_exceptions.HttpError as error:
553        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
554    elif getattr(args, 'add_cross_connect_subnetworks', None) is not None:
555      try:
556        op_ref = adapter.ModifyCrossConnectSubnetworks(
557            cluster_ref,
558            cluster.privateClusterConfig.crossConnectConfig,
559            add_subnetworks=args.add_cross_connect_subnetworks)
560      except apitools_exceptions.HttpError as error:
561        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
562    elif getattr(args, 'remove_cross_connect_subnetworks', None) is not None:
563      try:
564        op_ref = adapter.ModifyCrossConnectSubnetworks(
565            cluster_ref,
566            cluster.privateClusterConfig.crossConnectConfig,
567            remove_subnetworks=args.remove_cross_connect_subnetworks)
568
569      except apitools_exceptions.HttpError as error:
570        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
571    elif getattr(args, 'clear_cross_connect_subnetworks', None) is not None:
572      try:
573        op_ref = adapter.ModifyCrossConnectSubnetworks(
574            cluster_ref,
575            cluster.privateClusterConfig.crossConnectConfig,
576            clear_all_subnetworks=True)
577      except apitools_exceptions.HttpError as error:
578        raise exceptions.HttpException(error, util.HTTP_ERROR_FORMAT)
579    else:
580      if args.enable_legacy_authorization is not None:
581        op_ref = adapter.SetLegacyAuthorization(
582            cluster_ref, args.enable_legacy_authorization)
583      else:
584        options = self.ParseUpdateOptions(args, locations)
585        op_ref = adapter.UpdateCluster(cluster_ref, options)
586
587    if not args.async_:
588      adapter.WaitForOperation(
589          op_ref,
590          'Updating {0}'.format(cluster_ref.clusterId),
591          timeout_s=args.timeout)
592
593      log.UpdatedResource(cluster_ref)
594      cluster_url = util.GenerateClusterUrl(cluster_ref)
595      log.status.Print('To inspect the contents of your cluster, go to: ' +
596                       cluster_url)
597
598      if (args.start_ip_rotation or args.complete_ip_rotation or
599          args.start_credential_rotation or args.complete_credential_rotation):
600        cluster = adapter.GetCluster(cluster_ref)
601        try:
602          util.ClusterConfig.Persist(cluster, cluster_ref.projectId)
603        except kconfig.MissingEnvVarError as error:
604          log.warning(error)
605
606  def IsClusterRequired(self, args):
607    """Returns if failure getting the cluster should be an error."""
608    return bool(
609        getattr(args, 'maintenance_window_end', False) or
610        getattr(args, 'clear_maintenance_window', False) or
611        getattr(args, 'add_maintenance_exclusion_end', False) or
612        getattr(args, 'remove_maintenance_exclusion', False) or
613        getattr(args, 'add_cross_connect_subnetworks', False) or
614        getattr(args, 'remove_cross_connect_subnetworks', False) or
615        getattr(args, 'clear_cross_connect_subnetworks', False))
616
617
618@base.ReleaseTracks(base.ReleaseTrack.BETA)
619class UpdateBeta(Update):
620  """Update cluster settings for an existing container cluster."""
621
622  @staticmethod
623  def Args(parser):
624    _AddCommonArgs(parser)
625    group = parser.add_mutually_exclusive_group(required=True)
626    _AddMutuallyExclusiveArgs(group, base.ReleaseTrack.BETA)
627    flags.AddClusterAutoscalingFlags(parser, group)
628    group_locations = group.add_mutually_exclusive_group()
629    _AddAdditionalZonesArg(group_locations, deprecated=True)
630    flags.AddNodeLocationsFlag(group_locations)
631    group_logging_monitoring = group.add_group()
632    flags.AddLoggingServiceFlag(group_logging_monitoring)
633    flags.AddMonitoringServiceFlag(group_logging_monitoring)
634    flags.AddEnableStackdriverKubernetesFlag(group)
635    flags.AddEnableLoggingMonitoringSystemOnlyFlag(group)
636    flags.AddEnableWorkloadMonitoringEapFlag(group)
637    flags.AddEnableMasterSignalsFlags(group)
638    flags.AddMasterAuthorizedNetworksFlags(
639        parser, enable_group_for_update=group)
640    flags.AddEnableLegacyAuthorizationFlag(group)
641    flags.AddStartIpRotationFlag(group)
642    flags.AddStartCredentialRotationFlag(group)
643    flags.AddCompleteIpRotationFlag(group)
644    flags.AddCompleteCredentialRotationFlag(group)
645    flags.AddUpdateLabelsFlag(group)
646    flags.AddRemoveLabelsFlag(group)
647    flags.AddNetworkPolicyFlags(group)
648    flags.AddDailyMaintenanceWindowFlag(group, add_unset_text=True)
649    flags.AddRecurringMaintenanceWindowFlags(group, is_update=True)
650    flags.AddPodSecurityPolicyFlag(group)
651    flags.AddEnableBinAuthzFlag(group)
652    flags.AddAutoprovisioningFlags(group)
653    flags.AddAutoscalingProfilesFlag(group)
654    flags.AddVerticalPodAutoscalingFlag(group)
655    flags.AddResourceUsageExportFlags(group, is_update=True)
656    flags.AddIstioConfigFlag(parser)
657    flags.AddCloudRunConfigFlag(parser)
658    flags.AddEnableIntraNodeVisibilityFlag(group)
659    flags.AddWorkloadIdentityFlags(
660        group, use_identity_provider=True, use_workload_certificates=True)
661    flags.AddWorkloadIdentityUpdateFlags(group, use_workload_certificates=True)
662    flags.AddGkeOidcFlag(group)
663    flags.AddDatabaseEncryptionFlag(group)
664    flags.AddDisableDatabaseEncryptionFlag(group)
665    flags.AddReleaseChannelFlag(group, is_update=True, hidden=False)
666    flags.AddEnableShieldedNodesFlags(group)
667    flags.AddTpuFlags(group, enable_tpu_service_networking=True)
668    flags.AddMasterGlobalAccessFlag(group, is_update=True)
669    flags.AddEnableGvnicFlag(group)
670    flags.AddDisableDefaultSnatFlag(group, for_cluster_create=False)
671    flags.AddNotificationConfigFlag(group)
672    flags.AddPrivateIpv6GoogleAccessTypeFlag('v1beta1', group, hidden=False)
673    flags.AddKubernetesObjectsExportConfig(group)
674    flags.AddDisableAutopilotFlag(group, hidden=True)
675    flags.AddILBSubsettingFlags(group, hidden=True)
676    flags.AddClusterDNSFlags(group, hidden=True)
677    flags.AddCrossConnectSubnetworksMutationFlags(group)
678
679  def ParseUpdateOptions(self, args, locations):
680    get_default = lambda key: getattr(args, key)
681    flags.ValidateNotificationConfigFlag(args)
682    opts = container_command_util.ParseUpdateOptionsBase(args, locations)
683    opts.enable_pod_security_policy = args.enable_pod_security_policy
684    opts.istio_config = args.istio_config
685    opts.cloud_run_config = flags.GetLegacyCloudRunFlag('{}_config', args,
686                                                        get_default)
687    opts.resource_usage_bigquery_dataset = args.resource_usage_bigquery_dataset
688    opts.enable_intra_node_visibility = args.enable_intra_node_visibility
689    opts.clear_resource_usage_bigquery_dataset = \
690        args.clear_resource_usage_bigquery_dataset
691    opts.enable_network_egress_metering = args.enable_network_egress_metering
692    opts.enable_resource_consumption_metering = args.enable_resource_consumption_metering
693    opts.workload_identity_certificate_authority = args.workload_identity_certificate_authority
694    opts.disable_workload_identity_certificates = args.disable_workload_identity_certificates
695    flags.ValidateIstioConfigUpdateArgs(args.istio_config, args.disable_addons)
696    flags.ValidateCloudRunConfigUpdateArgs(opts.cloud_run_config,
697                                           args.disable_addons)
698    if args.disable_addons and api_adapter.NODELOCALDNS in args.disable_addons:
699      # NodeLocalDNS is being enabled or disabled
700      console_io.PromptContinue(
701          message='Enabling/Disabling NodeLocal DNSCache causes a re-creation '
702          'of all cluster nodes at versions 1.15 or above. '
703          'This operation is long-running and will block other '
704          'operations on the cluster (including delete) until it has run '
705          'to completion.',
706          cancel_on_no=True)
707
708    opts.enable_stackdriver_kubernetes = args.enable_stackdriver_kubernetes
709    opts.enable_logging_monitoring_system_only = args.enable_logging_monitoring_system_only
710    opts.master_logs = args.master_logs
711    opts.no_master_logs = args.no_master_logs
712    opts.enable_master_metrics = args.enable_master_metrics
713    opts.release_channel = args.release_channel
714    opts.autoscaling_profile = args.autoscaling_profile
715
716    # Top-level update options are automatically forced to be
717    # mutually-exclusive, so we don't need special handling for these two.
718    opts.identity_provider = args.identity_provider
719    opts.enable_shielded_nodes = args.enable_shielded_nodes
720    opts.enable_tpu = args.enable_tpu
721    opts.tpu_ipv4_cidr = args.tpu_ipv4_cidr
722    opts.enable_tpu_service_networking = args.enable_tpu_service_networking
723    opts.enable_master_global_access = args.enable_master_global_access
724    opts.enable_gvnic = args.enable_gvnic
725    opts.disable_default_snat = args.disable_default_snat
726    opts.notification_config = args.notification_config
727    opts.kubernetes_objects_changes_target = args.kubernetes_objects_changes_target
728    opts.kubernetes_objects_snapshots_target = args.kubernetes_objects_snapshots_target
729    opts.enable_gke_oidc = args.enable_gke_oidc
730    opts.enable_workload_monitoring_eap = args.enable_workload_monitoring_eap
731    opts.disable_autopilot = args.disable_autopilot
732    opts.enable_l4_ilb_subsetting = args.enable_l4_ilb_subsetting
733    opts.cluster_dns = args.cluster_dns
734    opts.cluster_dns_scope = args.cluster_dns_scope
735    opts.cluster_dns_domain = args.cluster_dns_domain
736    if opts.cluster_dns and opts.cluster_dns.lower() == 'clouddns':
737      console_io.PromptContinue(
738          message='Enabling CloudDNS is a one-way operation. Once enabled, '
739          'this configuration cannot be disabled. '
740          'All the node-pools in the cluster need to be re-created by the user '
741          'to start using CloudDNS for DNS lookups. It is highly recommended to'
742          ' complete this step shortly after enabling CloudDNS.',
743          cancel_on_no=True)
744    return opts
745
746
747@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
748class UpdateAlpha(Update):
749  """Update cluster settings for an existing container cluster."""
750
751  @staticmethod
752  def Args(parser):
753    _AddCommonArgs(parser)
754    group = parser.add_mutually_exclusive_group(required=True)
755    _AddMutuallyExclusiveArgs(group, base.ReleaseTrack.ALPHA)
756    flags.AddClusterAutoscalingFlags(parser, group)
757    group_locations = group.add_mutually_exclusive_group()
758    _AddAdditionalZonesArg(group_locations, deprecated=True)
759    flags.AddNodeLocationsFlag(group_locations)
760    group_logging_monitoring = group.add_group()
761    flags.AddLoggingServiceFlag(group_logging_monitoring)
762    flags.AddMonitoringServiceFlag(group_logging_monitoring)
763    flags.AddEnableStackdriverKubernetesFlag(group)
764    flags.AddEnableLoggingMonitoringSystemOnlyFlag(group)
765    flags.AddEnableWorkloadMonitoringEapFlag(group)
766    flags.AddEnableMasterSignalsFlags(group)
767    flags.AddMasterAuthorizedNetworksFlags(
768        parser, enable_group_for_update=group)
769    flags.AddEnableLegacyAuthorizationFlag(group)
770    flags.AddStartIpRotationFlag(group)
771    flags.AddStartCredentialRotationFlag(group)
772    flags.AddCompleteIpRotationFlag(group)
773    flags.AddCompleteCredentialRotationFlag(group)
774    flags.AddUpdateLabelsFlag(group)
775    flags.AddRemoveLabelsFlag(group)
776    flags.AddNetworkPolicyFlags(group)
777    flags.AddAutoprovisioningFlags(group, hidden=False)
778    flags.AddAutoscalingProfilesFlag(group)
779    flags.AddDailyMaintenanceWindowFlag(group, add_unset_text=True)
780    flags.AddRecurringMaintenanceWindowFlags(group, is_update=True)
781    flags.AddPodSecurityPolicyFlag(group)
782    flags.AddEnableBinAuthzFlag(group)
783    flags.AddResourceUsageExportFlags(group, is_update=True)
784    flags.AddVerticalPodAutoscalingFlag(group)
785    flags.AddSecurityProfileForUpdateFlag(group)
786    flags.AddIstioConfigFlag(parser)
787    flags.AddCloudRunConfigFlag(parser)
788    flags.AddEnableIntraNodeVisibilityFlag(group)
789    flags.AddWorkloadIdentityFlags(
790        group, use_identity_provider=True, use_workload_certificates=True)
791    flags.AddWorkloadIdentityUpdateFlags(group, use_workload_certificates=True)
792    flags.AddGkeOidcFlag(group)
793    flags.AddDisableDefaultSnatFlag(group, for_cluster_create=False)
794    flags.AddDatabaseEncryptionFlag(group)
795    flags.AddDisableDatabaseEncryptionFlag(group)
796    flags.AddCostManagementConfigFlag(group, is_update=True)
797    flags.AddReleaseChannelFlag(group, is_update=True, hidden=False)
798    flags.AddEnableShieldedNodesFlags(group)
799    flags.AddTpuFlags(group, enable_tpu_service_networking=True)
800    flags.AddMasterGlobalAccessFlag(group, is_update=True)
801    flags.AddEnableGvnicFlag(group)
802    flags.AddNotificationConfigFlag(group)
803    flags.AddPrivateIpv6GoogleAccessTypeFlag('v1alpha1', group, hidden=False)
804    flags.AddKubernetesObjectsExportConfig(group)
805    flags.AddDisableAutopilotFlag(group, hidden=True)
806    flags.AddILBSubsettingFlags(group, hidden=True)
807    flags.AddClusterDNSFlags(group, hidden=True)
808    flags.AddCrossConnectSubnetworksMutationFlags(group)
809
810  def ParseUpdateOptions(self, args, locations):
811    get_default = lambda key: getattr(args, key)
812    flags.ValidateNotificationConfigFlag(args)
813    opts = container_command_util.ParseUpdateOptionsBase(args, locations)
814    opts.autoscaling_profile = args.autoscaling_profile
815    opts.enable_pod_security_policy = args.enable_pod_security_policy
816    opts.resource_usage_bigquery_dataset = args.resource_usage_bigquery_dataset
817    opts.clear_resource_usage_bigquery_dataset = \
818        args.clear_resource_usage_bigquery_dataset
819    opts.security_profile = args.security_profile
820    opts.istio_config = args.istio_config
821    opts.cloud_run_config = flags.GetLegacyCloudRunFlag('{}_config', args,
822                                                        get_default)
823    opts.enable_intra_node_visibility = args.enable_intra_node_visibility
824    opts.enable_network_egress_metering = args.enable_network_egress_metering
825    opts.enable_resource_consumption_metering = args.enable_resource_consumption_metering
826    opts.workload_identity_certificate_authority = args.workload_identity_certificate_authority
827    opts.disable_workload_identity_certificates = args.disable_workload_identity_certificates
828    flags.ValidateIstioConfigUpdateArgs(args.istio_config, args.disable_addons)
829    flags.ValidateCloudRunConfigUpdateArgs(opts.cloud_run_config,
830                                           args.disable_addons)
831    if args.disable_addons and api_adapter.NODELOCALDNS in args.disable_addons:
832      # NodeLocalDNS is being enabled or disabled
833      console_io.PromptContinue(
834          message='Enabling/Disabling NodeLocal DNSCache causes a re-creation '
835          'of all cluster nodes at versions 1.15 or above. '
836          'This operation is long-running and will block other '
837          'operations on the cluster (including delete) until it has run '
838          'to completion.',
839          cancel_on_no=True)
840    opts.enable_stackdriver_kubernetes = args.enable_stackdriver_kubernetes
841    opts.enable_logging_monitoring_system_only = args.enable_logging_monitoring_system_only
842    opts.no_master_logs = args.no_master_logs
843    opts.master_logs = args.master_logs
844    opts.enable_master_metrics = args.enable_master_metrics
845    opts.release_channel = args.release_channel
846    opts.enable_tpu = args.enable_tpu
847    opts.tpu_ipv4_cidr = args.tpu_ipv4_cidr
848    opts.enable_tpu_service_networking = args.enable_tpu_service_networking
849
850    # Top-level update options are automatically forced to be
851    # mutually-exclusive, so we don't need special handling for these two.
852    opts.identity_provider = args.identity_provider
853    opts.enable_shielded_nodes = args.enable_shielded_nodes
854    opts.disable_default_snat = args.disable_default_snat
855    opts.enable_cost_management = args.enable_cost_management
856    opts.enable_master_global_access = args.enable_master_global_access
857    opts.enable_gvnic = args.enable_gvnic
858    opts.notification_config = args.notification_config
859    opts.kubernetes_objects_changes_target = args.kubernetes_objects_changes_target
860    opts.kubernetes_objects_snapshots_target = args.kubernetes_objects_snapshots_target
861    opts.enable_gke_oidc = args.enable_gke_oidc
862    opts.enable_workload_monitoring_eap = args.enable_workload_monitoring_eap
863    opts.disable_autopilot = args.disable_autopilot
864    opts.enable_l4_ilb_subsetting = args.enable_l4_ilb_subsetting
865    opts.cluster_dns = args.cluster_dns
866    opts.cluster_dns_scope = args.cluster_dns_scope
867    opts.cluster_dns_domain = args.cluster_dns_domain
868    if opts.cluster_dns and opts.cluster_dns.lower() == 'clouddns':
869      console_io.PromptContinue(
870          message='Enabling CloudDNS is a one-way operation. Once enabled, '
871          'this configuration cannot be disabled.'
872          'All the node-pools in the cluster need to be re-created by the user '
873          'to start using CloudDNS for DNS lookups. It is highly recommended to'
874          ' complete this step shortly after enabling CloudDNS.',
875          cancel_on_no=True)
876
877    return opts
878