1# -*- coding: utf-8 -*- #
2# Copyright 2017 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"""Command to set service account and scopes for an instance resource."""
16
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import unicode_literals
20
21from googlecloudsdk.api_lib.compute import base_classes
22from googlecloudsdk.api_lib.compute import constants
23from googlecloudsdk.calliope import base
24from googlecloudsdk.command_lib.compute import flags as compute_flags
25from googlecloudsdk.command_lib.compute import scope as compute_scope
26from googlecloudsdk.command_lib.compute.instances import exceptions
27from googlecloudsdk.command_lib.compute.instances import flags
28
29
30@base.ReleaseTracks(base.ReleaseTrack.GA)
31class SetServiceAccount(base.SilentCommand):
32  """Set service account and scopes for a Compute Engine instance."""
33
34  detailed_help = {
35      'EXAMPLES': """
36  To set a service account with `pubsub` and `trace` scopes, run:
37
38    $ {command} example-instance --scopes=pubsub,trace --zone=us-central1-b --service-account=example-account
39  """}
40
41  def __init__(self, *args, **kwargs):
42    super(self.__class__, self).__init__(*args, **kwargs)
43    self._instance = None
44
45  @staticmethod
46  def Args(parser):
47    flags.INSTANCE_ARG.AddArgument(parser)
48    flags.AddServiceAccountAndScopeArgs(parser, True)
49
50  def _get_instance(self, instance_ref, client):
51    """Return cached instance if there isn't one fetch referrenced one."""
52    if not self._instance:
53      request = (client.apitools_client.instances, 'Get',
54                 client.messages.ComputeInstancesGetRequest(
55                     **instance_ref.AsDict()))
56      instance = client.MakeRequests(requests=[request])
57
58      self._instance = instance[0]
59
60    return self._instance
61
62  def _original_email(self, instance_ref, client):
63    """Return email of service account instance is using."""
64    instance = self._get_instance(instance_ref, client)
65    if instance is None:
66      return None
67    orignal_service_accounts = instance.serviceAccounts
68    if orignal_service_accounts:
69      return orignal_service_accounts[0].email
70    return None
71
72  def _original_scopes(self, instance_ref, client):
73    """Return scopes instance is using."""
74    instance = self._get_instance(instance_ref, client)
75    if instance is None:
76      return []
77    orignal_service_accounts = instance.serviceAccounts
78    result = []
79    for accounts in orignal_service_accounts:
80      result += accounts.scopes
81    return result
82
83  def _email(self, args, instance_ref, client):
84    """Return email to set as service account for the instance."""
85    if args.no_service_account:
86      return None
87    if args.service_account:
88      return args.service_account
89    return self._original_email(instance_ref, client)
90
91  def _unprocessed_scopes(self, args, instance_ref, client):
92    """Return scopes to set for the instance."""
93    if args.no_scopes:
94      return []
95    if args.scopes is not None:  # Empty list goes here
96      return args.scopes
97    return self._original_scopes(instance_ref, client)
98
99  def _scopes(self, args, instance_ref, client):
100    """Get list of scopes to be assigned to the instance.
101
102    Args:
103      args: parsed command  line arguments.
104      instance_ref: reference to the instance to which scopes will be assigned.
105      client: a compute_holder.client instance
106
107    Returns:
108      List of scope urls extracted from args, with scope aliases expanded.
109    """
110    result = []
111    for unprocessed_scope in self._unprocessed_scopes(args,
112                                                      instance_ref, client):
113      scope = constants.SCOPES.get(unprocessed_scope, [unprocessed_scope])
114      result.extend(scope)
115    return result
116
117  def Run(self, args):
118    compute_holder = base_classes.ComputeApiHolder(self.ReleaseTrack())
119    client = compute_holder.client
120
121    flags.ValidateServiceAccountAndScopeArgs(args)
122
123    instance_ref = flags.INSTANCE_ARG.ResolveAsResource(
124        args, compute_holder.resources,
125        default_scope=compute_scope.ScopeEnum.ZONE,
126        scope_lister=compute_flags.GetDefaultScopeLister(client))
127
128    email = self._email(args, instance_ref, client)
129    scopes = self._scopes(args, instance_ref, client)
130
131    if scopes and not email:
132      raise exceptions.ScopesWithoutServiceAccountException(
133          'Can not set scopes when there is no service acoount.')
134
135    request = client.messages.ComputeInstancesSetServiceAccountRequest(
136        instancesSetServiceAccountRequest=(
137            client.messages.InstancesSetServiceAccountRequest(
138                email=email,
139                scopes=scopes,
140            )
141        ),
142        project=instance_ref.project,
143        zone=instance_ref.zone,
144        instance=instance_ref.Name()
145    )
146
147    return client.MakeRequests([(
148        client.apitools_client.instances,
149        'SetServiceAccount',
150        request)])
151