Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add keystone-shell container #185

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
23 changes: 23 additions & 0 deletions keystone-shell/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM python:3.6-alpine3.6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just use the monasca/python.
It should have all musl and stack-fix included already AFAIR.


# To force a rebuild, pass --build-arg REBUILD="$(DATE)" when running
# `docker build`
ARG REBUILD=1

COPY requirements.txt stack-fix.c /

RUN apk add --no-cache ca-certificates tini libffi && \
apk add --no-cache --virtual build-dep \
musl-dev linux-headers git make g++ libffi-dev openssl-dev && \
gcc -shared -fPIC /stack-fix.c -o /stack-fix.so && \
pip install urllib3 ipaddress ipython ptpython python-openstackclient && \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With monasca/python we could always install latest master, but I am not really sure if that's needed at all ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right, this should really be using monasca/python. The docker stuff is mostly a copy&paste hack job that came out of my frustration while debugging keystone-init.

I'm fine with holding off on merging until it's converted, though.

pip install -r /requirements.txt && \
rm -rf /root/.cache/pip && \
apk del build-dep

COPY keystone_shell_vars.py kubernetes.py ptpython_init.py start.sh /
COPY keystonerc.sh /root/.keystonerc

ENV ENV="/root/.keystonerc"
ENTRYPOINT ["sh", "-l", "/start.sh"]

106 changes: 106 additions & 0 deletions keystone-shell/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
keystone-shell
==============

This image allows you to quickly interact with a keystone environment.

Sources: [keystone-shell][1] · [Dockerfile][2] · [monasca-docker][3]

Tags
----

Images in this repository are tagged as follows:

* `latest`: refers to the latest stable point release, e.g. `1.0.0`
* `1.0.0`, `1.0`, `1`: standard semver tags

Basic Usage
-----------

This is mainly intended for use in Kubernetes environments configured using
[keystone-init][4]. To use, run:

kubectl run keystone-shell -i -t \
--rm=true \
--restart Never \
--image=monasca/keystone-shell:latest

(pass `-n <NAMESPACE>` as needed if the desired secrets are located elsewhere)

Once the container starts, press enter once to start the shell. Basic usage
information will be printed to the terminal.

There are three commands available:
- `secret <NAME>`: load keystone `OS_` variables from secret in current
namespace `NAME`
- `shell [NAME]`: start a Python shell (using ptipython, with highlighting
and autocomplete) with pre-connected Keystone and Kubernetes clients -
additional usage information will be printed on startup
- if specified, secret with `NAME` will be loaded first (as with
`secret NAME`) and will override existing environment variables
- `openstack ...`: the standard openstack client

Direct Shell
------------

You can also start a shell directly:

kubectl run keystone-shell \
--rm=true \
--restart Never \
--image=monasca/keystone-shell:latest \
--env="OS_USERNAME=admin" \
--env="OS_PASSWORD=secretadmin" \
--env="OS_PROJECT_NAME=admin" \
--env="OS_PROJECT_DOMAIN_NAME=Default" \
--env="OS_USER_DOMAIN_NAME=Default" \
--env="OS_AUTH_URL=http://monasca-keystone:35357/" \
--env="KEYSTONE_SHELL=true" \
--attach -i -t

Note the `KEYSTONE_SHELL` variable. `OS_` variables can also be provided in
lieu of a secret name (allowing the container to be run in docker rather than
Kubernetes).


Verifying Keystone Connectivity
-------------------------------

This container can be used as a one-off method to check if a Keystone instance
is available. To use, try:

kubectl run keystone-shell \
--rm=true \
--restart Never \
--image=monasca/keystone-shell:latest \
--env="KEYSTONE_SECRET=keystone-example-user" \
--attach

Note the absence of the `-i` and `-t` parameters. Note that `KEYSTONE_SECRET`
must be defined in this scenario.

If Keystone is available and the credentials work, the return code will be
zero. If Keystone is not available or the credentials are invalid, the return
code will be 1. Log information should be printed to help narrow down the
error, or at least as much as Keystone will be willing to divulge about it.

Configuration
-------------

| Variable | Default | Description |
|--------------------------|---------|----------------------------------------|
| `LOG_LEVEL` | `INFO` | Python logging level |
| `KEYSTONE_TIMEOUT` | `10` | Keystone connection timeout in seconds |
| `KEYSTONE_VERIFY` | `true` | If `false`, don't verify SSL |
| `KEYSTONE_CERT` | unset | Path to mounted alternative CA bundle |
| `KEYSTONE_SECRET` | unset | Secret to auto-load on startup |
| `OS_AUTH_URL` | unset | (**required**) |
| `OS_USERNAME` | unset | keystone username |
| `OS_PASSWORD` | unset | keystone password |
| `OS_USER_DOMAIN_NAME` | unset | keystone user domain name |
| `OS_PROJECT_NAME` | unset | keystone project name |
| `OS_PROJECT_DOMAIN_NAME` | unset | keystone project domain name |

[1]: https://github.com/monasca/monasca-docker/blob/master/keystone-shell/
[2]: https://github.com/monasca/monasca-docker/blob/master/keystone-shell/Dockerfile
[3]: https://github.com/monasca/monasca-docker/
[4]: https://github.com/monasca/monasca-docker/blob/master/keystone-init/
7 changes: 7 additions & 0 deletions keystone-shell/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
repository: monasca/keystone-shell
variants:
- tag: latest
aliases:
- :1.0.0
- :1.0
- :1
151 changes: 151 additions & 0 deletions keystone-shell/keystone_shell_vars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#!/usr/bin/env python3

# (C) Copyright 2017 Hewlett Packard Enterprise Development LP
#
# 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 base64
import logging
import os
import sys

from typing import Union

from keystoneauth1.identity import Password
from keystoneauth1.session import Session
from keystoneclient.discover import Discover
from requests import HTTPError

from kubernetes import KubernetesAPIClient, KubernetesAPIResponse

NAMESPACE_FILE = '/var/run/secrets/kubernetes.io/serviceaccount/namespace'

LOG_LEVEL = logging.getLevelName(os.environ.get('LOG_LEVEL', 'INFO'))
logging.basicConfig(level=LOG_LEVEL,
handlers=(logging.StreamHandler(sys.stderr),))

logger = logging.getLogger(__name__)

KEYSTONE_PASSWORD_ARGS = [
'auth_url', 'username', 'password', 'user_id', 'user_domain_id',
'user_domain_name', 'project_id', 'project_name', 'project_domain_id',
'project_domain_name', 'tenant_id', 'tenant_name', 'domain_id',
'domain_name', 'trust_id', 'default_domain_id', 'default_domain_name'
]
KEYSTONE_TIMEOUT = int(os.environ.get('KEYSTONE_TIMEOUT', '10'))
KEYSTONE_VERIFY = os.environ.get('KEYSTONE_VERIFY', 'true') == 'true'
KEYSTONE_CERT = os.environ.get('KEYSTONE_CERT', None)

_kubernetes_client = None


def get_current_namespace() -> str:
if 'NAMESPACE' in os.environ:
return os.environ['NAMESPACE']

if os.path.exists(NAMESPACE_FILE):
with open(NAMESPACE_FILE, 'r') as f:
return f.read()

logger.warning('Not running in cluster and $NAMESPACE is not set, '
'assuming \'default\'!')
return 'default'


def get_kubernetes_client() -> KubernetesAPIClient:
global _kubernetes_client

if _kubernetes_client is None:
_kubernetes_client = KubernetesAPIClient()
_kubernetes_client.load_auto_config()

return _kubernetes_client


def keystone_env_vars():
ret = {}
for arg in KEYSTONE_PASSWORD_ARGS:
name = 'OS_{}'.format(arg.upper())
if name in os.environ:
ret[name] = os.environ[name]

return ret


def keystone_args_from_env():
ret = {}
for arg in KEYSTONE_PASSWORD_ARGS:
ret[arg] = os.environ.get('OS_{}'.format(arg.upper()))

return ret


def get_keystone_client():
auth = Password(**keystone_args_from_env())
session = Session(auth=auth,
app_name='keystone-shell',
user_agent='keystone-shell',
timeout=KEYSTONE_TIMEOUT,
verify=KEYSTONE_VERIFY,
cert=KEYSTONE_CERT)

discover = Discover(session=session)
return discover.create_client()


def get_kubernetes_secret(name: str,
namespace: str=None) -> Union[KubernetesAPIResponse,
None]:
"""
:return: loaded secret dict or None if it does not exist
"""
client = get_kubernetes_client()

if namespace is None:
namespace = get_current_namespace()

try:
return client.get('/api/v1/namespaces/{}/secrets/{}', namespace, name)
except HTTPError as e:
if e.response.status_code != 404:
raise

return None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this return should have one less indentation level. I know you run it from except but this way you will make it default.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that it matters a ton since the code paths are effectively equivalent but my feeling was that (without the conditional to check for a 404) is a bit more clear about the intent.

Ideally it would've been:

try:
    return client.get(...)
except NotFound:
    return None

... which I think makes the intent more clear - that is, None isn't the "default" branch so much as the "not found" branch. Since requests doesn't have exception subclasses for different error codes, it gets a bit uglier when we compare the error code ... I'm not sure how much more readable the end result is, though.



def main():
if len(sys.argv) == 1:
logger.info('no secret name arg provided, will use default environment')
return

secret_name = sys.argv[1]

if len(sys.argv) > 2:
secret_namespace = sys.argv[2]
else:
secret_namespace = None

# purge existing keystone vars from the environment
for var in keystone_env_vars().keys():
print('unset {};'.format(var))

secret = get_kubernetes_secret(secret_name, secret_namespace)
for key, val in secret.data.items():
val_bytes = base64.b64decode(val)
print('export {}="{}"'.format(key, val_bytes.decode('utf-8')))

logger.info('now using account from secret %s', secret_name)


if __name__ == '__main__':
main()
11 changes: 11 additions & 0 deletions keystone-shell/keystonerc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
secret() {
eval "$(python /keystone_shell_vars.py $1)"
}

shell() {
if [ "$#" -ne 1 ]; then
eval "$(python /keystone_shell_vars.py $1)"
fi

LD_PRELOAD=/stack-fix.so ptipython -i /ptpython_init.py
}
Loading