From 65175da269250750d4137c0659daed1201343880 Mon Sep 17 00:00:00 2001 From: Martin Lindner Date: Tue, 6 Aug 2024 17:06:18 +0900 Subject: [PATCH 01/20] DEV: Add basic external app example --- External/.gitignore | 7 ++ External/Dockerfile | 49 ++++++++++++ External/Dockerfile.devspace | 41 ++++++++++ External/deploy/main.tf | 132 +++++++++++++++++++++++++++++++++ External/deploy/terraform.tf | 10 +++ External/deploy/variables.tf | 47 ++++++++++++ External/devspace.yaml | 115 ++++++++++++++++++++++++++++ External/src/main.py | 56 ++++++++++++++ External/src/startup-script.sh | 5 ++ 9 files changed, 462 insertions(+) create mode 100644 External/.gitignore create mode 100644 External/Dockerfile create mode 100644 External/Dockerfile.devspace create mode 100644 External/deploy/main.tf create mode 100644 External/deploy/terraform.tf create mode 100644 External/deploy/variables.tf create mode 100644 External/devspace.yaml create mode 100644 External/src/main.py create mode 100644 External/src/startup-script.sh diff --git a/External/.gitignore b/External/.gitignore new file mode 100644 index 0000000..3ebd780 --- /dev/null +++ b/External/.gitignore @@ -0,0 +1,7 @@ +.devspace + +.terraform +.terraform.lock.hcl +terraform.tfstate* + +app_environment.zbundle diff --git a/External/Dockerfile b/External/Dockerfile new file mode 100644 index 0000000..875923b --- /dev/null +++ b/External/Dockerfile @@ -0,0 +1,49 @@ +# This is the Dockerfile for the Enthought Edge "external" example. +# +# We use "edm-rockylinux-8" as the base image. This is a Rockylinux-8 based +# image which includes EDM. +# +# EDM dependencies for the app are brought in via a .zbundle file. This avoids +# the need to pass your EDM token and/or a custom edm.yaml into the Dockerfile. +# +# We perform a two-stage build, to avoid including the .zbundle in the layers +# of the published image. + +# First stage + +ARG BASE_IMAGE=quay.io/enthought/edm-rockylinux-8:latest + +FROM $BASE_IMAGE AS stage_one + +ARG EDGE_BUNDLE=app_environment.zbundle +COPY $EDGE_BUNDLE /tmp/app_environment.zbundle + +RUN adduser app +USER app +WORKDIR /home/app + +# Create a default EDM environment using the enthought_edge bundle +RUN edm env import -f /tmp/app_environment.zbundle edm && edm cache purge --yes + + +# Second stage + +FROM $BASE_IMAGE AS stage_two + +LABEL source="https://github.com/enthought/edge-examples/blob/main/External/Dockerfile" + +RUN adduser app +USER app +WORKDIR /home/app + +COPY --from=stage_one --chown=app /home/app/.edm /home/app/.edm + +# Make any global changes (yum install, e.g.) in the second stage. +# Don't change the user, and in particular don't make the user "root". + +# Copy startup script and application. +COPY --chown=app ./src/startup-script.sh /home/app/startup-script.sh +RUN chmod +x /home/app/startup-script.sh +COPY --chown=app ./src/main.py /home/app/main.py + +CMD ["/home/app/startup-script.sh"] diff --git a/External/Dockerfile.devspace b/External/Dockerfile.devspace new file mode 100644 index 0000000..98fd447 --- /dev/null +++ b/External/Dockerfile.devspace @@ -0,0 +1,41 @@ +# This is the Dockerfile for the Enthought Edge "external" example. +# +# We use "edm-rockylinux-8" as the base image. This is a Rockylinux-8 based +# image which includes EDM. +# +# EDM dependencies for the app are brought in via a .zbundle file. This avoids +# the need to pass your EDM token and/or a custom edm.yaml into the Dockerfile. +# +# We perform a two-stage build, to avoid including the .zbundle in the layers +# of the published image. + +# First stage + +ARG BASE_IMAGE=quay.io/enthought/edm-rockylinux-8:latest + +FROM $BASE_IMAGE AS stage_one + +ARG EDGE_BUNDLE=app_environment.zbundle +COPY $EDGE_BUNDLE /tmp/app_environment.zbundle + +RUN adduser app +USER app +WORKDIR /home/app + +# Create a default EDM environment using the enthought_edge bundle +RUN edm env import -f /tmp/app_environment.zbundle edm && edm cache purge --yes + + +# Second stage + +FROM $BASE_IMAGE AS stage_two + +LABEL source="https://github.com/enthought/edge-examples/blob/main/External/Dockerfile.devspace" + +RUN adduser app && mkdir /.devspace && chown app:app /.devspace +USER app +WORKDIR /home/app + +COPY --from=stage_one --chown=app /home/app/.edm /home/app/.edm + +CMD ["sleep", "infinity"] diff --git a/External/deploy/main.tf b/External/deploy/main.tf new file mode 100644 index 0000000..f5d6381 --- /dev/null +++ b/External/deploy/main.tf @@ -0,0 +1,132 @@ +provider "kubernetes" { + config_path = "~/.kube/config" + config_context = var.kube_context +} + +data "kubernetes_namespace_v1" "this" { + metadata { + name = var.namespace + } +} + +resource "kubernetes_deployment_v1" "this" { + metadata { + name = "${var.app_name}-${var.component_name}" + namespace = data.kubernetes_namespace_v1.this.metadata.0.name + labels = { + "app.kubernetes.io/name" = var.app_name + "app.kubernetes.io/component" = var.component_name + } + } + + lifecycle { + ignore_changes = [ + metadata[0].annotations["devspace.sh/replicas"] + ] + } + + wait_for_rollout = false + + spec { + replicas = 1 + selector { + match_labels = { + "app.kubernetes.io/name" = var.app_name + "app.kubernetes.io/component" = var.component_name + } + } + template { + metadata { + labels = { + "app.kubernetes.io/name" = var.app_name + "app.kubernetes.io/component" = var.component_name + } + } + spec { + container { + name = "main" + image = "${var.image_name}:${var.image_tag}" + image_pull_policy = "IfNotPresent" + + env { + name = "PORT" + value = var.container_port + } + + env { + name = "PREFIX" + value = var.prefix + } + + resources { + requests = { + cpu = "100m" + memory = "256Mi" + } + limits = { + cpu = "100m" + memory = "256Mi" + } + } + + port { + container_port = var.container_port + name = "http" + protocol = "TCP" + } + } + + dynamic "toleration" { + for_each = var.use_nodepool ? [1] : [] + content { + key = "enthought.com/node-pool-purpose" + operator = "Equal" + value = var.namespace + effect = "NoSchedule" + } + } + + dynamic "affinity" { + for_each = var.use_nodepool ? [1] : [] + content { + node_affinity { + required_during_scheduling_ignored_during_execution { + node_selector_term { + match_expressions { + key = "enthought.com/node-pool-purpose" + operator = "In" + values = [var.namespace] + } + match_expressions { + key = "karpenter.sh/capacity-type" + operator = "In" + values = ["on-demand"] + } + } + } + } + } + } + } + } + } +} + +resource "kubernetes_service_v1" "this" { + metadata { + name = "${var.app_name}-${var.component_name}" + namespace = data.kubernetes_namespace_v1.this.metadata.0.name + } + + spec { + selector = { + "app.kubernetes.io/name" = var.app_name + "app.kubernetes.io/component" = var.component_name + } + port { + port = var.service_port + target_port = var.container_port + protocol = "TCP" + } + } +} diff --git a/External/deploy/terraform.tf b/External/deploy/terraform.tf new file mode 100644 index 0000000..3cffeec --- /dev/null +++ b/External/deploy/terraform.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.27" + } + } +} \ No newline at end of file diff --git a/External/deploy/variables.tf b/External/deploy/variables.tf new file mode 100644 index 0000000..cee133d --- /dev/null +++ b/External/deploy/variables.tf @@ -0,0 +1,47 @@ +variable "namespace" { + type = string +} + +variable "kube_context" { + type = string +} + +variable "image_tag" { + type = string + default = "latest" +} + +variable "image_name" { + type = string + default = "edge-external-app-example" +} + +variable "prefix" { + type = string + default = "/external/default/example/" +} + +variable "use_nodepool" { + type = bool + default = true +} + +variable "app_name" { + type = string + default = "example" +} + +variable "component_name" { + type = string + default = "backend" +} + +variable "service_port" { + type = number + default = 9000 +} + +variable "container_port" { + type = number + default = 9000 +} diff --git a/External/devspace.yaml b/External/devspace.yaml new file mode 100644 index 0000000..545a116 --- /dev/null +++ b/External/devspace.yaml @@ -0,0 +1,115 @@ +version: v2beta1 +name: edge-external-app-example + +vars: + DEVSPACE_AWS_PROFILE: + source: env + default: AdministratorAccess-594315687794 + DEVSPACE_AWS_ECR_HOST: + source: env + default: 594315687794.dkr.ecr.us-east-1.amazonaws.com + +localRegistry: + enabled: false + +pipelines: + build: + run: |- + build_images --all + dev: + run: |- + build_images --all + create_deployments --all + start_dev --all + purge: + run: |- + stop_dev --all + purge_deployments --all + +hooks: +- name: "pre-deploy-hook" + command: |- + $( + if [ ${DEVSPACE_CONTEXT} != "docker-desktop" ] || [ ]; then + echo 'aws sts get-caller-identity --profile ${DEVSPACE_AWS_PROFILE} >/dev/null 2>&1 || aws sso login' + else + echo '' + fi + ) + events: ["before:deploy"] +- name: "pre-image-build-hook" + command: |- + $( + if ! [ -f app_environment.zbundle ]; then + echo 'edm bundle generate -i --version 3.8 --platform rh7-x86_64 -m 2.0 -f app_environment.zbundle flask setuptools gunicorn' + else + echo '' + fi + if [ ${DEVSPACE_CONTEXT} != "docker-desktop" ]; then + echo 'aws ecr get-login-password --profile ${DEVSPACE_AWS_PROFILE} | docker login --username AWS --password-stdin ${DEVSPACE_AWS_ECR_HOST}' + else + echo '' + fi + ) + events: ["before:build"] + + +images: + edge-external-app-example: + image: ${DEVSPACE_AWS_ECR_HOST}/edge-external-app-example + dockerfile: ./Dockerfile + context: . + edge-external-app-example-dev: + image: ${DEVSPACE_AWS_ECR_HOST}/edge-external-app-example-dev + dockerfile: ./Dockerfile.devspace + context: . + rebuildStrategy: "ignoreContextChanges" + +commands: + terraform-init: |- + cd deploy + terraform init + +functions: + create_deployments: |- + cd deploy + export TF_VAR_namespace=${DEVSPACE_NAMESPACE} + export TF_VAR_kube_context=${DEVSPACE_CONTEXT} + export TF_VAR_image_name=$(get_image --only=image edge-external-app-example) + export TF_VAR_image_tag=$(get_image --only=tag edge-external-app-example) + export TF_VAR_use_nodepool=$( [ ${DEVSPACE_CONTEXT} != "docker-desktop" ] && echo "true" || echo "false" ) + terraform apply -auto-approve + + purge_deployments: |- + cd deploy + export TF_VAR_namespace=${DEVSPACE_NAMESPACE} + export TF_VAR_kube_context=${DEVSPACE_CONTEXT} + terraform destroy -auto-approve + +dev: + edge-external-app-example: + labelSelector: + "app.kubernetes.io/name": "example" + "app.kubernetes.io/component": "backend" + devImage: ${runtime.images.edge-external-app-example-dev.image}:${runtime.images.edge-external-app-example-dev.tag} + command: [ "/bin/sh", "-c" ] + args: [ "/home/app/startup-script.sh --reload" ] + ports: + - port: 9000:9000 + logs: { } + sync: + - path: src/startup-script.sh:/home/app/startup-script.sh + file: true + startContainer: true + disableDownload: true + printLogs: true + onUpload: + exec: + - command: |- + chmod +x /home/app/startup-script.sh + restartContainer: true + - path: src/main.py:/home/app/main.py + file: true + startContainer: true + disableDownload: true + printLogs: true diff --git a/External/src/main.py b/External/src/main.py new file mode 100644 index 0000000..47c6cd4 --- /dev/null +++ b/External/src/main.py @@ -0,0 +1,56 @@ +# Enthought product code +# +# (C) Copyright 2010-2024 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This file and its contents are confidential information and NOT open source. +# Distribution is prohibited. + +""" + Example Flask application (external). +""" + +import os +from flask import Flask, render_template_string, request + +PREFIX = os.environ.get("PREFIX", "/") + +app = Flask(__name__) + +@app.get(PREFIX) +def root(): + headers = dict(request.headers) + + html = """ + + + External Example Flask application + + + +

