Browse Source

Merge "Implement encryption for genesis/join scripts"

Zuul 7 months ago
parent
commit
13d6831aa5

+ 6
- 2
Makefile View File

@@ -36,7 +36,7 @@ CHARTS := $(patsubst charts/%/.,%,$(wildcard charts/*/.))
36 36
 all: charts lint
37 37
 
38 38
 .PHONY: tests
39
-tests: gate-lint
39
+tests: external-deps gate-lint
40 40
 	tox
41 41
 
42 42
 .PHONY: tests-security
@@ -48,9 +48,13 @@ docs:
48 48
 	tox -e docs
49 49
 
50 50
 .PHONY: tests-unit
51
-tests-unit:
51
+tests-unit: external-deps
52 52
 	tox -e py35
53 53
 
54
+.PHONY: external-deps
55
+external-deps:
56
+	./tools/install-external-deps.sh
57
+
54 58
 .PHONY: tests-pep8
55 59
 tests-pep8:
56 60
 	tox -e pep8

+ 33
- 0
doc/source/configuration/encryption-policy.yaml View File

@@ -0,0 +1,33 @@
1
+EncryptionPolicy
2
+================
3
+
4
+Encryption policy defines how encryption should be applied via Promenade.  The
5
+primary use-case for this is to encrypt ``genesis.sh`` or ``join.sh`` scripts.
6
+
7
+Sample Document
8
+---------------
9
+
10
+.. code-block:: yaml
11
+
12
+    ---
13
+    schema: promenade/EncryptionPolicy/v1
14
+    metadata:
15
+      schema: metadata/Document/v1
16
+      name: encryption-policy
17
+      layeringDefinition:
18
+        abstract: false
19
+        layer: site
20
+      storagePolicy: cleartext
21
+    data:
22
+      scripts:
23
+        genesis:
24
+          gpg: {}
25
+    ...
26
+
27
+
28
+Scripts
29
+-------
30
+
31
+The genesis and join scripts can be built with sensitive content encrypted.
32
+Currently the only encryption method available is ``gpg``, which can be enabled
33
+by setting that key to an empty dictionary.

+ 1
- 0
doc/source/configuration/index.rst View File

@@ -12,6 +12,7 @@ Details about Promenade-specific documents can be found here:
12 12
     :caption: Documents
13 13
 
14 14
     docker
15
+    encryption-policy
15 16
     genesis
16 17
     host-system
17 18
     kubelet

+ 14
- 0
examples/basic/EncryptionPolicy.yaml View File

@@ -0,0 +1,14 @@
1
+---
2
+schema: promenade/EncryptionPolicy/v1
3
+metadata:
4
+  schema: metadata/Document/v1
5
+  name: encryption-policy
6
+  layeringDefinition:
7
+    abstract: false
8
+    layer: site
9
+  storagePolicy: cleartext
10
+data:
11
+  scripts:
12
+    genesis:
13
+      gpg: {}
14
+...

+ 51
- 11
promenade/builder.py View File

@@ -1,4 +1,4 @@
1
-from . import logging, renderer
1
+from . import encryption_method, logging, renderer
2 2
 from beaker.cache import CacheManager
3 3
 from beaker.util import parse_cache_config_options
4 4
 
@@ -72,6 +72,14 @@ class Builder:
72 72
             _write_script(output_dir, 'validate-cluster.sh', validate_script)
73 73
 
74 74
     def build_genesis(self, *, output_dir):
75
+        script = self.build_genesis_script()
76
+        _write_script(output_dir, 'genesis.sh', script)
77
+
78
+        if self.validators:
79
+            validate_script = self._build_genesis_validate_script()
80
+            _write_script(output_dir, 'validate-genesis.sh', validate_script)
81
+
82
+    def build_genesis_script(self):
75 83
         LOG.info('Building genesis script')
76 84
         sub_config = self.config.extract_genesis_config()
77 85
         tarball = renderer.build_tarball_from_roles(
@@ -79,17 +87,23 @@ class Builder:
79 87
             roles=['common', 'genesis'],
80 88
             file_specs=self.file_cache.values())
81 89
 
82
-        script = renderer.render_template(
90
+        (encrypted_tarball, decrypt_setup_command, decrypt_command,
91
+         decrypt_teardown_command) = _encrypt_genesis(sub_config, tarball)
92
+
93
+        return renderer.render_template(
83 94
             sub_config,
84 95
             template='scripts/genesis.sh',
85
-            context={'tarball': tarball})
86
-
87
-        _write_script(output_dir, 'genesis.sh', script)
88
-
89
-        if self.validators:
90
-            validate_script = renderer.render_template(
91
-                sub_config, template='scripts/validate-genesis.sh')
92
-            _write_script(output_dir, 'validate-genesis.sh', validate_script)
96
+            context={
97
+                'decrypt_command': decrypt_command,
98
+                'decrypt_setup_command': decrypt_setup_command,
99
+                'decrypt_teardown_command': decrypt_teardown_command,
100
+                'encrypted_tarball': encrypted_tarball,
101
+            })
102
+
103
+    def _build_genesis_validate_script(self):
104
+        sub_config = self.config.extract_genesis_config()
105
+        return renderer.render_template(
106
+            sub_config, template='scripts/validate-genesis.sh')
93 107
 
94 108
     def build_node(self, node_document, *, output_dir):
95 109
         node_name = node_document['metadata']['name']
@@ -112,10 +126,18 @@ class Builder:
112 126
         tarball = renderer.build_tarball_from_roles(
113 127
             config=sub_config, roles=['common', 'join'], file_specs=file_specs)
114 128
 
129
+        (encrypted_tarball, decrypt_setup_command, decrypt_command,
130
+         decrypt_teardown_command) = _encrypt_node(sub_config, tarball)
131
+
115 132
         return renderer.render_template(
116 133
             sub_config,
117 134
             template='scripts/join.sh',
118
-            context={'tarball': tarball})
135
+            context={
136
+                'decrypt_command': decrypt_command,
137
+                'decrypt_setup_command': decrypt_setup_command,
138
+                'decrypt_teardown_command': decrypt_teardown_command,
139
+                'encrypted_tarball': encrypted_tarball,
140
+            })
119 141
 
120 142
     def _build_node_validate_script(self, node_name):
121 143
         sub_config = self.config.extract_node_config(node_name)
@@ -123,6 +145,24 @@ class Builder:
123 145
             sub_config, template='scripts/validate-join.sh')
124 146
 
125 147
 
148
+def _encrypt_genesis(config, data):
149
+    return _encrypt(config.get_path('EncryptionPolicy:scripts.genesis'), data)
150
+
151
+
152
+def _encrypt_node(config, data):
153
+    return _encrypt(config.get_path('EncryptionPolicy:scripts.join'), data)
154
+
155
+
156
+def _encrypt(cfg_dict, data):
157
+    method = encryption_method.EncryptionMethod.from_config(cfg_dict)
158
+    encrypted_data = method.encrypt(data)
159
+    decrypt_setup_command = method.get_decrypt_setup_command()
160
+    decrypt_command = method.get_decrypt_command()
161
+    decrypt_teardown_command = method.get_decrypt_teardown_command()
162
+    return (encrypted_data, decrypt_setup_command, decrypt_command,
163
+            decrypt_teardown_command)
164
+
165
+
126 166
 @CACHE.cache('fetch_tarball_content', expire=72 * 3600)
127 167
 def _fetch_tar_content(url, path):
128 168
     content = _fetch_tar_url(url)

+ 182
- 0
promenade/encryption_method.py View File

@@ -0,0 +1,182 @@
1
+from . import exceptions, logging
2
+import abc
3
+import os
4
+# Ignore bandit false positive: B404:blacklist
5
+# The purpose of this module is to safely encapsulate calls via fork.
6
+import subprocess  # nosec
7
+import tempfile
8
+
9
+__all__ = ['EncryptionMethod']
10
+
11
+LOG = logging.getLogger(__name__)
12
+
13
+
14
+class EncryptionMethod(metaclass=abc.ABCMeta):
15
+    @abc.abstractmethod
16
+    def encrypt(self, data):
17
+        pass
18
+
19
+    @abc.abstractmethod
20
+    def get_decrypt_setup_command(self):
21
+        pass
22
+
23
+    @abc.abstractmethod
24
+    def get_decrypt_command(self):
25
+        pass
26
+
27
+    @abc.abstractmethod
28
+    def get_decrypt_teardown_command(self):
29
+        pass
30
+
31
+    @staticmethod
32
+    def from_config(config):
33
+        LOG.debug('Building EncryptionMethod from: %s', config)
34
+        if config:
35
+            # NOTE(mark-burnett): Relying on the schema to ensure valid
36
+            # configuration.
37
+            name = list(config.keys())[0]
38
+            kwargs = config[name]
39
+            if name == 'gpg':
40
+                return GPGEncryptionMethod(**kwargs)
41
+            else:
42
+                raise NotImplementedError('Unknown Encryption method')
43
+        else:
44
+            return NullEncryptionMethod()
45
+
46
+    def notify_user(self, message):
47
+        print('=== BEGIN NOTICE ===')
48
+        print(message)
49
+        print('=== END NOTICE ===')
50
+
51
+
52
+class NullEncryptionMethod(EncryptionMethod):
53
+    def encrypt(self, data):
54
+        LOG.debug('Performing NOOP encryption')
55
+        return data
56
+
57
+    def get_decrypt_setup_command(self):
58
+        return ''
59
+
60
+    def get_decrypt_command(self):
61
+        return 'cat'
62
+
63
+    def get_decrypt_teardown_command(self):
64
+        return ''
65
+
66
+
67
+class GPGEncryptionMethod(EncryptionMethod):
68
+    ENCRYPTION_KEY_ENV_NAME = 'PROMENADE_ENCRYPTION_KEY'
69
+
70
+    def __init__(self, *args, **kwargs):
71
+        super().__init__(*args, **kwargs)
72
+        self._gpg_version = _detect_gpg_version()
73
+
74
+    def encrypt(self, data):
75
+        key = self._get_key()
76
+        return self._encrypt_data(key, data)
77
+
78
+    def get_decrypt_setup_command(self):
79
+        return '''
80
+        export DECRYPTION_KEY=${PROMENADE_ENCRYPTION_KEY:-"NONE"}
81
+        if [[ ${PROMENADE_ENCRYPTION_KEY} = "NONE" ]]; then
82
+            read -p "Script decryption key: " -s DECRYPTION_KEY
83
+        fi
84
+        '''
85
+
86
+    def get_decrypt_command(self):
87
+        return ('/usr/bin/gpg --verbose --decrypt '
88
+                '--passphrase "${DECRYPTION_KEY}"')
89
+
90
+    def get_decrypt_teardown_command(self):
91
+        return 'unset DECRYPTION_KEY'
92
+
93
+    def _get_key(self):
94
+        key = os.environ.get(self.ENCRYPTION_KEY_ENV_NAME)
95
+        if key is None:
96
+            key = _generate_key()
97
+            self.notify_user('Copy this decryption key for use during script '
98
+                             'execution:\n%s' % key)
99
+        else:
100
+            LOG.info('Using encryption key from %s',
101
+                     self.ENCRYPTION_KEY_ENV_NAME)
102
+
103
+        return key
104
+
105
+    def _encrypt_data(self, key, data):
106
+        with tempfile.TemporaryDirectory() as tmp:
107
+            # Ignore bandit false positive:
108
+            #   B603:subprocess_without_shell_equals_true
109
+            # Here user input is allowed to be arbitrary, as it's simply input
110
+            # to the specified encryption algorithm.  Regardless, we only put a
111
+            # tarball here.
112
+            p = subprocess.Popen(  # nosec
113
+                [
114
+                    '/usr/bin/gpg',
115
+                    '--verbose',
116
+                    '--symmetric',
117
+                    '--homedir',
118
+                    tmp,
119
+                    '--passphrase',
120
+                    key,
121
+                ] + self._gpg_encrypt_options(),
122
+                cwd=tmp,
123
+                stdin=subprocess.PIPE,
124
+                stdout=subprocess.PIPE,
125
+                stderr=subprocess.PIPE)
126
+
127
+            try:
128
+                out, err = p.communicate(data, timeout=120)
129
+            except subprocess.TimeoutExpired:
130
+                p.kill()
131
+                out, err = p.communicate()
132
+
133
+            if p.returncode != 0:
134
+                LOG.error('Got errors from gpg encrypt: %s', err)
135
+                raise exceptions.EncryptionException(description=str(err))
136
+
137
+            return out
138
+
139
+    def _gpg_encrypt_options(self):
140
+        options = {
141
+            1: [],
142
+            2: ['--pinentry-mode', 'loopback'],
143
+        }
144
+        return options[self._gpg_version[0]]
145
+
146
+
147
+DETECTION_PREFIX = 'gpg (GnuPG) '
148
+
149
+
150
+def _detect_gpg_version():
151
+    with tempfile.TemporaryDirectory() as tmp:
152
+        # Ignore bandit false positive:
153
+        #   B603:subprocess_without_shell_equals_true
154
+        # This method takes no input and simply queries the version of gpg.
155
+        output = subprocess.check_output(  # nosec
156
+            [
157
+                '/usr/bin/gpg',
158
+                '--version',
159
+            ], cwd=tmp)
160
+        lines = output.decode('utf-8').strip().splitlines()
161
+        if lines:
162
+            version = lines[0][len(DETECTION_PREFIX):]
163
+            LOG.debug('Found GPG version %s', version)
164
+            return tuple(map(int, version.split('.')[:2]))
165
+        else:
166
+            raise exceptions.GPGDetectionException()
167
+
168
+
169
+def _generate_key():
170
+    # Ignore bandit false positive:
171
+    #   B603:subprocess_without_shell_equals_true
172
+    # This method takes no input and generates random output.
173
+    result = subprocess.run(  # nosec
174
+        ['/usr/bin/openssl', 'rand', '-hex', '48'],
175
+        check=True,
176
+        env={
177
+            'RANDFILE': '/tmp/rnd',
178
+        },
179
+        stdout=subprocess.PIPE,
180
+    )
181
+
182
+    return result.stdout.decode().strip()

+ 10
- 0
promenade/exceptions.py View File

@@ -343,6 +343,16 @@ class NodeNotFoundException(KubernetesApiError):
343 343
     status = falcon.HTTP_404
344 344
 
345 345
 
346
+class EncryptionException(ApiError):
347
+    title = 'Payload encryption error'
348
+    status = falcon.HTTP_500
349
+
350
+
351
+class GPGDetectionException(ApiError):
352
+    title = 'Failed to detect GPG version'
353
+    status = falcon.HTTP_500
354
+
355
+
346 356
 def massage_error_list(error_list, placeholder_description):
347 357
     """
348 358
     Returns a best-effort attempt to make a nice error list

+ 12
- 9
promenade/generator.py View File

@@ -10,17 +10,16 @@ LOG = logging.getLogger(__name__)
10 10
 
11 11
 
12 12
 class Generator:
13
-    def __init__(self, config):
13
+    def __init__(self, config, block_strings=True):
14 14
         self.config = config
15
-        self.keys = pki.PKI()
16
-        self.documents = []
15
+        self.keys = pki.PKI(block_strings=block_strings)
17 16
         self.outputs = collections.defaultdict(dict)
18 17
 
19 18
     @property
20 19
     def cluster_domain(self):
21 20
         return self.config['KubernetesNetwork:dns.cluster_domain']
22 21
 
23
-    def generate(self, output_dir):
22
+    def generate(self, output_dir=None):
24 23
         for catalog in self.config.iterate(kind='PKICatalog'):
25 24
             for ca_name, ca_def in catalog['data'].get(
26 25
                     'certificate_authorities', {}).items():
@@ -40,7 +39,8 @@ class Generator:
40 39
                 document_name = keypair_def['name']
41 40
                 self.get_or_gen_keypair(document_name)
42 41
 
43
-        self._write(output_dir)
42
+        if output_dir:
43
+            self._write(output_dir)
44 44
 
45 45
     def get_or_gen_ca(self, document_name):
46 46
         kinds = [
@@ -126,18 +126,21 @@ class Generator:
126 126
         return result
127 127
 
128 128
     def _write(self, output_dir):
129
-        docs = list(
130
-            itertools.chain.from_iterable(
131
-                v.values() for v in self.outputs.values()))
129
+        documents = self.get_documents()
132 130
         with open(os.path.join(output_dir, 'certificates.yaml'), 'w') as f:
133 131
             # Don't use safe_dump_all so we can block format certificate data.
134 132
             yaml.dump_all(
135
-                docs,
133
+                documents,
136 134
                 stream=f,
137 135
                 default_flow_style=False,
138 136
                 explicit_start=True,
139 137
                 indent=2)
140 138
 
139
+    def get_documents(self):
140
+        return list(
141
+            itertools.chain.from_iterable(
142
+                v.values() for v in self.outputs.values()))
143
+
141 144
 
142 145
 def get_host_list(service_names):
143 146
     service_list = []

+ 14
- 5
promenade/pki.py View File

@@ -13,7 +13,8 @@ LOG = logging.getLogger(__name__)
13 13
 
14 14
 
15 15
 class PKI:
16
-    def __init__(self):
16
+    def __init__(self, *, block_strings=True):
17
+        self.block_strings = block_strings
17 18
         self._ca_config_string = None
18 19
 
19 20
     @property
@@ -116,9 +117,11 @@ class PKI:
116 117
             # Ignore bandit false positive:
117 118
             #   B603:subprocess_without_shell_equals_true
118 119
             # This method wraps cfssl calls originating from this module.
119
-            return json.loads(  # nosec
120
-                subprocess.check_output(
121
-                    ['cfssl'] + command, cwd=tmp, stderr=subprocess.PIPE))
120
+            result = subprocess.check_output(  # nosec
121
+                ['cfssl'] + command, cwd=tmp, stderr=subprocess.PIPE)
122
+            if not isinstance(result, str):
123
+                result = result.decode('utf-8')
124
+            return json.loads(result)
122 125
 
123 126
     def _openssl(self, command, *, files=None):
124 127
         if not files:
@@ -175,9 +178,15 @@ class PKI:
175 178
                 },
176 179
                 'storagePolicy': 'cleartext',
177 180
             },
178
-            'data': block_literal(data),
181
+            'data': self._block_literal(data),
179 182
         }
180 183
 
184
+    def _block_literal(self, data):
185
+        if self.block_strings:
186
+            return block_literal(data)
187
+        else:
188
+            return data
189
+
181 190
 
182 191
 class block_literal(str):
183 192
     pass

+ 33
- 0
promenade/schemas/EncryptionPolicy.yaml View File

@@ -0,0 +1,33 @@
1
+---
2
+schema: deckhand/DataSchema/v1
3
+metadata:
4
+  schema: metadata/Control/v1
5
+  name: promenade/EncryptionPolicy/v1
6
+  labels:
7
+    application: promenade
8
+data:
9
+  $schema: http://json-schema.org/schema#
10
+
11
+  definitions:
12
+    script_encryption:
13
+      oneof:
14
+        - { $ref: '#/definitions/encryption_method_gpg' }
15
+
16
+    encryption_method_gpg:
17
+      properties:
18
+        gpg:
19
+          type: object
20
+          additionalProperties: false
21
+      required:
22
+        - gpg
23
+      additionalProperties: false
24
+
25
+  properties:
26
+    scripts:
27
+      properties:
28
+        genesis:
29
+          $ref: '#/definitions/script_encryption'
30
+        join:
31
+          $ref: '#/definitions/script_encryption'
32
+      additionalProperties: false
33
+...

+ 4
- 1
promenade/templates/include/up.sh View File

@@ -10,7 +10,10 @@ chmod 700 /etc/kubernetes
10 10
 set +x
11 11
 log
12 12
 log === Extracting prepared files ===
13
-echo "{{ tarball | b64enc }}" | base64 -d | tar -zxv -C / | tee /etc/promenade-manifest
13
+{{ decrypt_setup_command }}
14
+echo "{{ encrypted_tarball | b64enc }}" | base64 -d | {{ decrypt_command }} | tar -zxv -C / | tee /etc/promenade-manifest
15
+{{ decrypt_teardown_command }}
16
+set -x
14 17
 
15 18
 # Adding apt repositories
16 19
 #

+ 1
- 1
promenade/validation.py View File

@@ -105,7 +105,7 @@ def check_schema(document, schemas=None):
105 105
         except jsonschema.ValidationError as e:
106 106
             raise exceptions.ValidationException(str(e))
107 107
     else:
108
-        LOG.warning('Skipping validation for unknown schema: %s', schema_name)
108
+        LOG.debug('Skipping validation for unknown schema: %s', schema_name)
109 109
 
110 110
 
111 111
 SCHEMAS = {}

+ 18
- 0
tests/unit/builder_data/simple/Docker.yaml View File

@@ -0,0 +1,18 @@
1
+---
2
+schema: promenade/Docker/v1
3
+metadata:
4
+  schema: metadata/Document/v1
5
+  name: docker
6
+  layeringDefinition:
7
+    abstract: false
8
+    layer: site
9
+  storagePolicy: cleartext
10
+data:
11
+  config:
12
+    insecure-registries:
13
+      - registry:5000
14
+    live-restore: true
15
+    max-concurrent-downloads: 10
16
+    oom-score-adjust: -999
17
+    storage-driver: overlay2
18
+...

+ 16
- 0
tests/unit/builder_data/simple/EncryptionPolicy.yaml View File

@@ -0,0 +1,16 @@
1
+---
2
+schema: promenade/EncryptionPolicy/v1
3
+metadata:
4
+  schema: metadata/Document/v1
5
+  name: testingpolicy
6
+  layeringDefinition:
7
+    abstract: false
8
+    layer: site
9
+  storagePolicy: cleartext
10
+data:
11
+  scripts:
12
+    genesis:
13
+      gpg: {}
14
+    join:
15
+      gpg: {}
16
+...

+ 45
- 0
tests/unit/builder_data/simple/Genesis.yaml View File

@@ -0,0 +1,45 @@
1
+---
2
+schema: promenade/Genesis/v1
3
+metadata:
4
+  schema: metadata/Document/v1
5
+  name: genesis
6
+  layeringDefinition:
7
+    abstract: false
8
+    layer: site
9
+  storagePolicy: cleartext
10
+data:
11
+  hostname: n0
12
+  ip: 192.168.77.10
13
+  apiserver:
14
+    command_prefix:
15
+      - /apiserver
16
+      - --authorization-mode=Node,RBAC
17
+      - --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota,DefaultTolerationSeconds
18
+      - --service-cluster-ip-range=10.96.0.0/16
19
+      - --endpoint-reconciler-type=lease
20
+  armada:
21
+    target_manifest: cluster-bootstrap
22
+  labels:
23
+    dynamic:
24
+      - calico-etcd=enabled
25
+      - coredns=enabled
26
+      - kubernetes-apiserver=enabled
27
+      - kubernetes-controller-manager=enabled
28
+      - kubernetes-etcd=enabled
29
+      - kubernetes-scheduler=enabled
30
+      - promenade-genesis=enabled
31
+      - ucp-control-plane=enabled
32
+  images:
33
+    armada: quay.io/airshipit/armada:master
34
+    helm:
35
+      tiller: gcr.io/kubernetes-helm/tiller:v2.9.1
36
+    kubernetes:
37
+      apiserver: gcr.io/google_containers/hyperkube-amd64:v1.10.2
38
+      controller-manager: gcr.io/google_containers/hyperkube-amd64:v1.10.2
39
+      etcd: quay.io/coreos/etcd:v3.2.14
40
+      scheduler: gcr.io/google_containers/hyperkube-amd64:v1.10.2
41
+  files:
42
+    - path: /var/lib/anchor/calico-etcd-bootstrap
43
+      content: "# placeholder for triggering calico etcd bootstrapping"
44
+      mode: 0644
45
+...

+ 86
- 0
tests/unit/builder_data/simple/HostSystem.yaml View File

@@ -0,0 +1,86 @@
1
+---
2
+schema: promenade/HostSystem/v1
3
+metadata:
4
+  schema: metadata/Document/v1
5
+  name: host-system
6
+  layeringDefinition:
7
+    abstract: false
8
+    layer: site
9
+  storagePolicy: cleartext
10
+data:
11
+  files:
12
+    # NOTE(mark-burnett): A kubelet would be required for a real deployment
13
+    # (either here or via debian package; however, these unit tests don't
14
+    # attempt to actually run Kubernetes, only to construct the genesis and
15
+    # join scripts.
16
+    # - path: /opt/kubernetes/bin/kubelet
17
+    #   tar_url: https://dl.k8s.io/v1.10.2/kubernetes-node-linux-amd64.tar.gz
18
+    #   tar_path: kubernetes/node/bin/kubelet
19
+    #   mode: 0555
20
+    - path: /etc/logrotate.d/json-logrotate
21
+      mode: 0444
22
+      content: |-
23
+        /var/lib/docker/containers/*/*-json.log
24
+        {
25
+            compress
26
+            copytruncate
27
+            create 0644 root root
28
+            daily
29
+            dateext
30
+            dateformat -%Y%m%d-%s
31
+            maxsize 10M
32
+            missingok
33
+            notifempty
34
+            su root root
35
+            rotate 1
36
+        }
37
+  images:
38
+    haproxy: haproxy:1.8.3
39
+    helm:
40
+      helm: lachlanevenson/k8s-helm:v2.9.1
41
+    kubernetes:
42
+      kubectl: gcr.io/google_containers/hyperkube-amd64:v1.10.2
43
+  packages:
44
+    repositories:
45
+      - deb http://apt.dockerproject.org/repo ubuntu-xenial main
46
+    keys:
47
+      - |-
48
+        -----BEGIN PGP PUBLIC KEY BLOCK-----
49
+
50
+        mQINBFWln24BEADrBl5p99uKh8+rpvqJ48u4eTtjeXAWbslJotmC/CakbNSqOb9o
51
+        ddfzRvGVeJVERt/Q/mlvEqgnyTQy+e6oEYN2Y2kqXceUhXagThnqCoxcEJ3+KM4R
52
+        mYdoe/BJ/J/6rHOjq7Omk24z2qB3RU1uAv57iY5VGw5p45uZB4C4pNNsBJXoCvPn
53
+        TGAs/7IrekFZDDgVraPx/hdiwopQ8NltSfZCyu/jPpWFK28TR8yfVlzYFwibj5WK
54
+        dHM7ZTqlA1tHIG+agyPf3Rae0jPMsHR6q+arXVwMccyOi+ULU0z8mHUJ3iEMIrpT
55
+        X+80KaN/ZjibfsBOCjcfiJSB/acn4nxQQgNZigna32velafhQivsNREFeJpzENiG
56
+        HOoyC6qVeOgKrRiKxzymj0FIMLru/iFF5pSWcBQB7PYlt8J0G80lAcPr6VCiN+4c
57
+        NKv03SdvA69dCOj79PuO9IIvQsJXsSq96HB+TeEmmL+xSdpGtGdCJHHM1fDeCqkZ
58
+        hT+RtBGQL2SEdWjxbF43oQopocT8cHvyX6Zaltn0svoGs+wX3Z/H6/8P5anog43U
59
+        65c0A+64Jj00rNDr8j31izhtQMRo892kGeQAaaxg4Pz6HnS7hRC+cOMHUU4HA7iM
60
+        zHrouAdYeTZeZEQOA7SxtCME9ZnGwe2grxPXh/U/80WJGkzLFNcTKdv+rwARAQAB
61
+        tDdEb2NrZXIgUmVsZWFzZSBUb29sIChyZWxlYXNlZG9ja2VyKSA8ZG9ja2VyQGRv
62
+        Y2tlci5jb20+iQI4BBMBAgAiBQJVpZ9uAhsvBgsJCAcDAgYVCAIJCgsEFgIDAQIe
63
+        AQIXgAAKCRD3YiFXLFJgnbRfEAC9Uai7Rv20QIDlDogRzd+Vebg4ahyoUdj0CH+n
64
+        Ak40RIoq6G26u1e+sdgjpCa8jF6vrx+smpgd1HeJdmpahUX0XN3X9f9qU9oj9A4I
65
+        1WDalRWJh+tP5WNv2ySy6AwcP9QnjuBMRTnTK27pk1sEMg9oJHK5p+ts8hlSC4Sl
66
+        uyMKH5NMVy9c+A9yqq9NF6M6d6/ehKfBFFLG9BX+XLBATvf1ZemGVHQusCQebTGv
67
+        0C0V9yqtdPdRWVIEhHxyNHATaVYOafTj/EF0lDxLl6zDT6trRV5n9F1VCEh4Aal8
68
+        L5MxVPcIZVO7NHT2EkQgn8CvWjV3oKl2GopZF8V4XdJRl90U/WDv/6cmfI08GkzD
69
+        YBHhS8ULWRFwGKobsSTyIvnbk4NtKdnTGyTJCQ8+6i52s+C54PiNgfj2ieNn6oOR
70
+        7d+bNCcG1CdOYY+ZXVOcsjl73UYvtJrO0Rl/NpYERkZ5d/tzw4jZ6FCXgggA/Zxc
71
+        jk6Y1ZvIm8Mt8wLRFH9Nww+FVsCtaCXJLP8DlJLASMD9rl5QS9Ku3u7ZNrr5HWXP
72
+        HXITX660jglyshch6CWeiUATqjIAzkEQom/kEnOrvJAtkypRJ59vYQOedZ1sFVEL
73
+        MXg2UCkD/FwojfnVtjzYaTCeGwFQeqzHmM241iuOmBYPeyTY5veF49aBJA1gEJOQ
74
+        TvBR8Q==
75
+        =Fm3p
76
+        -----END PGP PUBLIC KEY BLOCK-----
77
+    additional:
78
+      - curl
79
+      - jq
80
+    required:
81
+      docker: docker-engine=1.13.1-0~ubuntu-xenial
82
+      socat: socat=1.7.3.1-1
83
+  validation:
84
+    pod_logs:
85
+      image: busybox:1.28.3
86
+...

+ 21
- 0
tests/unit/builder_data/simple/Kubelet.yaml View File

@@ -0,0 +1,21 @@
1
+---
2
+schema: promenade/Kubelet/v1
3
+metadata:
4
+  schema: metadata/Document/v1
5
+  name: kubelet
6
+  layeringDefinition:
7
+    abstract: false
8
+    layer: site
9
+  storagePolicy: cleartext
10
+data:
11
+  arguments:
12
+    - --cni-bin-dir=/opt/cni/bin
13
+    - --cni-conf-dir=/etc/cni/net.d
14
+    - --eviction-max-pod-grace-period=-1
15
+    - --network-plugin=cni
16
+    - --node-status-update-frequency=5s
17
+    - --serialize-image-pulls=false
18
+    - --v=5
19
+  images:
20
+    pause: gcr.io/google_containers/pause-amd64:3.0
21
+...

+ 43
- 0
tests/unit/builder_data/simple/KubernetesNetwork.yaml View File

@@ -0,0 +1,43 @@
1
+---
2
+schema: promenade/KubernetesNetwork/v1
3
+metadata:
4
+  schema: metadata/Document/v1
5
+  name: kubernetes-network
6
+  layeringDefinition:
7
+    abstract: false
8
+    layer: site
9
+  storagePolicy: cleartext
10
+data:
11
+  dns:
12
+    cluster_domain: cluster.local
13
+    service_ip: 10.96.0.10
14
+    bootstrap_validation_checks:
15
+      - calico-etcd.kube-system.svc.cluster.local
16
+      - google.com
17
+      - kubernetes-etcd.kube-system.svc.cluster.local
18
+      - kubernetes.default.svc.cluster.local
19
+    upstream_servers:
20
+      - 8.8.8.8
21
+      - 8.8.4.4
22
+
23
+  kubernetes:
24
+    apiserver_port: 6443
25
+    haproxy_port: 6553
26
+    pod_cidr: 10.97.0.0/16
27
+    service_cidr: 10.96.0.0/16
28
+    service_ip: 10.96.0.1
29
+
30
+  etcd:
31
+    container_port: 2379
32
+    haproxy_port: 2378
33
+
34
+  hosts_entries:
35
+    - ip: 192.168.77.1
36
+      names:
37
+        - registry
38
+
39
+#  proxy:
40
+#    url: http://proxy.example.com:8080
41
+#    additional_no_proxy:
42
+#      - 10.0.1.1
43
+...

+ 31
- 0
tests/unit/builder_data/simple/KubernetesNode.yaml View File

@@ -0,0 +1,31 @@
1
+---
2
+schema: promenade/KubernetesNode/v1
3
+metadata:
4
+  schema: metadata/Document/v1
5
+  name: n1
6
+  layeringDefinition:
7
+    abstract: false
8
+    layer: site
9
+  storagePolicy: cleartext
10
+data:
11
+  hostname: n1
12
+  ip: 192.169.77.11
13
+  join_ip: 192.169.77.10
14
+  labels:
15
+    dynamic:
16
+      - calico-etcd=enabled
17
+      - ceph-mds=enabled
18
+      - ceph-mon=enabled
19
+      - ceph-osd=enabled
20
+      - ceph-rgw=enabled
21
+      - ceph-mgr=enabled
22
+      - coredns=enabled
23
+      - kubernetes-apiserver=enabled
24
+      - kubernetes-controller-manager=enabled
25
+      - kubernetes-etcd=enabled
26
+      - kubernetes-scheduler=enabled
27
+      - openstack-compute-node=enabled
28
+      - openstack-control-plane=enabled
29
+      - openvswitch=enabled
30
+      - ucp-control-plane=enabled
31
+...

+ 11
- 0
tests/unit/builder_data/simple/LayeringPolicy.yaml View File

@@ -0,0 +1,11 @@
1
+---
2
+schema: deckhand/LayeringPolicy/v1
3
+metadata:
4
+  schema: metadata/Control/v1
5
+  name: layering-policy
6
+data:
7
+  layerOrder:
8
+    - global
9
+    - type
10
+    - site
11
+...

+ 167
- 0
tests/unit/builder_data/simple/PKICatalog.yaml View File

@@ -0,0 +1,167 @@
1
+---
2
+schema: promenade/PKICatalog/v1
3
+metadata:
4
+  schema: metadata/Document/v1
5
+  name: cluster-certificates
6
+  layeringDefinition:
7
+    abstract: false
8
+    layer: site
9
+  storagePolicy: cleartext
10
+data:
11
+  certificate_authorities:
12
+    kubernetes:
13
+      description: CA for Kubernetes components
14
+      certificates:
15
+        - document_name: apiserver
16
+          description: Service certificate for Kubernetes apiserver
17
+          common_name: apiserver
18
+          hosts:
19
+            - localhost
20
+            - 127.0.0.1
21
+            - 10.96.0.1
22
+          kubernetes_service_names:
23
+            - kubernetes.default.svc.cluster.local
24
+        - document_name: kubelet-genesis
25
+          common_name: system:node:n0
26
+          hosts:
27
+            - n0
28
+            - 192.168.77.10
29
+          groups:
30
+            - system:nodes
31
+        - document_name: kubelet-n0
32
+          common_name: system:node:n0
33
+          hosts:
34
+            - n0
35
+            - 192.168.77.10
36
+          groups:
37
+            - system:nodes
38
+        - document_name: kubelet-n1
39
+          common_name: system:node:n1
40
+          hosts:
41
+            - n1
42
+            - 192.168.77.11
43
+          groups:
44
+            - system:nodes
45
+        - document_name: scheduler
46
+          description: Service certificate for Kubernetes scheduler
47
+          common_name: system:kube-scheduler
48
+        - document_name: controller-manager
49
+          description: certificate for controller-manager
50
+          common_name: system:kube-controller-manager
51
+        - document_name: admin
52
+          common_name: admin
53
+          groups:
54
+            - system:masters
55
+        - document_name: armada
56
+          common_name: armada
57
+          groups:
58
+            - system:masters
59
+    kubernetes-etcd:
60
+      description: Certificates for Kubernetes's etcd servers
61
+      certificates:
62
+        - document_name: apiserver-etcd
63
+          description: etcd client certificate for use by Kubernetes apiserver
64
+          common_name: apiserver
65
+          # NOTE(mark-burnett): hosts not required for client certificates
66
+        - document_name: kubernetes-etcd-anchor
67
+          description: anchor
68
+          common_name: anchor
69
+        - document_name: kubernetes-etcd-genesis
70
+          common_name: kubernetes-etcd-genesis
71
+          hosts:
72
+            - n0
73
+            - 192.168.77.10
74
+            - 127.0.0.1
75
+            - localhost
76
+            - kubernetes-etcd.kube-system.svc.cluster.local
77
+        - document_name: kubernetes-etcd-n0
78
+          common_name: kubernetes-etcd-n0
79
+          hosts:
80
+            - n0
81
+            - 192.168.77.10
82
+            - 127.0.0.1
83
+            - localhost
84
+            - kubernetes-etcd.kube-system.svc.cluster.local
85
+        - document_name: kubernetes-etcd-n1
86
+          common_name: kubernetes-etcd-n1
87
+          hosts:
88
+            - n1
89
+            - 192.168.77.11
90
+            - 127.0.0.1
91
+            - localhost
92
+            - kubernetes-etcd.kube-system.svc.cluster.local
93
+    kubernetes-etcd-peer:
94
+      certificates:
95
+        - document_name: kubernetes-etcd-genesis-peer
96
+          common_name: kubernetes-etcd-genesis-peer
97
+          hosts:
98
+            - n0
99
+            - 192.168.77.10
100
+            - 127.0.0.1
101
+            - localhost
102
+            - kubernetes-etcd.kube-system.svc.cluster.local
103
+        - document_name: kubernetes-etcd-n0-peer
104
+          common_name: kubernetes-etcd-n0-peer
105
+          hosts:
106
+            - n0
107
+            - 192.168.77.10
108
+            - 127.0.0.1
109
+            - localhost
110
+            - kubernetes-etcd.kube-system.svc.cluster.local
111
+        - document_name: kubernetes-etcd-n1-peer
112
+          common_name: kubernetes-etcd-n1-peer
113
+          hosts:
114
+            - n1
115
+            - 192.168.77.11
116
+            - 127.0.0.1
117
+            - localhost
118
+            - kubernetes-etcd.kube-system.svc.cluster.local
119
+    calico-etcd:
120
+      description: Certificates for Calico etcd client traffic
121
+      certificates:
122
+        - document_name: calico-etcd-anchor
123
+          description: anchor
124
+          common_name: anchor
125
+        - document_name: calico-etcd-n0
126
+          common_name: calico-etcd-n0
127
+          hosts:
128
+            - n0
129
+            - 192.168.77.10
130
+            - 127.0.0.1
131
+            - localhost
132
+            - 10.96.232.136
133
+        - document_name: calico-etcd-n1
134
+          common_name: calico-etcd-n1
135
+          hosts:
136
+            - n1
137
+            - 192.168.77.11
138
+            - 127.0.0.1
139
+            - localhost
140
+            - 10.96.232.136
141
+        - document_name: calico-node
142
+          common_name: calcico-node
143
+    calico-etcd-peer:
144
+      description: Certificates for Calico etcd clients
145
+      certificates:
146
+        - document_name: calico-etcd-n0-peer
147
+          common_name: calico-etcd-n0-peer
148
+          hosts:
149
+            - n0
150
+            - 192.168.77.10
151
+            - 127.0.0.1
152
+            - localhost
153
+            - 10.96.232.136
154
+        - document_name: calico-etcd-n1-peer
155
+          common_name: calico-etcd-n1-peer
156
+          hosts:
157
+            - n1
158
+            - 192.168.77.11
159
+            - 127.0.0.1
160
+            - localhost
161
+            - 10.96.232.136
162
+        - document_name: calico-node-peer
163
+          common_name: calcico-node-peer
164
+  keypairs:
165
+    - name: service-account
166
+      description: Service account signing key for use by Kubernetes controller-manager.
167
+...

+ 1061
- 0
tests/unit/builder_data/simple/armada-resources.yaml
File diff suppressed because it is too large
View File


+ 57
- 0
tests/unit/test_builder.py View File

@@ -0,0 +1,57 @@
1
+# Copyright 2018 AT&T Intellectual Property.  All other rights reserved.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#    http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+
15
+from promenade import builder, generator, config, encryption_method
16
+import copy
17
+import os
18
+import pytest
19
+
20
+
21
+def load_full_config(dirname):
22
+    this_dir = os.path.dirname(os.path.realpath(__file__))
23
+    search_dir = os.path.join(this_dir, 'builder_data', dirname)
24
+    streams = []
25
+    for filename in os.listdir(search_dir):
26
+        stream = open(os.path.join(search_dir, filename))
27
+        streams.append(stream)
28
+
29
+    raw_config = config.Configuration.from_streams(
30
+        allow_missing_substitutions=True,
31
+        debug=True,
32
+        streams=streams,
33
+        substitute=True,
34
+        validate=False,
35
+    )
36
+    g = generator.Generator(raw_config, block_strings=False)
37
+    g.generate()
38
+
39
+    documents = copy.deepcopy(raw_config.documents)
40
+    documents.extend(copy.deepcopy(g.get_documents()))
41
+
42
+    return config.Configuration(
43
+        allow_missing_substitutions=False,
44
+        debug=True,
45
+        documents=documents,
46
+        substitute=True,
47
+        validate=True,
48
+    )
49
+
50
+
51
+def test_build_simple():
52
+    b = builder.Builder(load_full_config('simple'))
53
+    genesis_script = b.build_genesis_script()
54
+    assert len(genesis_script) > 0
55
+
56
+    n1_join_script = b.build_node_script('n1')
57
+    assert len(n1_join_script) > 0

+ 1
- 0
tools/g2/lib/config.sh View File

@@ -6,6 +6,7 @@ export NGINX_DIR="${TEMP_DIR}/nginx"
6 6
 export NGINX_URL="http://192.168.77.1:7777"
7 7
 export PROMENADE_BASE_URL="http://promenade-api.ucp.svc.cluster.local"
8 8
 export PROMENADE_DEBUG=${PROMENADE_DEBUG:-0}
9
+export PROMENADE_ENCRYPTION_KEY=${PROMENADE_ENCRYPTION_KEY:-testkey}
9 10
 export REGISTRY_DATA_DIR=${REGISTRY_DATA_DIR:-/mnt/registry}
10 11
 export VIRSH_POOL=${VIRSH_POOL:-promenade}
11 12
 export VIRSH_POOL_PATH=${VIRSH_POOL_PATH:-/var/lib/libvirt/promenade}

+ 1
- 0
tools/g2/stages/build-scripts.sh View File

@@ -13,6 +13,7 @@ docker run --rm -t \
13 13
     -w /target \
14 14
     -v "${TEMP_DIR}:/target" \
15 15
     -e "PROMENADE_DEBUG=${PROMENADE_DEBUG}" \
16
+    -e "PROMENADE_ENCRYPTION_KEY=${PROMENADE_ENCRYPTION_KEY}" \
16 17
     "${IMAGE_PROMENADE}" \
17 18
         promenade \
18 19
             build-all \

+ 1
- 1
tools/g2/stages/genesis.sh View File

@@ -7,7 +7,7 @@ source "${GATE_UTILS}"
7 7
 rsync_cmd "${TEMP_DIR}/scripts"/*genesis* "${GENESIS_NAME}:/root/promenade/"
8 8
 
9 9
 set -o pipefail
10
-ssh_cmd "${GENESIS_NAME}" /root/promenade/genesis.sh 2>&1 | tee -a "${LOG_FILE}"
10
+ssh_cmd "${GENESIS_NAME}" env "PROMENADE_ENCRYPTION_KEY=${PROMENADE_ENCRYPTION_KEY}" /root/promenade/genesis.sh 2>&1 | tee -a "${LOG_FILE}"
11 11
 ssh_cmd "${GENESIS_NAME}" /root/promenade/validate-genesis.sh 2>&1 | tee -a "${LOG_FILE}"
12 12
 set +o pipefail
13 13
 

+ 16
- 0
tools/install-external-deps.sh View File

@@ -0,0 +1,16 @@
1
+#!/bin/bash
2
+# Installs external dependencies required for basic testing
3
+
4
+set -ex
5
+
6
+CFSSL_URL=${CFSSL_URL:-https://pkg.cfssl.org/R1.2/cfssl_linux-amd64}
7
+
8
+if [[ ! $(which cfssl) ]]; then
9
+    TMP_DIR=$(mktemp -d)
10
+    pushd "${TMP_DIR}"
11
+    curl -Lo cfssl "${CFSSL_URL}"
12
+    chmod 755 cfssl
13
+    sudo mv cfssl /usr/local/bin/
14
+    popd
15
+    rm -rf "${TMP_DIR}"
16
+fi

+ 1
- 1
tools/lint_gate.sh View File

@@ -12,7 +12,7 @@ done
12 12
 
13 13
 if [[ -x $(which shellcheck) ]]; then
14 14
     echo Checking shell scripts..
15
-    shellcheck -s bash -e SC2029 "${WORKSPACE}"/tools/cleanup.sh "${WORKSPACE}"/tools/*gate*.sh "${WORKSPACE}"/tools/g2/stages/* "${WORKSPACE}"/tools/g2/lib/*
15
+    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
16 16
 else
17 17
     echo No shellcheck executable found.  Please, install it.
18 18
     exit 1

Loading…
Cancel
Save