source_util.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. # -*- coding: utf-8 -*- #
  2. # Copyright 2018 Google Inc. 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. """'functions deploy' utilities for function source code."""
  16. from __future__ import absolute_import
  17. from __future__ import division
  18. from __future__ import unicode_literals
  19. import os
  20. import random
  21. import re
  22. import string
  23. from apitools.base.py import http_wrapper
  24. from apitools.base.py import transfer
  25. from googlecloudsdk.api_lib.functions import exceptions
  26. from googlecloudsdk.api_lib.functions import util as api_util
  27. from googlecloudsdk.api_lib.storage import storage_util
  28. from googlecloudsdk.command_lib.util import gcloudignore
  29. from googlecloudsdk.core import http as http_utils
  30. from googlecloudsdk.core import log
  31. from googlecloudsdk.core import properties
  32. from googlecloudsdk.core.util import archive
  33. from googlecloudsdk.core.util import files as file_utils
  34. from six.moves import range
  35. def _GcloudIgnoreCreationPredicate(directory):
  36. return gcloudignore.AnyFileOrDirExists(
  37. directory, gcloudignore.GIT_FILES + ['node_modules'])
  38. def _GetChooser(path):
  39. default_ignore_file = gcloudignore.DEFAULT_IGNORE_FILE + '\nnode_modules\n'
  40. return gcloudignore.GetFileChooserForDir(
  41. path, default_ignore_file=default_ignore_file,
  42. gcloud_ignore_creation_predicate=_GcloudIgnoreCreationPredicate)
  43. def _ValidateUnpackedSourceSize(path):
  44. """Validate size of unpacked source files."""
  45. chooser = _GetChooser(path)
  46. predicate = chooser.IsIncluded
  47. try:
  48. size_b = file_utils.GetTreeSizeBytes(path, predicate=predicate)
  49. except OSError as e:
  50. raise exceptions.FunctionsError(
  51. 'Error building source archive from path [{path}]. '
  52. 'Could not validate source files: [{error}]. '
  53. 'Please ensure that path [{path}] contains function code or '
  54. 'specify another directory with --source'.format(path=path, error=e))
  55. size_limit_mb = 512
  56. size_limit_b = size_limit_mb * 2 ** 20
  57. if size_b > size_limit_b:
  58. raise exceptions.OversizedDeployment(
  59. str(size_b) + 'B', str(size_limit_b) + 'B')
  60. def _CreateSourcesZipFile(zip_dir, source_path):
  61. """Prepare zip file with source of the function to upload.
  62. Args:
  63. zip_dir: str, directory in which zip file will be located. Name of the file
  64. will be `fun.zip`.
  65. source_path: str, directory containing the sources to be zipped.
  66. Returns:
  67. Path to the zip file (str).
  68. Raises:
  69. FunctionsError
  70. """
  71. api_util.ValidateDirectoryExistsOrRaiseFunctionError(source_path)
  72. _ValidateUnpackedSourceSize(source_path)
  73. zip_file_name = os.path.join(zip_dir, 'fun.zip')
  74. try:
  75. chooser = _GetChooser(source_path)
  76. predicate = chooser.IsIncluded
  77. archive.MakeZipFromDir(zip_file_name, source_path, predicate=predicate)
  78. except ValueError as e:
  79. raise exceptions.FunctionsError(
  80. 'Error creating a ZIP archive with the source code '
  81. 'for directory {0}: {1}'.format(source_path, str(e)))
  82. return zip_file_name
  83. def _GenerateRemoteZipFileName(function_name):
  84. suffix = ''.join(random.choice(string.ascii_lowercase) for _ in range(12))
  85. return '{0}-{1}-{2}.zip'.format(
  86. properties.VALUES.functions.region.Get(), function_name, suffix)
  87. def _UploadFileToGcs(source, function_ref, stage_bucket):
  88. """Upload local source files to GCS staging bucket."""
  89. zip_file = _GenerateRemoteZipFileName(function_ref.RelativeName())
  90. bucket_ref = storage_util.BucketReference.FromArgument(
  91. stage_bucket)
  92. gcs_url = storage_util.ObjectReference(bucket_ref, zip_file).ToUrl()
  93. upload_result = storage_util.RunGsutilCommand('cp', [source, gcs_url])
  94. if upload_result != 0:
  95. raise exceptions.FunctionsError(
  96. 'Failed to upload the function source code to the bucket {0}'
  97. .format(stage_bucket))
  98. return gcs_url
  99. def _AddDefaultBranch(source_archive_url):
  100. cloud_repo_pattern = (r'^https://source\.developers\.google\.com'
  101. r'/projects/[^/]+'
  102. r'/repos/[^/]+$')
  103. if re.match(cloud_repo_pattern, source_archive_url):
  104. return source_archive_url + '/moveable-aliases/master'
  105. return source_archive_url
  106. def _GetUploadUrl(messages, service, function_ref):
  107. request = (messages.
  108. CloudfunctionsProjectsLocationsFunctionsGenerateUploadUrlRequest)(
  109. parent='projects/{}/locations/{}'.format(
  110. function_ref.projectsId, function_ref.locationsId))
  111. response = service.GenerateUploadUrl(request)
  112. return response.uploadUrl
  113. def _CheckUploadStatus(status_code):
  114. """Validates that HTTP status for upload is 2xx."""
  115. return status_code // 100 == 2
  116. def _UploadFileToGeneratedUrl(source, messages, service, function_ref):
  117. """Upload function source to URL generated by API."""
  118. url = _GetUploadUrl(messages, service, function_ref)
  119. upload = transfer.Upload.FromFile(source,
  120. mime_type='application/zip')
  121. upload_request = http_wrapper.Request(
  122. url, http_method='PUT', headers={
  123. 'content-type': 'application/zip',
  124. # Magic header, request will fail without it.
  125. # Not documented at the moment this comment was being written.
  126. 'x-goog-content-length-range': '0,104857600',
  127. 'Content-Length': '{0:d}'.format(upload.total_size)})
  128. upload_request.body = upload.stream.read()
  129. response = http_wrapper.MakeRequest(
  130. http_utils.Http(), upload_request, retry_func=upload.retry_func,
  131. retries=upload.num_retries)
  132. if not _CheckUploadStatus(response.status_code):
  133. raise exceptions.FunctionsError(
  134. 'Failed to upload the function source code to signed url: {url}. '
  135. 'Status: [{code}:{detail}]'.format(url=url,
  136. code=response.status_code,
  137. detail=response.content))
  138. return url
  139. def UploadFile(source, stage_bucket, messages, service, function_ref):
  140. if stage_bucket:
  141. return _UploadFileToGcs(source, function_ref, stage_bucket)
  142. return _UploadFileToGeneratedUrl(source, messages, service, function_ref)
  143. def SetFunctionSourceProps(function, function_ref, source_arg, stage_bucket):
  144. """Add sources to function.
  145. Args:
  146. function: The function to add a source to.
  147. function_ref: The reference to the function.
  148. source_arg: Location of source code to deploy.
  149. stage_bucket: The name of the Google Cloud Storage bucket where source code
  150. will be stored.
  151. Returns:
  152. A list of fields on the function that have been changed.
  153. """
  154. function.sourceArchiveUrl = None
  155. function.sourceRepository = None
  156. function.sourceUploadUrl = None
  157. messages = api_util.GetApiMessagesModule()
  158. if source_arg is None:
  159. source_arg = '.'
  160. source_arg = source_arg or '.'
  161. if source_arg.startswith('gs://'):
  162. if not source_arg.endswith('.zip'):
  163. # Users may have .zip archives with unusual names, and we don't want to
  164. # prevent those from being deployed; the deployment should go through so
  165. # just warn here.
  166. log.warning(
  167. '[{}] does not end with extension `.zip`. '
  168. 'The `--source` argument must designate the zipped source archive '
  169. 'when providing a Google Cloud Storage URI.'.format(source_arg))
  170. function.sourceArchiveUrl = source_arg
  171. return ['sourceArchiveUrl']
  172. elif source_arg.startswith('https://'):
  173. function.sourceRepository = messages.SourceRepository(
  174. url=_AddDefaultBranch(source_arg)
  175. )
  176. return ['sourceRepository']
  177. with file_utils.TemporaryDirectory() as tmp_dir:
  178. zip_file = _CreateSourcesZipFile(tmp_dir, source_arg)
  179. service = api_util.GetApiClientInstance().projects_locations_functions
  180. upload_url = UploadFile(
  181. zip_file, stage_bucket, messages, service, function_ref)
  182. if upload_url.startswith('gs://'):
  183. function.sourceArchiveUrl = upload_url
  184. return ['sourceArchiveUrl']
  185. else:
  186. function.sourceUploadUrl = upload_url
  187. return ['sourceUploadUrl']