OS Apps: Rustpad and Rustypaste

Published: Oct 3, 2024 by Isaac Johnson

Earlier this week we talked about some new local editors like Zed, Void and using Samsung apps via our Phone. Today, we’ll look at some newer hosted editors we can run in Docker or Kubernetes such as Rustpad.

I’ll also look at another Rust-based Pastebin tool, Rustypaste. We will launch that in Docker and then look to convert that into Kubernetes.

Rustpad

I found out about Rustpad from this MariusHosting post earlier this month and wanted to give it a try.

To start, I’ll try just running in Docker (as the https://github.com/ekzhang/rustpad)

$ docker run --rm -dp 3030:3030 ekzhang/rustpad
Unable to find image 'ekzhang/rustpad:latest' locally
latest: Pulling from ekzhang/rustpad
c645a60bff07: Pull complete
6f7ecbc35a88: Pull complete
Digest: sha256:b4237c9bd297341bb3f1b0e6a0747da9d3365d96910fd3083e3bc160702a8faf
Status: Downloaded newer image for ekzhang/rustpad:latest
6474fb7edeab8f16c266013c23303c0c6d5dcd9f26e9684485218d395d40e318

which fired right up

/content/images/2024/10/rustpad-01.png

We can see that with a shared URL, we can collaborate with others

I sort of expected it to require a hostname, but indeed the code just shows PORT as configurable.

Let’s see if we can host this in Kubernetes.

I’ll first fire up a manifest to deliver a deployment with a service:

$ kubectl apply -f ./kubernetes.yaml
deployment.apps/rustpad-deployment created
service/rustpad-service created

We can the verify it is running

$ kubectl get svc rustpad-service
NAME              TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
rustpad-service   ClusterIP   10.43.83.244   <none>        80/TCP    17s
$ kubectl get pods -l app=rustpad
NAME                                 READY   STATUS    RESTARTS   AGE
rustpad-deployment-8799874d6-g2jxb   1/1     Running   0          26s

We can now access on the service

$ kubectl port-forward svc/rustpad-service 8888:80
Forwarding from 127.0.0.1:8888 -> 3030
Forwarding from [::1]:8888 -> 3030
Handling connection for 8888
Handling connection for 8888
Handling connection for 8888

/content/images/2024/10/rustpad-02.png

I then need to setup a DNS entry


and then apply the Ingress.yaml

$ cat ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: gcpleprod2
    ingress.kubernetes.io/proxy-body-size: "0"
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.org/client-max-body-size: "0"
    nginx.org/proxy-connect-timeout: "3600"
    nginx.org/proxy-read-timeout: "3600"
  name: rustpadgcpingress
spec:
  rules:
  - host: rustpad.steeped.space
    http:
      paths:
      - backend:
          service:
            name: rustpad-service
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - rustpad.steeped.space
    secretName: rustpadgcp-tls

$ kubectl apply -f ./ingress.yaml
ingress.networking.k8s.io/rustpadgcpingress created

When I see the cert satisified

$ kubectl get cert rustpadgcp-tls
NAME             READY   SECRET           AGE
rustpadgcp-tls   True    rustpadgcp-tls   74s

I can reach it on our DNS name; rustpad.steeped.space

/content/images/2024/10/rustpad-03.png

However, I noticed with TLS, it was not “connecting to the server”.

/content/images/2024/10/rustpad-04.png

After some testing including switching to just HTTP, I discovered this app really needs the websocket redirect

$ cat ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: gcpleprod2
    ingress.kubernetes.io/proxy-body-size: "0"
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.org/client-max-body-size: "0"
    nginx.org/proxy-connect-timeout: "3600"
    nginx.org/proxy-read-timeout: "3600"
    nginx.org/websocket-services: rustpad-service
  name: rustpadgcpingress
spec:
  rules:
  - host: rustpad.steeped.space
    http:
      paths:
      - backend:
          service:
            name: rustpad-service
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - rustpad.steeped.space
    secretName: rustpadgcp-tls

$ kubectl delete -f ./ingress.yaml
ingress.networking.k8s.io "rustpadgcpingress" deleted
$ kubectl apply -f ./ingress.yaml
ingress.networking.k8s.io/rustpadgcpingress created

Since this is such a nice lightweight app, I don’t mind leaving the URL live at https://rustpad.steeped.space/.

RustyPaste

Keeping it going with Rust-based utilities, I had on my list RustyPaste as a pastebin alternative to explore.

The Docker invokation is pretty straightforward

$ docker run --rm -d \
  -v "$(pwd)/upload/":/app/upload \
  -v "$(pwd)/config.toml":/app/config.toml \
  --env-file "$(pwd)/.env" \
  -e "RUST_LOG=debug" \
  -p 8000:8000 \
  --name rustypaste \
  orhunp/rustypaste

I’ll clone the repo, make an upload dir and run it interactively to start

$ git clone https://github.com/orhun/rustypaste.git
$ cd rustypaste && mkdir upload && docker run --rm -v "$(pwd)/upload/":/app/upload   -v "$(pwd)/config.toml":/app/config.toml   --env-file "$(pwd)/.env"   -e "RUST_LOG=debug"   -p 8000:8000   --name rustypaste orhunp/rustypaste
Unable to find image 'orhunp/rustypaste:latest' locally
latest: Pulling from orhunp/rustypaste
374c2bae87f4: Pull complete
177c75011a7a: Pull complete
Digest: sha256:73f76bf697a361342a2576bf9465f1dd288d70039dff44c00bbcce71a1a33d4d
Status: Downloaded newer image for orhunp/rustypaste:latest
2024-10-02T11:17:38.514038Z DEBUG rustypaste: Running cleanup...
2024-10-02T11:17:38.514812Z  INFO rustypaste: Server is running at 0.0.0.0:8000
2024-10-02T11:17:38.514838Z  INFO actix_server::builder: starting 16 workers
2024-10-02T11:17:38.514852Z  INFO actix_server::server: Actix runtime found; starting in Actix runtime

And we can see it running

/content/images/2024/10/rustypaste-01.png

I can use curl locally to upload a file

builder@DESKTOP-QADGF36:~/Workspaces/rustypaste$ curl -X POST -F 'file=@README.md' http://localhost:8000
http://localhost:8000/unstateable-kieth.md

/content/images/2024/10/rustypaste-02.png

Which works just fine to transmit a basic text file

/content/images/2024/10/rustypaste-03.png

I tried a basic IWR in Powershell:

PS C:\Users\isaac> Invoke-WebRequest -Uri http://127.0.0.1:8000 -Method POST -InFile "C:\Users\isaac\OneDrive\Documents\MyInput.json"
Invoke-WebRequest : ContentTypeIncompatible
At line:1 char:1
+ Invoke-WebRequest -Uri http://127.0.0.1:8000 -Method POST -InFile "C: ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebExc
   eption
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

PS C:\Users\isaac\Downloads> Invoke-WebRequest -Uri http://localhost:8000 -Method POST -InFile "C:\Users\isaac\Downloads\wslogo.png"
Invoke-WebRequest : ContentTypeIncompatible
At line:1 char:1
+ Invoke-WebRequest -Uri http://localhost:8000 -Method POST -InFile "C: ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebExc
   eption
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

I tried base64’ing the image to see if that would work

PS C:\Users\isaac> $base64Image = [convert]::ToBase64String((get-content "C:\Users\isaac\Downloads\wslogo.png" -encoding
 byte))
PS C:\Users\isaac> Invoke-WebRequest -uri http://localhost:8000 -Method POST -Body $base64Image -ContentType "application/base64"
Invoke-WebRequest : ContentTypeIncompatible
At line:1 char:1
+ Invoke-WebRequest -uri http://localhost:8000 -Method POST -Body $base ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebExc
   eption
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand
PS C:\Users\isaac>

Try as I might, i could not get Powershell to do what I wanted, but curl was super duper

/content/images/2024/10/rustypaste-04.png

apiVersion: v1
kind: ConfigMap
metadata:
  name: rustypaste-config
data:
  config.toml: |
    [config]
    refresh_rate = "1s"

    [server]
    address = "127.0.0.1:8000"
    #url = "https://paste.example.com"
    #workers=4
    max_content_length = "10MB"
    upload_path = "./upload"
    timeout = "30s"
    expose_version = false
    expose_list = false
    #auth_tokens = [
    #  "super_secret_token1",
    #  "super_secret_token2",
    #]
    #delete_tokens = [
    #  "super_secret_token1",
    #  "super_secret_token3",
    #]
    handle_spaces = "replace" # or "encode"

    [landing_page]
    text = """
    ┬─┐┬ ┬┌─┐┌┬┐┬ ┬┌─┐┌─┐┌─┐┌┬┐┌─┐
    ├┬┘│ │└─┐ │ └┬┘├─┘├─┤└─┐ │ ├┤
    ┴└─└─┘└─┘ ┴  ┴ ┴  ┴ ┴└─┘ ┴ └─┘

    Submit files via HTTP POST here:
        curl -F 'file=@example.txt' <server>
    This will return the URL of the uploaded file.

    The server administrator might remove any pastes that they do not personally
    want to host.

    If you are the server administrator and want to change this page, just go
    into your config file and change it! If you change the expiry time, it is
    recommended that you do.

    By default, pastes expire every hour. The server admin may or may not have
    changed this.

    Check out the GitHub repository at https://github.com/orhun/rustypaste
    Command line tool is available  at https://github.com/orhun/rustypaste-cli
    """
    #file = "index.txt"
    content_type = "text/plain; charset=utf-8"

    [paste]
    random_url = { type = "petname", words = 2, separator = "-" }
    #random_url = { type = "alphanumeric", length = 8 }
    #random_url = { type = "alphanumeric", length = 6, suffix_mode = true }
    default_extension = "txt"
    mime_override = [
      { mime = "image/jpeg", regex = "^.*\\.jpg$" },
      { mime = "image/png", regex = "^.*\\.png$" },
      { mime = "image/svg+xml", regex = "^.*\\.svg$" },
      { mime = "video/webm", regex = "^.*\\.webm$" },
      { mime = "video/x-matroska", regex = "^.*\\.mkv$" },
      { mime = "application/octet-stream", regex = "^.*\\.bin$" },
      { mime = "text/plain", regex = "^.*\\.(log|txt|diff|sh|rs|toml)$" },
    ]
    mime_blacklist = [
      "application/x-dosexec",
      "application/java-archive",
      "application/java-vm",
    ]
    duplicate_files = true
    # default_expiry = "1h"
    delete_expired_files = { enabled = true, interval = "1h" }
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: rustypaste-upload-pvc
spec:
  storageClassName: local-path
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: rustypaste-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: rustypaste
  template:
    metadata:
      labels:
        app: rustypaste
    spec:
      containers:
      - name: rustypaste-container
        image: orhunp/rustypaste
        ports:
        - containerPort: 8000
        env:
        - name: RUST_LOG
          value: "debug"
        volumeMounts:
        - name: config-volume
          mountPath: /app/config.toml
          subPath: config.toml
        - name: upload-volume
          mountPath: /app/upload
      volumes:
      - name: config-volume
        configMap:
          name: rustypaste-config
      - name: upload-volume
        persistentVolumeClaim:
          claimName: rustypaste-upload-pvc

I can then apply that

$ kubectl apply -f ./rustypaste.yaml
configmap/rustypaste-config created
persistentvolumeclaim/rustypaste-upload-pvc created
deployment.apps/rustypaste-deployment created

I can now portforward

builder@LuiGi:~/Workspaces/jekyll-blog$ kubectl logs rustypaste-deployment-5f6f4fb9bb-2v59k
2024-10-03T00:34:51.314332Z DEBUG rustypaste: Running cleanup...
2024-10-03T00:34:51.314530Z  INFO rustypaste: Server is running at 0.0.0.0:8000
2024-10-03T00:34:51.314539Z  INFO actix_server::builder: starting 4 workers
2024-10-03T00:34:51.314570Z  INFO actix_server::server: Actix runtime found; starting in Actix runtime
builder@LuiGi:~/Workspaces/jekyll-blog$ kubectl port-forward rustypaste-deployment-5f6f4fb9bb-2v59k 8888:8000
Forwarding from 127.0.0.1:8888 -> 8000
Forwarding from [::1]:8888 -> 8000
Handling connection for 8888

and test a post

$ curl -X POST -F 'file=@README.md' http://localhost:8888
http://localhost:8888/antievolutional-margherita.md

I think it would be easier if I exposed it externally with TLS.

To do this, I’ll create an A record in GCP CloudDNS

$ gcloud dns --project=myanthosproject2 record-sets create rustypaste.steeped.space --zone="steepedspace" --type="A" --ttl="300" --rrdatas="75.73.224.240"
NAME                       TYPE  TTL  DATA
rustypaste.steeped.space.  A     300  75.73.224.240

Let’s then add a service and ingress that can use it

$ cat ./rustypaste.ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: gcpleprod2
    ingress.kubernetes.io/proxy-body-size: "0"
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.org/client-max-body-size: "0"
    nginx.org/proxy-connect-timeout: "3600"
    nginx.org/proxy-read-timeout: "3600"
    nginx.org/websocket-services: rustypaste-service
  name: rustypastegcpingress
spec:
  rules:
  - host: rustypaste.steeped.space
    http:
      paths:
      - backend:
          service:
            name: rustypaste-service
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - rustypaste.steeped.space
    secretName: rustypastegcp-tls
---
apiVersion: v1
kind: Service
metadata:
  name: rustypaste-service
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 8000
  selector:
    app: rustypaste
  type: ClusterIP

And applied

$ kubectl apply -f ./rustypaste.ingress.yaml
ingress.networking.k8s.io/rustypastegcpingress created
service/rustypaste-service created

When I see the cert satisified

$ kubectl get cert rustypastegcp-tls
NAME                READY   SECRET              AGE
rustypastegcp-tls   True    rustypastegcp-tls   2m47s

I can try with the URL

$ curl -X POST -F 'file=@README.md' https://rustypaste.steeped.space
https://rustypaste.steeped.space/promissory-lien.md

Which seemed to work just dandy

/content/images/2024/10/rustypaste-05.png

But yet on a completely different Windows laptop, i still failed to get IwR to work in Powershell

PS C:\Users\isaac\Documents> Invoke-WebRequest -Uri https://rustypaste.steeped.space -Method POST -InFile "C:\Users\isaac\Documents\index2.html"
Invoke-WebRequest : ContentTypeIncompatible
At line:1 char:1
+ Invoke-WebRequest -Uri https://rustypaste.steeped.space -Method POST  ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebExc
   eption
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

However, I could use the WebClient in Powershell to accomplish the same thing

PS C:\Users\isaac\Documents> $wc = New-Object System.Net.WebClient
PS C:\Users\isaac\Documents> $resp = $wc.UploadFile("https://rustypaste.steeped.space","C:\Users\isaac\Documents\index2.html")
PS C:\Users\isaac\Documents> $responseString = [System.Text.Encoding]::UTF8.GetString($resp)
PS C:\Users\isaac\Documents> Write-Output $responseString
https://rustypaste.steeped.space/morose-sydney.html

Which worked to upload the TiddlyWiki

/content/images/2024/10/rustypaste-06.png

Summary

We checked out Rustpad which turned out to be more of a very lightweight code collaboration tool (rather than an editor). It worked great, though I would likely use Project IDX or hosted Code instead as those have terminals and file browsers.

We then looked at another rust-based service, this time something in the pastebin camp that can do a basic POST and share for URLs. it was easy to run in Docker and then host in Kubernetes.

Kubernetes Docker Opensource Rustypaste Rustpad

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes