mirror of https://github.com/helm/helm
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
383 lines
12 KiB
383 lines
12 KiB
#!/usr/bin/env python
|
|
#
|
|
# Copyright 2015 The Kubernetes Authors 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.
|
|
|
|
"""Template expansion utilities."""
|
|
|
|
import os.path
|
|
import sys
|
|
import traceback
|
|
|
|
import jinja2
|
|
import yaml
|
|
|
|
from sandbox_loader import FileAccessRedirector
|
|
|
|
import schema_validation
|
|
|
|
|
|
def Expand(config, imports=None, env=None, validate_schema=False):
|
|
"""Expand the configuration with imports.
|
|
|
|
Args:
|
|
config: string, the raw config to be expanded.
|
|
imports: map from import file name, e.g. "helpers/constants.py" to
|
|
its contents.
|
|
env: map from string to string, the map of environment variable names
|
|
to their values
|
|
validate_schema: True to run schema validation; False otherwise
|
|
Returns:
|
|
YAML containing the expanded configuration and its layout, in the following
|
|
format:
|
|
|
|
config:
|
|
...
|
|
layout:
|
|
...
|
|
|
|
Raises:
|
|
ExpansionError: if there is any error occurred during expansion
|
|
"""
|
|
try:
|
|
return _Expand(config, imports=imports, env=env,
|
|
validate_schema=validate_schema)
|
|
except Exception as e:
|
|
# print traceback.format_exc()
|
|
raise ExpansionError('config', str(e))
|
|
|
|
|
|
def _Expand(config, imports=None, env=None, validate_schema=False):
|
|
"""Expand the configuration with imports."""
|
|
|
|
FileAccessRedirector.redirect(imports)
|
|
|
|
yaml_config = None
|
|
try:
|
|
yaml_config = yaml.safe_load(config)
|
|
except yaml.scanner.ScannerError as e:
|
|
# Here we know that YAML parser could not parse the template we've given it.
|
|
# YAML raises a ScannerError that specifies which file had the problem, as
|
|
# well as line and column, but since we're giving it the template from
|
|
# string, error message contains <string>, which is not very helpful on the
|
|
# user end, so replace it with word "template" and make it obvious that YAML
|
|
# contains a syntactic error.
|
|
msg = str(e).replace('"<string>"', 'template')
|
|
raise Exception('Error parsing YAML: %s' % msg)
|
|
|
|
# Handle empty file case
|
|
if not yaml_config:
|
|
return ''
|
|
|
|
# If the configuration does not have ':' in it, the yaml_config will be a
|
|
# string. If this is the case just return the str. The code below it assumes
|
|
# yaml_config is a map for common cases.
|
|
if type(yaml_config) is str:
|
|
return yaml_config
|
|
|
|
if not yaml_config.has_key('resources') or yaml_config['resources'] is None:
|
|
yaml_config['resources'] = []
|
|
|
|
config = {'resources': []}
|
|
layout = {'resources': []}
|
|
|
|
_ValidateUniqueNames(yaml_config['resources'])
|
|
|
|
# Iterate over all the resources to process.
|
|
for resource in yaml_config['resources']:
|
|
processed_resource = _ProcessResource(resource, imports, env,
|
|
validate_schema)
|
|
|
|
config['resources'].extend(processed_resource['config']['resources'])
|
|
layout['resources'].append(processed_resource['layout'])
|
|
|
|
result = {'config': config, 'layout': layout}
|
|
return yaml.safe_dump(result, default_flow_style=False)
|
|
|
|
|
|
def _ProcessResource(resource, imports, env, validate_schema=False):
|
|
"""Processes a resource and expands if template.
|
|
|
|
Args:
|
|
resource: the resource to be processed, as a map.
|
|
imports: map from string to string, the map of imported files names
|
|
and contents
|
|
env: map from string to string, the map of environment variable names
|
|
to their values
|
|
validate_schema: True to run schema validation; False otherwise
|
|
Returns:
|
|
A map containing the layout and configuration of the expanded
|
|
resource and any sub-resources, in the format:
|
|
|
|
{'config': ..., 'layout': ...}
|
|
Raises:
|
|
ExpansionError: if there is any error occurred during expansion
|
|
"""
|
|
# A resource has to have to a name.
|
|
if not resource.has_key('name'):
|
|
raise ExpansionError(resource, 'Resource does not have a name.')
|
|
|
|
# A resource has to have a type.
|
|
if not resource.has_key('type'):
|
|
raise ExpansionError(resource, 'Resource does not have type defined.')
|
|
|
|
config = {'resources': []}
|
|
# Initialize layout with basic resource information.
|
|
layout = {'name': resource['name'],
|
|
'type': resource['type']}
|
|
|
|
if resource['type'] in imports:
|
|
# A template resource, which contains sub-resources.
|
|
expanded_template = ExpandTemplate(resource, imports, env, validate_schema)
|
|
|
|
if expanded_template['resources']:
|
|
_ValidateUniqueNames(expanded_template['resources'], resource['type'])
|
|
|
|
# Process all sub-resources of this template.
|
|
for resource_to_process in expanded_template['resources']:
|
|
processed_resource = _ProcessResource(resource_to_process, imports, env,
|
|
validate_schema)
|
|
|
|
# Append all sub-resources to the config resources, and the resulting
|
|
# layout of sub-resources.
|
|
config['resources'].extend(processed_resource['config']['resources'])
|
|
|
|
# Lazy-initialize resources key here because it is not set for
|
|
# non-template layouts.
|
|
if 'resources' not in layout:
|
|
layout['resources'] = []
|
|
layout['resources'].append(processed_resource['layout'])
|
|
|
|
if 'properties' in resource:
|
|
layout['properties'] = resource['properties']
|
|
else:
|
|
# A normal resource has only itself for config.
|
|
config['resources'] = [resource]
|
|
|
|
return {'config': config,
|
|
'layout': layout}
|
|
|
|
|
|
def _ValidateUniqueNames(template_resources, template_name='config'):
|
|
"""Make sure that every resource name in the given template is unique."""
|
|
names = set()
|
|
# Validate that every resource name is unique
|
|
for resource in template_resources:
|
|
if 'name' in resource:
|
|
if resource['name'] in names:
|
|
raise ExpansionError(
|
|
resource,
|
|
'Resource name \'%s\' is not unique in %s.' % (resource['name'],
|
|
template_name))
|
|
names.add(resource['name'])
|
|
# If this resource doesn't have a name, we will report that error later
|
|
|
|
|
|
def ExpandTemplate(resource, imports, env, validate_schema=False):
|
|
"""Expands a template, calling expansion mechanism based on type.
|
|
|
|
Args:
|
|
resource: resource object, the resource that contains parameters to the
|
|
jinja file
|
|
imports: map from string to string, the map of imported files names
|
|
and contents
|
|
env: map from string to string, the map of environment variable names
|
|
to their values
|
|
validate_schema: True to run schema validation; False otherwise
|
|
Returns:
|
|
The final expanded template
|
|
|
|
Raises:
|
|
ExpansionError: if there is any error occurred during expansion
|
|
"""
|
|
source_file = resource['type']
|
|
path = resource['type']
|
|
|
|
# Look for Template in imports.
|
|
if source_file not in imports:
|
|
raise ExpansionError(
|
|
source_file,
|
|
'Unable to find source file %s in imports.' % (source_file))
|
|
|
|
# source_file could be a short version of the template (say github short name)
|
|
# so we need to potentially map this into the fully resolvable name.
|
|
if 'path' in imports[source_file] and imports[source_file]['path']:
|
|
path = imports[source_file]['path']
|
|
|
|
resource['imports'] = imports
|
|
|
|
# Populate the additional environment variables.
|
|
if env is None:
|
|
env = {}
|
|
env['name'] = resource['name']
|
|
env['type'] = resource['type']
|
|
resource['env'] = env
|
|
|
|
schema = source_file + '.schema'
|
|
if validate_schema and schema in imports:
|
|
properties = resource['properties'] if 'properties' in resource else {}
|
|
try:
|
|
resource['properties'] = schema_validation.Validate(
|
|
properties, schema, source_file, imports)
|
|
except schema_validation.ValidationErrors as e:
|
|
raise ExpansionError(resource['name'], e.message)
|
|
|
|
if path.endswith('jinja') or path.endswith('yaml'):
|
|
expanded_template = ExpandJinja(
|
|
source_file, imports[source_file]['content'], resource, imports)
|
|
elif path.endswith('py'):
|
|
# This is a Python template.
|
|
expanded_template = ExpandPython(
|
|
imports[source_file]['content'], source_file, resource)
|
|
else:
|
|
# The source file is not a jinja file or a python file.
|
|
# This in fact should never happen due to the IsTemplate check above.
|
|
raise ExpansionError(
|
|
resource['source'],
|
|
'Unsupported source file: %s.' % (source_file))
|
|
|
|
parsed_template = yaml.safe_load(expanded_template)
|
|
|
|
if parsed_template is None or 'resources' not in parsed_template:
|
|
raise ExpansionError(resource['type'],
|
|
'Template did not return a \'resources:\' field.')
|
|
|
|
return parsed_template
|
|
|
|
|
|
def ExpandJinja(file_name, source_template, resource, imports):
|
|
"""Render the jinja template using jinja libraries.
|
|
|
|
Args:
|
|
file_name: string, the file name.
|
|
source_template: string, the content of jinja file to be render
|
|
resource: resource object, the resource that contains parameters to the
|
|
jinja file
|
|
imports: map from string to map {name, path}, the map of imported files names
|
|
fully resolved path and contents
|
|
Returns:
|
|
The final expanded template
|
|
Raises:
|
|
ExpansionError in case we fail to expand the Jinja2 template.
|
|
"""
|
|
|
|
try:
|
|
env = jinja2.Environment(loader=jinja2.DictLoader(imports))
|
|
|
|
template = env.from_string(source_template)
|
|
|
|
if (resource.has_key('properties') or resource.has_key('env') or
|
|
resource.has_key('imports')):
|
|
return template.render(resource)
|
|
else:
|
|
return template.render()
|
|
except Exception:
|
|
st = 'Exception in %s\n%s'%(file_name, traceback.format_exc())
|
|
raise ExpansionError(file_name, st)
|
|
|
|
|
|
def ExpandPython(python_source, file_name, params):
|
|
"""Run python script to get the expanded template.
|
|
|
|
Args:
|
|
python_source: string, the python source file to run
|
|
file_name: string, the name of the python source file
|
|
params: object that contains 'imports' and 'params', the parameters to
|
|
the python script
|
|
Returns:
|
|
The final expanded template.
|
|
"""
|
|
|
|
try:
|
|
# Compile the python code to be run.
|
|
constructor = {}
|
|
compiled_code = compile(python_source, '<string>', 'exec')
|
|
exec compiled_code in constructor # pylint: disable=exec-used
|
|
|
|
# Construct the parameters to the python script.
|
|
evaluation_context = PythonEvaluationContext(params)
|
|
|
|
return constructor['GenerateConfig'](evaluation_context)
|
|
except Exception:
|
|
st = 'Exception in %s\n%s' % (file_name, traceback.format_exc())
|
|
raise ExpansionError(file_name, st)
|
|
|
|
|
|
class PythonEvaluationContext(object):
|
|
"""The python evaluation context.
|
|
|
|
Attributes:
|
|
params -- the parameters to be used in the expansion
|
|
"""
|
|
|
|
def __init__(self, params):
|
|
if params.has_key('properties'):
|
|
self.properties = params['properties']
|
|
else:
|
|
self.properties = None
|
|
|
|
if params.has_key('imports'):
|
|
self.imports = params['imports']
|
|
else:
|
|
self.imports = None
|
|
|
|
if params.has_key('env'):
|
|
self.env = params['env']
|
|
else:
|
|
self.env = None
|
|
|
|
|
|
class ExpansionError(Exception):
|
|
"""Exception raised for errors during expansion process.
|
|
|
|
Attributes:
|
|
resource: the resource processed that results in the error
|
|
message: the detailed message of the error
|
|
"""
|
|
|
|
def __init__(self, resource, message):
|
|
self.resource = resource
|
|
self.message = message + ' Resource: ' + str(resource)
|
|
super(ExpansionError, self).__init__(self.message)
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print >> sys.stderr, 'No input specified.'
|
|
sys.exit(1)
|
|
template = sys.argv[1]
|
|
idx = 2
|
|
imports = {}
|
|
while idx < len(sys.argv):
|
|
if idx + 1 == len(sys.argv):
|
|
print >>sys.stderr, 'Invalid import definition at argv pos %d' % idx
|
|
sys.exit(1)
|
|
name = sys.argv[idx]
|
|
path = sys.argv[idx + 1]
|
|
value = sys.argv[idx + 2]
|
|
imports[name] = {'content': value, 'path': path}
|
|
idx += 3
|
|
|
|
env = {}
|
|
env['deployment'] = os.environ['DEPLOYMENT_NAME']
|
|
env['project'] = os.environ['PROJECT']
|
|
|
|
validate_schema = 'VALIDATE_SCHEMA' in os.environ
|
|
|
|
# Call the expansion logic to actually expand the template.
|
|
print Expand(template, imports, env=env, validate_schema=validate_schema)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|