diff --git a/.github/actions/build-and-publish-server/action.yml b/.github/actions/build-and-publish-server/action.yml new file mode 100644 index 000000000..225d268f7 --- /dev/null +++ b/.github/actions/build-and-publish-server/action.yml @@ -0,0 +1,55 @@ +name: Build and Publish Server +description: Builds and publishes the docker image for the Spacedrive server +inputs: + gh_token: + description: 'A Github PAT' + required: true +runs: + using: 'composite' + steps: + - name: Log in to the Container registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ inputs.gh_token }} + + - name: Build Server + shell: bash + run: | + cargo build --release -p server + cp ./target/release/server ./apps/server/server + + - name: Determine image name & tag + shell: bash + run: | + if [ "$GITHUB_EVENT_NAME" == "release" ]; then + export IMAGE_TAG=${GITHUB_REF##*/} + else + export IMAGE_TAG=$(git rev-parse --short "$GITHUB_SHA") + fi + export GITHUB_REPOSITORY_LOWER=$(echo $GITHUB_REPOSITORY | awk '{print tolower($0)}') + export IMAGE_NAME="ghcr.io/$GITHUB_REPOSITORY_LOWER/server" + echo "IMAGE_NAME=$IMAGE_NAME" >> $GITHUB_ENV + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV + echo "Building $IMAGE_NAME:$IMAGE_TAG" + + - name: Build & push Docker image + shell: bash + run: | + docker build ./apps/server --tag $IMAGE_NAME:$IMAGE_TAG + docker push $IMAGE_NAME:$IMAGE_TAG + + - name: Tag & push image as latest staging image + if: github.event_name != 'release' + shell: bash + run: | + docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:staging + docker push $IMAGE_NAME:staging + + - name: Tag & push image as latest production image + if: github.event_name == 'release' + shell: bash + run: | + docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:production + docker push $IMAGE_NAME:production \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d7d14b37..8af60f121 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,4 +114,19 @@ jobs: with: projectPath: apps/desktop env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and publish server + if: matrix.platform == 'ubuntu-latest' + uses: ./.github/actions/build-and-publish-server + with: + gh_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Deploy Spacedrive Server to Kubernetes + if: matrix.platform == 'ubuntu-latest' + env: + K8S_KUBECONFIG: ${{ secrets.K8S_KUBECONFIG }} + run: | + mkdir -p ~/.kube + echo "$K8S_KUBECONFIG" > ~/.kube/config 2>&1 + kubectl rollout restart deployment/sdserver-deployment diff --git a/.github/workflows/org-readme.yml b/.github/workflows/org-readme.yml index 10d7d42f2..b12ca9b0a 100644 --- a/.github/workflows/org-readme.yml +++ b/.github/workflows/org-readme.yml @@ -4,6 +4,8 @@ on: push: branches: - main + paths: + - README.md workflow_dispatch: jobs: diff --git a/apps/desktop/src/index.html b/apps/desktop/src/index.html index 8ed05b99a..f14a6b3d5 100644 --- a/apps/desktop/src/index.html +++ b/apps/desktop/src/index.html @@ -1,7 +1,6 @@ - diff --git a/apps/landing/src/App.tsx b/apps/landing/src/App.tsx index 9f3feae88..bb685ef85 100644 --- a/apps/landing/src/App.tsx +++ b/apps/landing/src/App.tsx @@ -93,7 +93,7 @@ function App() { onError={(e) => { setShowApp(false); }} - src="http://localhost:8002?library_id=9068c6ec-cf90-451b-bb30-4174781e7bc6" + src={`${import.meta.env.VITE_SDWEB_BASE_URL || "http://localhost:8002"}?library_id=9068c6ec-cf90-451b-bb30-4174781e7bc6`} /> )}
+ +interface ImportMetaEnv { + readonly VITE_SDWEB_BASE_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile index fa1728122..308c0a2be 100644 --- a/apps/server/Dockerfile +++ b/apps/server/Dockerfile @@ -19,10 +19,11 @@ RUN mkdir /data ENV DATA_DIR /data # Drop privledges to non-root user -RUN adduser --system --no-create-home --shell /usr/sbin/nologin $USER && \ +RUN groupadd -g 1001 $USER && \ + adduser --system --no-create-home --shell /usr/sbin/nologin --uid 1001 --gid 1001 $USER && \ chown -R $USER /data && \ chmod -R 770 /data USER $USER # Run the CLI when the container is started -ENTRYPOINT [ "/sdserver" ] +ENTRYPOINT [ "/sdserver" ] \ No newline at end of file diff --git a/apps/server/k8s/infrastructure.yaml b/apps/server/k8s/infrastructure.yaml new file mode 100644 index 000000000..f5a100ab1 --- /dev/null +++ b/apps/server/k8s/infrastructure.yaml @@ -0,0 +1,42 @@ +# Infrastructure setups up the Kubernetes cluster for Spacedrive! +# +# To get the service account token use the following: +# ```bash +# TOKENNAME=`kubectl -n spacedrive get sa/spacedrive-ci -o jsonpath='{.secrets[0].name}'` +# kubectl -n spacedrive get secret $TOKENNAME -o jsonpath='{.data.token}' | base64 -d +# ``` + +apiVersion: v1 +kind: Namespace +metadata: + name: spacedrive +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: spacedrive-ci + namespace: spacedrive +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: spacedrive-ns-full + namespace: spacedrive +rules: +- apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: spacedrive-ci-rb + namespace: spacedrive +subjects: +- kind: ServiceAccount + name: spacedrive-ci + namespace: spacedrive +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: spacedrive-ns-full \ No newline at end of file diff --git a/apps/server/k8s/sdserver.yaml b/apps/server/k8s/sdserver.yaml new file mode 100644 index 000000000..d00d70a18 --- /dev/null +++ b/apps/server/k8s/sdserver.yaml @@ -0,0 +1,118 @@ +# This will deploy the Spacedrive Server container to the `spacedrive`` namespace on Kubernetes. + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: sdserver-ingress + namespace: spacedrive + labels: + app.kubernetes.io/name: sdserver + app.kubernetes.io/component: webserver + annotations: + traefik.ingress.kubernetes.io/router.tls.certresolver: le + traefik.ingress.kubernetes.io/router.middlewares: kube-system-antiseo@kubernetescrd +spec: + rules: + - host: spacedrive.otbeaumont.me + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: sdserver-service + port: + number: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: sdserver-service + namespace: spacedrive + labels: + app.kubernetes.io/name: sdserver + app.kubernetes.io/component: webserver +spec: + ports: + - port: 8080 + targetPort: 8080 + protocol: TCP + selector: + app.kubernetes.io/name: sdserver + app.kubernetes.io/component: webserver +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: sdserver-pvc + namespace: spacedrive +spec: + accessModes: + - ReadWriteOnce + storageClassName: local-path + resources: + requests: + storage: 512M +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sdserver-deployment + namespace: spacedrive + labels: + app.kubernetes.io/name: sdserver + app.kubernetes.io/component: webserver +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: sdserver + app.kubernetes.io/component: webserver + template: + metadata: + labels: + app.kubernetes.io/name: sdserver + app.kubernetes.io/component: webserver + spec: + restartPolicy: Always + # refer to Dockerfile to find securityContext values + securityContext: + runAsUser: 101 + runAsGroup: 101 + fsGroup: 101 + containers: + - name: sdserver + image: ghcr.io/oscartbeaumont/spacedrive/server:staging + imagePullPolicy: Always + ports: + - containerPort: 8080 + volumeMounts: + - name: data-volume + mountPath: /data + securityContext: + allowPrivilegeEscalation: false + resources: + limits: + memory: 100Mi + cpu: 100m + requests: + memory: 5Mi + cpu: 10m + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 10 + failureThreshold: 4 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 20 + failureThreshold: 3 + periodSeconds: 10 + volumes: + - name: data-volume + persistentVolumeClaim: + claimName: sdserver-pvc diff --git a/apps/server/src/main.rs b/apps/server/src/main.rs index 835b10391..305cac90c 100644 --- a/apps/server/src/main.rs +++ b/apps/server/src/main.rs @@ -5,7 +5,10 @@ use actix::{ Actor, AsyncContext, ContextFutureSpawner, Handler, Message, StreamHandler, WrapFuture, }; -use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer}; +use actix_web::{ + get, http::StatusCode, web, App, Error, HttpRequest, HttpResponse, HttpServer, + Responder, +}; use actix_web_actors::ws; use serde::{Deserialize, Serialize}; @@ -123,7 +126,18 @@ impl Handler for Socket { } } -async fn index( +#[get("/")] +async fn index() -> impl Responder { + format!("Spacedrive Server!") +} + +#[get("/health")] +async fn healthcheck() -> impl Responder { + format!("OK") +} + +#[get("/ws")] +async fn ws_handler( req: HttpRequest, stream: web::Payload, event_receiver: web::Data>, @@ -141,6 +155,10 @@ async fn index( resp } +async fn not_found() -> impl Responder { + HttpResponse::build(StatusCode::OK).body("We're past the event horizon...") +} + #[actix_web::main] async fn main() -> std::io::Result<()> { let (event_receiver, controller) = setup().await; @@ -150,7 +168,10 @@ async fn main() -> std::io::Result<()> { App::new() .app_data(event_receiver.clone()) .app_data(controller.clone()) - .route("/ws", web::get().to(index)) + .service(index) + .service(healthcheck) + .service(ws_handler) + .default_service(web::route().to(not_found)) }) .bind(("0.0.0.0", 8080))? .run() diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 78ff96559..8a0b8512b 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -4,7 +4,9 @@ import SpacedriveInterface from '@sd/interface'; import { ClientCommand, ClientQuery, CoreEvent } from '@sd/core'; import { BaseTransport } from '@sd/client'; -const websocket = new WebSocket('ws://localhost:8080/ws'); +const websocket = new WebSocket( + import.meta.env.VITE_SDSERVER_BASE_URL || 'wss://spacedrive.otbeaumont.me/ws' +); const randomId = () => Math.random().toString(36).slice(2); diff --git a/apps/web/src/env.d.ts b/apps/web/src/env.d.ts new file mode 100644 index 000000000..be63506a6 --- /dev/null +++ b/apps/web/src/env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_SDSERVER_BASE_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +}