blob: 4ad44d757d3e521dff8c5115fa3384f820c4d4a3 [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 Lekveishvili724885f2023-11-29 16:18:42 +040069 repo, err := ss.GetRepo("config")
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040070 if err != nil {
71 return err
72 }
73 repoIO := NewRepoIO(repo, ss.Signer)
74 if err := configureMainRepo(repoIO, env); err != nil {
75 return err
76 }
77 nsGen := NewPrefixGenerator(env.NamespacePrefix)
78 if err := b.installInfrastructureServices(repoIO, nsGen, b.ns, env); err != nil {
79 return err
80 }
81 if err := b.installEnvManager(ss, repoIO, nsGen, b.ns, env); err != nil {
82 return err
83 }
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040084 return nil
85}
86
87func (b Bootstrapper) installMetallb(env EnvConfig) error {
88 if err := b.installMetallbNamespace(env); err != nil {
89 return err
90 }
91 if err := b.installMetallbService(); err != nil {
92 return err
93 }
94 if err := b.installMetallbIPAddressPool(IPAddressPoolLocal, true, env.ServiceIPs.From, env.ServiceIPs.To); err != nil {
95 return err
96 }
97 if err := b.installMetallbIPAddressPool(IPAddressPoolConfigRepo, false, env.ServiceIPs.ConfigRepo, env.ServiceIPs.ConfigRepo); err != nil {
98 return err
99 }
100 if err := b.installMetallbIPAddressPool(IPAddressPoolIngressPublic, false, env.ServiceIPs.IngressPublic, env.ServiceIPs.IngressPublic); err != nil {
101 return err
102 }
103 return nil
104}
105
106func (b Bootstrapper) installMetallbNamespace(env EnvConfig) error {
107 fmt.Println("Installing metallb namespace")
108 config, err := b.ha.New(env.Name)
109 if err != nil {
110 return err
111 }
112 chart, err := b.cl.Load("namespace")
113 if err != nil {
114 return err
115 }
116 values := map[string]any{
117 "namespace": "metallb-system",
118 "labels": []string{
119 "pod-security.kubernetes.io/audit: privileged",
120 "pod-security.kubernetes.io/enforce: privileged",
121 "pod-security.kubernetes.io/warn: privileged",
122 },
123 }
124 installer := action.NewInstall(config)
125 installer.Namespace = env.Name
126 installer.ReleaseName = "metallb-ns"
127 installer.Wait = true
128 installer.WaitForJobs = true
129 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
130 return err
131 }
132 return nil
133}
134
135func (b Bootstrapper) installMetallbService() error {
136 fmt.Println("Installing metallb")
137 config, err := b.ha.New("metallb-system")
138 if err != nil {
139 return err
140 }
141 chart, err := b.cl.Load("metallb")
142 if err != nil {
143 return err
144 }
145 values := map[string]any{ // TODO(giolekva): add loadBalancerClass?
146 "controller": map[string]any{
147 "image": map[string]any{
148 "repository": "quay.io/metallb/controller",
Giorgi Lekveishvilic06164d2023-11-22 13:51:29 +0400149 "tag": "v0.13.12",
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400150 "pullPolicy": "IfNotPresent",
151 },
152 "logLevel": "info",
153 },
154 "speaker": map[string]any{
155 "image": map[string]any{
156 "repository": "quay.io/metallb/speaker",
Giorgi Lekveishvilic06164d2023-11-22 13:51:29 +0400157 "tag": "v0.13.12",
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400158 "pullPolicy": "IfNotPresent",
159 },
160 "logLevel": "info",
161 },
162 }
163 installer := action.NewInstall(config)
164 installer.Namespace = "metallb-system"
165 installer.CreateNamespace = true
166 installer.ReleaseName = "metallb"
167 installer.IncludeCRDs = true
168 installer.Wait = true
169 installer.WaitForJobs = true
170 installer.Timeout = 20 * time.Minute
171 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
172 return err
173 }
174 return nil
175}
176
177func (b Bootstrapper) installMetallbIPAddressPool(name string, autoAssign bool, from, to netip.Addr) error {
178 fmt.Printf("Installing metallb-ipaddresspool: %s\n", name)
179 config, err := b.ha.New("metallb-system")
180 if err != nil {
181 return err
182 }
183 chart, err := b.cl.Load("metallb-ipaddresspool")
184 if err != nil {
185 return err
186 }
187 values := map[string]any{
188 "name": name,
189 "autoAssign": autoAssign,
190 "from": from.String(),
191 "to": to.String(),
192 }
193 installer := action.NewInstall(config)
194 installer.Namespace = "metallb-system"
195 installer.CreateNamespace = true
196 installer.ReleaseName = name
197 installer.Wait = true
198 installer.WaitForJobs = true
199 installer.Timeout = 20 * time.Minute
200 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
201 return err
202 }
203 return nil
204}
205
206func (b Bootstrapper) installLonghorn(envName string, storageDir string, volumeDefaultReplicaCount int) error {
207 fmt.Println("Installing Longhorn")
208 config, err := b.ha.New(envName)
209 if err != nil {
210 return err
211 }
212 chart, err := b.cl.Load("longhorn")
213 if err != nil {
214 return err
215 }
216 values := map[string]any{
217 "defaultSettings": map[string]any{
218 "defaultDataPath": storageDir,
219 },
220 "persistence": map[string]any{
221 "defaultClassReplicaCount": volumeDefaultReplicaCount,
222 },
223 "service": map[string]any{
224 "ui": map[string]any{
225 "type": "LoadBalancer",
226 },
227 },
228 "ingress": map[string]any{
229 "enabled": false,
230 },
231 }
232 installer := action.NewInstall(config)
233 installer.Namespace = "longhorn-system"
234 installer.CreateNamespace = true
235 installer.ReleaseName = "longhorn"
236 installer.Wait = true
237 installer.WaitForJobs = true
238 installer.Timeout = 20 * time.Minute
239 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
240 return err
241 }
242 return nil
243}
244
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400245func (b Bootstrapper) installSoftServe(adminPublicKey string, namespace string, repoIP netip.Addr) error {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400246 fmt.Println("Installing SoftServe")
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400247 keys, err := NewSSHKeyPair("soft-serve")
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400248 if err != nil {
249 return err
250 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400251 config, err := b.ha.New(namespace)
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400252 if err != nil {
253 return err
254 }
255 chart, err := b.cl.Load("soft-serve")
256 if err != nil {
257 return err
258 }
259 values := map[string]any{
260 "image": map[string]any{
261 "repository": "charmcli/soft-serve",
Giorgi Lekveishvilic06164d2023-11-22 13:51:29 +0400262 "tag": "v0.7.1",
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400263 "pullPolicy": "IfNotPresent",
264 },
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400265 "privateKey": string(keys.RawPrivateKey()),
266 "publicKey": string(keys.RawAuthorizedKey()),
267 "adminKey": adminPublicKey,
268 "reservedIP": repoIP.String(),
269 "serviceType": "LoadBalancer",
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400270 }
271 installer := action.NewInstall(config)
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400272 installer.Namespace = namespace
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400273 installer.CreateNamespace = true
274 installer.ReleaseName = "soft-serve"
275 installer.Wait = true
276 installer.WaitForJobs = true
277 installer.Timeout = 20 * time.Minute
278 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
279 return err
280 }
281 return nil
282}
283
284func (b Bootstrapper) installFluxcd(ss *soft.Client, envName string) error {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400285 keys, err := NewSSHKeyPair("fluxcd")
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400286 if err != nil {
287 return err
288 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400289 if err := ss.AddUser("flux", keys.AuthorizedKey()); err != nil {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400290 return err
291 }
292 if err := ss.MakeUserAdmin("flux"); err != nil {
293 return err
294 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400295 if err := ss.AddRepository("config"); err != nil {
296 return err
297 }
298 repo, err := ss.GetRepo("config")
299 if err != nil {
300 return err
301 }
302 repoIO := NewRepoIO(repo, ss.Signer)
303 if err := repoIO.WriteCommitAndPush("README.md", fmt.Sprintf("# %s systems", envName), "readme"); err != nil {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400304 return err
305 }
306 fmt.Println("Installing Flux")
307 ssPublic, err := ss.GetPublicKey()
308 if err != nil {
309 return err
310 }
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400311 host := strings.Split(ss.Addr, ":")[0]
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400312 if err := b.installFluxBootstrap(
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400313 ss.GetRepoAddress("config"),
314 host,
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400315 string(ssPublic),
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400316 string(keys.RawPrivateKey()),
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400317 envName,
318 ); err != nil {
319 return err
320 }
321 return nil
322}
323
324func (b Bootstrapper) installFluxBootstrap(repoAddr, repoHost, repoHostPubKey, privateKey, envName string) error {
325 config, err := b.ha.New(envName)
326 if err != nil {
327 return err
328 }
329 chart, err := b.cl.Load("flux-bootstrap")
330 if err != nil {
331 return err
332 }
333 values := map[string]any{
334 "image": map[string]any{
Giorgi Lekveishvilic06164d2023-11-22 13:51:29 +0400335 "repository": "fluxcd/flux-cli",
336 "tag": "v2.1.2",
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400337 "pullPolicy": "IfNotPresent",
338 },
339 "repositoryAddress": repoAddr,
340 "repositoryHost": repoHost,
341 "repositoryHostPublicKey": repoHostPubKey,
342 "privateKey": privateKey,
343 "installationNamespace": fmt.Sprintf("%s-flux", envName),
344 }
345 installer := action.NewInstall(config)
346 installer.Namespace = envName
347 installer.CreateNamespace = true
348 installer.ReleaseName = "flux"
349 installer.Wait = true
350 installer.WaitForJobs = true
351 installer.Timeout = 20 * time.Minute
352 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
353 return err
354 }
355 return nil
356}
357
358func (b Bootstrapper) installInfrastructureServices(repo RepoIO, nsGen NamespaceGenerator, nsCreator NamespaceCreator, env EnvConfig) error {
359 appRepo := NewInMemoryAppRepository(CreateAllApps())
360 install := func(name string) error {
361 app, err := appRepo.Find(name)
362 if err != nil {
363 return err
364 }
365 namespaces := make([]string, len(app.Namespaces))
366 for i, n := range app.Namespaces {
367 namespaces[i], err = nsGen.Generate(n)
368 if err != nil {
369 return err
370 }
371 }
372 for _, n := range namespaces {
373 if err := nsCreator.Create(n); err != nil {
374 return err
375 }
376 }
377 derived := Derived{
378 Global: Values{
379 PCloudEnvName: env.Name,
380 },
381 }
382 if len(namespaces) > 0 {
383 derived.Release.Namespace = namespaces[0]
384 }
385 values := map[string]any{
386 "IngressPublicIP": env.ServiceIPs.IngressPublic.String(),
387 }
388 return repo.InstallApp(*app, filepath.Join("/infrastructure", app.Name), values, derived)
389 }
390 appsToInstall := []string{
391 "resource-renderer-controller",
392 "headscale-controller",
393 "csi-driver-smb",
394 "ingress-public",
395 "cert-manager",
396 "cert-manager-webhook-gandi",
397 "cert-manager-webhook-gandi-role",
398 }
399 for _, name := range appsToInstall {
400 if err := install(name); err != nil {
401 return err
402 }
403 }
404 return nil
405}
406
407func configureMainRepo(repo RepoIO, env EnvConfig) error {
408 if err := repo.WriteYaml("config.yaml", env); err != nil {
409 return err
410 }
411 kust := NewKustomization()
412 kust.AddResources(
413 fmt.Sprintf("%s-flux", env.Name),
414 "infrastructure",
415 "environments",
416 )
417 if err := repo.WriteKustomization("kustomization.yaml", kust); err != nil {
418 return err
419 }
420 {
421 out, err := repo.Writer("infrastructure/pcloud-charts.yaml")
422 if err != nil {
423 return err
424 }
425 defer out.Close()
426 _, err = out.Write([]byte(fmt.Sprintf(`
427apiVersion: source.toolkit.fluxcd.io/v1
428kind: GitRepository
429metadata:
430 name: pcloud # TODO(giolekva): use more generic name
431 namespace: %s
432spec:
433 interval: 1m0s
434 url: https://github.com/giolekva/pcloud
435 ref:
436 branch: main
437`, env.Name)))
438 if err != nil {
439 return err
440 }
441 }
442 infraKust := NewKustomization()
443 infraKust.AddResources("pcloud-charts.yaml")
444 if err := repo.WriteKustomization("infrastructure/kustomization.yaml", infraKust); err != nil {
445 return err
446 }
447 if err := repo.WriteKustomization("environments/kustomization.yaml", NewKustomization()); err != nil {
448 return err
449 }
450 if err := repo.CommitAndPush("initialize pcloud directory structure"); err != nil {
451 return err
452 }
453 return nil
454}
455
456func (b Bootstrapper) installEnvManager(ss *soft.Client, repo RepoIO, nsGen NamespaceGenerator, nsCreator NamespaceCreator, env EnvConfig) error {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400457 keys, err := NewSSHKeyPair("env-manager")
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400458 if err != nil {
459 return err
460 }
461 user := fmt.Sprintf("%s-env-manager", env.Name)
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400462 if err := ss.AddUser(user, keys.AuthorizedKey()); err != nil {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400463 return err
464 }
465 if err := ss.MakeUserAdmin(user); err != nil {
466 return err
467 }
468 appRepo := NewInMemoryAppRepository(CreateAllApps())
469 app, err := appRepo.Find("env-manager")
470 if err != nil {
471 return err
472 }
473 namespaces := make([]string, len(app.Namespaces))
474 for i, n := range app.Namespaces {
475 namespaces[i], err = nsGen.Generate(n)
476 if err != nil {
477 return err
478 }
479 }
480 for _, n := range namespaces {
481 if err := nsCreator.Create(n); err != nil {
482 return err
483 }
484 }
485 derived := Derived{
486 Global: Values{
487 PCloudEnvName: env.Name,
488 },
489 Values: map[string]any{
490 "RepoIP": env.ServiceIPs.ConfigRepo,
491 "RepoPort": 22,
Giorgi Lekveishvili724885f2023-11-29 16:18:42 +0400492 "RepoName": "config",
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400493 "SSHPrivateKey": string(keys.RawPrivateKey()),
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400494 },
495 }
496 if len(namespaces) > 0 {
497 derived.Release.Namespace = namespaces[0]
498 }
499 return repo.InstallApp(*app, filepath.Join("/infrastructure", app.Name), derived.Values, derived)
500}
501
502type HelmActionConfigFactory interface {
503 New(namespace string) (*action.Configuration, error)
504}
505
506type ChartLoader interface {
507 Load(name string) (*chart.Chart, error)
508}
509
510type fsChartLoader struct {
511 baseDir string
512}
513
514func NewFSChartLoader(baseDir string) ChartLoader {
515 return &fsChartLoader{baseDir}
516}
517
518func (l *fsChartLoader) Load(name string) (*chart.Chart, error) {
519 return loader.Load(filepath.Join(l.baseDir, name))
520}