Secret support

Support of k8s secrets is introduced. To create a secret, put
an additional section 'secrets' to the definition
of the service:

secrets:
  name-for-reference:
    type: "Opaque"
    data:
      "file1": "some content"
      "file2": "another one"
    secret:
        secretName: name-in-k8s
    path: /where/to/mount

You can reference to this secret from the container definition:

daemon:
  secrets:
    - name-for-reference

The referenced secret must be defined in the 'secrets' section.

Change-Id: Iaaede4ccb94c99d70f3ecad040d5ab6c41428c5e
Partial-Bug: #1651392
Partial-Bug: #1651394
This commit is contained in:
Dmitry Klenov 2017-01-20 09:42:08 +00:00
parent 9fddedad13
commit 80447e7411
7 changed files with 205 additions and 21 deletions

View File

@ -17,4 +17,4 @@ import pbr.version
version_info = pbr.version.VersionInfo("fuel_ccp")
__version__ = version_info.version_string()
dsl_version = "0.5.0"
dsl_version = "0.6.0"

View File

@ -25,10 +25,10 @@ YAML_FILE_RE = re.compile(r'\.yaml$')
JOBS_ROLE = '_ccp_jobs'
def _expand_files(service, files):
def _expand_items(service, kind, items):
def _expand(cmd):
if cmd.get("files"):
cmd["files"] = {f: files[f] for f in cmd["files"]}
if cmd.get(kind):
cmd[kind] = {f: items[f] for f in cmd[kind]}
for cont in service["containers"]:
_expand(cont["daemon"])
@ -79,6 +79,15 @@ def serialize_workflows(workflows):
workflows[k] = json.dumps(v, sort_keys=True)
def _process_secrets(secrets):
if secrets:
for secret in six.itervalues(secrets):
type = secret.get("type", "Opaque")
data = secret.get("data", {})
yield templates.serialize_secret(secret["secret"]["secretName"],
type, data)
def parse_role(component, topology, configmaps):
service_dir = component["service_dir"]
role = component["service_content"]
@ -90,13 +99,18 @@ def parse_role(component, topology, configmaps):
raise ValueError('The %s is not defined in topology.' % service_name)
LOG.info("Scheduling service %s deployment", service_name)
files = role.get("files")
for kind in ["files", "secrets"]:
_expand_items(service, kind, role.get(kind))
files_header = service['exports_ctx']['files_header']
_expand_files(service, files)
files = role.get("files")
process_files(files, service_dir)
files_cm = _create_files_configmap(service_name, files, files_header)
meta_cm = _create_meta_configmap(service)
yield _process_secrets(role.get("secrets"))
workflows = _parse_workflows(service)
serialize_workflows(workflows)
workflow_cm = _create_workflow(workflows, service_name)

View File

@ -1,5 +1,7 @@
import base64
import itertools
import json
import six
from fuel_ccp import config
from fuel_ccp.config import images
@ -85,6 +87,14 @@ def serialize_volume_mounts(container, for_job=None):
"mountPath": v.get("mount-path", v["path"]),
"readOnly": v.get("readOnly", False)
})
if "daemon" in container:
for (name, secret) in six.iteritems(
container["daemon"].get("secrets", {})):
spec.append({
"name": name,
"mountPath": secret["path"]
})
return spec
@ -302,6 +312,20 @@ def serialize_volumes(service, for_job=None):
raise ValueError("Volume type \"%s\" not supported" %
v["type"])
volume_names.append(v["name"])
for cont in service["containers"]:
if "daemon" in cont:
for (name, secret) in six.iteritems(
cont["daemon"].get("secrets", {})):
if name in volume_names:
# TODO(dklenov): move to validation
continue
vol_spec.append({
"name": name,
"secret": secret["secret"]
})
volume_names.append(name)
return vol_spec
@ -490,3 +514,19 @@ def serialize_ingress(name, rules):
"rules": rules
}
}
def serialize_secret(name, type="Opaque", data={}):
data = dict(
[(key, base64.b64encode(value.encode()).decode())
for (key, value) in six.iteritems(data)]
)
return {
"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"name": name
},
"type": type,
"data": data
}

View File

