diff options
Diffstat (limited to 'cmd/future-sight')
| -rw-r--r-- | cmd/future-sight/.gitignore | 1 | ||||
| -rw-r--r-- | cmd/future-sight/main.go | 325 | ||||
| -rw-r--r-- | cmd/future-sight/manifest.dev.yaml | 195 | ||||
| -rw-r--r-- | cmd/future-sight/manifest.yaml | 226 | ||||
| -rwxr-xr-x | cmd/future-sight/port-forward.sh | 10 | ||||
| -rw-r--r-- | cmd/future-sight/var/.gitignore | 2 | ||||
| -rw-r--r-- | cmd/future-sight/yeetfile.js | 5 |
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"); |
