blob: 914307c5dd6d5b5e4c729a7eed902609b8b2e7d9 [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 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +040022 r := installer.NewRepoIO(repo, st.ssClient.Signer)
23 appManager, err := installer.NewAppManager(r, st.nsCreator)
24 if err != nil {
25 return err
26 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040027 st.appManager = appManager
28 st.appsRepo = installer.NewInMemoryAppRepository(installer.CreateAllApps())
29 st.nsGen = installer.NewPrefixGenerator(env.Name + "-")
30 st.emptySuffixGen = installer.NewEmptySuffixGenerator()
31 return nil
32 })
Giorgi Lekveishviliab7ff6e2024-03-29 13:11:30 +040033 t.beforeStart = func() {
34 st.infoListener("Setting up core infrastructure services.")
35 }
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +040036 return &t
37}
38
39func SetupInfra(env Env, startIP net.IP, st *state) Task {
40 return newConcurrentParentTask(
41 "Setup core services",
42 true,
43 SetupNetwork(env, startIP, st),
44 SetupCertificateIssuers(env, st),
45 SetupAuth(env, st),
46 SetupGroupMemberships(env, st),
47 SetupHeadscale(env, startIP, st),
48 SetupWelcome(env, st),
49 SetupAppStore(env, st),
50 )
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040051}
52
53func CommitEnvironmentConfiguration(env Env, st *state) Task {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +040054 t := newLeafTask("commit config", func() error {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040055 repo, err := st.ssClient.GetRepo("config")
56 if err != nil {
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +040057 return err
58 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040059 r := installer.NewRepoIO(repo, st.ssClient.Signer)
60 {
61 // TODO(giolekva): private domain can be configurable as well
62 config := installer.Config{
63 Values: installer.Values{
64 PCloudEnvName: env.PCloudEnvName,
65 Id: env.Name,
66 ContactEmail: env.ContactEmail,
67 Domain: env.Domain,
68 PrivateDomain: fmt.Sprintf("p.%s", env.Domain),
69 PublicIP: st.publicIPs[0].String(),
70 NamespacePrefix: fmt.Sprintf("%s-", env.Name),
71 },
72 }
73 if err := r.WriteYaml("config.yaml", config); err != nil {
74 return err
75 }
76 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +040077 {
78 out, err := r.Writer("pcloud-charts.yaml")
79 if err != nil {
80 return err
81 }
82 defer out.Close()
83 _, err = fmt.Fprintf(out, `
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040084apiVersion: source.toolkit.fluxcd.io/v1
85kind: GitRepository
86metadata:
87 name: pcloud
88 namespace: %s
89spec:
90 interval: 1m0s
91 url: https://github.com/giolekva/pcloud
92 ref:
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040093 branch: ingress-port-allocator
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +040094`, env.Name)
95 if err != nil {
96 return err
97 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040098 rootKust, err := r.ReadKustomization("kustomization.yaml")
99 if err != nil {
100 return err
101 }
102 rootKust.AddResources("pcloud-charts.yaml")
103 if err := r.WriteKustomization("kustomization.yaml", *rootKust); err != nil {
104 return err
105 }
106 r.CommitAndPush("configure charts repo")
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400107 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400108 return nil
109 })
110 return &t
111}
112
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400113type firstAccount struct {
114 Created bool `json:"created"`
115 Groups []string `json:"groups"`
116}
117
118func ConfigureFirstAccount(env Env, st *state) Task {
119 t := newLeafTask("Configure first account settings", func() error {
120 repo, err := st.ssClient.GetRepo("config")
121 if err != nil {
122 return err
123 }
124 r := installer.NewRepoIO(repo, st.ssClient.Signer)
125 fa := firstAccount{false, initGroups}
126 if err := r.WriteYaml("first-account.yaml", fa); err != nil {
127 return err
128 }
129 return r.CommitAndPush("first account membership configuration")
130 })
131 return &t
132}
133
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400134func SetupNetwork(env Env, startIP net.IP, st *state) Task {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400135 t := newLeafTask("Setup private and public networks", func() error {
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400136 startAddr, err := netip.ParseAddr(startIP.String())
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400137 if err != nil {
138 return err
139 }
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400140 if !startAddr.Is4() {
141 return fmt.Errorf("Expected IPv4, got %s instead", startAddr)
142 }
143 addr := startAddr.AsSlice()
144 if addr[3] != 0 {
145 return fmt.Errorf("Expected last byte to be zero, got %d instead", addr[3])
146 }
147 addr[3] = 10
148 fromIP, ok := netip.AddrFromSlice(addr)
149 if !ok {
150 return fmt.Errorf("Must not reach")
151 }
152 addr[3] = 254
153 toIP, ok := netip.AddrFromSlice(addr)
154 if !ok {
155 return fmt.Errorf("Must not reach")
156 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400157 {
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400158 ingressPrivateIP := startAddr
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400159 headscaleIP := ingressPrivateIP.Next()
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400160 app, err := st.appsRepo.Find("metallb-ipaddresspool")
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400161 if err != nil {
162 return err
163 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400164 if err := st.appManager.Install(app, st.nsGen, installer.NewSuffixGenerator("-ingress-private"), map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400165 "name": fmt.Sprintf("%s-ingress-private", env.Name),
166 "from": ingressPrivateIP.String(),
167 "to": ingressPrivateIP.String(),
168 "autoAssign": false,
169 "namespace": "metallb-system",
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400170 }); err != nil {
171 return err
172 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400173 if err := st.appManager.Install(app, st.nsGen, installer.NewSuffixGenerator("-headscale"), map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400174 "name": fmt.Sprintf("%s-headscale", env.Name),
175 "from": headscaleIP.String(),
176 "to": headscaleIP.String(),
177 "autoAssign": false,
178 "namespace": "metallb-system",
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400179 }); err != nil {
180 return err
181 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400182 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400183 "name": env.Name,
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400184 "from": fromIP.String(),
185 "to": toIP.String(),
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400186 "autoAssign": false,
187 "namespace": "metallb-system",
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400188 }); err != nil {
189 return err
190 }
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400191 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400192 {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400193 keys, err := installer.NewSSHKeyPair("port-allocator")
194 if err != nil {
195 return err
196 }
197 user := fmt.Sprintf("%s-port-allocator", env.Name)
198 if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
199 return err
200 }
201 if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
202 return err
203 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400204 app, err := st.appsRepo.Find("private-network")
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400205 if err != nil {
206 return err
207 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400208 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400209 "privateNetwork": map[string]any{
210 "hostname": "private-network-proxy",
211 "username": "private-network-proxy",
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400212 "ipSubnet": fmt.Sprintf("%s/24", startIP.String()),
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400213 },
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400214 "sshPrivateKey": string(keys.RawPrivateKey()),
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400215 }); err != nil {
216 return err
217 }
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400218 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400219 return nil
220 })
221 return &t
222}
223
224func SetupCertificateIssuers(env Env, st *state) Task {
225 pub := newLeafTask(fmt.Sprintf("Public %s", env.Domain), func() error {
226 app, err := st.appsRepo.Find("certificate-issuer-public")
227 if err != nil {
228 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400229 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400230 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{}); err != nil {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400231 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400232 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400233 return nil
234 })
235 priv := newLeafTask(fmt.Sprintf("Private p.%s", env.Domain), func() error {
236 app, err := st.appsRepo.Find("certificate-issuer-private")
237 if err != nil {
238 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400239 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400240 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400241 "apiConfigMap": map[string]any{
242 "name": "api-config", // TODO(gio): take from global pcloud config
243 "namespace": fmt.Sprintf("%s-dns-zone-manager", env.PCloudEnvName),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400244 },
245 }); err != nil {
246 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400247 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400248 return nil
249 })
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400250 return newSequentialParentTask("Configure TLS certificate issuers", false, &pub, &priv)
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400251}
252
253func SetupAuth(env Env, st *state) Task {
254 t := newLeafTask("Setup", func() error {
255 app, err := st.appsRepo.Find("core-auth")
256 if err != nil {
257 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400258 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400259 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400260 "subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400261 }); err != nil {
262 return err
263 }
264 return nil
265 })
266 return newSequentialParentTask(
267 "Authentication services",
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400268 false,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400269 &t,
270 waitForAddr(fmt.Sprintf("https://accounts-ui.%s", env.Domain)),
271 )
272}
273
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400274func SetupGroupMemberships(env Env, st *state) Task {
275 t := newLeafTask("Setup", func() error {
276 app, err := st.appsRepo.Find("memberships")
277 if err != nil {
278 return err
279 }
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400280 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
281 "authGroups": strings.Join(initGroups, ","),
282 }); err != nil {
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400283 return err
284 }
285 return nil
286 })
287 return newSequentialParentTask(
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400288 "Group membership",
289 false,
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400290 &t,
291 waitForAddr(fmt.Sprintf("https://memberships.p.%s", env.Domain)),
292 )
293}
294
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400295func SetupHeadscale(env Env, startIP net.IP, st *state) Task {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400296 t := newLeafTask("Setup", func() error {
297 app, err := st.appsRepo.Find("headscale")
298 if err != nil {
299 return err
300 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400301 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400302 "subdomain": "headscale",
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400303 "ipSubnet": fmt.Sprintf("%s/24", startIP),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400304 }); err != nil {
305 return err
306 }
307 return nil
308 })
309 return newSequentialParentTask(
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400310 "Setup mesh VPN",
311 false,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400312 &t,
313 waitForAddr(fmt.Sprintf("https://headscale.%s/apple", env.Domain)),
314 )
315}
316
317func SetupWelcome(env Env, st *state) Task {
318 t := newLeafTask("Setup", func() error {
319 keys, err := installer.NewSSHKeyPair("welcome")
320 if err != nil {
321 return err
322 }
323 user := fmt.Sprintf("%s-welcome", env.Name)
324 if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
325 return err
326 }
327 if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
328 return err
329 }
330 app, err := st.appsRepo.Find("welcome")
331 if err != nil {
332 return err
333 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400334 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400335 "repoAddr": st.ssClient.GetRepoAddress("config"),
336 "sshPrivateKey": string(keys.RawPrivateKey()),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400337 }); err != nil {
338 return err
339 }
340 return nil
341 })
342 return newSequentialParentTask(
343 "Welcome service",
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400344 false,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400345 &t,
346 waitForAddr(fmt.Sprintf("https://welcome.%s", env.Domain)),
347 )
348}
349
350func SetupAppStore(env Env, st *state) Task {
351 t := newLeafTask("Application marketplace", func() error {
352 user := fmt.Sprintf("%s-appmanager", env.Name)
353 keys, err := installer.NewSSHKeyPair(user)
354 if err != nil {
355 return err
356 }
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("app-manager") // TODO(giolekva): configure
364 if err != nil {
365 return err
366 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400367 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400368 "repoAddr": st.ssClient.GetRepoAddress("config"),
369 "sshPrivateKey": string(keys.RawPrivateKey()),
Giorgi Lekveishvili3c91e8b2024-03-25 20:20:14 +0400370 "authGroups": strings.Join(initGroups, ","),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400371 }); err != nil {
372 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400373 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400374 return nil
375 })
376 return &t
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400377}
378
379type DNSSecKey struct {
380 Basename string `json:"basename,omitempty"`
381 Key []byte `json:"key,omitempty"`
382 Private []byte `json:"private,omitempty"`
383 DS []byte `json:"ds,omitempty"`
384}
385
386func newDNSSecKey(zone string) (DNSSecKey, error) {
387 key := &dns.DNSKEY{
388 Hdr: dns.RR_Header{Name: dns.Fqdn(zone), Class: dns.ClassINET, Ttl: 3600, Rrtype: dns.TypeDNSKEY},
389 Algorithm: dns.ECDSAP256SHA256, Flags: 257, Protocol: 3,
390 }
391 priv, err := key.Generate(256)
392 if err != nil {
393 return DNSSecKey{}, err
394 }
395 return DNSSecKey{
396 Basename: fmt.Sprintf("K%s+%03d+%05d", key.Header().Name, key.Algorithm, key.KeyTag()),
397 Key: []byte(key.String()),
398 Private: []byte(key.PrivateKeyString(priv)),
399 DS: []byte(key.ToDS(dns.SHA256).String()),
400 }, nil
401}