Implement encryption for genesis/join scripts
This introduces a new document called `EncryptionPolicy` to configure this behavior. It currently only supports using symmetric encryption with `GPG`, but that should be available on all Ubuntu systems (which is what we currently support) and should also be fairly reliable. Change-Id: I06d4faa119b736773df0d8cbf0e7a23fd98edcdf Depends-On: https://review.openstack.org/#/c/602175/
This commit is contained in:
parent
24e4ebf37a
commit
8bc8c7c028
8
Makefile
8
Makefile
@ -36,7 +36,7 @@ CHARTS := $(patsubst charts/%/.,%,$(wildcard charts/*/.))
|
||||
all: charts lint
|
||||
|
||||
.PHONY: tests
|
||||
tests: gate-lint
|
||||
tests: external-deps gate-lint
|
||||
tox
|
||||
|
||||
.PHONY: tests-security
|
||||
@ -48,9 +48,13 @@ docs:
|
||||
tox -e docs
|
||||
|
||||
.PHONY: tests-unit
|
||||
tests-unit:
|
||||
tests-unit: external-deps
|
||||
tox -e py35
|
||||
|
||||
.PHONY: external-deps
|
||||
external-deps:
|
||||
./tools/install-external-deps.sh
|
||||
|
||||
.PHONY: tests-pep8
|
||||
tests-pep8:
|
||||
tox -e pep8
|
||||
|
33
doc/source/configuration/encryption-policy.yaml
Normal file
33
doc/source/configuration/encryption-policy.yaml
Normal file
@ -0,0 +1,33 @@
|
||||
EncryptionPolicy
|
||||
================
|
||||
|
||||
Encryption policy defines how encryption should be applied via Promenade. The
|
||||
primary use-case for this is to encrypt ``genesis.sh`` or ``join.sh`` scripts.
|
||||
|
||||
Sample Document
|
||||
---------------
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
---
|
||||
schema: promenade/EncryptionPolicy/v1
|
||||
metadata:
|
||||
schema: metadata/Document/v1
|
||||
name: encryption-policy
|
||||
layeringDefinition:
|
||||
abstract: false
|
||||
layer: site
|
||||
storagePolicy: cleartext
|
||||
data:
|
||||
scripts:
|
||||
genesis:
|
||||
gpg: {}
|
||||
...
|
||||
|
||||
|
||||
Scripts
|
||||
-------
|
||||
|
||||
The genesis and join scripts can be built with sensitive content encrypted.
|
||||
Currently the only encryption method available is ``gpg``, which can be enabled
|
||||
by setting that key to an empty dictionary.
|
@ -12,6 +12,7 @@ Details about Promenade-specific documents can be found here:
|
||||
:caption: Documents
|
||||
|
||||
docker
|
||||
encryption-policy
|
||||
genesis
|
||||
host-system
|
||||
kubelet
|
||||
|
14
examples/basic/EncryptionPolicy.yaml
Normal file
14
examples/basic/EncryptionPolicy.yaml
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
schema: promenade/EncryptionPolicy/v1
|
||||
metadata:
|
||||
schema: metadata/Document/v1
|
||||
name: encryption-policy
|
||||
layeringDefinition:
|
||||
abstract: false
|
||||
layer: site
|
||||
storagePolicy: cleartext
|
||||
data:
|
||||
scripts:
|
||||
genesis:
|
||||
gpg: {}
|
||||
...
|
@ -1,4 +1,4 @@
|
||||
from . import logging, renderer
|
||||
from . import encryption_method, logging, renderer
|
||||
from beaker.cache import CacheManager
|
||||
from beaker.util import parse_cache_config_options
|
||||
|
||||
@ -72,6 +72,14 @@ class Builder:
|
||||
_write_script(output_dir, 'validate-cluster.sh', validate_script)
|
||||
|
||||
def build_genesis(self, *, output_dir):
|
||||
script = self.build_genesis_script()
|
||||
_write_script(output_dir, 'genesis.sh', script)
|
||||
|
||||
if self.validators:
|
||||
validate_script = self._build_genesis_validate_script()
|
||||
_write_script(output_dir, 'validate-genesis.sh', validate_script)
|
||||
|
||||
def build_genesis_script(self):
|
||||
LOG.info('Building genesis script')
|
||||
sub_config = self.config.extract_genesis_config()
|
||||
tarball = renderer.build_tarball_from_roles(
|
||||
@ -79,17 +87,23 @@ class Builder:
|
||||
roles=['common', 'genesis'],
|
||||
file_specs=self.file_cache.values())
|
||||
|
||||
script = renderer.render_template(
|
||||
(encrypted_tarball, decrypt_setup_command, decrypt_command,
|
||||
decrypt_teardown_command) = _encrypt_genesis(sub_config, tarball)
|
||||
|
||||
return renderer.render_template(
|
||||
sub_config,
|
||||
template='scripts/genesis.sh',
|
||||
context={'tarball': tarball})
|
||||
context={
|
||||
'decrypt_command': decrypt_command,
|
||||
'decrypt_setup_command': decrypt_setup_command,
|
||||
'decrypt_teardown_command': decrypt_teardown_command,
|
||||
'encrypted_tarball': encrypted_tarball,
|
||||
})
|
||||
|
||||
_write_script(output_dir, 'genesis.sh', script)
|
||||
|
||||
if self.validators:
|
||||
validate_script = renderer.render_template(
|
||||
def _build_genesis_validate_script(self):
|
||||
sub_config = self.config.extract_genesis_config()
|
||||
return renderer.render_template(
|
||||
sub_config, template='scripts/validate-genesis.sh')
|
||||
_write_script(output_dir, 'validate-genesis.sh', validate_script)
|
||||
|
||||
def build_node(self, node_document, *, output_dir):
|
||||
node_name = node_document['metadata']['name']
|
||||
@ -112,10 +126,18 @@ class Builder:
|
||||
tarball = renderer.build_tarball_from_roles(
|
||||
config=sub_config, roles=['common', 'join'], file_specs=file_specs)
|
||||
|
||||
(encrypted_tarball, decrypt_setup_command, decrypt_command,
|
||||
decrypt_teardown_command) = _encrypt_node(sub_config, tarball)
|
||||
|
||||
return renderer.render_template(
|
||||
sub_config,
|
||||
template='scripts/join.sh',
|
||||
context={'tarball': tarball})
|
||||
context={
|
||||
'decrypt_command': decrypt_command,
|
||||
'decrypt_setup_command': decrypt_setup_command,
|
||||
'decrypt_teardown_command': decrypt_teardown_command,
|
||||
'encrypted_tarball': encrypted_tarball,
|
||||
})
|
||||
|
||||
def _build_node_validate_script(self, node_name):
|
||||
sub_config = self.config.extract_node_config(node_name)
|
||||
@ -123,6 +145,24 @@ class Builder:
|
||||
sub_config, template='scripts/validate-join.sh')
|
||||
|
||||
|
||||
def _encrypt_genesis(config, data):
|
||||
return _encrypt(config.get_path('EncryptionPolicy:scripts.genesis'), data)
|
||||
|
||||
|
||||
def _encrypt_node(config, data):
|
||||
return _encrypt(config.get_path('EncryptionPolicy:scripts.join'), data)
|
||||
|
||||
|
||||
def _encrypt(cfg_dict, data):
|
||||
method = encryption_method.EncryptionMethod.from_config(cfg_dict)
|
||||
encrypted_data = method.encrypt(data)
|
||||
decrypt_setup_command = method.get_decrypt_setup_command()
|
||||
decrypt_command = method.get_decrypt_command()
|
||||
decrypt_teardown_command = method.get_decrypt_teardown_command()
|
||||
return (encrypted_data, decrypt_setup_command, decrypt_command,
|
||||
decrypt_teardown_command)
|
||||
|
||||
|
||||
@CACHE.cache('fetch_tarball_content', expire=72 * 3600)
|
||||
def _fetch_tar_content(url, path):
|
||||
content = _fetch_tar_url(url)
|
||||
|
182
promenade/encryption_method.py
Normal file
182
promenade/encryption_method.py
Normal file
@ -0,0 +1,182 @@
|
||||
from . import exceptions, logging
|
||||
import abc
|
||||
import os
|
||||
# Ignore bandit false positive: B404:blacklist
|
||||
# The purpose of this module is to safely encapsulate calls via fork.
|
||||
import subprocess # nosec
|
||||
import tempfile
|
||||
|
||||
__all__ = ['EncryptionMethod']
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EncryptionMethod(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def encrypt(self, data):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_decrypt_setup_command(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_decrypt_command(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_decrypt_teardown_command(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def from_config(config):
|
||||
LOG.debug('Building EncryptionMethod from: %s', config)
|
||||
if config:
|
||||
# NOTE(mark-burnett): Relying on the schema to ensure valid
|
||||
# configuration.
|
||||
name = list(config.keys())[0]
|
||||
kwargs = config[name]
|
||||
if name == 'gpg':
|
||||
return GPGEncryptionMethod(**kwargs)
|
||||
else:
|
||||
raise NotImplementedError('Unknown Encryption method')
|
||||
else:
|
||||
return NullEncryptionMethod()
|
||||
|
||||
def notify_user(self, message):
|
||||
print('=== BEGIN NOTICE ===')
|
||||
print(message)
|
||||
print('=== END NOTICE ===')
|
||||
|
||||
|
||||
class NullEncryptionMethod(EncryptionMethod):
|
||||
def encrypt(self, data):
|
||||
LOG.debug('Performing NOOP encryption')
|
||||
return data
|
||||
|
||||
def get_decrypt_setup_command(self):
|
||||
return ''
|
||||
|
||||
def get_decrypt_command(self):
|
||||
return 'cat'
|
||||
|
||||
def get_decrypt_teardown_command(self):
|
||||
return ''
|
||||
|
||||
|
||||
class GPGEncryptionMethod(EncryptionMethod):
|
||||
ENCRYPTION_KEY_ENV_NAME = 'PROMENADE_ENCRYPTION_KEY'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._gpg_version = _detect_gpg_version()
|
||||
|
||||
def encrypt(self, data):
|
||||
key = self._get_key()
|
||||
return self._encrypt_data(key, data)
|
||||
|
||||
def get_decrypt_setup_command(self):
|
||||
return '''
|
||||
export DECRYPTION_KEY=${PROMENADE_ENCRYPTION_KEY:-"NONE"}
|
||||
if [[ ${PROMENADE_ENCRYPTION_KEY} = "NONE" ]]; then
|
||||
read -p "Script decryption key: " -s DECRYPTION_KEY
|
||||
fi
|
||||
'''
|
||||
|
||||
def get_decrypt_command(self):
|
||||
return ('/usr/bin/gpg --verbose --decrypt '
|
||||
'--passphrase "${DECRYPTION_KEY}"')
|
||||
|
||||
def get_decrypt_teardown_command(self):
|
||||
return 'unset DECRYPTION_KEY'
|
||||
|
||||
def _get_key(self):
|
||||
key = os.environ.get(self.ENCRYPTION_KEY_ENV_NAME)
|
||||
if key is None:
|
||||
key = _generate_key()
|
||||
self.notify_user('Copy this decryption key for use during script '
|
||||
'execution:\n%s' % key)
|
||||
else:
|
||||
LOG.info('Using encryption key from %s',
|
||||
self.ENCRYPTION_KEY_ENV_NAME)
|
||||
|
||||
return key
|
||||
|
||||
def _encrypt_data(self, key, data):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
# Ignore bandit false positive:
|
||||
# B603:subprocess_without_shell_equals_true
|
||||
# Here user input is allowed to be arbitrary, as it's simply input
|
||||
# to the specified encryption algorithm. Regardless, we only put a
|
||||
# tarball here.
|
||||
p = subprocess.Popen( # nosec
|
||||
[
|
||||
'/usr/bin/gpg',
|
||||
'--verbose',
|
||||
'--symmetric',
|
||||
'--homedir',
|
||||
tmp,
|
||||
'--passphrase',
|
||||
key,
|
||||
] + self._gpg_encrypt_options(),
|
||||
cwd=tmp,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
|
||||
try:
|
||||
out, err = p.communicate(data, timeout=120)
|
||||
except subprocess.TimeoutExpired:
|
||||
p.kill()
|
||||
out, err = p.communicate()
|
||||
|
||||
if p.returncode != 0:
|
||||
LOG.error('Got errors from gpg encrypt: %s', err)
|
||||
raise exceptions.EncryptionException(description=str(err))
|
||||
|
||||
return out
|
||||
|
||||
def _gpg_encrypt_options(self):
|
||||
options = {
|
||||
1: [],
|
||||
2: ['--pinentry-mode', 'loopback'],
|
||||
}
|
||||
return options[self._gpg_version[0]]
|
||||
|
||||
|
||||
DETECTION_PREFIX = 'gpg (GnuPG) '
|
||||
|
||||
|
||||
def _detect_gpg_version():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
# Ignore bandit false positive:
|
||||
# B603:subprocess_without_shell_equals_true
|
||||
# This method takes no input and simply queries the version of gpg.
|
||||
output = subprocess.check_output( # nosec
|
||||
[
|
||||
'/usr/bin/gpg',
|
||||
'--version',
|
||||
], cwd=tmp)
|
||||
lines = output.decode('utf-8').strip().splitlines()
|
||||
if lines:
|
||||
version = lines[0][len(DETECTION_PREFIX):]
|
||||
LOG.debug('Found GPG version %s', version)
|
||||
return tuple(map(int, version.split('.')[:2]))
|
||||
else:
|
||||
raise exceptions.GPGDetectionException()
|
||||
|
||||
|
||||
def _generate_key():
|
||||
# Ignore bandit false positive:
|
||||
# B603:subprocess_without_shell_equals_true
|
||||
# This method takes no input and generates random output.
|
||||
result = subprocess.run( # nosec
|
||||
['/usr/bin/openssl', 'rand', '-hex', '48'],
|
||||
check=True,
|
||||
env={
|
||||
'RANDFILE': '/tmp/rnd',
|
||||
},
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
|
||||
return result.stdout.decode().strip()
|
@ -343,6 +343,16 @@ class NodeNotFoundException(KubernetesApiError):
|
||||
status = falcon.HTTP_404
|
||||
|
||||
|
||||
class EncryptionException(ApiError):
|
||||
title = 'Payload encryption error'
|
||||
status = falcon.HTTP_500
|
||||
|
||||
|
||||
class GPGDetectionException(ApiError):
|
||||
title = 'Failed to detect GPG version'
|
||||
status = falcon.HTTP_500
|
||||
|
||||
|
||||
def massage_error_list(error_list, placeholder_description):
|
||||
"""
|
||||
Returns a best-effort attempt to make a nice error list
|
||||
|
@ -10,17 +10,16 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Generator:
|
||||
def __init__(self, config):
|
||||
def __init__(self, config, block_strings=True):
|
||||
self.config = config
|
||||
self.keys = pki.PKI()
|
||||
self.documents = []
|
||||
self.keys = pki.PKI(block_strings=block_strings)
|
||||
self.outputs = collections.defaultdict(dict)
|
||||
|
||||
@property
|
||||
def cluster_domain(self):
|
||||
return self.config['KubernetesNetwork:dns.cluster_domain']
|
||||
|
||||
def generate(self, output_dir):
|
||||
def generate(self, output_dir=None):
|
||||
for catalog in self.config.iterate(kind='PKICatalog'):
|
||||
for ca_name, ca_def in catalog['data'].get(
|
||||
'certificate_authorities', {}).items():
|
||||
@ -40,6 +39,7 @@ class Generator:
|
||||
document_name = keypair_def['name']
|
||||
self.get_or_gen_keypair(document_name)
|
||||
|
||||
if output_dir:
|
||||
self._write(output_dir)
|
||||
|
||||
def get_or_gen_ca(self, document_name):
|
||||
@ -126,18 +126,21 @@ class Generator:
|
||||
return result
|
||||
|
||||
def _write(self, output_dir):
|
||||
docs = list(
|
||||
itertools.chain.from_iterable(
|
||||
v.values() for v in self.outputs.values()))
|
||||
documents = self.get_documents()
|
||||
with open(os.path.join(output_dir, 'certificates.yaml'), 'w') as f:
|
||||
# Don't use safe_dump_all so we can block format certificate data.
|
||||
yaml.dump_all(
|
||||
docs,
|
||||
documents,
|
||||
stream=f,
|
||||
default_flow_style=False,
|
||||
explicit_start=True,
|
||||
indent=2)
|
||||
|
||||
def get_documents(self):
|
||||
return list(
|
||||
itertools.chain.from_iterable(
|
||||
v.values() for v in self.outputs.values()))
|
||||
|
||||
|
||||
def get_host_list(service_names):
|
||||
service_list = []
|
||||
|
@ -13,7 +13,8 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PKI:
|
||||
def __init__(self):
|
||||
def __init__(self, *, block_strings=True):
|
||||
self.block_strings = block_strings
|
||||
self._ca_config_string = None
|
||||
|
||||
@property
|
||||
@ -116,9 +117,11 @@ class PKI:
|
||||
# Ignore bandit false positive:
|
||||
# B603:subprocess_without_shell_equals_true
|
||||
# This method wraps cfssl calls originating from this module.
|
||||
return json.loads( # nosec
|
||||
subprocess.check_output(
|
||||
['cfssl'] + command, cwd=tmp, stderr=subprocess.PIPE))
|
||||
result = subprocess.check_output( # nosec
|
||||
['cfssl'] + command, cwd=tmp, stderr=subprocess.PIPE)
|
||||
if not isinstance(result, str):
|
||||
result = result.decode('utf-8')
|
||||
return json.loads(result)
|
||||
|
||||
def _openssl(self, command, *, files=None):
|
||||
if not files:
|
||||
@ -175,9 +178,15 @@ class PKI:
|
||||
},
|
||||
'storagePolicy': 'cleartext',
|
||||
},
|
||||
'data': block_literal(data),
|
||||
'data': self._block_literal(data),
|
||||
}
|
||||
|
||||
def _block_literal(self, data):
|
||||
if self.block_strings:
|
||||
return block_literal(data)
|
||||
else:
|
||||
return data
|
||||
|
||||
|
||||
class block_literal(str):
|
||||
pass
|
||||
|
33
promenade/schemas/EncryptionPolicy.yaml
Normal file
33
promenade/schemas/EncryptionPolicy.yaml
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
schema: deckhand/DataSchema/v1
|
||||
metadata:
|
||||
schema: metadata/Control/v1
|
||||
name: promenade/EncryptionPolicy/v1
|
||||
labels:
|
||||
application: promenade
|
||||
data:
|
||||
$schema: http://json-schema.org/schema#
|
||||
|
||||
definitions:
|
||||
script_encryption:
|
||||
oneof:
|
||||
- { $ref: '#/definitions/encryption_method_gpg' }
|
||||
|
||||
encryption_method_gpg:
|
||||
properties:
|
||||
gpg:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- gpg
|
||||
additionalProperties: false
|
||||
|
||||
properties:
|
||||
scripts:
|
||||
properties:
|
||||
genesis:
|
||||
$ref: '#/definitions/script_encryption'
|
||||
join:
|
||||
$ref: '#/definitions/script_encryption'
|
||||
additionalProperties: false
|
||||
...
|
@ -10,7 +10,10 @@ chmod 700 /etc/kubernetes
|
||||
set +x
|
||||
log
|
||||
log === Extracting prepared files ===
|
||||
echo "{{ tarball | b64enc }}" | base64 -d | tar -zxv -C / | tee /etc/promenade-manifest
|
||||
{{ decrypt_setup_command }}
|
||||
echo "{{ encrypted_tarball | b64enc }}" | base64 -d | {{ decrypt_command }} | tar -zxv -C / | tee /etc/promenade-manifest
|
||||
{{ decrypt_teardown_command }}
|
||||
set -x
|
||||
|
||||
# Adding apt repositories
|
||||
#
|
||||
|
@ -105,7 +105,7 @@ def check_schema(document, schemas=None):
|
||||
except jsonschema.ValidationError as e:
|
||||
raise exceptions.ValidationException(str(e))
|
||||
else:
|
||||
LOG.warning('Skipping validation for unknown schema: %s', schema_name)
|
||||
LOG.debug('Skipping validation for unknown schema: %s', schema_name)
|
||||
|
||||
|
||||
SCHEMAS = {}
|
||||
|
18
tests/unit/builder_data/simple/Docker.yaml
Normal file
18
tests/unit/builder_data/simple/Docker.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
schema: promenade/Docker/v1
|
||||
metadata:
|
||||
schema: metadata/Document/v1
|
||||
name: docker
|
||||
layeringDefinition:
|
||||
abstract: false
|
||||
layer: site
|
||||
storagePolicy: cleartext
|
||||
data:
|
||||
config:
|
||||
insecure-registries:
|
||||
- registry:5000
|
||||
live-restore: true
|
||||
max-concurrent-downloads: 10
|
||||
oom-score-adjust: -999
|
||||
storage-driver: overlay2
|
||||
...
|
16
tests/unit/builder_data/simple/EncryptionPolicy.yaml
Normal file
16
tests/unit/builder_data/simple/EncryptionPolicy.yaml
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
schema: promenade/EncryptionPolicy/v1
|
||||
metadata:
|
||||
schema: metadata/Document/v1
|
||||
name: testingpolicy
|
||||
layeringDefinition:
|
||||
abstract: false
|
||||
layer: site
|
||||
storagePolicy: cleartext
|
||||
data:
|
||||
scripts:
|
||||
genesis:
|
||||
gpg: {}
|
||||
join:
|
||||
gpg: {}
|
||||
...
|
45
tests/unit/builder_data/simple/Genesis.yaml
Normal file
45
tests/unit/builder_data/simple/Genesis.yaml
Normal file
@ -0,0 +1,45 @@
|
||||
---
|
||||
schema: promenade/Genesis/v1
|
||||
metadata:
|
||||
schema: metadata/Document/v1
|
||||
name: genesis
|
||||
layeringDefinition:
|
||||
abstract: false
|
||||
layer: site
|
||||
storagePolicy: cleartext
|
||||
data:
|
||||
hostname: n0
|
||||
ip: 192.168.77.10
|
||||
apiserver:
|
||||
command_prefix:
|
||||
- /apiserver
|
||||
- --authorization-mode=Node,RBAC
|
||||
- --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota,DefaultTolerationSeconds
|
||||
- --service-cluster-ip-range=10.96.0.0/16
|
||||
- --endpoint-reconciler-type=lease
|
||||
armada:
|
||||
target_manifest: cluster-bootstrap
|
||||
labels:
|
||||
dynamic:
|
||||
- calico-etcd=enabled
|
||||
- coredns=enabled
|
||||
- kubernetes-apiserver=enabled
|
||||
- kubernetes-controller-manager=enabled
|
||||
- kubernetes-etcd=enabled
|
||||
- kubernetes-scheduler=enabled
|
||||
- promenade-genesis=enabled
|
||||
- ucp-control-plane=enabled
|
||||
images:
|
||||
armada: quay.io/airshipit/armada:master
|
||||
helm:
|
||||
tiller: gcr.io/kubernetes-helm/tiller:v2.9.1
|
||||
kubernetes:
|
||||
apiserver: gcr.io/google_containers/hyperkube-amd64:v1.10.2
|
||||
controller-manager: gcr.io/google_containers/hyperkube-amd64:v1.10.2
|
||||
etcd: quay.io/coreos/etcd:v3.2.14
|
||||
scheduler: gcr.io/google_containers/hyperkube-amd64:v1.10.2
|
||||
files:
|
||||
- path: /var/lib/anchor/calico-etcd-bootstrap
|
||||
content: "# placeholder for triggering calico etcd bootstrapping"
|
||||
mode: 0644
|
||||
...
|
86
tests/unit/builder_data/simple/HostSystem.yaml
Normal file
86
tests/unit/builder_data/simple/HostSystem.yaml
Normal file
@ -0,0 +1,86 @@
|
||||
---
|
||||
schema: promenade/HostSystem/v1
|
||||
metadata:
|
||||
schema: metadata/Document/v1
|
||||
name: host-system
|
||||
layeringDefinition:
|
||||
abstract: false
|
||||
layer: site
|
||||
storagePolicy: cleartext
|
||||
data:
|
||||
files:
|
||||
# NOTE(mark-burnett): A kubelet would be required for a real deployment
|
||||
# (either here or via debian package; however, these unit tests don't
|
||||
# attempt to actually run Kubernetes, only to construct the genesis and
|
||||
# join scripts.
|
||||
# - path: /opt/kubernetes/bin/kubelet
|
||||
# tar_url: https://dl.k8s.io/v1.10.2/kubernetes-node-linux-amd64.tar.gz
|
||||
# tar_path: kubernetes/node/bin/kubelet
|
||||
# mode: 0555
|
||||
- path: /etc/logrotate.d/json-logrotate
|
||||
mode: 0444
|
||||
content: |-
|
||||
/var/lib/docker/containers/*/*-json.log
|
||||
{
|
||||
compress
|
||||
copytruncate
|
||||
create 0644 root root
|
||||
daily
|
||||
dateext
|
||||
dateformat -%Y%m%d-%s
|
||||
maxsize 10M
|
||||
missingok
|
||||
notifempty
|
||||
su root root
|
||||
rotate 1
|
||||
}
|
||||
images:
|
||||
haproxy: haproxy:1.8.3
|
||||
helm:
|
||||
helm: lachlanevenson/k8s-helm:v2.9.1
|
||||
kubernetes:
|
||||
kubectl: gcr.io/google_containers/hyperkube-amd64:v1.10.2
|
||||
packages:
|
||||
repositories:
|
||||
- deb http://apt.dockerproject.org/repo ubuntu-xenial main
|
||||
keys:
|
||||
- |-
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBFWln24BEADrBl5p99uKh8+rpvqJ48u4eTtjeXAWbslJotmC/CakbNSqOb9o
|
||||
ddfzRvGVeJVERt/Q/mlvEqgnyTQy+e6oEYN2Y2kqXceUhXagThnqCoxcEJ3+KM4R
|
||||
mYdoe/BJ/J/6rHOjq7Omk24z2qB3RU1uAv57iY5VGw5p45uZB4C4pNNsBJXoCvPn
|
||||
TGAs/7IrekFZDDgVraPx/hdiwopQ8NltSfZCyu/jPpWFK28TR8yfVlzYFwibj5WK
|
||||
dHM7ZTqlA1tHIG+agyPf3Rae0jPMsHR6q+arXVwMccyOi+ULU0z8mHUJ3iEMIrpT
|
||||
X+80KaN/ZjibfsBOCjcfiJSB/acn4nxQQgNZigna32velafhQivsNREFeJpzENiG
|
||||
HOoyC6qVeOgKrRiKxzymj0FIMLru/iFF5pSWcBQB7PYlt8J0G80lAcPr6VCiN+4c
|
||||
NKv03SdvA69dCOj79PuO9IIvQsJXsSq96HB+TeEmmL+xSdpGtGdCJHHM1fDeCqkZ
|
||||
hT+RtBGQL2SEdWjxbF43oQopocT8cHvyX6Zaltn0svoGs+wX3Z/H6/8P5anog43U
|
||||
65c0A+64Jj00rNDr8j31izhtQMRo892kGeQAaaxg4Pz6HnS7hRC+cOMHUU4HA7iM
|
||||
zHrouAdYeTZeZEQOA7SxtCME9ZnGwe2grxPXh/U/80WJGkzLFNcTKdv+rwARAQAB
|
||||
tDdEb2NrZXIgUmVsZWFzZSBUb29sIChyZWxlYXNlZG9ja2VyKSA8ZG9ja2VyQGRv
|
||||
Y2tlci5jb20+iQI4BBMBAgAiBQJVpZ9uAhsvBgsJCAcDAgYVCAIJCgsEFgIDAQIe
|
||||
AQIXgAAKCRD3YiFXLFJgnbRfEAC9Uai7Rv20QIDlDogRzd+Vebg4ahyoUdj0CH+n
|
||||
Ak40RIoq6G26u1e+sdgjpCa8jF6vrx+smpgd1HeJdmpahUX0XN3X9f9qU9oj9A4I
|
||||
1WDalRWJh+tP5WNv2ySy6AwcP9QnjuBMRTnTK27pk1sEMg9oJHK5p+ts8hlSC4Sl
|
||||
uyMKH5NMVy9c+A9yqq9NF6M6d6/ehKfBFFLG9BX+XLBATvf1ZemGVHQusCQebTGv
|
||||
0C0V9yqtdPdRWVIEhHxyNHATaVYOafTj/EF0lDxLl6zDT6trRV5n9F1VCEh4Aal8
|
||||
L5MxVPcIZVO7NHT2EkQgn8CvWjV3oKl2GopZF8V4XdJRl90U/WDv/6cmfI08GkzD
|
||||
YBHhS8ULWRFwGKobsSTyIvnbk4NtKdnTGyTJCQ8+6i52s+C54PiNgfj2ieNn6oOR
|
||||
7d+bNCcG1CdOYY+ZXVOcsjl73UYvtJrO0Rl/NpYERkZ5d/tzw4jZ6FCXgggA/Zxc
|
||||
jk6Y1ZvIm8Mt8wLRFH9Nww+FVsCtaCXJLP8DlJLASMD9rl5QS9Ku3u7ZNrr5HWXP
|
||||
HXITX660jglyshch6CWeiUATqjIAzkEQom/kEnOrvJAtkypRJ59vYQOedZ1sFVEL
|
||||
MXg2UCkD/FwojfnVtjzYaTCeGwFQeqzHmM241iuOmBYPeyTY5veF49aBJA1gEJOQ
|
||||
TvBR8Q==
|
||||
=Fm3p
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
additional:
|
||||
- curl
|
||||
- jq
|
||||
required:
|
||||
docker: docker-engine=1.13.1-0~ubuntu-xenial
|
||||
socat: socat=1.7.3.1-1
|
||||
validation:
|
||||
pod_logs:
|
||||
image: busybox:1.28.3
|
||||
...
|
21
tests/unit/builder_data/simple/Kubelet.yaml
Normal file
21
tests/unit/builder_data/simple/Kubelet.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
schema: promenade/Kubelet/v1
|
||||
metadata:
|
||||
schema: metadata/Document/v1
|
||||
name: kubelet
|
||||
layeringDefinition:
|
||||
abstract: false
|
||||
layer: site
|
||||
storagePolicy: cleartext
|
||||
data:
|
||||
arguments:
|
||||
- --cni-bin-dir=/opt/cni/bin
|
||||
- --cni-conf-dir=/etc/cni/net.d
|
||||
- --eviction-max-pod-grace-period=-1
|
||||
- --network-plugin=cni
|
||||
- --node-status-update-frequency=5s
|
||||
- --serialize-image-pulls=false
|
||||
- --v=5
|
||||
images:
|
||||
pause: gcr.io/google_containers/pause-amd64:3.0
|
||||
...
|
43
tests/unit/builder_data/simple/KubernetesNetwork.yaml
Normal file
43
tests/unit/builder_data/simple/KubernetesNetwork.yaml
Normal file
@ -0,0 +1,43 @@
|
||||
---
|
||||
schema: promenade/KubernetesNetwork/v1
|
||||
metadata:
|
||||
schema: metadata/Document/v1
|
||||
name: kubernetes-network
|
||||
layeringDefinition:
|
||||
abstract: false
|
||||
layer: site
|
||||
storagePolicy: cleartext
|
||||
data:
|
||||
dns:
|
||||
cluster_domain: cluster.local
|
||||
service_ip: 10.96.0.10
|
||||
bootstrap_validation_checks:
|
||||
- calico-etcd.kube-system.svc.cluster.local
|
||||
- google.com
|
||||
- kubernetes-etcd.kube-system.svc.cluster.local
|
||||
- kubernetes.default.svc.cluster.local
|
||||
upstream_servers:
|
||||
- 8.8.8.8
|
||||
- 8.8.4.4
|
||||
|
||||
kubernetes:
|
||||
apiserver_port: 6443
|
||||
haproxy_port: 6553
|
||||
pod_cidr: 10.97.0.0/16
|
||||
service_cidr: 10.96.0.0/16
|
||||
service_ip: 10.96.0.1
|
||||
|
||||
etcd:
|
||||
container_port: 2379
|
||||
haproxy_port: 2378
|
||||
|
||||
hosts_entries:
|
||||
- ip: 192.168.77.1
|
||||
names:
|
||||
- registry
|
||||
|
||||
# proxy:
|
||||
# url: http://proxy.example.com:8080
|
||||
# additional_no_proxy:
|
||||
# - 10.0.1.1
|
||||
...
|
31
tests/unit/builder_data/simple/KubernetesNode.yaml
Normal file
31
tests/unit/builder_data/simple/KubernetesNode.yaml
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
schema: promenade/KubernetesNode/v1
|
||||
metadata:
|
||||
schema: metadata/Document/v1
|
||||
name: n1
|
||||
layeringDefinition:
|
||||
abstract: false
|
||||
layer: site
|
||||
storagePolicy: cleartext
|
||||
data:
|
||||
hostname: n1
|
||||
ip: 192.169.77.11
|
||||
join_ip: 192.169.77.10
|
||||
labels:
|
||||
dynamic:
|
||||
- calico-etcd=enabled
|
||||
- ceph-mds=enabled
|
||||
- ceph-mon=enabled
|
||||
- ceph-osd=enabled
|
||||
- ceph-rgw=enabled
|
||||
- ceph-mgr=enabled
|
||||
- coredns=enabled
|
||||
- kubernetes-apiserver=enabled
|
||||
- kubernetes-controller-manager=enabled
|
||||
- kubernetes-etcd=enabled
|
||||
- kubernetes-scheduler=enabled
|
||||
- openstack-compute-node=enabled
|
||||
- openstack-control-plane=enabled
|
||||
- openvswitch=enabled
|
||||
- ucp-control-plane=enabled
|
||||
...
|
11
tests/unit/builder_data/simple/LayeringPolicy.yaml
Normal file
11
tests/unit/builder_data/simple/LayeringPolicy.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
schema: deckhand/LayeringPolicy/v1
|
||||
metadata:
|
||||
schema: metadata/Control/v1
|
||||
name: layering-policy
|
||||
data:
|
||||
layerOrder:
|
||||
- global
|
||||
- type
|
||||
- site
|
||||
...
|
167
tests/unit/builder_data/simple/PKICatalog.yaml
Normal file
167
tests/unit/builder_data/simple/PKICatalog.yaml
Normal file
@ -0,0 +1,167 @@
|
||||
---
|
||||
schema: promenade/PKICatalog/v1
|
||||
metadata:
|
||||
schema: metadata/Document/v1
|
||||
name: cluster-certificates
|
||||
layeringDefinition:
|
||||
abstract: false
|
||||
layer: site
|
||||
storagePolicy: cleartext
|
||||
data:
|
||||
certificate_authorities:
|
||||
kubernetes:
|
||||
description: CA for Kubernetes components
|
||||
certificates:
|
||||
- document_name: apiserver
|
||||
description: Service certificate for Kubernetes apiserver
|
||||
common_name: apiserver
|
||||
hosts:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- 10.96.0.1
|
||||
kubernetes_service_names:
|
||||
- kubernetes.default.svc.cluster.local
|
||||
- document_name: kubelet-genesis
|
||||
common_name: system:node:n0
|
||||
hosts:
|
||||
- n0
|
||||
- 192.168.77.10
|
||||
groups:
|
||||
- system:nodes
|
||||
- document_name: kubelet-n0
|
||||
common_name: system:node:n0
|
||||
hosts:
|
||||
- n0
|
||||
- 192.168.77.10
|
||||
groups:
|
||||
- system:nodes
|
||||
- document_name: kubelet-n1
|
||||
common_name: system:node:n1
|
||||
hosts:
|
||||
- n1
|
||||
- 192.168.77.11
|
||||
groups:
|
||||
- system:nodes
|
||||
- document_name: scheduler
|
||||
description: Service certificate for Kubernetes scheduler
|
||||
common_name: system:kube-scheduler
|
||||
- document_name: controller-manager
|
||||
description: certificate for controller-manager
|
||||
common_name: system:kube-controller-manager
|
||||
- document_name: admin
|
||||
common_name: admin
|
||||
groups:
|
||||
- system:masters
|
||||
- document_name: armada
|
||||
common_name: armada
|
||||
groups:
|
||||
- system:masters
|
||||
kubernetes-etcd:
|
||||
description: Certificates for Kubernetes's etcd servers
|
||||
certificates:
|
||||
- document_name: apiserver-etcd
|
||||
description: etcd client certificate for use by Kubernetes apiserver
|
||||
common_name: apiserver
|
||||
# NOTE(mark-burnett): hosts not required for client certificates
|
||||
- document_name: kubernetes-etcd-anchor
|
||||
description: anchor
|
||||
common_name: anchor
|
||||
- document_name: kubernetes-etcd-genesis
|
||||
common_name: kubernetes-etcd-genesis
|
||||
hosts:
|
||||
- n0
|
||||
- 192.168.77.10
|
||||
- 127.0.0.1
|
||||
- localhost
|
||||
- kubernetes-etcd.kube-system.svc.cluster.local
|
||||
- document_name: kubernetes-etcd-n0
|
||||
common_name: kubernetes-etcd-n0
|
||||
hosts:
|
||||
- n0
|
||||
- 192.168.77.10
|
||||
- 127.0.0.1
|
||||
- localhost
|
||||
- kubernetes-etcd.kube-system.svc.cluster.local
|
||||
- document_name: kubernetes-etcd-n1
|
||||
common_name: kubernetes-etcd-n1
|
||||
hosts:
|
||||
- n1
|
||||
- 192.168.77.11
|
||||
- 127.0.0.1
|
||||
- localhost
|
||||
- kubernetes-etcd.kube-system.svc.cluster.local
|
||||
kubernetes-etcd-peer:
|
||||
certificates:
|
||||
- document_name: kubernetes-etcd-genesis-peer
|
||||
common_name: kubernetes-etcd-genesis-peer
|
||||
hosts:
|
||||
- n0
|
||||
- 192.168.77.10
|
||||
- 127.0.0.1
|
||||
- localhost
|
||||
- kubernetes-etcd.kube-system.svc.cluster.local
|
||||
- document_name: kubernetes-etcd-n0-peer
|
||||
common_name: kubernetes-etcd-n0-peer
|
||||
hosts:
|
||||
- n0
|
||||
- 192.168.77.10
|
||||
- 127.0.0.1
|
||||
- localhost
|
||||
- kubernetes-etcd.kube-system.svc.cluster.local
|
||||
- document_name: kubernetes-etcd-n1-peer
|
||||
common_name: kubernetes-etcd-n1-peer
|
||||
hosts:
|
||||
- n1
|
||||
- 192.168.77.11
|
||||
- 127.0.0.1
|
||||
- localhost
|
||||
- kubernetes-etcd.kube-system.svc.cluster.local
|
||||
calico-etcd:
|
||||
description: Certificates for Calico etcd client traffic
|
||||
certificates:
|
||||
- document_name: calico-etcd-anchor
|
||||
description: anchor
|
||||
common_name: anchor
|
||||
- document_name: calico-etcd-n0
|
||||
common_name: calico-etcd-n0
|
||||
hosts:
|
||||
- n0
|
||||
- 192.168.77.10
|
||||
- 127.0.0.1
|
||||
- localhost
|
||||
- 10.96.232.136
|
||||
- document_name: calico-etcd-n1
|
||||
common_name: calico-etcd-n1
|
||||
hosts:
|
||||
- n1
|
||||
- 192.168.77.11
|
||||
- 127.0.0.1
|
||||
- localhost
|
||||
- 10.96.232.136
|
||||
- document_name: calico-node
|
||||
common_name: calcico-node
|
||||
calico-etcd-peer:
|
||||
description: Certificates for Calico etcd clients
|
||||
certificates:
|
||||
- document_name: calico-etcd-n0-peer
|
||||
common_name: calico-etcd-n0-peer
|
||||
hosts:
|
||||
- n0
|
||||
- 192.168.77.10
|
||||
- 127.0.0.1
|
||||
- localhost
|
||||
- 10.96.232.136
|
||||
- document_name: calico-etcd-n1-peer
|
||||
common_name: calico-etcd-n1-peer
|
||||
hosts:
|
||||
- n1
|
||||
- 192.168.77.11
|
||||
- 127.0.0.1
|
||||
- localhost
|
||||
- 10.96.232.136
|
||||
- document_name: calico-node-peer
|
||||
common_name: calcico-node-peer
|
||||
keypairs:
|
||||
- name: service-account
|
||||
description: Service account signing key for use by Kubernetes controller-manager.
|
||||
...
|
1061
tests/unit/builder_data/simple/armada-resources.yaml
Normal file
1061
tests/unit/builder_data/simple/armada-resources.yaml
Normal file
File diff suppressed because it is too large
Load Diff
57
tests/unit/test_builder.py
Normal file
57
tests/unit/test_builder.py
Normal file
@ -0,0 +1,57 @@
|
||||
# Copyright 2018 AT&T Intellectual Property. All other 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.
|
||||
|
||||
from promenade import builder, generator, config, encryption_method
|
||||
import copy
|
||||
import os
|
||||
import pytest
|
||||
|
||||
|
||||
def load_full_config(dirname):
|
||||
this_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
search_dir = os.path.join(this_dir, 'builder_data', dirname)
|
||||
streams = []
|
||||
for filename in os.listdir(search_dir):
|
||||
stream = open(os.path.join(search_dir, filename))
|
||||
streams.append(stream)
|
||||
|
||||
raw_config = config.Configuration.from_streams(
|
||||
allow_missing_substitutions=True,
|
||||
debug=True,
|
||||
streams=streams,
|
||||
substitute=True,
|
||||
validate=False,
|
||||
)
|
||||
g = generator.Generator(raw_config, block_strings=False)
|
||||
g.generate()
|
||||
|
||||
documents = copy.deepcopy(raw_config.documents)
|
||||
documents.extend(copy.deepcopy(g.get_documents()))
|
||||
|
||||
return config.Configuration(
|
||||
allow_missing_substitutions=False,
|
||||
debug=True,
|
||||
documents=documents,
|
||||
substitute=True,
|
||||
validate=True,
|
||||
)
|
||||
|
||||
|
||||
def test_build_simple():
|
||||
b = builder.Builder(load_full_config('simple'))
|
||||
genesis_script = b.build_genesis_script()
|
||||
assert len(genesis_script) > 0
|
||||
|
||||
n1_join_script = b.build_node_script('n1')
|
||||
assert len(n1_join_script) > 0
|
@ -6,6 +6,7 @@ export NGINX_DIR="${TEMP_DIR}/nginx"
|
||||
export NGINX_URL="http://192.168.77.1:7777"
|
||||
export PROMENADE_BASE_URL="http://promenade-api.ucp.svc.cluster.local"
|
||||
export PROMENADE_DEBUG=${PROMENADE_DEBUG:-0}
|
||||
export PROMENADE_ENCRYPTION_KEY=${PROMENADE_ENCRYPTION_KEY:-testkey}
|
||||
export REGISTRY_DATA_DIR=${REGISTRY_DATA_DIR:-/mnt/registry}
|
||||
export VIRSH_POOL=${VIRSH_POOL:-promenade}
|
||||
export VIRSH_POOL_PATH=${VIRSH_POOL_PATH:-/var/lib/libvirt/promenade}
|
||||
|
@ -13,6 +13,7 @@ docker run --rm -t \
|
||||
-w /target \
|
||||
-v "${TEMP_DIR}:/target" \
|
||||
-e "PROMENADE_DEBUG=${PROMENADE_DEBUG}" \
|
||||
-e "PROMENADE_ENCRYPTION_KEY=${PROMENADE_ENCRYPTION_KEY}" \
|
||||
"${IMAGE_PROMENADE}" \
|
||||
promenade \
|
||||
build-all \
|
||||
|
@ -7,7 +7,7 @@ source "${GATE_UTILS}"
|
||||
rsync_cmd "${TEMP_DIR}/scripts"/*genesis* "${GENESIS_NAME}:/root/promenade/"
|
||||
|
||||
set -o pipefail
|
||||
ssh_cmd "${GENESIS_NAME}" /root/promenade/genesis.sh 2>&1 | tee -a "${LOG_FILE}"
|
||||
ssh_cmd "${GENESIS_NAME}" env "PROMENADE_ENCRYPTION_KEY=${PROMENADE_ENCRYPTION_KEY}" /root/promenade/genesis.sh 2>&1 | tee -a "${LOG_FILE}"
|
||||
ssh_cmd "${GENESIS_NAME}" /root/promenade/validate-genesis.sh 2>&1 | tee -a "${LOG_FILE}"
|
||||
set +o pipefail
|
||||
|
||||
|
16
tools/install-external-deps.sh
Executable file
16
tools/install-external-deps.sh
Executable file
@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Installs external dependencies required for basic testing
|
||||
|
||||
set -ex
|
||||
|
||||
CFSSL_URL=${CFSSL_URL:-https://pkg.cfssl.org/R1.2/cfssl_linux-amd64}
|
||||
|
||||
if [[ ! $(which cfssl) ]]; then
|
||||
TMP_DIR=$(mktemp -d)
|
||||
pushd "${TMP_DIR}"
|
||||
curl -Lo cfssl "${CFSSL_URL}"
|
||||
chmod 755 cfssl
|
||||
sudo mv cfssl /usr/local/bin/
|
||||
popd
|
||||
rm -rf "${TMP_DIR}"
|
||||
fi
|
@ -12,7 +12,7 @@ done
|
||||
|
||||
if [[ -x $(which shellcheck) ]]; then
|
||||
echo Checking shell scripts..
|
||||
shellcheck -s bash -e SC2029 "${WORKSPACE}"/tools/cleanup.sh "${WORKSPACE}"/tools/*gate*.sh "${WORKSPACE}"/tools/g2/stages/* "${WORKSPACE}"/tools/g2/lib/*
|
||||
shellcheck -s bash -e SC2029 "${WORKSPACE}"/tools/cleanup.sh "${WORKSPACE}"/tools/*gate*.sh "${WORKSPACE}"/tools/g2/stages/* "${WORKSPACE}"/tools/g2/lib/* "${WORKSPACE}"/tools/install-external-deps.sh
|
||||
else
|
||||
echo No shellcheck executable found. Please, install it.
|
||||
exit 1
|
||||
|
Loading…
x
Reference in New Issue
Block a user