242 lines
8.2 KiB
Python
242 lines
8.2 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2016 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.
|
|
"""High-level client for interacting with the Cloud Build API."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import json
|
|
import time
|
|
|
|
from apitools.base.py import encoding
|
|
from googlecloudsdk.api_lib.cloudbuild import cloudbuild_util
|
|
from googlecloudsdk.api_lib.cloudbuild import logs as cloudbuild_logs
|
|
from googlecloudsdk.api_lib.util import requests
|
|
from googlecloudsdk.core import exceptions
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core import properties
|
|
from six.moves import range # pylint: disable=redefined-builtin
|
|
|
|
|
|
_ERROR_FORMAT_STRING = ('Error Response:{status_code? [{?}]}'
|
|
'{status_message? {?}}{url?\n{?}}'
|
|
'{details?\n\nDetails:\n{?}}')
|
|
|
|
|
|
def GetBuildProp(build_op, prop_key, required=False):
|
|
"""Extract the value of a build's prop_key from a build operation.
|
|
|
|
Args:
|
|
build_op: A Google Cloud Builder build operation.
|
|
prop_key: str, The property name.
|
|
required: If True, raise an OperationError if prop_key isn't present.
|
|
|
|
Returns:
|
|
The corresponding build operation value indexed by prop_key.
|
|
|
|
Raises:
|
|
OperationError: The required prop_key was not found.
|
|
"""
|
|
if build_op.metadata is not None:
|
|
for prop in build_op.metadata.additionalProperties:
|
|
if prop.key == 'build':
|
|
for build_prop in prop.value.object_value.properties:
|
|
if build_prop.key == prop_key:
|
|
string_value = build_prop.value.string_value
|
|
return string_value or build_prop.value
|
|
if required:
|
|
raise OperationError('Build operation does not contain required '
|
|
'property [{}]'.format(prop_key))
|
|
|
|
|
|
def _GetStatusFromOp(op):
|
|
"""Get the Cloud Build Status from an Operation object.
|
|
|
|
The op.response field is supposed to have a copy of the build object; however,
|
|
the wire JSON from the server doesn't get deserialized into an actual build
|
|
object. Instead, it is stored as a generic ResponseValue object, so we have
|
|
to root around a bit.
|
|
|
|
Args:
|
|
op: the Operation object from a CloudBuild build request.
|
|
|
|
Returns:
|
|
string status, likely "SUCCESS" or "ERROR".
|
|
"""
|
|
if op.response and op.response.additionalProperties:
|
|
for prop in op.response.additionalProperties:
|
|
if prop.key == 'status':
|
|
return prop.value.string_value
|
|
return 'UNKNOWN'
|
|
|
|
|
|
class BuildFailedError(exceptions.Error):
|
|
"""Raised when a Google Cloud Builder build fails."""
|
|
|
|
|
|
class OperationTimeoutError(exceptions.Error):
|
|
"""Raised when an operation times out."""
|
|
pass
|
|
|
|
|
|
class OperationError(exceptions.Error):
|
|
"""Raised when an operation contains an error."""
|
|
pass
|
|
|
|
|
|
class CloudBuildClient(object):
|
|
"""High-level client for interacting with the Cloud Build API."""
|
|
|
|
_RETRY_INTERVAL = 1
|
|
_MAX_RETRIES = 60 * 60
|
|
CLOUDBUILD_SUCCESS = 'SUCCESS'
|
|
CLOUDBUILD_LOGFILE_FMT_STRING = 'log-{build_id}.txt'
|
|
|
|
def __init__(self, client=None, messages=None):
|
|
self.client = client or cloudbuild_util.GetClientInstance()
|
|
self.messages = messages or cloudbuild_util.GetMessagesModule()
|
|
|
|
def ExecuteCloudBuildAsync(self, build, project=None):
|
|
"""Execute a call to CloudBuild service and return the build operation.
|
|
|
|
|
|
Args:
|
|
build: Build object. The Build to execute.
|
|
project: The project to execute, or None to use the current project
|
|
property.
|
|
|
|
Raises:
|
|
BuildFailedError: when the build fails.
|
|
|
|
Returns:
|
|
build_op, an in-progress build operation.
|
|
"""
|
|
if project is None:
|
|
project = properties.VALUES.core.project.Get(required=True)
|
|
|
|
build_op = self.client.projects_builds.Create(
|
|
self.messages.CloudbuildProjectsBuildsCreateRequest(
|
|
projectId=project,
|
|
build=build,))
|
|
return build_op
|
|
|
|
def ExecuteCloudBuild(self, build, project=None):
|
|
"""Execute a call to CloudBuild service and wait for it to finish.
|
|
|
|
|
|
Args:
|
|
build: Build object. The Build to execute.
|
|
project: The project to execute, or None to use the current project
|
|
property.
|
|
|
|
Raises:
|
|
BuildFailedError: when the build fails.
|
|
"""
|
|
|
|
build_op = self.ExecuteCloudBuildAsync(build, project)
|
|
self.WaitAndStreamLogs(build_op)
|
|
|
|
def WaitAndStreamLogs(self, build_op):
|
|
"""Wait for a Cloud Build to finish, streaming logs if possible."""
|
|
build_id = GetBuildProp(build_op, 'id', required=True)
|
|
logs_uri = GetBuildProp(build_op, 'logUrl')
|
|
logs_bucket = GetBuildProp(build_op, 'logsBucket')
|
|
log.status.Print(
|
|
'Started cloud build [{build_id}].'.format(build_id=build_id))
|
|
log_loc = 'in the Cloud Console.'
|
|
log_tailer = None
|
|
if logs_bucket:
|
|
log_object = self.CLOUDBUILD_LOGFILE_FMT_STRING.format(build_id=build_id)
|
|
log_tailer = cloudbuild_logs.GCSLogTailer(
|
|
bucket=logs_bucket,
|
|
obj=log_object)
|
|
if logs_uri:
|
|
log.status.Print('To see logs in the Cloud Console: ' + logs_uri)
|
|
log_loc = 'at ' + logs_uri
|
|
else:
|
|
log.status.Print('Logs can be found in the Cloud Console.')
|
|
|
|
callback = None
|
|
if log_tailer:
|
|
callback = log_tailer.Poll
|
|
|
|
try:
|
|
op = self.WaitForOperation(operation=build_op, retry_callback=callback)
|
|
except OperationTimeoutError:
|
|
log.debug('', exc_info=True)
|
|
raise BuildFailedError('Cloud build timed out. Check logs ' + log_loc)
|
|
|
|
# Poll the logs one final time to ensure we have everything. We know this
|
|
# final poll will get the full log contents because GCS is strongly
|
|
# consistent and Cloud Build waits for logs to finish pushing before
|
|
# marking the build complete.
|
|
if log_tailer:
|
|
log_tailer.Poll(is_last=True)
|
|
|
|
final_status = _GetStatusFromOp(op)
|
|
if final_status != self.CLOUDBUILD_SUCCESS:
|
|
message = requests.ExtractErrorMessage(
|
|
encoding.MessageToPyValue(op.error))
|
|
raise BuildFailedError('Cloud build failed. Check logs ' + log_loc
|
|
+ ' Failure status: ' + final_status + ': '
|
|
+ message)
|
|
|
|
def WaitForOperation(self, operation, retry_callback=None):
|
|
"""Wait until the operation is complete or times out.
|
|
|
|
This does not use the core api_lib.util.waiter because the cloud build logs
|
|
serve as a progress tracker.
|
|
|
|
Args:
|
|
operation: The operation resource to wait on
|
|
retry_callback: A callback to be executed before each retry, if desired.
|
|
Returns:
|
|
The operation resource when it has completed
|
|
Raises:
|
|
OperationTimeoutError: when the operation polling times out
|
|
"""
|
|
|
|
completed_operation = self._PollUntilDone(operation, retry_callback)
|
|
if not completed_operation:
|
|
raise OperationTimeoutError(('Operation [{0}] timed out. This operation '
|
|
'may still be underway.').format(
|
|
operation.name))
|
|
|
|
return completed_operation
|
|
|
|
def _PollUntilDone(self, operation, retry_callback):
|
|
"""Polls the operation resource until it is complete or times out."""
|
|
if operation.done:
|
|
return operation
|
|
|
|
request_type = self.client.operations.GetRequestType('Get')
|
|
request = request_type(name=operation.name)
|
|
|
|
for _ in range(self._MAX_RETRIES):
|
|
operation = self.client.operations.Get(request)
|
|
if operation.done:
|
|
log.debug('Operation [{0}] complete. Result: {1}'.format(
|
|
operation.name,
|
|
json.dumps(encoding.MessageToDict(operation), indent=4)))
|
|
return operation
|
|
log.debug('Operation [{0}] not complete. Waiting {1}s.'.format(
|
|
operation.name, self._RETRY_INTERVAL))
|
|
time.sleep(self._RETRY_INTERVAL)
|
|
if retry_callback is not None:
|
|
retry_callback()
|
|
|
|
return None
|