auth-proxy: reusable ingress with auth proxy object for cue configs (#113)

affects: #110

Creates reusable auth proxy object in base cue config, and migrates rpuppy, url-shortener, pihole and memberships app to it.

Memberships app always requires authentication.
url-shortener now supports non-auth based interactions.
diff --git a/apps/url-shortener/main.go b/apps/url-shortener/main.go
index bb805e4..23365fd 100644
--- a/apps/url-shortener/main.go
+++ b/apps/url-shortener/main.go
@@ -19,6 +19,9 @@
 
 var port = flag.Int("port", 8080, "Port to listen on")
 var dbPath = flag.String("db-path", "url-shortener.db", "Path to the SQLite file")
+var requireAuth = flag.Bool("require-auth", false, "If false there won't be made any distinctions between users")
+
+const anyUser = "__any__"
 
 //go:embed index.html
 var indexHTML embed.FS
@@ -157,6 +160,9 @@
 }
 
 func getLoggedInUser(r *http.Request) (string, error) {
+	if !*requireAuth {
+		return anyUser, nil
+	}
 	if user := r.Header.Get("X-User"); user != "" {
 		return user, nil
 	} else {
diff --git a/charts/url-shortener/templates/install.yaml b/charts/url-shortener/templates/install.yaml
index e562b02..a0fc858 100644
--- a/charts/url-shortener/templates/install.yaml
+++ b/charts/url-shortener/templates/install.yaml
@@ -39,6 +39,7 @@
         - url-shortener
         - --port=8080
         - --db-path=/data/urls.db
+        - --require-auth={{ .Values.requireAuth }}
         volumeMounts:
         - name: url-shortener
           mountPath: /data
diff --git a/charts/url-shortener/values.yaml b/charts/url-shortener/values.yaml
index 3b0d837..dd3da7d 100644
--- a/charts/url-shortener/values.yaml
+++ b/charts/url-shortener/values.yaml
@@ -5,3 +5,4 @@
 storage:
   size: 1Gi
 portName: http
+requireAuth: false
diff --git a/core/installer/app.go b/core/installer/app.go
index 938d08f..e91174a 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -79,6 +79,20 @@
 	domain: string
 }
 
+networks: {
+	public: #Network & {
+		name: "Public"
+		ingressClass: "\(global.pcloudEnvName)-ingress-public"
+		certificateIssuer: "\(global.id)-public"
+		domain: global.domain
+	}
+	private: #Network & {
+		name: "Private"
+		ingressClass: "\(global.id)-ingress-private"
+		domain: global.privateDomain
+	}
+}
+
 #Image: {
 	registry: string | *"docker.io"
 	repository: string
@@ -122,6 +136,91 @@
 _issuerPrivate: "\(global.id)-private"
 _issuerPublic: "\(global.id)-public"
 