Hello, {{ headers.get('X-Forwarded-Display-Name', 'Unknown') }}!

+

Request Headers

+ + + + + + {% for header, value in headers.items() %} + + + + + {% endfor %} +
HeaderValue
{{ header }}{{ value }}
+ + + """ + + return render_template_string(html, headers=headers) diff --git a/External/src/startup-script.sh b/External/src/startup-script.sh new file mode 100644 index 0000000..1ce29a8 --- /dev/null +++ b/External/src/startup-script.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e + +exec edm run -- gunicorn main:app -b 0.0.0.0:9000 -w 1 --access-logfile - --error-logfile - "$@" From 9f3cd2987979353c9a5f2ab1fd89f64afd17d76d Mon Sep 17 00:00:00 2001 From: Martin Lindner Date: Thu, 8 Aug 2024 18:05:00 +0900 Subject: [PATCH 02/20] DEV: Add separate header injector module for local dev --- .../terraform/deployments/devspace/main.tf | 55 ++++++ .../deployments/devspace}/terraform.tf | 0 .../deployments/devspace/variables.tf | 22 +++ .../{ => terraform/modules/app}/main.tf | 5 - .../deploy/terraform/modules/app/terraform.tf | 10 ++ .../{ => terraform/modules/app}/variables.tf | 12 -- .../modules/istio-inject-headers/istio.tf | 158 ++++++++++++++++++ .../modules/istio-inject-headers/main.tf | 27 +++ .../modules/istio-inject-headers/terraform.tf | 10 ++ .../modules/istio-inject-headers/variables.tf | 28 ++++ 10 files changed, 310 insertions(+), 17 deletions(-) create mode 100644 External/deploy/terraform/deployments/devspace/main.tf rename External/deploy/{ => terraform/deployments/devspace}/terraform.tf (100%) create mode 100644 External/deploy/terraform/deployments/devspace/variables.tf rename External/deploy/{ => terraform/modules/app}/main.tf (96%) create mode 100644 External/deploy/terraform/modules/app/terraform.tf rename External/deploy/{ => terraform/modules/app}/variables.tf (62%) create mode 100644 External/deploy/terraform/modules/istio-inject-headers/istio.tf create mode 100644 External/deploy/terraform/modules/istio-inject-headers/main.tf create mode 100644 External/deploy/terraform/modules/istio-inject-headers/terraform.tf create mode 100644 External/deploy/terraform/modules/istio-inject-headers/variables.tf diff --git a/External/deploy/terraform/deployments/devspace/main.tf b/External/deploy/terraform/deployments/devspace/main.tf new file mode 100644 index 0000000..26232dd --- /dev/null +++ b/External/deploy/terraform/deployments/devspace/main.tf @@ -0,0 +1,55 @@ +provider "kubernetes" { + config_path = "~/.kube/config" + config_context = var.kube_context +} + +locals { + app_name = "example" + component_name = "backend" + prefix = "/external/default/example/" + service_port = 9000 + container_port = 9000 + + inject_headers = { + "X-Forwarded-Email" = "testuser@example.com" + "X-Forwarded-Groups" = "role:edge-external-app-example:user" + "X-Forwarded-Preferred-Username" = "testuser@example.com" + "X-Forwarded-User" = "abababab-abab-abab-abab-abababababab" + "X-Forwarded-Display-Name" = "Test User" + } +} + +module "istio_inject_headers" { + count = var.local ? 1 : 0 + + source = "../../modules/istio-inject-headers" + + app_name = local.app_name + component_name = local.component_name + container_port = local.container_port + service_port = local.service_port + + prefix = local.prefix + + namespace = var.namespace + + inject_headers = local.inject_headers +} + +module "app" { + source = "../../modules/app" + + use_nodepool = !var.local + + app_name = local.app_name + component_name = local.component_name + container_port = local.container_port + service_port = local.service_port + + prefix = local.prefix + + namespace = var.namespace + + image_name = var.image_name + image_tag = var.image_tag +} diff --git a/External/deploy/terraform.tf b/External/deploy/terraform/deployments/devspace/terraform.tf similarity index 100% rename from External/deploy/terraform.tf rename to External/deploy/terraform/deployments/devspace/terraform.tf diff --git a/External/deploy/terraform/deployments/devspace/variables.tf b/External/deploy/terraform/deployments/devspace/variables.tf new file mode 100644 index 0000000..191edf5 --- /dev/null +++ b/External/deploy/terraform/deployments/devspace/variables.tf @@ -0,0 +1,22 @@ +variable "kube_context" { + type = string +} + +variable "namespace" { + type = string +} + +variable "image_tag" { + type = string + default = "latest" +} + +variable "image_name" { + type = string + default = "edge-external-app-example" +} + +variable "local" { + type = bool + default = true +} diff --git a/External/deploy/main.tf b/External/deploy/terraform/modules/app/main.tf similarity index 96% rename from External/deploy/main.tf rename to External/deploy/terraform/modules/app/main.tf index f5d6381..65758e7 100644 --- a/External/deploy/main.tf +++ b/External/deploy/terraform/modules/app/main.tf @@ -1,8 +1,3 @@ -provider "kubernetes" { - config_path = "~/.kube/config" - config_context = var.kube_context -} - data "kubernetes_namespace_v1" "this" { metadata { name = var.namespace diff --git a/External/deploy/terraform/modules/app/terraform.tf b/External/deploy/terraform/modules/app/terraform.tf new file mode 100644 index 0000000..3cffeec --- /dev/null +++ b/External/deploy/terraform/modules/app/terraform.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.27" + } + } +} \ No newline at end of file diff --git a/External/deploy/variables.tf b/External/deploy/terraform/modules/app/variables.tf similarity index 62% rename from External/deploy/variables.tf rename to External/deploy/terraform/modules/app/variables.tf index cee133d..195970c 100644 --- a/External/deploy/variables.tf +++ b/External/deploy/terraform/modules/app/variables.tf @@ -2,46 +2,34 @@ variable "namespace" { type = string } -variable "kube_context" { - type = string -} - variable "image_tag" { type = string - default = "latest" } variable "image_name" { type = string - default = "edge-external-app-example" } variable "prefix" { type = string - default = "/external/default/example/" } variable "use_nodepool" { type = bool - default = true } variable "app_name" { type = string - default = "example" } variable "component_name" { type = string - default = "backend" } variable "service_port" { type = number - default = 9000 } variable "container_port" { type = number - default = 9000 } diff --git a/External/deploy/terraform/modules/istio-inject-headers/istio.tf b/External/deploy/terraform/modules/istio-inject-headers/istio.tf new file mode 100644 index 0000000..aefe4ab --- /dev/null +++ b/External/deploy/terraform/modules/istio-inject-headers/istio.tf @@ -0,0 +1,158 @@ +resource "kubernetes_manifest" "gateway_localhost" { + manifest = { + apiVersion = "networking.istio.io/v1beta1" + kind = "Gateway" + metadata = { + name = "localhost" + namespace = "istio-system" + } + + spec = { + selector = { + istio = "ingressgateway" + } + servers = [ + { + hosts = [ + "*" + ] + port = { + name = "http" + number = 80 + protocol = "HTTP" + } + }, + ] + } + } +} + +resource "kubernetes_manifest" "telemetry_deployment" { + manifest = { + apiVersion = "telemetry.istio.io/v1alpha1" + kind = "Telemetry" + metadata = { + name = "deployment" + namespace = data.kubernetes_namespace_v1.this.metadata.0.name + } + spec = { + accessLogging = [ + { + providers = [ + { + name = "envoy" + } + ] + filter = { + expression = "response.code >= 400" + } + } + ] + } + } +} + +resource "kubernetes_manifest" "authorization_policy_allow_nothing" { + manifest = { + apiVersion = "security.istio.io/v1beta1" + kind = "AuthorizationPolicy" + metadata = { + name = "allow-nothing" + namespace = data.kubernetes_namespace_v1.this.metadata.0.name + } + spec = {} + } +} + +resource "kubernetes_manifest" "authorization_policy_backend" { + manifest = { + apiVersion = "security.istio.io/v1beta1" + kind = "AuthorizationPolicy" + metadata = { + name = "${var.app_name}-${var.component_name}" + namespace = data.kubernetes_namespace_v1.this.metadata.0.name + } + spec = { + selector = { + matchLabels = { + "app.kubernetes.io/name" = var.app_name + "app.kubernetes.io/component" = var.component_name + } + } + action = "ALLOW" + rules = [ + { + from = [ + { + source = { + principals = [ + "cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account" + ] + } + } + ] + to = [ + { + operation = { + ports = [ + var.container_port + ] + } + } + ] + } + ] + } + } +} + + +resource "kubernetes_manifest" "virtualservice" { + manifest = { + apiVersion = "networking.istio.io/v1beta1" + kind = "VirtualService" + metadata = { + name = var.app_name + namespace = data.kubernetes_namespace_v1.this.metadata.0.name + } + spec = { + gateways = [ + "${kubernetes_manifest.gateway_localhost.manifest.metadata.namespace}/${kubernetes_manifest.gateway_localhost.manifest.metadata.name}" + ] + hosts = [ + "*" + ] + http = [ + { + match = [ + { + uri = { + prefix = trimsuffix(var.prefix, "/") + } + } + ] + headers = { + request = { + set = merge( + { + "X-Forwarded-For" = "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%" + }, + { for k, v in var.inject_headers : k => v } + ) + } + } + route = [ + { + destination = { + host = "${var.app_name}-${var.component_name}.${data.kubernetes_namespace_v1.this.metadata.0.name}.svc.cluster.local" + port = { + number = var.service_port + } + } + } + ] + } + ] + } + } +} diff --git a/External/deploy/terraform/modules/istio-inject-headers/main.tf b/External/deploy/terraform/modules/istio-inject-headers/main.tf new file mode 100644 index 0000000..1f3556b --- /dev/null +++ b/External/deploy/terraform/modules/istio-inject-headers/main.tf @@ -0,0 +1,27 @@ +# resource "data.kubernetes_namespace_v1" "this" { +# metadata { +# name = var.namespace +# labels = { +# istio-injection = "enabled" +# } +# } +# } + +data "kubernetes_namespace_v1" "this" { + metadata { + name = var.namespace + } +} + +resource "kubernetes_labels" "this" { + api_version = "v1" + kind = "Namespace" + + metadata { + name = data.kubernetes_namespace_v1.this.metadata.0.name + } + + labels = { + istio-injection = "enabled" + } +} diff --git a/External/deploy/terraform/modules/istio-inject-headers/terraform.tf b/External/deploy/terraform/modules/istio-inject-headers/terraform.tf new file mode 100644 index 0000000..3cffeec --- /dev/null +++ b/External/deploy/terraform/modules/istio-inject-headers/terraform.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.27" + } + } +} \ No newline at end of file diff --git a/External/deploy/terraform/modules/istio-inject-headers/variables.tf b/External/deploy/terraform/modules/istio-inject-headers/variables.tf new file mode 100644 index 0000000..dc808b4 --- /dev/null +++ b/External/deploy/terraform/modules/istio-inject-headers/variables.tf @@ -0,0 +1,28 @@ +variable "prefix" { + type = string +} + +variable "app_name" { + type = string +} + +variable "component_name" { + type = string +} + +variable "service_port" { + type = number +} + +variable "container_port" { + type = number +} + +variable "namespace" { + type = string +} + +variable "inject_headers" { + type = map(string) + default = {} +} From 0e1008ead36c43a206dc304bd0cb0185b1285afb Mon Sep 17 00:00:00 2001 From: Martin Lindner Date: Thu, 8 Aug 2024 18:05:14 +0900 Subject: [PATCH 03/20] CLN: Move Dockerfiles --- External/{ => docker}/Dockerfile | 4 ++-- External/{ => docker}/Dockerfile.devspace | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename External/{ => docker}/Dockerfile (91%) rename External/{ => docker}/Dockerfile.devspace (100%) diff --git a/External/Dockerfile b/External/docker/Dockerfile similarity index 91% rename from External/Dockerfile rename to External/docker/Dockerfile index 875923b..4624564 100644 --- a/External/Dockerfile +++ b/External/docker/Dockerfile @@ -42,8 +42,8 @@ COPY --from=stage_one --chown=app /home/app/.edm /home/app/.edm # Don't change the user, and in particular don't make the user "root". # Copy startup script and application. -COPY --chown=app ./src/startup-script.sh /home/app/startup-script.sh +COPY --chown=app ./startup-script.sh /home/app/startup-script.sh RUN chmod +x /home/app/startup-script.sh -COPY --chown=app ./src/main.py /home/app/main.py +COPY --chown=app ./main.py /home/app/main.py CMD ["/home/app/startup-script.sh"] diff --git a/External/Dockerfile.devspace b/External/docker/Dockerfile.devspace similarity index 100% rename from External/Dockerfile.devspace rename to External/docker/Dockerfile.devspace From b1fefcb8a29de114b8a6886de4a4d8c772a274f8 Mon Sep 17 00:00:00 2001 From: Martin Lindner Date: Thu, 8 Aug 2024 18:06:35 +0900 Subject: [PATCH 04/20] DEV: Adjust devspace.yaml for local deployment, cleanup --- External/devspace.yaml | 85 ++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/External/devspace.yaml b/External/devspace.yaml index 545a116..487c678 100644 --- a/External/devspace.yaml +++ b/External/devspace.yaml @@ -8,15 +8,20 @@ vars: DEVSPACE_AWS_ECR_HOST: source: env default: 594315687794.dkr.ecr.us-east-1.amazonaws.com + EDM_APP_DEPENDENCIES: "flask setuptools gunicorn" localRegistry: enabled: false pipelines: build: + flags: + - name: rebuild-zbundle run: |- build_images --all dev: + flags: + - name: rebuild-zbundle run: |- build_images --all create_deployments --all @@ -27,61 +32,63 @@ pipelines: purge_deployments --all hooks: -- name: "pre-deploy-hook" - command: |- - $( - if [ ${DEVSPACE_CONTEXT} != "docker-desktop" ] || [ ]; then - echo 'aws sts get-caller-identity --profile ${DEVSPACE_AWS_PROFILE} >/dev/null 2>&1 || aws sso login' - else - echo '' - fi - ) - events: ["before:deploy"] -- name: "pre-image-build-hook" - command: |- - $( - if ! [ -f app_environment.zbundle ]; then - echo 'edm bundle generate -i --version 3.8 --platform rh7-x86_64 -m 2.0 -f app_environment.zbundle flask setuptools gunicorn' - else - echo '' - fi - if [ ${DEVSPACE_CONTEXT} != "docker-desktop" ]; then - echo 'aws ecr get-login-password --profile ${DEVSPACE_AWS_PROFILE} | docker login --username AWS --password-stdin ${DEVSPACE_AWS_ECR_HOST}' - else - echo '' - fi - ) - events: ["before:build"] - + - name: "pre-deploy-hook" + command: |- + $!( + if [ ${DEVSPACE_CONTEXT} != "docker-desktop" ] && [ ${DEVSPACE_CONTEXT} != "minikube" ]; then + echo 'aws sts get-caller-identity --profile ${DEVSPACE_AWS_PROFILE} >/dev/null 2>&1 || aws sso login' + else + echo '' + fi + ) + events: ["before:deploy"] + - name: "pre-image-build-hook" + command: |- + $!( + if ! [ -f ./src/app_environment.zbundle ] || [ $(get_flag "rebuild-zbundle") == "true" ]; then + echo 'edm bundle generate -i --version 3.8 --platform rh7-x86_64 -m 2.0 -f ./src/app_environment.zbundle ${EDM_APP_DEPENDENCIES}' + else + echo '' + fi + if [ ${DEVSPACE_CONTEXT} != "docker-desktop" ] && [ ${DEVSPACE_CONTEXT} != "minikube" ]; then + echo 'aws sts get-caller-identity --profile ${DEVSPACE_AWS_PROFILE} >/dev/null 2>&1 || aws sso login' + echo 'aws ecr get-login-password --profile ${DEVSPACE_AWS_PROFILE} | docker login --username AWS --password-stdin ${DEVSPACE_AWS_ECR_HOST}' + else + echo '' + fi + ) + events: ["before:build"] images: edge-external-app-example: image: ${DEVSPACE_AWS_ECR_HOST}/edge-external-app-example - dockerfile: ./Dockerfile - context: . + dockerfile: ./docker/Dockerfile + context: ./src edge-external-app-example-dev: image: ${DEVSPACE_AWS_ECR_HOST}/edge-external-app-example-dev - dockerfile: ./Dockerfile.devspace - context: . - rebuildStrategy: "ignoreContextChanges" + dockerfile: ./docker/Dockerfile.devspace + context: ./src commands: terraform-init: |- - cd deploy + cd deploy/terraform/deployments/devspace terraform init + create-edm-devenv: |- + edm env create ${DEVSPACE_NAME} --version 3.8 --platform rh7-x86_64 + edm install -e ${DEVSPACE_NAME} -y ${EDM_APP_DEPENDENCIES} functions: create_deployments: |- - cd deploy + cd deploy/terraform/deployments/devspace export TF_VAR_namespace=${DEVSPACE_NAMESPACE} export TF_VAR_kube_context=${DEVSPACE_CONTEXT} export TF_VAR_image_name=$(get_image --only=image edge-external-app-example) export TF_VAR_image_tag=$(get_image --only=tag edge-external-app-example) - export TF_VAR_use_nodepool=$( [ ${DEVSPACE_CONTEXT} != "docker-desktop" ] && echo "true" || echo "false" ) + export TF_VAR_local=$( [ ${DEVSPACE_CONTEXT} == "docker-desktop" ] || [ ${DEVSPACE_CONTEXT} == "minikube" ] && echo "true" || echo "false" ) terraform apply -auto-approve purge_deployments: |- - cd deploy + cd deploy/terraform/deployments/devspace export TF_VAR_namespace=${DEVSPACE_NAMESPACE} export TF_VAR_kube_context=${DEVSPACE_CONTEXT} terraform destroy -auto-approve @@ -92,11 +99,9 @@ dev: "app.kubernetes.io/name": "example" "app.kubernetes.io/component": "backend" devImage: ${runtime.images.edge-external-app-example-dev.image}:${runtime.images.edge-external-app-example-dev.tag} - command: [ "/bin/sh", "-c" ] - args: [ "/home/app/startup-script.sh --reload" ] - ports: - - port: 9000:9000 - logs: { } + command: ["/bin/sh", "-c"] + args: ["/home/app/startup-script.sh --reload"] + logs: {} sync: - path: src/startup-script.sh:/home/app/startup-script.sh file: true From 8d33fdca8e6259b2735d751c3e45f043fee16e4e Mon Sep 17 00:00:00 2001 From: Martin Lindner Date: Thu, 8 Aug 2024 18:07:00 +0900 Subject: [PATCH 05/20] DEV: Demo app header table formatting --- External/src/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/External/src/main.py b/External/src/main.py index 47c6cd4..59a5e34 100644 --- a/External/src/main.py +++ b/External/src/main.py @@ -35,7 +35,7 @@ def root(): -

Hello, {{ headers.get('X-Forwarded-Display-Name', 'Unknown') }}!

+

Hello again, {{ headers.get('X-Forwarded-Display-Name', 'Unknown') }}!

Request Headers

@@ -44,8 +44,8 @@ def root(): {% for header, value in headers.items() %} - - + + {% endfor %}
{{ header }}{{ value }}{{ header }}{{ value }}
From 326754b769e38cbb2e5ebf5963c50f8ea29d7438 Mon Sep 17 00:00:00 2001 From: Martin Lindner Date: Fri, 9 Aug 2024 16:43:56 +0900 Subject: [PATCH 06/20] DOC: Add basic README --- External/README.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 External/README.md diff --git a/External/README.md b/External/README.md new file mode 100644 index 0000000..3afcdc3 --- /dev/null +++ b/External/README.md @@ -0,0 +1,59 @@ +# Externally hosted app example + +This example shows how to develop and deploy an externally hosted application that integrates +with Edge's upstream identity provider (Identity/Keycloak) for authentication and retrieval of user metadata. + + +## Before you begin + +Before starting, ensure you have the following installed: + +* [EDM](https://www.enthought.com/edm/), the Enthought Deployment Manager +* [Docker Desktop](https://docker.com) +* [DevSpace](https://www.devspace.sh/docs/getting-started/installation) +* [Terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli) + +For this example, your ``edm.yaml`` file should have the public ``enthought/free`` and ``enthought/lgpl`` +repositories enabled. + +The example can be deployed and run locally or on a remote Kubernetes cluster. + +### Remote deployment + +For the remote deployment, please contact DevOps, who will set up a namespace, networking, Keycloak configuration +and authentication middleware in an appropriate Kubernetes cluster for your use case. + +The team will also guide you through the process of adjusting the configuration of this example to work with the +remote deployment. + +### Local deployment + +The local deployment options relies on a local Kubernetes cluster, such as Docker Desktop's built-in option. +You can enable the Kubernetes feature in Docker Desktop under Settings -> Kubernetes -> Enable Kubernetes. + +User metadata is passed to the application via HTTP headers. For the local deployment, we are mocking the headers +by injecting test user metadata into incoming requests via Istio. + +After enabling Kubernetes in Docker Desktop, you will need to install Istio. +[Installing Istio's default profile](https://istio.io/latest/docs/setup/install/istioctl/) is sufficient for this example. + +## Quick start + +The following steps will guide you through the process of deploying the example app locally. + +1. Make sure that your Kubenetes context is pointing to the local cluster by running ``devspace use context docker-desktop``. + +2. Run ``devspace run terraform-init`` to initialize the Terraform workspace that will deploy the application resources into your local Kubernetes cluster. + +3. **Optionally**, run ``devspace run create-edm-devenv`` to create a development environment in EDM. This will create a new EDM environment called ``edge-externally-hosted-app`` and install the required dependencies. Note that is only meant to provide a development environment for your IDE and is not required for the application to run. + +4. Run ``devspace dev`` to start the application in development mode. This will build the Docker image, deploy the application and set up a port-forward to access it. +You should now be able to access the application at [http://localhost:8080/external/app/example](http://localhost:8080/external/app/example). + +You can now start developing your application. The application is set up to sync changes to the source code and automatically reload. Application logs are streamed to the terminal. + +To stop the sync and the application, press `Ctrl+C` in the terminal where `devspace dev` is running. + +## Cleaning up + +To clean up the resources created by the example, run ``devspace purge``. From 18e71011497cca42e10f0489a1ef861fa48d1da9 Mon Sep 17 00:00:00 2001 From: Martin Lindner Date: Fri, 9 Aug 2024 16:44:15 +0900 Subject: [PATCH 07/20] DEV: Add istio-ingress port-forward --- External/devspace.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/External/devspace.yaml b/External/devspace.yaml index 487c678..1a4d610 100644 --- a/External/devspace.yaml +++ b/External/devspace.yaml @@ -8,6 +8,7 @@ vars: DEVSPACE_AWS_ECR_HOST: source: env default: 594315687794.dkr.ecr.us-east-1.amazonaws.com + DEVSPACE_FLAGS: "-n ${DEVSPACE_NAME}" EDM_APP_DEPENDENCIES: "flask setuptools gunicorn" localRegistry: @@ -118,3 +119,17 @@ dev: startContainer: true disableDownload: true printLogs: true + istio-ingressgateway: |- + $( + if [ ${DEVSPACE_CONTEXT} == "docker-desktop" ] && [ ${DEVSPACE_CONTEXT} == "minikube" ]; then + echo "null" + else + output=' + labelSelector: + istio: ingressgateway + namespace: istio-system + ports: + - port: "8080:8080"' + echo "$output" + fi + ) From 219394d05a64707c5242c8b7f72d47afbb546923 Mon Sep 17 00:00:00 2001 From: Martin Lindner Date: Fri, 9 Aug 2024 18:26:44 +0900 Subject: [PATCH 08/20] FIX: Ingress gateway port-forward conditional --- External/devspace.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/External/devspace.yaml b/External/devspace.yaml index 1a4d610..db41edb 100644 --- a/External/devspace.yaml +++ b/External/devspace.yaml @@ -121,7 +121,7 @@ dev: printLogs: true istio-ingressgateway: |- $( - if [ ${DEVSPACE_CONTEXT} == "docker-desktop" ] && [ ${DEVSPACE_CONTEXT} == "minikube" ]; then + if [ ${DEVSPACE_CONTEXT} != "docker-desktop" ] && [ ${DEVSPACE_CONTEXT} != "minikube" ]; then echo "null" else output=' From 40bccbc34d3d9d0190fe0ba872646c6172b87daf Mon Sep 17 00:00:00 2001 From: Martin Lindner Date: Fri, 9 Aug 2024 20:42:51 +0900 Subject: [PATCH 09/20] STY: Add missing newlines at EOF --- External/deploy/terraform/deployments/devspace/terraform.tf | 2 +- External/deploy/terraform/modules/app/terraform.tf | 2 +- .../deploy/terraform/modules/istio-inject-headers/terraform.tf | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/External/deploy/terraform/deployments/devspace/terraform.tf b/External/deploy/terraform/deployments/devspace/terraform.tf index 3cffeec..1a9b345 100644 --- a/External/deploy/terraform/deployments/devspace/terraform.tf +++ b/External/deploy/terraform/deployments/devspace/terraform.tf @@ -7,4 +7,4 @@ terraform { version = "~> 2.27" } } -} \ No newline at end of file +} diff --git a/External/deploy/terraform/modules/app/terraform.tf b/External/deploy/terraform/modules/app/terraform.tf index 3cffeec..1a9b345 100644 --- a/External/deploy/terraform/modules/app/terraform.tf +++ b/External/deploy/terraform/modules/app/terraform.tf @@ -7,4 +7,4 @@ terraform { version = "~> 2.27" } } -} \ No newline at end of file +} diff --git a/External/deploy/terraform/modules/istio-inject-headers/terraform.tf b/External/deploy/terraform/modules/istio-inject-headers/terraform.tf index 3cffeec..1a9b345 100644 --- a/External/deploy/terraform/modules/istio-inject-headers/terraform.tf +++ b/External/deploy/terraform/modules/istio-inject-headers/terraform.tf @@ -7,4 +7,4 @@ terraform { version = "~> 2.27" } } -} \ No newline at end of file +} From 2f96d0686802caabded0a8da2a61e60274a38e5c Mon Sep 17 00:00:00 2001 From: Martin Lindner Date: Fri, 9 Aug 2024 20:43:23 +0900 Subject: [PATCH 10/20] FIX: Wording --- External/src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/External/src/main.py b/External/src/main.py index 59a5e34..3544d7a 100644 --- a/External/src/main.py +++ b/External/src/main.py @@ -35,7 +35,7 @@ def root(): -

Hello again, {{ headers.get('X-Forwarded-Display-Name', 'Unknown') }}!

+

Hello {{ headers.get('X-Forwarded-Display-Name', 'Unknown') }}!

Request Headers

From 3072e6230eaa39a8686062d2a642ce40cdd8e623 Mon Sep 17 00:00:00 2001 From: Martin Lindner Date: Fri, 9 Aug 2024 22:18:36 +0900 Subject: [PATCH 11/20] CLN: Replace default vars with placeholders --- External/devspace.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/External/devspace.yaml b/External/devspace.yaml index db41edb..8f4b3e0 100644 --- a/External/devspace.yaml +++ b/External/devspace.yaml @@ -4,10 +4,10 @@ name: edge-external-app-example vars: DEVSPACE_AWS_PROFILE: source: env - default: AdministratorAccess-594315687794 + default: PlaceholderProfile-000000000000 DEVSPACE_AWS_ECR_HOST: source: env - default: 594315687794.dkr.ecr.us-east-1.amazonaws.com + default: 000000000000.dkr.ecr.us-east-1.amazonaws.com DEVSPACE_FLAGS: "-n ${DEVSPACE_NAME}" EDM_APP_DEPENDENCIES: "flask setuptools gunicorn" From 3b0c5d3dc8bf80c1f5a4a58ab428cf8b0bf25d9e Mon Sep 17 00:00:00 2001 From: Martin Lindner Date: Mon, 19 Aug 2024 20:03:39 +0900 Subject: [PATCH 12/20] DOC: PR review suggestions, add Minikube instructions. --- External/README.md | 46 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/External/README.md b/External/README.md index 3afcdc3..70f7f86 100644 --- a/External/README.md +++ b/External/README.md @@ -1,15 +1,19 @@ -# Externally hosted app example +# Kubernetes hosted app example -This example shows how to develop and deploy an externally hosted application that integrates -with Edge's upstream identity provider (Identity/Keycloak) for authentication and retrieval of user metadata. +This example demonstrates how to develop and deploy a Kubernetes hosted application alongside Enthought Edge. +It is designed to integrate with the authentication, monitoring, logging and scaling tooling available +on Enthought-managed Kubernetes clusters, while retrieving user metadata from the upstream identity provider +(Identity/Keycloak) shared with Edge. ## Before you begin Before starting, ensure you have the following installed: * [EDM](https://www.enthought.com/edm/), the Enthought Deployment Manager -* [Docker Desktop](https://docker.com) +* A local Docker installation for building container images and hosting a Kubernetes cluster (for local deployment): + * [Docker Desktop](https://docs.docker.com/desktop/) or + * [Minikube](https://minikube.sigs.k8s.io/docs/start/) * [DevSpace](https://www.devspace.sh/docs/getting-started/installation) * [Terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli) @@ -18,6 +22,29 @@ repositories enabled. The example can be deployed and run locally or on a remote Kubernetes cluster. +### Local deployment + +The local deployment option relies on a local Kubernetes cluster and has been tested with Docker Desktop's built-in Kubernetes feature and with Minikube. + +User metadata is passed to the application via HTTP headers. For the local deployment, we are mocking the headers +by injecting test user metadata into incoming requests via Istio. + +#### Docker Desktop + +For Docker Desktop, you will need to perform the following steps: + +1. Make sure that Docker Desktop has been installed and is running. +2. Enable the built-in Kubernetes feature via Settings -> Kubernetes -> Enable Kubernetes. +3. Install Istio. [Istio's default profile](https://istio.io/latest/docs/setup/install/istioctl/#install-istio-using-the-default-profile) is sufficient for this example. + +#### Minikube + +1. Make sure that the Minikube CLI has been installed. +2. Start a Minikube cluster (with Istio) by running `minikube start --addons="istio-provisioner,istio"` + +> [!NOTE] +> Minikube will automatically try to detect the appropriate driver for your system. If you want to use a specific driver, you can specify it with the `--driver` flag. See the [Minikube documentation](https://minikube.sigs.k8s.io/docs/start/) for more information. We have successfully tested this example with the `docker` driver, `hyper-v` driver on Windows and `hyperkit` driver on MacOS. + ### Remote deployment For the remote deployment, please contact DevOps, who will set up a namespace, networking, Keycloak configuration @@ -26,16 +53,7 @@ and authentication middleware in an appropriate Kubernetes cluster for your use The team will also guide you through the process of adjusting the configuration of this example to work with the remote deployment. -### Local deployment - -The local deployment options relies on a local Kubernetes cluster, such as Docker Desktop's built-in option. -You can enable the Kubernetes feature in Docker Desktop under Settings -> Kubernetes -> Enable Kubernetes. - -User metadata is passed to the application via HTTP headers. For the local deployment, we are mocking the headers -by injecting test user metadata into incoming requests via Istio. - -After enabling Kubernetes in Docker Desktop, you will need to install Istio. -[Installing Istio's default profile](https://istio.io/latest/docs/setup/install/istioctl/) is sufficient for this example. +Remote deployments will use the actual user metadata provided by Identity/Keycloak and therefore share a login session with Edge. ## Quick start From 6ff9de067ff4481fb71d5cfeea9244960295fdfb Mon Sep 17 00:00:00 2001 From: Martin Lindner Date: Mon, 19 Aug 2024 20:07:09 +0900 Subject: [PATCH 13/20] DEV: Remove external strings from demo app --- External/src/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/External/src/main.py b/External/src/main.py index 3544d7a..7f2b044 100644 --- a/External/src/main.py +++ b/External/src/main.py @@ -7,7 +7,7 @@ # Distribution is prohibited. """ - Example Flask application (external). + Example Flask application. """ import os @@ -24,7 +24,7 @@ def root(): html = """ - External Example Flask application + Example Flask application