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