Skip to content

Commit

Permalink
Create a version of echo-server to echo headers.
Browse files Browse the repository at this point in the history
* This is intended to support debugging IAP; we want to see what headers
  are on resulting requests.

* See kubeflow#574

* While creating this I ran into an issue with ksonnet not formatting the
  Ambassador mapping correctly unless we import it from a libsonnet file see
  ksonnet/ksonnet#670
  • Loading branch information
jlewi committed Jun 28, 2018
1 parent 28fab67 commit 113dba3
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 0 deletions.
8 changes: 8 additions & 0 deletions components/echo-server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM python:2.7-slim

ADD . /app
WORKDIR /app

RUN pip install -r requirements.txt

ENTRYPOINT ["gunicorn", "-b", ":8080", "main:app"]
40 changes: 40 additions & 0 deletions components/echo-server/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright 2017 The Kubernetes Authors.
#
# 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.
#
# Requirements:
# https://github.com/mattrobenolt/jinja2-cli
# pip install jinja2-clie
IMG = gcr.io/kubeflow-images-staging/echo-server

TAG := $(shell date +v%Y%m%d)-$(shell git describe --always --dirty)-$(shell git diff | shasum -a256 | cut -c -6)
DIR := ${CURDIR}

all: build

# To build without the cache set the environment variable
# export DOCKER_BUILD_OPTS=--no-cache
build:
docker build ${DOCKER_BUILD_OPTS} -t $(IMG):$(TAG) .
docker tag ${DOCKER_BUILD_OPTS} $(IMG):$(TAG) $(IMG):latest
@echo Built $(IMG):$(TAG)

# Build but don't attach the latest tag. This allows manual testing/inspection of the image
# first.
push: build
gcloud docker -- push $(IMG):$(TAG)
@echo Pushed $(IMG) with :$(TAG) tags

push-latest: push
gcloud container images add-tag --quiet $(IMG):$(TAG) $(IMG):latest --verbosity=info
echo created $(IMG):latest
98 changes: 98 additions & 0 deletions components/echo-server/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# 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.

"""Google Cloud Endpoints sample application.
Demonstrates how to create a simple echo API as well as how to deal with
various authentication methods.
"""

import base64
import json
import logging

from flask import Flask, jsonify, request
from flask_cors import cross_origin
from six.moves import http_client


app = Flask(__name__)


def _base64_decode(encoded_str):
# Add paddings manually if necessary.
num_missed_paddings = 4 - len(encoded_str) % 4
if num_missed_paddings != 4:
encoded_str += b'=' * num_missed_paddings
return base64.b64decode(encoded_str).decode('utf-8')


@app.route('/echo', methods=['POST'])
def echo():
"""Simple echo service."""
message = request.get_json().get('message', '')
return jsonify({'message': message})

@app.route('/')
@app.route('/headers')
def headers():
return jsonify({'headers': request.headers.to_list()})

def auth_info():
"""Retrieves the authenication information from Google Cloud Endpoints."""
encoded_info = request.headers.get('X-Endpoint-API-UserInfo', None)

if encoded_info:
info_json = _base64_decode(encoded_info)
user_info = json.loads(info_json)
else:
user_info = {'id': 'anonymous'}

return jsonify(user_info)


@app.route('/auth/info/googlejwt', methods=['GET'])
def auth_info_google_jwt():
"""Auth info with Google signed JWT."""
return auth_info()


@app.route('/auth/info/googleidtoken', methods=['GET'])
def auth_info_google_id_token():
"""Auth info with Google ID token."""
return auth_info()


@app.route('/auth/info/firebase', methods=['GET'])
@cross_origin(send_wildcard=True)
def auth_info_firebase():
"""Auth info with Firebase auth."""
return auth_info()


@app.errorhandler(http_client.INTERNAL_SERVER_ERROR)
def unexpected_error(e):
"""Handle exceptions by returning swagger-compliant json."""
logging.exception('An error occured while processing the request.')
response = jsonify({
'code': http_client.INTERNAL_SERVER_ERROR,
'message': 'Exception: {}'.format(e)})
response.status_code = http_client.INTERNAL_SERVER_ERROR
return response


