aboutsummaryrefslogtreecommitdiff
path: root/cmd/future-sight
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/future-sight')
-rw-r--r--cmd/future-sight/.gitignore1
-rw-r--r--cmd/future-sight/main.go325
-rw-r--r--cmd/future-sight/manifest.dev.yaml195
-rw-r--r--cmd/future-sight/manifest.yaml226
-rwxr-xr-xcmd/future-sight/port-forward.sh10
-rw-r--r--cmd/future-sight/var/.gitignore2
-rw-r--r--cmd/future-sight/yeetfile.js5
7 files changed, 764 insertions, 0 deletions
diff --git a/cmd/future-sight/.gitignore b/cmd/future-sight/.gitignore
new file mode 100644
index 0000000..abe61a7
--- /dev/null
+++ b/cmd/future-sight/.gitignore
@@ -0,0 +1 @@
+.env.prod \ No newline at end of file
diff --git a/cmd/future-sight/main.go b/cmd/future-sight/main.go
new file mode 100644
index 0000000..4948324
--- /dev/null
+++ b/cmd/future-sight/main.go
@@ -0,0 +1,325 @@
+package main
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/base64"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "log/slog"
+ "net/http"
+ "os"
+ "path/filepath"
+ "runtime"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/credentials"
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/nats-io/nats.go"
+ "github.com/redis/go-redis/v9"
+ "golang.org/x/sync/errgroup"
+ "google.golang.org/protobuf/proto"
+ "within.website/x/internal"
+ "within.website/x/internal/xesite"
+ pb "within.website/x/proto/future-sight"
+ "within.website/x/web/useragent"
+)
+
+var (
+ apiBind = flag.String("api-bind", ":8080", "address to bind API to")
+ bind = flag.String("bind", ":8081", "address to bind zipserver to")
+
+ awsAccessKeyID = flag.String("aws-access-key-id", "", "AWS access key ID")
+ awsSecretKey = flag.String("aws-secret-access-key", "", "AWS secret access key")
+ awsEndpointS3 = flag.String("aws-endpoint-url-s3", "http://localhost:9000", "AWS S3 endpoint")
+ awsRegion = flag.String("aws-region", "auto", "AWS region")
+ bucketName = flag.String("bucket-name", "xesite-preview-versions", "bucket to fetch previews from")
+ dataDir = flag.String("data-dir", "./var", "directory to store data in (not permanent)")
+ natsURL = flag.String("nats-url", "nats://localhost:4222", "nats url")
+ usePathStyle = flag.Bool("use-path-style", false, "use path style for S3")
+ valkeyHost = flag.String("valkey-host", "localhost:6379", "host:port for valkey")
+ valkeyPassword = flag.String("valkey-password", "", "password for valkey")
+)
+
+func main() {
+ internal.HandleStartup()
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ creds := credentials.NewStaticCredentialsProvider(*awsAccessKeyID, *awsSecretKey, "")
+
+ s3c := s3.New(s3.Options{
+ AppID: useragent.GenUserAgent("future-sight-push", "https://xeiaso.net"),
+ BaseEndpoint: awsEndpointS3,
+ ClientLogMode: aws.LogRetries | aws.LogRequest | aws.LogResponse,
+ Credentials: creds,
+ EndpointResolver: s3.EndpointResolverFromURL(*awsEndpointS3),
+ //Logger: logging.NewStandardLogger(os.Stderr),
+ UsePathStyle: *usePathStyle,
+ Region: *awsRegion,
+ })
+
+ slog.Debug("details",
+ "awsAccessKeyID", *awsAccessKeyID,
+ "awsSecretKey", *awsSecretKey,
+ "awsEndpointS3", *awsEndpointS3,
+ "awsRegion", *awsRegion,
+ "bucketName", *bucketName,
+ "natsURL", *natsURL,
+ "usePathStyle", *usePathStyle,
+ "valkeyHost", *valkeyHost,
+ "valkeyPassword", *valkeyPassword,
+ )
+
+ vk := redis.NewClient(&redis.Options{
+ Addr: *valkeyHost,
+ Password: *valkeyPassword,
+ DB: 0,
+ })
+ defer vk.Close()
+
+ if _, err := vk.Ping(context.Background()).Result(); err != nil {
+ log.Fatal(err)
+ }
+
+ nc, err := nats.Connect(*natsURL)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer nc.Close()
+
+ zs, err := xesite.NewZipServer(filepath.Join(*dataDir, "current.zip"), *dataDir)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ s := &Server{
+ s3c: s3c,
+ vk: vk,
+ nc: nc,
+ zs: zs,
+ dir: *dataDir,
+ }
+
+ currentVersion, err := vk.Get(ctx, "future-sight:current").Result()
+ if err != nil && err != redis.Nil {
+ log.Fatal(err)
+ }
+
+ if currentVersion != "" {
+ nv := pb.NewVersion{
+ Slug: currentVersion,
+ }
+
+ if err := s.fetchVersion(ctx, &nv); err != nil {
+ slog.Error("can't fetch current version", "err", err)
+ }
+ }
+
+ if _, err := nc.Subscribe("future-sight-push", s.HandleFutureSightPushMsg); err != nil {
+ log.Fatal(err)
+ }
+
+ apiMux := http.NewServeMux()
+ apiMux.HandleFunc("/upload", s.UploadVersion)
+ apiMux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintln(w, "OK")
+ })
+
+ g, _ := errgroup.WithContext(context.Background())
+
+ g.Go(func() error {
+ slog.Info("listening", "for", "api", "addr", *apiBind)
+ return http.ListenAndServe(*apiBind, apiMux)
+ })
+
+ g.Go(func() error {
+ slog.Info("listening", "for", "zipserver", "addr", *bind)
+ return http.ListenAndServe(*bind, zs)
+ })
+
+ if err := g.Wait(); err != nil {
+ slog.Error("error doing work", "err", err)
+ os.Exit(1)
+ }
+}
+
+type Server struct {
+ s3c *s3.Client
+ vk *redis.Client
+ nc *nats.Conn
+ zs *xesite.ZipServer
+ dir string
+}
+
+func (s *Server) UploadVersion(w http.ResponseWriter, r *http.Request) {
+ ctx, cancel := context.WithCancel(r.Context())
+ defer cancel()
+
+ slog.Info("uploading version")
+
+ if err := r.ParseMultipartForm(10 << 24); err != nil {
+ slog.Error("failed to parse form", "err", err)
+ http.Error(w, "failed to parse form", http.StatusBadRequest)
+ return
+ }
+
+ f, header, err := r.FormFile("file")
+ if err != nil {
+ slog.Error("failed to get file", "err", err)
+ http.Error(w, "failed to get file", http.StatusBadRequest)
+ return
+ }
+ defer f.Close()
+
+ slog.Info("got file", "filename", header.Filename)
+
+ fout, err := os.CreateTemp(s.dir, "future-sight-upload-*")
+ if err != nil {
+ slog.Error("failed to create temp file", "err", err)
+ http.Error(w, "failed to create temp file", http.StatusInternalServerError)
+ return
+ }
+ defer fout.Close()
+ defer os.Remove(fout.Name())
+
+ if _, err := io.Copy(fout, f); err != nil {
+ slog.Error("failed to copy file", "err", err)
+ http.Error(w, "failed to copy file", http.StatusInternalServerError)
+ return
+ }
+
+ hash, err := hashFileSha256(fout)
+ if err != nil {
+ slog.Error("failed to hash file", "err", err)
+ http.Error(w, "failed to hash file", http.StatusInternalServerError)
+ return
+ }
+
+ st, err := fout.Stat()
+ if err != nil {
+ slog.Error("failed to stat file", "err", err)
+ http.Error(w, "failed to stat file", http.StatusInternalServerError)
+ return
+ }
+
+ slog.Info("hashed file", "hash", hash)
+
+ if _, err := s.s3c.PutObject(ctx, &s3.PutObjectInput{
+ Bucket: bucketName,
+ Key: aws.String(hash),
+ Body: fout,
+ ContentType: aws.String("application/zip"),
+ ContentLength: aws.Int64(st.Size()),
+ Metadata: map[string]string{
+ "host_os": runtime.GOOS,
+ },
+ }); err != nil {
+ slog.Error("failed to push file", "bucketName", *bucketName, "hash", hash, "err", err)
+ http.Error(w, "failed to push file", http.StatusInternalServerError)
+ return
+ }
+
+ nv := &pb.NewVersion{
+ Slug: hash,
+ }
+
+ if err := s.PushVersion(ctx, nv); err != nil {
+ slog.Error("failed to push version", "slug", nv.Slug, "err", err)
+ http.Error(w, "failed to push version", http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+}
+
+func (s *Server) PushVersion(ctx context.Context, nv *pb.NewVersion) error {
+ slog.Info("got new version", "version", nv)
+
+ msg, err := proto.Marshal(nv)
+ if err != nil {
+ slog.Error("failed to marshal message", "slug", nv.Slug, "err", err)
+ return err
+ }
+
+ if err := s.nc.Publish("future-sight-push", msg); err != nil {
+ slog.Error("failed to publish message", "slug", nv.Slug, "err", err)
+ return err
+ }
+
+ if _, err := s.vk.Set(ctx, "future-sight:current", nv.Slug, 0).Result(); err != nil {
+ slog.Error("failed to set current version", "slug", nv.Slug, "err", err)
+ return err
+ }
+
+ return nil
+}
+
+func (s *Server) HandleFutureSightPushMsg(msg *nats.Msg) {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ nv := new(pb.NewVersion)
+ if err := proto.Unmarshal(msg.Data, nv); err != nil {
+ slog.Error("failed to unmarshal message", "err", err)
+ return
+ }
+
+ if err := s.fetchVersion(ctx, nv); err != nil {
+ slog.Error("failed to handle message", "slug", nv.Slug, "err", err)
+ return
+ }
+
+ slog.Info("handled message", "slug", nv.Slug)
+}
+
+func (s *Server) fetchVersion(ctx context.Context, nv *pb.NewVersion) error {
+ os.Remove(filepath.Join(s.dir, "current.zip"))
+
+ fout, err := os.Create(filepath.Join(s.dir, "current.zip"))
+ if err != nil {
+ return err
+ }
+ defer fout.Close()
+
+ obj, err := s.s3c.GetObject(ctx, &s3.GetObjectInput{
+ Bucket: bucketName,
+ Key: aws.String(nv.Slug),
+ })
+ if err != nil {
+ os.Remove(filepath.Join(s.dir, "current.zip"))
+ slog.Error("failed to get object", "slug", nv.Slug, "err", err)
+ return err
+ }
+ defer obj.Body.Close()
+
+ if _, err := io.Copy(fout, obj.Body); err != nil {
+ os.Remove(filepath.Join(s.dir, "current.zip"))
+ slog.Error("failed to copy object", "slug", nv.Slug, "err", err)
+ return err
+ }
+
+ if err := s.zs.Update(filepath.Join(s.dir, "current.zip")); err != nil {
+ slog.Error("failed to update zipserver", "slug", nv.Slug, "err", err)
+ return err
+ }
+
+ return nil
+}
+
+// hashFileSha256 hashes a file with Sha256 and returns the hash as a base64 encoded string.
+func hashFileSha256(fin *os.File) (string, error) {
+ h := sha256.New()
+ if _, err := io.Copy(h, fin); err != nil {
+ return "", err
+ }
+
+ // rewind the file
+ if _, err := fin.Seek(0, io.SeekStart); err != nil {
+ return "", err
+ }
+
+ return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
+}
diff --git a/cmd/future-sight/manifest.dev.yaml b/cmd/future-sight/manifest.dev.yaml
new file mode 100644
index 0000000..49a71f6
--- /dev/null
+++ b/cmd/future-sight/manifest.dev.yaml
@@ -0,0 +1,195 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: future-sight
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: nats
+ namespace: future-sight
+spec:
+ replicas: 1
+ strategy: {}
+ selector:
+ matchLabels:
+ app: nats
+ template:
+ metadata:
+ labels:
+ app: nats
+ spec:
+ containers:
+ - name: nats
+ image: nats:2-alpine
+ ports:
+ - containerPort: 4222
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: nats
+ namespace: future-sight
+spec:
+ selector:
+ app: nats
+ ports:
+ - port: 4222
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: valkey-secret
+ namespace: future-sight
+ labels:
+ app: valkey
+data:
+ VALKEY_PASSWORD: hunter2
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: valkey
+ namespace: future-sight
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: valkey
+ template:
+ metadata:
+ labels:
+ app: valkey
+ spec:
+ containers:
+ - name: valkey
+ image: 'docker.io/bitnami/valkey:latest'
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 6379
+ envFrom:
+ - configMapRef:
+ name: valkey-secret
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: valkey
+ namespace: future-sight
+ labels:
+ app: valkey
+spec:
+ type: ClusterIP
+ ports:
+ - port: 6379
+ selector:
+ app: valkey
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: minio
+ namespace: future-sight
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 10Gi
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: minio
+ namespace: future-sight
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: minio
+ template:
+ metadata:
+ labels:
+ app: minio
+ spec:
+ volumes:
+ - name: data
+ persistentVolumeClaim:
+ claimName: minio
+ containers:
+ - name: minio
+ volumeMounts:
+ - name: data
+ mountPath: /data
+ image: minio/minio
+ args:
+ - server
+ - /data
+ - --console-address=:9001
+ env:
+ - name: MINIO_ACCESS_KEY
+ value: "minio"
+ - name: MINIO_SECRET_KEY
+ value: "minio123"
+ - name: MINIO_ROOT_USER
+ value: root
+ - name: MINIO_ROOT_PASSWORD
+ value: hunter22
+ ports:
+ - containerPort: 9000
+ name: http
+ - containerPort: 9001
+ name: webui
+---
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: create-bucket
+ namespace: future-sight
+spec:
+ template:
+ spec:
+ containers:
+ - name: create-bucket
+ image: minio/mc
+ command: ["/bin/sh"]
+ args:
+ - "-c"
+ - |
+ /usr/bin/mc config host add k8s http://minio:9000 minio minio123;
+ /usr/bin/mc rm -r --force myminio/xesite-preview-versions;
+ /usr/bin/mc mb myminio/xesite-preview-versions;
+ /usr/bin/mc policy download myminio/xesite-preview-versions;
+ exit 0;
+ restartPolicy: Never
+ backoffLimit: 4
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: minio
+ namespace: future-sight
+spec:
+ type: ClusterIP
+ ports:
+ - name: http
+ port: 80
+ targetPort: 9000
+ protocol: TCP
+ selector:
+ app: minio
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: minio-webui
+ namespace: future-sight
+spec:
+ type: ClusterIP
+ ports:
+ - name: webui
+ port: 80
+ targetPort: 9001
+ protocol: TCP
+ selector:
+ app: minio \ No newline at end of file
diff --git a/cmd/future-sight/manifest.yaml b/cmd/future-sight/manifest.yaml
new file mode 100644
index 0000000..3260bd5
--- /dev/null
+++ b/cmd/future-sight/manifest.yaml
@@ -0,0 +1,226 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: future-sight
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: nats
+ namespace: future-sight
+spec:
+ replicas: 1
+ strategy: {}
+ selector:
+ matchLabels:
+ app: nats
+ template:
+ metadata:
+ labels:
+ app: nats
+ spec:
+ containers:
+ - name: nats
+ image: nats:2-alpine
+ ports:
+ - containerPort: 4222
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: nats
+ namespace: future-sight
+spec:
+ selector:
+ app: nats
+ ports:
+ - port: 4222
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: valkey-secret
+ namespace: future-sight
+ labels:
+ app: valkey
+data:
+ VALKEY_PASSWORD: hunter2
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: valkey
+ namespace: future-sight
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: valkey
+ template:
+ metadata:
+ labels:
+ app: valkey
+ spec:
+ containers:
+ - name: valkey
+ image: 'docker.io/bitnami/valkey:latest'
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 6379
+ envFrom:
+ - configMapRef:
+ name: valkey-secret
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: valkey
+ namespace: future-sight
+ labels:
+ app: valkey
+spec:
+ type: ClusterIP
+ ports:
+ - port: 6379
+ selector:
+ app: valkey
+---
+apiVersion: onepassword.com/v1
+kind: OnePasswordItem
+metadata:
+ name: tigris-creds
+ namespace: future-sight
+ labels:
+ app.kubernetes.io/name: future-sight
+spec:
+ itemPath: "vaults/Kubernetes/items/Tigris creds"
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: future-sight
+ namespace: future-sight
+ labels:
+ app.kubernetes.io/name: future-sight
+data:
+ BUCKET_NAME: xesite-preview-versions
+ DATA_DIR: /cache
+ NATS_URL: nats://nats:4222
+ VALKEY_HOST: valkey:6379
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: future-sight
+ namespace: future-sight
+ labels:
+ app.kubernetes.io/name: future-sight
+ annotations:
+ operator.1password.io/auto-restart: "true"
+spec:
+ replicas: 3
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: future-sight
+ template:
+ metadata:
+ namespace: future-sight
+ labels:
+ app.kubernetes.io/name: future-sight
+ spec:
+ volumes:
+ - name: tigris
+ secret:
+ secretName: tigris-creds
+ - name: cache
+ emptyDir: {}
+ securityContext:
+ fsGroup: 1000
+ containers:
+ - name: main
+ image: ghcr.io/xe/x/future-sight:latest
+ imagePullPolicy: Always
+ resources:
+ limits:
+ cpu: "250m"
+ memory: "512Mi"
+ requests:
+ cpu: "100m"
+ memory: "256Mi"
+ securityContext:
+ runAsUser: 1000
+ runAsGroup: 1000
+ runAsNonRoot: true
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - ALL
+ seccompProfile:
+ type: RuntimeDefault
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: 8080
+ httpHeaders:
+ - name: X-Kubernetes
+ value: "is kinda okay"
+ initialDelaySeconds: 3
+ periodSeconds: 3
+ volumeMounts:
+ - name: tigris
+ mountPath: /run/secrets/tigris
+ - name: cache
+ mountPath: /cache
+ envFrom:
+ - configMapRef:
+ name: valkey-secret
+ - configMapRef:
+ name: future-sight
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: future-sight
+ namespace: future-sight
+ labels:
+ app.kubernetes.io/name: future-sight
+spec:
+ selector:
+ app.kubernetes.io/name: future-sight
+ ports:
+ - protocol: TCP
+ port: 80
+ targetPort: 8081
+ name: web
+ - protocol: TCP
+ port: 8080
+ targetPort: 8080
+ name: api
+ type: ClusterIP
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: future-sight
+ namespace: future-sight
+ labels:
+ app.kubernetes.io/name: future-sight
+ annotations:
+ cert-manager.io/cluster-issuer: "letsencrypt-prod"
+spec:
+ ingressClassName: nginx
+ tls:
+ - hosts:
+ - preview.xeiaso.net
+ secretName: preview-xeiaso-net-tls
+ rules:
+ - host: preview.xeiaso.net
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: future-sight
+ port:
+ name: http \ No newline at end of file
diff --git a/cmd/future-sight/port-forward.sh b/cmd/future-sight/port-forward.sh
new file mode 100755
index 0000000..984ebb7
--- /dev/null
+++ b/cmd/future-sight/port-forward.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+kubectl apply -f manifest.dev.yaml
+
+kubectl port-forward -n future-sight svc/nats 4222:4222 &
+kubectl port-forward -n future-sight deploy/minio 9000:9000 &
+kubectl port-forward -n future-sight deploy/minio 9001:9001 &
+kubectl port-forward -n future-sight svc/valkey 6379:6379 &
+
+wait \ No newline at end of file
diff --git a/cmd/future-sight/var/.gitignore b/cmd/future-sight/var/.gitignore
new file mode 100644
index 0000000..c96a04f
--- /dev/null
+++ b/cmd/future-sight/var/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore \ No newline at end of file
diff --git a/cmd/future-sight/yeetfile.js b/cmd/future-sight/yeetfile.js
new file mode 100644
index 0000000..1b31c6c
--- /dev/null
+++ b/cmd/future-sight/yeetfile.js
@@ -0,0 +1,5 @@
+nix.build(".#docker.future-sight");
+docker.load("./result");
+docker.push(`ghcr.io/xe/x/future-sight`);
+yeet.run("kubectl", "apply", "-f=manifest.yaml");
+yeet.run("sh", "-c", "kubectl rollout restart -n future-sight deployments/future-sight");