Skip to content

Commit cce0913

Browse files
committed
feat: deploy to k8s
1 parent b410c3b commit cce0913

File tree

9 files changed

+254
-19
lines changed

9 files changed

+254
-19
lines changed

.dockerignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.git
2+
.github
3+
target
4+
*.md

.github/workflows/deploy.yml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
name: Deploy Previewer
2+
3+
on:
4+
push:
5+
branches: [master, main]
6+
workflow_dispatch:
7+
8+
env:
9+
REGISTRY_PROJECT: batbin
10+
IMAGE_NAME: previewer
11+
IMAGE_TAG: ${{ github.sha }}
12+
13+
jobs:
14+
build-and-deploy:
15+
name: Build and Deploy
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- name: Checkout code
20+
uses: actions/checkout@v4
21+
22+
- name: Connect to Tailscale
23+
uses: tailscale/github-action@v2
24+
with:
25+
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
26+
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
27+
tags: tag:ci
28+
29+
- name: Set up Docker Buildx
30+
uses: docker/setup-buildx-action@v3
31+
32+
- name: Login to Container Registry
33+
uses: docker/login-action@v3
34+
with:
35+
registry: ${{ secrets.REGISTRY_URL }}
36+
username: ${{ secrets.REGISTRY_USERNAME }}
37+
password: ${{ secrets.REGISTRY_PASSWORD }}
38+
39+
- name: Build and push Docker image
40+
uses: docker/build-push-action@v5
41+
with:
42+
context: .
43+
push: true
44+
tags: |
45+
${{ secrets.REGISTRY_URL }}/${{ env.REGISTRY_PROJECT }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
46+
${{ secrets.REGISTRY_URL }}/${{ env.REGISTRY_PROJECT }}/${{ env.IMAGE_NAME }}:latest
47+
cache-from: type=gha
48+
cache-to: type=gha,mode=max
49+
50+
- name: Set up kubeconfig
51+
run: |
52+
mkdir -p ~/.kube
53+
echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config
54+
chmod 600 ~/.kube/config
55+
56+
- name: Update deployment image
57+
run: |
58+
sed -i 's|image: previewer:latest|image: ${{ secrets.REGISTRY_URL }}/${{ env.REGISTRY_PROJECT }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}|g' deploy/previewer.yaml
59+
60+
- name: Deploy to Kubernetes
61+
run: |
62+
kubectl apply -f deploy/previewer.yaml
63+
64+
- name: Wait for deployment rollout
65+
run: |
66+
kubectl -n batbin rollout status deployment/batbin-previewer --timeout=120s
67+
68+
- name: Show deployment status
69+
if: always()
70+
run: |
71+
kubectl -n batbin get pods -l app=batbin-previewer

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ conv = "0.3.3"
1414
once_cell = "1.7"
1515
syntect = { version = "4.4", default_features = false, features = ["assets", "dump-load", "regex-fancy", "parsing"] }
1616
futures = "0.3"
17+
reqwest = { version = "0.11", features = ["rustls-tls"], default-features = false }
1718

1819
[profile.release]
1920
opt-level = 3

Dockerfile

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Build stage
2+
FROM rust:1.75-slim AS builder
3+
4+
WORKDIR /app
5+
6+
# Install dependencies
7+
RUN apt-get update && apt-get install -y \
8+
pkg-config \
9+
libssl-dev \
10+
&& rm -rf /var/lib/apt/lists/*
11+
12+
# Copy manifests
13+
COPY Cargo.toml Cargo.lock ./
14+
15+
# Create dummy src to cache dependencies
16+
RUN mkdir -p src assets && echo "fn main() {}" > src/main.rs
17+
RUN cargo build --release || true
18+
RUN rm -rf src
19+
20+
# Copy actual source
21+
COPY src ./src
22+
COPY assets ./assets
23+
COPY TwoDark.tmTheme ./TwoDark.tmTheme
24+
COPY editor.png ./editor.png
25+
26+
# Build release
27+
RUN cargo build --release
28+
29+
# Runtime stage
30+
FROM debian:bookworm-slim
31+
32+
RUN apt-get update && apt-get install -y \
33+
ca-certificates \
34+
&& rm -rf /var/lib/apt/lists/*
35+
36+
WORKDIR /app
37+
38+
# Copy binary
39+
COPY --from=builder /app/target/release/previewer_batbin /app/previewer
40+
41+
# Copy assets
42+
RUN mkdir -p /data
43+
COPY --from=builder /app/TwoDark.tmTheme /data/TwoDark.tmTheme
44+
COPY --from=builder /app/editor.png /data/editor.png
45+
46+
# Environment variables
47+
ENV PASTE_API_URL=https://api-umbra.batbin.me
48+
49+
EXPOSE 3030
50+
51+
ENTRYPOINT ["/app/previewer"]
52+
CMD ["/data"]

deploy/previewer.yaml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: batbin-previewer
5+
namespace: batbin
6+
labels:
7+
app: batbin-previewer
8+
spec:
9+
replicas: 1
10+
selector:
11+
matchLabels:
12+
app: batbin-previewer
13+
template:
14+
metadata:
15+
labels:
16+
app: batbin-previewer
17+
spec:
18+
containers:
19+
- name: previewer
20+
image: previewer:latest
21+
imagePullPolicy: IfNotPresent
22+
env:
23+
- name: PASTE_API_URL
24+
value: "http://batbin.batbin.svc.cluster.local"
25+
ports:
26+
- containerPort: 3030
27+
name: http
28+
resources:
29+
requests:
30+
memory: "128Mi"
31+
cpu: "50m"
32+
limits:
33+
memory: "512Mi"
34+
cpu: "500m"
35+
livenessProbe:
36+
tcpSocket:
37+
port: 3030
38+
initialDelaySeconds: 10
39+
periodSeconds: 10
40+
readinessProbe:
41+
tcpSocket:
42+
port: 3030
43+
initialDelaySeconds: 5
44+
periodSeconds: 5
45+
---
46+
apiVersion: v1
47+
kind: Service
48+
metadata:
49+
name: batbin-previewer
50+
namespace: batbin
51+
spec:
52+
selector:
53+
app: batbin-previewer
54+
ports:
55+
- name: http
56+
port: 80
57+
targetPort: 3030
58+
type: ClusterIP
59+
---
60+
apiVersion: networking.k8s.io/v1
61+
kind: Ingress
62+
metadata:
63+
name: batbin-previewer
64+
namespace: batbin
65+
annotations:
66+
traefik.ingress.kubernetes.io/router.entrypoints: web
67+
spec:
68+
ingressClassName: traefik
69+
rules:
70+
- host: preview-umbra.batbin.me
71+
http:
72+
paths:
73+
- path: /
74+
pathType: Prefix
75+
backend:
76+
service:
77+
name: batbin-previewer
78+
port:
79+
number: 80

src/app.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,28 @@ use image::{ImageBuffer, Rgba};
33
use syntect::parsing::SyntaxSet;
44
use syntect::highlighting::{Theme, ThemeSet};
55
use std::path::Path;
6-
use syntect::dumps::{from_binary};
7-
6+
use syntect::dumps::from_binary;
7+
use reqwest::Client;
88

99
pub struct AppState {
10-
pub pastes: String,
10+
pub api_url: String,
1111
pub base_img: ImageBuffer<Rgba<u8>, Vec<u8>>,
1212
pub font: Font<'static>,
1313
pub syntaxes: SyntaxSet,
1414
pub highlight_theme: Theme,
15+
pub http_client: Client,
1516
}
1617

1718
impl AppState {
18-
pub fn new(pastes: String, base_img: ImageBuffer<Rgba<u8>, Vec<u8>>, font: Font<'static>) -> Self {
19-
let theme_path = format!("{}/{}", &pastes, "TwoDark.tmTheme");
19+
pub fn new(api_url: String, assets_path: String, base_img: ImageBuffer<Rgba<u8>, Vec<u8>>, font: Font<'static>) -> Self {
20+
let theme_path = format!("{}/{}", &assets_path, "TwoDark.tmTheme");
2021
Self {
21-
pastes,
22+
api_url,
2223
base_img,
2324
font,
2425
syntaxes: from_binary::<SyntaxSet>(include_bytes!("../assets/syntaxes.bin")),
25-
highlight_theme: ThemeSet::get_theme(Path::new(&theme_path)).unwrap_or_else(|_| panic!("Couldn't load the TwoDark theme!"))
26+
highlight_theme: ThemeSet::get_theme(Path::new(&theme_path)).unwrap_or_else(|_| panic!("Couldn't load the TwoDark theme!")),
27+
http_client: Client::new(),
2628
}
2729
}
2830
}

src/handlers/mod.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,36 @@
11
use crate::{app::AppState, syntax};
22
use tokio::task;
3-
use tokio::fs;
4-
53

64
async fn get_preview(state: &'static AppState, uuid: String, ext: Option<String>) -> Result<impl warp::Reply, warp::Rejection> {
7-
let f = format!("{}/{}", &state.pastes, uuid);
8-
let code = fs::read_to_string(f).await.map_err(|_| warp::reject())?;
5+
// Fetch paste content from API
6+
let url = format!("{}/api/v2/paste/{}", &state.api_url, uuid);
7+
8+
let response = state.http_client
9+
.get(&url)
10+
.send()
11+
.await
12+
.map_err(|e| {
13+
eprintln!("Failed to fetch paste {}: {}", uuid, e);
14+
warp::reject()
15+
})?;
16+
17+
if !response.status().is_success() {
18+
eprintln!("API returned {} for paste {}", response.status(), uuid);
19+
return Err(warp::reject());
20+
}
21+
22+
let code = response
23+
.text()
24+
.await
25+
.map_err(|e| {
26+
eprintln!("Failed to read response body for {}: {}", uuid, e);
27+
warp::reject()
28+
})?;
929

1030
let rendered = task::spawn_blocking(move || -> Result<Vec<u8>, warp::Rejection> {
1131
let res = syntax::render_preview(state, &code, ext);
1232
return if let Err(e) = res {
13-
eprintln!("err while trying to fetch preview for {}: {}", uuid, e);
33+
eprintln!("err while trying to render preview for {}: {}", uuid, e);
1434
Err(warp::reject())
1535
} else {
1636
Ok(res.unwrap())

src/main.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,30 @@ use std::env;
1313

1414
#[tokio::main]
1515
async fn main() {
16-
let arg = if env::args().count() == 2 {
16+
// Get API URL from environment or use default
17+
let api_url = env::var("PASTE_API_URL")
18+
.unwrap_or_else(|_| "https://api-umbra.batbin.me".to_string());
19+
20+
// Get assets path from command line or use default
21+
let assets_path = if env::args().count() >= 2 {
1722
env::args().nth(1).unwrap()
1823
} else {
19-
panic!("Absolute path of pastes directory *without* trailing slash was not passed!")
24+
"/data".to_string()
2025
};
2126

2227
let mut png_header = HeaderMap::new();
2328
png_header.insert("Content-type", HeaderValue::from_static("image/png"));
2429

25-
let image = image::open(format!("{}/{}", arg, "editor.png"))
30+
let image = image::open(format!("{}/{}", &assets_path, "editor.png"))
2631
.expect("ERR No template image found at provided path")
2732
.to_rgba8();
2833

2934
let font: Font<'static> = Font::try_from_bytes(include_bytes!("FiraCode-Retina.ttf") as &[u8]).unwrap();
3035

31-
utils::init_state(arg, image, font);
36+
utils::init_state(api_url.clone(), assets_path, image, font);
3237

3338
println!("Starting warp server on port 3030");
39+
println!("Using paste API: {}", api_url);
3440

3541
let p = warp::path("p");
3642
let ap = utils::with_state()
@@ -44,6 +50,6 @@ async fn main() {
4450
.and_then(handlers::root_handler_known);
4551

4652
warp::serve(p.and(ap).or(pk.and(apk)).with(warp::reply::with::headers(png_header)))
47-
.run(([127, 0, 0, 1], 3030))
53+
.run(([0, 0, 0, 0], 3030))
4854
.await;
4955
}

src/utils.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ fn init_space_width(font: &Font<'static>) {
1818
SPACE_WIDTH.set(width).ok().expect("INVALID INIT STATE!!");
1919
}
2020

21-
pub fn init_state(pastes: String, base_img: ImageBuffer<Rgba<u8>, Vec<u8>>, font: Font<'static>) {
21+
pub fn init_state(api_url: String, assets_path: String, base_img: ImageBuffer<Rgba<u8>, Vec<u8>>, font: Font<'static>) {
2222
init_space_width(&font);
23-
STATE.set(AppState::new(pastes, base_img, font)).ok().expect("INVALID INIT STATE!!");
23+
STATE.set(AppState::new(api_url, assets_path, base_img, font)).ok().expect("INVALID INIT STATE!!");
2424
}
2525

2626
pub fn space_width() -> i32 {

0 commit comments

Comments
 (0)