+_IngressWithAuthProxy: {
+	inp: {
+		auth: #Auth
+		network: #Network
+		subdomain: string
+		serviceName: string
+		port: { name: string } | { number: int & > 0 }
+	}
+
+	_domain: "\(inp.subdomain).\(inp.network.domain)"
+    _authProxyHTTPPortName: "http"
+
+	out: {
+		images: {
+			authProxy: #Image & {
+				repository: "giolekva"
+				name: "auth-proxy"
+				tag: "latest"
+				pullPolicy: "Always"
+			}
+		}
+		charts: {
+			ingress: #Chart & {
+				chart: "charts/ingress"
+				sourceRef: {
+					kind: "GitRepository"
+					name: "pcloud"
+					namespace: global.id
+				}
+			}
+			authProxy: #Chart & {
+				chart: "charts/auth-proxy"
+				sourceRef: {
+					kind: "GitRepository"
+					name: "pcloud"
+					namespace: global.id
+				}
+			}
+		}
+		helm: {
+			if inp.auth.enabled {
+				"auth-proxy": {
+					chart: charts.authProxy
+					values: {
+						image: {
+							repository: images.authProxy.fullName
+							tag: images.authProxy.tag
+							pullPolicy: images.authProxy.pullPolicy
+						}
+						upstream: "\(inp.serviceName).\(release.namespace).svc.cluster.local"
+						whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
+						loginAddr: "https://accounts-ui.\(global.domain)/login"
+						membershipAddr: "http://memberships.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
+						groups: inp.auth.groups
+						portName: _authProxyHTTPPortName
+					}
+				}
+			}
+			ingress: {
+				chart: charts.ingress
+				values: {
+					domain: _domain
+					ingressClassName: inp.network.ingressClass
+					certificateIssuer: inp.network.certificateIssuer
+					service: {
+						if inp.auth.enabled {
+							name: "auth-proxy"
+                            port: name: _authProxyHTTPPortName
+						}
+						if !inp.auth.enabled {
+							name: inp.serviceName
+							if inp.port.name != _|_ {
+								port: name: inp.port.name
+							}
+							if inp.port.number != _|_ {
+								port: number: inp.port.number
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+}
+
 images: {
 	for key, value in images {
 		"\(key)": #Image & value
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 05cb496..744f9a2 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -132,6 +132,7 @@
 	return m.repoIO.RemoveApp(filepath.Join(appDir, instanceId))
 }
 
+// TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
 func CreateNetworks(global Config) []Network {
 	return []Network{
 		{
diff --git a/core/installer/app_test.go b/core/installer/app_test.go
index 6162299..6b84005 100644
--- a/core/installer/app_test.go
+++ b/core/installer/app_test.go
@@ -4,13 +4,128 @@
 	"testing"
 )
 
-func TestHeadscaleUser(t *testing.T) {
+func TestAuthProxyEnabled(t *testing.T) {
 	r := NewInMemoryAppRepository(CreateAllApps())
-	a, err := r.Find("headscale-user")
+	for _, app := range []string{"rpuppy", "Pi-hole", "url-shortener"} {
+		a, err := r.Find(app)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if a == nil {
+			t.Fatal("returned app is nil")
+		}
+		d := Derived{
+			Release: Release{
+				Namespace: "foo",
+			},
+			Global: Values{
+				PCloudEnvName:   "dodo",
+				Id:              "id",
+				ContactEmail:    "foo@bar.ge",
+				Domain:          "bar.ge",
+				PrivateDomain:   "p.bar.ge",
+				PublicIP:        "1.2.3.4",
+				NamespacePrefix: "id-",
+			},
+			Values: map[string]any{
+				"network": map[string]any{
+					"name":              "Public",
+					"ingressClass":      "dodo-ingress-public",
+					"certificateIssuer": "id-public",
+					"domain":            "bar.ge",
+				},
+				"subdomain": "woof",
+				"auth": map[string]any{
+					"enabled": true,
+					"groups":  "a,b",
+				},
+			},
+		}
+		rendered, err := a.Render(d)
+		if err != nil {
+			t.Fatal(err)
+		}
+		for _, r := range rendered.Resources {
+			t.Log(string(r))
+		}
+	}
+}
+
+func TestAuthProxyDisabled(t *testing.T) {
+	r := NewInMemoryAppRepository(CreateAllApps())
+	for _, app := range []string{"rpuppy", "Pi-hole", "url-shortener"} {
+		a, err := r.Find(app)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if a == nil {
+			t.Fatal("returned app is nil")
+		}
+		d := Derived{
+			Release: Release{
+				Namespace: "foo",
+			},
+			Global: Values{
+				PCloudEnvName:   "dodo",
+				Id:              "id",
+				ContactEmail:    "foo@bar.ge",
+				Domain:          "bar.ge",
+				PrivateDomain:   "p.bar.ge",
+				PublicIP:        "1.2.3.4",
+				NamespacePrefix: "id-",
+			},
+			Values: map[string]any{
+				"network": map[string]any{
+					"name":              "Public",
+					"ingressClass":      "dodo-ingress-public",
+					"certificateIssuer": "id-public",
+					"domain":            "bar.ge",
+				},
+				"subdomain": "woof",
+				"auth": map[string]any{
+					"enabled": false,
+				},
+			},
+		}
+		rendered, err := a.Render(d)
+		if err != nil {
+			t.Fatal(err)
+		}
+		for _, r := range rendered.Resources {
+			t.Log(string(r))
+		}
+	}
+}
+
+func TestGroupMemberships(t *testing.T) {
+	r := NewInMemoryAppRepository(CreateAllApps())
+	a, err := r.Find("memberships")
 	if err != nil {
 		t.Fatal(err)
 	}
 	if a == nil {
 		t.Fatal("returned app is nil")
 	}
+	d := Derived{
+		Release: Release{
+			Namespace: "foo",
+		},
+		Global: Values{
+			PCloudEnvName:   "dodo",
+			Id:              "id",
+			ContactEmail:    "foo@bar.ge",
+			Domain:          "bar.ge",
+			PrivateDomain:   "p.bar.ge",
+			PublicIP:        "1.2.3.4",
+			NamespacePrefix: "id-",
+		},
+		Values: map[string]any{},
+	}
+	rendered, err := a.Render(d)
+	if err != nil {
+		t.Fatal(err)
+	}
+	for _, r := range rendered.Resources {
+		t.Log(string(r))
+	}
 }
diff --git a/core/installer/values-tmpl/memberships.cue b/core/installer/values-tmpl/memberships.cue
index 6790cfd..898cffd 100644
--- a/core/installer/values-tmpl/memberships.cue
+++ b/core/installer/values-tmpl/memberships.cue
@@ -1,6 +1,7 @@
 input: {}
 
-_domain: "memberships.\(global.privateDomain)"
+_subdomain: "memberships"
+_domain: "\(_subdomain).\(global.privateDomain)"
 
 name: "memberships"
 namespace: "core-auth-memberships"
@@ -8,22 +9,31 @@
 description: "The application is a membership management system designed to facilitate the organization and administration of groups and memberships. Can be configured to be reachable only from private network or publicly."
 icon: "<svg xmlns='http://www.w3.org/2000/svg' width='50' height='50' viewBox='0 0 24 24'><path fill='currentColor' d='M15.43 15.48c-1.1-.49-2.26-.73-3.43-.73c-1.18 0-2.33.25-3.43.73c-.23.1-.4.29-.49.52h7.85a.978.978 0 0 0-.5-.52m-2.49-6.69C12.86 8.33 12.47 8 12 8s-.86.33-.94.79l-.2 1.21h2.28z' opacity='0.3'/><path fill='currentColor' d='M10.27 12h3.46a1.5 1.5 0 0 0 1.48-1.75l-.3-1.79a2.951 2.951 0 0 0-5.82.01l-.3 1.79c-.15.91.55 1.74 1.48 1.74m.79-3.21c.08-.46.47-.79.94-.79s.86.33.94.79l.2 1.21h-2.28zm-9.4 2.32c-.13.26-.18.57-.1.88c.16.69.76 1.03 1.53 1h1.95c.83 0 1.51-.58 1.51-1.29c0-.14-.03-.27-.07-.4c-.01-.03-.01-.05.01-.08c.09-.16.14-.34.14-.53c0-.31-.14-.6-.36-.82c-.03-.03-.03-.06-.02-.1c.07-.2.07-.43.01-.65a1.12 1.12 0 0 0-.99-.74a.09.09 0 0 1-.07-.03C5.03 8.14 4.72 8 4.37 8c-.3 0-.57.1-.75.26c-.03.03-.06.03-.09.02a1.24 1.24 0 0 0-1.7 1.03c0 .02-.01.04-.03.06c-.29.26-.46.65-.41 1.05c.03.22.12.43.25.6c.03.02.03.06.02.09m14.58 2.54c-1.17-.52-2.61-.9-4.24-.9c-1.63 0-3.07.39-4.24.9A2.988 2.988 0 0 0 6 16.39V18h12v-1.61c0-1.18-.68-2.26-1.76-2.74M8.07 16a.96.96 0 0 1 .49-.52c1.1-.49 2.26-.73 3.43-.73c1.18 0 2.33.25 3.43.73c.23.1.4.29.49.52zm-6.85-1.42A2.01 2.01 0 0 0 0 16.43V18h4.5v-1.61c0-.83.23-1.61.63-2.29c-.37-.06-.74-.1-1.13-.1c-.99 0-1.93.21-2.78.58m21.56 0A6.95 6.95 0 0 0 20 14c-.39 0-.76.04-1.13.1c.4.68.63 1.46.63 2.29V18H24v-1.57c0-.81-.48-1.53-1.22-1.85M22 11v-.5c0-1.1-.9-2-2-2h-2c-.42 0-.65.48-.39.81l.7.63c-.19.31-.31.67-.31 1.06c0 1.1.9 2 2 2s2-.9 2-2'/></svg>"
 
-images: {
+_httpPortName: "http"
+
+_ingressWithAuthProxy: _IngressWithAuthProxy & {
+	inp: {
+		auth: {
+			enabled: true
+			groups: "" // TODO(gio): set admin
+		}
+		network: networks.private
+		subdomain: _subdomain
+		serviceName: "memberships"
+		port: name: _httpPortName
+	}
+}
+
+images: _ingressWithAuthProxy.out.images & {
     memberships: {
         repository: "giolekva"
         name: "memberships"
         tag: "latest"
         pullPolicy: "Always"
     }
-    authProxy: {
-        repository: "giolekva"
-        name: "auth-proxy"
-        tag: "latest"
-        pullPolicy: "Always"
-    }
 }
 
-charts: {
+charts: _ingressWithAuthProxy.out.charts & {
     memberships: {
         chart: "charts/memberships"
         sourceRef: {
@@ -32,29 +42,9 @@
             namespace: global.id
         }
     }
-    ingress: {
-        chart: "charts/ingress"
-        sourceRef: {
-            kind: "GitRepository"
-            name: "pcloud"
-            namespace: global.id
-        }
-    }
-    authProxy: {
-        chart: "charts/auth-proxy"
-        sourceRef: {
-            kind: "GitRepository"
-            name: "pcloud"
-            namespace: global.id
-        }
-    }
 }
 
-_membershipsServiceName: "memberships"
-_authProxyServiceName: "auth-proxy"
-_httpPortName: "http"
-
-helm: {
+helm: _ingressWithAuthProxy.out.helm & {
     memberships: {
         chart: charts.memberships
         values: {
@@ -69,37 +59,4 @@
             portName: _httpPortName
         }
     }
-    if input.requireAuth {
-        "auth-proxy": {
-            chart: charts.authProxy
-            values: {
-                image: {
-                    repository: images.authProxy.fullName
-                    tag: images.authProxy.tag
-                    pullPolicy: images.authProxy.pullPolicy
-                }
-                upstream: "\(_membershipsServiceName).\(release.namespace).svc.cluster.local"
-                whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
-                loginAddr: "https://accounts-ui.\(global.domain)/login"
-                portName: _httpPortName
-            }
-        }
-    }
-    ingress: {
-        chart: charts.ingress
-        values: {
-            domain: _domain
-            ingressClassName: input.network.ingressClass
-            certificateIssuer: input.network.certificateIssuer
-            service: {
-                if input.requireAuth {
-                    name: _authProxyServiceName
-                }
-                if !input.requireAuth {
-                    name: _membershipsServiceName
-                }
-                port: name: _httpPortName
-            }
-        }
-    }
 }
diff --git a/core/installer/values-tmpl/pihole.cue b/core/installer/values-tmpl/pihole.cue
index ff04fe2..3f33842 100644
--- a/core/installer/values-tmpl/pihole.cue
+++ b/core/installer/values-tmpl/pihole.cue
@@ -12,22 +12,28 @@
 description: "Pi-hole is a Linux network-level advertisement and Internet tracker blocking application which acts as a DNS sinkhole and optionally a DHCP server, intended for use on a private network."
 icon: "<svg xmlns='http://www.w3.org/2000/svg' width='50' height='50' viewBox='0 0 24 24'><path fill='currentColor' d='M4.344 0c.238 4.792 3.256 7.056 6.252 7.376c.165-1.692-4.319-5.6-4.319-5.6c-.008-.011.009-.025.019-.014c0 0 4.648 4.01 5.423 5.645c2.762-.15 5.196-1.947 5-4.912c0 0-4.12-.613-5 4.618C11.48 2.753 8.993 0 4.344 0zM12 7.682v.002a3.68 3.68 0 0 0-2.591 1.077L4.94 13.227a3.683 3.683 0 0 0-.86 1.356a3.31 3.31 0 0 0-.237 1.255A3.681 3.681 0 0 0 4.92 18.45l4.464 4.466a3.69 3.69 0 0 0 2.251 1.06l.002.001c.093.01.187.015.28.017l-.1-.008c.06.003.117.009.177.009l-.077-.001L12 24l-.004-.005a3.68 3.68 0 0 0 2.61-1.077l4.469-4.465a3.683 3.683 0 0 0 1.006-1.888l.012-.063a3.682 3.682 0 0 0 .057-.541l.003-.061c0-.017.003-.05.004-.06h-.002a3.683 3.683 0 0 0-1.077-2.607l-4.466-4.468a3.694 3.694 0 0 0-1.564-.927l-.07-.02a3.43 3.43 0 0 0-.946-.133L12 7.682zm3.165 3.357c.023 1.748-1.33 3.078-1.33 4.806c.164 2.227 1.733 3.207 3.266 3.146c-.035.003-.068.007-.104.009c-1.847.135-3.209-1.326-5.002-1.326c-2.23.164-3.21 1.736-3.147 3.27l-.008-.104c-.133-1.847 1.328-3.21 1.328-5.002c-.173-2.32-1.867-3.284-3.46-3.132c.1-.011.203-.021.31-.027c1.847-.133 3.209 1.328 5.002 1.328c2.082-.155 3.074-1.536 3.145-2.968zM4.344 0c.238 4.792 3.256 7.056 6.252 7.376c.165-1.692-4.319-5.6-4.319-5.6c-.008-.011.009-.025.019-.014c0 0 4.648 4.01 5.423 5.645c2.762-.15 5.196-1.947 5-4.912c0 0-4.12-.613-5 4.618C11.48 2.753 8.993 0 4.344 0zM12 7.682v.002a3.68 3.68 0 0 0-2.591 1.077L4.94 13.227a3.683 3.683 0 0 0-.86 1.356a3.31 3.31 0 0 0-.237 1.255A3.681 3.681 0 0 0 4.92 18.45l4.464 4.466a3.69 3.69 0 0 0 2.251 1.06l.002.001c.093.01.187.015.28.017l-.1-.008c.06.003.117.009.177.009l-.077-.001L12 24l-.004-.005a3.68 3.68 0 0 0 2.61-1.077l4.469-4.465a3.683 3.683 0 0 0 1.006-1.888l.012-.063a3.682 3.682 0 0 0 .057-.541l.003-.061c0-.017.003-.05.004-.06h-.002a3.683 3.683 0 0 0-1.077-2.607l-4.466-4.468a3.694 3.694 0 0 0-1.564-.927l-.07-.02a3.43 3.43 0 0 0-.946-.133L12 7.682zm3.165 3.357c.023 1.748-1.33 3.078-1.33 4.806c.164 2.227 1.733 3.207 3.266 3.146c-.035.003-.068.007-.104.009c-1.847.135-3.209-1.326-5.002-1.326c-2.23.164-3.21 1.736-3.147 3.27l-.008-.104c-.133-1.847 1.328-3.21 1.328-5.002c-.173-2.32-1.867-3.284-3.46-3.132c.1-.011.203-.021.31-.027c1.847-.133 3.209 1.328 5.002 1.328c2.082-.155 3.074-1.536 3.145-2.968z'/></svg>"
 
-images: {
+_serviceWebPort: 80
+
+_ingressWithAuthProxy: _IngressWithAuthProxy & {
+	inp: {
+		auth: input.auth
+		network: input.network
+		subdomain: input.subdomain
+		serviceName: "pihole-web"
+		port: number: _serviceWebPort
+	}
+}
+
+images: _ingressWithAuthProxy.out.images & {
 	pihole: {
 		repository: "pihole"
 		name: "pihole"
 		tag: "v5.8.1"
 		pullPolicy: "IfNotPresent"
 	}
-	authProxy: {
-		repository: "giolekva"
-		name: "auth-proxy"
-		tag: "latest"
-		pullPolicy: "Always"
-	}
 }
 
-charts: {
+charts: _ingressWithAuthProxy.out.charts & {
 	pihole: {
 		chart: "charts/pihole"
 		sourceRef: {
@@ -36,30 +42,9 @@
 			namespace: global.id
 		}
 	}
-	ingress: {
-		chart: "charts/ingress"
-		sourceRef: {
-			kind: "GitRepository"
-			name: "pcloud"
-			namespace: global.id
-		}
-	}
-	authProxy: {
-		chart: "charts/auth-proxy"
-		sourceRef: {
-			kind: "GitRepository"
-			name: "pcloud"
-			namespace: global.id
-		}
-	}
 }
 
-_piholeServiceName: "pihole-web"
-_authProxyServiceName: "auth-proxy"
-_httpPortName: "http"
-_serviceWebPort: 80
-
-helm: {
+helm: _ingressWithAuthProxy.out.helm & {
 	pihole: {
 		chart: charts.pihole
 		values: {
@@ -108,40 +93,4 @@
 			}
 		}
 	}
-	if input.auth.enabled {
-		"auth-proxy": {
-			chart: charts.authProxy
-			values: {
-				image: {
-					repository: images.authProxy.fullName
-					tag: images.authProxy.tag
-					pullPolicy: images.authProxy.pullPolicy
-				}
-				upstream: "\(_piholeServiceName).\(release.namespace).svc.cluster.local"
-				whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
-				loginAddr: "https://accounts-ui.\(global.domain)/login"
-				membershipAddr: "http://memberships.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
-				groups: input.auth.groups
-				portName: _httpPortName
-			}
-		}
-	}
-	ingress: {
-		chart: charts.ingress
-		values: {
-			domain: _domain
-			ingressClassName: input.network.ingressClass
-			certificateIssuer: input.network.certificateIssuer
-			service: {
-				if input.auth.enabled {
-					name: _authProxyServiceName
-					port: name: _httpPortName
-				}
-				if !input.auth.enabled {
-					name: _piholeServiceName
-					port: number: _serviceWebPort
-				}
-			}
-		}
-	}
 }
diff --git a/core/installer/values-tmpl/rpuppy.cue b/core/installer/values-tmpl/rpuppy.cue
index 4955f81..9b1d8f2 100644
--- a/core/installer/values-tmpl/rpuppy.cue
+++ b/core/installer/values-tmpl/rpuppy.cue
@@ -12,22 +12,28 @@
 description: "Delights users with randomly generate puppy pictures. Can be configured to be reachable only from private network or publicly."
 icon: "<svg xmlns='http://www.w3.org/2000/svg' width='50' height='50' viewBox='0 0 256 256'><path fill='currentColor' d='M100 140a8 8 0 1 1-8-8a8 8 0 0 1 8 8Zm64 8a8 8 0 1 0-8-8a8 8 0 0 0 8 8Zm64.94-9.11a12.12 12.12 0 0 1-5 1.11a11.83 11.83 0 0 1-9.35-4.62l-2.59-3.29V184a36 36 0 0 1-36 36H80a36 36 0 0 1-36-36v-51.91l-2.53 3.27A11.88 11.88 0 0 1 32.1 140a12.08 12.08 0 0 1-5-1.11a11.82 11.82 0 0 1-6.84-13.14l16.42-88a12 12 0 0 1 14.7-9.43h.16L104.58 44h46.84l53.08-15.6h.16a12 12 0 0 1 14.7 9.43l16.42 88a11.81 11.81 0 0 1-6.84 13.06ZM97.25 50.18L49.34 36.1a4.18 4.18 0 0 0-.92-.1a4 4 0 0 0-3.92 3.26l-16.42 88a4 4 0 0 0 7.08 3.22ZM204 121.75L150 52h-44l-54 69.75V184a28 28 0 0 0 28 28h44v-18.34l-14.83-14.83a4 4 0 0 1 5.66-5.66L128 186.34l13.17-13.17a4 4 0 0 1 5.66 5.66L132 193.66V212h44a28 28 0 0 0 28-28Zm23.92 5.48l-16.42-88a4 4 0 0 0-4.84-3.16l-47.91 14.11l62.11 80.28a4 4 0 0 0 7.06-3.23Z'/></svg>"
 
-images: {
+_httpPortName: "http"
+
+_ingressWithAuthProxy: _IngressWithAuthProxy & {
+	inp: {
+		auth: input.auth
+		network: input.network
+		subdomain: input.subdomain
+		serviceName: "rpuppy"
+		port: name: _httpPortName
+	}
+}
+
+images: _ingressWithAuthProxy.out.images & {
 	rpuppy: {
 		repository: "giolekva"
 		name: "rpuppy"
 		tag: "latest"
 		pullPolicy: "Always"
 	}
-	authProxy: {
-		repository: "giolekva"
-		name: "auth-proxy"
-		tag: "latest"
-		pullPolicy: "Always"
-	}
 }
 
-charts: {
+charts: _ingressWithAuthProxy.out.charts & {
 	rpuppy: {
 		chart: "charts/rpuppy"
 		sourceRef: {
@@ -36,29 +42,9 @@
 			namespace: global.id
 		}
 	}
-	ingress: {
-		chart: "charts/ingress"
-		sourceRef: {
-			kind: "GitRepository"
-			name: "pcloud"
-			namespace: global.id
-		}
-	}
-	authProxy: {
-		chart: "charts/auth-proxy"
-		sourceRef: {
-			kind: "GitRepository"
-			name: "pcloud"
-			namespace: global.id
-		}
-	}
 }
 
-_rpuppyServiceName: "rpuppy"
-_authProxyServiceName: "auth-proxy"
-_httpPortName: "http"
-
-helm: {
+helm: _ingressWithAuthProxy.out.helm & {
 	rpuppy: {
 		chart: charts.rpuppy
 		values: {
@@ -70,39 +56,4 @@
 			portName: _httpPortName
 		}
 	}
-	if input.auth.enabled {
-		"auth-proxy": {
-			chart: charts.authProxy
-			values: {
-				image: {
-					repository: images.authProxy.fullName
-					tag: images.authProxy.tag
-					pullPolicy: images.authProxy.pullPolicy
-				}
-				upstream: "\(_rpuppyServiceName).\(release.namespace).svc.cluster.local"
-				whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
-				loginAddr: "https://accounts-ui.\(global.domain)/login"
-				membershipAddr: "http://memberships.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
-				groups: input.auth.groups
-				portName: _httpPortName
-			}
-		}
-	}
-	ingress: {
-		chart: charts.ingress
-		values: {
-			domain: _domain
-			ingressClassName: input.network.ingressClass
-			certificateIssuer: input.network.certificateIssuer
-			service: {
-				if input.auth.enabled {
-					name: _authProxyServiceName
-				}
-				if !input.auth.enabled {
-					name: _rpuppyServiceName
-				}
-				port: name: _httpPortName
-			}
-		}
-	}
 }
diff --git a/core/installer/values-tmpl/url-shortener.cue b/core/installer/values-tmpl/url-shortener.cue
index 42a3ce8..a6e95ab 100644
--- a/core/installer/values-tmpl/url-shortener.cue
+++ b/core/installer/values-tmpl/url-shortener.cue
@@ -12,22 +12,28 @@
 description: "Provides URL shortening service. Can be configured to be reachable only from private network or publicly."
 icon: "<svg xmlns='http://www.w3.org/2000/svg' width='40.63' height='50' viewBox='0 0 13 16'><circle cx='2' cy='10' r='1' fill='currentColor'/><circle cx='2' cy='6' r='1' fill='currentColor'/><path fill='currentColor' d='M4.5 14c-.06 0-.11 0-.17-.03a.501.501 0 0 1-.3-.64l4-11a.501.501 0 0 1 .94.34l-4 11c-.07.2-.27.33-.47.33m3 0c-.06 0-.11 0-.17-.03a.501.501 0 0 1-.3-.64l4-11a.501.501 0 0 1 .94.34l-4 11c-.07.2-.27.33-.47.33'/></svg>"
 
-images: {
+_httpPortName: "http"
+
+_ingressWithAuthProxy: _IngressWithAuthProxy & {
+	inp: {
+		auth: input.auth
+		network: input.network
+		subdomain: input.subdomain
+		serviceName: "url-shortener"
+		port: name: _httpPortName
+	}
+}
+
+images: _ingressWithAuthProxy.out.images & {
 	urlShortener: {
 		repository: "giolekva"
 		name: "url-shortener"
 		tag: "latest"
 		pullPolicy: "Always"
 	}
-	authProxy: {
-		repository: "giolekva"
-		name: "auth-proxy"
-		tag: "latest"
-		pullPolicy: "Always"
-	}
 }
 
-charts: {
+charts: _ingressWithAuthProxy.out.charts & {
     urlShortener: {
         chart: "charts/url-shortener"
         sourceRef: {
@@ -36,29 +42,9 @@
             namespace: global.id
         }
     }
-	ingress: {
-		chart: "charts/ingress"
-		sourceRef: {
-			kind: "GitRepository"
-			name: "pcloud"
-			namespace: global.id
-		}
-	}
-	authProxy: {
-		chart: "charts/auth-proxy"
-		sourceRef: {
-			kind: "GitRepository"
-			name: "pcloud"
-			namespace: global.id
-		}
-	}
 }
 
-_urlShortenerServiceName: "url-shortener"
-_authProxyServiceName: "auth-proxy"
-_httpPortName: "http"
-
-helm: {
+helm: _ingressWithAuthProxy.out.helm & {
     "url-shortener": {
         chart: charts.urlShortener
         values: {
@@ -71,41 +57,7 @@
 				pullPolicy: images.urlShortener.pullPolicy
 			}
             portName: _httpPortName
+			requireAuth: input.auth.enabled
         }
     }
-	if input.auth.enabled {
-		"auth-proxy": {
-			chart: charts.authProxy
-			values: {
-				image: {
-					repository: images.authProxy.fullName
-					tag: images.authProxy.tag
-					pullPolicy: images.authProxy.pullPolicy
-				}
-				upstream: "\(_urlShortenerServiceName).\(release.namespace).svc.cluster.local"
-				whoAmIAddr: "https://accounts.\(global.domain)/sessions/whoami"
-				loginAddr: "https://accounts-ui.\(global.domain)/login"
-				membershipAddr: "http://memberships.\(global.id)-core-auth-memberships.svc.cluster.local/api/user"
-				groups: input.auth.groups
-				portName: _httpPortName
-			}
-		}
-	}
-	ingress: {
-		chart: charts.ingress
-		values: {
-			domain: _domain
-			ingressClassName: input.network.ingressClass
-			certificateIssuer: input.network.certificateIssuer
-			service: {
-				if input.auth.enabled {
-					name: _authProxyServiceName
-				}
-				if !input.auth.enabled {
-					name: _urlShortenerServiceName
-				}
-				port: name: _httpPortName
-			}
-		}
-	}
 }