Installer: Handle custom networks/domains

Change-Id: Id88e82a0757365466d92fb31223e21b7199ef940
diff --git a/charts/dodo-app/templates/install.yaml b/charts/dodo-app/templates/install.yaml
index 50ff7ab..d63326d 100644
--- a/charts/dodo-app/templates/install.yaml
+++ b/charts/dodo-app/templates/install.yaml
@@ -122,6 +122,7 @@
         - --api-port={{ .Values.apiPort }}
         - --self={{ .Values.self }}
         - --namespace={{ .Values.namespace }} # TODO(gio): maybe use .Release.Namespace ?
+        - --env-app-manager-addr={{ .Values.envAppManagerAddr }}
         - --env-config=/pcloud/env-config/config.json
         - --app-admin-key={{ .Values.appAdminKey }}
         - --git-repo-public-key={{ .Values.gitRepoPublicKey }}
diff --git a/charts/dodo-app/values.yaml b/charts/dodo-app/values.yaml
index 50aae4d..4c0b788 100644
--- a/charts/dodo-app/values.yaml
+++ b/charts/dodo-app/values.yaml
@@ -9,6 +9,7 @@
 sshPrivateKey: key
 self: ""
 namespace: ""
+envAppManagerAddr: ""
 envConfig: ""
 appAdminKey: ""
 gitRepoPublicKey: ""
