blob: 953666ca025b15a0d36d3c25ef5ff5dc9dd27880 [file] [log] [blame]
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +04001package installer
2
3import (
4 "context"
5 _ "embed"
6 "fmt"
7 "log"
8 "net/netip"
9 "path/filepath"
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040010 "strings"
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040011 "time"
12
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040013 "helm.sh/helm/v3/pkg/action"
14 "helm.sh/helm/v3/pkg/chart"
15 "helm.sh/helm/v3/pkg/chart/loader"
16
17 "github.com/giolekva/pcloud/core/installer/soft"
18)
19
20const IPAddressPoolLocal = "local"
21const IPAddressPoolConfigRepo = "config-repo"
22const IPAddressPoolIngressPublic = "ingress-public"
23
24type Bootstrapper struct {
25 cl ChartLoader
26 ns NamespaceCreator
27 ha HelmActionConfigFactory
28}
29
30func NewBootstrapper(cl ChartLoader, ns NamespaceCreator, ha HelmActionConfigFactory) Bootstrapper {
31 return Bootstrapper{cl, ns, ha}
32}
33
34func (b Bootstrapper) Run(env EnvConfig) error {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040035 if err := b.ns.Create(env.Name); err != nil {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040036 return err
37 }
38 if err := b.installMetallb(env); err != nil {
39 return err
40 }
41 if err := b.installLonghorn(env.Name, env.StorageDir, env.VolumeDefaultReplicaCount); err != nil {
42 return err
43 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040044 bootstrapJobKeys, err := NewSSHKeyPair("bootstrapper")
45 if err != nil {
46 return err
47 }
48 if err := b.installSoftServe(bootstrapJobKeys.AuthorizedKey(), env.Name, env.ServiceIPs.ConfigRepo); err != nil {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040049 return err
50 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040051 ss, err := soft.WaitForClient(
52 netip.AddrPortFrom(env.ServiceIPs.ConfigRepo, 22).String(),
53 bootstrapJobKeys.RawPrivateKey(),
54 log.Default())
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040055 if err != nil {
56 return err
57 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040058 defer func() {
59 if ss.RemovePublicKey("admin", bootstrapJobKeys.AuthorizedKey()); err != nil {
60 fmt.Printf("Failed to remove admin public key: %s\n", err.Error())
61 }
62 }()
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040063 if ss.AddPublicKey("admin", string(env.AdminPublicKey)); err != nil {
64 return err
65 }
66 if err := b.installFluxcd(ss, env.Name); err != nil {
67 return err
68 }
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +040069 fmt.Println("Fluxcd installed")
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +040070 repo, err := ss.GetRepo("config")
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040071 if err != nil {
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +040072 fmt.Println("Failed to get config repo")
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040073 return err
74 }
75 repoIO := NewRepoIO(repo, ss.Signer)
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +040076 fmt.Println("Configuring main repo")
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040077 if err := configureMainRepo(repoIO, env); err != nil {
78 return err
79 }
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +040080 fmt.Println("Installing infrastructure services")
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040081 nsGen := NewPrefixGenerator(env.NamespacePrefix)
82 if err := b.installInfrastructureServices(repoIO, nsGen, b.ns, env); err != nil {
83 return err
84 }
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +040085 fmt.Println("Installing DNS Zone Manager")
86 if err := b.installDNSZoneManager(ss, repoIO, nsGen, b.ns, env); err != nil {
87 return err
88 }
89 fmt.Println("Installing env manager")
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040090 if err := b.installEnvManager(ss, repoIO, nsGen, b.ns, env); err != nil {
91 return err
92 }
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +040093 fmt.Println("Environment ready to use")
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040094 return nil
95}
96
97func (b Bootstrapper) installMetallb(env EnvConfig) error {
98 if err := b.installMetallbNamespace(env); err != nil {
99 return err
100 }
101 if err := b.installMetallbService(); err != nil {
102 return err
103 }
104 if err := b.installMetallbIPAddressPool(IPAddressPoolLocal, true, env.ServiceIPs.From, env.ServiceIPs.To); err != nil {
105 return err
106 }
107 if err := b.installMetallbIPAddressPool(IPAddressPoolConfigRepo, false, env.ServiceIPs.ConfigRepo, env.ServiceIPs.ConfigRepo); err != nil {
108 return err
109 }
110 if err := b.installMetallbIPAddressPool(IPAddressPoolIngressPublic, false, env.ServiceIPs.IngressPublic, env.ServiceIPs.IngressPublic); err != nil {
111 return err
112 }
113 return nil
114}
115
116func (b Bootstrapper) installMetallbNamespace(env EnvConfig) error {
117 fmt.Println("Installing metallb namespace")
118 config, err := b.ha.New(env.Name)
119 if err != nil {
120 return err
121 }
122 chart, err := b.cl.Load("namespace")
123 if err != nil {
124 return err
125 }
126 values := map[string]any{
127 "namespace": "metallb-system",
128 "labels": []string{
129 "pod-security.kubernetes.io/audit: privileged",
130 "pod-security.kubernetes.io/enforce: privileged",
131 "pod-security.kubernetes.io/warn: privileged",
132 },
133 }
134 installer := action.NewInstall(config)
135 installer.Namespace = env.Name
136 installer.ReleaseName = "metallb-ns"
137 installer.Wait = true
138 installer.WaitForJobs = true
139 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
140 return err
141 }
142 return nil
143}
144
145func (b Bootstrapper) installMetallbService() error {
146 fmt.Println("Installing metallb")
147 config, err := b.ha.New("metallb-system")
148 if err != nil {
149 return err
150 }
151 chart, err := b.cl.Load("metallb")
152 if err != nil {
153 return err
154 }
155 values := map[string]any{ // TODO(giolekva): add loadBalancerClass?
156 "controller": map[string]any{
157 "image": map[string]any{
158 "repository": "quay.io/metallb/controller",
Giorgi Lekveishvilic06164d2023-11-22 13:51:29 +0400159 "tag": "v0.13.12",
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400160 "pullPolicy": "IfNotPresent",
161 },
162 "logLevel": "info",
163 },
164 "speaker": map[string]any{
165 "image": map[string]any{
166 "repository": "quay.io/metallb/speaker",
Giorgi Lekveishvilic06164d2023-11-22 13:51:29 +0400167 "tag": "v0.13.12",
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400168 "pullPolicy": "IfNotPresent",
169 },
170 "logLevel": "info",
171 },
172 }
173 installer := action.NewInstall(config)
174 installer.Namespace = "metallb-system"
175 installer.CreateNamespace = true
176 installer.ReleaseName = "metallb"
177 installer.IncludeCRDs = true
178 installer.Wait = true
179 installer.WaitForJobs = true
180 installer.Timeout = 20 * time.Minute
181 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
182 return err
183 }
184 return nil
185}
186
187func (b Bootstrapper) installMetallbIPAddressPool(name string, autoAssign bool, from, to netip.Addr) error {
188 fmt.Printf("Installing metallb-ipaddresspool: %s\n", name)
189 config, err := b.ha.New("metallb-system")
190 if err != nil {
191 return err
192 }
193 chart, err := b.cl.Load("metallb-ipaddresspool")
194 if err != nil {
195 return err
196 }
197 values := map[string]any{
198 "name": name,
199 "autoAssign": autoAssign,
200 "from": from.String(),
201 "to": to.String(),
202 }
203 installer := action.NewInstall(config)
204 installer.Namespace = "metallb-system"
205 installer.CreateNamespace = true
206 installer.ReleaseName = name
207 installer.Wait = true
208 installer.WaitForJobs = true
209 installer.Timeout = 20 * time.Minute
210 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
211 return err
212 }
213 return nil
214}
215
216func (b Bootstrapper) installLonghorn(envName string, storageDir string, volumeDefaultReplicaCount int) error {
217 fmt.Println("Installing Longhorn")
218 config, err := b.ha.New(envName)
219 if err != nil {
220 return err
221 }
222 chart, err := b.cl.Load("longhorn")
223 if err != nil {
224 return err
225 }
226 values := map[string]any{
227 "defaultSettings": map[string]any{
228 "defaultDataPath": storageDir,
229 },
230 "persistence": map[string]any{
231 "defaultClassReplicaCount": volumeDefaultReplicaCount,
232 },
233 "service": map[string]any{
234 "ui": map[string]any{
235 "type": "LoadBalancer",
236 },
237 },
238 "ingress": map[string]any{
239 "enabled": false,
240 },
241 }
242 installer := action.NewInstall(config)
243 installer.Namespace = "longhorn-system"
244 installer.CreateNamespace = true
245 installer.ReleaseName = "longhorn"
246 installer.Wait = true
247 installer.WaitForJobs = true
248 installer.Timeout = 20 * time.Minute
249 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
250 return err
251 }
252 return nil
253}
254
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400255func (b Bootstrapper) installSoftServe(adminPublicKey string, namespace string, repoIP netip.Addr) error {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400256 fmt.Println("Installing SoftServe")
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400257 keys, err := NewSSHKeyPair("soft-serve")
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400258 if err != nil {
259 return err
260 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400261 config, err := b.ha.New(namespace)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400262 if err != nil {
263 return err
264 }
265 chart, err := b.cl.Load("soft-serve")
266 if err != nil {
267 return err
268 }
269 values := map[string]any{
270 "image": map[string]any{
271 "repository": "charmcli/soft-serve",
Giorgi Lekveishvilic06164d2023-11-22 13:51:29 +0400272 "tag": "v0.7.1",
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400273 "pullPolicy": "IfNotPresent",
274 },
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400275 "privateKey": string(keys.RawPrivateKey()),
276 "publicKey": string(keys.RawAuthorizedKey()),
277 "adminKey": adminPublicKey,
278 "reservedIP": repoIP.String(),
279 "serviceType": "LoadBalancer",
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400280 }
281 installer := action.NewInstall(config)
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400282 installer.Namespace = namespace
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400283 installer.CreateNamespace = true
284 installer.ReleaseName = "soft-serve"
285 installer.Wait = true
286 installer.WaitForJobs = true
287 installer.Timeout = 20 * time.Minute
288 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
289 return err
290 }
291 return nil
292}
293
294func (b Bootstrapper) installFluxcd(ss *soft.Client, envName string) error {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400295 keys, err := NewSSHKeyPair("fluxcd")
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400296 if err != nil {
297 return err
298 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400299 if err := ss.AddUser("flux", keys.AuthorizedKey()); err != nil {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400300 return err
301 }
302 if err := ss.MakeUserAdmin("flux"); err != nil {
303 return err
304 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400305 if err := ss.AddRepository("config"); err != nil {
306 return err
307 }
308 repo, err := ss.GetRepo("config")
309 if err != nil {
310 return err
311 }
312 repoIO := NewRepoIO(repo, ss.Signer)
313 if err := repoIO.WriteCommitAndPush("README.md", fmt.Sprintf("# %s systems", envName), "readme"); err != nil {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400314 return err
315 }
316 fmt.Println("Installing Flux")
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400317 ssPublicKeys, err := ss.GetPublicKeys()
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400318 if err != nil {
319 return err
320 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400321 host := strings.Split(ss.Addr, ":")[0]
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400322 if err := b.installFluxBootstrap(
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400323 ss.GetRepoAddress("config"),
324 host,
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400325 ssPublicKeys,
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400326 string(keys.RawPrivateKey()),
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400327 envName,
328 ); err != nil {
329 return err
330 }
331 return nil
332}
333
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400334func (b Bootstrapper) installFluxBootstrap(repoAddr, repoHost string, repoHostPubKeys []string, privateKey, envName string) error {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400335 config, err := b.ha.New(envName)
336 if err != nil {
337 return err
338 }
339 chart, err := b.cl.Load("flux-bootstrap")
340 if err != nil {
341 return err
342 }
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400343 var lines []string
344 for _, k := range repoHostPubKeys {
345 lines = append(lines, fmt.Sprintf("%s %s", repoHost, k))
346 }
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400347 values := map[string]any{
348 "image": map[string]any{
Giorgi Lekveishvilic06164d2023-11-22 13:51:29 +0400349 "repository": "fluxcd/flux-cli",
350 "tag": "v2.1.2",
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400351 "pullPolicy": "IfNotPresent",
352 },
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400353 "repositoryAddress": repoAddr,
354 "repositoryHost": repoHost,
355 "repositoryHostPublicKeys": strings.Join(lines, "\n"),
356 "privateKey": privateKey,
357 "installationNamespace": fmt.Sprintf("%s-flux", envName),
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400358 }
359 installer := action.NewInstall(config)
360 installer.Namespace = envName
361 installer.CreateNamespace = true
362 installer.ReleaseName = "flux"
363 installer.Wait = true
364 installer.WaitForJobs = true
365 installer.Timeout = 20 * time.Minute
366 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
367 return err
368 }
369 return nil
370}
371
372func (b Bootstrapper) installInfrastructureServices(repo RepoIO, nsGen NamespaceGenerator, nsCreator NamespaceCreator, env EnvConfig) error {
373 appRepo := NewInMemoryAppRepository(CreateAllApps())
374 install := func(name string) error {
375 app, err := appRepo.Find(name)
376 if err != nil {
377 return err
378 }
379 namespaces := make([]string, len(app.Namespaces))
380 for i, n := range app.Namespaces {
381 namespaces[i], err = nsGen.Generate(n)
382 if err != nil {
383 return err
384 }
385 }
386 for _, n := range namespaces {
387 if err := nsCreator.Create(n); err != nil {
388 return err
389 }
390 }
391 derived := Derived{
392 Global: Values{
393 PCloudEnvName: env.Name,
394 },
395 }
396 if len(namespaces) > 0 {
397 derived.Release.Namespace = namespaces[0]
398 }
399 values := map[string]any{
400 "IngressPublicIP": env.ServiceIPs.IngressPublic.String(),
401 }
402 return repo.InstallApp(*app, filepath.Join("/infrastructure", app.Name), values, derived)
403 }
404 appsToInstall := []string{
405 "resource-renderer-controller",
406 "headscale-controller",
407 "csi-driver-smb",
408 "ingress-public",
409 "cert-manager",
410 "cert-manager-webhook-gandi",
411 "cert-manager-webhook-gandi-role",
412 }
413 for _, name := range appsToInstall {
414 if err := install(name); err != nil {
415 return err
416 }
417 }
418 return nil
419}
420
421func configureMainRepo(repo RepoIO, env EnvConfig) error {
422 if err := repo.WriteYaml("config.yaml", env); err != nil {
423 return err
424 }
425 kust := NewKustomization()
426 kust.AddResources(
427 fmt.Sprintf("%s-flux", env.Name),
428 "infrastructure",
429 "environments",
430 )
431 if err := repo.WriteKustomization("kustomization.yaml", kust); err != nil {
432 return err
433 }
434 {
435 out, err := repo.Writer("infrastructure/pcloud-charts.yaml")
436 if err != nil {
437 return err
438 }
439 defer out.Close()
440 _, err = out.Write([]byte(fmt.Sprintf(`
441apiVersion: source.toolkit.fluxcd.io/v1
442kind: GitRepository
443metadata:
444 name: pcloud # TODO(giolekva): use more generic name
445 namespace: %s
446spec:
447 interval: 1m0s
448 url: https://github.com/giolekva/pcloud
449 ref:
450 branch: main
451`, env.Name)))
452 if err != nil {
453 return err
454 }
455 }
456 infraKust := NewKustomization()
457 infraKust.AddResources("pcloud-charts.yaml")
458 if err := repo.WriteKustomization("infrastructure/kustomization.yaml", infraKust); err != nil {
459 return err
460 }
461 if err := repo.WriteKustomization("environments/kustomization.yaml", NewKustomization()); err != nil {
462 return err
463 }
464 if err := repo.CommitAndPush("initialize pcloud directory structure"); err != nil {
465 return err
466 }
467 return nil
468}
469
470func (b Bootstrapper) installEnvManager(ss *soft.Client, repo RepoIO, nsGen NamespaceGenerator, nsCreator NamespaceCreator, env EnvConfig) error {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400471 keys, err := NewSSHKeyPair("env-manager")
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400472 if err != nil {
473 return err
474 }
475 user := fmt.Sprintf("%s-env-manager", env.Name)
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400476 if err := ss.AddUser(user, keys.AuthorizedKey()); err != nil {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400477 return err
478 }
479 if err := ss.MakeUserAdmin(user); err != nil {
480 return err
481 }
482 appRepo := NewInMemoryAppRepository(CreateAllApps())
483 app, err := appRepo.Find("env-manager")
484 if err != nil {
485 return err
486 }
487 namespaces := make([]string, len(app.Namespaces))
488 for i, n := range app.Namespaces {
489 namespaces[i], err = nsGen.Generate(n)
490 if err != nil {
491 return err
492 }
493 }
494 for _, n := range namespaces {
495 if err := nsCreator.Create(n); err != nil {
496 return err
497 }
498 }
499 derived := Derived{
500 Global: Values{
501 PCloudEnvName: env.Name,
502 },
503 Values: map[string]any{
504 "RepoIP": env.ServiceIPs.ConfigRepo,
505 "RepoPort": 22,
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400506 "RepoName": "config",
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400507 "SSHPrivateKey": string(keys.RawPrivateKey()),
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400508 },
509 }
510 if len(namespaces) > 0 {
511 derived.Release.Namespace = namespaces[0]
512 }
513 return repo.InstallApp(*app, filepath.Join("/infrastructure", app.Name), derived.Values, derived)
514}
515
Giorgi Lekveishvili106a9352023-12-04 11:20:11 +0400516func (b Bootstrapper) installDNSZoneManager(ss *soft.Client, repo RepoIO, nsGen NamespaceGenerator, nsCreator NamespaceCreator, env EnvConfig) error {
517 const (
518 volumeClaimName = "dns-zone-configs"
519 volumeMountPath = "/etc/pcloud/dns-zone-configs"
520 )
521 ns, err := nsGen.Generate("dns-zone-manager")
522 if err != nil {
523 return err
524 }
525 if err := nsCreator.Create(ns); err != nil {
526 return err
527 }
528 appRepo := NewInMemoryAppRepository(CreateAllApps())
529 {
530 app, err := appRepo.Find("dns-zone-manager")
531 if err != nil {
532 return err
533 }
534 derived := Derived{
535 Global: Values{
536 PCloudEnvName: env.Name,
537 },
538 Values: map[string]any{
539 "Volume": map[string]any{
540 "ClaimName": volumeClaimName,
541 "MountPath": volumeMountPath,
542 "Size": "1Gi",
543 },
544 },
545 Release: Release{
546 Namespace: ns,
547 },
548 }
549 if err := repo.InstallApp(*app, filepath.Join("/infrastructure", app.Name), derived.Values, derived); err != nil {
550 return err
551 }
552 }
553 return nil
554}
555
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400556type HelmActionConfigFactory interface {
557 New(namespace string) (*action.Configuration, error)
558}
559
560type ChartLoader interface {
561 Load(name string) (*chart.Chart, error)
562}
563
564type fsChartLoader struct {
565 baseDir string
566}
567
568func NewFSChartLoader(baseDir string) ChartLoader {
569 return &fsChartLoader{baseDir}
570}
571
572func (l *fsChartLoader) Load(name string) (*chart.Chart, error) {
573 return loader.Load(filepath.Join(l.baseDir, name))
574}