Merge pull request #135 from grahamawelch/master

Schema updates for Expansion.
pull/139/head
Brendan Melville 10 years ago
commit d4bf89c52d

@ -140,12 +140,13 @@ def _ProcessResource(resource, imports, env, validate_schema=False):
# A template resource, which contains sub-resources.
expanded_template = ExpandTemplate(resource, imports, env, validate_schema)
if expanded_template['resources'] is not None:
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)
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.
@ -228,7 +229,7 @@ def ExpandTemplate(resource, imports, env, validate_schema=False):
resource['properties'] = schema_validation.Validate(
properties, schema, source_file, imports)
except schema_validation.ValidationErrors as e:
raise ExpansionError(resource, e.message)
raise ExpansionError(resource['name'], e.message)
if source_file.endswith('jinja'):
expanded_template = ExpandJinja(

@ -0,0 +1,504 @@
######################################################################
# 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.
######################################################################
"""Basic unit tests for template expansion library."""
import expansion
import os
import unittest
import yaml
def GetFilePath():
"""Find our source and data files."""
return os.path.dirname(os.path.abspath(__file__))
def ReadTestFile(filename):
"""Returns contents of a file from the test/ directory."""
full_path = GetFilePath() + '/../test/templates/' + filename
test_file = open(full_path, 'r')
return test_file.read()
def GetTestBasePath(filename):
"""Returns the base path of a file from the testdata/ directory."""
full_path = GetFilePath() + '/../test/templates/' + filename
return os.path.dirname(full_path)
class ExpansionTest(unittest.TestCase):
"""Tests basic functionality of the template expansion library."""
EMPTY_RESPONSE = 'config:\n resources: []\nlayout:\n resources: []\n'
def testEmptyExpansion(self):
template = ''
expanded_template = expansion.Expand(
template)
self.assertEqual('', expanded_template)
def testNoResourcesList(self):
template = 'imports: [ test.import ]'
expanded_template = expansion.Expand(
template)
self.assertEqual(self.EMPTY_RESPONSE, expanded_template)
def testResourcesListEmpty(self):
template = 'resources:'
expanded_template = expansion.Expand(
template)
self.assertEqual(self.EMPTY_RESPONSE, expanded_template)
def testSimpleNoExpansionTemplate(self):
template = ReadTestFile('simple.yaml')
expanded_template = expansion.Expand(
template)
result_file = ReadTestFile('simple_result.yaml')
self.assertEquals(result_file, expanded_template)
def testJinjaExpansion(self):
template = ReadTestFile('jinja_template.yaml')
imports = {}
imports['jinja_template.jinja'] = ReadTestFile('jinja_template.jinja')
expanded_template = expansion.Expand(
template, imports)
result_file = ReadTestFile('jinja_template_result.yaml')
self.assertEquals(result_file, expanded_template)
def testJinjaWithNoParamsExpansion(self):
template = ReadTestFile('jinja_noparams.yaml')
imports = {}
imports['jinja_noparams.jinja'] = ReadTestFile('jinja_noparams.jinja')
expanded_template = expansion.Expand(
template, imports)
result_file = ReadTestFile('jinja_noparams_result.yaml')
self.assertEquals(result_file, expanded_template)
def testPythonWithNoParamsExpansion(self):
template = ReadTestFile('python_noparams.yaml')
imports = {}
imports['python_noparams.py'] = ReadTestFile('python_noparams.py')
expanded_template = expansion.Expand(
template, imports)
result_file = ReadTestFile('python_noparams_result.yaml')
self.assertEquals(result_file, expanded_template)
def testPythonExpansion(self):
template = ReadTestFile('python_template.yaml')
imports = {}
imports['python_template.py'] = ReadTestFile('python_template.py')
expanded_template = expansion.Expand(
template, imports)
result_file = ReadTestFile('python_template_result.yaml')
self.assertEquals(result_file, expanded_template)
def testPythonAndJinjaExpansion(self):
template = ReadTestFile('python_and_jinja_template.yaml')
imports = {}
imports['python_and_jinja_template.py'] = ReadTestFile(
'python_and_jinja_template.py')
imports['python_and_jinja_template.jinja'] = ReadTestFile(
'python_and_jinja_template.jinja')
expanded_template = expansion.Expand(
template, imports)
result_file = ReadTestFile('python_and_jinja_template_result.yaml')
self.assertEquals(result_file, expanded_template)
def testNoImportErrors(self):
template = 'resources: \n- type: something.jinja\n name: something'
expansion.Expand(template, {})
def testInvalidConfig(self):
template = ReadTestFile('invalid_config.yaml')
try:
expansion.Expand(
template)
self.fail('Expansion should fail')
except expansion.ExpansionError as e:
self.assertNotIn(os.path.basename(expansion.__name__), e.message,
'Do not leak internals')
def testJinjaWithImport(self):
template = ReadTestFile('jinja_template_with_import.yaml')
imports = {}
imports['jinja_template_with_import.jinja'] = ReadTestFile(
'jinja_template_with_import.jinja')
imports['helpers/common.jinja'] = ReadTestFile(
'helpers/common.jinja')
yaml_template = yaml.safe_load(template)
expanded_template = expansion.Expand(
str(yaml_template), imports)
result_file = ReadTestFile('jinja_template_with_import_result.yaml')
self.assertEquals(result_file, expanded_template)
def testJinjaWithInlinedFile(self):
template = ReadTestFile('jinja_template_with_inlinedfile.yaml')
imports = {}
imports['jinja_template_with_inlinedfile.jinja'] = ReadTestFile(
'jinja_template_with_inlinedfile.jinja')
imports['helpers/common.jinja'] = ReadTestFile(
'helpers/common.jinja')
imports['description_text.txt'] = ReadTestFile('description_text.txt')
yaml_template = yaml.safe_load(template)
expanded_template = expansion.Expand(
str(yaml_template), imports)
result_file = ReadTestFile('jinja_template_with_inlinedfile_result.yaml')
self.assertEquals(result_file, expanded_template)
def testPythonWithImport(self):
template = ReadTestFile('python_template_with_import.yaml')
imports = {}
imports['python_template_with_import.py'] = ReadTestFile(
'python_template_with_import.py')
imports['helpers/common.py'] = ReadTestFile('helpers/common.py')
imports['helpers/extra/common2.py'] = ReadTestFile(
'helpers/extra/common2.py')
imports['helpers/extra'] = ReadTestFile('helpers/extra/__init__.py')
yaml_template = yaml.safe_load(template)
expanded_template = expansion.Expand(
str(yaml_template), imports)
result_file = ReadTestFile('python_template_with_import_result.yaml')
self.assertEquals(result_file, expanded_template)
def testPythonWithInlinedFile(self):
template = ReadTestFile('python_template_with_inlinedfile.yaml')
imports = {}
imports['python_template_with_inlinedfile.py'] = ReadTestFile(
'python_template_with_inlinedfile.py')
imports['helpers/common.py'] = ReadTestFile('helpers/common.py')
imports['helpers/extra/common2.py'] = ReadTestFile(
'helpers/extra/common2.py')
imports['description_text.txt'] = ReadTestFile('description_text.txt')
yaml_template = yaml.safe_load(template)
expanded_template = expansion.Expand(
str(yaml_template), imports)
result_file = ReadTestFile(
'python_template_with_inlinedfile_result.yaml')
self.assertEquals(result_file, expanded_template)
def testPythonWithEnvironment(self):
template = ReadTestFile('python_template_with_env.yaml')
imports = {}
imports['python_template_with_env.py'] = ReadTestFile(
'python_template_with_env.py')
env = {'project': 'my-project'}
expanded_template = expansion.Expand(
template, imports, env)
result_file = ReadTestFile('python_template_with_env_result.yaml')
self.assertEquals(result_file, expanded_template)
def testJinjaWithEnvironment(self):
template = ReadTestFile('jinja_template_with_env.yaml')
imports = {}
imports['jinja_template_with_env.jinja'] = ReadTestFile(
'jinja_template_with_env.jinja')
env = {'project': 'test-project', 'deployment': 'test-deployment'}
expanded_template = expansion.Expand(
template, imports, env)
result_file = ReadTestFile('jinja_template_with_env_result.yaml')
self.assertEquals(result_file, expanded_template)
def testMissingNameErrors(self):
template = 'resources: \n- type: something.jinja\n'
try:
expansion.Expand(template, {})
self.fail('Expansion should fail')
except expansion.ExpansionError as e:
self.assertTrue('not have a name' in e.message)
def testDuplicateNamesErrors(self):
template = ReadTestFile('duplicate_names.yaml')
try:
expansion.Expand(template, {})
self.fail('Expansion should fail')
except expansion.ExpansionError as e:
self.assertTrue(("Resource name 'my_instance' is not unique"
" in config.") in e.message)
def testDuplicateNamesInSubtemplates(self):
template = ReadTestFile('duplicate_names_in_subtemplates.yaml')
imports = {}
imports['duplicate_names_in_subtemplates.jinja'] = ReadTestFile(
'duplicate_names_in_subtemplates.jinja')
try:
expansion.Expand(
template, imports)
self.fail('Expansion should fail')
except expansion.ExpansionError as e:
self.assertTrue('not unique in duplicate_names_in_subtemplates.jinja'
in e.message)
def testDuplicateNamesMixedLevel(self):
template = ReadTestFile('duplicate_names_mixed_level.yaml')
imports = {}
imports['duplicate_names_B.jinja'] = ReadTestFile(
'duplicate_names_B.jinja')
imports['duplicate_names_C.jinja'] = ReadTestFile(
'duplicate_names_C.jinja')
expanded_template = expansion.Expand(
template, imports)
result_file = ReadTestFile('duplicate_names_mixed_level_result.yaml')
self.assertEquals(result_file, expanded_template)
def testDuplicateNamesParentChild(self):
template = ReadTestFile('duplicate_names_parent_child.yaml')
imports = {}
imports['duplicate_names_B.jinja'] = ReadTestFile(
'duplicate_names_B.jinja')
expanded_template = expansion.Expand(
template, imports)
result_file = ReadTestFile('duplicate_names_parent_child_result.yaml')
self.assertEquals(result_file, expanded_template)
# Note, this template will fail in the frontend for duplicate resource names
def testTemplateReturnsEmpty(self):
template = ReadTestFile('no_resources.yaml')
imports = {}
imports['no_resources.py'] = ReadTestFile(
'no_resources.py')
try:
expansion.Expand(
template, imports)
self.fail('Expansion should fail')
except expansion.ExpansionError as e:
self.assertIn('Template did not return a \'resources:\' field.',
e.message)
self.assertIn('no_resources.py', e.message)
def testJinjaDefaultsSchema(self):
# Loop 1000 times to make sure we don't rely on dictionary ordering.
for unused_x in range(0, 1000):
template = ReadTestFile('jinja_defaults.yaml')
imports = {}
imports['jinja_defaults.jinja'] = ReadTestFile(
'jinja_defaults.jinja')
imports['jinja_defaults.jinja.schema'] = ReadTestFile(
'jinja_defaults.jinja.schema')
expanded_template = expansion.Expand(
template, imports,
validate_schema=True)
result_file = ReadTestFile('jinja_defaults_result.yaml')
self.assertEquals(result_file, expanded_template)
def testPythonDefaultsOverrideSchema(self):
template = ReadTestFile('python_schema.yaml')
imports = {}
imports['python_schema.py'] = ReadTestFile('python_schema.py')
imports['python_schema.py.schema'] = ReadTestFile('python_schema.py.schema')
env = {'project': 'my-project'}
expanded_template = expansion.Expand(
template, imports, env=env,
validate_schema=True)
result_file = ReadTestFile('python_schema_result.yaml')
self.assertEquals(result_file, expanded_template)
def testJinjaMissingRequiredPropertySchema(self):
template = ReadTestFile('jinja_missing_required.yaml')
imports = {}
imports['jinja_missing_required.jinja'] = ReadTestFile(
'jinja_missing_required.jinja')
imports['jinja_missing_required.jinja.schema'] = ReadTestFile(
'jinja_missing_required.jinja.schema')
try:
expansion.Expand(
template, imports,
validate_schema=True)
self.fail('Expansion error expected')
except expansion.ExpansionError as e:
self.assertIn('Invalid properties', e.message)
self.assertIn("'important' is a required property", e.message)
self.assertIn('jinja_missing_required_resource_name', e.message)
def testJinjaErrorFileMessage(self):
template = ReadTestFile('jinja_unresolved.yaml')
imports = {}
imports['jinja_unresolved.jinja'] = ReadTestFile('jinja_unresolved.jinja')
try:
expansion.Expand(
template, imports,
validate_schema=False)
self.fail('Expansion error expected')
except expansion.ExpansionError as e:
self.assertIn('jinja_unresolved.jinja', e.message)
def testJinjaMultipleErrorsSchema(self):
template = ReadTestFile('jinja_multiple_errors.yaml')
imports = {}
imports['jinja_multiple_errors.jinja'] = ReadTestFile(
'jinja_multiple_errors.jinja')
imports['jinja_multiple_errors.jinja.schema'] = ReadTestFile(
'jinja_multiple_errors.jinja.schema')
try:
expansion.Expand(
template, imports,
validate_schema=True)
self.fail('Expansion error expected')
except expansion.ExpansionError as e:
self.assertIn('Invalid properties', e.message)
self.assertIn("'a string' is not of type 'integer'", e.message)
self.assertIn("'d' is not one of ['a', 'b', 'c']", e.message)
self.assertIn("'longer than 10 chars' is too long", e.message)
self.assertIn("{'multipleOf': 2} is not allowed for 6", e.message)
def testPythonBadSchema(self):
template = ReadTestFile('python_bad_schema.yaml')
imports = {}
imports['python_bad_schema.py'] = ReadTestFile(
'python_bad_schema.py')
imports['python_bad_schema.py.schema'] = ReadTestFile(
'python_bad_schema.py.schema')
try:
expansion.Expand(
template, imports,
validate_schema=True)
self.fail('Expansion error expected')
except expansion.ExpansionError as e:
self.assertIn('Invalid schema', e.message)
self.assertIn("'int' is not valid under any of the given schemas",
e.message)
self.assertIn("'maximum' is a dependency of u'exclusiveMaximum'",
e.message)
self.assertIn("10 is not of type u'boolean'", e.message)
self.assertIn("'not a list' is not of type u'array'", e.message)
def testNoProperties(self):
template = ReadTestFile('no_properties.yaml')
imports = {}
imports['no_properties.py'] = ReadTestFile(
'no_properties.py')
expanded_template = expansion.Expand(
template, imports,
validate_schema=True)
result_file = ReadTestFile('no_properties_result.yaml')
self.assertEquals(result_file, expanded_template)
def testNestedTemplateSchema(self):
template = ReadTestFile('use_helper.yaml')
imports = {}
imports['use_helper.jinja'] = ReadTestFile(
'use_helper.jinja')
imports['use_helper.jinja.schema'] = ReadTestFile(
'use_helper.jinja.schema')
imports['helper.jinja'] = ReadTestFile(
'helper.jinja')
imports['helper.jinja.schema'] = ReadTestFile(
'helper.jinja.schema')
expanded_template = expansion.Expand(
template, imports,
validate_schema=True)
result_file = ReadTestFile('use_helper_result.yaml')
self.assertEquals(result_file, expanded_template)
if __name__ == '__main__':
unittest.main()

@ -26,13 +26,12 @@ PROPERTIES = "properties"
# This validator will set default values in properties.
# This does not return a complete set of errors; use only for setting defaults.
# Pass this object a schema to get a validator for that schema.
DEFAULT_VALIDATOR = schema_validation_utils.OnlyValidateProperties(
schema_validation_utils.ExtendWithDefault(jsonschema.Draft4Validator))
DEFAULT_SETTER = schema_validation_utils.ExtendWithDefault(
jsonschema.Draft4Validator)
# This is a regular validator, use after using the DEFAULT_VALIDATOR
# This is a regular validator, use after using the DEFAULT_SETTER
# Pass this object a schema to get a validator for that schema.
VALIDATOR = schema_validation_utils.OnlyValidateProperties(
jsonschema.Draft4Validator)
VALIDATOR = jsonschema.Draft4Validator
# This is a validator using the default Draft4 metaschema,
# use it to validate user schemas.
@ -61,29 +60,61 @@ IMPORT_SCHEMA_VALIDATOR = jsonschema.Draft4Validator(
yaml.safe_load(IMPORT_SCHEMA))
def _ValidateSchema(schema, validating_imports, schema_name, template_name):
"""Validate that the passed in schema file is correctly formatted.
Args:
schema: contents of the schema file
validating_imports: boolean, if we should validate the 'imports'
section of the schema
schema_name: name of the schema file to validate
template_name: name of the template whose properties are being validated
Raises:
ValidationErrors: A list of ValidationError errors that occured when
validating the schema file
"""
schema_errors = []
# Validate the syntax of the optional "imports:" section of the schema
if validating_imports:
schema_errors.extend(IMPORT_SCHEMA_VALIDATOR.iter_errors(schema))
# Validate the syntax of the jsonSchema section of the schema
try:
schema_errors.extend(SCHEMA_VALIDATOR.iter_errors(schema))
except jsonschema.RefResolutionError as e:
# Calls to iter_errors could throw a RefResolution exception
raise ValidationErrors(schema_name, template_name,
[e], is_schema_error=True)
if schema_errors:
raise ValidationErrors(schema_name, template_name,
schema_errors, is_schema_error=True)
def Validate(properties, schema_name, template_name, imports):
"""Given a set of properties, validates it against the given schema.
Args:
properties: dict, the properties to be validated
schema_name: name of the schema file to validate
template_name: name of the template whose's properties are being validated
imports: map from string to string, the map of imported files names
and contents
template_name: name of the template whose properties are being validated
imports: the map of imported files names to file contents
Returns:
Dict containing the validated properties, with defaults filled in
Raises:
ValidationErrors: A list of ValidationError errors that occurred when
validating the properties and schema
validating the properties and schema,
or if the schema file was not found
"""
if schema_name not in imports:
raise ValidationErrors(schema_name, template_name,
["Could not find schema file '"
+ schema_name + "'."])
else:
raw_schema = imports[schema_name]
["Could not find schema file '%s'." % schema_name])
raw_schema = imports[schema_name]
if properties is None:
properties = {}
@ -91,33 +122,14 @@ def Validate(properties, schema_name, template_name, imports):
schema = yaml.safe_load(raw_schema)
# If the schema is empty, do nothing.
if schema is None:
if not schema:
return properties
schema_errors = []
validating_imports = IMPORTS in schema and schema[IMPORTS]
# Validate the syntax of the optional "imports:" section of the schema
if validating_imports:
schema_errors.extend(list(IMPORT_SCHEMA_VALIDATOR.iter_errors(schema)))
# Validate the syntax of the optional "properties:" section of the schema
if PROPERTIES in schema and schema[PROPERTIES]:
try:
schema_errors.extend(
list(SCHEMA_VALIDATOR.iter_errors(schema[PROPERTIES])))
except jsonschema.RefResolutionError as e:
# Calls to iter_errors could throw a RefResolution exception
raise ValidationErrors(schema_name, template_name,
list(e), is_schema_error=True)
if schema_errors:
raise ValidationErrors(schema_name, template_name,
schema_errors, is_schema_error=True)
# If this doesn't raise any exceptions, we can assume we have a valid schema
_ValidateSchema(schema, validating_imports, schema_name, template_name)
######
# Assume we have a valid schema
######
errors = []
# Validate that all files specified as "imports:" were included
@ -131,24 +143,25 @@ def Validate(properties, schema_name, template_name, imports):
import_name = import_object["path"]
if import_name not in imports:
errors.append(("File '" + import_name + "' requested in schema '"
+ schema_name + "' but not included with imports."))
errors.append(("File '%s' requested in schema '%s' "
"but not included with imports."
% (import_name, schema_name)))
try:
# This code block uses DEFAULT_VALIDATOR and VALIDATOR for two very
# This code block uses DEFAULT_SETTER and VALIDATOR for two very
# different purposes.
# DEFAULT_VALIDATOR is based on JSONSchema 4, but uses modified validators:
# DEFAULT_SETTER is based on JSONSchema 4, but uses modified validators:
# - The 'required' validator does nothing
# - The 'properties' validator sets default values on user properties
# With these changes, the validator does not report errors correctly.
#
# So, we do error reporting in two steps:
# 1) Use DEFAULT_VALIDATOR to set default values in the user's properties
# 1) Use DEFAULT_SETTER to set default values in the user's properties
# 2) Use the unmodified VALIDATOR to report all of the errors
# Calling iter_errors mutates properties in place, adding default values.
# You must call list()! This is a generator, not a function!
list(DEFAULT_VALIDATOR(schema).iter_errors(properties))
list(DEFAULT_SETTER(schema).iter_errors(properties))
# Now that we have default values, validate the properties
errors.extend(list(VALIDATOR(schema).iter_errors(properties)))
@ -158,7 +171,7 @@ def Validate(properties, schema_name, template_name, imports):
except jsonschema.RefResolutionError as e:
# Calls to iter_errors could throw a RefResolution exception
raise ValidationErrors(schema_name, template_name,
list(e), is_schema_error=True)
[e], is_schema_error=True)
except TypeError as e:
raise ValidationErrors(
schema_name, template_name,
@ -191,7 +204,7 @@ class ValidationErrors(Exception):
message = "Invalid properties for '%s':\n" % self.template_name
for error in self.errors:
if type(error) is jsonschema.exceptions.ValidationError:
if isinstance(error, jsonschema.exceptions.ValidationError):
error_message = error.message
location = list(error.path)
if location and len(location):

@ -0,0 +1,616 @@
######################################################################
# 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.
######################################################################
import os
import unittest
import schema_validation
import yaml
INVALID_PROPERTIES = "Invalid properties for 'template.py'"
def GetFilePath():
"""Find our source and data files."""
return os.path.dirname(os.path.abspath(__file__))
def ReadTestFile(filename):
"""Returns contents of a file from the testdata/ directory."""
full_path = os.path.join(GetFilePath(), '..', 'test', 'schemas', filename)
return open(full_path, 'r').read()
def RawValidate(raw_properties, schema_name, raw_schema):
return ImportsRawValidate(raw_properties, schema_name,
{schema_name: raw_schema})
def ImportsRawValidate(raw_properties, schema_name, import_map):
"""Takes raw properties, calls validate and returns yaml properties."""
properties = yaml.safe_load(raw_properties)
return schema_validation.Validate(properties, schema_name, 'template.py',
import_map)
class SchemaValidationTest(unittest.TestCase):
"""Tests of the schema portion of the template expansion library."""
def testDefaults(self):
schema_name = 'defaults.jinja.schema'
schema = ReadTestFile(schema_name)
empty_properties = ''
expected_properties = """
alpha: alpha
one: 1
"""
self.assertEqual(yaml.safe_load(expected_properties),
RawValidate(empty_properties, schema_name, schema))
def testNestedDefaults(self):
schema_name = 'nested_defaults.py.schema'
schema = ReadTestFile(schema_name)
properties = """
zone: us-central1-a
disks:
- name: backup # diskType and sizeGb set by default
- name: cache # sizeGb set by default
diskType: pd-ssd
- name: data # Nothing set by default
diskType: pd-ssd
sizeGb: 150
- name: swap # diskType set by default
sizeGb: 200
"""
expected_properties = """
zone: us-central1-a
disks:
- sizeGb: 100
diskType: pd-standard
name: backup
- sizeGb: 100
diskType: pd-ssd
name: cache
- sizeGb: 150
diskType: pd-ssd
name: data
- sizeGb: 200
diskType: pd-standard
name: swap
"""
self.assertEqual(yaml.safe_load(expected_properties),
RawValidate(properties, schema_name, schema))
def testNestedRefDefaults(self):
schema_name = 'ref_nested_defaults.py.schema'
schema = ReadTestFile(schema_name)
properties = """
zone: us-central1-a
disks:
- name: backup # diskType and sizeGb set by default
- name: cache # sizeGb set by default
diskType: pd-ssd
- name: data # Nothing set by default
diskType: pd-ssd
sizeGb: 150
- name: swap # diskType set by default
sizeGb: 200
"""
expected_properties = """
zone: us-central1-a
disks:
- sizeGb: 100
diskType: pd-standard
name: backup
- sizeGb: 100
diskType: pd-ssd
name: cache
- sizeGb: 150
diskType: pd-ssd
name: data
- sizeGb: 200
diskType: pd-standard
name: swap
"""
self.assertEqual(yaml.safe_load(expected_properties),
RawValidate(properties, schema_name, schema))
def testInvalidDefault(self):
schema_name = 'invalid_default.jinja.schema'
schema = ReadTestFile(schema_name)
empty_properties = ''
try:
RawValidate(empty_properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn(INVALID_PROPERTIES, e.message)
self.assertIn("'string' is not of type 'integer' at ['number']",
e.message)
def testRequiredDefault(self):
schema_name = 'required_default.jinja.schema'
schema = ReadTestFile(schema_name)
empty_properties = ''
expected_properties = """
name: my_name
"""
self.assertEqual(yaml.safe_load(expected_properties),
RawValidate(empty_properties, schema_name, schema))
def testRequiredDefaultReference(self):
schema_name = 'req_default_ref.py.schema'
schema = ReadTestFile(schema_name)
empty_properties = ''
try:
RawValidate(empty_properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn(INVALID_PROPERTIES, e.message)
self.assertIn("'my_name' is not of type 'integer' at ['number']",
e.message)
def testDefaultReference(self):
schema_name = 'default_ref.jinja.schema'
schema = ReadTestFile(schema_name)
empty_properties = ''
expected_properties = 'number: 1'
self.assertEqual(yaml.safe_load(expected_properties),
RawValidate(empty_properties, schema_name, schema))
def testMissingQuoteInReference(self):
schema_name = 'missing_quote.py.schema'
schema = ReadTestFile(schema_name)
properties = 'number: 1'
try:
RawValidate(properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(2, len(e.errors))
self.assertIn("Invalid schema '%s'" % schema_name, e.message)
self.assertIn("type 'NoneType' is not iterable", e.message)
self.assertIn('around your reference', e.message)
def testRequiredPropertyMissing(self):
schema_name = 'required.jinja.schema'
schema = ReadTestFile(schema_name)
empty_properties = ''
try:
RawValidate(empty_properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn(INVALID_PROPERTIES, e.message)
self.assertIn("'name' is a required property", e.errors[0].message)
def testRequiredPropertyValid(self):
schema_name = 'required.jinja.schema'
schema = ReadTestFile(schema_name)
properties = """
name: my-name
"""
self.assertEqual(yaml.safe_load(properties),
RawValidate(properties, schema_name, schema))
def testMultipleErrors(self):
schema_name = 'defaults.py.schema'
schema = ReadTestFile(schema_name)
properties = """
one: not a number
alpha: 12345
"""
try:
RawValidate(properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(2, len(e.errors))
self.assertIn(INVALID_PROPERTIES, e.message)
self.assertIn("'not a number' is not of type 'integer' at ['one']",
e.message)
self.assertIn("12345 is not of type 'string' at ['alpha']", e.message)
def testNumbersValid(self):
schema_name = 'numbers.py.schema'
schema = ReadTestFile(schema_name)
properties = """
minimum0: 0
exclusiveMin0: 1
maximum10: 10
exclusiveMax10: 9
even: 20
odd: 21
"""
self.assertEquals(yaml.safe_load(properties),
RawValidate(properties, schema_name, schema))
def testNumbersInvalid(self):
schema_name = 'numbers.py.schema'
schema = ReadTestFile(schema_name)
properties = """
minimum0: -1
exclusiveMin0: 0
maximum10: 11
exclusiveMax10: 10
even: 21
odd: 20
"""
try:
RawValidate(properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(6, len(e.errors))
self.assertIn(INVALID_PROPERTIES, e.message)
self.assertIn("-1 is less than the minimum of 0 at ['minimum0']",
e.message)
self.assertIn(('0 is less than or equal to the minimum of 0'
" at ['exclusiveMin0']"), e.message)
self.assertIn("11 is greater than the maximum of 10 at ['maximum10']",
e.message)
self.assertIn(('10 is greater than or equal to the maximum of 10'
" at ['exclusiveMax10']"), e.message)
self.assertIn("21 is not a multiple of 2 at ['even']", e.message)
self.assertIn("{'multipleOf': 2} is not allowed for 20 at ['odd']",
e.message)
def testReference(self):
schema_name = 'reference.jinja.schema'
schema = ReadTestFile(schema_name)
properties = """
odd: 6
"""
try:
RawValidate(properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn('even', e.message)
self.assertIn('is not allowed for 6', e.message)
def testBadSchema(self):
schema_name = 'bad.jinja.schema'
schema = ReadTestFile(schema_name)
empty_properties = ''
try:
RawValidate(empty_properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(2, len(e.errors))
self.assertIn("Invalid schema '%s'" % schema_name, e.message)
self.assertIn("u'minimum' is a dependency of u'exclusiveMinimum'",
e.message)
self.assertIn("0 is not of type u'boolean'", e.message)
def testInvalidReference(self):
schema_name = 'invalid_reference.py.schema'
schema = ReadTestFile(schema_name)
properties = 'odd: 1'
try:
RawValidate(properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn("Invalid schema '%s'" % schema_name, e.message)
self.assertIn('Unresolvable JSON pointer', e.message)
def testInvalidReferenceInSchema(self):
schema_name = 'invalid_reference_schema.py.schema'
schema = ReadTestFile(schema_name)
empty_properties = ''
try:
RawValidate(empty_properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn("Invalid schema '%s'" % schema_name, e.message)
self.assertIn('Unresolvable JSON pointer', e.message)
def testMetadata(self):
schema_name = 'metadata.py.schema'
schema = ReadTestFile(schema_name)
properties = """
one: 2
alpha: beta
"""
self.assertEquals(yaml.safe_load(properties),
RawValidate(properties, schema_name, schema))
def testInvalidInput(self):
schema_name = 'schema'
schema = """
info:
title: Invalid Input
properties: invalid
"""
properties = """
one: 2
alpha: beta
"""
try:
RawValidate(properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn("Invalid schema '%s'" % schema_name, e.message)
self.assertIn("'invalid' is not of type u'object'", e.message)
def testPattern(self):
schema_name = 'schema'
schema = r"""
properties:
bad-zone:
pattern: \w+-\w+-\w+
zone:
pattern: \w+-\w+-\w+
"""
properties = """
bad-zone: abc
zone: us-central1-a
"""
try:
RawValidate(properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn('Invalid properties', e.message)
self.assertIn("'abc' does not match", e.message)
self.assertIn('bad-zone', e.message)
def testUniqueItems(self):
schema_name = 'schema'
schema = """
properties:
bad-list:
type: array
uniqueItems: true
list:
type: array
uniqueItems: true
"""
properties = """
bad-list:
- a
- b
- a
list:
- a
- b
- c
"""
try:
RawValidate(properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn('Invalid properties', e.message)
self.assertIn('has non-unique elements', e.message)
self.assertIn('bad-list', e.message)
def testUniqueItemsOnString(self):
schema_name = 'schema'
schema = """
properties:
ok-string:
type: string
uniqueItems: true
string:
type: string
uniqueItems: true
"""
properties = """
ok-string: aaa
string: abc
"""
self.assertEquals(yaml.safe_load(properties),
RawValidate(properties, schema_name, schema))
def testRequiredTopLevel(self):
schema_name = 'schema'
schema = """
info:
title: Invalid Input
required:
- name
"""
properties = """
one: 2
alpha: beta
"""
try:
RawValidate(properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn(INVALID_PROPERTIES, e.message)
self.assertIn("'name' is a required property", e.message)
def testEmptySchemaProperties(self):
schema_name = 'schema'
schema = """
info:
title: Empty Input
properties:
"""
properties = """
one: 2
alpha: beta
"""
try:
RawValidate(properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn("Invalid schema '%s'" % schema_name, e.message)
self.assertIn("None is not of type u'object' at [u'properties']",
e.message)
def testNoInput(self):
schema = """
info:
title: No other sections
"""
properties = """
one: 2
alpha: beta
"""
self.assertEquals(yaml.safe_load(properties),
RawValidate(properties, 'schema', schema))
def testEmptySchema(self):
schema = ''
properties = """
one: 2
alpha: beta
"""
self.assertEquals(yaml.safe_load(properties),
RawValidate(properties, 'schema', schema))
def testImportPathSchema(self):
schema = """
imports:
- path: a
- path: path/to/b
name: b
"""
properties = """
one: 2
alpha: beta
"""
import_map = {'schema': schema,
'a': '',
'b': ''}
self.assertEquals(yaml.safe_load(properties),
ImportsRawValidate(properties, 'schema', import_map))
def testImportSchemaMissing(self):
schema = ''
empty_properties = ''
try:
properties = yaml.safe_load(empty_properties)
schema_validation.Validate(properties, 'schema', 'template',
{'wrong_name': schema})
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn("Could not find schema file 'schema'", e.message)
def testImportsMalformedNotAList(self):
schema_name = 'schema'
schema = """
imports: not-a-list
"""
empty_properties = ''
try:
RawValidate(empty_properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn("Invalid schema '%s'" % schema_name, e.message)
self.assertIn("is not of type 'array' at ['imports']", e.message)
def testImportsMalformedMissingPath(self):
schema_name = 'schema'
schema = """
imports:
- name: no_path.yaml
"""
empty_properties = ''
try:
RawValidate(empty_properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn("Invalid schema '%s'" % schema_name, e.message)
self.assertIn("'path' is a required property", e.message)
def testImportsMalformedNonunique(self):
schema_name = 'schema'
schema = """
imports:
- path: a.yaml
name: a
- path: a.yaml
name: a
"""
empty_properties = ''
try:
RawValidate(empty_properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn("Invalid schema '%s'" % schema_name, e.message)
self.assertIn('non-unique elements', e.message)
def testImportsMalformedAdditionalProperties(self):
schema_name = 'schema'
schema = """
imports:
- path: a.yaml
gnome: a
"""
empty_properties = ''
try:
RawValidate(empty_properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(1, len(e.errors))
self.assertIn("Invalid schema '%s'" % schema_name, e.message)
self.assertIn('Additional properties are not allowed'
" ('gnome' was unexpected)", e.message)
def testImportAndInputErrors(self):
schema = """
imports:
- path: file
required:
- name
"""
empty_properties = ''
try:
RawValidate(empty_properties, 'schema', schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(2, len(e.errors))
self.assertIn("'file' requested in schema 'schema'", e.message)
self.assertIn("'name' is a required property", e.message)
def testImportAndInputSchemaErrors(self):
schema_name = 'schema'
schema = """
imports: not-a-list
required: not-a-list
"""
empty_properties = ''
try:
RawValidate(empty_properties, schema_name, schema)
self.fail('Validation should fail')
except schema_validation.ValidationErrors as e:
self.assertEqual(2, len(e.errors))
self.assertIn("Invalid schema '%s'" % schema_name, e.message)
self.assertIn("is not of type 'array' at ['imports']", e.message)
self.assertIn("is not of type u'array' at [u'required']", e.message)
if __name__ == '__main__':
unittest.main()

@ -15,35 +15,10 @@
import jsonschema
DEFAULT = "default"
PROPERTIES = "properties"
REF = "$ref"
REQUIRED = "required"
def OnlyValidateProperties(validator_class):
"""Takes a validator and makes it process only the 'properties' top level.
Args:
validator_class: A class to add a new validator to
Returns:
A validator_class that will validate properties against things
under the top level "properties" field
"""
def PropertiesValidator(unused_validator, inputs, instance, schema):
if inputs is None:
inputs = {}
for error in validator_class(schema).iter_errors(instance, inputs):
yield error
# This makes sure the only keyword jsonschema will validate is 'properties'
new_validators = ClearValidatorMap(validator_class.VALIDATORS)
new_validators.update({PROPERTIES: PropertiesValidator})
return jsonschema.validators.extend(
validator_class, new_validators)
DEFAULT = 'default'
PROPERTIES = 'properties'
REF = '$ref'
REQUIRED = 'required'
def ExtendWithDefault(validator_class):
@ -55,33 +30,33 @@ def ExtendWithDefault(validator_class):
Returns:
A validator_class that will set default values and ignore required fields
"""
validate_properties = validator_class.VALIDATORS['properties']
def SetDefaultsInProperties(validator, properties, instance, unused_schema):
if properties is None:
properties = {}
SetDefaults(validator, properties, instance)
def SetDefaultsInProperties(validator, user_schema, user_properties,
parent_schema):
SetDefaults(validator, user_schema or {}, user_properties, parent_schema,
validate_properties)
return jsonschema.validators.extend(
validator_class, {PROPERTIES: SetDefaultsInProperties,
REQUIRED: IgnoreKeyword})
def SetDefaults(validator, properties, instance):
def SetDefaults(validator, user_schema, user_properties, parent_schema,
validate_properties):
"""Populate the default values of properties.
Args:
validator: A generator that validates the "properties" keyword
properties: User properties on which to set defaults
instance: Piece of user schema containing "properties"
validator: A generator that validates the "properties" keyword of the schema
user_schema: Schema which might define defaults, might be a nested part of
the entire schema file.
user_properties: User provided values which we are setting defaults on
parent_schema: Schema object that contains the schema being evaluated on
this pass, user_schema.
validate_properties: Validator function, called recursively.
"""
if not properties:
return
for dm_property, subschema in properties.iteritems():
# If the property already has a value, we don't need it's default
if dm_property in instance:
return
for schema_property, subschema in user_schema.iteritems():
# The ordering of these conditions assumes that '$ref' blocks override
# all other schema info, which is what the jsonschema library assumes.
@ -89,17 +64,21 @@ def SetDefaults(validator, properties, instance):
# see if that reference defines a 'default' value
if REF in subschema:
out = ResolveReferencedDefault(validator, subschema[REF])
instance.setdefault(dm_property, out)
user_properties.setdefault(schema_property, out)
# Otherwise, see if the subschema has a 'default' value
elif DEFAULT in subschema:
instance.setdefault(dm_property, subschema[DEFAULT])
user_properties.setdefault(schema_property, subschema[DEFAULT])
# Recursively apply defaults. This is a generator, so we must wrap with list()
list(validate_properties(validator, user_schema,
user_properties, parent_schema))
def ResolveReferencedDefault(validator, ref):
"""Resolves a reference, and returns any default value it defines.
Args:
validator: A generator the validates the "$ref" keyword
validator: A generator that validates the "$ref" keyword
ref: The target of the "$ref" keyword
Returns:
@ -110,14 +89,6 @@ def ResolveReferencedDefault(validator, ref):
return resolved[DEFAULT]
def ClearValidatorMap(validators):
"""Remaps all JsonSchema validators to make them do nothing."""
ignore_validators = {}
for keyword in validators:
ignore_validators.update({keyword: IgnoreKeyword})
return ignore_validators
def IgnoreKeyword(
unused_validator, unused_required, unused_instance, unused_schema):
"""Validator for JsonSchema that does nothing."""

@ -0,0 +1,9 @@
info:
title: Schema with a lots of errors in it
imports:
properties:
exclusiveMin:
type: integer
exclusiveMinimum: 0

@ -0,0 +1,14 @@
info:
title: Schema with a property that has a referenced default value
imports:
properties:
number:
$ref: '#/level/mult'
level:
mult:
type: integer
multipleOf: 1
default: 1

@ -0,0 +1,12 @@
info:
title: Schema with properties that have default values
imports:
properties:
one:
type: integer
default: 1
alpha:
type: string
default: alpha

@ -0,0 +1,12 @@
info:
title: Schema with properties that have default values
imports:
properties:
one:
type: integer
default: 1
alpha:
type: string
default: alpha

@ -0,0 +1,11 @@
info:
title: Schema with a required integer property that has a default string value
imports:
required:
- number
properties:
number:
type: integer
default: string

@ -0,0 +1,10 @@
info:
title: Schema with references to something that doesnt exist
imports:
properties:
odd:
type: integer
not:
$ref: '#/wheeeeeee'

@ -0,0 +1,8 @@
info:
title: Schema with references to something that doesnt exist
imports:
properties:
odd:
$ref: '#/wheeeeeee'

@ -0,0 +1,20 @@
info:
title: Schema with properties that have extra metadata
imports:
properties:
one:
type: integer
default: 1
metadata:
gcloud: is great!
compute: is awesome
alpha:
type: string
default: alpha
metadata:
- you
- can
- do
- anything

@ -0,0 +1,11 @@
info:
title: Schema with references
imports:
properties:
number:
$ref: #/number
number:
type: integer

@ -0,0 +1,33 @@
info:
title: VM with Disks
author: Kubernetes
description: Creates a single vm, then attaches disks to it.
required:
- zone
properties:
zone:
type: string
description: GCP zone
default: us-central1-a
disks:
type: array
items:
type: object
required:
- name
properties:
name:
type: string
description: Suffix for this disk
sizeGb:
type: integer
default: 100
diskType:
type: string
enum:
- pd-standard
- pd-ssd
default: pd-standard
additionalProperties: false

@ -0,0 +1,27 @@
info:
title: Schema with a lots of number properties and restrictions
imports:
properties:
minimum0:
type: integer
minimum: 0
exclusiveMin0:
type: integer
minimum: 0
exclusiveMinimum: true
maximum10:
type: integer
maximum: 10
exclusiveMax10:
type: integer
maximum: 10
exclusiveMaximum: true
even:
type: integer
multipleOf: 2
odd:
type: integer
not:
multipleOf: 2

@ -0,0 +1,36 @@
info:
title: VM with Disks
author: Kubernetes
description: Creates a single vm, then attaches disks to it.
required:
- zone
properties:
zone:
type: string
description: GCP zone
default: us-central1-a
disks:
type: array
items:
$ref: '#/disk'
disk:
type: object
required:
- name
properties:
name:
type: string
description: Suffix for this disk
sizeGb:
type: integer
default: 100
diskType:
type: string
enum:
- pd-standard
- pd-ssd
default: pd-standard
additionalProperties: false

@ -0,0 +1,14 @@
info:
title: Schema with references
imports:
properties:
odd:
type: integer
not:
$ref: '#/even'
even:
multipleOf: 2

@ -0,0 +1,14 @@
info:
title: Schema with a required property that has a referenced default value
imports:
required:
- number
properties:
number:
$ref: '#/default_val'
default_val:
type: integer
default: my_name

@ -0,0 +1,10 @@
info:
title: Schema with a required property
imports:
required:
- name
properties:
name:
type: string

@ -0,0 +1,11 @@
info:
title: Schema with a required property that has a default value
imports:
required:
- name
properties:
name:
type: string
default: my_name

@ -0,0 +1 @@
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

@ -0,0 +1,9 @@
resources:
- type: compute.v1.instance
name: my_instance
properties:
zone: test-zone-a
- type: compute.v1.instance
name: my_instance
properties:
zone: test-zone-b

@ -0,0 +1,5 @@
resources:
- type: compute.v1.instance
name: B
properties:
zone: test-zone-b

@ -0,0 +1,5 @@
resources:
- type: compute.v1.instance
name: C
properties:
zone: test-zone-c

@ -0,0 +1,9 @@
resources:
- type: compute.v1.instance
name: my_instance
properties:
zone: test-zone-a
- type: compute.v1.instance
name: my_instance
properties:
zone: test-zone-b

@ -0,0 +1,5 @@
imports: ["duplicate_names_in_subtemplates.jinja"]
resources:
- name: subtemplate
type: duplicate_names_in_subtemplates.jinja

@ -0,0 +1,7 @@
imports: ["duplicate_names_B.jinja", "duplicate_names_C.jinja"]
resources:
- name: A
type: duplicate_names_B.jinja
- name: B
type: duplicate_names_C.jinja

@ -0,0 +1,22 @@
config:
resources:
- name: B
properties:
zone: test-zone-b
type: compute.v1.instance
- name: C
properties:
zone: test-zone-c
type: compute.v1.instance
layout:
resources:
- name: A
resources:
- name: B
type: compute.v1.instance
type: duplicate_names_B.jinja
- name: B
resources:
- name: C
type: compute.v1.instance
type: duplicate_names_C.jinja

@ -0,0 +1,7 @@
imports: ["duplicate_names_B.jinja"]
resources:
- name: A
type: duplicate_names_B.jinja
- name: B
type: compute.v1.instance

@ -0,0 +1,17 @@
config:
resources:
- name: B
properties:
zone: test-zone-b
type: compute.v1.instance
- name: B
type: compute.v1.instance
layout:
resources:
- name: A
resources:
- name: B
type: compute.v1.instance
type: duplicate_names_B.jinja
- name: B
type: compute.v1.instance

@ -0,0 +1,5 @@
resources:
- name: helper
type: bar
properties:
test: {{ properties["foobar"] }}

@ -0,0 +1,4 @@
properties:
foobar:
type: string
default: Use this schema

@ -0,0 +1,3 @@
{%- macro GenerateMachineName(prefix='', suffix='') -%}
{{ prefix + "-" + suffix }}
{%- endmacro %}

@ -0,0 +1,8 @@
# Copyright 2014 Google Inc. All Rights Reserved.
"""Dummy helper methods invoked in other constructors."""
def GenerateMachineName(prefix, suffix):
"""Generates name of a VM."""
return prefix + "-" + suffix

@ -0,0 +1,8 @@
# Copyright 2014 Google Inc. All Rights Reserved.
"""Dummy helper methods invoked in other constructors."""
def GenerateMachineSize():
"""Generates size of a VM."""
return "big"

@ -0,0 +1,2 @@
resources:
- name: foo properties: bar: baz

@ -0,0 +1,16 @@
resources:
- type: compute.v1.instance
name: vm-created-by-cloud-config-{{ properties["deployment"] }}
properties:
zone: {{ properties["zone"] }}
machineType: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/zones/{{ properties["zone"] }}/machineTypes/f1-micro
disks:
- deviceName: boot
type: PERSISTENT
boot: true
autoDelete: true
initializeParams:
diskName: disk-created-by-cloud-config-{{ properties["deployment"] }}
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/global/networks/default

@ -0,0 +1,18 @@
info:
title: Schema for a basic jinja template that includes default values
imports:
properties:
foo:
description: blah
type: string
zone:
type: string
default: test-zone
project:
type: string
default: test-project
deployment:
type: string
default: test-deployment

@ -0,0 +1,9 @@
imports:
- path: "jinja_defaults.jinja"
- path: "jinja_defaults.jinja.schema"
resources:
- name: jinja_defaults_name
type: jinja_defaults.jinja
properties:
foo: bar

@ -0,0 +1,29 @@
config:
resources:
- name: vm-created-by-cloud-config-test-deployment
properties:
disks:
- autoDelete: true
boot: true
deviceName: boot
initializeParams:
diskName: disk-created-by-cloud-config-test-deployment
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
type: PERSISTENT
machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default
zone: test-zone
type: compute.v1.instance
layout:
resources:
- name: jinja_defaults_name
properties:
deployment: test-deployment
foo: bar
project: test-project
zone: test-zone
resources:
- name: vm-created-by-cloud-config-test-deployment
type: compute.v1.instance
type: jinja_defaults.jinja

@ -0,0 +1,4 @@
Nothing here because this file should never be called.
The validation will fail before this file is used.
{{% Invalid

@ -0,0 +1,11 @@
info:
title: Schema with a required property
imports:
required:
- important
properties:
important:
type: string

@ -0,0 +1,9 @@
imports:
- path: "jinja_missing_required.jinja"
- path: "jinja_missing_required.jinja.schema"
resources:
- name: jinja_missing_required_resource_name
type: jinja_missing_required.jinja
properties:
less-important: an optional property

@ -0,0 +1,4 @@
Nothing here because this file should never be called.
The validation will fail before this file is used.
{{% Invalid

@ -0,0 +1,22 @@
info:
title: Schema with several rules
imports:
properties:
number:
type: integer
short-string:
type: string
maxLength: 10
odd:
type: integer
not:
multipleOf: 2
abc:
type: string
enum:
- a
- b
- c

@ -0,0 +1,12 @@
imports:
- path: "jinja_multiple_errors.jinja"
- path: "jinja_multiple_errors.jinja.schema"
resources:
- name: jinja_multiple_errors
type: jinja_multiple_errors.jinja
properties:
number: a string
short-string: longer than 10 chars
odd: 6
abc: d

@ -0,0 +1,19 @@
resources:
{% for name in ['name1', 'name2'] %}
- type: compute.v1.instance
name: {{ name }}
properties:
zone: test-zone
machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro
disks:
- deviceName: boot
type: PERSISTENT
boot: true
autoDelete: true
initializeParams:
diskName: disk-created-by-cloud-config-test-deployment
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default
{% endfor %}

@ -0,0 +1,6 @@
imports: ["jinja_noparams.jinja"]
resources:
- name: jinja_noparams_name
type: jinja_noparams.jinja

@ -0,0 +1,41 @@
config:
resources:
- name: name1
properties:
disks:
- autoDelete: true
boot: true
deviceName: boot
initializeParams:
diskName: disk-created-by-cloud-config-test-deployment
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
type: PERSISTENT
machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default
zone: test-zone
type: compute.v1.instance
- name: name2
properties:
disks:
- autoDelete: true
boot: true
deviceName: boot
initializeParams:
diskName: disk-created-by-cloud-config-test-deployment
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
type: PERSISTENT
machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default
zone: test-zone
type: compute.v1.instance
layout:
resources:
- name: jinja_noparams_name
resources:
- name: name1
type: compute.v1.instance
- name: name2
type: compute.v1.instance
type: jinja_noparams.jinja

@ -0,0 +1,18 @@
resources:
- type: compute.v1.instance
name: vm-created-by-cloud-config-{{ properties["deployment"] }}
properties:
zone: {{ properties["zone"] }}
machineType: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/zones/{{ properties["zone"] }}/machineTypes/f1-micro
disks:
- deviceName: boot
type: PERSISTENT
boot: true
autoDelete: true
initializeParams:
diskName: disk-created-by-cloud-config-{{ properties["deployment"] }}
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/global/networks/default

@ -0,0 +1,10 @@
imports: ["jinja_template.jinja"]
resources:
- name: jinja_template_name
type: jinja_template.jinja
properties:
zone: test-zone
project: test-project
deployment: test-deployment

@ -0,0 +1,28 @@
config:
resources:
- name: vm-created-by-cloud-config-test-deployment
properties:
disks:
- autoDelete: true
boot: true
deviceName: boot
initializeParams:
diskName: disk-created-by-cloud-config-test-deployment
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
type: PERSISTENT
machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default
zone: test-zone
type: compute.v1.instance
layout:
resources:
- name: jinja_template_name
properties:
deployment: test-deployment
project: test-project
zone: test-zone
resources:
- name: vm-created-by-cloud-config-test-deployment
type: compute.v1.instance
type: jinja_template.jinja

@ -0,0 +1,18 @@
resources:
- type: compute.v1.instance
name: vm-created-by-cloud-config-{{ env["deployment"] }}
properties:
zone: {{ properties["zone"] }}
machineType: https://www.googleapis.com/compute/v1/projects/{{ env["project"] }}/zones/{{ properties["zone"] }}/machineTypes/f1-micro
disks:
- deviceName: boot
type: PERSISTENT
boot: true
autoDelete: true
initializeParams:
diskName: disk-created-by-cloud-config-{{ env["deployment"] }}-{{ env["name"] }}-{{ env["type"] }}
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/{{ env["project"] }}/global/networks/default

@ -0,0 +1,8 @@
imports: ["jinja_template_with_env.jinja"]
resources:
- name: jinja_template_with_env_name
type: jinja_template_with_env.jinja
properties:
zone: test-zone

@ -0,0 +1,26 @@
config:
resources:
- name: vm-created-by-cloud-config-test-deployment
properties:
disks:
- autoDelete: true
boot: true
deviceName: boot
initializeParams:
diskName: disk-created-by-cloud-config-test-deployment-jinja_template_with_env_name-jinja_template_with_env.jinja
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
type: PERSISTENT
machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default
zone: test-zone
type: compute.v1.instance
layout:
resources:
- name: jinja_template_with_env_name
properties:
zone: test-zone
resources:
- name: vm-created-by-cloud-config-test-deployment
type: compute.v1.instance
type: jinja_template_with_env.jinja

@ -0,0 +1,6 @@
{% import 'helpers/common.jinja' as common %}
resources:
- name: {{ common.GenerateMachineName("myFrontend", "prod") }}
type: compute.v1.instance
properties:
machineSize: big

@ -0,0 +1,5 @@
imports: ["jinja_template_with_import.jinja", "helpers/common.jinja"]
resources:
- name: jinja_template_with_import_name
type: jinja_template_with_import.jinja

@ -0,0 +1,13 @@
config:
resources:
- name: myFrontend-prod
properties:
machineSize: big
type: compute.v1.instance
layout:
resources:
- name: jinja_template_with_import_name
resources:
- name: myFrontend-prod
type: compute.v1.instance
type: jinja_template_with_import.jinja

@ -0,0 +1,7 @@
{% import 'helpers/common.jinja' as common %}
resources:
- name: {{ common.GenerateMachineName("myFrontend", "prod") }}
type: compute.v1.instance
properties:
description: {{ imports[properties["description-file"]] }}
machineSize: big

@ -0,0 +1,7 @@
imports: ["jinja_template_with_inlinedfile.jinja", "helpers/common.jinja", "description_text.txt"]
resources:
- name: jinja_template_with_inlinedfile_name
type: jinja_template_with_inlinedfile.jinja
properties:
description-file: description_text.txt

@ -0,0 +1,21 @@
config:
resources:
- name: myFrontend-prod
properties:
description: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
machineSize: big
type: compute.v1.instance
layout:
resources:
- name: jinja_template_with_inlinedfile_name
properties:
description-file: description_text.txt
resources:
- name: myFrontend-prod
type: compute.v1.instance
type: jinja_template_with_inlinedfile.jinja

@ -0,0 +1,18 @@
resources:
- type: compute.v1.instance
name: vm-created-by-cloud-config-{{ porcelain["deployment"] }}
properties:
zone: {{ properties["zone"] }}
machineType: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/zones/{{ properties["zone"] }}/machineTypes/f1-micro
disks:
- deviceName: boot
type: PERSISTENT
boot: true
autoDelete: true
initializeParams:
diskName: disk-created-by-cloud-config-{{ properties["deployment"] }}
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/global/networks/default

@ -0,0 +1,10 @@
imports: ["jinja_unresolved.jinja"]
resources:
- name: jinja_template_name
type: jinja_unresolved.jinja
properties:
zone: test-zone
project: test-project
deployment: test-deployment

@ -0,0 +1,5 @@
"""Return empty resources block."""
def GenerateConfig(_):
return """resources:"""

@ -0,0 +1,6 @@
imports:
- path: "no_properties.py"
resources:
- name: test-resource
type: no_properties.py

@ -0,0 +1,6 @@
config:
resources: []
layout:
resources:
- name: test-resource
type: no_properties.py

@ -0,0 +1,6 @@
"""Does nothing."""
def GenerateConfig(_):
"""Returns empty string."""
return ''

@ -0,0 +1,6 @@
imports:
- path: "no_resources.py"
resources:
- name: test-resource
type: no_resources.py

@ -0,0 +1,17 @@
resources:
- type: compute.v1.instance
name: vm-created-by-cloud-config-{{ properties["deployment"] }}
properties:
zone: {{ properties["zone"] }}
machineType: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/zones/{{ properties["zone"] }}/machineTypes/f1-micro
disks:
- deviceName: boot
type: PERSISTENT
boot: true
autoDelete: true
initializeParams:
diskName: disk-created-by-cloud-config-{{ properties["deployment"] }}
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/{{ properties["project"] }}/global/networks/default

@ -0,0 +1,37 @@
# Copyright 2014 Google Inc. All Rights Reserved.
#% description: Creates a VM running a Salt master daemon in a Docker container.
#% parameters:
#% - name: masterAddress
#% type: string
#% description: Name of the Salt master VM.
#% required: true
#% - name: project
#% type: string
#% description: Name of the Cloud project.
#% required: true
#% - name: zone
#% type: string
#% description: Zone to create the resources in.
#% required: true
"""Generates config for a VM running a SaltStack master.
Just for fun this template is in Python, while the others in this
directory are in Jinja2.
"""
def GenerateConfig(evaluation_context):
return """
resources:
- name: python_and_jinja_template_jinja_name
type: python_and_jinja_template.jinja
properties:
zone: %(zone)s
project: %(project)s
deployment: %(master)s
""" % {"master": evaluation_context.properties["masterAddress"],
"project": evaluation_context.properties["project"],
"zone": evaluation_context.properties["zone"]}

@ -0,0 +1,9 @@
imports: ["python_and_jinja_template.jinja", "python_and_jinja_template.py"]
resources:
- name: python_and_jinja_template_name
type: python_and_jinja_template.py
properties:
masterAddress: master-address
project: my-project
zone: my-zone

@ -0,0 +1,35 @@
config:
resources:
- name: vm-created-by-cloud-config-master-address
properties:
disks:
- autoDelete: true
boot: true
deviceName: boot
initializeParams:
diskName: disk-created-by-cloud-config-master-address
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
type: PERSISTENT
machineType: https://www.googleapis.com/compute/v1/projects/my-project/zones/my-zone/machineTypes/f1-micro
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default
zone: my-zone
type: compute.v1.instance
layout:
resources:
- name: python_and_jinja_template_name
properties:
masterAddress: master-address
project: my-project
zone: my-zone
resources:
- name: python_and_jinja_template_jinja_name
properties:
deployment: master-address
project: my-project
zone: my-zone
resources:
- name: vm-created-by-cloud-config-master-address
type: compute.v1.instance
type: python_and_jinja_template.jinja
type: python_and_jinja_template.py

@ -0,0 +1,3 @@
"""Throws an exception."""
raise Exception

@ -0,0 +1,19 @@
info:
title: Schema with several errors
imports:
properties:
bad-type:
type: int
missing-cond:
type: string
exclusiveMaximum: 10
odd-string:
type: string
not:
multipleOf: 2
bad-enum:
type: string
enum: not a list

@ -0,0 +1,9 @@
imports:
- path: "python_bad_schema.py"
- path: "python_bad_schema.py.schema"
resources:
- name: python_bad_schema
type: python_bad_schema.py
properties:
innocent: true

@ -0,0 +1,14 @@
# Copyright 2014 Google Inc. All Rights Reserved.
"""Constructs a VM."""
def GenerateConfig(_):
"""Generates config of a VM."""
return """
resources:
- name: myBackend
type: compute.v1.instance
properties:
machineSize: big
"""

@ -0,0 +1,9 @@
imports: ["python_noparams.py"]
resources:
- name: myFrontend
type: compute.v1.instance
properties:
machineSize: big
- name: python_noparams_name
type: python_noparams.py

@ -0,0 +1,19 @@
config:
resources:
- name: myFrontend
properties:
machineSize: big
type: compute.v1.instance
- name: myBackend
properties:
machineSize: big
type: compute.v1.instance
layout:
resources:
- name: myFrontend
type: compute.v1.instance
- name: python_noparams_name
resources:
- name: myBackend
type: compute.v1.instance
type: python_noparams.py

@ -0,0 +1,59 @@
# Copyright 2014 Google Inc. All Rights Reserved.
#% description: Creates a VM running a Salt master daemon in a Docker container.
#% parameters:
#% - name: masterAddress
#% type: string
#% description: Name of the Salt master VM.
#% required: true
#% - name: project
#% type: string
#% description: Name of the Cloud project.
#% required: true
#% - name: zone
#% type: string
#% description: Zone to create the resources in.
#% required: true
"""Generates config for a VM running a SaltStack master.
Just for fun this template is in Python, while the others in this
directory are in Jinja2.
"""
def GenerateConfig(evaluation_context):
return """
resources:
- type: compute.v1.firewall
name: %(master)s-firewall
properties:
network: https://www.googleapis.com/compute/v1/projects/%(project)s/global/networks/default
sourceRanges: [ "0.0.0.0/0" ]
allowed:
- IPProtocol: tcp
ports: [ "4505", "4506" ]
- type: compute.v1.instance
name: %(master)s
properties:
zone: %(zone)s
machineType: https://www.googleapis.com/compute/v1/projects/%(project)s/zones/%(zone)s/machineTypes/f1-micro
disks:
- deviceName: boot
type: PERSISTENT
boot: true
autoDelete: true
initializeParams:
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/%(project)s/global/networks/default
accessConfigs:
- name: External NAT
type: ONE_TO_ONE_NAT
metadata:
items:
- key: startup-script
value: startup-script-value
""" % {"master": evaluation_context.properties["masterAddress"],
"project": evaluation_context.env["project"],
"zone": evaluation_context.properties["zone"]}

@ -0,0 +1,14 @@
info:
title: A simple python template that has a schema.
imports:
properties:
masterAddress:
type: string
default: slave-address
description: masterAddress
zone:
type: string
default: not-test-zone
description: zone

@ -0,0 +1,10 @@
imports:
- path: "python_schema.py"
- path: "python_schema.py.schema"
resources:
- name: python_schema
type: python_schema.py
properties:
masterAddress: master-address
zone: my-zone

@ -0,0 +1,46 @@
config:
resources:
- name: master-address-firewall
properties:
allowed:
- IPProtocol: tcp
ports:
- '4505'
- '4506'
network: https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default
sourceRanges:
- 0.0.0.0/0
type: compute.v1.firewall
- name: master-address
properties:
disks:
- autoDelete: true
boot: true
deviceName: boot
initializeParams:
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
type: PERSISTENT
machineType: https://www.googleapis.com/compute/v1/projects/my-project/zones/my-zone/machineTypes/f1-micro
metadata:
items:
- key: startup-script
value: startup-script-value
networkInterfaces:
- accessConfigs:
- name: External NAT
type: ONE_TO_ONE_NAT
network: https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default
zone: my-zone
type: compute.v1.instance
layout:
resources:
- name: python_schema
properties:
masterAddress: master-address
zone: my-zone
resources:
- name: master-address-firewall
type: compute.v1.firewall
- name: master-address
type: compute.v1.instance
type: python_schema.py

@ -0,0 +1,59 @@
# Copyright 2014 Google Inc. All Rights Reserved.
#% description: Creates a VM running a Salt master daemon in a Docker container.
#% parameters:
#% - name: masterAddress
#% type: string
#% description: Name of the Salt master VM.
#% required: true
#% - name: project
#% type: string
#% description: Name of the Cloud project.
#% required: true
#% - name: zone
#% type: string
#% description: Zone to create the resources in.
#% required: true
"""Generates config for a VM running a SaltStack master.
Just for fun this template is in Python, while the others in this
directory are in Jinja2.
"""
def GenerateConfig(evaluation_context):
return """
resources:
- type: compute.v1.firewall
name: %(master)s-firewall
properties:
network: https://www.googleapis.com/compute/v1/projects/%(project)s/global/networks/default
sourceRanges: [ "0.0.0.0/0" ]
allowed:
- IPProtocol: tcp
ports: [ "4505", "4506" ]
- type: compute.v1.instance
name: %(master)s
properties:
zone: %(zone)s
machineType: https://www.googleapis.com/compute/v1/projects/%(project)s/zones/%(zone)s/machineTypes/f1-micro
disks:
- deviceName: boot
type: PERSISTENT
boot: true
autoDelete: true
initializeParams:
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/%(project)s/global/networks/default
accessConfigs:
- name: External NAT
type: ONE_TO_ONE_NAT
metadata:
items:
- key: startup-script
value: startup-script-value
""" % {"master": evaluation_context.properties["masterAddress"],
"project": evaluation_context.properties["project"],
"zone": evaluation_context.properties["zone"]}

@ -0,0 +1,9 @@
imports: ["python_template.py"]
resources:
- name: python_template_name
type: python_template.py
properties:
masterAddress: master-address
project: my-project
zone: my-zone

@ -0,0 +1,47 @@
config:
resources:
- name: master-address-firewall
properties:
allowed:
- IPProtocol: tcp
ports:
- '4505'
- '4506'
network: https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default
sourceRanges:
- 0.0.0.0/0
type: compute.v1.firewall
- name: master-address
properties:
disks:
- autoDelete: true
boot: true
deviceName: boot
initializeParams:
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
type: PERSISTENT
machineType: https://www.googleapis.com/compute/v1/projects/my-project/zones/my-zone/machineTypes/f1-micro
metadata:
items:
- key: startup-script
value: startup-script-value
networkInterfaces:
- accessConfigs:
- name: External NAT
type: ONE_TO_ONE_NAT
network: https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default
zone: my-zone
type: compute.v1.instance
layout:
resources:
- name: python_template_name
properties:
masterAddress: master-address
project: my-project
zone: my-zone
resources:
- name: master-address-firewall
type: compute.v1.firewall
- name: master-address
type: compute.v1.instance
type: python_template.py

@ -0,0 +1,59 @@
# Copyright 2014 Google Inc. All Rights Reserved.
#% description: Creates a VM running a Salt master daemon in a Docker container.
#% parameters:
#% - name: masterAddress
#% type: string
#% description: Name of the Salt master VM.
#% required: true
#% - name: project
#% type: string
#% description: Name of the Cloud project.
#% required: true
#% - name: zone
#% type: string
#% description: Zone to create the resources in.
#% required: true
"""Generates config for a VM running a SaltStack master.
Just for fun this template is in Python, while the others in this
directory are in Jinja2.
"""
def GenerateConfig(evaluation_context):
return """
resources:
- type: compute.v1.firewall
name: %(master)s-firewall
properties:
network: https://www.googleapis.com/compute/v1/projects/%(project)s/global/networks/default
sourceRanges: [ "0.0.0.0/0" ]
allowed:
- IPProtocol: tcp
ports: [ "4505", "4506" ]
- type: compute.v1.instance
name: %(master)s
properties:
zone: %(zone)s
machineType: https://www.googleapis.com/compute/v1/projects/%(project)s/zones/%(zone)s/machineTypes/f1-micro
disks:
- deviceName: boot
type: PERSISTENT
boot: true
autoDelete: true
initializeParams:
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/%(project)s/global/networks/default
accessConfigs:
- name: External NAT
type: ONE_TO_ONE_NAT
metadata:
items:
- key: startup-script
value: startup-script-value
""" % {"master": evaluation_context.properties["masterAddress"],
"project": evaluation_context.env["project"],
"zone": evaluation_context.properties["zone"]}

@ -0,0 +1,8 @@
imports: ["python_template_with_env.py"]
resources:
- name: python_template_with_env_name
type: python_template_with_env.py
properties:
masterAddress: master-address
zone: my-zone

@ -0,0 +1,46 @@
config:
resources:
- name: master-address-firewall
properties:
allowed:
- IPProtocol: tcp
ports:
- '4505'
- '4506'
network: https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default
sourceRanges:
- 0.0.0.0/0
type: compute.v1.firewall
- name: master-address
properties:
disks:
- autoDelete: true
boot: true
deviceName: boot
initializeParams:
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
type: PERSISTENT
machineType: https://www.googleapis.com/compute/v1/projects/my-project/zones/my-zone/machineTypes/f1-micro
metadata:
items:
- key: startup-script
value: startup-script-value
networkInterfaces:
- accessConfigs:
- name: External NAT
type: ONE_TO_ONE_NAT
network: https://www.googleapis.com/compute/v1/projects/my-project/global/networks/default
zone: my-zone
type: compute.v1.instance
layout:
resources:
- name: python_template_with_env_name
properties:
masterAddress: master-address
zone: my-zone
resources:
- name: master-address-firewall
type: compute.v1.firewall
- name: master-address
type: compute.v1.instance
type: python_template_with_env.py

@ -0,0 +1,20 @@
# Copyright 2014 Google Inc. All Rights Reserved.
"""Constructs a VM."""
import json
import helpers.common
import helpers.extra.common2
def GenerateConfig(_):
"""Generates config of a VM."""
return """
resources:
- name: %s
type: compute.v1.instance
properties:
machineSize: %s
""" % (helpers.common.GenerateMachineName(
json.dumps('myFrontend').strip('"'), 'prod'),
helpers.extra.common2.GenerateMachineSize())

@ -0,0 +1,5 @@
imports: ["python_template_with_import.py", "helpers/common.py", "helpers/common2.py", "helpers/__init__.py"]
resources:
- name: python_template_with_import_name
type: python_template_with_import.py

@ -0,0 +1,13 @@
config:
resources:
- name: myFrontend-prod
properties:
machineSize: big
type: compute.v1.instance
layout:
resources:
- name: python_template_with_import_name
resources:
- name: myFrontend-prod
type: compute.v1.instance
type: python_template_with_import.py

@ -0,0 +1,22 @@
# Copyright 2014 Google Inc. All Rights Reserved.
"""Constructs a VM."""
# Verify that both ways of hierarchical imports work.
from helpers import common
import helpers.extra.common2
def GenerateConfig(evaluation_context):
"""Generates config of a VM."""
return """
resources:
- name: %s
type: compute.v1.instance
properties:
description: %s
machineSize: %s
""" % (common.GenerateMachineName("myFrontend", "prod"),
evaluation_context.imports[
evaluation_context.properties["description-file"]],
helpers.extra.common2.GenerateMachineSize())

@ -0,0 +1,7 @@
imports: ["python_template_with_inlinedfile.py", "helpers/common.py", "helpers/common2.py", "helpers/__init__.py", "description_text.txt"]
resources:
- name: python_template_with_inlinedfile_name
type: python_template_with_inlinedfile.py
properties:
description-file: description_text.txt

@ -0,0 +1,21 @@
config:
resources:
- name: myFrontend-prod
properties:
description: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
machineSize: big
type: compute.v1.instance
layout:
resources:
- name: python_template_with_inlinedfile_name
properties:
description-file: description_text.txt
resources:
- name: myFrontend-prod
type: compute.v1.instance
type: python_template_with_inlinedfile.py

@ -0,0 +1,9 @@
# Copyright 2014 Google Inc. All Rights Reserved.
"""A python script that raise exceptions.
"""
def GenerateConfig(unused_context):
raise NameError('No file found')

@ -0,0 +1,9 @@
imports: ["python_with_exception.py"]
resources:
- name: python_with_exception_name
type: python_with_exception.py
properties:
masterAddress: master-address
project: my-project
zone: my-zone

@ -0,0 +1,18 @@
resources:
- type: compute.v1.instance
name: vm-created-by-cloud-config-{{ params["deployment"] }}
properties:
zone: test-zone
machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro
disks:
- deviceName: boot
type: PERSISTENT
boot: true
autoDelete: true
initializeParams:
diskName: disk-created-by-cloud-config-test-deployment
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default

@ -0,0 +1,21 @@
config:
resources:
- name: vm-created-by-cloud-config-{{ params["deployment"] }}
properties:
disks:
- autoDelete: true
boot: true
deviceName: boot
initializeParams:
diskName: disk-created-by-cloud-config-test-deployment
sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140619
type: PERSISTENT
machineType: https://www.googleapis.com/compute/v1/projects/test-project/zones/test-zone/machineTypes/f1-micro
networkInterfaces:
- network: https://www.googleapis.com/compute/v1/projects/test-project/global/networks/default
zone: test-zone
type: compute.v1.instance
layout:
resources:
- name: vm-created-by-cloud-config-{{ params["deployment"] }}
type: compute.v1.instance

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save