1# -*- coding: utf-8 -*- # 2# Copyright 2018 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 16"""Shared resource flags for Cloud Run commands.""" 17 18from __future__ import absolute_import 19from __future__ import division 20from __future__ import print_function 21from __future__ import unicode_literals 22 23import abc 24import os 25import re 26 27from googlecloudsdk.api_lib.run import global_methods 28from googlecloudsdk.calliope.concepts import concepts 29from googlecloudsdk.calliope.concepts import deps 30from googlecloudsdk.command_lib.run import exceptions 31from googlecloudsdk.command_lib.run import platforms 32from googlecloudsdk.command_lib.util.concepts import presentation_specs 33from googlecloudsdk.core import log 34from googlecloudsdk.core import properties 35from googlecloudsdk.core import resources 36from googlecloudsdk.core.console import console_io 37 38 39class PromptFallthrough(deps.Fallthrough): 40 """Fall through to reading from an interactive prompt.""" 41 42 def __init__(self, hint): 43 super(PromptFallthrough, self).__init__(function=None, hint=hint) 44 45 @abc.abstractmethod 46 def _Prompt(self, parsed_args): 47 pass 48 49 def _Call(self, parsed_args): 50 if not console_io.CanPrompt(): 51 return None 52 return self._Prompt(parsed_args) 53 54 55def GenerateServiceName(image): 56 """Produce a valid default service name. 57 58 Converts a file path or image path into a reasonable default service name by 59 stripping file path delimeters, image tags, and image hashes. 60 For example, the image name 'gcr.io/myproject/myimage:latest' would produce 61 the service name 'myimage'. 62 63 Args: 64 image: str, The container path. 65 66 Returns: 67 A valid Cloud Run service name. 68 """ 69 base_name = os.path.basename(image.rstrip(os.sep)) 70 base_name = base_name.split(':')[0] # Discard image tag if present. 71 base_name = base_name.split('@')[0] # Disacard image hash if present. 72 # Remove non-supported special characters. 73 return re.sub(r'[^a-zA-Z0-9-]', '', base_name).strip('-').lower() 74 75 76class ResourcePromptFallthrough(PromptFallthrough): 77 """Fall through to reading the resource name from an interactive prompt.""" 78 79 def __init__(self, resource_type_lower): 80 super(ResourcePromptFallthrough, self).__init__( 81 'specify the {} name from an interactive prompt'.format( 82 resource_type_lower)) 83 self.resource_type_lower = resource_type_lower 84 85 def _Prompt(self, parsed_args): 86 image = None 87 if hasattr(parsed_args, 'image'): 88 image = parsed_args.image 89 message = self.resource_type_lower.capitalize() + ' name' 90 if image: 91 default_name = GenerateServiceName(image) 92 service_name = console_io.PromptWithDefault( 93 message=message, default=default_name) 94 else: 95 service_name = console_io.PromptResponse(message='{}: '.format(message)) 96 return service_name 97 98 99class ServicePromptFallthrough(ResourcePromptFallthrough): 100 101 def __init__(self): 102 super(ServicePromptFallthrough, self).__init__('service') 103 104 105class JobPromptFallthrough(ResourcePromptFallthrough): 106 107 def __init__(self): 108 super(JobPromptFallthrough, self).__init__('job') 109 110 111class DefaultFallthrough(deps.Fallthrough): 112 """Use the namespace "default". 113 114 For Knative only. 115 116 For Cloud Run, raises an ArgumentError if project not set. 117 """ 118 119 def __init__(self): 120 super(DefaultFallthrough, self).__init__( 121 function=None, 122 hint='For Cloud Run on Kubernetes Engine, defaults to "default". ' 123 'Otherwise, defaults to project ID.') 124 125 def _Call(self, parsed_args): 126 if (platforms.GetPlatform() == platforms.PLATFORM_GKE or 127 platforms.GetPlatform() == platforms.PLATFORM_KUBERNETES): 128 return 'default' 129 elif not (getattr(parsed_args, 'project', None) or 130 properties.VALUES.core.project.Get()): 131 # HACK: Compensate for how "namespace" is actually "project" in Cloud Run 132 # by providing an error message explicitly early here. 133 raise exceptions.ArgumentError( 134 'The [project] resource is not properly specified. ' 135 'Please specify the argument [--project] on the command line or ' 136 'set the property [core/project].') 137 return None 138 139 140def NamespaceAttributeConfig(): 141 return concepts.ResourceParameterAttributeConfig( 142 name='namespace', 143 help_text='Specific to Cloud Run for Anthos: ' 144 'Kubernetes namespace for the {resource}.', 145 fallthroughs=[ 146 deps.PropertyFallthrough(properties.VALUES.run.namespace), 147 DefaultFallthrough(), 148 deps.ArgFallthrough('project'), 149 deps.PropertyFallthrough(properties.VALUES.core.project), 150 ]) 151 152 153def ServiceAttributeConfig(prompt=False): 154 """Attribute config with fallthrough prompt only if requested.""" 155 if prompt: 156 fallthroughs = [ServicePromptFallthrough()] 157 else: 158 fallthroughs = [] 159 return concepts.ResourceParameterAttributeConfig( 160 name='service', 161 help_text='Service for the {resource}.', 162 fallthroughs=fallthroughs) 163 164 165def ConfigurationAttributeConfig(): 166 return concepts.ResourceParameterAttributeConfig( 167 name='configuration', 168 help_text='Configuration for the {resource}.') 169 170 171def RouteAttributeConfig(): 172 return concepts.ResourceParameterAttributeConfig( 173 name='route', 174 help_text='Route for the {resource}.') 175 176 177def RevisionAttributeConfig(): 178 return concepts.ResourceParameterAttributeConfig( 179 name='revision', 180 help_text='Revision for the {resource}.') 181 182 183def DomainAttributeConfig(): 184 return concepts.ResourceParameterAttributeConfig( 185 name='domain', 186 help_text='Name of the domain to be mapped to.') 187 188 189def JobAttributeConfig(prompt=False): 190 if prompt: 191 fallthroughs = [JobPromptFallthrough()] 192 else: 193 fallthroughs = [] 194 return concepts.ResourceParameterAttributeConfig( 195 name='jobs', 196 help_text='Job for the {resource}.', 197 fallthroughs=fallthroughs) 198 199 200class ClusterPromptFallthrough(PromptFallthrough): 201 """Fall through to reading the cluster name from an interactive prompt.""" 202 203 def __init__(self): 204 super(ClusterPromptFallthrough, self).__init__( 205 'specify the cluster from a list of available clusters') 206 207 def _Prompt(self, parsed_args): 208 """Fallthrough to reading the cluster name from an interactive prompt. 209 210 Only prompt for cluster name if the user-specified platform is GKE. 211 212 Args: 213 parsed_args: Namespace, the args namespace. 214 215 Returns: 216 A cluster name string 217 """ 218 if platforms.GetPlatform() != platforms.PLATFORM_GKE: 219 return 220 221 project = properties.VALUES.core.project.Get(required=True) 222 cluster_location = ( 223 getattr(parsed_args, 'cluster_location', None) or 224 properties.VALUES.run.cluster_location.Get()) 225 cluster_location_msg = ' in [{}]'.format( 226 cluster_location) if cluster_location else '' 227 228 cluster_refs = global_methods.MultiTenantClustersForProject( 229 project, cluster_location) 230 if not cluster_refs: 231 raise exceptions.ConfigurationError( 232 'No compatible clusters found{}. ' 233 'Ensure your cluster has Cloud Run enabled.'.format( 234 cluster_location_msg)) 235 236 cluster_refs_descs = [ 237 self._GetClusterDescription(c, cluster_location, project) 238 for c in cluster_refs 239 ] 240 241 idx = console_io.PromptChoice( 242 cluster_refs_descs, 243 message='GKE cluster{}:'.format(cluster_location_msg), 244 cancel_option=True) 245 246 cluster_ref = cluster_refs[idx] 247 248 if cluster_location: 249 location_help_text = '' 250 else: 251 location_help_text = ( 252 ' && gcloud config set run/cluster_location {}'.format( 253 cluster_ref.zone)) 254 255 cluster_name = cluster_ref.Name() 256 257 if cluster_ref.projectId != project: 258 cluster_name = cluster_ref.RelativeName() 259 location_help_text = '' 260 261 log.status.Print('To make this the default cluster, run ' 262 '`gcloud config set run/cluster {cluster}' 263 '{location}`.\n'.format( 264 cluster=cluster_name, location=location_help_text)) 265 return cluster_ref.SelfLink() 266 267 def _GetClusterDescription(self, cluster, cluster_location, project): 268 """Description of cluster for prompt.""" 269 270 response = cluster.Name() 271 if not cluster_location: 272 response = '{} in {}'.format(response, cluster.zone) 273 if project != cluster.projectId: 274 response = '{} in {}'.format(response, cluster.projectId) 275 276 return response 277 278 279def ClusterAttributeConfig(): 280 return concepts.ResourceParameterAttributeConfig( 281 name='cluster', 282 help_text='Name of the Kubernetes Engine cluster to use. ' 283 'Alternatively, set the property [run/cluster].', 284 fallthroughs=[ 285 deps.PropertyFallthrough(properties.VALUES.run.cluster), 286 ClusterPromptFallthrough() 287 ]) 288 289 290class ClusterLocationPromptFallthrough(PromptFallthrough): 291 """Fall through to reading the cluster name from an interactive prompt.""" 292 293 def __init__(self): 294 super(ClusterLocationPromptFallthrough, self).__init__( 295 'specify the cluster location from a list of available zones') 296 297 def _Prompt(self, parsed_args): 298 """Fallthrough to reading the cluster location from an interactive prompt. 299 300 Only prompt for cluster location if the user-specified platform is GKE 301 and if cluster name is already defined. 302 303 Args: 304 parsed_args: Namespace, the args namespace. 305 306 Returns: 307 A cluster location string 308 """ 309 cluster_name = ( 310 getattr(parsed_args, 'cluster', None) or 311 properties.VALUES.run.cluster.Get()) 312 if platforms.GetPlatform() == platforms.PLATFORM_GKE and cluster_name: 313 clusters = [ 314 c for c in global_methods.ListClusters() if c.name == cluster_name 315 ] 316 if not clusters: 317 raise exceptions.ConfigurationError( 318 'No cluster locations found for cluster [{}]. ' 319 'Ensure your clusters have Cloud Run enabled.' 320 .format(cluster_name)) 321 cluster_locations = [c.zone for c in clusters] 322 idx = console_io.PromptChoice( 323 cluster_locations, 324 message='GKE cluster location for [{}]:'.format( 325 cluster_name), 326 cancel_option=True) 327 location = cluster_locations[idx] 328 log.status.Print( 329 'To make this the default cluster location, run ' 330 '`gcloud config set run/cluster_location {}`.\n'.format(location)) 331 return location 332 333 334def ClusterLocationAttributeConfig(): 335 return concepts.ResourceParameterAttributeConfig( 336 name='location', 337 help_text='Zone in which the {resource} is located. ' 338 'Alternatively, set the property [run/cluster_location].', 339 fallthroughs=[ 340 deps.PropertyFallthrough(properties.VALUES.run.cluster_location), 341 ClusterLocationPromptFallthrough() 342 ]) 343 344 345def GetClusterResourceSpec(): 346 return concepts.ResourceSpec( 347 'container.projects.zones.clusters', 348 projectId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG, 349 zone=ClusterLocationAttributeConfig(), 350 clusterId=ClusterAttributeConfig(), 351 resource_name='cluster') 352 353 354def GetServiceResourceSpec(prompt=False): 355 return concepts.ResourceSpec( 356 'run.namespaces.services', 357 namespacesId=NamespaceAttributeConfig(), 358 servicesId=ServiceAttributeConfig(prompt), 359 resource_name='service') 360 361 362def GetConfigurationResourceSpec(): 363 return concepts.ResourceSpec( 364 'run.namespaces.configurations', 365 namespacesId=NamespaceAttributeConfig(), 366 configurationsId=ConfigurationAttributeConfig(), 367 resource_name='configuration') 368 369 370def GetRouteResourceSpec(): 371 return concepts.ResourceSpec( 372 'run.namespaces.routes', 373 namespacesId=NamespaceAttributeConfig(), 374 routesId=RouteAttributeConfig(), 375 resource_name='route') 376 377 378def GetRevisionResourceSpec(): 379 return concepts.ResourceSpec( 380 'run.namespaces.revisions', 381 namespacesId=NamespaceAttributeConfig(), 382 revisionsId=RevisionAttributeConfig(), 383 resource_name='revision') 384 385 386def GetDomainMappingResourceSpec(): 387 return concepts.ResourceSpec( 388 'run.namespaces.domainmappings', 389 namespacesId=NamespaceAttributeConfig(), 390 domainmappingsId=DomainAttributeConfig(), 391 resource_name='DomainMapping') 392 393 394def GetJobResourceSpec(prompt=False): 395 return concepts.ResourceSpec( 396 'run.namespaces.jobs', 397 namespacesId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG, 398 jobsId=JobAttributeConfig(prompt=prompt), 399 resource_name='Job', 400 api_version='v1alpha1') 401 402 403def GetNamespaceResourceSpec(): 404 """Returns a resource spec for the namespace.""" 405 # TODO(b/148817410): Remove this when the api has been split. 406 # This try/except block is needed because the v1alpha1 and v1 run apis 407 # have different collection names for the namespaces. 408 try: 409 return concepts.ResourceSpec( 410 'run.namespaces', 411 namespacesId=NamespaceAttributeConfig(), 412 resource_name='namespace') 413 except resources.InvalidCollectionException: 414 return concepts.ResourceSpec( 415 'run.api.v1.namespaces', 416 namespacesId=NamespaceAttributeConfig(), 417 resource_name='namespace') 418 419 420CLUSTER_PRESENTATION = presentation_specs.ResourcePresentationSpec( 421 '--cluster', 422 GetClusterResourceSpec(), 423 'Kubernetes Engine cluster to connect to.', 424 required=False, 425 prefixes=True) 426