DodoApp: Implement dev proxy mode
With dev proxy user can substitute any service with their own local
machine. In which case dodo will run proxy server on the platform
which will forward all requests to the configured address.
When VPN is enabled, dodo will run tailscale sidecar in the proxy pod.
Change-Id: I12592ae77d2e88e0582c8fe1e0f82e5fd24e02cb
diff --git a/core/headscale/Dockerfile b/core/headscale/Dockerfile
index d53b665..c93ca62 100644
--- a/core/headscale/Dockerfile
+++ b/core/headscale/Dockerfile
@@ -2,4 +2,4 @@
ARG TARGETARCH
-COPY server_${TARGETARCH} /usr/bin/headscale-api
+COPY server_${TARGETARCH} /usr/bin/eadscale-api
diff --git a/core/headscale/main.go b/core/headscale/main.go
index d4a129f..8a3a9ec 100644
--- a/core/headscale/main.go
+++ b/core/headscale/main.go
@@ -39,6 +39,11 @@
},
},
"acls": [
+ {
+ "action": "accept",
+ "src": ["*"],
+ "dst": ["*:*"]
+ },
// {
// "action": "accept",
// "src": ["10.42.0.0/16", "10.43.0.0/16", "135.181.48.180/32", "65.108.39.172/32"],
diff --git a/core/installer/app_configs/dodo_app.cue b/core/installer/app_configs/dodo_app.cue
index 0ce8baa..794f0fd 100644
--- a/core/installer/app_configs/dodo_app.cue
+++ b/core/installer/app_configs/dodo_app.cue
@@ -74,10 +74,18 @@
for svc in service {
if svc.dev.enabled {
- username: string | *svc.dev.username
- vpnAuthKey: string @role(VPNAuthKey) @usernameField(username)
- if svc.dev.ssh != null {
- "port_service_\(svc.name)_ssh": int @role(port)
+ if svc.dev.mode == "VM" {
+ username: string | *svc.dev.username
+ vpnAuthKey: string @role(VPNAuthKey) @usernameField(username)
+ if svc.dev.ssh != null {
+ "port_service_\(svc.name)_ssh": int @role(port)
+ }
+ }
+ if svc.dev.mode == "PROXY" {
+ if svc.dev.vpn.enabled == true {
+ username: svc.dev.vpn.username
+ vpnAuthKey: string @role(VPNAuthKey) @usernameField(username)
+ }
}
}
}
@@ -126,14 +134,33 @@
enabled: false
}
-#DevEnabled: {
+#DevEnabledVM: {
enabled: true
+ mode: "VM"
username: string
vpn: bool | *false
codeServer: #Domain | null | *null
ssh: #Domain | null | *null
}
+#PortMapping: {
+ src: int
+ dst: int
+}
+
+#DevEnabledProxy: {
+ enabled: true
+ mode: "PROXY"
+ address: string
+ ports: [...#PortMapping]
+ vpn: {enabled: false} | {
+ enabled: true
+ username: string
+ } | *{enabled: false}
+}
+
+#DevEnabled: *#DevEnabledVM | #DevEnabledProxy
+
#Dev: #DevEnabled | #DevDisabled
#VMCustomization: {
@@ -761,7 +788,7 @@
// TODO(gio): Support remote clusters.
// TODO(gio): pass all svc ports and not only ssh and code-server
-#ServiceDevEnabled: #WithOut & {
+#ServiceDevEnabledVM: #WithOut & {
cluster?: #Cluster
_cc: #Cluster | null | *null
if cluster != _|_ {
@@ -770,6 +797,7 @@
svc: #App & {
dev: {
enabled: true
+ mode: "VM"
}
externalEnv: envVars
}
@@ -870,6 +898,121 @@
}
}
+#ServiceDevEnabledProxy: #WithOut & {
+ cluster?: #Cluster
+ _cc: #Cluster | null | *null
+ if cluster != _|_ {
+ _cc: cluster
+ }
+ svc: #App & {
+ dev: {
+ enabled: true
+ mode: "PROXY"
+ }
+ externalEnv: envVars
+ }
+
+ _namedPorts: {
+ for p in svc.ports {
+ "\(p.name)": p.value
+ }
+ }
+
+ // TODO(gio): reuse envProfile from above
+ _envProfile: strings.Join(list.Concat([
+ svc.vm.env,
+ [for e in svc.lastCmdEnv {"export \(e)"}],
+ ]), "\n")
+ ingress: {
+ if svc.ingress != _|_ {
+ {for i, ingress in svc.ingress {
+ "\(svc.name)-\(i)": {
+ label: svc.name
+ network: networks[strings.ToLower(ingress.network)]
+ subdomain: ingress.subdomain
+ auth: ingress.auth
+ if svc.agentPort == service.port {
+ agentName: svc.name
+ }
+ service: {
+ name: svc.name
+ if ingress.port.value != _|_ {
+ port: ingress.port.value
+ }
+ if ingress.port.name != _|_ {
+ port: _namedPorts[ingress.port.name]
+ }
+ }
+ }
+ }}
+ }
+ }
+ images: {
+ nginx: {
+ registry: "docker.io"
+ repository: "library"
+ name: "nginx"
+ tag: "1.27.1-alpine3.20-slim"
+ pullPolicy: "IfNotPresent"
+ }
+ tailscale: {
+ repository: "tailscale"
+ name: "tailscale"
+ tag: "v1.82.0"
+ pullPolicy: "IfNotPresent"
+ }
+ }
+
+ charts: {
+ nginx: {
+ kind: "GitRepository"
+ address: "https://code.v1.dodo.cloud/helm-charts"
+ branch: "main"
+ path: "charts/proxy"
+ }
+ }
+
+ _nginxCfg: #NginxProxyConfig & {
+ address: svc.dev.address
+ ports: svc.dev.ports
+ }
+
+ helm: {
+ proxy: {
+ chart: charts.nginx
+ annotations: {
+ "dodo.cloud/resource-type": "service"
+ "dodo.cloud/resource.service.name": svc.name
+ }
+ values: {
+ image: {
+ repository: images.nginx.fullName
+ tag: images.nginx.tag
+ pullPolicy: images.nginx.pullPolicy
+ }
+ name: svc.name
+ config: _nginxCfg.cfg
+ ports: svc.ports
+ if svc.dev.vpn.enabled {
+ vpn: {
+ enabled: true
+ image: {
+ repository: images.tailscale.fullName
+ tag: images.tailscale.tag
+ pullPolicy: images.tailscale.pullPolicy
+ }
+ preAuthKey: input.vpnAuthKey
+ loginServer: "https://headscale.\(global.domain)"
+ hostname: "proxy-\(release.appInstanceId)-\(svc.name)"
+ }
+ }
+ }
+ }
+ }
+}
+
+#ServiceDevEnabled: #ServiceDevEnabledVM | #ServiceDevEnabledProxy
+
#Service: #ServiceDevEnabled | #ServiceDevDisabled
#Service: {
svc: type: string
@@ -998,3 +1141,37 @@
}
_appDir: "/dodo-app"
+
+#NginxProxyConfig: {
+ address: string
+ ports: [...#PortMapping]
+
+ _upstreams: [for p in ports {
+ """
+ upstream backend_\(p.dst) {
+ server \(address):\(p.dst);
+ }
+ """
+ }]
+
+ _servers: [for p in ports {
+ """
+ server {
+ listen \(p.src);
+ proxy_pass backend_\(p.dst);
+ }
+ """
+ }]
+
+ cfg: """
+ worker_processes 1;
+ worker_rlimit_nofile 8192;
+ events {
+ worker_connections 1024;
+ }
+ stream {
+ \(strings.Join(_upstreams, "\n"))
+ \(strings.Join(_servers, "\n"))
+ }
+ """
+}
diff --git a/core/installer/app_test.go b/core/installer/app_test.go
index 6d982a8..f8b0758 100644
--- a/core/installer/app_test.go
+++ b/core/installer/app_test.go
@@ -492,6 +492,7 @@
volume: ["data"]
dev: {
enabled: true
+ mode: "VM"
username: "gio"
}
source: repository: "ssh://foo.bar"
@@ -559,10 +560,10 @@
}
keyGen := testKeyGen{}
r, err := app.Render(release, env, networks, nil, map[string]any{
- "managerAddr": "",
- "appId": "",
- "sshPrivateKey": "",
- "username": "",
+ "managerAddr": "",
+ "appId": "",
+ "sshPrivateKey": "",
+ "username": "",
"password_mongodb_db": "foo",
}, nil, keyGen)
if err != nil {
diff --git a/core/installer/dodo_app_test.go b/core/installer/dodo_app_test.go
index a733c11..73f7025 100644
--- a/core/installer/dodo_app_test.go
+++ b/core/installer/dodo_app_test.go
@@ -149,59 +149,66 @@
const canvas = `
{
- "service": [
+ "service": [
+ {
+ "type": "nextjs:deno-2.0.0",
+ "name": "app",
+ "source": {
+ "repository": "ssh://d.p.v1.dodo.cloud:62533/myblog",
+ "branch": "master",
+ "rootDir": "/"
+ },
+ "ports": [
{
- "type": "nextjs:deno-2.0.0",
- "name": "app",
- "source": {
- "repository": "ssh://d.p.v1.dodo.cloud:62533/myblog",
- "branch": "master",
- "rootDir": "/"
- },
- "ports": [
- {
- "name": "web",
- "value": 3000,
- "protocol": "TCP"
- }
- ],
- "env": [
- {
- "name": "DODO_POSTGRESQL_DB_CONNECTION_URL"
- }
- ],
- "ingress": [{
- "network": "Private",
- "subdomain": "foo",
- "port": {
- "name": "web"
- },
- "auth": {
- "enabled": false
- }
- }, {
- "network": "Public",
- "subdomain": "foo",
- "port": {
- "name": "web"
- },
- "auth": {
- "enabled": false
- }
- }],
- "expose": [],
- "dev": { "enabled": true, "username": "gio" }
+ "name": "web",
+ "value": 3000,
+ "protocol": "TCP"
}
- ],
- "volume": [],
- "postgresql": [
+ ],
+ "env": [
{
- "name": "db",
- "size": "1Gi",
- "expose": []
+ "name": "DODO_POSTGRESQL_DB_CONNECTION_URL"
}
- ],
- "mongodb": []
+ ],
+ "ingress": [
+ {
+ "network": "Private",
+ "subdomain": "foo",
+ "port": {
+ "name": "web"
+ },
+ "auth": {
+ "enabled": false
+ }
+ },
+ {
+ "network": "Public",
+ "subdomain": "foo",
+ "port": {
+ "name": "web"
+ },
+ "auth": {
+ "enabled": false
+ }
+ }
+ ],
+ "expose": [],
+ "dev": {
+ "enabled": true,
+ "mode": "VM",
+ "username": "gio"
+ }
+ }
+ ],
+ "volume": [],
+ "postgresql": [
+ {
+ "name": "db",
+ "size": "1Gi",
+ "expose": []
+ }
+ ],
+ "mongodb": []
}
`
@@ -481,6 +488,7 @@
}],
"dev": {
"enabled": true,
+ "mode": "VM",
"username": "gio",
"codeServer": {
"network": "Private",
@@ -535,78 +543,67 @@
const foo = `
{
- "service": [
- {
- "type": "deno:2.2.0",
- "name": "qwe",
- "source": {
- "repository": "git@github.com:giolekva/dodo-blog.git"
- },
- "ports": [
- {
- "name": "web",
- "value": 8080,
- "protocol": "TCP"
- }
- ],
- "env": [
- {
- "name": "DODO_POSTGRESQL_DB_URL"
- },
- {
- "name": "DODO_PORT_WEB"
- }
- ],
- "ingress": [
- {
- "network": "Private",
- "subdomain": "blog",
- "port": {
- "name": "web"
- },
- "auth": {
- "enabled": false
- }
- }
- ],
- "expose": [
- {
- "network": "Private",
- "subdomain": "blog",
- "port": {
- "name": "web"
- }
- }
- ],
- "preBuildCommands": [],
- "dev": {
- "enabled": true,
- "username": "gio",
- "codeServer": {
- "network": "Private",
- "subdomain": "code"
- },
- "ssh": {
- "network": "Public",
- "subdomain": "code"
- }
- }
- }
- ],
- "volume": [],
- "postgresql": [
- {
- "name": "db",
- "size": "1Gi",
- "expose": [
- {
- "network": "Private",
- "subdomain": "pg"
- }
- ]
- }
- ],
- "mongodb": []
+ "service": [
+ {
+ "type": "deno:2.2.0",
+ "name": "qwe",
+ "source": {
+ "repository": "git@github.com:giolekva/dodo-blog.git"
+ },
+ "ports": [
+ {
+ "name": "web",
+ "value": 8081,
+ "protocol": "TCP"
+ }
+ ],
+ "env": [
+ {
+ "name": "DODO_POSTGRESQL_DB_URL"
+ },
+ {
+ "name": "DODO_PORT_WEB"
+ }
+ ],
+ "ingress": [
+ {
+ "network": "Private",
+ "subdomain": "blog",
+ "port": {
+ "name": "web"
+ },
+ "auth": {
+ "enabled": false
+ }
+ }
+ ],
+ "expose": [
+ {
+ "network": "Private",
+ "subdomain": "blog",
+ "port": {
+ "name": "web"
+ }
+ }
+ ],
+ "preBuildCommands": [],
+ "dev": {
+ "enabled": true,
+ "mode": "PROXY",
+ "vpn": { "enabled": true, "username": "gio" },
+ "address": "65.108.39.172",
+ "ports": [
+ {
+ "src": 8081,
+ "dst": 80
+ }
+ ]
+ }
+ }
+ ],
+ "volume": [],
+ "postgresql": [],
+ "mongodb": []
}
`
@@ -636,11 +633,7 @@
if err != nil {
t.Fatal(err)
}
- access, err := json.Marshal(r.Access)
- if err != nil {
- t.Fatal(err)
- }
- t.Log(string(access))
+ t.Log(string(r.Raw))
}
const sketch = `
diff --git a/core/installer/samples/canvas.rest b/core/installer/samples/canvas.rest
index 42bc95d..6cbbd8d 100644
--- a/core/installer/samples/canvas.rest
+++ b/core/installer/samples/canvas.rest
@@ -2,23 +2,70 @@
Content-Type: application/json
{
- "config": {
- "input": {
- "sketch_dev_gemini_api_key": "AIzaSyAx_vF0HJyT55A09iXtjPhf2JocNOGaWCo"
- },
- "agent": [
- {
- "name": "dev",
- "ingress": [
- {
- "network": "private",
- "port": {
- "value": 2001
- },
- "subdomain": "sketch"
- }
- ]
- }
- ]
- }
-}
+ "config": {
+ "service": [
+ {
+ "type": "deno:2.2.0",
+ "name": "qwe",
+ "source": {
+ "repository": "git@github.com:giolekva/dodo-blog.git"
+ },
+ "ports": [
+ {
+ "name": "web",
+ "value": 8081,
+ "protocol": "TCP"
+ }
+ ],
+ "env": [
+ {
+ "name": "DODO_POSTGRESQL_DB_URL"
+ },
+ {
+ "name": "DODO_PORT_WEB"
+ }
+ ],
+ "ingress": [
+ {
+ "network": "Private",
+ "subdomain": "blog",
+ "port": {
+ "name": "web"
+ },
+ "auth": {
+ "enabled": false
+ }
+ }
+ ],
+ "expose": [
+ {
+ "network": "Private",
+ "subdomain": "blog",
+ "port": {
+ "name": "web"
+ }
+ }
+ ],
+ "preBuildCommands": [],
+ "dev": {
+ "enabled": true,
+ "mode": "PROXY",
+ "vpn": {
+ "enabled": true,
+ "username": "gio"
+ },
+ "address": "gl-mbp-m1-max-2-vi8huz7s",
+ "ports": [
+ {
+ "src": 8081,
+ "dst": 8080
+ }
+ ]
+ }
+ }
+ ],
+ "volume": [],
+ "postgresql": [],
+ "mongodb": []
+ }
+}
\ No newline at end of file
diff --git a/core/installer/server/appmanager/server.go b/core/installer/server/appmanager/server.go
index 3b5d5b5..c3bd236 100644
--- a/core/installer/server/appmanager/server.go
+++ b/core/installer/server/appmanager/server.go
@@ -187,6 +187,10 @@
func (s *Server) handleDodoAppInstall(w http.ResponseWriter, r *http.Request) {
s.l.Lock()
defer s.l.Unlock()
+ fmt.Println("INSTALLING DODO APP")
+ defer func() {
+ fmt.Println("DONE")
+ }()
var req dodoAppInstallReq
// TODO(gio): validate that no internal fields are overridden by request
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {