diff --git a/.gitignore b/.gitignore index a206c26..1c93ab1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -**/*.vagrant/ \ No newline at end of file +**/*.vagrant/ +.gitlab.env \ No newline at end of file diff --git a/bonus/Makefile b/bonus/Makefile new file mode 100644 index 0000000..70f576e --- /dev/null +++ b/bonus/Makefile @@ -0,0 +1,197 @@ +# Simple GitLab lifecycle for a Helm-based install on Kubernetes +# Usage: +# make status # Show what's running in the GitLab namespace +# make gitlab-up # Install/upgrade with LOW-MEM values (default) +# make migrate # Run db:prepare + db:migrate in toolbox (with retries) +# make restart # Restart webservice + sidekiq and wait for rollout +# make logs # Tail webservice + sidekiq logs +# make pf # Port-forward API(8181) and SSH(2222) +# make clean # Uninstall Helm release (keeps namespace + PVCs/data) +# make purge # Full wipe: uninstall + delete PVCs + delete namespace (DATA LOSS) +# make help # Print this help + +SHELL := bash +.SHELLFLAGS := -eu -o pipefail -c + +# ---- Config ------------------------------------------------------------------- +NS ?= gitlab +REL ?= gitlab +HELM ?= helm +KUBECTL ?= kubectl +REPO_PATH ?= $(USER)/Inception-of-Things +BASE_HOST ?= localhost:8081 + +# Always use the low-memory values by default +VALUES ?= confs/gitlab.constrained.yaml +ENV_FILE := .gitlab.env + +-include $(ENV_FILE) +ifneq ("$(wildcard $(ENV_FILE))","") +# Export any VAR present in .gitlab.env whether lines are "KEY=V" or "export KEY=V" +export $(shell sed -n -E 's/^[[:space:]]*(export[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*)=.*/\2/p' $(ENV_FILE)) +endif + +# ---- Full Setup ---------------------------------------------------------------- + +.PHONY: up +up: gitlab-up argocd-up pf + @echo "βœ… GitLab and ArgoCD are up and running!" + +.PHONY: setup +setup: gitlab-pat setup-account show-urls hint-remote-http hint-remote-ssh + @echo "βœ… GitLab account and PAT setup done!" + +# ---- Helpers ------------------------------------------------------------------ +.PHONY: help +help: + @grep -E '^[a-zA-Z0-9_-]+:.*?#' $(MAKEFILE_LIST) | \ + awk 'BEGIN{FS=":.*?#"} {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' + +# ---- Inspect ------------------------------------------------------------------ +.PHONY: status +status: ## Show Pods/Services/PVCs in the GitLab namespace + @echo "==> Pods"; $(KUBECTL) -n $(NS) get pods -o wide || true; echo + @echo "==> Services"; $(KUBECTL) -n $(NS) get svc || true; echo + @echo "==> PVCs"; $(KUBECTL) -n $(NS) get pvc || true + +.PHONY: logs +logs: ## Tail logs for webservice and sidekiq + -$(KUBECTL) -n $(NS) logs deploy/$(REL)-webservice-default -c webservice -f --tail=200 & + -$(KUBECTL) -n $(NS) logs deploy/$(REL)-sidekiq-all-in-1-v2 -c sidekiq -f --tail=200 & \ + wait || true + +.PHONY: pf +pf: ## Port-forward API(8081) and SSH(2222) + @echo "API -> http://localhost:8081" + @echo "SSH -> ssh://git@localhost:2222" + -$(KUBECTL) -n $(NS) port-forward svc/$(REL)-webservice-default 8081:8181 & + -$(KUBECTL) -n $(NS) port-forward svc/$(REL)-gitlab-shell 2222:22 & + +# ---- Install / Upgrade -------------------------------------------------------- + +.PHONY: argocd-up +argocd-up: ## Install/Upgrade ArgoCD in the cluster + @./scripts/bootstrap_argocd.sh + +.PHONY: gitlab-up +gitlab-up: ## Install/Upgrade GitLab using LOW-MEM values (confs/gitlab.constrained.yaml) + @./scripts/install_gitlab.sh $(VALUES) + +# ---- DB migrations & rollouts ------------------------------------------------- +.PHONY: migrate +migrate: ## Run db:prepare + db:migrate in toolbox (with retries) + @set -euo pipefail; \ + for cmd in "gitlab-rake db:prepare" "gitlab-rake db:migrate"; do \ + echo ">>> $$cmd"; \ + for i in 1 2 3 4 5; do \ + if $(KUBECTL) -n $(NS) exec deploy/$(REL)-toolbox -c toolbox -- $$cmd; then \ + echo "OK: $$cmd"; break; \ + else \ + echo "Retry $$i/5 in 10s..."; sleep 10; \ + fi; \ + done; \ + done + +.PHONY: restart +restart: ## Restart webservice + sidekiq and wait for rollout + -$(KUBECTL) -n $(NS) rollout restart deploy/$(REL)-webservice-default || true + -$(KUBECTL) -n $(NS) rollout restart deploy/$(REL)-sidekiq-all-in-1-v2 || true + -$(KUBECTL) -n $(NS) rollout status deploy/$(REL)-webservice-default --timeout=1200s || true + -$(KUBECTL) -n $(NS) rollout status deploy/$(REL)-sidekiq-all-in-1-v2 --timeout=1200s || true + +# ---- Setup GitLab ------------------------------------------------------ +.PHONY: setup-account +setup-account: ## Setup GitLab account and upload local SSH key to GitLab + @./scripts/setup_gitlab_account.sh + +.PHONY: gitlab-pat +gitlab-pat: + @if [ -n "$${REPO_PAT:-}" ]; then \ + echo "βœ… REPO_PAT already set in environment, skipping creation."; \ + exit 0; \ + return 0; \ + fi + @set -euo pipefail; \ + echo "πŸ‘‰ Creating PAT (scopes: read_repository, write_repository, api, admin_mode)"; \ + TOKEN="$$( \ + $(KUBECTL) -n $(NS) exec deploy/$(REL)-toolbox -c toolbox -- \ + gitlab-rails runner \ + "u = User.find_by_username('root'); \ + t = u.personal_access_tokens.create(scopes: %w[read_repository write_repository api admin_mode], \ + name: 'push-token', expires_at: 365.days.from_now); \ + puts t.token" \ + )"; \ + echo "export REPO_PAT=$$TOKEN" >> .gitlab.env; \ + echo "βœ… Wrote .gitlab.env"; \ + echo "πŸ‘‰ Smoke test: /api/v4/user"; \ + curl -fsS -H "PRIVATE-TOKEN: $$TOKEN" http://$(BASE_HOST)/api/v4/user >/dev/null && echo "βœ… Token works" + +# ---- Cleanup ------------------------------------------------------------------ +.PHONY: unport-forward +unport-forward: ## Kill any port-forwarding processes + -@pkill -f "kubectl.*port-forward" || true + @echo "βœ… Killed port-forward processes." + +.PHONY: clean +clean: unport-forward uninstall wait-gone ## Uninstall Helm release (keeps namespace + PVCs/data) + @echo "βœ… Clean done (release removed, data kept)." + @rm -f .gitlab.env + @ssh-keygen -f "$(HOME)/.ssh/known_hosts" -R "[localhost]:2222" + @unset GITLAB_ROOT_PASSWORD || true + @unset REPO_PAT || true + +.PHONY: purge +purge: clean delete-pvcs delete-ns ## Full wipe: release + PVCs + namespace (DATA LOSS) + @echo "🧨 Purge done (everything removed)." + +.PHONY: uninstall +uninstall: ## Helm uninstall of the GitLab release + @echo "==> Helm uninstall $(REL) in namespace $(NS)" + -@$(HELM) uninstall $(REL) -n $(NS) || { echo "Helm release not found."; exit 0; } + +.PHONY: wait-gone +wait-gone: ## Wait for all Pods in the namespace to terminate (best-effort) + @echo "==> Waiting for Pods to terminate (best-effort)" + -@$(KUBECTL) -n $(NS) wait --for=delete pod --all --timeout=120s || true + +.PHONY: delete-pvcs +delete-pvcs: ## Delete all PVCs in the namespace (DATA LOSS) + @echo "==> Deleting PVCs in namespace $(NS) (DATA LOSS)" + -@$(KUBECTL) -n $(NS) delete pvc --all || true + +.PHONY: delete-ns +delete-ns: ## Delete the entire namespace (DATA LOSS) + @echo "==> Deleting namespace $(NS) (DATA LOSS)" + -@$(KUBECTL) delete ns $(NS) --ignore-not-found + +.PHONY: show-urls +show-urls: + @echo "Web/UI: http://$(BASE_HOST)" + @echo "HTTP Git: http://$(BASE_HOST)/$(REPO_PATH).git" + @echo "SSH Git: ssh://git@localhost:2222/$(REPO_PATH).git" + +.PHONY: env +env: + @echo "NS=$(NS)" + @echo "REL=$(REL)" + @echo "BASE_HOST=$(BASE_HOST)" + @echo "REPO_PAT set? $$( [ -n "$${REPO_PAT:-}" ] && echo yes || echo no )" + @echo "GITLAB_ROOT_PASSWORD set? $$( [ -n "$${GITLAB_ROOT_PASSWORD:-}" ] && echo yes || echo no )" + +.PHONY: test-http-git +test-http-git: + @test -n "$$REPO_PAT" || (echo "❌ Please: source $(ENV_FILE) or export REPO_PAT=..."; exit 1) + @echo "πŸ‘‰ Testing HTTP API with REPO_PAT" + @curl -sS -H "PRIVATE-TOKEN: $$REPO_PAT" http://$(BASE_HOST)/api/v4/user && \ + echo -e "\nβœ… HTTP API works" || (echo -e "\n❌ HTTP API failed"; exit 1) + +.PHONY: hint-remote +hint-remote-http: + @echo "# HTTP remote (use this if you set REPO_PAT in your git credentials)" + @echo "git config --global credential.helper store" + @echo 'printf "protocol=http\nhost=$(BASE_HOST)\nusername=root\npassword=$$REPO_PAT\n\n" | git credential approve' + @echo "git remote set-url gitlab http://$(BASE_HOST)/$(REPO_PATH).git" + +hint-remote-ssh: + @echo "# SSH remote (ensure your SSH key is added to GitLab)" + @echo "git remote set-url gitlab ssh://git@localhost:2222/$(REPO_PATH).git" diff --git a/bonus/confs/gitlab.constrained.yaml b/bonus/confs/gitlab.constrained.yaml new file mode 100644 index 0000000..ecd0e55 --- /dev/null +++ b/bonus/confs/gitlab.constrained.yaml @@ -0,0 +1,83 @@ +# confs/gitlab.constrained.yaml +# GitLab Helm Chart configuration for constrained environments +# Fully disable cert-manager and issuer (no TLS for local PoC) +certmanager-issuer: + enabled: false +installCertmanager: false + +global: + edition: ce + hosts: + https: false + tls: + enabled: false + ingress: + configureCertmanager: false + class: traefik + appConfig: + # Turn off IO/space heavy features + lfs: { enabled: false } + artifacts: { enabled: false } + packages: { enabled: false } + dependencyProxy: { enabled: false } + uploads: { storage: local } + pages: { enabled: false } + +nginx-ingress: + enabled: false + +gitlab: + kas: { enabled: false } + + webservice: + minReplicas: 1 + maxReplicas: 1 + ingress: { enabled: false } + service: { type: ClusterIP } + hpa: { enabled: false } + # Give Puma enough warmup time; avoid readiness flapping + readinessProbe: + initialDelaySeconds: 180 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 30 + resources: + requests: { cpu: "100m", memory: "768Mi" } + limits: { cpu: "800m", memory: "2048Mi" } + + gitlab-shell: + service: { type: ClusterIP } + hpa: { enabled: false } + resources: + requests: { cpu: "20m", memory: "64Mi" } + limits: { cpu: "200m", memory: "128Mi" } + + sidekiq: + concurrency: 1 + hpa: { enabled: false } + resources: + requests: { cpu: "50m", memory: "512Mi" } + limits: { cpu: "300m", memory: "1024Mi" } + + gitaly: + resources: + requests: { cpu: "100m", memory: "256Mi" } + limits: { cpu: "600m", memory: "384Mi" } + +gitlab-runner: + install: false + +registry: { enabled: false } +prometheus: { install: false } +grafana: { enabled: false } + +postgresql: + resources: + requests: { cpu: "100m", memory: "512Mi" } + limits: { cpu: "500m", memory: "1024Mi" } + +redis: + master: + resources: + requests: { cpu: "20m", memory: "128Mi" } + limits: { cpu: "200m", memory: "512Mi" } diff --git a/bonus/manifests/argocd/application-dev.tmpl.yaml b/bonus/manifests/argocd/application-dev.tmpl.yaml new file mode 100644 index 0000000..aec3265 --- /dev/null +++ b/bonus/manifests/argocd/application-dev.tmpl.yaml @@ -0,0 +1,19 @@ +# bonus/manifests/argocd/application-dev.tmpl.yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: bonus-dev + namespace: argocd +spec: + project: default + source: + repoURL: ${REPO_URL} + targetRevision: ${REVISION} + path: ${APP_PATH} + destination: + server: https://kubernetes.default.svc + namespace: dev + syncPolicy: + automated: + prune: true + selfHeal: true diff --git a/bonus/manifests/dev/deployment.yaml b/bonus/manifests/dev/deployment.yaml new file mode 100644 index 0000000..b43f0e7 --- /dev/null +++ b/bonus/manifests/dev/deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: wil-playground + namespace: dev +spec: + replicas: 1 + selector: + matchLabels: + app: wil-playground + template: + metadata: + labels: + app: wil-playground + spec: + containers: + - name: app + image: wil42/playground:v1 + ports: + - containerPort: 8888 + imagePullPolicy: IfNotPresent diff --git a/bonus/manifests/dev/kustomization.yaml b/bonus/manifests/dev/kustomization.yaml new file mode 100644 index 0000000..481acde --- /dev/null +++ b/bonus/manifests/dev/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - deployment.yaml + - service.yaml +images: + - name: wil42/playground + newTag: v1 diff --git a/bonus/manifests/dev/service.yaml b/bonus/manifests/dev/service.yaml new file mode 100644 index 0000000..9a1a0b0 --- /dev/null +++ b/bonus/manifests/dev/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: playground-svc + namespace: dev +spec: + selector: + app: wil-playground + ports: + - port: 8888 + targetPort: 8888 + protocol: TCP + type: LoadBalancer diff --git a/bonus/scripts/bootstrap_argocd.sh b/bonus/scripts/bootstrap_argocd.sh new file mode 100755 index 0000000..6810586 --- /dev/null +++ b/bonus/scripts/bootstrap_argocd.sh @@ -0,0 +1,264 @@ +#!/usr/bin/env bash +# scripts/bootstrap_argocd.sh +set -euo pipefail + +# ========================= +# Helpers +# ========================= +log() { echo -e "πŸ‘‰ \e[1m$*\e[0m"; } +ok() { echo -e "βœ… $*"; } +warn() { echo -e "⚠️ $*"; } +die() { echo -e "❌ $*" >&2; exit 1; } + +# ========================= +# Config +# ========================= +CLUSTER_NAME="${CLUSTER_NAME:-mycluster}" +ARGOCD_INSTALL_URL=${ARGOCD_INSTALL_URL:-"https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml"} +ARGOCD_HOST_PORT="${ARGOCD_HOST_PORT:-8080}" # maps to cluster LB :8443 +APP_HOST_PORT="${APP_HOST_PORT:-8888}" # maps to cluster LB :8888 +ARGOCD_MANIFESTS_DIR="${ARGOCD_MANIFESTS_DIR:-manifests/argocd}" + +SSH_PRIVATE_KEY_PATH="${SSH_PRIVATE_KEY_PATH:-$HOME/.ssh/gitlab_k3d}" +GITLAB_SSH_HOST="${GITLAB_SSH_HOST:-gitlab-gitlab-shell.gitlab.svc}" +GITLAB_SSH_PORT="${GITLAB_SSH_PORT:-22}" + +ARGOCD_APP_TEMPLATE="${ARGOCD_APP_TEMPLATE:-manifests/argocd/application-dev.tmpl.yaml}" +REPO_URL="${REPO_URL:-ssh://git@gitlab-gitlab-shell.gitlab.svc/$USER/Inception-of-Things.git}" + +# TODO: Need to change the branch name to main later +REVISION="${REVISION:-lea/bonus}" +APP_PATH="${APP_PATH:-manifests/dev}" + +# ========================= +# Pre-flight +# ========================= +need() { command -v "$1" >/dev/null 2>&1 || die "Missing command: $1"; } + +ensure_docker_ready() { + need docker + if ! systemctl is-active --quiet docker; then + log "Starting Docker service..." + sudo systemctl enable --now docker + fi + if ! id -nG "$USER" | grep -qw docker; then + die "Current user is not in the 'docker' group. Run: sudo usermod -aG docker $USER && re-login (or run 'newgrp docker')." + fi + docker ps >/dev/null 2>&1 || die "Cannot connect to Docker daemon. Check /var/run/docker.sock permissions." +} + +ensure_k3d_cluster_with_ports() { + need k3d + if k3d cluster list | awk 'NR>1 {print $1}' | grep -qx "${CLUSTER_NAME}"; then + warn "Detected existing k3d cluster: ${CLUSTER_NAME}" + local lb_name="k3d-${CLUSTER_NAME}-serverlb" + local lb_id; lb_id=$(docker ps -q -f "name=^/${lb_name}$" || true) + if [[ -z "${lb_id}" ]]; then + warn "Server LB container '${lb_name}' not found; possibly old k3d or custom setup." + else + local ports; ports=$(docker port "${lb_id}" || true) + echo "${ports}" + if ! grep -q "${ARGOCD_HOST_PORT}.*->" <<<"${ports}" || ! grep -q "${APP_HOST_PORT}.*->" <<<"${ports}"; then + warn "Existing cluster lacks required host port mappings:" + warn " Need host ${ARGOCD_HOST_PORT} β†’ LB:8443 and host ${APP_HOST_PORT} β†’ LB:8888" + warn "Consider recreating the cluster:" + warn " k3d cluster delete ${CLUSTER_NAME} && \\" + warn " k3d cluster create ${CLUSTER_NAME} --wait \\" + warn " --port \"${ARGOCD_HOST_PORT}:8443@loadbalancer\" \\" + warn " --port \"${APP_HOST_PORT}:8888@loadbalancer\"" + else + ok "Existing cluster has required host port mappings." + fi + fi + return + fi + + log "Creating k3d cluster '${CLUSTER_NAME}' with host port mappings..." + k3d cluster create "${CLUSTER_NAME}" --wait \ + --port "${ARGOCD_HOST_PORT}:8443@loadbalancer" \ + --port "${APP_HOST_PORT}:8888@loadbalancer" + ok "k3d cluster created." +} + +ensure_argocd_repo_secret_ssh() { + [[ -f "$SSH_PRIVATE_KEY_PATH" ]] || die "SSH private key not found: $SSH_PRIVATE_KEY_PATH" + + if ! ssh-keygen -y -P "" -f "$SSH_PRIVATE_KEY_PATH" >/dev/null 2>&1; then + die "SSH private key appears to be passphrase-protected. Please use a key without passphrase for Argo CD." + fi + + wait_svc_endpoints gitlab gitlab-gitlab-shell 300 + + local KH_RAW="/tmp/kh.raw" KH="/tmp/known_hosts" + local host="$GITLAB_SSH_HOST" port="$GITLAB_SSH_PORT" + local pod="keyscan-tmp" + kubectl -n argocd run "$pod" --image=alpine:3.20 --restart=Never --command -- \ + sh -lc "apk add --no-cache openssh-client >/dev/null; ssh-keyscan -t rsa,ecdsa,ed25519 -p $port $host" + for i in $(seq 1 60); do + if kubectl -n argocd logs "$pod" >/dev/null 2>&1; then + kubectl -n argocd logs "$pod" > "$KH_RAW" || true + break + fi + sleep 1 + done + kubectl -n argocd delete pod "$pod" --ignore-not-found >/dev/null + [[ -s "$KH_RAW" ]] || die "failed to fetch SSH host keys from in-cluster for $host:$port" + + awk -v h="$host" -v p="$port" ' + $1==h { print; printf("[%s]:%s %s %s\n", h, p, $2, $3); next }1 + ' "$KH_RAW" > "$KH" + + local CRED_URL="ssh://git@${host}" + + log "Applying Argo CD repo-creds (SSH) for ${CRED_URL}" + kubectl -n argocd create secret generic gitlab-ssh-creds \ + --from-literal=url="$CRED_URL" \ + --from-file=sshPrivateKey="$SSH_PRIVATE_KEY_PATH" \ + --from-file=sshKnownHosts="$KH" \ + --dry-run=client -o yaml \ + | kubectl label --local -f - argocd.argoproj.io/secret-type=repo-creds -o yaml --overwrite \ + | kubectl apply -n argocd -f - + + ok "Repo-creds Secret ready: gitlab-ssh-creds" + + log "Applying Argo CD ssh-known-hosts ConfigMap for ${host}:${port}" + kubectl -n argocd create configmap argocd-ssh-known-hosts-cm \ + --from-file=ssh_known_hosts="$KH" \ + --dry-run=client -o yaml | kubectl apply -f - + + log "Restarting argocd-repo-server to pick up new known_hosts..." + kubectl -n argocd rollout restart deploy/argocd-repo-server + ok "SSH known_hosts ConfigMap ready: argocd-ssh-known-hosts-cm" +} + +# ========================= +# Waiters (ServiceLB: svclb-*) +# ========================= +wait_svclb_ready() { + local ns="$1"; local svc="$2"; local timeout="${3:-300}" + log "Waiting for ServiceLB helper pod 'svclb-${svc}-*' in namespace '${ns}' (timeout ${timeout}s)..." + local end=$((SECONDS+timeout)) + while (( SECONDS <= end )); do + local lines + lines="$(kubectl -n "${ns}" get pods --no-headers 2>/dev/null | awk '/^svclb-'"${svc}"'-/ {print $1, $2, $3}')" + if [[ -n "${lines}" ]]; then + local notready + notready="$(awk '$2!="1/1" || $3!="Running" {print $0}' <<<"${lines}" || true)" + if [[ -z "${notready}" ]]; then + ok "ServiceLB helper for '${svc}' is Running." + return 0 + fi + fi + sleep 3 + done + die "ServiceLB helper for '${svc}' not ready within ${timeout}s" +} + +wait_svc_endpoints() { + local ns="$1" svc="$2" timeout="${3:-300}" + log "Waiting for service '${ns}/${svc}' endpoints (timeout ${timeout}s)..." + local end=$((SECONDS+timeout)) + while (( SECONDS <= end )); do + if kubectl -n "$ns" get svc "$svc" >/dev/null 2>&1; then + local eps + eps="$(kubectl -n "$ns" get endpoints "$svc" -o jsonpath='{.subsets[*].addresses[*].ip}' 2>/dev/null || true)" + if [[ -n "$eps" ]]; then + ok "Service '${ns}/${svc}' has endpoints: ${eps}" + return 0 + fi + fi + sleep 3 + done + die "Service '${ns}/${svc}' not ready (no endpoints)." +} + +gen_known_hosts_in_cluster() { + local host="$1" port="$2" out="$3" + local pod="keyscan-tmp" + kubectl -n argocd run "$pod" --image=alpine:3.20 --restart=Never --command -- \ + sh -lc "apk add --no-cache openssh-client >/dev/null; ssh-keyscan -p $port $host" + for i in $(seq 1 60); do + if kubectl -n argocd logs "$pod" >/dev/null 2>&1; then + kubectl -n argocd logs "$pod" > "$out" || true + break + fi + sleep 1 + done + kubectl -n argocd delete pod "$pod" --ignore-not-found >/dev/null + [[ -s "$out" ]] || die "failed to fetch known_hosts from in-cluster for $host:$port" +} + +# ========================= +# Argo CD +# ========================= +bootstrap_argocd() { + log "Creating namespaces (argocd, dev)..." + kubectl create ns argocd --dry-run=client -o yaml | kubectl apply -f - + kubectl create ns dev --dry-run=client -o yaml | kubectl apply -f - + ok "Namespaces ready." + + log "Installing Argo CD (in-cluster)..." + kubectl apply -n argocd -f "${ARGOCD_INSTALL_URL}" + + log "Waiting for argocd-server to be ready (up to 5 minutes)..." + kubectl rollout status deploy/argocd-server -n argocd --timeout=300s || true + + # Make argocd-server Service type=LoadBalancer and add port 8443 + log "Exposing argocd-server as LoadBalancer and adding 8443 port..." + kubectl -n argocd patch svc argocd-server -p '{"spec":{"type":"LoadBalancer"}}' >/dev/null + kubectl -n argocd patch svc argocd-server --type merge -p '{ + "spec": { + "type": "LoadBalancer", + "ports": [ + {"name":"https-alt","port":8443,"targetPort":8080} + ] + } + }' >/dev/null + + + # wait svclb_ready or it will get connection refused + wait_svclb_ready kube-system argocd-server 300 + + log "Retrieving Argo CD initial admin password..." + ARGOCD_ADMIN_PASSWORD=$(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d) + ok "Argo CD UI: https://localhost:${ARGOCD_HOST_PORT} (user: admin, password: ${ARGOCD_ADMIN_PASSWORD})" + + ensure_argocd_repo_secret_ssh + + # apply Application + if [[ -f "${ARGOCD_APP_TEMPLATE}" ]]; then + need envsubst + log "Rendering Application from template via envsubst" + log " REPO_URL = ${REPO_URL}" + log " REVISION = ${REVISION}" + log " APP_PATH = ${APP_PATH}" + REPO_URL="${REPO_URL}" REVISION="${REVISION}" APP_PATH="${APP_PATH}" \ + envsubst < "${ARGOCD_APP_TEMPLATE}" | kubectl apply -f - + else + warn "Template not found: ${ARGOCD_APP_TEMPLATE}" + log "Falling back to ${ARGOCD_MANIFESTS_DIR}/application-dev.yaml" + kubectl apply -f "${ARGOCD_MANIFESTS_DIR}/application-dev.yaml" + fi + ok "Application applied." + + if kubectl -n dev get deploy playground >/dev/null 2>&1; then + log "Waiting for Deployment 'playground' Available..." + kubectl -n dev wait --for=condition=available deploy/playground --timeout=300s || true + else + warn "Deployment 'playground' not found yet; will continue and rely on Argo CD sync." + fi + + # wait svclb_ready or it will get connection refused + wait_svclb_ready kube-system playground-svc 300 + + ok "Playground app: http://localhost:${APP_HOST_PORT}" +} + +# ========================= +# Main +# ========================= +need kubectl +ensure_docker_ready +ensure_k3d_cluster_with_ports +bootstrap_argocd +ok "All done πŸŽ‰ Open: https://localhost:${ARGOCD_HOST_PORT} and http://localhost:${APP_HOST_PORT}" diff --git a/bonus/scripts/install_gitlab.sh b/bonus/scripts/install_gitlab.sh new file mode 100755 index 0000000..93a7d03 --- /dev/null +++ b/bonus/scripts/install_gitlab.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +# scripts/install_gitlab.sh +# Always install GitLab with low-memory values unless an explicit values file is passed +set -euo pipefail + +NS="${NS:-gitlab}" +REL="${REL:-gitlab}" +DEFAULT_VALUES="${DEFAULT_VALUES:-confs/gitlab.constrained.yaml}" +VALUES_FILE="${1:-$DEFAULT_VALUES}" +ENV_FILE=".gitlab.env" + +log() { echo -e "πŸ‘‰ $*"; } +ok() { echo -e "βœ… $*"; } +warn(){ echo -e "⚠️ $*"; } +die() { echo -e "❌ $*" >&2; exit 1; } + +need() { command -v "$1" >/dev/null 2>&1 || die "Missing command: $1"; } + +wait_rollout() { + local kind="$1" name="$2" timeout="${3:-900s}" + log "Waiting for $kind/$name to be Ready (timeout $timeout)..." + if ! kubectl -n "$NS" rollout status "$kind/$name" --timeout="$timeout"; then + warn "$kind/$name not Ready in time." + else + ok "$kind/$name Ready." + fi +} + +run_migration() { + local cmd="$1" tries=6 sleepsec=12 + for i in $(seq 1 "$tries"); do + log "Running migration: '$cmd' (attempt $i/$tries)" + if kubectl -n "$NS" exec deploy/${REL}-toolbox -c toolbox -- $cmd; then + ok "Succeeded: $cmd" + return 0 + fi + warn "Failed (attempt $i). Sleep ${sleepsec}s…" + sleep "$sleepsec" + done + die "Migration failed repeatedly: $cmd" +} + +wait_toolbox_ready() { + log "Waiting toolbox API (gitlab-rails runner) to be available..." + local tries=60 sleepsec=5 + for i in $(seq 1 "$tries"); do + if kubectl -n "$NS" exec deploy/${REL}-toolbox -c toolbox -- \ + gitlab-rails runner "puts 'pong'" >/dev/null 2>&1; then + ok "toolbox rails runner OK." + return 0 + fi + sleep "$sleepsec" + done + die "toolbox rails runner not responding" +} + +ensure_http_git_enabled() { + log "Ensuring HTTP Git + PAT auth are enabled (idempotent)..." + kubectl -n "$NS" exec deploy/${REL}-toolbox -c toolbox -- \ + gitlab-rails runner \ +"s=ApplicationSetting.current; s.update!(enabled_git_access_protocol: 'all', password_authentication_enabled_for_git: true); +puts \"enabled_git_access_protocol=#{s.enabled_git_access_protocol}, password_authentication_enabled_for_git=#{s.password_authentication_enabled_for_git}\"" \ + || warn "Failed to update ApplicationSetting (will continue)." +} + +ensure_initial_root_password_secret() { + log "Ensuring initial root password secret (${REL}-gitlab-initial-root-password) ..." + if kubectl -n "$NS" get secret "${REL}-gitlab-initial-root-password" >/dev/null 2>&1; then + ok "Secret exists. (skip)" + return 0 + fi + + local PASS="${GITLAB_ROOT_PASSWORD:-${GITLAB_ROOT_PASSWORD_FILE:-}}" + if [[ -z "$PASS" ]]; then + PASS='ChangeMe_+VeryStrong#2025' + fi + + kubectl -n "$NS" create secret generic "${REL}-gitlab-initial-root-password" \ + --from-literal=password="$PASS" + + printf "export GITLAB_ROOT_PASSWORD=%q\n" "$PASS" > "$ENV_FILE" + ok "Created secret and saved GITLAB_ROOT_PASSWORD to $ENV_FILE" +} + +ensure_root_user() { + log "Ensuring 'root' user exists (will seed if missing)..." + local tries=60 sleepsec=5 seeded_once=false + for i in $(seq 1 "$tries"); do + local out + out=$(kubectl -n "$NS" exec deploy/${REL}-toolbox -c toolbox -- \ + gitlab-rails runner "u=User.find_by(username: 'root'); puts(u ? 'present' : 'missing')" 2>/dev/null || true) + + if [[ "$out" == "present" ]]; then + ok "root user present." + return 0 + fi + if [[ "$seeded_once" == false ]]; then + warn "root missing; running db:seed_fu once..." + kubectl -n "$NS" exec deploy/${REL}-toolbox -c toolbox -- gitlab-rake db:seed_fu || true + seeded_once=true + fi + + sleep "$sleepsec" + done + die "root user was not created after waiting" +} + +wait_initial_root_password_secret() { + log "Waiting for initial root password secret..." + local tries=120 sleepsec=2 PASS="" + for i in $(seq 1 "$tries"); do + PASS="$(kubectl -n "$NS" get secret ${REL}-gitlab-initial-root-password \ + -o jsonpath='{.data.password}' 2>/dev/null | base64 -d || true)" + if [[ -n "$PASS" ]]; then + ok "Initial root password secret available." + echo "root initial password: $PASS" + export GITLAB_ROOT_PASSWORD="$PASS" + printf "export GITLAB_ROOT_PASSWORD=%q\n" "$PASS" > "$ENV_FILE" + return 0 + fi + sleep "$sleepsec" + done + warn "Initial root password secret not found in time (continuing)." +} + +need kubectl +need helm + +log "Using values: ${VALUES_FILE}" + +log "Ensuring namespace" +kubectl create ns "$NS" --dry-run=client -o yaml | kubectl apply -f - + +ensure_initial_root_password_secret + +log "Adding/Updating Helm repo" +helm repo add gitlab https://charts.gitlab.io/ >/dev/null +helm repo update >/dev/null + +log "Installing/Upgrading GitLab (low-memory mode)" +helm upgrade --install "$REL" gitlab/gitlab -n "$NS" \ + -f "$VALUES_FILE" \ + --set certmanager-issuer.enabled=false \ + --set gitlab.kas.enabled=false \ + --set gitlab.gitlab-exporter.enabled=false \ + --set registry.enabled=false \ + --set prometheus.install=false \ + --set grafana.enabled=false \ + --timeout 1800s + +# Wait core deps, then run migrations +kubectl -n gitlab rollout status statefulset/gitlab-postgresql --timeout=900s +kubectl -n gitlab rollout status statefulset/gitlab-redis-master --timeout=900s +kubectl -n gitlab rollout status statefulset/gitlab-gitaly --timeout=900s +wait_rollout "deployment" "${REL}-toolbox" "900s" || true + +run_migration "gitlab-rake db:prepare" +run_migration "gitlab-rake db:migrate" + +wait_toolbox_ready +# ensure_http_git_enabled +ensure_root_user +wait_initial_root_password_secret + +log "Restarting webservice and sidekiq" +kubectl -n "$NS" rollout restart deploy/${REL}-webservice-default || true +kubectl -n "$NS" rollout restart deploy/${REL}-sidekiq-all-in-1-v2 || true + + +log ">>> Capturing initial root password…" +PASS="$( + kubectl -n "$NS" get secret "${REL}-gitlab-initial-root-password" \ + -o jsonpath='{.data.password}' \ + | base64 --decode | tr -d '\n' +)" +log "root initial password: $PASS" +export GITLAB_ROOT_PASSWORD="$PASS" +printf "export GITLAB_ROOT_PASSWORD=%q\n" "$PASS" > "$ENV_FILE" + +ok "Saved export line to $ENV_FILE" +echo "πŸ‘‰ Load it into your current shell with: source $ENV_FILE" + +ok "No-UI mode. Use port-forward when needed:" +echo " API: kubectl -n $NS port-forward svc/${REL}-webservice-default 8081:8181" +echo " SSH: kubectl -n $NS port-forward svc/${REL}-gitlab-shell 2222:22" diff --git a/bonus/scripts/setup_gitlab_account.sh b/bonus/scripts/setup_gitlab_account.sh new file mode 100755 index 0000000..98f1b1d --- /dev/null +++ b/bonus/scripts/setup_gitlab_account.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# setup_gitlab_account.sh +# Create a GitLab user (no-UI) using REST API and upload a local SSH public key. +# - Uses local $USER as username by default +# - Generates an ed25519 key (~/.ssh/gitlab_k3d) if missing +# - Requires a ROOT admin PAT with at least 'api' scope (env: GITLAB_PAT / REPO_PAT / PAT) + +set -euo pipefail + +# ========== Config (override via env) ========== +BASE="${BASE:-http://localhost:8081}" # Workhorse endpoint (after port-forward) +ACCOUNT_USERNAME="${ACCOUNT_USERNAME:-$USER}" # Username in GitLab (default to local $USER) +ACCOUNT_NAME="${ACCOUNT_NAME:-$(getent passwd "$ACCOUNT_USERNAME" 2>/dev/null | cut -d: -f5 | cut -d, -f1 || echo "$ACCOUNT_USERNAME")}" +ACCOUNT_EMAIL="${ACCOUNT_EMAIL:-${ACCOUNT_USERNAME}@localhost}" # Non-routable default +PASSWORD="${PASSWORD:-$(openssl rand -base64 24 | tr -d '\n')}" # Random initial password + +PRIVKEY_PATH="${PRIVKEY_PATH:-$HOME/.ssh/gitlab_k3d}" +PUBKEY_PATH="${PUBKEY_PATH:-${PRIVKEY_PATH}.pub}" +KEY_TITLE="${KEY_TITLE:-$(hostname)-k3d}" + +# Admin PAT (root). Accept several common env names for convenience. +GITLAB_PAT="${GITLAB_PAT:-${REPO_PAT:-${PAT:-}}}" + +# ========== Helpers ========== +log() { echo -e "πŸ‘‰ $*"; } +ok() { echo -e "βœ… $*"; } +warn() { echo -e "⚠️ $*"; } +die() { echo -e "❌ $*" >&2; exit 1; } + +need() { command -v "$1" >/dev/null 2>&1 || die "Missing command: $1"; } + +api_get() { curl -sS --fail -H "PRIVATE-TOKEN: $GITLAB_PAT" "$@"; } +api_post() { curl -sS --fail -H "PRIVATE-TOKEN: $GITLAB_PAT" -X POST "$@"; } +api_put() { curl -sS --fail -H "PRIVATE-TOKEN: $GITLAB_PAT" -X PUT "$@"; } + +# ========== Pre-flight ========== +need curl +need jq +need ssh-keygen +need openssl + +[[ -n "${GITLAB_PAT}" ]] || die "GITLAB_PAT (or REPO_PAT/PAT) is empty. Export a root admin token with 'api' scope." + +log "Checking GitLab availability at $BASE ..." +if ! curl -sS -I "$BASE/users/sign_in" >/dev/null; then + warn "Cannot reach $BASE. Did you run: kubectl -n gitlab port-forward svc/gitlab-webservice-default 8081:8181 ?" +fi + +# ========== Ensure SSH key exists ========== +if [[ ! -f "$PRIVKEY_PATH" || ! -f "$PUBKEY_PATH" ]]; then + log "Generating SSH key: $PRIVKEY_PATH (ed25519, empty passphrase)" + mkdir -p "$(dirname "$PRIVKEY_PATH")" + ssh-keygen -t ed25519 -f "$PRIVKEY_PATH" -N "" -C "$ACCOUNT_USERNAME@$(hostname)" +else + ok "SSH key exists: $PUBKEY_PATH" +fi + +# ========== Get or create user ========== +log "Checking if user '$ACCOUNT_USERNAME' exists..." +USER_JSON="$(api_get "$BASE/api/v4/users?username=$(printf %s "$ACCOUNT_USERNAME")" || true)" +USER_ID="$(jq -r '.[0].id // empty' <<<"$USER_JSON")" + +if [[ -z "$USER_ID" ]]; then + log "Creating user '$ACCOUNT_USERNAME' ..." + CREATE_JSON="$(api_post "$BASE/api/v4/users" \ + --data-urlencode "name=${ACCOUNT_NAME}" \ + --data-urlencode "username=${ACCOUNT_USERNAME}" \ + --data-urlencode "email=${ACCOUNT_EMAIL}" \ + --data-urlencode "password=${PASSWORD}" \ + --data-urlencode "skip_confirmation=true" + )" + USER_ID="$(jq -r '.id' <<<"$CREATE_JSON")" + [[ -n "$USER_ID" && "$USER_ID" != "null" ]] || die "Failed to create user. Response: $CREATE_JSON" + ok "Created user '$ACCOUNT_USERNAME' (id=$USER_ID, email=$ACCOUNT_EMAIL)" + ok "Initial password: $PASSWORD" +else + ok "User exists '$ACCOUNT_USERNAME' (id=$USER_ID)" +fi + +# ========== Upload SSH public key ========== +PUBKEY_CONTENT="$(cat "$PUBKEY_PATH")" +log "Uploading SSH key to user '$ACCOUNT_USERNAME' ..." +KEY_JSON="$(api_post "$BASE/api/v4/users/$USER_ID/keys" \ + --data-urlencode "title=${KEY_TITLE}" \ + --data-urlencode "key=${PUBKEY_CONTENT}" || true)" + +# If key already exists, GitLab returns 400. Check if key list already contains it. +if jq -e '.id' <<<"$KEY_JSON" >/dev/null 2>&1; then + ok "SSH key added: $(jq -r '.title' <<<"$KEY_JSON")" +else + warn "Could not add SSH key (maybe it already exists). Checking existing keys..." + KEYS_LIST="$(api_get "$BASE/api/v4/users/$USER_ID/keys")" + if grep -qF "$(cut -d' ' -f1-2 <<<"$PUBKEY_CONTENT")" <<<"$KEYS_LIST"; then + ok "SSH key is already present for '$ACCOUNT_USERNAME'." + else + die "Failed to add SSH key. Response: $KEY_JSON" + fi +fi + +# ========== Output summary ========== +echo +ok "Done!" +cat < Running bootstrap with CLUSTER_NAME=$(CLUSTER_NAME) ARGOCD_HOST_PORT=$(ARGOCD_HOST_PORT) APP_HOST_PORT=$(APP_HOST_PORT)" + @CLUSTER_NAME="$(CLUSTER_NAME)" ARGOCD_HOST_PORT="$(ARGOCD_HOST_PORT)" APP_HOST_PORT="$(APP_HOST_PORT)" "$(BOOTSTRAP_SCRIPT)" + +## Remove app + Argo CD namespaces (cluster remains) +clean: + @echo "==> Deleting Argo CD Application (if any) and namespaces (argocd, dev)" + -@$(KUBECTL) -n argocd delete application p3-dev 2>/dev/null || true + -@$(KUBECTL) delete ns dev argocd 2>/dev/null || true + @echo "==> Clean done (cluster '$(CLUSTER_NAME)' still present)" + +## Full clean: clean + delete k3d cluster +fclean: clean + @echo "==> Deleting k3d cluster '$(CLUSTER_NAME)'" + -@$(K3D) cluster delete "$(CLUSTER_NAME)" 2>/dev/null || true + @echo "==> Full clean done" + +## Rebuild from scratch +re: fclean all diff --git a/p3/manifests/argocd/application-dev.yaml b/p3/manifests/argocd/application-dev.yaml new file mode 100644 index 0000000..e3f31a7 --- /dev/null +++ b/p3/manifests/argocd/application-dev.yaml @@ -0,0 +1,18 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: p3-dev + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/42-CC-RNCP/Inception-of-Things.git + targetRevision: lea/p3 + path: p3/manifests/dev + destination: + server: https://kubernetes.default.svc + namespace: dev + syncPolicy: + automated: + prune: true + selfHeal: true diff --git a/p3/manifests/dev/deployment.yaml b/p3/manifests/dev/deployment.yaml new file mode 100644 index 0000000..b43f0e7 --- /dev/null +++ b/p3/manifests/dev/deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: wil-playground + namespace: dev +spec: + replicas: 1 + selector: + matchLabels: + app: wil-playground + template: + metadata: + labels: + app: wil-playground + spec: + containers: + - name: app + image: wil42/playground:v1 + ports: + - containerPort: 8888 + imagePullPolicy: IfNotPresent diff --git a/p3/manifests/dev/kustomization.yaml b/p3/manifests/dev/kustomization.yaml new file mode 100644 index 0000000..481acde --- /dev/null +++ b/p3/manifests/dev/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - deployment.yaml + - service.yaml +images: + - name: wil42/playground + newTag: v1 diff --git a/p3/manifests/dev/service.yaml b/p3/manifests/dev/service.yaml new file mode 100644 index 0000000..9a1a0b0 --- /dev/null +++ b/p3/manifests/dev/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: playground-svc + namespace: dev +spec: + selector: + app: wil-playground + ports: + - port: 8888 + targetPort: 8888 + protocol: TCP + type: LoadBalancer diff --git a/p3/tools/bootstrap_argocd.sh b/p3/tools/bootstrap_argocd.sh new file mode 100755 index 0000000..ba872c7 --- /dev/null +++ b/p3/tools/bootstrap_argocd.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ========================= +# Helpers +# ========================= +log() { echo -e "πŸ‘‰ \e[1m$*\e[0m"; } +ok() { echo -e "βœ… $*"; } +warn() { echo -e "⚠️ $*"; } +die() { echo -e "❌ $*" >&2; exit 1; } + +# ========================= +# Config +# ========================= +CLUSTER_NAME="${CLUSTER_NAME:-mycluster}" +ARGOCD_INSTALL_URL=${ARGOCD_INSTALL_URL:-"https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml"} +ARGOCD_HOST_PORT="${ARGOCD_HOST_PORT:-8080}" # maps to cluster LB :8443 +APP_HOST_PORT="${APP_HOST_PORT:-8888}" # maps to cluster LB :8888 +ARGOCD_MANIFESTS_DIR="${ARGOCD_MANIFESTS_DIR:-manifests/argocd}" + +# ========================= +# Pre-flight +# ========================= +need() { command -v "$1" >/dev/null 2>&1 || die "Missing command: $1"; } + +ensure_docker_ready() { + need docker + if ! systemctl is-active --quiet docker; then + log "Starting Docker service..." + sudo systemctl enable --now docker + fi + if ! id -nG "$USER" | grep -qw docker; then + die "Current user is not in the 'docker' group. Run: sudo usermod -aG docker $USER && re-login (or run 'newgrp docker')." + fi + docker ps >/dev/null 2>&1 || die "Cannot connect to Docker daemon. Check /var/run/docker.sock permissions." +} + +ensure_k3d_cluster_with_ports() { + need k3d + if k3d cluster list | awk 'NR>1 {print $1}' | grep -qx "${CLUSTER_NAME}"; then + warn "Detected existing k3d cluster: ${CLUSTER_NAME}" + local lb_name="k3d-${CLUSTER_NAME}-serverlb" + local lb_id; lb_id=$(docker ps -q -f "name=^/${lb_name}$" || true) + if [[ -z "${lb_id}" ]]; then + warn "Server LB container '${lb_name}' not found; possibly old k3d or custom setup." + else + local ports; ports=$(docker port "${lb_id}" || true) + echo "${ports}" + if ! grep -q "${ARGOCD_HOST_PORT}.*->" <<<"${ports}" || ! grep -q "${APP_HOST_PORT}.*->" <<<"${ports}"; then + warn "Existing cluster lacks required host port mappings:" + warn " Need host ${ARGOCD_HOST_PORT} β†’ LB:8443 and host ${APP_HOST_PORT} β†’ LB:8888" + warn "Consider recreating the cluster:" + warn " k3d cluster delete ${CLUSTER_NAME} && \\" + warn " k3d cluster create ${CLUSTER_NAME} --wait \\" + warn " --port \"${ARGOCD_HOST_PORT}:8443@loadbalancer\" \\" + warn " --port \"${APP_HOST_PORT}:8888@loadbalancer\"" + else + ok "Existing cluster has required host port mappings." + fi + fi + return + fi + + log "Creating k3d cluster '${CLUSTER_NAME}' with host port mappings..." + k3d cluster create "${CLUSTER_NAME}" --wait \ + --port "${ARGOCD_HOST_PORT}:8443@loadbalancer" \ + --port "${APP_HOST_PORT}:8888@loadbalancer" + ok "k3d cluster created." +} + +# ========================= +# Waiters (ServiceLB: svclb-*) +# ========================= +wait_svclb_ready() { + local ns="$1"; local svc="$2"; local timeout="${3:-300}" + log "Waiting for ServiceLB helper pod 'svclb-${svc}-*' in namespace '${ns}' (timeout ${timeout}s)..." + local end=$((SECONDS+timeout)) + while (( SECONDS <= end )); do + local lines + lines="$(kubectl -n "${ns}" get pods --no-headers 2>/dev/null | awk '/^svclb-'"${svc}"'-/ {print $1, $2, $3}')" + if [[ -n "${lines}" ]]; then + local notready + notready="$(awk '$2!="1/1" || $3!="Running" {print $0}' <<<"${lines}" || true)" + if [[ -z "${notready}" ]]; then + ok "ServiceLB helper for '${svc}' is Running." + return 0 + fi + fi + sleep 3 + done + die "ServiceLB helper for '${svc}' not ready within ${timeout}s" +} + +# ========================= +# Argo CD +# ========================= +bootstrap_argocd() { + log "Creating namespaces (argocd, dev)..." + kubectl create ns argocd --dry-run=client -o yaml | kubectl apply -f - + kubectl create ns dev --dry-run=client -o yaml | kubectl apply -f - + ok "Namespaces ready." + + log "Installing Argo CD (in-cluster)..." + kubectl apply -n argocd -f "${ARGOCD_INSTALL_URL}" + + log "Waiting for argocd-server to be ready (up to 5 minutes)..." + kubectl rollout status deploy/argocd-server -n argocd --timeout=300s || true + + # Make argocd-server Service type=LoadBalancer and add port 8443 + log "Exposing argocd-server as LoadBalancer and adding 8443 port..." + kubectl -n argocd patch svc argocd-server -p '{"spec":{"type":"LoadBalancer"}}' >/dev/null + kubectl -n argocd patch svc argocd-server --type merge -p '{ + "spec": { + "type": "LoadBalancer", + "ports": [ + {"name":"https-alt","port":8443,"targetPort":8080} + ] + } + }' >/dev/null + + + # wait svclb_ready or it will get connection refused + wait_svclb_ready kube-system argocd-server 300 + + log "Retrieving Argo CD initial admin password..." + ARGOCD_ADMIN_PASSWORD=$(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d) + ok "Argo CD UI: https://localhost:${ARGOCD_HOST_PORT} (user: admin, password: ${ARGOCD_ADMIN_PASSWORD})" + + # apply Application + log "Applying Argo CD Application (dev/playground)..." + kubectl apply -f "${ARGOCD_MANIFESTS_DIR}/application-dev.yaml" + ok "Application applied." + + if kubectl -n dev get deploy playground >/dev/null 2>&1; then + log "Waiting for Deployment 'playground' Available..." + kubectl -n dev wait --for=condition=available deploy/playground --timeout=300s || true + else + warn "Deployment 'playground' not found yet; will continue and rely on Argo CD sync." + fi + + # wait svclb_ready or it will get connection refused + wait_svclb_ready kube-system playground-svc 300 + + ok "Playground app: http://localhost:${APP_HOST_PORT}" +} + +# ========================= +# Main +# ========================= +need kubectl +ensure_docker_ready +ensure_k3d_cluster_with_ports +bootstrap_argocd +ok "All done πŸŽ‰ Open: https://localhost:${ARGOCD_HOST_PORT} and http://localhost:${APP_HOST_PORT}" diff --git a/scripts/setup_host.sh b/scripts/setup_host.sh index 2502582..d99e84a 100755 --- a/scripts/setup_host.sh +++ b/scripts/setup_host.sh @@ -1,41 +1,231 @@ -#!/bin/bash -set -e - -VAGRANT_VERSION="2.4.7" -ARCH="amd64" -DEB_FILE="vagrant_${VAGRANT_VERSION}-1_${ARCH}.deb" -DOWNLOAD_URL="https://releases.hashicorp.com/vagrant/${VAGRANT_VERSION}/${DEB_FILE}" - -echo "Installing dependencies..." -sudo apt update -sudo apt install -y wget curl gnupg lsb-release - -# Remove old version if any -if command -v vagrant >/dev/null; then - echo "Removing existing Vagrant installation..." - sudo apt remove -y vagrant -fi +#!/usr/bin/env bash +set -euo pipefail + +# ========================= +# Config (env or args) +# ========================= +VAGRANT_VERSION="${VAGRANT_VERSION:-2.4.7}" +CLUSTER_NAME="${CLUSTER_NAME:-k3d-cluster}" + +# ========================= +# Helpers +# ========================= +log() { echo -e "πŸ‘‰ \e[1m$*\e[0m"; } +ok() { echo -e "βœ… $*"; } +warn() { echo -e "⚠️ $*"; } +die() { echo -e "❌ $*" >&2; exit 1; } + +DISTRO_CODENAME="$(lsb_release -cs 2>/dev/null || echo bookworm)" +ARCH_DEB="$(dpkg --print-architecture)" +ARCH_UNAME="$(uname -m)" + +case "${ARCH_DEB}" in + amd64) KUBECTL_ARCH="amd64" ;; + arm64) KUBECTL_ARCH="arm64" ;; + *) die "Unsupported arch: ${ARCH_DEB}" ;; +esac + +# default: install all +DO_VAGRANT=1 +DO_PART3=1 +DO_PROVISION=1 + +usage() { + cat </dev/null 2>&1; then + echo "❌ Cannot access Docker Daemon (/var/run/docker.sock)." + echo " Check group and service status, then retry." + exit 1 + fi +} -echo "Installing Vagrant..." -sudo dpkg -i "/tmp/${DEB_FILE}" +# ========================= +# Common base +# ========================= +install_base() { + log "Installing base packages..." + sudo apt-get update -y + sudo apt-get install -y ca-certificates curl wget gnupg lsb-release apt-transport-https software-properties-common jq + ok "Base packages installed" +} -# Cleanup -rm "/tmp/${DEB_FILE}" +# ========================= +# Vagrant (Part1/2) +# ========================= +install_vagrant() { + log "Installing/updating Vagrant ${VAGRANT_VERSION}..." + local DEB_FILE="vagrant_${VAGRANT_VERSION}-1_${ARCH_DEB}.deb" + local URL="https://releases.hashicorp.com/vagrant/${VAGRANT_VERSION}/${DEB_FILE}" -# Verify installation -echo "βœ… Vagrant installed successfully:" -vagrant --version + if command -v vagrant >/dev/null 2>&1; then + warn "Detected existing Vagrant: $(vagrant --version). Attempting to update/overwrite to ${VAGRANT_VERSION}" + sudo apt-get remove -y vagrant || true + fi -echo "Installing VirtualBox..." -wget -O- -q https://www.virtualbox.org/download/oracle_vbox_2016.asc | sudo gpg --dearmour -o /usr/share/keyrings/oracle_vbox_2016.gpg -echo "deb [arch=amd64 signed-by=/usr/share/keyrings/oracle_vbox_2016.gpg] http://download.virtualbox.org/virtualbox/debian bookworm contrib" | sudo tee /etc/apt/sources.list.d/virtualbox.list -sudo apt update -sudo apt install virtualbox-7.1 + wget -q "${URL}" -O "/tmp/${DEB_FILE}" || die "Failed to download Vagrant: ${URL}" + sudo dpkg -i "/tmp/${DEB_FILE}" || sudo apt-get -f install -y + rm -f "/tmp/${DEB_FILE}" + ok "Vagrant installed: $(vagrant --version)" +} + +install_virtualbox_amd64() { + log "installing VirtualBox (amd64) as Vagrant provider..." + # Oracle keyring + wget -qO- https://www.virtualbox.org/download/oracle_vbox_2016.asc \ + | sudo gpg --dearmor -o /usr/share/keyrings/oracle_vbox_2016.gpg + echo "deb [arch=amd64 signed-by=/usr/share/keyrings/oracle_vbox_2016.gpg] http://download.virtualbox.org/virtualbox/debian ${DISTRO_CODENAME} contrib" \ + | sudo tee /etc/apt/sources.list.d/virtualbox.list >/dev/null + sudo apt-get update -y + sudo apt-get install -y virtualbox-7.1 + ok "VirtualBox installed: $(vboxmanage --version || echo ok)" +} + +install_libvirt_arm64() { + log "installing libvirt + QEMU (arm64) as Vagrant provider..." + sudo apt-get update -y + sudo apt-get install -y qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils ebtables dnsmasq-base + sudo systemctl enable --now libvirtd + sudo usermod -aG libvirt,kvm "$USER" || true + # install vagrant-libvirt plugin + vagrant plugin install vagrant-libvirt + ok "libvirt/kvm and vagrant-libvirt plugin installed. (Please re-login for group changes to take effect)" +} + +install_vagrant_with_provider() { + install_vagrant + if [[ "${ARCH_DEB}" == "amd64" ]]; then + install_virtualbox_amd64 + ok "Part1/2: Vagrant + VirtualBox ready." + else + install_libvirt_arm64 + ok "Part1/2: Vagrant + libvirt ready. (VirtualBox not supported on ARM)" + fi +} + +# ========================= +# Part3: Docker + kubectl + k3d +# ========================= +install_docker() { + if command -v docker >/dev/null 2>&1; then + ok "Docker already installed: $(docker --version)" + return + fi + log "Installing Docker (official script)..." + curl -fsSL https://get.docker.com | sh + sudo usermod -aG docker "$USER" || true + newgrp docker + ok "Docker installed." +} + +install_kubectl() { + if command -v kubectl >/dev/null 2>&1; then + ok "kubectl already installed: $(kubectl version --client --output=yaml | grep gitVersion || true)" + return + fi + log "Installing kubectl (stable)..." + tmpdir="$(mktemp -d)" + pushd "$tmpdir" >/dev/null + KVER="$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)" + curl -LO "https://storage.googleapis.com/kubernetes-release/release/${KVER}/bin/linux/${KUBECTL_ARCH}/kubectl" + chmod +x kubectl + sudo mv kubectl /usr/local/bin/ + popd >/dev/null + rm -rf "$tmpdir" + ok "kubectl installed: $(kubectl version --client --short | tr -s ' ')" +} + +install_k3d() { + if command -v k3d >/dev/null 2>&1; then + ok "k3d already installed: $(k3d version | head -n1)" + return + fi + log "Installing k3d..." + curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash + ok "k3d installed: $(k3d version | head -n1)" +} + +create_k3d_cluster() { + if k3d cluster list | grep -q "^${CLUSTER_NAME}\b"; then + ok "k3d cluster ${CLUSTER_NAME} already exists, skipping creation." + return + fi + log "Creating k3d cluster: ${CLUSTER_NAME} ..." + k3d cluster create "${CLUSTER_NAME}" --wait + ok "k3d cluster created." +} + +part3_stack() { + install_docker + install_kubectl + install_k3d + if [[ "${DO_PROVISION}" -eq 1 ]]; then + ensure_docker_ready + create_k3d_cluster + else + warn "--no-provision: Only installing tools, skipping cluster creation/Argo CD installation." + fi +} + +# ========================= +# Run +# ========================= +install_base + +if [[ "${DO_VAGRANT}" -eq 1 ]]; then + install_vagrant_with_provider +else + warn "Skipping Vagrant installation (--part3-only)" +fi + +if [[ "${DO_PART3}" -eq 1 ]]; then + part3_stack +else + warn "Skipping Part3 (Docker/k3d/kubectl) installation (--vagrant-only)" +fi -# Verify VirtualBox installation -echo "βœ… VirtualBox installed successfully:" -virtualbox --help | head -n 1 +ok "All done πŸŽ‰" +warn "If you just added docker/libvirt/kvm groups, please re-login for the changes to take effect."