diff --git a/core/installer/app.go b/core/installer/app.go
index 1922b48..dcd2ca2 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -186,7 +186,7 @@
 
 type EnvApp interface {
 	App
-	Render(release Release, env EnvConfig, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (EnvAppRendered, error)
+	Render(release Release, env EnvConfig, networks []Network, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (EnvAppRendered, error)
 }
 
 type cueApp struct {
@@ -435,8 +435,13 @@
 	return AppTypeEnv
 }
 
-func (a cueEnvApp) Render(release Release, env EnvConfig, values map[string]any, charts map[string]helmv2.HelmChartTemplateSpec) (EnvAppRendered, error) {
-	networks := CreateNetworks(env)
+func (a cueEnvApp) Render(
+	release Release,
+	env EnvConfig,
+	networks []Network,
+	values map[string]any,
+	charts map[string]helmv2.HelmChartTemplateSpec,
+) (EnvAppRendered, error) {
 	derived, err := deriveValues(values, a.Schema(), networks)
 	if err != nil {
 		return EnvAppRendered{}, err
diff --git a/core/installer/app_manager.go b/core/installer/app_manager.go
index 0930356..4fc0b1f 100644
--- a/core/installer/app_manager.go
+++ b/core/installer/app_manager.go
@@ -87,7 +87,11 @@
 func (m *AppManager) FindAllAppInstances(name string) ([]AppInstanceConfig, error) {
 	kust, err := soft.ReadKustomization(m.repoIO, filepath.Join(m.appDirRoot, "kustomization.yaml"))
 	if err != nil {
-		return nil, err
+		if errors.Is(err, fs.ErrNotExist) {
+			return nil, nil
+		} else {
+			return nil, err
+		}
 	}
 	ret := make([]AppInstanceConfig, 0)
 	for _, app := range kust.Resources {
@@ -404,6 +408,16 @@
 			return ReleaseResources{}, err
 		}
 	}
+	var networks []Network
+	if o.Networks != nil {
+		networks = o.Networks
+	} else {
+		var err error
+		networks, err = m.CreateNetworks(env)
+		if err != nil {
+			return ReleaseResources{}, err
+		}
+	}
 	var lg LocalChartGenerator
 	if o.LG != nil {
 		lg = o.LG
@@ -416,7 +430,7 @@
 		RepoAddr:      m.repoIO.FullAddress(),
 		AppDir:        appDir,
 	}
-	rendered, err := app.Render(release, env, values, nil)
+	rendered, err := app.Render(release, env, networks, values, nil)
 	if err != nil {
 		return ReleaseResources{}, err
 	}
@@ -447,7 +461,7 @@
 	if o.FetchContainerImages {
 		release.ImageRegistry = imageRegistry
 	}
-	rendered, err = app.Render(release, env, values, localCharts)
+	rendered, err = app.Render(release, env, networks, values, localCharts)
 	if err != nil {
 		return ReleaseResources{}, err
 	}
@@ -456,7 +470,6 @@
 	}
 	// TODO(gio): add ingress-nginx to release resources
 	if err := openPorts(rendered.Ports, portReservations, allocators); err != nil {
-		fmt.Println(err)
 		return ReleaseResources{}, err
 	}
 	return ReleaseResources{
@@ -531,7 +544,11 @@
 	if err != nil {
 		return ReleaseResources{}, err
 	}
-	rendered, err := app.Render(config.Release, env, values, renderedCfg.LocalCharts)
+	networks, err := m.CreateNetworks(env)
+	if err != nil {
+		return ReleaseResources{}, err
+	}
+	rendered, err := app.Render(config.Release, env, networks, values, renderedCfg.LocalCharts)
 	if err != nil {
 		return ReleaseResources{}, err
 	}
@@ -563,15 +580,14 @@
 		return err
 	}
 	if err := closePorts(portForward); err != nil {
-		fmt.Println(err)
 		return err
 	}
 	return nil
 }
 
 // TODO(gio): deduplicate with cue definition in app.go, this one should be removed.
-func CreateNetworks(env EnvConfig) []Network {
-	return []Network{
+func (m *AppManager) CreateNetworks(env EnvConfig) ([]Network, error) {
+	ret := []Network{
 		{
 			Name:               "Public",
 			IngressClass:       fmt.Sprintf("%s-ingress-public", env.InfraName),
@@ -590,11 +606,28 @@
 			DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
 		},
 	}
+	n, err := m.FindAllAppInstances("network")
+	if err != nil {
+		return nil, err
+	}
+	for _, a := range n {
+		ret = append(ret, Network{
+			Name:               a.Input["name"].(string),
+			IngressClass:       fmt.Sprintf("%s-ingress-public", env.InfraName),
+			CertificateIssuer:  fmt.Sprintf("%s-public", env.Id),
+			Domain:             a.Input["domain"].(string),
+			AllocatePortAddr:   fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
+			ReservePortAddr:    fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
+			DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
+		})
+	}
+	return ret, nil
 }
 
 type installOptions struct {
 	NoPublish            bool
 	Env                  *EnvConfig
+	Networks             []Network
 	Branch               string
 	LG                   LocalChartGenerator
 	FetchContainerImages bool
@@ -610,6 +643,12 @@
 	}
 }
 
+func WithNetworks(networks []Network) InstallOption {
+	return func(o *installOptions) {
+		o.Networks = networks
+	}
+}
+
 func WithBranch(branch string) InstallOption {
 	return func(o *installOptions) {
 		o.Branch = branch
diff --git a/core/installer/app_repository.go b/core/installer/app_repository.go
index 27b9b43..df35ae8 100644
--- a/core/installer/app_repository.go
+++ b/core/installer/app_repository.go
@@ -33,6 +33,7 @@
 	// "values-tmpl/qbittorrent.cue",
 	// "values-tmpl/jellyfin.cue",
 	"values-tmpl/rpuppy.cue",
+	"values-tmpl/certificate-issuer-custom.cue",
 }
 
 var envAppConfigs = []string{
diff --git a/core/installer/app_test.go b/core/installer/app_test.go
index 7935ec2..7e64d50 100644
--- a/core/installer/app_test.go
+++ b/core/installer/app_test.go
@@ -2,28 +2,51 @@
 
 import (
 	_ "embed"
+	"fmt"
 	"net"
 	"testing"
 )
 
-var env = EnvConfig{
-	InfraName:       "dodo",
-	Id:              "id",
-	ContactEmail:    "foo@bar.ge",
-	Domain:          "bar.ge",
-	PrivateDomain:   "p.bar.ge",
-	PublicIP:        []net.IP{net.ParseIP("1.2.3.4")},
-	NameserverIP:    []net.IP{net.ParseIP("1.2.3.4")},
-	NamespacePrefix: "id-",
-	Network: EnvNetwork{
-		DNS:            net.ParseIP("1.1.1.1"),
-		DNSInClusterIP: net.ParseIP("2.2.2.2"),
-		Ingress:        net.ParseIP("3.3.3.3"),
-		Headscale:      net.ParseIP("4.4.4.4"),
-		ServicesFrom:   net.ParseIP("5.5.5.5"),
-		ServicesTo:     net.ParseIP("6.6.6.6"),
-	},
-}
+var (
+	env = EnvConfig{
+		InfraName:       "dodo",
+		Id:              "id",
+		ContactEmail:    "foo@bar.ge",
+		Domain:          "bar.ge",
+		PrivateDomain:   "p.bar.ge",
+		PublicIP:        []net.IP{net.ParseIP("1.2.3.4")},
+		NameserverIP:    []net.IP{net.ParseIP("1.2.3.4")},
+		NamespacePrefix: "id-",
+		Network: EnvNetwork{
+			DNS:            net.ParseIP("1.1.1.1"),
+			DNSInClusterIP: net.ParseIP("2.2.2.2"),
+			Ingress:        net.ParseIP("3.3.3.3"),
+			Headscale:      net.ParseIP("4.4.4.4"),
+			ServicesFrom:   net.ParseIP("5.5.5.5"),
+			ServicesTo:     net.ParseIP("6.6.6.6"),
+		},
+	}
+
+	networks = []Network{
+		{
+			Name:               "Public",
+			IngressClass:       fmt.Sprintf("%s-ingress-public", env.InfraName),
+			CertificateIssuer:  fmt.Sprintf("%s-public", env.Id),
+			Domain:             env.Domain,
+			AllocatePortAddr:   fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/allocate", env.InfraName),
+			ReservePortAddr:    fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/reserve", env.InfraName),
+			DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-public.svc.cluster.local/api/remove", env.InfraName),
+		},
+		{
+			Name:               "Private",
+			IngressClass:       fmt.Sprintf("%s-ingress-private", env.Id),
+			Domain:             env.PrivateDomain,
+			AllocatePortAddr:   fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/allocate", env.Id),
+			ReservePortAddr:    fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/reserve", env.Id),
+			DeallocatePortAddr: fmt.Sprintf("http://port-allocator.%s-ingress-private.svc.cluster.local/api/remove", env.Id),
+		},
+	}
+)
 
 func TestAuthProxyEnabled(t *testing.T) {
 	r := NewInMemoryAppRepository(CreateAllApps())
@@ -46,7 +69,7 @@
 				"groups":  "a,b",
 			},
 		}
-		rendered, err := a.Render(release, env, values, nil)
+		rendered, err := a.Render(release, env, networks, values, nil)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -76,7 +99,7 @@
 				"enabled": false,
 			},
 		}
-		rendered, err := a.Render(release, env, values, nil)
+		rendered, err := a.Render(release, env, networks, values, nil)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -101,7 +124,7 @@
 	values := map[string]any{
 		"authGroups": "foo,bar",
 	}
-	rendered, err := a.Render(release, env, values, nil)
+	rendered, err := a.Render(release, env, networks, values, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -131,7 +154,7 @@
 		},
 		"sshPort": 22,
 	}
-	rendered, err := a.Render(release, env, values, nil)
+	rendered, err := a.Render(release, env, networks, values, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -156,7 +179,7 @@
 		"subdomain": "jenkins",
 		"network":   "Private",
 	}
-	rendered, err := a.Render(release, env, values, nil)
+	rendered, err := a.Render(release, env, networks, values, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -215,7 +238,7 @@
 		},
 		"sshPrivateKey": "private",
 	}
-	rendered, err := a.Render(release, env, values, nil)
+	rendered, err := a.Render(release, env, networks, values, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -248,7 +271,7 @@
 			"groups":  "a,b",
 		},
 	}
