blob: 7e3cb8a6a2ed6f10f524aa34bb49b254b3ea76a2 [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"
10 "time"
11
12 "github.com/cenkalti/backoff/v4"
13 "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 }
44 time.Sleep(1 * time.Minute) // TODO(giolekva): implement proper wait
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040045 bootstrapJobKeys, err := NewSSHKeyPair("bootstrapper")
46 if err != nil {
47 return err
48 }
49 if err := b.installSoftServe(bootstrapJobKeys.AuthorizedKey(), env.Name, env.ServiceIPs.ConfigRepo); err != nil {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040050 return err
51 }
52 var ss *soft.Client
53 err = backoff.Retry(func() error {
54 var err error
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040055 ss, err = soft.NewClient(netip.AddrPortFrom(env.ServiceIPs.ConfigRepo, 22), bootstrapJobKeys.RawPrivateKey(), log.Default())
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040056 return err
57 }, backoff.NewConstantBackOff(5*time.Second))
58 if err != nil {
59 return err
60 }
61 if ss.AddPublicKey("admin", string(env.AdminPublicKey)); err != nil {
62 return err
63 }
64 if err := b.installFluxcd(ss, env.Name); err != nil {
65 return err
66 }
67 repo, err := ss.GetRepo(env.Name)
68 if err != nil {
69 return err
70 }
71 repoIO := NewRepoIO(repo, ss.Signer)
72 if err := configureMainRepo(repoIO, env); err != nil {
73 return err
74 }
75 nsGen := NewPrefixGenerator(env.NamespacePrefix)
76 if err := b.installInfrastructureServices(repoIO, nsGen, b.ns, env); err != nil {
77 return err
78 }
79 if err := b.installEnvManager(ss, repoIO, nsGen, b.ns, env); err != nil {
80 return err
81 }
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +040082 if ss.RemovePublicKey("admin", bootstrapJobKeys.AuthorizedKey()); err != nil {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +040083 return err
84 }
85 return nil
86}
87
88func (b Bootstrapper) installMetallb(env EnvConfig) error {
89 if err := b.installMetallbNamespace(env); err != nil {
90 return err
91 }
92 if err := b.installMetallbService(); err != nil {
93 return err
94 }
95 if err := b.installMetallbIPAddressPool(IPAddressPoolLocal, true, env.ServiceIPs.From, env.ServiceIPs.To); err != nil {
96 return err
97 }
98 if err := b.installMetallbIPAddressPool(IPAddressPoolConfigRepo, false, env.ServiceIPs.ConfigRepo, env.ServiceIPs.ConfigRepo); err != nil {
99 return err
100 }
101 if err := b.installMetallbIPAddressPool(IPAddressPoolIngressPublic, false, env.ServiceIPs.IngressPublic, env.ServiceIPs.IngressPublic); err != nil {
102 return err
103 }
104 return nil
105}
106
107func (b Bootstrapper) installMetallbNamespace(env EnvConfig) error {
108 fmt.Println("Installing metallb namespace")
109 config, err := b.ha.New(env.Name)
110 if err != nil {
111 return err
112 }
113 chart, err := b.cl.Load("namespace")
114 if err != nil {
115 return err
116 }
117 values := map[string]any{
118 "namespace": "metallb-system",
119 "labels": []string{
120 "pod-security.kubernetes.io/audit: privileged",
121 "pod-security.kubernetes.io/enforce: privileged",
122 "pod-security.kubernetes.io/warn: privileged",
123 },
124 }
125 installer := action.NewInstall(config)
126 installer.Namespace = env.Name
127 installer.ReleaseName = "metallb-ns"
128 installer.Wait = true
129 installer.WaitForJobs = true
130 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
131 return err
132 }
133 return nil
134}
135
136func (b Bootstrapper) installMetallbService() error {
137 fmt.Println("Installing metallb")
138 config, err := b.ha.New("metallb-system")
139 if err != nil {
140 return err
141 }
142 chart, err := b.cl.Load("metallb")
143 if err != nil {
144 return err
145 }
146 values := map[string]any{ // TODO(giolekva): add loadBalancerClass?
147 "controller": map[string]any{
148 "image": map[string]any{
149 "repository": "quay.io/metallb/controller",
150 "tag": "v0.13.9",
151 "pullPolicy": "IfNotPresent",
152 },
153 "logLevel": "info",
154 },
155 "speaker": map[string]any{
156 "image": map[string]any{
157 "repository": "quay.io/metallb/speaker",
158 "tag": "v0.13.9",
159 "pullPolicy": "IfNotPresent",
160 },
161 "logLevel": "info",
162 },
163 }
164 installer := action.NewInstall(config)
165 installer.Namespace = "metallb-system"
166 installer.CreateNamespace = true
167 installer.ReleaseName = "metallb"
168 installer.IncludeCRDs = true
169 installer.Wait = true
170 installer.WaitForJobs = true
171 installer.Timeout = 20 * time.Minute
172 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
173 return err
174 }
175 return nil
176}
177
178func (b Bootstrapper) installMetallbIPAddressPool(name string, autoAssign bool, from, to netip.Addr) error {
179 fmt.Printf("Installing metallb-ipaddresspool: %s\n", name)
180 config, err := b.ha.New("metallb-system")
181 if err != nil {
182 return err
183 }
184 chart, err := b.cl.Load("metallb-ipaddresspool")
185 if err != nil {
186 return err
187 }
188 values := map[string]any{
189 "name": name,
190 "autoAssign": autoAssign,
191 "from": from.String(),
192 "to": to.String(),
193 }
194 installer := action.NewInstall(config)
195 installer.Namespace = "metallb-system"
196 installer.CreateNamespace = true
197 installer.ReleaseName = name
198 installer.Wait = true
199 installer.WaitForJobs = true
200 installer.Timeout = 20 * time.Minute
201 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
202 return err
203 }
204 return nil
205}
206
207func (b Bootstrapper) installLonghorn(envName string, storageDir string, volumeDefaultReplicaCount int) error {
208 fmt.Println("Installing Longhorn")
209 config, err := b.ha.New(envName)
210 if err != nil {
211 return err
212 }
213 chart, err := b.cl.Load("longhorn")
214 if err != nil {
215 return err
216 }
217 values := map[string]any{
218 "defaultSettings": map[string]any{
219 "defaultDataPath": storageDir,
220 },
221 "persistence": map[string]any{
222 "defaultClassReplicaCount": volumeDefaultReplicaCount,
223 },
224 "service": map[string]any{
225 "ui": map[string]any{
226 "type": "LoadBalancer",
227 },
228 },
229 "ingress": map[string]any{
230 "enabled": false,
231 },
232 }
233 installer := action.NewInstall(config)
234 installer.Namespace = "longhorn-system"
235 installer.CreateNamespace = true
236 installer.ReleaseName = "longhorn"
237 installer.Wait = true
238 installer.WaitForJobs = true
239 installer.Timeout = 20 * time.Minute
240 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
241 return err
242 }
243 return nil
244}
245
246func (b Bootstrapper) installSoftServe(adminPublicKey string, envName string, repoIP netip.Addr) error {
247 fmt.Println("Installing SoftServe")
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400248 keys, err := NewSSHKeyPair("soft-serve")
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400249 if err != nil {
250 return err
251 }
252 config, err := b.ha.New(envName)
253 if err != nil {
254 return err
255 }
256 chart, err := b.cl.Load("soft-serve")
257 if err != nil {
258 return err
259 }
260 values := map[string]any{
261 "image": map[string]any{
262 "repository": "charmcli/soft-serve",
263 "tag": "v0.5.4",
264 "pullPolicy": "IfNotPresent",
265 },
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400266 "privateKey": string(keys.RawPrivateKey()),
267 "publicKey": string(keys.RawAuthorizedKey()),
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400268 "adminKey": adminPublicKey,
269 "reservedIP": repoIP.String(),
270 }
271 installer := action.NewInstall(config)
272 installer.Namespace = envName
273 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 }
295 fmt.Printf("Creating /%s repo", envName)
296 if err := ss.AddRepository(envName, "# dodo Systems"); err != nil {
297 return err
298 }
299 fmt.Println("Installing Flux")
300 ssPublic, err := ss.GetPublicKey()
301 if err != nil {
302 return err
303 }
304 if err := b.installFluxBootstrap(
305 ss.GetRepoAddress(envName),
306 ss.Addr.Addr().String(),
307 string(ssPublic),
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400308 string(keys.RawPrivateKey()),
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400309 envName,
310 ); err != nil {
311 return err
312 }
313 return nil
314}
315
316func (b Bootstrapper) installFluxBootstrap(repoAddr, repoHost, repoHostPubKey, privateKey, envName string) error {
317 config, err := b.ha.New(envName)
318 if err != nil {
319 return err
320 }
321 chart, err := b.cl.Load("flux-bootstrap")
322 if err != nil {
323 return err
324 }
325 values := map[string]any{
326 "image": map[string]any{
327 "repository": "fluxcd/flux-cli", // "giolekva/flux",
328 "tag": "v2.0.0",
329 "pullPolicy": "IfNotPresent",
330 },
331 "repositoryAddress": repoAddr,
332 "repositoryHost": repoHost,
333 "repositoryHostPublicKey": repoHostPubKey,
334 "privateKey": privateKey,
335 "installationNamespace": fmt.Sprintf("%s-flux", envName),
336 }
337 installer := action.NewInstall(config)
338 installer.Namespace = envName
339 installer.CreateNamespace = true
340 installer.ReleaseName = "flux"
341 installer.Wait = true
342 installer.WaitForJobs = true
343 installer.Timeout = 20 * time.Minute
344 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
345 return err
346 }
347 return nil
348}
349
350func (b Bootstrapper) installInfrastructureServices(repo RepoIO, nsGen NamespaceGenerator, nsCreator NamespaceCreator, env EnvConfig) error {
351 appRepo := NewInMemoryAppRepository(CreateAllApps())
352 install := func(name string) error {
353 app, err := appRepo.Find(name)
354 if err != nil {
355 return err
356 }
357 namespaces := make([]string, len(app.Namespaces))
358 for i, n := range app.Namespaces {
359 namespaces[i], err = nsGen.Generate(n)
360 if err != nil {
361 return err
362 }
363 }
364 for _, n := range namespaces {
365 if err := nsCreator.Create(n); err != nil {
366 return err
367 }
368 }
369 derived := Derived{
370 Global: Values{
371 PCloudEnvName: env.Name,
372 },
373 }
374 if len(namespaces) > 0 {
375 derived.Release.Namespace = namespaces[0]
376 }
377 values := map[string]any{
378 "IngressPublicIP": env.ServiceIPs.IngressPublic.String(),
379 }
380 return repo.InstallApp(*app, filepath.Join("/infrastructure", app.Name), values, derived)
381 }
382 appsToInstall := []string{
383 "resource-renderer-controller",
384 "headscale-controller",
385 "csi-driver-smb",
386 "ingress-public",
387 "cert-manager",
388 "cert-manager-webhook-gandi",
389 "cert-manager-webhook-gandi-role",
390 }
391 for _, name := range appsToInstall {
392 if err := install(name); err != nil {
393 return err
394 }
395 }
396 return nil
397}
398
399func configureMainRepo(repo RepoIO, env EnvConfig) error {
400 if err := repo.WriteYaml("config.yaml", env); err != nil {
401 return err
402 }
403 kust := NewKustomization()
404 kust.AddResources(
405 fmt.Sprintf("%s-flux", env.Name),
406 "infrastructure",
407 "environments",
408 )
409 if err := repo.WriteKustomization("kustomization.yaml", kust); err != nil {
410 return err
411 }
412 {
413 out, err := repo.Writer("infrastructure/pcloud-charts.yaml")
414 if err != nil {
415 return err
416 }
417 defer out.Close()
418 _, err = out.Write([]byte(fmt.Sprintf(`
419apiVersion: source.toolkit.fluxcd.io/v1
420kind: GitRepository
421metadata:
422 name: pcloud # TODO(giolekva): use more generic name
423 namespace: %s
424spec:
425 interval: 1m0s
426 url: https://github.com/giolekva/pcloud
427 ref:
428 branch: main
429`, env.Name)))
430 if err != nil {
431 return err
432 }
433 }
434 infraKust := NewKustomization()
435 infraKust.AddResources("pcloud-charts.yaml")
436 if err := repo.WriteKustomization("infrastructure/kustomization.yaml", infraKust); err != nil {
437 return err
438 }
439 if err := repo.WriteKustomization("environments/kustomization.yaml", NewKustomization()); err != nil {
440 return err
441 }
442 if err := repo.CommitAndPush("initialize pcloud directory structure"); err != nil {
443 return err
444 }
445 return nil
446}
447
448func (b Bootstrapper) installEnvManager(ss *soft.Client, repo RepoIO, nsGen NamespaceGenerator, nsCreator NamespaceCreator, env EnvConfig) error {
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400449 keys, err := NewSSHKeyPair("env-manager")
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400450 if err != nil {
451 return err
452 }
453 user := fmt.Sprintf("%s-env-manager", env.Name)
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400454 if err := ss.AddUser(user, keys.AuthorizedKey()); err != nil {
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400455 return err
456 }
457 if err := ss.MakeUserAdmin(user); err != nil {
458 return err
459 }
460 appRepo := NewInMemoryAppRepository(CreateAllApps())
461 app, err := appRepo.Find("env-manager")
462 if err != nil {
463 return err
464 }
465 namespaces := make([]string, len(app.Namespaces))
466 for i, n := range app.Namespaces {
467 namespaces[i], err = nsGen.Generate(n)
468 if err != nil {
469 return err
470 }
471 }
472 for _, n := range namespaces {
473 if err := nsCreator.Create(n); err != nil {
474 return err
475 }
476 }
477 derived := Derived{
478 Global: Values{
479 PCloudEnvName: env.Name,
480 },
481 Values: map[string]any{
482 "RepoIP": env.ServiceIPs.ConfigRepo,
483 "RepoPort": 22,
484 "RepoName": env.Name,
Giorgi Lekveishvilia1e77902023-11-06 14:48:27 +0400485 "SSHPrivateKey": string(keys.RawPrivateKey()),
Giorgi Lekveishvili94cda9d2023-07-20 10:16:09 +0400486 },
487 }
488 if len(namespaces) > 0 {
489 derived.Release.Namespace = namespaces[0]
490 }
491 return repo.InstallApp(*app, filepath.Join("/infrastructure", app.Name), derived.Values, derived)
492}
493
494type HelmActionConfigFactory interface {
495 New(namespace string) (*action.Configuration, error)
496}
497
498type ChartLoader interface {
499 Load(name string) (*chart.Chart, error)
500}
501
502type fsChartLoader struct {
503 baseDir string
504}
505
506func NewFSChartLoader(baseDir string) ChartLoader {
507 return &fsChartLoader{baseDir}
508}
509
510func (l *fsChartLoader) Load(name string) (*chart.Chart, error) {
511 return loader.Load(filepath.Join(l.baseDir, name))
512}