From: David Garcia Date: Thu, 27 Aug 2020 14:53:44 +0000 (+0200) Subject: Add keystone charm and interface X-Git-Tag: release-v9.0-start~66 X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2Fdevops.git;a=commitdiff_plain;h=009a5d691dba1ec6aa8567bc27eb4d468e5e0db4 Add keystone charm and interface - Bundles updated - Installer updated Change-Id: I0f8e9aafd51e9579159f9166864eb8634292f99c Signed-off-by: David Garcia --- diff --git a/installers/charm/build.sh b/installers/charm/build.sh index 17eea948..5d1c868a 100755 --- a/installers/charm/build.sh +++ b/installers/charm/build.sh @@ -23,3 +23,6 @@ build 'nbi-k8s' build 'pol-k8s' build 'ro-k8s' build 'ui-k8s' +build 'keystone' +build 'ng-ui' +build 'pla' \ No newline at end of file diff --git a/installers/charm/bundles/osm-ha/bundle.yaml b/installers/charm/bundles/osm-ha/bundle.yaml index ea3d7a14..8928028b 100644 --- a/installers/charm/bundles/osm-ha/bundle.yaml +++ b/installers/charm/bundles/osm-ha/bundle.yaml @@ -91,6 +91,7 @@ applications: options: log_level: "INFO" DATABASE_COMMONKEY: osm + auth-backend: keystone annotations: gui-x: 0 gui-y: -200 @@ -214,6 +215,14 @@ applications: annotations: gui-x: 250 gui-y: 550 + keystone: + charm: '%(prefix)s/keystone%(suffix)s' + channel: '%(channel)s' + scale: 1 + series: kubernetes + annotations: + gui-x: -250 + gui-y: 550 relations: - - "kafka-k8s:zookeeper" @@ -254,3 +263,7 @@ relations: - "mongodb-k8s:mongo" - - 'ng-ui:nbi' - 'nbi-k8s:nbi' + - - 'keystone:db' + - 'mariadb-k8s:mysql' + - - 'keystone:keystone' + - 'nbi-k8s:keystone' diff --git a/installers/charm/bundles/osm/bundle.yaml b/installers/charm/bundles/osm/bundle.yaml index c4567aca..a3ac4085 100644 --- a/installers/charm/bundles/osm/bundle.yaml +++ b/installers/charm/bundles/osm/bundle.yaml @@ -91,6 +91,7 @@ applications: options: log_level: "INFO" DATABASE_COMMONKEY: osm + auth-backend: keystone annotations: gui-x: 0 gui-y: -200 @@ -214,6 +215,14 @@ applications: annotations: gui-x: 250 gui-y: 550 + keystone: + charm: '%(prefix)s/keystone%(suffix)s' + channel: '%(channel)s' + scale: 1 + series: kubernetes + annotations: + gui-x: -250 + gui-y: 550 relations: - - "kafka-k8s:zookeeper" @@ -254,3 +263,7 @@ relations: - "mongodb-k8s:mongo" - - 'ng-ui:nbi' - 'nbi-k8s:nbi' + - - 'keystone:db' + - 'mariadb-k8s:mysql' + - - 'keystone:keystone' + - 'nbi-k8s:keystone' diff --git a/installers/charm/interfaces/keystone/interface.yaml b/installers/charm/interfaces/keystone/interface.yaml new file mode 100644 index 00000000..be1d09bd --- /dev/null +++ b/installers/charm/interfaces/keystone/interface.yaml @@ -0,0 +1,16 @@ +# Copyright 2020 Canonical Ltd. +# +# 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. +name: keystone +summary: Keystone Interface +version: 1 diff --git a/installers/charm/interfaces/keystone/provides.py b/installers/charm/interfaces/keystone/provides.py new file mode 100644 index 00000000..bda5d2f4 --- /dev/null +++ b/installers/charm/interfaces/keystone/provides.py @@ -0,0 +1,63 @@ +# Copyright 2020 Canonical Ltd. +# +# 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 charms.reactive import Endpoint +from charms.reactive import when +from charms.reactive import set_flag, clear_flag + + +class KeystoneProvides(Endpoint): + @when("endpoint.{endpoint_name}.joined") + def _joined(self): + set_flag(self.expand_name("{endpoint_name}.joined")) + + @when("endpoint.{endpoint_name}.changed") + def _changed(self): + set_flag(self.expand_name("{endpoint_name}.ready")) + + @when("endpoint.{endpoint_name}.departed") + def _departed(self): + set_flag(self.expand_name("{endpoint_name}.departed")) + clear_flag(self.expand_name("{endpoint_name}.joined")) + + def publish_info( + self, + host, + port, + keystone_db_password, + region_id, + user_domain_name, + project_domain_name, + admin_username, + admin_password, + admin_project_name, + username, + password, + service, + ): + for relation in self.relations: + relation.to_publish["host"] = host + relation.to_publish["port"] = port + relation.to_publish["keystone_db_password"] = keystone_db_password + relation.to_publish["region_id"] = region_id + relation.to_publish["user_domain_name"] = user_domain_name + relation.to_publish["project_domain_name"] = project_domain_name + relation.to_publish["admin_username"] = admin_username + relation.to_publish["admin_password"] = admin_password + relation.to_publish["admin_project_name"] = admin_project_name + relation.to_publish["username"] = username + relation.to_publish["password"] = password + relation.to_publish["service"] = service + + def mark_complete(self): + clear_flag(self.expand_name("{endpoint_name}.joined")) diff --git a/installers/charm/interfaces/keystone/requires.py b/installers/charm/interfaces/keystone/requires.py new file mode 100644 index 00000000..c0d8d473 --- /dev/null +++ b/installers/charm/interfaces/keystone/requires.py @@ -0,0 +1,72 @@ +# Copyright 2020 Canonical Ltd. +# +# 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 charms.reactive import Endpoint +from charms.reactive import when +from charms.reactive import set_flag, clear_flag + + +class KeystoneRequires(Endpoint): + @when("endpoint.{endpoint_name}.joined") + def _joined(self): + set_flag(self.expand_name("{endpoint_name}.joined")) + + @when("endpoint.{endpoint_name}.changed") + def _changed(self): + if len(self.keystones()) > 0: + set_flag(self.expand_name("{endpoint_name}.ready")) + else: + clear_flag(self.expand_name("{endpoint_name}.ready")) + + @when("endpoint.{endpoint_name}.departed") + def _departed(self): + set_flag(self.expand_name("{endpoint_name}.departed")) + clear_flag(self.expand_name("{endpoint_name}.joined")) + clear_flag(self.expand_name("{endpoint_name}.ready")) + + def keystones(self): + """ + Return Keystone Data: + [{ + 'host': , + 'port': , + 'keystone_db_password: , + 'region_id: , + 'admin_username: , + 'admin_password: , + 'admin_project_name: , + 'username: , + 'password: , + 'service: + }] + """ + keystones = [] + for relation in self.relations: + for unit in relation.units: + data = { + "host": unit.received["host"], + "port": unit.received["port"], + "keystone_db_password": unit.received["keystone_db_password"], + "region_id": unit.received["region_id"], + "user_domain_name": unit.received["user_domain_name"], + "project_domain_name": unit.received["project_domain_name"], + "admin_username": unit.received["admin_username"], + "admin_password": unit.received["admin_password"], + "admin_project_name": unit.received["admin_project_name"], + "username": unit.received["username"], + "password": unit.received["password"], + "service": unit.received["service"], + } + if all(data.values()): + keystones.append(data) + return keystones diff --git a/installers/charm/keystone/.gitignore b/installers/charm/keystone/.gitignore new file mode 100644 index 00000000..2545cca8 --- /dev/null +++ b/installers/charm/keystone/.gitignore @@ -0,0 +1,16 @@ +# Copyright 2020 Canonical Ltd. +# +# 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. +.vscode +build +keystone.charm \ No newline at end of file diff --git a/installers/charm/keystone/.yamllint.yaml b/installers/charm/keystone/.yamllint.yaml new file mode 100644 index 00000000..08ab437b --- /dev/null +++ b/installers/charm/keystone/.yamllint.yaml @@ -0,0 +1,25 @@ +# Copyright 2020 Canonical Ltd. +# +# 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. +--- +extends: default + +yaml-files: + - "*.yaml" + - "*.yml" + - ".yamllint" +ignore: | + .tox + build/ + mod/ + lib/ diff --git a/installers/charm/keystone/README.md b/installers/charm/keystone/README.md new file mode 100644 index 00000000..1ca9764c --- /dev/null +++ b/installers/charm/keystone/README.md @@ -0,0 +1,17 @@ + +# Keystone operator Charm for Kubernetes + +## Requirements + diff --git a/installers/charm/keystone/config.yaml b/installers/charm/keystone/config.yaml new file mode 100644 index 00000000..c99d8783 --- /dev/null +++ b/installers/charm/keystone/config.yaml @@ -0,0 +1,117 @@ +# Copyright 2020 Canonical Ltd. +# +# 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. +options: + image: + type: string + default: opensourcemano/keystone:latest + description: The docker image to install. + image_username: + type: string + description: | + The username for accessing the registry specified in image. + default: "" + image_password: + type: string + description: | + The password associated with image_username for accessing + the registry specified in image. + default: "" + max_file_size: + type: int + description: | + The maximum file size, in megabytes. + + If there is a reverse proxy in front of Keystone, it may + need to be configured to handle the requested size. + default: 5 + ingress_whitelist_source_range: + type: string + description: | + A comma-separated list of CIDRs to store in the + ingress.kubernetes.io/whitelist-source-range annotation. + + This can be used to lock down access to + Keystone based on source IP address. + default: "" + tls_secret_name: + type: string + description: TLS Secret name + default: "" + site_url: + type: string + description: Ingress URL + default: "" + ldap_enabled: + type: boolean + description: Boolean to enable/disable LDAP authentication + default: false + region_id: + type: string + description: Region ID to be created when starting the service + default: RegionOne + keystone_db_password: + type: string + description: Keystone DB Password + default: admin + admin_username: + type: string + description: Admin username to be created when starting the service + default: admin + admin_password: + type: string + description: Admin password to be created when starting the service + default: admin + admin_project: + type: string + description: Admin project to be created when starting the service + default: admin + service_username: + type: string + description: Service Username to be created when starting the service + default: nbi + service_password: + type: string + description: Service Password to be created when starting the service + default: nbi + service_project: + type: string + description: Service Project to be created when starting the service + default: service + user_domain_name: + type: string + description: User domain name (Hardcoded in the container start.sh script) + default: default + project_domain_name: + type: string + description: | + Project domain name (Hardcoded in the container start.sh script) + default: default + + # ENV LDAP_AUTHENTICATION_DOMAIN_NAME no default + # ENV LDAP_URL ldap://localhost + # ENV LDAP_BIND_USER no defauslt + # ENV LDAP_BIND_PASSWORD no default + # ENV LDAP_USER_TREE_DN no default + # ENV LDAP_USER_OBJECTCLASS inetOrgPerson + # ENV LDAP_USER_ID_ATTRIBUTE cn + # ENV LDAP_USER_NAME_ATTRIBUTE sn + # ENV LDAP_USER_PASS_ATTRIBUTE userPassword + # ENV LDAP_USER_FILTER no default + # ENV LDAP_USER_ENABLED_ATTRIBUTE enabled + # ENV LDAP_USER_ENABLED_MASK 0 + # ENV LDAP_USER_ENABLED_DEFAULT true + # ENV LDAP_USER_ENABLED_INVERT false + # ENV LDAP_USE_STARTTLS false + # ENV LDAP_TLS_CACERT_BASE64 no default + # ENV LDAP_TLS_REQ_CERT demand diff --git a/installers/charm/keystone/metadata.yaml b/installers/charm/keystone/metadata.yaml new file mode 100644 index 00000000..eb3c8efc --- /dev/null +++ b/installers/charm/keystone/metadata.yaml @@ -0,0 +1,30 @@ +# Copyright 2020 Canonical Ltd. +# +# 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. +name: keystone +summary: A Keystone K8s charm +description: | + Transmission +series: + - kubernetes +min-juju-version: 2.8.0 +requires: + db: + interface: mysql + limit: 1 +provides: + keystone: + interface: keystone +deployment: + type: stateless + service: cluster diff --git a/installers/charm/keystone/requirements.txt b/installers/charm/keystone/requirements.txt new file mode 100644 index 00000000..10ecdcd5 --- /dev/null +++ b/installers/charm/keystone/requirements.txt @@ -0,0 +1,14 @@ +# Copyright 2020 Canonical Ltd. +# +# 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. +ops diff --git a/installers/charm/keystone/src/charm.py b/installers/charm/keystone/src/charm.py new file mode 100755 index 00000000..632e96a4 --- /dev/null +++ b/installers/charm/keystone/src/charm.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from urllib.parse import urlparse + +from ops.charm import CharmBase + +# from ops.framework import StoredState +from ops.main import main +from ops.model import ( + ActiveStatus, + BlockedStatus, + # MaintenanceStatus, + WaitingStatus, + # ModelError, +) +from ops.framework import StoredState + +logger = logging.getLogger(__name__) + +REQUIRED_SETTINGS = [] + +DATABASE_NAME = "keystone" # This is hardcoded in the keystone container script +# We expect the keystone container to use the default port +KEYSTONE_PORT = 5000 + + +class KeystoneCharm(CharmBase): + + state = StoredState() + + def __init__(self, *args): + super().__init__(*args) + + # Register all of the events we want to observe + self.framework.observe(self.on.config_changed, self.configure_pod) + self.framework.observe(self.on.start, self.configure_pod) + self.framework.observe(self.on.upgrade_charm, self.configure_pod) + + # Register relation events + self.state.set_default( + db_host=None, db_port=None, db_user=None, db_password=None + ) + self.framework.observe( + self.on.db_relation_changed, self._on_db_relation_changed + ) + self.framework.observe( + self.on.keystone_relation_joined, self._publish_keystone_info + ) + + def _publish_keystone_info(self, event): + config = self.model.config + if self.unit.is_leader(): + rel_data = { + "host": f"http://{self.app.name}:{KEYSTONE_PORT}/v3", + "port": str(KEYSTONE_PORT), + "keystone_db_password": config["keystone_db_password"], + "region_id": config["region_id"], + "user_domain_name": config["user_domain_name"], + "project_domain_name": config["project_domain_name"], + "admin_username": config["admin_username"], + "admin_password": config["admin_password"], + "admin_project_name": config["admin_project"], + "username": config["service_username"], + "password": config["service_password"], + "service": config["service_project"], + } + for k, v in rel_data.items(): + event.relation.data[self.model.unit][k] = v + + def _on_db_relation_changed(self, event): + self.state.db_host = event.relation.data[event.unit].get("host") + self.state.db_port = event.relation.data[event.unit].get("port", 3306) + self.state.db_user = "root" # event.relation.data[event.unit].get("user") + self.state.db_password = event.relation.data[event.unit].get("root_password") + if self.state.db_host: + self.configure_pod(event) + + def _check_settings(self): + problems = [] + config = self.model.config + + for setting in REQUIRED_SETTINGS: + if not config.get(setting): + problem = f"missing config {setting}" + problems.append(problem) + + return ";".join(problems) + + def _make_pod_image_details(self): + config = self.model.config + image_details = { + "imagePath": config["image"], + } + if config["image_username"]: + image_details.update( + { + "username": config["image_username"], + "password": config["image_password"], + } + ) + return image_details + + def _make_pod_ports(self): + return [ + {"name": "keystone", "containerPort": KEYSTONE_PORT, "protocol": "TCP"}, + ] + + def _make_pod_envconfig(self): + config = self.model.config + + return { + "DB_HOST": self.state.db_host, + "DB_PORT": self.state.db_port, + "ROOT_DB_USER": self.state.db_user, + "ROOT_DB_PASSWORD": self.state.db_password, + "KEYSTONE_DB_PASSWORD": config["keystone_db_password"], + "REGION_ID": config["region_id"], + "KEYSTONE_HOST": self.app.name, + "ADMIN_USERNAME": config["admin_username"], + "ADMIN_PASSWORD": config["admin_password"], + "ADMIN_PROJECT": config["admin_project"], + "SERVICE_USERNAME": config["service_username"], + "SERVICE_PASSWORD": config["service_password"], + "SERVICE_PROJECT": config["service_project"], + } + + def _make_pod_ingress_resources(self): + site_url = self.model.config["site_url"] + + if not site_url: + return + + parsed = urlparse(site_url) + + if not parsed.scheme.startswith("http"): + return + + max_file_size = self.model.config["max_file_size"] + ingress_whitelist_source_range = self.model.config[ + "ingress_whitelist_source_range" + ] + + annotations = { + "nginx.ingress.kubernetes.io/proxy-body-size": "{}m".format(max_file_size) + } + + if ingress_whitelist_source_range: + annotations[ + "nginx.ingress.kubernetes.io/whitelist-source-range" + ] = ingress_whitelist_source_range + + ingress_spec_tls = None + + if parsed.scheme == "https": + ingress_spec_tls = [{"hosts": [parsed.hostname]}] + tls_secret_name = self.model.config["tls_secret_name"] + if tls_secret_name: + ingress_spec_tls[0]["secretName"] = tls_secret_name + else: + annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" + + ingress = { + "name": "{}-ingress".format(self.app.name), + "annotations": annotations, + "spec": { + "rules": [ + { + "host": parsed.hostname, + "http": { + "paths": [ + { + "path": "/", + "backend": { + "serviceName": self.app.name, + "servicePort": KEYSTONE_PORT, + }, + } + ] + }, + } + ], + }, + } + if ingress_spec_tls: + ingress["spec"]["tls"] = ingress_spec_tls + + return [ingress] + + def configure_pod(self, event): + """Assemble the pod spec and apply it, if possible.""" + + if not self.state.db_host: + self.unit.status = WaitingStatus("Waiting for database relation") + event.defer() + return + + if not self.unit.is_leader(): + self.unit.status = ActiveStatus() + return + + # Check problems in the settings + problems = self._check_settings() + if problems: + self.unit.status = BlockedStatus(problems) + return + + self.unit.status = BlockedStatus("Assembling pod spec") + image_details = self._make_pod_image_details() + ports = self._make_pod_ports() + env_config = self._make_pod_envconfig() + ingress_resources = self._make_pod_ingress_resources() + + pod_spec = { + "version": 3, + "containers": [ + { + "name": self.framework.model.app.name, + "imageDetails": image_details, + "ports": ports, + "envConfig": env_config, + } + ], + "kubernetesResources": {"ingressResources": ingress_resources or []}, + } + self.model.pod.set_spec(pod_spec) + self.unit.status = ActiveStatus() + + +if __name__ == "__main__": + main(KeystoneCharm) diff --git a/installers/charm/keystone/tox.ini b/installers/charm/keystone/tox.ini new file mode 100644 index 00000000..cff5193b --- /dev/null +++ b/installers/charm/keystone/tox.ini @@ -0,0 +1,47 @@ +# Copyright 2020 Canonical Ltd. +# +# 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. +[tox] +envlist = pep8 +skipsdist = True + +[testenv] +setenv = VIRTUAL_ENV={envdir} + PYTHONHASHSEED=0 +install_command = + pip install {opts} {packages} + +[testenv:build] +basepython = python3 +passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY +whitelist_externals = charmcraft + rm + unzip +commands = + rm -rf release + charmcraft build + unzip keystone.charm -d release + +[testenv:lint] +basepython = python3 +deps = + black + yamllint + flake8 +commands = + black --check --diff . --exclude "build/|.tox/|mod/|lib/" + yamllint . + flake8 . --max-line-length=100 --exclude "build/ .tox/ mod/ lib/" + +[testenv:venv] +commands = {posargs} diff --git a/installers/charmed_install.sh b/installers/charmed_install.sh index 8900270c..89b85cc8 100755 --- a/installers/charmed_install.sh +++ b/installers/charmed_install.sh @@ -324,6 +324,9 @@ applications: ng-ui: options: image: opensourcemano/ng-ui:$TAG + keystone: + options: + image: opensourcemano/keystone:$TAG EOF mv /tmp/images-overlay.yaml $IMAGES_OVERLAY_FILE