173 lines
5.8 KiB
Python
173 lines
5.8 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.
|
|
"""Parse cloudbuild config files.
|
|
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import re
|
|
from googlecloudsdk.api_lib.cloudbuild import cloudbuild_util
|
|
from googlecloudsdk.core import exceptions
|
|
|
|
|
|
# Don't apply camel case to keys for dict or list values with these field names.
|
|
# These correspond to map fields in our proto message, where we expect keys to
|
|
# be sent exactly as the user typed them, without transformation to camelCase.
|
|
_SKIP_CAMEL_CASE = [
|
|
'secretEnv', 'secret_env', 'substitutions', 'envMap', 'env_map'
|
|
]
|
|
|
|
# Regex for a valid user-defined substitution variable.
|
|
_BUILTIN_SUBSTITUTION_REGEX = re.compile('^_[A-Z0-9_]+$')
|
|
|
|
# What we call cloudbuild.yaml for error messages that try to parse it.
|
|
_BUILD_CONFIG_FRIENDLY_NAME = 'build config'
|
|
|
|
|
|
class InvalidBuildConfigException(exceptions.Error):
|
|
"""Build config message is not valid.
|
|
|
|
"""
|
|
|
|
def __init__(self, path, msg):
|
|
msg = 'validating {path} as build config: {msg}'.format(
|
|
path=path,
|
|
msg=msg,
|
|
)
|
|
super(InvalidBuildConfigException, self).__init__(msg)
|
|
|
|
|
|
def FinalizeCloudbuildConfig(build, path, params=None, no_source=None):
|
|
"""Validate the given build message, and merge substitutions.
|
|
|
|
Args:
|
|
build: The build message to finalize.
|
|
path: The path of the original build config, for error messages.
|
|
params: Any additional substitution parameters as a dict.
|
|
no_source: CLI flag value for --no-source. If set, the build config can
|
|
provide a remote build source.
|
|
|
|
Raises:
|
|
InvalidBuildConfigException: If the build config is invalid.
|
|
|
|
Returns:
|
|
The valid build message with substitutions complete.
|
|
"""
|
|
subst_value = build.substitutions
|
|
if subst_value is None:
|
|
subst_value = build.SubstitutionsValue()
|
|
if params is None:
|
|
params = {}
|
|
|
|
# Convert substitutions value to dict temporarily.
|
|
subst_dict = {}
|
|
for kv in subst_value.additionalProperties:
|
|
subst_dict[kv.key] = kv.value
|
|
|
|
# Validate the substitution keys in the message.
|
|
for key in subst_dict:
|
|
if not _BUILTIN_SUBSTITUTION_REGEX.match(key):
|
|
raise InvalidBuildConfigException(
|
|
path,
|
|
'substitution key {} does not respect format {}'.format(
|
|
key, _BUILTIN_SUBSTITUTION_REGEX.pattern
|
|
),
|
|
)
|
|
|
|
# Merge the substitutions passed in the flag.
|
|
subst_dict.update(params)
|
|
|
|
# Convert substitutions dict back into value, and store it.
|
|
# Sort so that tests work.
|
|
subst_value = build.SubstitutionsValue()
|
|
for key, value in sorted(subst_dict.items()):
|
|
ap = build.SubstitutionsValue.AdditionalProperty()
|
|
ap.key = key
|
|
ap.value = value
|
|
subst_value.additionalProperties.append(ap)
|
|
if subst_value.additionalProperties:
|
|
build.substitutions = subst_value
|
|
|
|
# Some problems can be caught before talking to the cloudbuild service.
|
|
if not no_source and build.source:
|
|
raise InvalidBuildConfigException(
|
|
path, 'config cannot specify source without the flag --no-source'
|
|
)
|
|
if not build.remoteConfig and not build.steps:
|
|
raise InvalidBuildConfigException(
|
|
path,
|
|
'config must list at least one step or specify remote_config',
|
|
)
|
|
return build
|
|
|
|
|
|
def LoadCloudbuildConfigFromStream(
|
|
stream,
|
|
messages,
|
|
params=None,
|
|
path=None,
|
|
):
|
|
"""Load a cloudbuild config file into a Build message.
|
|
|
|
Args:
|
|
stream: file-like object containing the JSON or YAML data to be decoded.
|
|
messages: module, The messages module that has a Build type.
|
|
params: dict, parameters to substitute into a templated Build spec.
|
|
path: str or None. Optional path to be used in error messages.
|
|
|
|
Raises:
|
|
ParserError: If there was a problem parsing the stream as a dict.
|
|
ParseProtoException: If there was a problem interpreting the stream as the
|
|
given message type.
|
|
InvalidBuildConfigException: If the build config has illegal values.
|
|
|
|
Returns:
|
|
Build message, The build that got decoded.
|
|
"""
|
|
build = cloudbuild_util.LoadMessageFromStream(stream, messages.Build,
|
|
_BUILD_CONFIG_FRIENDLY_NAME,
|
|
_SKIP_CAMEL_CASE, path)
|
|
build = FinalizeCloudbuildConfig(build, path, params)
|
|
return build
|
|
|
|
|
|
def LoadCloudbuildConfigFromPath(path, messages, params=None, no_source=None):
|
|
"""Load a cloudbuild config file into a Build message.
|
|
|
|
Args:
|
|
path: str. Path to the JSON or YAML data to be decoded.
|
|
messages: module, The messages module that has a Build type.
|
|
params: dict, parameters to substitute into a templated Build spec.
|
|
no_source: CLI flag value for --no-source. If set, the build config can
|
|
provide a remote build source.
|
|
|
|
Raises:
|
|
files.MissingFileError: If the file does not exist.
|
|
ParserError: If there was a problem parsing the file as a dict.
|
|
ParseProtoException: If there was a problem interpreting the file as the
|
|
given message type.
|
|
InvalidBuildConfigException: If the build config has illegal values.
|
|
|
|
Returns:
|
|
Build message, The build that got decoded.
|
|
"""
|
|
build = cloudbuild_util.LoadMessageFromPath(
|
|
path, messages.Build, _BUILD_CONFIG_FRIENDLY_NAME, _SKIP_CAMEL_CASE)
|
|
build = FinalizeCloudbuildConfig(build, path, params, no_source)
|
|
return build
|