-	rendered, err := app.Render(release, env, values, nil)
+	rendered, err := app.Render(release, env, networks, values, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -314,7 +337,7 @@
 		RepoAddr:      "ssh://192.168.100.210:22/config",
 		AppDir:        "/foo/bar",
 	}
-	_, err = app.Render(release, env, map[string]any{
+	_, err = app.Render(release, env, networks, map[string]any{
 		"repoAddr":      "",
 		"managerAddr":   "",
 		"appId":         "",
@@ -342,7 +365,7 @@
 		"repoHost":         "",
 		"gitRepoPublicKey": "",
 	}
-	rendered, err := a.Render(release, env, values, nil)
+	rendered, err := a.Render(release, env, networks, values, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/core/installer/cmd/dodo_app.go b/core/installer/cmd/dodo_app.go
index a6521d8..2eb5b8f 100644
--- a/core/installer/cmd/dodo_app.go
+++ b/core/installer/cmd/dodo_app.go
@@ -17,16 +17,17 @@
 )
 
 var dodoAppFlags struct {
-	port             int
-	apiPort          int
-	sshKey           string
-	repoAddr         string
-	self             string
-	namespace        string
-	envConfig        string
-	appAdminKey      string
-	gitRepoPublicKey string
-	db               string
+	port              int
+	apiPort           int
+	sshKey            string
+	repoAddr          string
+	self              string
+	namespace         string
+	envAppManagerAddr string
+	envConfig         string
+	appAdminKey       string
+	gitRepoPublicKey  string
+	db                string
 }
 
 func dodoAppCmd() *cobra.Command {
@@ -77,6 +78,12 @@
 		"",
 	)
 	cmd.Flags().StringVar(
+		&dodoAppFlags.envAppManagerAddr,
+		"env-app-manager-addr",
+		"",
+		"",
+	)
+	cmd.Flags().StringVar(
 		&dodoAppFlags.envConfig,
 		"env-config",
 		"",
@@ -152,6 +159,7 @@
 		dodoAppFlags.gitRepoPublicKey,
 		softClient,
 		dodoAppFlags.namespace,
+		dodoAppFlags.envAppManagerAddr,
 		nsc,
 		jc,
 		env,
diff --git a/core/installer/values-tmpl/certificate-issuer-custom.cue b/core/installer/values-tmpl/certificate-issuer-custom.cue
new file mode 100644
index 0000000..382e8fa
--- /dev/null
+++ b/core/installer/values-tmpl/certificate-issuer-custom.cue
@@ -0,0 +1,41 @@
+input: {
+	name: string
+	domain: string
+}
+
+images: {}
+
+name: "Network"
+namespace: "ingress-custom"
+readme: "Configure custom public domain"
+description: readme
+icon: "<svg xmlns='http://www.w3.org/2000/svg' width='50' height='50' viewBox='0 0 48 48'><g fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='4'><path d='M4 34h8v8H4zM8 6h32v12H8zm16 28V18'/><path d='M8 34v-8h32v8m-4 0h8v8h-8zm-16 0h8v8h-8zm-6-22h2'/></g></svg>"
+
+charts: {
+	"certificate-issuer-public": {
+		kind: "GitRepository"
+		address: "https://github.com/giolekva/pcloud.git"
+		branch: "main"
+		path: "charts/certificate-issuer-public"
+	}
+}
+
+helm: {
+	"certificate-issuer-public": {
+		chart: charts["certificate-issuer-public"]
+		dependsOn: [{
+			name: "ingress-nginx"
+			namespace: "\(global.namespacePrefix)ingress-private"
+		}]
+		values: {
+			issuer: {
+				name: input.name
+				server: "https://acme-v02.api.letsencrypt.org/directory"
+				// server: "https://acme-staging-v02.api.letsencrypt.org/directory"
+				domain: input.domain
+				contactEmail: global.contactEmail
+				ingressClass: ingressPublic
+			}
+		}
+	}
+}
diff --git a/core/installer/values-tmpl/dodo-app.cue b/core/installer/values-tmpl/dodo-app.cue
index ddd3b17..a6d342d 100644
--- a/core/installer/values-tmpl/dodo-app.cue
+++ b/core/installer/values-tmpl/dodo-app.cue
@@ -112,6 +112,7 @@
 			sshPrivateKey: base64.Encode(null, input.dAppKeys.private)
 			self: "api.\(release.namespace).svc.cluster.local"
 			namespace: release.namespace
+			envAppManagerAddr: "http://appmanager.\(global.namespacePrefix)appmanager.svc.cluster.local"
 			envConfig: base64.Encode(null, json.Marshal(global))
 			appAdminKey: input.adminKey
 			gitRepoPublicKey: input.ssKeys.public
diff --git a/core/installer/welcome/appmanager.go b/core/installer/welcome/appmanager.go
index a4430b8..2c434c6 100644
--- a/core/installer/welcome/appmanager.go
+++ b/core/installer/welcome/appmanager.go
@@ -93,6 +93,7 @@
 func (s *AppManagerServer) Start() error {
 	r := mux.NewRouter()
 	r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticAssets))})
+	r.HandleFunc("/api/networks", s.handleNetworks).Methods(http.MethodGet)
 	r.HandleFunc("/api/app-repo", s.handleAppRepo)
 	r.HandleFunc("/api/app/{slug}/install", s.handleAppInstall).Methods(http.MethodPost)
 	r.HandleFunc("/api/app/{slug}", s.handleApp).Methods(http.MethodGet)
@@ -116,6 +117,23 @@
 	Instances        []installer.AppInstanceConfig `json:"instances,omitempty"`
 }
 
