blob: 0685a6d3ab6b9b9f8f98e02ef81b0127eafc230f [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 Lekveishvili5c1b06e2024-03-28 15:19:44 +040033 return &t
34}
35
36func SetupInfra(env Env, startIP net.IP, st *state) Task {
37 return newConcurrentParentTask(
38 "Setup core services",
39 true,
40 SetupNetwork(env, startIP, st),
41 SetupCertificateIssuers(env, st),
42 SetupAuth(env, st),
43 SetupGroupMemberships(env, st),
44 SetupHeadscale(env, startIP, st),
45 SetupWelcome(env, st),
46 SetupAppStore(env, st),
47 )
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040048}
49
50func CommitEnvironmentConfiguration(env Env, st *state) Task {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +040051 t := newLeafTask("commit config", func() error {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040052 repo, err := st.ssClient.GetRepo("config")
53 if err != nil {
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +040054 return err
55 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040056 r := installer.NewRepoIO(repo, st.ssClient.Signer)
57 {
58 // TODO(giolekva): private domain can be configurable as well
59 config := installer.Config{
60 Values: installer.Values{
61 PCloudEnvName: env.PCloudEnvName,
62 Id: env.Name,
63 ContactEmail: env.ContactEmail,
64 Domain: env.Domain,
65 PrivateDomain: fmt.Sprintf("p.%s", env.Domain),
66 PublicIP: st.publicIPs[0].String(),
67 NamespacePrefix: fmt.Sprintf("%s-", env.Name),
68 },
69 }
70 if err := r.WriteYaml("config.yaml", config); err != nil {
71 return err
72 }
73 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +040074 {
75 out, err := r.Writer("pcloud-charts.yaml")
76 if err != nil {
77 return err
78 }
79 defer out.Close()
80 _, err = fmt.Fprintf(out, `
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +040081apiVersion: source.toolkit.fluxcd.io/v1
82kind: GitRepository
83metadata:
84 name: pcloud
85 namespace: %s
86spec:
87 interval: 1m0s
88 url: https://github.com/giolekva/pcloud
89 ref:
Giorgi Lekveishvili024757c2024-03-14 13:27:29 +040090 branch: main
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +040091`, env.Name)
92 if err != nil {
93 return err
94 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +040095 rootKust, err := r.ReadKustomization("kustomization.yaml")
96 if err != nil {
97 return err
98 }
99 rootKust.AddResources("pcloud-charts.yaml")
100 if err := r.WriteKustomization("kustomization.yaml", *rootKust); err != nil {
101 return err
102 }
103 r.CommitAndPush("configure charts repo")
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400104 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400105 return nil
106 })
107 return &t
108}
109
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400110type firstAccount struct {
111 Created bool `json:"created"`
112 Groups []string `json:"groups"`
113}
114
115func ConfigureFirstAccount(env Env, st *state) Task {
116 t := newLeafTask("Configure first account settings", func() error {
117 repo, err := st.ssClient.GetRepo("config")
118 if err != nil {
119 return err
120 }
121 r := installer.NewRepoIO(repo, st.ssClient.Signer)
122 fa := firstAccount{false, initGroups}
123 if err := r.WriteYaml("first-account.yaml", fa); err != nil {
124 return err
125 }
126 return r.CommitAndPush("first account membership configuration")
127 })
128 return &t
129}
130
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400131func SetupNetwork(env Env, startIP net.IP, st *state) Task {
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400132 t := newLeafTask("Setup private and public networks", func() error {
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400133 startAddr, err := netip.ParseAddr(startIP.String())
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400134 if err != nil {
135 return err
136 }
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400137 if !startAddr.Is4() {
138 return fmt.Errorf("Expected IPv4, got %s instead", startAddr)
139 }
140 addr := startAddr.AsSlice()
141 if addr[3] != 0 {
142 return fmt.Errorf("Expected last byte to be zero, got %d instead", addr[3])
143 }
144 addr[3] = 10
145 fromIP, ok := netip.AddrFromSlice(addr)
146 if !ok {
147 return fmt.Errorf("Must not reach")
148 }
149 addr[3] = 254
150 toIP, ok := netip.AddrFromSlice(addr)
151 if !ok {
152 return fmt.Errorf("Must not reach")
153 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400154 {
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400155 ingressPrivateIP := startAddr
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400156 headscaleIP := ingressPrivateIP.Next()
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400157 app, err := st.appsRepo.Find("metallb-ipaddresspool")
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400158 if err != nil {
159 return err
160 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400161 if err := st.appManager.Install(app, st.nsGen, installer.NewSuffixGenerator("-ingress-private"), map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400162 "name": fmt.Sprintf("%s-ingress-private", env.Name),
163 "from": ingressPrivateIP.String(),
164 "to": ingressPrivateIP.String(),
165 "autoAssign": false,
166 "namespace": "metallb-system",
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400167 }); err != nil {
168 return err
169 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400170 if err := st.appManager.Install(app, st.nsGen, installer.NewSuffixGenerator("-headscale"), map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400171 "name": fmt.Sprintf("%s-headscale", env.Name),
172 "from": headscaleIP.String(),
173 "to": headscaleIP.String(),
174 "autoAssign": false,
175 "namespace": "metallb-system",
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400176 }); err != nil {
177 return err
178 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400179 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400180 "name": env.Name,
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400181 "from": fromIP.String(),
182 "to": toIP.String(),
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400183 "autoAssign": false,
184 "namespace": "metallb-system",
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400185 }); err != nil {
186 return err
187 }
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400188 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400189 {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400190 app, err := st.appsRepo.Find("private-network")
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400191 if err != nil {
192 return err
193 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400194 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400195 "privateNetwork": map[string]any{
196 "hostname": "private-network-proxy",
197 "username": "private-network-proxy",
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400198 "ipSubnet": fmt.Sprintf("%s/24", startIP.String()),
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400199 },
200 }); err != nil {
201 return err
202 }
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400203 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400204 return nil
205 })
206 return &t
207}
208
209func SetupCertificateIssuers(env Env, st *state) Task {
210 pub := newLeafTask(fmt.Sprintf("Public %s", env.Domain), func() error {
211 app, err := st.appsRepo.Find("certificate-issuer-public")
212 if err != nil {
213 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400214 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400215 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{}); err != nil {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400216 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400217 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400218 return nil
219 })
220 priv := newLeafTask(fmt.Sprintf("Private p.%s", env.Domain), func() error {
221 app, err := st.appsRepo.Find("certificate-issuer-private")
222 if err != nil {
223 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400224 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400225 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400226 "apiConfigMap": map[string]any{
227 "name": "api-config", // TODO(gio): take from global pcloud config
228 "namespace": fmt.Sprintf("%s-dns-zone-manager", env.PCloudEnvName),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400229 },
230 }); err != nil {
231 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400232 }
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400233 return nil
234 })
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400235 return newSequentialParentTask("Configure TLS certificate issuers", false, &pub, &priv)
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400236}
237
238func SetupAuth(env Env, st *state) Task {
239 t := newLeafTask("Setup", func() error {
240 app, err := st.appsRepo.Find("core-auth")
241 if err != nil {
242 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400243 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400244 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400245 "subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400246 }); err != nil {
247 return err
248 }
249 return nil
250 })
251 return newSequentialParentTask(
252 "Authentication services",
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400253 false,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400254 &t,
255 waitForAddr(fmt.Sprintf("https://accounts-ui.%s", env.Domain)),
256 )
257}
258
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400259func SetupGroupMemberships(env Env, st *state) Task {
260 t := newLeafTask("Setup", func() error {
261 app, err := st.appsRepo.Find("memberships")
262 if err != nil {
263 return err
264 }
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400265 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
266 "authGroups": strings.Join(initGroups, ","),
267 }); err != nil {
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400268 return err
269 }
270 return nil
271 })
272 return newSequentialParentTask(
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400273 "Group membership",
274 false,
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400275 &t,
276 waitForAddr(fmt.Sprintf("https://memberships.p.%s", env.Domain)),
277 )
278}
279
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400280func SetupHeadscale(env Env, startIP net.IP, st *state) Task {
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400281 t := newLeafTask("Setup", func() error {
282 app, err := st.appsRepo.Find("headscale")
283 if err != nil {
284 return err
285 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400286 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400287 "subdomain": "headscale",
Giorgi Lekveishvili9d5e3f52024-03-13 15:02:50 +0400288 "ipSubnet": fmt.Sprintf("%s/24", startIP),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400289 }); err != nil {
290 return err
291 }
292 return nil
293 })
294 return newSequentialParentTask(
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400295 "Setup mesh VPN",
296 false,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400297 &t,
298 waitForAddr(fmt.Sprintf("https://headscale.%s/apple", env.Domain)),
299 )
300}
301
302func SetupWelcome(env Env, st *state) Task {
303 t := newLeafTask("Setup", func() error {
304 keys, err := installer.NewSSHKeyPair("welcome")
305 if err != nil {
306 return err
307 }
308 user := fmt.Sprintf("%s-welcome", env.Name)
309 if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
310 return err
311 }
312 if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
313 return err
314 }
315 app, err := st.appsRepo.Find("welcome")
316 if err != nil {
317 return err
318 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400319 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400320 "repoAddr": st.ssClient.GetRepoAddress("config"),
321 "sshPrivateKey": string(keys.RawPrivateKey()),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400322 }); err != nil {
323 return err
324 }
325 return nil
326 })
327 return newSequentialParentTask(
328 "Welcome service",
Giorgi Lekveishvili5c1b06e2024-03-28 15:19:44 +0400329 false,
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400330 &t,
331 waitForAddr(fmt.Sprintf("https://welcome.%s", env.Domain)),
332 )
333}
334
335func SetupAppStore(env Env, st *state) Task {
336 t := newLeafTask("Application marketplace", func() error {
337 user := fmt.Sprintf("%s-appmanager", env.Name)
338 keys, err := installer.NewSSHKeyPair(user)
339 if err != nil {
340 return err
341 }
342 if err := st.ssClient.AddUser(user, keys.AuthorizedKey()); err != nil {
343 return err
344 }
345 if err := st.ssClient.AddReadWriteCollaborator("config", user); err != nil {
346 return err
347 }
348 app, err := st.appsRepo.Find("app-manager") // TODO(giolekva): configure
349 if err != nil {
350 return err
351 }
Giorgi Lekveishvili08af67a2024-01-18 08:53:05 +0400352 if err := st.appManager.Install(app, st.nsGen, st.emptySuffixGen, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400353 "repoAddr": st.ssClient.GetRepoAddress("config"),
354 "sshPrivateKey": string(keys.RawPrivateKey()),
Giorgi Lekveishvili3c91e8b2024-03-25 20:20:14 +0400355 "authGroups": strings.Join(initGroups, ","),
Giorgi Lekveishvili378ea882023-12-12 13:59:18 +0400356 }); err != nil {
357 return err
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400358 }
Giorgi Lekveishvili77ee2dc2023-12-11 16:51:10 +0400359 return nil
360 })
361 return &t
Giorgi Lekveishvili46743d42023-12-10 15:47:23 +0400362}
363
364type DNSSecKey struct {
365 Basename string `json:"basename,omitempty"`
366 Key []byte `json:"key,omitempty"`
367 Private []byte `json:"private,omitempty"`
368 DS []byte `json:"ds,omitempty"`
369}
370
371func newDNSSecKey(zone string) (DNSSecKey, error) {
372 key := &dns.DNSKEY{
373 Hdr: dns.RR_Header{Name: dns.Fqdn(zone), Class: dns.ClassINET, Ttl: 3600, Rrtype: dns.TypeDNSKEY},
374 Algorithm: dns.ECDSAP256SHA256, Flags: 257, Protocol: 3,
375 }
376 priv, err := key.Generate(256)
377 if err != nil {
378 return DNSSecKey{}, err
379 }
380 return DNSSecKey{
381 Basename: fmt.Sprintf("K%s+%03d+%05d", key.Header().Name, key.Algorithm, key.KeyTag()),
382 Key: []byte(key.String()),
383 Private: []byte(key.PrivateKeyString(priv)),
384 DS: []byte(key.ToDS(dns.SHA256).String()),
385 }, nil
386}