blob: 39a1cd573ca08da034b5f67f3df8dff6d989a70f [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 Lekveishvili024757c2024-03-14 13:27:29 +040093 branch: main
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 Lekveishvili378ea882023-12-12 13:59:18 +0400193 app, err := st.appsRepo.Find("private-network")
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400194 if err != nil {
195 return err
196 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400197 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400198 "privateNetwork": map[string]any{
199 "hostname": "private-network-proxy",
200 "username": "private-network-proxy",
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400201 "ipSubnet": fmt.Sprintf("%s/24", startIP.String()),
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400202 },
203 }); err != nil {
204 return err
205 }
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400206 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400207 return nil
208 })
209 return &t
210}
211
212func SetupCertificateIssuers(env Env, st *state) Task {
213 pub := newLeafTask(fmt.Sprintf("Public %s", env.Domain), func() error {
214 app, err := st.appsRepo.Find("certificate-issuer-public")
215 if err != nil {
216 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400217 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400218 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{}); err != nil {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400219 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400220 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400221 return nil
222 })
223 priv := newLeafTask(fmt.Sprintf("Private p.%s", env.Domain), func() error {
224 app, err := st.appsRepo.Find("certificate-issuer-private")
225 if err != nil {
226 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400227 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400228 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400229 "apiConfigMap": map[string]any{
230 "name": "api-config", // TODO(gio): take from global pcloud config
231 "namespace": fmt.Sprintf("%s-dns-zone-manager", env.PCloudEnvName),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400232 },
233 }); err != nil {
234 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400235 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400236 return nil
237 })
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400238 return newSequentialParentTask("Configure TLS certificate issuers", false, &pub, &priv)
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400239}
240
241func SetupAuth(env Env, st *state) Task {
242 t := newLeafTask("Setup", func() error {
243 app, err := st.appsRepo.Find("core-auth")
244 if err != nil {
245 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400246 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400247 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400248 "subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400249 }); err != nil {
250 return err
251 }
252 return nil
253 })
254 return newSequentialParentTask(
255 "Authentication services",
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400256 false,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400257 &t,
258 waitForAddr(fmt.Sprintf("https://accounts-ui.%s", env.Domain)),
259 )
260}
261
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400262func SetupGroupMemberships(env Env, st *state) Task {
263 t := newLeafTask("Setup", func() error {
264 app, err := st.appsRepo.Find("memberships")
265 if err != nil {
266 return err
267 }
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400268 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
269 "authGroups": strings.Join(initGroups, ","),
270 }); err != nil {
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400271 return err
272 }
273 return nil
274 })
275 return newSequentialParentTask(
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400276 "Group membership",
277 false,
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400278 &t,
279 waitForAddr(fmt.Sprintf("https://memberships.p.%s", env.Domain)),
280 )
281}
282
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400283func SetupHeadscale(env Env, startIP net.IP, st *state) Task {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400284 t := newLeafTask("Setup", func() error {
285 app, err := st.appsRepo.Find("headscale")
286 if err != nil {
287 return err
288 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400289 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400290 "subdomain": "headscale",
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400291 "ipSubnet": fmt.Sprintf("%s/24", startIP),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400292 }); err != nil {
293 return err
294 }
295 return nil
296 })
297 return newSequentialParentTask(
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400298 "Setup mesh VPN",
299 false,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400300 &t,
301 waitForAddr(fmt.Sprintf("https://headscale.%s/apple", env.Domain)),
302 )
303}
304
305func SetupWelcome(env Env, st *state) Task {
306 t := newLeafTask("Setup", func() error {
307 keys, err := installer.NewSSHKeyPair("welcome")
308 if err != nil {
309 return err
310 }
311 user := fmt.Sprintf("%s-welcome", env.Name)
312 if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
313 return err
314 }
315 if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
316 return err
317 }
318 app, err := st.appsRepo.Find("welcome")
319 if err != nil {
320 return err
321 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400322 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400323 "repoAddr": st.ssClient.GetRepoAddress("config"),
324 "sshPrivateKey": string(keys.RawPrivateKey()),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400325 }); err != nil {
326 return err
327 }
328 return nil
329 })
330 return newSequentialParentTask(
331 "Welcome service",
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400332 false,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400333 &t,
334 waitForAddr(fmt.Sprintf("https://welcome.%s", env.Domain)),
335 )
336}
337
338func SetupAppStore(env Env, st *state) Task {
339 t := newLeafTask("Application marketplace", func() error {
340 user := fmt.Sprintf("%s-appmanager", env.Name)
341 keys, err := installer.NewSSHKeyPair(user)
342 if err != nil {
343 return err
344 }
345 if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
346 return err
347 }
348 if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
349 return err
350 }
351 app, err := st.appsRepo.Find("app-manager") // TODO(giolekva): configure
352 if err != nil {
353 return err
354 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400355 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400356 "repoAddr": st.ssClient.GetRepoAddress("config"),
357 "sshPrivateKey": string(keys.RawPrivateKey()),
Giorgi Lekveishvili3c91e8b2024-03-25 20:20:14 +0400358 "authGroups": strings.Join(initGroups, ","),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400359 }); err != nil {
360 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400361 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400362 return nil
363 })
364 return &t
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400365}
366
367type DNSSecKey struct {
368 Basename string `json:"basename,omitempty"`
369 Key []byte `json:"key,omitempty"`
370 Private []byte `json:"private,omitempty"`
371 DS []byte `json:"ds,omitempty"`
372}
373
374func newDNSSecKey(zone string) (DNSSecKey, error) {
375 key := &dns.DNSKEY{
376 Hdr: dns.RR_Header{Name: dns.Fqdn(zone), Class: dns.ClassINET, Ttl: 3600, Rrtype: dns.TypeDNSKEY},
377 Algorithm: dns.ECDSAP256SHA256, Flags: 257, Protocol: 3,
378 }
379 priv, err := key.Generate(256)
380 if err != nil {
381 return DNSSecKey{}, err
382 }
383 return DNSSecKey{
384 Basename: fmt.Sprintf("K%s+%03d+%05d", key.Header().Name, key.Algorithm, key.KeyTag()),
385 Key: []byte(key.String()),
386 Private: []byte(key.PrivateKeyString(priv)),
387 DS: []byte(key.ToDS(dns.SHA256).String()),
388 }, nil
389}