diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..8e27675 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,114 @@ +name: CI + +on: + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + container: + image: gitea.siegeln.net/cameleer/cameleer-build:1 + credentials: + username: cameleer + password: ${{ secrets.REGISTRY_TOKEN }} + steps: + - uses: actions/checkout@v4 + + - name: Build UI + working-directory: ui + run: | + echo '//gitea.siegeln.net/api/packages/cameleer/npm/:_authToken=${REGISTRY_TOKEN}' >> .npmrc + npm ci + npm run build + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + + - name: Build Backend + run: mvn clean package -DskipTests -B + + docker: + needs: build + runs-on: ubuntu-latest + container: + image: gitea.siegeln.net/cameleer/cameleer-docker-builder:1 + credentials: + username: cameleer + password: ${{ secrets.REGISTRY_TOKEN }} + steps: + - name: Checkout + run: | + git clone --depth=1 --branch=${GITHUB_REF_NAME} https://cameleer:${REGISTRY_TOKEN}@gitea.siegeln.net/${GITHUB_REPOSITORY}.git . + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + + - name: Login to registry + run: echo "$REGISTRY_TOKEN" | docker login gitea.siegeln.net -u cameleer --password-stdin + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + + - name: Set up QEMU + run: docker run --rm --privileged gitea.siegeln.net/cameleer/binfmt:1 --install all + + - name: Build and push + run: | + docker buildx create --use --name cibuilder + docker buildx build --platform linux/amd64 \ + --build-arg REGISTRY_TOKEN="$REGISTRY_TOKEN" \ + -t gitea.siegeln.net/cameleer/cameleer-deploy-demo:${{ github.sha }} \ + -t gitea.siegeln.net/cameleer/cameleer-deploy-demo:latest \ + --cache-from type=registry,ref=gitea.siegeln.net/cameleer/cameleer-deploy-demo:buildcache \ + --cache-to type=registry,ref=gitea.siegeln.net/cameleer/cameleer-deploy-demo:buildcache,mode=max \ + --provenance=false \ + --push . + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + + - name: Cleanup + run: docker system prune -af --filter "until=24h" + if: always() + + deploy: + needs: docker + runs-on: ubuntu-latest + container: + image: alpine/k8s:1.32.3 + steps: + - name: Checkout + run: | + git clone --depth=1 --branch=${GITHUB_REF_NAME} https://cameleer:${REGISTRY_TOKEN}@gitea.siegeln.net/${GITHUB_REPOSITORY}.git . + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + + - name: Configure kubectl + run: | + mkdir -p ~/.kube + echo "$KUBECONFIG_B64" | base64 -d > ~/.kube/config + env: + KUBECONFIG_B64: ${{ secrets.KUBECONFIG_BASE64 }} + + - name: Create deployer kubeconfig secret + run: | + kubectl create secret generic deployer-kubeconfig \ + --namespace=cameleer \ + --from-literal=config="$(echo "$KUBECONFIG_B64" | base64 -d)" \ + --dry-run=client -o yaml | kubectl apply -f - + env: + KUBECONFIG_B64: ${{ secrets.KUBECONFIG_BASE64 }} + + - name: Deploy + run: | + kubectl apply -f deploy/deploy-demo.yaml + + kubectl -n cameleer set image deployment/cameleer-deploy-demo \ + deploy-demo=gitea.siegeln.net/cameleer/cameleer-deploy-demo:${{ github.sha }} + + kubectl -n cameleer rollout status deployment/cameleer-deploy-demo --timeout=120s + + - name: Print URL + run: | + echo "====================================" + echo "Deploy Demo available at:" + echo "http://deploy.cameleer.siegeln.net" + echo "http://192.168.50.86:30082" + echo "====================================" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3a36ab4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM --platform=$BUILDPLATFORM maven:3.9-eclipse-temurin-21 AS backend-build +WORKDIR /build +COPY pom.xml . +RUN mvn dependency:go-offline -B || true +COPY src/ src/ +RUN mvn clean package -DskipTests -B + +FROM --platform=$BUILDPLATFORM node:22-alpine AS ui-build +WORKDIR /ui +ARG REGISTRY_TOKEN +COPY ui/package.json ui/package-lock.json ui/.npmrc ./ +RUN echo "//gitea.siegeln.net/api/packages/cameleer/npm/:_authToken=${REGISTRY_TOKEN}" >> .npmrc && \ + npm ci +COPY ui/ . +RUN npm run build + +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY --from=backend-build /build/target/cameleer-deploy-demo-*.jar /app/server.jar +COPY --from=ui-build /ui/dist /app/static + +EXPOSE 8082 +ENV TZ=UTC +ENTRYPOINT exec java -Duser.timezone=UTC \ + -Dserver.port=8082 \ + -Dspring.web.resources.static-locations=file:/app/static/ \ + -jar /app/server.jar diff --git a/deploy/deploy-demo.yaml b/deploy/deploy-demo.yaml new file mode 100644 index 0000000..850914e --- /dev/null +++ b/deploy/deploy-demo.yaml @@ -0,0 +1,143 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: cameleer-demo +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cameleer-deploy-demo + namespace: cameleer + labels: + app: cameleer-deploy-demo +spec: + replicas: 1 + selector: + matchLabels: + app: cameleer-deploy-demo + template: + metadata: + labels: + app: cameleer-deploy-demo + spec: + imagePullSecrets: + - name: gitea-registry + serviceAccountName: cameleer-deployer + containers: + - name: deploy-demo + image: gitea.siegeln.net/cameleer/cameleer-deploy-demo:latest + ports: + - containerPort: 8082 + env: + - name: CAMELEER_SERVER_URL + value: "http://cameleer3-server.cameleer.svc:8081" + - name: CAMELEER_BOOTSTRAP_TOKEN + valueFrom: + secretKeyRef: + name: cameleer-auth + key: CAMELEER_AUTH_TOKEN + - name: CAMELEER_REGISTRY + value: "gitea.siegeln.net/cameleer/demo-apps" + - name: CAMELEER_AGENT_MAVEN_URL + value: "https://gitea.siegeln.net/api/packages/cameleer/maven/com/cameleer3/cameleer3-agent/1.0-SNAPSHOT/cameleer3-agent-1.0-SNAPSHOT.jar" + - name: CAMELEER_DEMO_NAMESPACE + value: "cameleer-demo" + - name: CAMELEER_SERVER_UI + value: "http://192.168.50.86:30090" + - name: DOCKER_HOST + value: "unix:///var/run/docker.sock" + volumeMounts: + - name: docker-sock + mountPath: /var/run/docker.sock + - name: kubectl-config + mountPath: /root/.kube + readOnly: true + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: /api/apps + port: 8082 + initialDelaySeconds: 20 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /api/apps + port: 8082 + initialDelaySeconds: 10 + periodSeconds: 5 + volumes: + - name: docker-sock + hostPath: + path: /var/run/docker.sock + - name: kubectl-config + secret: + secretName: deployer-kubeconfig +--- +apiVersion: v1 +kind: Service +metadata: + name: cameleer-deploy-demo + namespace: cameleer +spec: + type: NodePort + selector: + app: cameleer-deploy-demo + ports: + - port: 8082 + targetPort: 8082 + nodePort: 30082 +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: cameleer-deployer + namespace: cameleer +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cameleer-deployer +rules: + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "list", "create", "update", "patch", "delete"] + - apiGroups: [""] + resources: ["pods", "services", "namespaces"] + verbs: ["get", "list", "create", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: cameleer-deployer +subjects: + - kind: ServiceAccount + name: cameleer-deployer + namespace: cameleer +roleRef: + kind: ClusterRole + name: cameleer-deployer + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cameleer-deploy-demo + namespace: cameleer +spec: + rules: + - host: deploy.cameleer.siegeln.net + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: cameleer-deploy-demo + port: + number: 8082 diff --git a/src/main/java/com/cameleer/deploy/config/SpaConfig.java b/src/main/java/com/cameleer/deploy/config/SpaConfig.java new file mode 100644 index 0000000..546b613 --- /dev/null +++ b/src/main/java/com/cameleer/deploy/config/SpaConfig.java @@ -0,0 +1,25 @@ +package com.cameleer.deploy.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Serves the SPA frontend. All non-API, non-static paths forward to index.html + * so that client-side routing works. + */ +@Configuration +public class SpaConfig implements WebMvcConfigurer { + @Override + public void addViewControllers(ViewControllerRegistry registry) { + // Forward non-API paths to index.html for SPA routing + registry.addViewController("/").setViewName("forward:/index.html"); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/**") + .addResourceLocations("classpath:/static/", "file:static/"); + } +}