@ -29,7 +29,7 @@ class TestDeploy(base.TestCase):
deploy._fill_cmd(workflow, cmd)
self.assertDictEqual({"command": "ps"}, workflow)
def test_expand_files(self):
def test_expand_items(self):
service = {
"containers": [{
"daemon": {
@ -58,7 +58,7 @@ class TestDeploy(base.TestCase):
"content": "bolik"
}
}
deploy._expand_files(service, files)
deploy._expand_items(service, "files", files)
expected = {
"containers": [{
"daemon": {

View File

@ -1,3 +1,6 @@
import base64
import six
from fuel_ccp import templates
from fuel_ccp.tests import base
@ -23,6 +26,9 @@ class TestDeploy(base.TestCase):
"command": "true",
"type": "exec"
}
},
"daemon": {
"command": "run.sh"
}
}
container_spec = templates.serialize_daemon_container_spec(container)
@ -133,3 +139,22 @@ class TestDeploy(base.TestCase):
}
probe_spec = templates.serialize_liveness_probe(probe_definition)
self.assertDictEqual(expected, probe_spec)
def test_serialize_secret(self):
name = "the-most-secret"
data = {"a": "value1", "b": " ./?+{}()[]|\\\'\""}
expected = {
"apiVersion": "v1",
"data": data,
"kind": "Secret",
"metadata": {
"name": name
},
"type": "Opaque"
}
serialized = templates.serialize_secret(name, data=data)
serialized["data"] = dict(
[(key, base64.b64decode(value).decode())
for (key, value) in six.iteritems(serialized["data"])]
)
self.assertDictEqual(expected, serialized)

View File

@ -1,4 +1,6 @@
import copy
import fixtures
import jsonschema
import mock
import testscenarios
@ -116,3 +118,58 @@ class TestServiceValidation(testscenarios.WithScenarios, base.TestCase):
service_validation.validate_service_versions(
components_map, ['test']
)
class TestSchemaValidation(base.TestCase):
def test_secret_permissions_validation(self):
correct_permissions = ["0400", "0777", "0001"]
for perm in correct_permissions:
jsonschema.validate(perm, service_validation.PERMISSION_SCHEMA)
incorrect_permissions = ["123", "0778", "1400"]
for perm in incorrect_permissions:
self.assertRaisesRegexp(
jsonschema.exceptions.ValidationError,
"'" + perm + "' does not match.*",
jsonschema.validate,
perm, service_validation.PERMISSION_SCHEMA)
def test_secret_definition_validation(self):
incorrect_secret = {
"path": "/etc/keystone/fernet-keys",
"secret": {
}
}
self.assertRaisesRegexp(
jsonschema.exceptions.ValidationError,
"'secretName' is a required property.*",
jsonschema.validate,
incorrect_secret, service_validation.SECRET_SCHEMA)
minimal_correct_secret = copy.deepcopy(incorrect_secret)
minimal_correct_secret["secret"].update({"secretName": "fernet"})
jsonschema.validate(minimal_correct_secret,
service_validation.SECRET_SCHEMA)
correct_secret = copy.deepcopy(minimal_correct_secret)
correct_secret["secret"] = {
"secretName": "fernet",
"defaultMode": "0777",
"items": [
{
"key": "username",
"path": "/specific/username/path",
"mode": "0777"
},
{
"key": "password",
"path": "/specific/password/path",
"mode": "0400"
}
]
}
correct_secret["data"] = {
"item1": "value 1",
"2": "/path/file.ext"
}
jsonschema.validate(correct_secret, service_validation.SECRET_SCHEMA)

View File

@ -11,6 +11,8 @@ LOG = logging.getLogger(__name__)
PATH_RE = r'^(/|((/[\w.-]+)+/?))$'
FILE_PATH_RE = r'^(/|((/[\w.-]+)+))$'
SECRET_PERMISSIONS_RE = r'^(0[0-7]{3})$'
NOT_EMPTY_STRING_RE = r"^\s*\S.*$"
class ServiceFormatChecker(jsonschema.FormatChecker):
@ -24,7 +26,7 @@ class ServiceFormatChecker(jsonschema.FormatChecker):
NOT_EMPTY_STRING_SCHEMA = {
"type": "string",
"pattern": r"^\s*\S.*$"
"pattern": NOT_EMPTY_STRING_RE
}
NOT_EMPTY_STRING_ARRAY_SCHEMA = {
@ -34,6 +36,16 @@ NOT_EMPTY_STRING_ARRAY_SCHEMA = {
"items": NOT_EMPTY_STRING_SCHEMA
}
PERMISSION_SCHEMA = {
"type": "string",
"pattern": SECRET_PERMISSIONS_RE
}
PATH_SCHEMA = {
"type": "string",
"pattern": PATH_RE
}
LOCAL_COMMAND_SCHEMA = {
"type": "object",
"additionalProperties": False,
@ -47,6 +59,7 @@ LOCAL_COMMAND_SCHEMA = {
"enum": ["local"]
},
"files": NOT_EMPTY_STRING_ARRAY_SCHEMA,
"secrets": NOT_EMPTY_STRING_ARRAY_SCHEMA,
"user": NOT_EMPTY_STRING_SCHEMA
}
}
@ -81,14 +94,8 @@ EMPTY_DIR_VOLUME_SCHEMA = {
"type": {
"enum": ["empty-dir"]
},
"path": {
"type": "string",
"pattern": PATH_RE
},
"mount-path": {
"type": "string",
"pattern": PATH_RE
},
"path": PATH_SCHEMA,
"mount-path": PATH_SCHEMA,
"readOnly": {
"type": "boolean"
}
@ -158,6 +165,44 @@ PROBE_SCHEMA = {
]
}
SECRET_SCHEMA = {
"type": "object",
"additionalProperties": False,
"required": ["secret", "path"],
"properties": {
"type": NOT_EMPTY_STRING_SCHEMA,
"data": {
"type": "object",
"patternProperties": {
NOT_EMPTY_STRING_RE: NOT_EMPTY_STRING_SCHEMA
}
},
"secret": {
"type": "object",
"additionalProperties": False,
"required": ["secretName"],
"properties": {
"secretName": NOT_EMPTY_STRING_SCHEMA,
"defaultMode": PERMISSION_SCHEMA,
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": False,
"required": ["key", "path"],
"properties": {
"key": NOT_EMPTY_STRING_SCHEMA,
"path": PATH_SCHEMA,
"mode": PERMISSION_SCHEMA
}
}
}
}
},
"path": PATH_SCHEMA
}
}
SERVICE_SCHEMA = {
"type": "object",
@ -288,10 +333,7 @@ SERVICE_SCHEMA = {
"required": ["path", "content"],
"properties": {
"path": {
"type": "string",
"pattern": FILE_PATH_RE
},
"path": PATH_SCHEMA,
"content": NOT_EMPTY_STRING_SCHEMA,
"perm": {
"type": "string",
@ -301,6 +343,12 @@ SERVICE_SCHEMA = {
}
}
}
},
"secrets": {
"type": "object",
"patternProperties": {
NOT_EMPTY_STRING_RE: SECRET_SCHEMA
}
}
}
}