Commit 59b150d8 authored by Pierre Smeyers's avatar Pierre Smeyers
Browse files

Merge branch 'feat/slim' into 'main'

feat: Reduced image size from 79 MB to 24 MB (-69%)

See merge request to-be-continuous/tools/gcp-auth-provider!64
parents 2b9a94c6 6a050acc
Loading
Loading
Loading
Loading
+9 −4
Original line number Diff line number Diff line
include:
  # $TBC_NAMESPACE is a group variable; can be globally overridden
  - project: "$TBC_NAMESPACE/docker"
    ref: "5.6"
    ref: "5.8.2"
    file: "templates/gitlab-ci-docker.yml"
  - project: "$TBC_NAMESPACE/python"
    ref: "6.5"
    ref: "6.7"
    file: "/templates/gitlab-ci-python.yml"

stages:
@@ -51,12 +51,17 @@ variables:
  function assert_eq() {
    local expected="$1"
    local actual="$2"
    local error_msg="$3"

    if [ "$expected" == "$actual" ]; then
      log_info "$expected == $actual"
      return 0
    else
      if [ -z "$error_msg" ]; then
        fail "$expected == $actual"
      else
        fail "$expected == $actual  msg: $error_msg"
      fi
      return 1
    fi
  }
@@ -125,7 +130,7 @@ test-token-succeeds:
  script:
    - |
      response_status=$(curl -s -o "resp.txt" -w "%{http_code}" "http://gcp-auth-provider/token")
      assert_eq "200" $response_status
      assert_eq "200" $response_status "$(cat resp.txt)"
      token=$(cat resp.txt)

      response_status=$(curl -s -o resp.txt -w "%{http_code}" -H "Authorization: Bearer $token" "https://cloudresourcemanager.googleapis.com/v1/projects/$GCP_PROJECT")
+6 −21
Original line number Diff line number Diff line
FROM registry.hub.docker.com/library/python:3.12 as requirements-stage
 
WORKDIR /tmp

# hadolint ignore=DL3013
RUN pip install --no-cache-dir poetry

COPY ./pyproject.toml ./poetry.lock* /tmp/

RUN poetry export -f requirements.txt --output requirements.txt --without-hashes

###########

FROM registry.hub.docker.com/library/python:3.11-slim-buster
FROM registry.hub.docker.com/library/python:3.12-alpine

ENV PORT=80
WORKDIR /code

COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt

RUN apt-get -y update && apt-get -y upgrade \
    &&  rm -rf /var/lib/apt/lists/* \
    && pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY pyproject.toml poetry.lock README.md /code/
COPY ./gcp_auth_provider /code/gcp_auth_provider

COPY ./app /code/app
RUN apk upgrade --no-cache  \
    && pip install --no-cache-dir .

EXPOSE ${PORT}
# hadolint ignore=DL3025
CMD uvicorn app.main:app --host=0.0.0.0 --port=${PORT}
CMD uvicorn gcp_auth_provider.main:app --host=0.0.0.0 --port=${PORT}
+0 −0

File moved.

+55 −0
Original line number Diff line number Diff line
import requests, json, os
from fastapi import HTTPException
import os

JWT_TOKEN = os.environ.get('GCP_JWT') or os.environ.get('CI_JOB_JWT_V2')
import certifi
import urllib3
from starlette.exceptions import HTTPException

http = urllib3.PoolManager(cert_reqs="CERT_REQUIRED", ca_certs=certifi.where())

JWT_TOKEN = os.environ.get("GCP_JWT") or os.environ.get("CI_JOB_JWT_V2")


def get_iam_credentials(service_account, federated_token):
    resp = requests.request(
        method='POST',
        url='https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken' % service_account,
    resp = urllib3.request(
        method="POST",
        url=f"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{service_account}:generateAccessToken",
        headers={
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'Authorization': 'Bearer %s' % federated_token
            "Accept": "application/json",
            "Content-Type": "application/json",
            "Authorization": f"Bearer {federated_token}",
        },
        data=json.dumps({
            "scope": ["https://www.googleapis.com/auth/cloud-platform"]
        })
        json={"scope": ["https://www.googleapis.com/auth/cloud-platform"]},
    )
    if resp.status_code != 200:
    if resp.status != 200:
        raise HTTPException(
            status_code=500,
            detail=f'Failed to get iam credential token: {resp.text}'
            detail=f"Failed to get iam credential token for service_account={service_account} msg: {resp.json()}",
        )
    return resp.json()['accessToken']
    return resp.json()["accessToken"]


def get_sts_token(audience):
    if not JWT_TOKEN:
        raise HTTPException(
            status_code=401,
            detail='Missing $CI_JOB_JWT_V2 token'
            status_code=401, detail="Missing $CI_JOB_JWT_V2 or $GCP_JWT token"
        )

    resp = requests.request(
        method='POST',
        url='https://sts.googleapis.com/v1/token',
        headers={
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        data=json.dumps({
    resp = urllib3.request(
        method="POST",
        url="https://sts.googleapis.com/v1/token",
        headers={"Accept": "application/json", "Content-Type": "application/json"},
        json={
            "audience": audience,
            "grantType": "urn:ietf:params:oauth:grant-type:token-exchange",
            "requestedTokenType": "urn:ietf:params:oauth:token-type:access_token",
            "scope": "https://www.googleapis.com/auth/cloud-platform",
            "subjectTokenType": "urn:ietf:params:oauth:token-type:jwt",
            "subjectToken": JWT_TOKEN
        })
            "subjectToken": JWT_TOKEN,
        },
    )
    if resp.status_code != 200:
    if resp.status != 200:
        raise HTTPException(
            status_code=500,
            detail=f'Failed to get sts token: {resp.text}'
            detail=f"Failed to get sts token for audience={audience} msg: {resp.json()}",
        )
    return resp.json()['access_token']
    return resp.json()["access_token"]
+35 −20
Original line number Diff line number Diff line
import os
import re

from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import PlainTextResponse
from starlette.applications import Starlette
from starlette.exceptions import HTTPException
from starlette.requests import Request
from starlette.responses import PlainTextResponse
from starlette.routing import Route
import uvicorn

from app.gcp_client import get_iam_credentials, get_sts_token
from gcp_auth_provider.gcp_client import get_iam_credentials, get_sts_token

app = FastAPI()
# app = FastAPI()


def guess_env_type() -> str:
    env_type = os.getenv('ENV_TYPE')
    env_type = os.getenv("ENV_TYPE")
    if env_type:
        return env_type
    # guess from GitLab CI predefined vars
@@ -52,19 +56,15 @@ def get_oidc_provider(var_prefix: str) -> str:
    )


@app.get("/ping", response_class=PlainTextResponse)
def ping():
    return "pong"
def ping(request: Request):
    return PlainTextResponse("pong")


@app.get("/token", response_class=PlainTextResponse)
def token(
    workload_identity_provider: str = Query(
        default=None, alias="workloadIdentityProvider"
    ),
    service_account: str = Query(default=None, alias="serviceAccount"),
    env_type: str = Query(default=None, alias="envType"),
):
def token(request: Request):
    workload_identity_provider = request.query_params.get("workloadIdentityProvider")
    service_account = request.query_params.get("serviceAccount")
    env_type = request.query_params.get("envType")

    # projects/%s/locations/global/workloadIdentityPools/%s/providers/%s
    if (not workload_identity_provider) or (not service_account):
        # retrieve from TBC standard variables
@@ -78,12 +78,27 @@ def token(
        if (not workload_identity_provider) or (not service_account):
            raise HTTPException(
                status_code=400,
                detail=f"Couldn't retrieve implicit OIDC provider/account from env for '{env_type}'",
                detail=f"Token couldn't retrieve implicit OIDC provider/account for env='{env_type}', workloadIdentityProvider={workload_identity_provider}, service=Account{service_account}",
            )

    audience = "//iam.googleapis.com/%s" % workload_identity_provider
    audience = f"//iam.googleapis.com/{workload_identity_provider}"

    federated_token = get_sts_token(audience)
    gcloud_auth_token = get_iam_credentials(service_account, federated_token)

    return gcloud_auth_token
    return PlainTextResponse(gcloud_auth_token)


def startup():
    print("Ready to go")


routes = [
    Route("/ping", ping),
    Route("/token", token),
]

app = Starlette(debug=False, routes=routes, on_startup=[startup])

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)
 No newline at end of file
Loading