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
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
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
However, I noticed with TLS, it was not “connecting to the server”.
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
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
Which works just fine to transmit a basic text file
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
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
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
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.