From 0fd99e625b0c4e8999acbb4b34043210aba6f94f Mon Sep 17 00:00:00 2001 From: Chris Vermeulen Date: Wed, 8 Aug 2018 08:38:58 +0200 Subject: [PATCH] Initial project --- .gitignore | 2 + Dockerfile | 17 ++++++ README.md | 29 ++++++++++ docker-compose.yml | 19 +++++++ entrypoint.sh | 3 ++ map_services.py | 129 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 199 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh create mode 100644 map_services.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..227d9c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea/ +/.DS_store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..91b5359 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM ubuntu:16.04 + +WORKDIR /code + +RUN apt-get update && \ + apt-get install -y apt-transport-https curl python3-dev nginx && \ + curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \ + touch /etc/apt/sources.list.d/kubernetes.list && \ + echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" | tee -a /etc/apt/sources.list.d/kubernetes.list && \ + apt-get update && \ + apt-get install -y kubectl=1.11.1-00 + +COPY . /code + +RUN chmod +x map_services.py entrypoint.sh + +ENTRYPOINT sh entrypoint.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..59f8e1f --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Kubernetes port forward utility + +> When developing services locally, we sometimes require dependant services to be proxied from our cluster to our local machine to ease development. +> This image serves the purpose of easing that process. + +### Usage + +The image uses the environment variable $SERVICES to see which services you need proxied. + +This variable should contain a comma-delimited list of deployments & ports to forward + +The format for this variable is DEPLOYMENT_NAME:PORT:TARGET_PORT, OTHER_DEPLOYMENT:PORT:TARGET_PORT etc. + +TARGET_PORT is optional. If none is supplied the image will use PORT as both remote and local port. + +In your docker-compose file you can simply add: +```yaml +kube-forwards: + image: chris-cmsoft/kube_forward:1.0.2 + environment: + - SERVICES=someservice:8000, someotherservice:8001:8000 + volumes: + - ~/.kube/config:/root/.kube/config +some-service: + image: some-image:1.0 +``` + +Now if you exec into the some-service container you can curl http://kube-forwards:8000 and it will hit someservice on the Kubernetes cluster. + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..631aa39 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: '2' + +services: + kube_forwards: + build: . + entrypoint: ['tail', '-f', '/dev/null'] + environment: + - SERVICES=authenticationservice:8000, srservice:8001:8000 + ports: + - 8011:8000 + - 8012:8001 + volumes: + - ~/.kube/config:/root/.kube/config + - .:/code + networks: + - default + +networks: + default: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..0504213 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +python3 map_services.py && tail -f port-log.txt \ No newline at end of file diff --git a/map_services.py b/map_services.py new file mode 100644 index 0000000..4932c85 --- /dev/null +++ b/map_services.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 + +""" +Library to find deployments that need to be proxied + +options: (To be specified as environment variables) + + SERVICES: The deployments that need to be proxied + + This script will look for the first running container and proxy that through to the machine. +""" + +import os +import logging +from subprocess import run, PIPE, Popen + + +nginx_directives = '' + +def validate_service_spec(service_spec): + """ + Valid service entries are services with: + * A deployment name + * At least one port + + Allowed additions: + * second port for local machine + """ + service_parts = service_spec.strip().split(':') + + # There have to be at least two parts to the string + if len(service_parts) <= 1: + logging.warn('Service spec needs at lease one deployment name & port in the format NAME:PORT') + logging.error( + 'Service: (' + service_spec + ') does not comply with standard. service needs to be secified in the ' + 'format NAME:TARGET_PORT:PORT_FORWARD_PORT(optional).') + exit(1) + + if len(service_parts[0]) <= 0: + logging.warn('Service name must be string') + logging.error('Service: (' + service_spec + ') does not comply with standard.') + exit(1) + + if not service_parts[1].isdigit(): + logging.warn('Service port must be integer') + logging.error('Service: (' + service_spec + ') does not comply with standard. (' + service_parts[ + 1] + ') os not a valid integer.') + exit(1) + + if len(service_parts) >= 3 and not service_parts[2].isdigit(): + logging.warn('Service port must be integer') + logging.error('Service: (' + service_spec + ') does not comply with standard. (' + service_parts[ + 2] + ') os not a valid integer.') + exit(1) + + return True + + +def port_forward_service(service_spec): + # kubectl get pods --selector=app=authenticationservice --output=jsonpath={.items..metadata.name} + + service_parts = service_spec.strip().split(':') + + output = run(['kubectl', 'get', 'pods', '--selector=app={}'.format(service_parts[0].strip()), + '--output=jsonpath={.items..metadata.name}'], stdout=PIPE) + + if not output.stdout.decode().strip(): + logging.error('No pods found for selector {}'.format(service_parts[0])) + exit(1) + + local_port = service_parts[1] + local_proxy_port = int(local_port) + 1000 + remote_port = local_port if (len(service_parts) == 2) else service_parts[2] + global nginx_directives + nginx_directives += """ + server {{ + listen {listen_port}; + + server_name _; + + location / {{ + proxy_pass http://127.0.0.1:{proxy_port}; + proxy_pass_header Server; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Scheme $scheme; + proxy_set_header X-Forwarded-Proto https; + }} + }} + """.format(listen_port=local_port, proxy_port=local_proxy_port) + + Popen('kubectl port-forward {} {}:{} >> port-log.txt &'.format( + output.stdout.decode().strip(), + local_proxy_port, + service_parts[1] if (len(service_parts) == 2) else service_parts[2] + ), close_fds=True, shell=True) + + print('Port Forwarding: {} to local port {} from port {}'.format( + service_parts[0], # Service name + local_port, # Local Port + remote_port, + )) + +def update_nginx_configs(): + file = open('/etc/nginx/sites-enabled/default', 'w+') + file.truncate(0) + file.write(nginx_directives) + file.close() + + +if __name__ == "__main__": + services = os.environ.get('SERVICES', None) + + if not services: + print('In order to proxy services, you need to specify the SERVICES environment variable.') + exit(0) + + service_list = services.split(',') + + for service in service_list: + # Validate will throw error and exit if services i oncorrectly defined. + validate_service_spec(service) + + for service in service_list: + port_forward_service(service) + + update_nginx_configs() + + Popen('service nginx restart', shell=True)