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:
parent
9fddedad13
commit
80447e7411
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue