diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..998a4fe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.github +setup/terraform/.terraform* diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..df98aa2 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,53 @@ +name: ci +on: + push: + +jobs: + run: + runs-on: ubuntu-latest + steps: + + # Prepare + + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Humanitec CLI + uses: humanitec/setup-cli-action@v1 + with: + version: "0.21.1" + - name: Setup tflit + uses: terraform-linters/setup-tflint@v4 + with: + tflint_version: v0.49.0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Validate + + - name: Lint + run: make lint + - name: Simulate a humctl login + run: | + yq e -n '.token = "'"${HUMANITEC_TOKEN}"'"' > ~/.humctl + env: + HUMANITEC_TOKEN: ${{ secrets.HUMANITEC_TOKEN }} + - name: Test + run: make test + env: + HUMANITEC_ORG: ${{ secrets.HUMANITEC_ORG }} + + # Push + + - if: github.ref == 'refs/heads/main' + name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - if: github.ref == 'refs/heads/main' + name: Push image + run: make push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c762e2c --- /dev/null +++ b/.gitignore @@ -0,0 +1,94 @@ +kube/* + +# Created by https://www.toptal.com/developers/gitignore/api/macos,visualstudiocode,terraform +# Edit at https://www.toptal.com/developers/gitignore?templates=macos,visualstudiocode,terraform + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Terraform ### +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/macos,visualstudiocode,terraform \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0c05308 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "yaml.schemas": { + "https://raw.githubusercontent.com/score-spec/schema/main/score-v1b1.json": "score.yaml", + "https://json.schemastore.org/yamllint.json": "setup/kind/cluster.yaml" + } +} \ No newline at end of file diff --git a/0_install.sh b/0_install.sh new file mode 100755 index 0000000..1c80af0 --- /dev/null +++ b/0_install.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +set -eo pipefail + +mkdir -p /state/kube + +# 1. Create registry container unless it already exists +reg_name='kind-registry' +reg_port='5001' +if [ "$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" != 'true' ]; then + docker run \ + -d --restart=always -p "127.0.0.1:${reg_port}:5000" --network bridge --name "${reg_name}" \ + registry:2 +fi + +# 2. Create Kind cluster +if [ ! -f /state/kube/config.yaml ]; then + kind create cluster -n 5min-idp --kubeconfig /state/kube/config.yaml --config ./setup/kind/cluster.yaml +fi + +# connect current container to the kind network +container_name="5min-idp" +if [ "$(docker inspect -f='{{json .NetworkSettings.Networks.kind}}' "${container_name}")" = 'null' ]; then + docker network connect "kind" "${container_name}" +fi + +# used by humanitec-agent / inside docker to reach the cluster +kubeconfig_docker=/state/kube/config-internal.yaml +kind export kubeconfig --internal -n 5min-idp --kubeconfig "$kubeconfig_docker" + +### Export needed env-vars for terraform +export TF_VAR_humanitec_org=$HUMANITEC_ORG +# Aim for service user if present, otherwise use current user token (max 24h validity) +if [ -z "$HUMANITEC_SERVICE_USER" ]; then + export TF_VAR_humanitec_token=$HUMANITEC_SERVICE_USER +else + export TF_VAR_humanitec_token=$(yq -r '.token' ~/.humctl) +fi +# Variables for TLS in Terraform +export TF_VAR_tls_ca_cert=$TLS_CA_CERT +export TF_VAR_tls_cert_string=$TLS_CERT_STRING +export TF_VAR_tls_key_string=$TLS_KEY_STRING +# Kubeconfig for Terraform +export TF_VAR_kubeconfig=$kubeconfig_docker + +terraform -chdir=setup/terraform init -upgrade +terraform -chdir=setup/terraform apply -auto-approve + +# Create Gitea Runner for Actions CI +RUNNER_TOKEN="" +while [[ -z $RUNNER_TOKEN ]]; do + response=$(curl -k -s -X 'GET' 'https://5min-idp-control-plane/api/v1/admin/runners/registration-token' -H 'accept: application/json' -H 'authorization: Basic NW1pbmFkbWluOjVtaW5hZG1pbg==') + if [[ $response == *"token"* ]]; then + RUNNER_TOKEN=$(echo $response | jq -r '.token') + fi + sleep 1 +done + +# Start Gitea Runner +docker volume create gitea_runner_data +docker create \ + --name gitea_runner \ + -v gitea_runner_data:/data \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e CONFIG_FILE=/config.yaml \ + -e GITEA_INSTANCE_URL=https://5min-idp-control-plane \ + -e GITEA_RUNNER_REGISTRATION_TOKEN=$RUNNER_TOKEN \ + -e GITEA_RUNNER_NAME=local \ + -e GITEA_RUNNER_LABELS=local \ + --network kind \ + gitea/act_runner:latest +sed 's|###ca-certficates.crt###|'"$TLS_CA_CERT"'|' setup/gitea/config.yaml > setup/gitea/config.done.yaml +docker cp setup/gitea/config.done.yaml gitea_runner:/config.yaml +docker start gitea_runner + +# Create Gitea org and Backstage clone with configuration +curl -k -X 'POST' \ + 'https://5min-idp-control-plane/api/v1/orgs' \ + -H 'accept: application/json' \ + -H 'authorization: Basic NW1pbmFkbWluOjVtaW5hZG1pbg==' \ + -H 'Content-Type: application/json' \ + -d '{ + "repo_admin_change_team_access": true, + "username": "5minorg", + "visibility": "public" +}' +curl -k -X 'POST' \ + 'https://5min-idp-control-plane/api/v1/repos/migrate' \ + -H 'accept: application/json' \ + -H 'authorization: Basic NW1pbmFkbWluOjVtaW5hZG1pbg==' \ + -H 'Content-Type: application/json' \ + -d '{ + "clone_addr": "https://github.com/humanitec-architecture/backstage.git", + "mirror": false, + "private": false, + "repo_name": "backstage", + "repo_owner": "5minorg" +}' +curl -k -X 'POST' \ + 'https://5min-idp-control-plane/api/v1/orgs/5minorg/actions/variables/CLOUD_PROVIDER' \ + -H 'accept: application/json' \ + -H 'authorization: Basic NW1pbmFkbWluOjVtaW5hZG1pbg==' \ + -H 'Content-Type: application/json' \ + -d '{ + "value": "5min" +}' +curl -k -X 'POST' \ + 'https://5min-idp-control-plane/api/v1/orgs/5minorg/actions/variables/HUMANITEC_ORG_ID' \ + -H 'accept: application/json' \ + -H 'authorization: Basic NW1pbmFkbWluOjVtaW5hZG1pbg==' \ + -H 'Content-Type: application/json' \ + -d '{ + "value": "'$HUMANITEC_ORG'" +}' +humanitec_app_backstage=$(terraform -chdir=setup/terraform output -raw humanitec_app_backstage) +curl -k -X 'POST' \ + 'https://5min-idp-control-plane/api/v1/orgs/5minorg/actions/variables/HUMANITEC_APP_ID' \ + -H 'accept: application/json' \ + -H 'authorization: Basic NW1pbmFkbWluOjVtaW5hZG1pbg==' \ + -H 'Content-Type: application/json' \ + -d '{ + "value": "'$humanitec_app_backstage'" +}' +### TODO -> Use from env if present instead of extracting +curl -k -X 'PUT' \ + 'https://5min-idp-control-plane/api/v1/orgs/5minorg/actions/secrets/HUMANITEC_TOKEN' \ + -H 'accept: application/json' \ + -H 'authorization: Basic NW1pbmFkbWluOjVtaW5hZG1pbg==' \ + -H 'Content-Type: application/json' \ + -d '{ + "data": "'$TF_VAR_humanitec_token'" +}' + +# 3. Add the registry config to the nodes +# +# This is necessary because localhost resolves to loopback addresses that are +# network-namespace local. +# In other words: localhost in the container is not localhost on the host. +# +# We want a consistent name that works from both ends, so we tell containerd to +# alias localhost:${reg_port} to the registry container when pulling images +REGISTRY_DIR="/etc/containerd/certs.d/localhost:${reg_port}" +for node in $(kind get nodes -n 5min-idp); do + docker exec "${node}" mkdir -p "${REGISTRY_DIR}" + cat <>>> Everything prepared, ready to deploy application." diff --git a/1_demo.sh b/1_demo.sh new file mode 100755 index 0000000..a7d23b6 --- /dev/null +++ b/1_demo.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -eo pipefail + +echo "Deploying workload" + +humanitec_app=$(terraform -chdir=setup/terraform output -raw humanitec_app) + +humctl score deploy --app "$humanitec_app" --env 5min-local -f ./score.yaml --wait + +workload_host=$(humctl get active-resources --app "$humanitec_app" --env 5min-local -o yaml | yq '.[] | select(.metadata.type == "route") | .status.resource.host') + +echo "Waiting for workload to be available" + +# manually change the host here as the workload host resolves to localhost, which is not reachable from the container +if curl -I --retry 30 --retry-delay 3 --retry-all-errors --fail \ + --connect-to "$workload_host:30080:5min-idp-control-plane:30080" \ + "http://$workload_host:30080"; then + echo "Workload available at: http://$workload_host:30080" +else + echo "Workload not available" + kubectl get pods --all-namespaces + kubectl -n "$humanitec_app-development" logs deployment/hello-world + exit 1 +fi diff --git a/2_cleanup.sh b/2_cleanup.sh new file mode 100755 index 0000000..c3f9a07 --- /dev/null +++ b/2_cleanup.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -eo pipefail + +# connect current container to the kind network +container_name="5min-idp" +if [ "$(docker inspect -f='{{json .NetworkSettings.Networks.kind}}' "${container_name}")" = 'null' ]; then + docker network connect "kind" "${container_name}" +fi + +kubeconfig_docker=/state/kube/config-internal.yaml + +export TF_VAR_humanitec_org=$HUMANITEC_ORG +export TF_VAR_kubeconfig=$kubeconfig_docker + +terraform -chdir=setup/terraform init -upgrade +terraform -chdir=setup/terraform destroy -auto-approve + +kind delete cluster -n 5min-idp + +docker stop gitea_runner +docker rm gitea_runner +docker volume rm gitea_runner_data + +docker stop kind-registry +docker rm kind-registry + +rm -rf /state/kube diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..970778a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +FROM alpine:3.19 + +LABEL org.opencontainers.image.source https://github.com/humanitec-tutorials/5min-idp + +RUN apk add --no-cache \ + bash curl git jq bash-completion docker-cli && \ + mkdir -p /etc/bash_completion.d + +# inject the target architecture (https://docs.docker.com/reference/dockerfile/#automatic-platform-args-in-the-global-scope) +ARG TARGETARCH + +# install kubectl +RUN curl -fsSL "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/$TARGETARCH/kubectl" > /tmp/kubectl && \ + install -o root -g root -m 0755 /tmp/kubectl /usr/local/bin/kubectl && \ + kubectl completion bash > /etc/bash_completion.d/kubectl && \ + rm /tmp/kubectl + +# install helm (https://github.com/helm/helm/releases) +RUN mkdir /tmp/helm && \ + curl -fsSL https://get.helm.sh/helm-v3.14.4-linux-${TARGETARCH}.tar.gz > /tmp/helm/helm.tar.gz && \ + tar -zxvf /tmp/helm/helm.tar.gz -C /tmp/helm && \ + install -o root -g root -m 0755 /tmp/helm/linux-${TARGETARCH}/helm /usr/local/bin/helm && \ + helm completion bash > /etc/bash_completion.d/helm && \ + rm -rf /tmp/helm + +# install kind https://kind.sigs.k8s.io/docs/user/quick-start/#installing-from-release-binaries +RUN curl -fsSL https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-${TARGETARCH} > /tmp/kind && \ + install -o root -g root -m 0755 /tmp/kind /usr/local/bin/kind && \ + rm /tmp/kind + +# install terraform (https://github.com/hashicorp/terraform/releases) +RUN mkdir /tmp/terraform && \ + curl -fsSL https://releases.hashicorp.com/terraform/1.8.1/terraform_1.8.1_linux_${TARGETARCH}.zip > /tmp/terraform/terraform.zip && \ + unzip /tmp/terraform/terraform.zip -d /tmp/terraform && \ + install -o root -g root -m 0755 /tmp/terraform/terraform /usr/local/bin/terraform && \ + rm -rf /tmp/terraform + +# install yq +RUN curl -fsSL https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${TARGETARCH} > /tmp/yq && \ + install -o root -g root -m 0755 /tmp/yq /usr/local/bin/yq && \ + yq shell-completion bash > /etc/bash_completion.d/yq && \ + rm /tmp/yq + +# install humctl (https://github.com/humanitec/cli/releases) +RUN mkdir /tmp/humctl && \ + curl -fsSL https://github.com/humanitec/cli/releases/download/v0.23.0/cli_0.23.0_linux_${TARGETARCH}.tar.gz > /tmp/humctl/humctl.tar.gz && \ + tar -zxvf /tmp/humctl/humctl.tar.gz -C /tmp/humctl && \ + install -o root -g root -m 0755 /tmp/humctl/humctl /usr/local/bin/humctl && \ + humctl completion bash > /etc/bash_completion.d/humctl && \ + rm -rf /tmp/humctl + +ENV KUBECONFIG="/state/kube/config-internal.yaml" + +COPY . /app + +WORKDIR /app + +ENTRYPOINT ["/bin/bash"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..be30ce0 --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +IMG_TAG ?= latest +IMG ?= ghcr.io/humanitec-tutorials/5min-idp:$(IMG_TAG) +PLATFORM ?= linux/amd64,linux/arm64 + +# Build the 5min-idp image +build: + docker buildx build --platform $(PLATFORM) -t $(IMG) . + # Ideally we could remove the next step, but docker on GHA doesn't support + # loading multi-platform builds yet + docker buildx build -t $(IMG) --load . + +# Check the 5min-idp image +check-image: + docker run --rm -v $(PWD):/app $(IMG) ./image/check.sh + +# Push the 5min-idp image +push: + docker buildx build --platform $(PLATFORM) -t $(IMG) --push . + +# Initialize tflint +lint-init: + tflint --init + +# Lint terraform directory +lint: lint-init + tflint --config ../.tflint.hcl --chdir=./setup/terraform + +# Test the 5min-idp +test: build check-image + docker run --rm -i -h 5min-idp --name 5min-idp \ + -e HUMANITEC_ORG \ + -v hum-5min-idp:/state \ + -v $(HOME)/.humctl:/root/.humctl \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --network bridge \ + $(IMG) ./image/test.sh + +# Run the locally built image +run-local: build + docker run --rm -it -h 5min-idp --name 5min-idp \ + -e HUMANITEC_ORG \ + -e HUMANITEC_SERVICE_USER \ + -e TLS_CA_CERT \ + -e TLS_CERT_STRING \ + -e TLS_KEY_STRING \ + -v hum-5min-idp:/state \ + -v $(HOME)/.humctl:/root/.humctl \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --network bridge \ + $(IMG) +# Run the locally built image and don't delete the state +run-local-persistent: build + docker run -it -h 5min-idp --name 5min-idp \ + -e HUMANITEC_ORG \ + -e HUMANITEC_SERVICE_USER \ + -e TLS_CA_CERT \ + -e TLS_CERT_STRING \ + -e TLS_KEY_STRING \ + -v hum-5min-idp:/state \ + -v $(HOME)/.humctl:/root/.humctl \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --network bridge \ + $(IMG) + +# Re-do the Humanitec token in a running 5min-IDP +.PHONY: renew-token +renew-token: + humctl login -v9 + docker cp ~/.humctl 5min-idp:/root/.humctl diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e2311f --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Pocket IDP + +Your Humanitec Internal Developer Platform (IDP) demo environment in less than five minutes. + +The material in this project provides the tooling for the ["Five-minute IDP"](https://developer.humanitec.com/introduction/getting-started/the-five-minute-idp/) getting started guide in the Humanitec developer docs. Please refer to that guide for usage instructions. + +For the pocket IDP you need to prepare a few more things on top of the 5min-IDP flow. + +1. Create a local CA and sign a certificate that you can provide + + See [mkcert installation]([Install](https://github.com/FiloSottile/mkcert?tab=readme-ov-file#installation)) + ```shell + brew install mkcert + mkcert -install + mkcert 5min-idp 5min-idp-control-plane kubernetes.docker.internal localhost 127.0.0.1 ::1 + ``` + +1. Populate environment variables + + See [direnv installation](https://direnv.net/#basic-installation) + + ```shell + brew install direnv + HUMANITEC_ORG="" #set me + HUMANITEC_SERVICE_USER="" #set token + TLS_CA_CERT="" #Export CA in PEM format and set here + TLS_CERT_STRING="" #Your cert in base64 encoded format + TLS_KEY_STRING="" #Your key in base64 encoded format + humctl login + direnv allow + ``` + +2. Run the PocketIDP + + ```shell + #For the non-prebuilt container + gh clone jayonthenet/5min-idp + git checkout pocketidp + make run-local + ``` + diff --git a/image/check.sh b/image/check.sh new file mode 100755 index 0000000..df3b5fe --- /dev/null +++ b/image/check.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -exo pipefail + +kubectl version --client +helm version +kind version +terraform version +humctl version diff --git a/image/test.sh b/image/test.sh new file mode 100755 index 0000000..97e1a8f --- /dev/null +++ b/image/test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -exo pipefail + +./0_install.sh +./1_demo.sh +./2_cleanup.sh diff --git a/score.yaml b/score.yaml new file mode 100644 index 0000000..4a95c88 --- /dev/null +++ b/score.yaml @@ -0,0 +1,32 @@ +# Based on https://github.com/score-spec/sample-score-app + +apiVersion: score.dev/v1b1 +metadata: + name: hello-world +service: + ports: + www: + port: 8080 + targetPort: 3000 +containers: + hello-world: + image: ghcr.io/score-spec/sample-score-app + variables: + PORT: "3000" + MESSAGE: "Hello, World!" + DB_DATABASE: ${resources.db.name} + DB_USER: ${resources.db.username} + DB_PASSWORD: ${resources.db.password} + DB_HOST: ${resources.db.host} + DB_PORT: ${resources.db.port} +resources: + dns: + type: dns + route: + type: route + params: + host: ${resources.dns.host} + path: / + port: 8080 + db: + type: postgres diff --git a/setup/.tflint.hcl b/setup/.tflint.hcl new file mode 100644 index 0000000..427121c --- /dev/null +++ b/setup/.tflint.hcl @@ -0,0 +1,4 @@ +plugin "terraform" { + enabled = true + preset = "recommended" +} diff --git a/setup/gitea/config.yaml b/setup/gitea/config.yaml new file mode 100644 index 0000000..ce82a03 --- /dev/null +++ b/setup/gitea/config.yaml @@ -0,0 +1,99 @@ +# Example configuration file, it's safe to copy this as the default config file without any modification. + +# You don't have to copy this file to your instance, +# just run `./act_runner generate-config > config.yaml` to generate a config file. + +log: + # The level of logging, can be trace, debug, info, warn, error, fatal + level: info + +runner: + # Where to store the registration result. + file: .runner + # Execute how many tasks concurrently at the same time. + capacity: 1 + # Extra environment variables to run jobs. + envs: + A_TEST_ENV_NAME_1: a_test_env_value_1 + A_TEST_ENV_NAME_2: a_test_env_value_2 + NODE_EXTRA_CA_CERTS: "/etc/ssl/certs/ca-certificates.crt" + # Extra environment variables to run jobs from a file. + # It will be ignored if it's empty or the file doesn't exist. + env_file: .env + # The timeout for a job to be finished. + # Please note that the Gitea instance also has a timeout (3h by default) for the job. + # So the job could be stopped by the Gitea instance if it's timeout is shorter than this. + timeout: 3h + # Whether skip verifying the TLS certificate of the Gitea instance. + insecure: true + # The timeout for fetching the job from the Gitea instance. + fetch_timeout: 5s + # The interval for fetching the job from the Gitea instance. + fetch_interval: 2s + # The labels of a runner are used to determine which jobs the runner can run, and how to run them. + # Like: "macos-arm64:host" or "ubuntu-latest:docker://gitea/runner-images:ubuntu-latest" + # Find more images provided by Gitea at https://gitea.com/gitea/runner-images . + # If it's empty when registering, it will ask for inputting labels. + # If it's empty when execute `daemon`, will use labels in `.runner` file. + labels: + - "ubuntu-latest:docker://gitea/runner-images:ubuntu-latest" + - "ubuntu-22.04:docker://gitea/runner-images:ubuntu-22.04" + - "ubuntu-20.04:docker://gitea/runner-images:ubuntu-20.04" + +cache: + # Enable cache server to use actions/cache. + enabled: true + # The directory to store the cache data. + # If it's empty, the cache data will be stored in $HOME/.cache/actcache. + dir: "" + # The host of the cache server. + # It's not for the address to listen, but the address to connect from job containers. + # So 0.0.0.0 is a bad choice, leave it empty to detect automatically. + host: "" + # The port of the cache server. + # 0 means to use a random available port. + port: 0 + # The external cache server URL. Valid only when enable is true. + # If it's specified, act_runner will use this URL as the ACTIONS_CACHE_URL rather than start a server by itself. + # The URL should generally end with "/". + external_server: "" + +container: + # Specifies the network to which the container will connect. + # Could be host, bridge or the name of a custom network. + # If it's empty, act_runner will create a network automatically. + network: "kind" + # Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker). + privileged: false + # And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway). + options: --mount type=bind,source=###ca-certficates.crt###,target=/etc/ssl/certs/ca-certificates.crt,readonly + # The parent directory of a job's working directory. + # NOTE: There is no need to add the first '/' of the path as act_runner will add it automatically. + # If the path starts with '/', the '/' will be trimmed. + # For example, if the parent directory is /path/to/my/dir, workdir_parent should be path/to/my/dir + # If it's empty, /workspace will be used. + workdir_parent: + # Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob + # You can specify multiple volumes. If the sequence is empty, no volumes can be mounted. + # For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to: + # valid_volumes: + # - data + # - /src/*.json + # If you want to allow any volume, please use the following configuration: + # valid_volumes: + # - '**' + valid_volumes: [ '**' ] + # overrides the docker client host with the specified one. + # If it's empty, act_runner will find an available docker host automatically. + # If it's "-", act_runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers. + # If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work. + docker_host: "" + # Pull docker image(s) even if already present + force_pull: false + # Rebuild docker image(s) even if already present + force_rebuild: false + +host: + # The parent directory of a job's working directory. + # If it's empty, $HOME/.cache/act/ will be used. + workdir_parent: diff --git a/setup/kind/cluster.yaml b/setup/kind/cluster.yaml new file mode 100644 index 0000000..d2113d9 --- /dev/null +++ b/setup/kind/cluster.yaml @@ -0,0 +1,18 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +name: 5min-idp +nodes: +- role: control-plane + extraPortMappings: + - containerPort: 30080 + hostPort: 30080 + listenAddress: "0.0.0.0" + protocol: TCP + - containerPort: 30443 + hostPort: 30443 + listenAddress: "0.0.0.0" + protocol: TCP +containerdConfigPatches: +- |- + [plugins."io.containerd.grpc.v1.cri".registry] + config_path = "/etc/containerd/certs.d" diff --git a/setup/terraform/gitea_values.yaml b/setup/terraform/gitea_values.yaml new file mode 100644 index 0000000..063fe85 --- /dev/null +++ b/setup/terraform/gitea_values.yaml @@ -0,0 +1,51 @@ +redis-cluster: + enabled: false +redis: + enabled: true +postgresql: + enabled: true +postgresql-ha: + enabled: false + +persistence: + enabled: true + +ingress: + enabled: true + className: "nginx" + hosts: + - host: "git.localhost" + paths: + - path: "/" + pathType: "Prefix" + - host: "5min-idp-control-plane" + paths: + - path: "/" + pathType: "Prefix" + - host: "pidp-127-0-0-1.nip.io" + paths: + - path: "/" + pathType: "Prefix" + tls: + - secretName: "gitea-tls" + hosts: + - "git.localhost" + - "5min-idp-control-plane" + - "pidp-127-0-0-1.nip.io" + +gitea: + config: + database: + DB_TYPE: postgres + indexer: + ISSUE_INDEXER_TYPE: bleve + REPO_INDEXER_ENABLED: true + APP_NAME: "Pocket IDP - Git Service" + repository: + ROOT: "~/gitea-repositories" + repository.pull-request: + WORK_IN_PROGRESS_PREFIXES: "WIP:,[WIP]:" + admin: + username: "5minadmin" + password: "5minadmin" + email: "jay@humanitec.com" \ No newline at end of file diff --git a/setup/terraform/idp-backstage.tf b/setup/terraform/idp-backstage.tf new file mode 100644 index 0000000..582ec08 --- /dev/null +++ b/setup/terraform/idp-backstage.tf @@ -0,0 +1,32 @@ +# Configure required values for backstage +resource "humanitec_value" "backstage_github_org_id" { + app_id = humanitec_application.backstage.id + key = "GITHUB_ORG_ID" + description = "" + value = "5minorg" + is_secret = false +} + +resource "humanitec_value" "backstage_humanitec_org" { + app_id = humanitec_application.backstage.id + key = "HUMANITEC_ORG_ID" + description = "" + value = var.humanitec_org + is_secret = false +} + +resource "humanitec_value" "backstage_humanitec_token" { + app_id = humanitec_application.backstage.id + key = "HUMANITEC_TOKEN" + description = "" + value = var.humanitec_token + is_secret = true +} + +resource "humanitec_value" "backstage_cloud_provider" { + app_id = humanitec_application.backstage.id + key = "CLOUD_PROVIDER" + description = "" + value = "5min" + is_secret = false +} diff --git a/setup/terraform/idp-base.tf b/setup/terraform/idp-base.tf new file mode 100644 index 0000000..1f757ea --- /dev/null +++ b/setup/terraform/idp-base.tf @@ -0,0 +1,114 @@ +# Ensure we don't have name conflicts +resource "random_string" "install_id" { + length = 4 + special = false + upper = false + numeric = false +} + +locals { + app = "5min-idp-${random_string.install_id.result}" + backstage = "5min-backstage-${random_string.install_id.result}" + prefix = "${local.app}-" + env_type = "5min-local" +} + +resource "humanitec_environment_type" "local" { + id = local.env_type + description = "Local cluster used by 5min IDP." +} + +# Demo application +resource "humanitec_application" "demo" { + id = local.app + name = local.app +} +resource "humanitec_environment" "demo" { + app_id = local.app + id = local.env_type + name = "5min IDP local environment" + type = local.env_type + + depends_on = [ humanitec_environment_type.local ] +} + +# Backstage application & config +resource "humanitec_application" "backstage" { + id = local.backstage + name = local.backstage +} +resource "humanitec_environment" "backstage" { + app_id = local.backstage + id = local.env_type + name = "5min IDP local environment" + type = local.env_type + + depends_on = [ humanitec_environment_type.local ] +} + +# Configure k8s namespace naming +resource "humanitec_resource_definition" "k8s_namespace" { + driver_type = "humanitec/echo" + id = "${local.prefix}k8s-namespace" + name = "${local.prefix}k8s-namespace" + type = "k8s-namespace" + + driver_inputs = { + values_string = jsonencode({ + "namespace" = "$${context.app.id}-$${context.env.id}" + }) + } +} + +resource "humanitec_resource_definition_criteria" "k8s_namespace" { + resource_definition_id = humanitec_resource_definition.k8s_namespace.id + env_type = local.env_type + + force_delete = true + depends_on = [humanitec_environment_type.local] +} + +# Configure DNS for localhost +resource "humanitec_resource_definition" "dns_localhost" { + id = "${local.prefix}dns-localhost" + name = "${local.prefix}dns-localhost" + type = "dns" + driver_type = "humanitec/dns-wildcard" + + driver_inputs = { + values_string = jsonencode({ + "domain" = "localhost" + "template" = "$${context.app.id}-{{ randAlphaNum 4 | lower}}" + }) + } + + provision = { + ingress = { + match_dependents = false + is_dependent = false + } + } +} + +resource "humanitec_resource_definition_criteria" "dns_localhost" { + resource_definition_id = humanitec_resource_definition.dns_localhost.id + env_type = local.env_type + + force_delete = true + depends_on = [humanitec_environment_type.local] +} + +# Provide postgres resource +module "postgres_basic" { + source = "github.com/humanitec-architecture/resource-packs-in-cluster//humanitec-resource-defs/postgres/basic?ref=v2024-06-05" + prefix = local.prefix +} + +resource "humanitec_resource_definition_criteria" "postgres_basic" { + resource_definition_id = module.postgres_basic.id + class = "default" + env_type = local.env_type + + force_delete = true + depends_on = [humanitec_environment_type.local] +} diff --git a/setup/terraform/idp-cluster.tf b/setup/terraform/idp-cluster.tf new file mode 100644 index 0000000..fcf485c --- /dev/null +++ b/setup/terraform/idp-cluster.tf @@ -0,0 +1,100 @@ +# Configure k8s cluster by exposing the locally running Kubernetes Cluster to the Humanitec Orchestrator +# using the Humanitec Agent + +resource "tls_private_key" "agent_private_key" { + algorithm = "RSA" + rsa_bits = 4096 +} + +locals { + agent_id = "${local.prefix}agent" +} + +resource "humanitec_agent" "agent" { + id = local.agent_id + description = "5min-idp" + public_keys = [{ + key = tls_private_key.agent_private_key.public_key_pem + }] +} + +resource "helm_release" "humanitec_agent" { + name = "humanitec-agent" + namespace = "humanitec-agent" + create_namespace = true + + repository = "oci://ghcr.io/humanitec/charts" + chart = "humanitec-agent" + + set { + name = "humanitec.org" + value = var.humanitec_org + } + + set { + name = "humanitec.privateKey" + value = tls_private_key.agent_private_key.private_key_pem + } + + depends_on = [ + humanitec_agent.agent + ] +} + +resource "humanitec_resource_definition" "agent" { + id = local.agent_id + name = local.agent_id + type = "agent" + + driver_type = "humanitec/agent" + driver_inputs = { + values_string = jsonencode({ + id = local.agent_id + }) + } + + depends_on = [ + helm_release.humanitec_agent + ] +} +resource "humanitec_resource_definition_criteria" "agent_local" { + resource_definition_id = humanitec_resource_definition.agent.id + res_id = "agent" + env_type = local.env_type + + force_delete = true + depends_on = [humanitec_environment_type.local] +} + +locals { + parsed_kubeconfig = yamldecode(file(var.kubeconfig)) +} + +resource "humanitec_resource_definition" "cluster_local" { + id = "${local.prefix}k8s-cluster" + name = "${local.prefix}k8s-cluster" + type = "k8s-cluster" + driver_type = "humanitec/k8s-cluster" + + driver_inputs = { + values_string = jsonencode({ + loadbalancer = "0.0.0.0" # ensure dns records are created pointing to localhost + cluster_data = local.parsed_kubeconfig["clusters"][0]["cluster"] + }) + secrets_string = jsonencode({ + agent_url = "$${resources['agent#agent'].outputs.url}" + credentials = local.parsed_kubeconfig["users"][0]["user"] + }) + } +} + +resource "humanitec_resource_definition_criteria" "cluster_local" { + resource_definition_id = humanitec_resource_definition.cluster_local.id + env_type = local.env_type + + force_delete = true + + depends_on = [ + humanitec_resource_definition_criteria.agent_local, humanitec_environment_type.local + ] +} diff --git a/setup/terraform/idp-git.tf b/setup/terraform/idp-git.tf new file mode 100644 index 0000000..6578479 --- /dev/null +++ b/setup/terraform/idp-git.tf @@ -0,0 +1,36 @@ +resource "kubernetes_namespace" "gitea" { + metadata { + name = "gitea" + } +} + +resource "kubernetes_secret_v1" "gitea_cert" { + depends_on = [ kubernetes_namespace.gitea ] + metadata { + name = "gitea-tls" + namespace = "gitea" + } + type = "kubernetes.io/tls" + data = { + "tls.crt" = base64decode(var.tls_cert_string) + "tls.key" = base64decode(var.tls_key_string) + } +} + +resource "helm_release" "gitea" { + name = "gitea" + namespace = "gitea" + create_namespace = true + repository = "https://dl.gitea.com/charts/" + + chart = "gitea" + version = "10.3.0" + wait = true + timeout = 600 + + values = [ + file("${path.module}/gitea_values.yaml") + ] + + depends_on = [kubernetes_secret_v1.gitea_cert] +} diff --git a/setup/terraform/idp-ingress.tf b/setup/terraform/idp-ingress.tf new file mode 100644 index 0000000..65da667 --- /dev/null +++ b/setup/terraform/idp-ingress.tf @@ -0,0 +1,34 @@ +# Ingress used to expose workload + +resource "helm_release" "ingress_nginx" { + name = "ingress-nginx" + namespace = "ingress-nginx" + create_namespace = true + repository = "https://kubernetes.github.io/ingress-nginx" + + chart = "ingress-nginx" + version = "4.10.0" + wait = true + timeout = 600 + + set { + name = "controller.hostPort.enabled" + value = "true" + } + + set { + name = "controller.service.type" + value = "NodePort" + } + + set { + name = "controller.service.nodePorts.http" + value = "30080" + } + + set { + name = "controller.service.nodePorts.https" + value = "30443" + } + +} diff --git a/setup/terraform/outputs.tf b/setup/terraform/outputs.tf new file mode 100644 index 0000000..000373b --- /dev/null +++ b/setup/terraform/outputs.tf @@ -0,0 +1,9 @@ +output "humanitec_app" { + description = "The ID of the Humanitec application" + value = humanitec_application.demo.id +} + +output "humanitec_app_backstage" { + description = "The ID of the Humanitec application for Backstage" + value = humanitec_application.backstage.id +} diff --git a/setup/terraform/providers.tf b/setup/terraform/providers.tf new file mode 100644 index 0000000..20c9924 --- /dev/null +++ b/setup/terraform/providers.tf @@ -0,0 +1,44 @@ +terraform { + backend "local" { + path = "/state/terraform/terraform.tfstate" + } + + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "2.31.0" + } + helm = { + source = "hashicorp/helm" + version = "~> 2.13" + } + humanitec = { + source = "humanitec/humanitec" + version = "~> 1.6" + } + random = { + source = "hashicorp/random" + version = "~> 3.6" + } + tls = { + source = "hashicorp/tls" + version = "~> 4.0" + } + } + + required_version = ">= 1.3.0" +} + +provider "humanitec" { + org_id = var.humanitec_org +} + +provider "kubernetes" { + config_path = var.kubeconfig +} + +provider "helm" { + kubernetes { + config_path = var.kubeconfig + } +} diff --git a/setup/terraform/variables.tf b/setup/terraform/variables.tf new file mode 100644 index 0000000..38749c8 --- /dev/null +++ b/setup/terraform/variables.tf @@ -0,0 +1,35 @@ +variable "humanitec_org" { + description = "The ID of the organization" + default = "humanitec" + type = string +} + +variable "humanitec_token" { + description = "Token for accessing Humanitec" + default = "humanitec" + type = string +} + +variable "kubeconfig" { + description = "Kubeconfig used by the Humanitec Agent / terraform" + type = string + default = "/state/kube/config-internal.yaml" +} + +variable "tls_ca_cert" { + description = "Path to CA certificate that needs to be trusted" + type = string + default = "" +} + +variable "tls_cert_string" { + description = "Cert as string for TLS setup" + type = string + default = "" +} + +variable "tls_key_string" { + description = "Key as string for TLS setup" + type = string + default = "" +}