if __name__ == '__main__':
# This is used when running locally. Gunicorn is used to run the
# application on Google App Engine. See entrypoint in app.yaml.
app.run(host='127.0.0.1', port=8080, debug=True)
82 changes: 82 additions & 0 deletions components/echo-server/main_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# 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 json
import os

import pytest

import main


@pytest.fixture
def client(monkeypatch):
monkeypatch.chdir(os.path.dirname(main.__file__))
main.app.testing = True
client = main.app.test_client()
return client


def test_echo(client):
r = client.post(
'/echo',
data='{"message": "Hello"}',
headers={
'Content-Type': 'application/json'
})

assert r.status_code == 200
data = json.loads(r.data.decode('utf-8'))
assert data['message'] == 'Hello'


def test_auth_info(client):
endpoints = [
'/auth/info/googlejwt',
'/auth/info/googleidtoken',
'/auth/info/firebase']

encoded_info = base64.b64encode(json.dumps({
'id': '123'
}).encode('utf-8'))

for endpoint in endpoints:
r = client.get(
endpoint,
headers={
'Content-Type': 'application/json'
})

assert r.status_code == 200
data = json.loads(r.data.decode('utf-8'))
assert data['id'] == 'anonymous'

r = client.get(
endpoint,
headers={
'Content-Type': 'application/json',
'X-Endpoint-API-UserInfo': encoded_info
})

assert r.status_code == 200
data = json.loads(r.data.decode('utf-8'))
assert data['id'] == '123'


def test_cors(client):
r = client.options(
'/auth/info/firebase', headers={'Origin': 'example.com'})
assert r.status_code == 200
assert r.headers['Access-Control-Allow-Origin'] == '*'
8 changes: 8 additions & 0 deletions components/echo-server/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Flask==0.12.2
flask-cors==3.0.3
gunicorn==19.7.1
six==1.11.0
pyyaml==3.12
requests==2.18.4
google-auth==1.4.1
google-auth-oauthlib==0.2.0
79 changes: 79 additions & 0 deletions kubeflow/core/echo-server.libsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
service(namespace, name):: {
apiVersion: "v1",
kind: "Service",
metadata: {
labels: {
app: name,
},
name: name,
namespace: namespace,
annotations: {
"getambassador.io/config":
std.join("\n", [
"---",
"apiVersion: ambassador/v0",
"kind: Mapping",
"name: " + name + "-mapping",
"prefix: /" + name,
"rewrite: /",
"service: " + name + "." + namespace,
]),
}, //annotations
},
spec: {
ports: [
{
port: 80,
targetPort: 8080,
},
],
selector: {
app: name,
},
type: "ClusterIP",
},
},

deploy(namespace, name, image):: {
apiVersion: "extensions/v1beta1",
kind: "Deployment",
metadata: {
name: name,
namespace: namespace,

},
spec: {
replicas: 1,
template: {
metadata: {
labels: {
app: name,
},
},
spec: {
containers: [
{
image: image,
name: "app",
ports: [
{
containerPort: 8080,
},
],

readinessProbe: {
httpGet: {
path: "/headers",
port: 8080,
},
initialDelaySeconds: 5,
periodSeconds: 30,
},
},
],
},
},
},
},
}
16 changes: 16 additions & 0 deletions kubeflow/core/prototypes/echo-server.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// @apiVersion 0.1
// @name io.ksonnet.pkg.echo-server
// @description Provides a simple server for testing connections; primarily IAP.
// @shortDescription A simple echo server.
// @param name string Name for the component
// @optionalParam image string gcr.io/kubeflow-images-staging/echo-server:v20180628-e545118c-dirty-8a27d6 The image to use.

local k = import "k.libsonnet";

// TODO(https://github.com/ksonnet/ksonnet/issues/670) If we don't import the service
// from a libsonnet file the annotation doesn't end up being escaped/represented in a way that
// Ambassador can understand.
local echoParts = import "kubeflow/core/echo-server.libsonnet";
local namespace = env.namespace;

std.prune(k.core.v1.list.new([echoParts.service(namespace, params.name), echoParts.deploy(namespace, params.name, params.image)]))

0 comments on commit 113dba3

Please sign in to comment.