armada/armada/tests/unit/handlers/test_chartbuilder.py

418 lines
16 KiB
Python

# Copyright 2017 The Armada Authors.
#
# 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 inspect
import os
import shutil
import yaml
import fixtures
from hapi.chart.chart_pb2 import Chart
from hapi.chart.metadata_pb2 import Metadata
import mock
from supermutes.dot import dotify
import testtools
from armada.handlers.chartbuilder import ChartBuilder
class ChartBuilderTestCase(testtools.TestCase):
chart_yaml = """
apiVersion: v1
description: A sample Helm chart for Kubernetes
name: hello-world-chart
version: 0.1.0
"""
chart_value = """
# Default values for hello-world-chart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: nginx
tag: stable
pullPolicy: IfNotPresent
service:
name: nginx
type: ClusterIP
externalPort: 38443
internalPort: 80
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
"""
chart_stream = """
chart:
chart_name: mariadb
release: mariadb
namespace: openstack
install:
no_hooks: false
upgrade:
no_hooks: false
values:
replicas: 1
volume:
size: 1Gi
source:
type: git
location: git://github.com/openstack/openstack-helm
subpath: mariadb
reference: master
dependencies: []
"""
dependency_chart_yaml = """
apiVersion: v1
description: Another sample Helm chart for Kubernetes
name: dependency-chart
version: 0.1.0
"""
dependency_chart_stream = """
chart:
chart_name: keystone
release: keystone
namespace: undercloud
timeout: 100
install:
no_hooks: false
upgrade:
no_hooks: false
values: {}
source:
type: git
location: git://github.com/example/example
subpath: example-chart
reference: master
dependencies: []
"""
def _write_temporary_file_contents(self, directory, filename, contents):
path = os.path.join(directory, filename)
fd = os.open(path, os.O_CREAT | os.O_WRONLY)
try:
os.write(fd, contents.encode('utf-8'))
finally:
os.close(fd)
def _make_temporary_subdirectory(self, parent_path, sub_path):
subdir = os.path.join(parent_path, sub_path)
if not os.path.exists(subdir):
os.makedirs(subdir)
self.addCleanup(shutil.rmtree, subdir)
return subdir
def test_source_clone(self):
# Create a temporary directory with Chart.yaml that contains data
# from ``self.chart_yaml``.
chart_dir = self.useFixture(fixtures.TempDir())
self.addCleanup(shutil.rmtree, chart_dir.path)
self._write_temporary_file_contents(chart_dir.path, 'Chart.yaml',
self.chart_yaml)
mock_chart = mock.Mock(source_dir=[chart_dir.path, ''])
chartbuilder = ChartBuilder(mock_chart)
# Validate response type is :class:`hapi.chart.metadata_pb2.Metadata`
resp = chartbuilder.get_metadata()
self.assertIsInstance(resp, Metadata)
def test_get_files(self):
"""Validates that ``get_files()`` ignores 'Chart.yaml', 'values.yaml'
and 'templates' subfolder and all the files contained therein.
"""
# Create a temporary directory that represents a chart source directory
# with various files, including 'Chart.yaml' and 'values.yaml' which
# should be ignored by `get_files()`.
chart_dir = self.useFixture(fixtures.TempDir())
self.addCleanup(shutil.rmtree, chart_dir.path)
for filename in ['foo', 'bar', 'Chart.yaml', 'values.yaml']:
self._write_temporary_file_contents(chart_dir.path, filename, "")
# Create a template directory -- 'templates' -- nested inside the chart
# directory which should also be ignored.
templates_subdir = self._make_temporary_subdirectory(
chart_dir.path, 'templates')
for filename in ['template%d' % x for x in range(3)]:
self._write_temporary_file_contents(templates_subdir, filename, "")
mock_chart = mock.Mock(source_dir=[chart_dir.path, ''])
chartbuilder = ChartBuilder(mock_chart)
expected_files = ('[type_url: "%s"\n, type_url: "%s"\n]' % ('./bar',
'./foo'))
# Validate that only 'foo' and 'bar' are returned.
actual_files = sorted(chartbuilder.get_files(),
key=lambda x: x.type_url)
self.assertEqual(expected_files, repr(actual_files).strip())
def test_get_basic_helm_chart(self):
# Before ChartBuilder is executed the `source_dir` points to a
# directory that was either clone or unpacked from a tarball... pretend
# that that logic has already been performed.
chart_dir = self.useFixture(fixtures.TempDir())
self.addCleanup(shutil.rmtree, chart_dir.path)
self._write_temporary_file_contents(chart_dir.path, 'Chart.yaml',
self.chart_yaml)
ch = yaml.safe_load(self.chart_stream)['chart']
ch['source_dir'] = (chart_dir.path, '')
test_chart = dotify(ch)
chartbuilder = ChartBuilder(test_chart)
helm_chart = chartbuilder.get_helm_chart()
expected = inspect.cleandoc(
"""
metadata {
name: "hello-world-chart"
version: "0.1.0"
description: "A sample Helm chart for Kubernetes"
}
values {
}
"""
).strip()
self.assertIsInstance(helm_chart, Chart)
self.assertTrue(hasattr(helm_chart, 'metadata'))
self.assertTrue(hasattr(helm_chart, 'values'))
self.assertEqual(expected, repr(helm_chart).strip())
def test_get_helm_chart_with_values(self):
chart_dir = self.useFixture(fixtures.TempDir())
self.addCleanup(shutil.rmtree, chart_dir.path)
self._write_temporary_file_contents(chart_dir.path, 'Chart.yaml',
self.chart_yaml)
self._write_temporary_file_contents(chart_dir.path, 'values.yaml',
self.chart_value)
ch = yaml.safe_load(self.chart_stream)['chart']
ch['source_dir'] = (chart_dir.path, '')
test_chart = dotify(ch)
chartbuilder = ChartBuilder(test_chart)
helm_chart = chartbuilder.get_helm_chart()
self.assertIsInstance(helm_chart, Chart)
self.assertTrue(hasattr(helm_chart, 'metadata'))
self.assertTrue(hasattr(helm_chart, 'values'))
self.assertTrue(hasattr(helm_chart.values, 'raw'))
self.assertEqual(self.chart_value, helm_chart.values.raw)
def test_get_helm_chart_with_files(self):
# Create a chart directory with some test files.
chart_dir = self.useFixture(fixtures.TempDir())
self.addCleanup(shutil.rmtree, chart_dir.path)
# Chart.yaml is mandatory for `ChartBuilder.get_metadata`.
self._write_temporary_file_contents(chart_dir.path, 'Chart.yaml',
self.chart_yaml)
self._write_temporary_file_contents(chart_dir.path, 'foo', "foobar")
self._write_temporary_file_contents(chart_dir.path, 'bar', "bazqux")
# Also create a nested directory and verify that files from it are also
# added.
nested_dir = self._make_temporary_subdirectory(
chart_dir.path, 'nested')
self._write_temporary_file_contents(nested_dir, 'nested0', "random")
ch = yaml.safe_load(self.chart_stream)['chart']
ch['source_dir'] = (chart_dir.path, '')
test_chart = dotify(ch)
chartbuilder = ChartBuilder(test_chart)
helm_chart = chartbuilder.get_helm_chart()
expected_files = ('[type_url: "%s"\nvalue: "bazqux"\n, '
'type_url: "%s"\nvalue: "foobar"\n, '
'type_url: "%s"\nvalue: "random"\n]' %
('./bar', './foo', 'nested/nested0'))
self.assertIsInstance(helm_chart, Chart)
self.assertTrue(hasattr(helm_chart, 'metadata'))
self.assertTrue(hasattr(helm_chart, 'values'))
self.assertTrue(hasattr(helm_chart, 'files'))
actual_files = sorted(helm_chart.files, key=lambda x: x.value)
self.assertEqual(expected_files, repr(actual_files).strip())
def test_get_helm_chart_includes_only_relevant_files(self):
chart_dir = self.useFixture(fixtures.TempDir())
self.addCleanup(shutil.rmtree, chart_dir.path)
templates_subdir = self._make_temporary_subdirectory(
chart_dir.path, 'templates')
charts_subdir = self._make_temporary_subdirectory(
chart_dir.path, 'charts')
templates_nested_subdir = self._make_temporary_subdirectory(
templates_subdir, 'bin')
charts_nested_subdir = self._make_temporary_subdirectory(
charts_subdir, 'extra')
self._write_temporary_file_contents(chart_dir.path, 'Chart.yaml',
self.chart_yaml)
self._write_temporary_file_contents(chart_dir.path, 'foo', "foobar")
self._write_temporary_file_contents(chart_dir.path, 'bar', "bazqux")
# Files to ignore within top-level directory.
files_to_ignore = ['Chart.yaml', 'values.yaml', 'values.toml']
for file in files_to_ignore:
self._write_temporary_file_contents(chart_dir.path, file, "")
file_to_ignore = 'file_to_ignore'
# Files to ignore within templates/ subdirectory.
self._write_temporary_file_contents(
templates_subdir, file_to_ignore, "")
# Files to ignore within charts/ subdirectory.
self._write_temporary_file_contents(
charts_subdir, file_to_ignore, "")
# Files to ignore within templates/bin subdirectory.
self._write_temporary_file_contents(
templates_nested_subdir, file_to_ignore, "")
# Files to ignore within charts/extra subdirectory.
self._write_temporary_file_contents(
charts_nested_subdir, file_to_ignore, "")
# Files to **include** within charts/ subdirectory.
self._write_temporary_file_contents(
charts_subdir, '.prov', "xyzzy")
ch = yaml.safe_load(self.chart_stream)['chart']
ch['source_dir'] = (chart_dir.path, '')
test_chart = dotify(ch)
chartbuilder = ChartBuilder(test_chart)
helm_chart = chartbuilder.get_helm_chart()
expected_files = ('[type_url: "%s"\nvalue: "bazqux"\n, '
'type_url: "%s"\nvalue: "foobar"\n, '
'type_url: "%s"\nvalue: "xyzzy"\n]' %
('./bar', './foo', 'charts/.prov'))
# Validate that only relevant files are included, that the ignored
# files are present.
self.assertIsInstance(helm_chart, Chart)
self.assertTrue(hasattr(helm_chart, 'metadata'))
self.assertTrue(hasattr(helm_chart, 'values'))
self.assertTrue(hasattr(helm_chart, 'files'))
actual_files = sorted(helm_chart.files, key=lambda x: x.value)
self.assertEqual(expected_files, repr(actual_files).strip())
def test_get_helm_chart_with_dependencies(self):
# Main chart directory and files.
chart_dir = self.useFixture(fixtures.TempDir())
self.addCleanup(shutil.rmtree, chart_dir.path)
self._write_temporary_file_contents(chart_dir.path, 'Chart.yaml',
self.chart_yaml)
ch = yaml.safe_load(self.chart_stream)['chart']
ch['source_dir'] = (chart_dir.path, '')
# Dependency chart directory and files.
dep_chart_dir = self.useFixture(fixtures.TempDir())
self.addCleanup(shutil.rmtree, dep_chart_dir.path)
self._write_temporary_file_contents(dep_chart_dir.path, 'Chart.yaml',
self.dependency_chart_yaml)
dep_ch = yaml.safe_load(self.dependency_chart_stream)
dep_ch['chart']['source_dir'] = (dep_chart_dir.path, '')
main_chart = dotify(ch)
dependency_chart = dotify(dep_ch)
main_chart.dependencies = [dependency_chart]
chartbuilder = ChartBuilder(main_chart)
helm_chart = chartbuilder.get_helm_chart()
expected_dependency = inspect.cleandoc("""
metadata {
name: "dependency-chart"
version: "0.1.0"
description: "Another sample Helm chart for Kubernetes"
}
values {
}
""").strip()
expected = inspect.cleandoc("""
metadata {
name: "hello-world-chart"
version: "0.1.0"
description: "A sample Helm chart for Kubernetes"
}
dependencies {
metadata {
name: "dependency-chart"
version: "0.1.0"
description: "Another sample Helm chart for Kubernetes"
}
values {
}
}
values {
}
""").strip()
# Validate the main chart.
self.assertIsInstance(helm_chart, Chart)
self.assertTrue(hasattr(helm_chart, 'metadata'))
self.assertTrue(hasattr(helm_chart, 'values'))
self.assertEqual(expected, repr(helm_chart).strip())
# Validate the dependency chart.
self.assertTrue(hasattr(helm_chart, 'dependencies'))
self.assertEqual(1, len(helm_chart.dependencies))
dep_helm_chart = helm_chart.dependencies[0]
self.assertIsInstance(dep_helm_chart, Chart)
self.assertTrue(hasattr(dep_helm_chart, 'metadata'))
self.assertTrue(hasattr(dep_helm_chart, 'values'))
self.assertEqual(expected_dependency, repr(dep_helm_chart).strip())
def test_dump(self):
# Validate base case.
chart_dir = self.useFixture(fixtures.TempDir())
self.addCleanup(shutil.rmtree, chart_dir.path)
self._write_temporary_file_contents(chart_dir.path, 'Chart.yaml',
self.chart_yaml)
ch = yaml.safe_load(self.chart_stream)['chart']
ch['source_dir'] = (chart_dir.path, '')
test_chart = dotify(ch)
chartbuilder = ChartBuilder(test_chart)
self.assertRegex(
repr(chartbuilder.dump()),
'hello-world-chart.*A sample Helm chart for Kubernetes.*')
# Validate recursive case (with dependencies).
dep_chart_dir = self.useFixture(fixtures.TempDir())
self.addCleanup(shutil.rmtree, dep_chart_dir.path)
self._write_temporary_file_contents(dep_chart_dir.path, 'Chart.yaml',
self.dependency_chart_yaml)
dep_ch = yaml.safe_load(self.dependency_chart_stream)
dep_ch['chart']['source_dir'] = (dep_chart_dir.path, '')
dependency_chart = dotify(dep_ch)
test_chart.dependencies = [dependency_chart]
chartbuilder = ChartBuilder(test_chart)
re = inspect.cleandoc("""
hello-world-chart.*A sample Helm chart for Kubernetes.*
dependency-chart.*Another sample Helm chart for Kubernetes.*
""").replace('\n', '').strip()
self.assertRegex(repr(chartbuilder.dump()), re)