diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5ce8e92 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.git +.gitea +logs +run +*.exe +*.test +*.out +api +worker +seed-sample-job + diff --git a/.gitea/scripts/update-config-center-images.sh b/.gitea/scripts/update-config-center-images.sh new file mode 100644 index 0000000..7577968 --- /dev/null +++ b/.gitea/scripts/update-config-center-images.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${CONFIG_CENTER_UPDATES_FILE:?CONFIG_CENTER_UPDATES_FILE is required}" + +CONFIG_CENTER_CLUSTERS="${CONFIG_CENTER_CLUSTERS:-ack-dev}" + +if [ ! -s "$CONFIG_CENTER_UPDATES_FILE" ]; then + echo "No config-center image updates were recorded." + exit 0 +fi + +if command -v python3 >/dev/null 2>&1; then + PYTHON=python3 +elif command -v python >/dev/null 2>&1; then + PYTHON=python +else + echo "Python is required for direct config-center update." + exit 1 +fi + +WORKDIR="$(mktemp -d)" +trap 'rm -rf "$WORKDIR"' EXIT + +echo "Updating config-center after image build succeeded." +git clone git@git.imall.cloud:im-group/config-center.git "$WORKDIR/config-center" +cd "$WORKDIR/config-center" + +"$PYTHON" - "$CONFIG_CENTER_UPDATES_FILE" "$CONFIG_CENTER_CLUSTERS" <<'PY' +from __future__ import annotations + +import sys +from pathlib import Path + +updates_file = Path(sys.argv[1]) +clusters = sys.argv[2].split() +updates = [ + tuple(line.split("|")) + for line in updates_file.read_text(encoding="utf-8").splitlines() + if line.strip() +] + + +def update_file(path: Path, deployment: str, image: str, container: str): + lines = path.read_text(encoding="utf-8").splitlines(keepends=True) + changed = False + matched = False + + def indent_of(line: str) -> int: + return len(line) - len(line.lstrip(" ")) + + doc_starts = [ + idx for idx, line in enumerate(lines) if line.lstrip(" ").startswith("- apiVersion:") + ] + doc_starts.append(len(lines)) + + for pos, start in enumerate(doc_starts[:-1]): + end = doc_starts[pos + 1] + block = lines[start:end] + if not any(line.lstrip(" ").strip() == "kind: Deployment" for line in block): + continue + + metadata_idx = next( + (idx for idx, line in enumerate(block) if line.lstrip(" ").startswith("metadata:")), + None, + ) + if metadata_idx is None: + continue + metadata_indent = indent_of(block[metadata_idx]) + next_peer = next( + ( + idx + for idx in range(metadata_idx + 1, len(block)) + if block[idx].strip() and indent_of(block[idx]) <= metadata_indent + ), + len(block), + ) + metadata = block[metadata_idx:next_peer] + if not any( + line.lstrip(" ").startswith("name: ") + and line.split(":", 1)[1].strip() == deployment + for line in metadata + ): + continue + + list_starts = [] + for idx, line in enumerate(block): + stripped = line.lstrip(" ") + if stripped.startswith(("- image:", "- command:", "- name:")): + list_starts.append(idx) + list_starts.append(len(block)) + + for item_pos, item_start in enumerate(list_starts[:-1]): + item_end = list_starts[item_pos + 1] + item = block[item_start:item_end] + if container and not any( + line.lstrip(" ").startswith("name: ") + and line.split(":", 1)[1].strip() == container + for line in item + ): + continue + for rel_idx, line in enumerate(item): + stripped = line.lstrip(" ") + if stripped.startswith("image:") or stripped.startswith("- image:"): + matched = True + abs_idx = start + item_start + rel_idx + old_line = lines[abs_idx] + indent = indent_of(old_line) + newline = "\n" if old_line.endswith("\n") else "" + prefix = old_line[:indent] + marker = "- " if old_line.lstrip(" ").startswith("- image:") else "" + lines[abs_idx] = f"{prefix}{marker}image: {image}{newline}" + changed = changed or lines[abs_idx] != old_line + break + if matched: + break + if matched: + break + + if changed: + path.write_text("".join(lines), encoding="utf-8") + return matched, changed + + +matched_any = False +changed_any = False +for cluster in clusters: + file = Path("apps/openim/overlays") / cluster / "deployments.yaml" + if not file.exists(): + raise SystemExit(f"{file} does not exist") + for deployment, image, container in updates: + matched, changed = update_file(file, deployment, image, container) + status = "updated" if changed else "already current" if matched else "no matching change" + print(f"{cluster}: {deployment} -> {image}: {status}") + matched_any = matched_any or matched + changed_any = changed_any or changed + +if not matched_any: + raise SystemExit("no config-center deployment/container matches were found") + +if not changed_any: + print("All matching config-center images are already current.") +PY + +if git diff --quiet -- apps/openim/overlays; then + echo "No config-center image changes to commit." + exit 0 +fi + +git config user.name "gitea-actions" +git config user.email "gitea-actions@git.imall.cloud" +git add apps/openim/overlays/*/deployments.yaml +git commit -m "Deploy scheduler-backend image after successful build" +git push origin main diff --git a/.gitea/workflows/build-and-push.yml b/.gitea/workflows/build-and-push.yml new file mode 100644 index 0000000..fcf9b08 --- /dev/null +++ b/.gitea/workflows/build-and-push.yml @@ -0,0 +1,120 @@ +name: Build scheduler-backend image + +on: + push: + branches: [dev, main] + paths: + - "**" + workflow_dispatch: + inputs: + tag: + description: "Optional image tag. Defaults to -." + required: false + default: "" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: docker.io + DOCKER_USER: ${{ secrets.DOCKER_USERNAME || 'mag1666888' }} + SERVICE_NAME: scheduler-backend + DEPLOYMENT_NAME: scheduler-backend + CONFIG_CENTER_CONTAINER: scheduler-backend + DOCKERFILE: Dockerfile + INPUT_TAG: ${{ github.event.inputs.tag }} + +jobs: + build-and-push: + runs-on: [openim, config-center] + steps: + - name: Configure SSH + env: + GIT_SSH_PRIVATE_KEY: ${{ secrets.GIT_SSH_PRIVATE_KEY }} + run: | + set -euo pipefail + + if [ -z "${GIT_SSH_PRIVATE_KEY:-}" ]; then + echo "GIT_SSH_PRIVATE_KEY is required." + exit 1 + fi + + mkdir -p ~/.ssh + chmod 700 ~/.ssh + if printf '%s' "$GIT_SSH_PRIVATE_KEY" | grep -q "BEGIN .*PRIVATE KEY"; then + printf '%s\n' "$GIT_SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + else + printf '%s' "$GIT_SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_rsa + fi + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H git.imall.cloud >> ~/.ssh/known_hosts 2>/dev/null || true + chmod 644 ~/.ssh/known_hosts + ssh -o BatchMode=yes -o StrictHostKeyChecking=yes -T git@git.imall.cloud || true + + - name: Checkout + run: | + set -euo pipefail + + BRANCH_NAME="${GITHUB_REF_NAME:-${GITEA_REF_NAME:-main}}" + COMMIT_SHA="${GITHUB_SHA:-${GITEA_SHA:-}}" + + git init . + git remote remove origin 2>/dev/null || true + git remote add origin git@git.imall.cloud:im-group/scheduler-backend.git + if [ -n "$COMMIT_SHA" ]; then + git fetch --depth 1 origin "$COMMIT_SHA" + fi + if [ -z "$COMMIT_SHA" ]; then + git fetch --depth 1 origin "$BRANCH_NAME" + fi + git checkout --force FETCH_HEAD + + - name: Login to Docker Hub + uses: docker/login-action@v3.3.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.8.0 + + - name: Test + run: | + set -euo pipefail + + docker build \ + --target test \ + -f "$DOCKERFILE" \ + . + + - name: Build and push image + run: | + set -euo pipefail + + SHORT_SHA="$(git rev-parse --short=7 HEAD)" + BRANCH_NAME="${GITHUB_REF_NAME:-${GITEA_REF_NAME:-$(git rev-parse --abbrev-ref HEAD)}}" + BRANCH_TAG="$(printf '%s' "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9-]/-/g' | tr '[:upper:]' '[:lower:]')" + if [ -n "${INPUT_TAG:-}" ]; then + VERSION_TAG="$INPUT_TAG" + else + VERSION_TAG="${BRANCH_TAG}-${SHORT_SHA}" + fi + + IMAGE="${DOCKER_USER}/${SERVICE_NAME}:${VERSION_TAG}" + docker buildx build \ + --platform linux/amd64 \ + -f "$DOCKERFILE" \ + -t "$IMAGE" \ + --push \ + . + + echo "Built and pushed $IMAGE for deployment $DEPLOYMENT_NAME" + + CONFIG_CENTER_UPDATES_FILE="$(mktemp)" + export CONFIG_CENTER_UPDATES_FILE + printf '%s|%s|%s\n' "$DEPLOYMENT_NAME" "$IMAGE" "$CONFIG_CENTER_CONTAINER" > "$CONFIG_CENTER_UPDATES_FILE" + + CONFIG_CENTER_CLUSTERS="${CONFIG_CENTER_CLUSTERS:-ack-dev}" \ + bash .gitea/scripts/update-config-center-images.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2246782 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +FROM golang:1.24-alpine AS base + +WORKDIR /src + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +FROM base AS test +RUN go test ./... + +FROM base AS builder +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/api ./cmd/api \ + && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/worker ./cmd/worker \ + && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/seed-sample-job ./cmd/tools/seed-sample-job + +FROM alpine:3.20 + +RUN addgroup -S app && adduser -S app -G app + +WORKDIR /app + +COPY --from=builder /out/api /app/api +COPY --from=builder /out/worker /app/worker +COPY --from=builder /out/seed-sample-job /app/seed-sample-job +COPY job-config-list /app/job-config-list +COPY docker-entrypoint.sh /app/docker-entrypoint.sh + +RUN chmod +x /app/api /app/worker /app/seed-sample-job /app/docker-entrypoint.sh \ + && chown -R app:app /app + +USER app + +ENV HTTP_PORT=16811 \ + WORKER_HTTP_PORT=16812 + +EXPOSE 16811 16812 + +ENTRYPOINT ["/app/docker-entrypoint.sh"] diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..53b93e8 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,31 @@ +#!/bin/sh +set -eu + +/app/api & +api_pid="$!" + +/app/worker & +worker_pid="$!" + +term() { + kill "$api_pid" "$worker_pid" 2>/dev/null || true + wait "$api_pid" "$worker_pid" 2>/dev/null || true +} + +trap term INT TERM + +while :; do + if ! kill -0 "$api_pid" 2>/dev/null; then + wait "$api_pid" || true + term + exit 1 + fi + + if ! kill -0 "$worker_pid" 2>/dev/null; then + wait "$worker_pid" || true + term + exit 1 + fi + + sleep 2 +done