blob: d71842848276f3e750860faeffb3f5c0ac1ee212 [file] [log] [blame]
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +04001package tasks
2
3import (
4 "fmt"
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +04005 "net"
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +04006 "net/netip"
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +04007 "strings"
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +04008
9 "github.com/miekg/dns"
10
11 "github.com/giolekva/pcloud/core/installer"
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040012)
13
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040014var initGroups = []string{"admin"}
15
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +040016func CreateRepoClient(env Env, st *state) Task {
17 t := newLeafTask("Create repo client", func() error {
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +040018 repo, err := st.ssClient.GetRepo("config")
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040019 if err != nil {
20 return err
21 }
gio3af43942024-04-16 08:13:50 +040022 r, err := installer.NewRepoIO(repo, st.ssClient.Signer)
23 if err != nil {
24 return err
25 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +040026 appManager, err := installer.NewAppManager(r, st.nsCreator)
27 if err != nil {
28 return err
29 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040030 st.appManager = appManager
31 st.appsRepo = installer.NewInMemoryAppRepository(installer.CreateAllApps())
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040032 return nil
33 })
Giorgi Lekveishviliab7ff6e2024-03-29 13:11:30 +040034 t.beforeStart = func() {
35 st.infoListener("Setting up core infrastructure services.")
36 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +040037 return &t
38}
39
40func SetupInfra(env Env, startIP net.IP, st *state) Task {
41 return newConcurrentParentTask(
42 "Setup core services",
43 true,
44 SetupNetwork(env, startIP, st),
45 SetupCertificateIssuers(env, st),
46 SetupAuth(env, st),
47 SetupGroupMemberships(env, st),
48 SetupHeadscale(env, startIP, st),
49 SetupWelcome(env, st),
50 SetupAppStore(env, st),
51 )
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040052}
53
54func CommitEnvironmentConfiguration(env Env, st *state) Task {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +040055 t := newLeafTask("commit config", func() error {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040056 repo, err := st.ssClient.GetRepo("config")
57 if err != nil {
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +040058 return err
59 }
gio3af43942024-04-16 08:13:50 +040060 r, err := installer.NewRepoIO(repo, st.ssClient.Signer)
61 if err != nil {
62 return err
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040063 }
gio3af43942024-04-16 08:13:50 +040064 r.Atomic(func(r installer.RepoFS) (string, error) {
65 {
66 // TODO(giolekva): private domain can be configurable as well
67 config := installer.Config{
68 Values: installer.Values{
69 PCloudEnvName: env.PCloudEnvName,
70 Id: env.Name,
71 ContactEmail: env.ContactEmail,
72 Domain: env.Domain,
73 PrivateDomain: fmt.Sprintf("p.%s", env.Domain),
74 PublicIP: st.publicIPs[0].String(),
75 NamespacePrefix: fmt.Sprintf("%s-", env.Name),
76 },
77 }
78 if err := installer.WriteYaml(r, "config.yaml", config); err != nil {
79 return "", err
80 }
81 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +040082 out, err := r.Writer("pcloud-charts.yaml")
83 if err != nil {
gio3af43942024-04-16 08:13:50 +040084 return "", err
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +040085 }
86 defer out.Close()
87 _, err = fmt.Fprintf(out, `
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040088apiVersion: source.toolkit.fluxcd.io/v1
89kind: GitRepository
90metadata:
91 name: pcloud
92 namespace: %s
93spec:
94 interval: 1m0s
95 url: https://github.com/giolekva/pcloud
96 ref:
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040097 branch: ingress-port-allocator
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +040098`, env.Name)
99 if err != nil {
gio3af43942024-04-16 08:13:50 +0400100 return "", err
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400101 }
gio3af43942024-04-16 08:13:50 +0400102 rootKust, err := installer.ReadKustomization(r, "kustomization.yaml")
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400103 if err != nil {
gio3af43942024-04-16 08:13:50 +0400104 return "", err
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400105 }
106 rootKust.AddResources("pcloud-charts.yaml")
gio3af43942024-04-16 08:13:50 +0400107 if err := installer.WriteYaml(r, "kustomization.yaml", rootKust); err != nil {
108 return "", err
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400109 }
gio3af43942024-04-16 08:13:50 +0400110 return "configure charts repo", nil
111 })
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400112 return nil
113 })
114 return &t
115}
116
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400117type firstAccount struct {
118 Created bool `json:"created"`
119 Groups []string `json:"groups"`
120}
121
122func ConfigureFirstAccount(env Env, st *state) Task {
123 t := newLeafTask("Configure first account settings", func() error {
124 repo, err := st.ssClient.GetRepo("config")
125 if err != nil {
126 return err
127 }
gio3af43942024-04-16 08:13:50 +0400128 r, err := installer.NewRepoIO(repo, st.ssClient.Signer)
129 if err != nil {
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400130 return err
131 }
gio3af43942024-04-16 08:13:50 +0400132 return r.Atomic(func(r installer.RepoFS) (string, error) {
133 fa := firstAccount{false, initGroups}
134 if err := installer.WriteYaml(r, "first-account.yaml", fa); err != nil {
135 return "", err
136 }
137 return "first account membership configuration", nil
138 })
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400139 })
140 return &t
141}
142
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400143func SetupNetwork(env Env, startIP net.IP, st *state) Task {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400144 t := newLeafTask("Setup private and public networks", func() error {
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400145 startAddr, err := netip.ParseAddr(startIP.String())
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400146 if err != nil {
147 return err
148 }
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400149 if !startAddr.Is4() {
150 return fmt.Errorf("Expected IPv4, got %s instead", startAddr)
151 }
152 addr := startAddr.AsSlice()
153 if addr[3] != 0 {
154 return fmt.Errorf("Expected last byte to be zero, got %d instead", addr[3])
155 }
156 addr[3] = 10
157 fromIP, ok := netip.AddrFromSlice(addr)
158 if !ok {
159 return fmt.Errorf("Must not reach")
160 }
161 addr[3] = 254
162 toIP, ok := netip.AddrFromSlice(addr)
163 if !ok {
164 return fmt.Errorf("Must not reach")
165 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400166 {
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400167 ingressPrivateIP := startAddr
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400168 headscaleIP := ingressPrivateIP.Next()
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400169 app, err := st.appsRepo.Find("metallb-ipaddresspool")
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400170 if err != nil {
171 return err
172 }
gio3af43942024-04-16 08:13:50 +0400173 {
174 appDir := fmt.Sprintf("/apps/%s-ingress-private", app.Name())
175 namespace := fmt.Sprintf("%s%s-ingress-private", env.NamespacePrefix, app.Namespace())
176 if err := st.appManager.Install(app, appDir, namespace, map[string]any{
177 "name": fmt.Sprintf("%s-ingress-private", env.Name),
178 "from": ingressPrivateIP.String(),
179 "to": ingressPrivateIP.String(),
180 "autoAssign": false,
181 "namespace": "metallb-system",
182 }); err != nil {
183 return err
184 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400185 }
gio3af43942024-04-16 08:13:50 +0400186 {
187 appDir := fmt.Sprintf("/apps/%s-headscale", app.Name())
188 namespace := fmt.Sprintf("%s%s-ingress-private", env.NamespacePrefix, app.Namespace())
189 if err := st.appManager.Install(app, appDir, namespace, map[string]any{
190 "name": fmt.Sprintf("%s-headscale", env.Name),
191 "from": headscaleIP.String(),
192 "to": headscaleIP.String(),
193 "autoAssign": false,
194 "namespace": "metallb-system",
195 }); err != nil {
196 return err
197 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400198 }
gio3af43942024-04-16 08:13:50 +0400199 {
200 appDir := fmt.Sprintf("/apps/%s", app.Name())
201 namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
202 if err := st.appManager.Install(app, appDir, namespace, map[string]any{
203 "name": env.Name,
204 "from": fromIP.String(),
205 "to": toIP.String(),
206 "autoAssign": false,
207 "namespace": "metallb-system",
208 }); err != nil {
209 return err
210 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400211 }
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400212 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400213 {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400214 keys, err := installer.NewSSHKeyPair("port-allocator")
215 if err != nil {
216 return err
217 }
218 user := fmt.Sprintf("%s-port-allocator", env.Name)
219 if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
220 return err
221 }
222 if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
223 return err
224 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400225 app, err := st.appsRepo.Find("private-network")
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400226 if err != nil {
227 return err
228 }
gio3af43942024-04-16 08:13:50 +0400229 appDir := fmt.Sprintf("/apps/%s", app.Name())
230 namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
231 if err := st.appManager.Install(app, appDir, namespace, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400232 "privateNetwork": map[string]any{
233 "hostname": "private-network-proxy",
234 "username": "private-network-proxy",
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400235 "ipSubnet": fmt.Sprintf("%s/24", startIP.String()),
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400236 },
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400237 "sshPrivateKey": string(keys.RawPrivateKey()),
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400238 }); err != nil {
239 return err
240 }
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400241 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400242 return nil
243 })
244 return &t
245}
246
247func SetupCertificateIssuers(env Env, st *state) Task {
248 pub := newLeafTask(fmt.Sprintf("Public %s", env.Domain), func() error {
249 app, err := st.appsRepo.Find("certificate-issuer-public")
250 if err != nil {
251 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400252 }
gio3af43942024-04-16 08:13:50 +0400253 appDir := fmt.Sprintf("/apps/%s", app.Name())
254 namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
255 if err := st.appManager.Install(app, appDir, namespace, map[string]any{}); err != nil {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400256 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400257 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400258 return nil
259 })
260 priv := newLeafTask(fmt.Sprintf("Private p.%s", env.Domain), func() error {
261 app, err := st.appsRepo.Find("certificate-issuer-private")
262 if err != nil {
263 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400264 }
gio3af43942024-04-16 08:13:50 +0400265 appDir := fmt.Sprintf("/apps/%s", app.Name())
266 namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
267 if err := st.appManager.Install(app, appDir, namespace, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400268 "apiConfigMap": map[string]any{
269 "name": "api-config", // TODO(gio): take from global pcloud config
270 "namespace": fmt.Sprintf("%s-dns-zone-manager", env.PCloudEnvName),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400271 },
272 }); err != nil {
273 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400274 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400275 return nil
276 })
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400277 return newSequentialParentTask("Configure TLS certificate issuers", false, &pub, &priv)
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400278}
279
280func SetupAuth(env Env, st *state) Task {
281 t := newLeafTask("Setup", func() error {
282 app, err := st.appsRepo.Find("core-auth")
283 if err != nil {
284 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400285 }
gio3af43942024-04-16 08:13:50 +0400286 appDir := fmt.Sprintf("/apps/%s", app.Name())
287 namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
288 if err := st.appManager.Install(app, appDir, namespace, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400289 "subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400290 }); err != nil {
291 return err
292 }
293 return nil
294 })
295 return newSequentialParentTask(
296 "Authentication services",
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400297 false,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400298 &t,
299 waitForAddr(fmt.Sprintf("https://accounts-ui.%s", env.Domain)),
300 )
301}
302
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400303func SetupGroupMemberships(env Env, st *state) Task {
304 t := newLeafTask("Setup", func() error {
305 app, err := st.appsRepo.Find("memberships")
306 if err != nil {
307 return err
308 }
gio3af43942024-04-16 08:13:50 +0400309 appDir := fmt.Sprintf("/apps/%s", app.Name())
310 namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
311 if err := st.appManager.Install(app, appDir, namespace, map[string]any{
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400312 "authGroups": strings.Join(initGroups, ","),
313 }); err != nil {
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400314 return err
315 }
316 return nil
317 })
318 return newSequentialParentTask(
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400319 "Group membership",
320 false,
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400321 &t,
322 waitForAddr(fmt.Sprintf("https://memberships.p.%s", env.Domain)),
323 )
324}
325
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400326func SetupHeadscale(env Env, startIP net.IP, st *state) Task {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400327 t := newLeafTask("Setup", func() error {
328 app, err := st.appsRepo.Find("headscale")
329 if err != nil {
330 return err
331 }
gio3af43942024-04-16 08:13:50 +0400332 appDir := fmt.Sprintf("/apps/%s", app.Name())
333 namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
334 if err := st.appManager.Install(app, appDir, namespace, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400335 "subdomain": "headscale",
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400336 "ipSubnet": fmt.Sprintf("%s/24", startIP),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400337 }); err != nil {
338 return err
339 }
340 return nil
341 })
342 return newSequentialParentTask(
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400343 "Setup mesh VPN",
344 false,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400345 &t,
346 waitForAddr(fmt.Sprintf("https://headscale.%s/apple", env.Domain)),
347 )
348}
349
350func SetupWelcome(env Env, st *state) Task {
351 t := newLeafTask("Setup", func() error {
352 keys, err := installer.NewSSHKeyPair("welcome")
353 if err != nil {
354 return err
355 }
356 user := fmt.Sprintf("%s-welcome", env.Name)
357 if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
358 return err
359 }
360 if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
361 return err
362 }
363 app, err := st.appsRepo.Find("welcome")
364 if err != nil {
365 return err
366 }
gio3af43942024-04-16 08:13:50 +0400367 appDir := fmt.Sprintf("/apps/%s", app.Name())
368 namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
369 if err := st.appManager.Install(app, appDir, namespace, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400370 "repoAddr": st.ssClient.GetRepoAddress("config"),
371 "sshPrivateKey": string(keys.RawPrivateKey()),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400372 }); err != nil {
373 return err
374 }
375 return nil
376 })
377 return newSequentialParentTask(
378 "Welcome service",
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400379 false,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400380 &t,
381 waitForAddr(fmt.Sprintf("https://welcome.%s", env.Domain)),
382 )
383}
384
385func SetupAppStore(env Env, st *state) Task {
386 t := newLeafTask("Application marketplace", func() error {
387 user := fmt.Sprintf("%s-appmanager", env.Name)
388 keys, err := installer.NewSSHKeyPair(user)
389 if err != nil {
390 return err
391 }
392 if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
393 return err
394 }
395 if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
396 return err
397 }
398 app, err := st.appsRepo.Find("app-manager") // TODO(giolekva): configure
399 if err != nil {
400 return err
401 }
gio3af43942024-04-16 08:13:50 +0400402 appDir := fmt.Sprintf("/apps/%s", app.Name())
403 namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
404 if err := st.appManager.Install(app, appDir, namespace, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400405 "repoAddr": st.ssClient.GetRepoAddress("config"),
406 "sshPrivateKey": string(keys.RawPrivateKey()),
Giorgi Lekveishvili3c91e8b2024-03-25 20:20:14 +0400407 "authGroups": strings.Join(initGroups, ","),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400408 }); err != nil {
409 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400410 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400411 return nil
412 })
413 return &t
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400414}
415
416type DNSSecKey struct {
417 Basename string `json:"basename,omitempty"`
418 Key []byte `json:"key,omitempty"`
419 Private []byte `json:"private,omitempty"`
420 DS []byte `json:"ds,omitempty"`
421}
422
423func newDNSSecKey(zone string) (DNSSecKey, error) {
424 key := &dns.DNSKEY{
425 Hdr: dns.RR_Header{Name: dns.Fqdn(zone), Class: dns.ClassINET, Ttl: 3600, Rrtype: dns.TypeDNSKEY},
426 Algorithm: dns.ECDSAP256SHA256, Flags: 257, Protocol: 3,
427 }
428 priv, err := key.Generate(256)
429 if err != nil {
430 return DNSSecKey{}, err
431 }
432 return DNSSecKey{
433 Basename: fmt.Sprintf("K%s+%03d+%05d", key.Header().Name, key.Algorithm, key.KeyTag()),
434 Key: []byte(key.String()),
435 Private: []byte(key.PrivateKeyString(priv)),
436 DS: []byte(key.ToDS(dns.SHA256).String()),
437 }, nil
438}