+func (s *AppManagerServer) handleNetworks(w http.ResponseWriter, r *http.Request) {
+	env, err := s.m.Config()
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	networks, err := s.m.CreateNetworks(env)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if err := json.NewEncoder(w).Encode(networks); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
 func (s *AppManagerServer) handleAppRepo(w http.ResponseWriter, r *http.Request) {
 	all, err := s.r.GetAll()
 	if err != nil {
@@ -414,10 +432,15 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
+	networks, err := s.m.CreateNetworks(global)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 	data := appPageData{
 		App:               a,
 		Instances:         instances,
-		AvailableNetworks: installer.CreateNetworks(global),
+		AvailableNetworks: networks,
 		CurrentPage:       a.Name(),
 	}
 	if err := s.tmpl.app.Execute(w, data); err != nil {
@@ -452,12 +475,17 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
+	networks, err := s.m.CreateNetworks(global)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 	t := s.tasks[slug]
 	data := appPageData{
 		App:               a,
 		Instance:          instance,
 		Instances:         instances,
-		AvailableNetworks: installer.CreateNetworks(global),
+		AvailableNetworks: networks,
 		Task:              t,
 		CurrentPage:       instance.Id,
 	}
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index 75d12b9..20e881d 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -29,21 +29,22 @@
 )
 
 type DodoAppServer struct {
-	l                sync.Locker
-	st               Store
-	port             int
-	apiPort          int
-	self             string
-	sshKey           string
-	gitRepoPublicKey string
-	client           soft.Client
-	namespace        string
-	env              installer.EnvConfig
-	nsc              installer.NamespaceCreator
-	jc               installer.JobCreator
-	workers          map[string]map[string]struct{}
-	appNs            map[string]string
-	sc               *securecookie.SecureCookie
+	l                 sync.Locker
+	st                Store
+	port              int
+	apiPort           int
+	self              string
+	sshKey            string
+	gitRepoPublicKey  string
+	client            soft.Client
+	namespace         string
+	envAppManagerAddr string
+	env               installer.EnvConfig
+	nsc               installer.NamespaceCreator
+	jc                installer.JobCreator
+	workers           map[string]map[string]struct{}
+	appNs             map[string]string
+	sc                *securecookie.SecureCookie
 }
 
 // TODO(gio): Initialize appNs on startup
@@ -56,6 +57,7 @@
 	gitRepoPublicKey string,
 	client soft.Client,
 	namespace string,
+	envAppManagerAddr string,
 	nsc installer.NamespaceCreator,
 	jc installer.JobCreator,
 	env installer.EnvConfig,
@@ -74,6 +76,7 @@
 		gitRepoPublicKey,
 		client,
 		namespace,
+		envAppManagerAddr,
 		env,
 		nsc,
 		jc,
@@ -277,7 +280,11 @@
 	}
 	// TODO(gio): Create commit record on app init as well
 	go func() {
-		if err := s.updateDodoApp(req.Repository.Name, s.appNs[req.Repository.Name]); err != nil {
+		networks, err := getNetworks(fmt.Sprintf("%s/api/networks", s.envAppManagerAddr))
+		if err != nil {
+			return
+		}
+		if err := s.updateDodoApp(req.Repository.Name, s.appNs[req.Repository.Name], networks); err != nil {
 			if err := s.st.CreateCommit(req.Repository.Name, req.After, err.Error()); err != nil {
 				fmt.Printf("Error: %s\n", err.Error())
 				return
@@ -417,7 +424,11 @@
 	}
 	namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, app.Namespace(), suffix)
 	s.appNs[appName] = namespace
-	if err := s.updateDodoApp(appName, namespace); err != nil {
+	networks, err := getNetworks(fmt.Sprintf("%s/api/networks", s.envAppManagerAddr))
+	if err != nil {
+		return "", err
+	}
+	if err := s.updateDodoApp(appName, namespace, networks); err != nil {
 		return "", err
 	}
 	repo, err := s.client.GetRepo(ConfigRepoName)
@@ -449,6 +460,7 @@
 				"gitRepoPublicKey": s.gitRepoPublicKey,
 			},
 			installer.WithConfig(&s.env),
+			installer.WithNetworks(networks),
 			installer.WithNoPublish(),
 			installer.WithNoLock(),
 		); err != nil {
@@ -509,7 +521,7 @@
 	}
 }
 
-func (s *DodoAppServer) updateDodoApp(name, namespace string) error {
+func (s *DodoAppServer) updateDodoApp(name, namespace string, networks []installer.Network) error {
 	repo, err := s.client.GetRepo(name)
 	if err != nil {
 		return err
@@ -540,6 +552,7 @@
 			"sshPrivateKey": s.sshKey,
 		},
 		installer.WithConfig(&s.env),
+		installer.WithNetworks(networks),
 		installer.WithLocalChartGenerator(lg),
 		installer.WithBranch("dodo"),
 		installer.WithForce(),
@@ -620,3 +633,15 @@
 func generatePassword() string {
 	return "foo"
 }
+
+func getNetworks(addr string) ([]installer.Network, error) {
+	resp, err := http.Get(addr)
+	if err != nil {
+		return nil, err
+	}
+	ret := []installer.Network{}
+	if json.NewDecoder(resp.Body).Decode(&ret); err != nil {
+		return nil, err
+	}
+	return ret, nil
+}