blob: 135b6b480b83fbb638f0a3149946680a049977a1 [file] [log] [blame]
giolekva8aa73e82022-07-09 11:34:39 +04001package main
2
3import (
4 "context"
giolekva8aa73e82022-07-09 11:34:39 +04005 _ "embed"
giolekva8aa73e82022-07-09 11:34:39 +04006 "fmt"
giolekva8aa73e82022-07-09 11:34:39 +04007 "log"
8 "os"
9 "path/filepath"
10 "time"
11
giolekva8aa73e82022-07-09 11:34:39 +040012 "github.com/spf13/cobra"
13 "helm.sh/helm/v3/pkg/action"
14 "helm.sh/helm/v3/pkg/chart/loader"
15 "helm.sh/helm/v3/pkg/kube"
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040016
17 "github.com/giolekva/pcloud/core/installer"
18 "github.com/giolekva/pcloud/core/installer/soft"
giolekva8aa73e82022-07-09 11:34:39 +040019)
20
21var bootstrapFlags struct {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040022 pcloudEnvName string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040023 chartsDir string
24 adminPubKey string
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040025 storageDir string
26 volumeDefaultReplicaCount int
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040027 softServeIP string // TODO(giolekva): reserve using metallb IPAddressPool
giolekva8aa73e82022-07-09 11:34:39 +040028}
29
30func bootstrapCmd() *cobra.Command {
31 cmd := &cobra.Command{
32 Use: "bootstrap",
33 RunE: bootstrapCmdRun,
34 }
35 cmd.Flags().StringVar(
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040036 &bootstrapFlags.pcloudEnvName,
37 "pcloud-env-name",
38 "pcloud",
39 "",
40 )
41 cmd.Flags().StringVar(
giolekva8aa73e82022-07-09 11:34:39 +040042 &bootstrapFlags.chartsDir,
43 "charts-dir",
44 "",
45 "",
46 )
47 cmd.Flags().StringVar(
48 &bootstrapFlags.adminPubKey,
49 "admin-pub-key",
50 "",
51 "",
52 )
53 cmd.Flags().StringVar(
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +040054 &bootstrapFlags.storageDir,
55 "storage-dir",
56 "",
57 "",
58 )
59 cmd.Flags().IntVar(
60 &bootstrapFlags.volumeDefaultReplicaCount,
61 "volume-default-replica-count",
62 3,
63 "",
64 )
65 cmd.Flags().StringVar(
66 &bootstrapFlags.softServeIP,
67 "soft-serve-ip",
68 "",
69 "",
70 )
giolekva8aa73e82022-07-09 11:34:39 +040071 return cmd
72}
73
74func bootstrapCmdRun(cmd *cobra.Command, args []string) error {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040075 adminPubKey, err := os.ReadFile(bootstrapFlags.adminPubKey)
Giorgi Lekveishvili677b4572023-05-26 15:02:37 +040076 if err != nil {
77 return err
78 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040079 bootstrapJobKeys, err := installer.NewSSHKeyPair()
Giorgi Lekveishvili677b4572023-05-26 15:02:37 +040080 if err != nil {
81 return err
82 }
Giorgi Lekveishvili677b4572023-05-26 15:02:37 +040083 if err := installMetallb(); err != nil {
84 return err
85 }
Giorgi Lekveishvili677b4572023-05-26 15:02:37 +040086 if err := installLonghorn(); err != nil {
87 return err
88 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040089 time.Sleep(5 * time.Minute) // TODO(giolekva): implement proper wait
90 if err := installSoftServe(bootstrapJobKeys.Public); err != nil {
Giorgi Lekveishvili677b4572023-05-26 15:02:37 +040091 return err
92 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040093 time.Sleep(2 * time.Minute) // TODO(giolekva): implement proper wait
94 ss, err := soft.NewClient(bootstrapFlags.softServeIP, 22, []byte(bootstrapJobKeys.Private), log.Default())
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +040095 if err != nil {
96 return err
97 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +040098 if ss.AddPublicKey("admin", string(adminPubKey)); err != nil {
99 return err
100 }
101 if err := installFluxcd(ss, bootstrapFlags.pcloudEnvName); err != nil {
102 return err
103 }
104 repo, err := ss.GetRepo(bootstrapFlags.pcloudEnvName)
Giorgi Lekveishvili677b4572023-05-26 15:02:37 +0400105 if err != nil {
106 return err
107 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400108 repoIO := installer.NewRepoIO(repo, ss.Signer)
109 if err := configurePCloudRepo(repoIO); err != nil {
Giorgi Lekveishvili677b4572023-05-26 15:02:37 +0400110 return err
111 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400112 // TODO(giolekva): commit this to the repo above
113 global := map[string]any{
114 "PCloudEnvName": bootstrapFlags.pcloudEnvName,
115 }
116 if err := installInfrastructureServices(repoIO, global); err != nil {
Giorgi Lekveishvili677b4572023-05-26 15:02:37 +0400117 return err
118 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400119 if err := installEnvManager(ss, repoIO, global); err != nil {
Giorgi Lekveishvili677b4572023-05-26 15:02:37 +0400120 return err
121 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400122 if ss.RemovePublicKey("admin", bootstrapJobKeys.Public); err != nil {
Giorgi Lekveishvili677b4572023-05-26 15:02:37 +0400123 return err
124 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400125
126 return nil
127}
128
129func installMetallb() error {
130 if err := installMetallbNamespace(); err != nil {
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400131 return err
132 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400133 if err := installMetallbService(); err != nil {
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400134 return err
135 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400136 if err := installMetallbConfig(); err != nil {
Giorgi Lekveishvilid6e80cc2023-06-09 17:38:49 +0400137 return err
138 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400139 return nil
140}
141
142func installMetallbNamespace() error {
143 fmt.Println("Installing metallb namespace")
144 // config, err := createActionConfig("default")
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400145 config, err := createActionConfig(bootstrapFlags.pcloudEnvName)
giolekva8aa73e82022-07-09 11:34:39 +0400146 if err != nil {
147 return err
148 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400149 chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "namespace"))
giolekva8aa73e82022-07-09 11:34:39 +0400150 if err != nil {
151 return err
152 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400153 values := map[string]interface{}{
154 // "namespace": "pcloud-metallb",
155 "namespace": "metallb-system",
156 "labels": []string{
157 "pod-security.kubernetes.io/audit: privileged",
158 "pod-security.kubernetes.io/enforce: privileged",
159 "pod-security.kubernetes.io/warn: privileged",
160 },
161 }
162 installer := action.NewInstall(config)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400163 installer.Namespace = bootstrapFlags.pcloudEnvName
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400164 installer.ReleaseName = "metallb-ns"
165 installer.Wait = true
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400166 installer.WaitForJobs = true
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400167 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
168 return err
169 }
170 return nil
171}
172
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400173func installMetallbService() error {
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400174 fmt.Println("Installing metallb")
175 // config, err := createActionConfig("default")
176 config, err := createActionConfig("metallb-system")
giolekva8aa73e82022-07-09 11:34:39 +0400177 if err != nil {
178 return err
179 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400180 chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "metallb"))
giolekva8aa73e82022-07-09 11:34:39 +0400181 if err != nil {
182 return err
183 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400184 values := map[string]interface{}{ // TODO(giolekva): add loadBalancerClass?
185 "controller": map[string]interface{}{
186 "image": map[string]interface{}{
187 "repository": "quay.io/metallb/controller",
188 "tag": "v0.13.9",
189 "pullPolicy": "IfNotPresent",
190 },
191 "logLevel": "info",
192 },
193 "speaker": map[string]interface{}{
194 "image": map[string]interface{}{
195 "repository": "quay.io/metallb/speaker",
196 "tag": "v0.13.9",
197 "pullPolicy": "IfNotPresent",
198 },
199 "logLevel": "info",
200 },
201 }
202 installer := action.NewInstall(config)
203 installer.Namespace = "metallb-system" // "pcloud-metallb"
204 installer.CreateNamespace = true
205 installer.ReleaseName = "metallb"
206 installer.IncludeCRDs = true
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400207 installer.Wait = true
208 installer.WaitForJobs = true
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400209 installer.Timeout = 20 * time.Minute
210 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
giolekva8aa73e82022-07-09 11:34:39 +0400211 return err
212 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400213 return nil
214}
215
216func installMetallbConfig() error {
217 fmt.Println("Installing metallb-config")
218 // config, err := createActionConfig("default")
219 config, err := createActionConfig("metallb-system")
220 if err != nil {
giolekva8aa73e82022-07-09 11:34:39 +0400221 return err
222 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400223 chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "metallb-config"))
224 if err != nil {
giolekva8aa73e82022-07-09 11:34:39 +0400225 return err
226 }
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400227 values := map[string]interface{}{
228 "from": "192.168.0.210",
229 "to": "192.168.0.240",
230 }
231 installer := action.NewInstall(config)
232 installer.Namespace = "metallb-system" // "pcloud-metallb"
233 installer.CreateNamespace = true
234 installer.ReleaseName = "metallb-cfg"
235 installer.Wait = true
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400236 installer.WaitForJobs = true
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400237 installer.Timeout = 20 * time.Minute
238 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
239 return err
240 }
241 return nil
242}
243
244func installLonghorn() error {
245 fmt.Println("Installing Longhorn")
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400246 config, err := createActionConfig(bootstrapFlags.pcloudEnvName)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400247 if err != nil {
248 return err
249 }
250 chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "longhorn"))
251 if err != nil {
252 return err
253 }
254 values := map[string]interface{}{
255 "defaultSettings": map[string]interface{}{
256 "defaultDataPath": bootstrapFlags.storageDir,
257 },
258 "persistence": map[string]interface{}{
259 "defaultClassReplicaCount": bootstrapFlags.volumeDefaultReplicaCount,
260 },
261 "service": map[string]interface{}{
262 "ui": map[string]interface{}{
263 "type": "LoadBalancer",
264 },
265 },
266 "ingress": map[string]interface{}{
267 "enabled": false,
268 },
269 }
270 installer := action.NewInstall(config)
271 installer.Namespace = "longhorn-system"
272 installer.CreateNamespace = true
273 installer.ReleaseName = "longhorn"
274 installer.Wait = true
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400275 installer.WaitForJobs = true
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400276 installer.Timeout = 20 * time.Minute
277 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
giolekva8aa73e82022-07-09 11:34:39 +0400278 return err
279 }
280 return nil
281}
282
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400283func installSoftServe(adminPublicKey string) error {
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400284 fmt.Println("Installing SoftServe")
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400285 keys, err := installer.NewSSHKeyPair()
286 if err != nil {
287 return err
288 }
289 config, err := createActionConfig(bootstrapFlags.pcloudEnvName)
giolekva8aa73e82022-07-09 11:34:39 +0400290 if err != nil {
291 return err
292 }
293 chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "soft-serve"))
294 if err != nil {
295 return err
296 }
297 values := map[string]interface{}{
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400298 "privateKey": keys.Private,
299 "publicKey": keys.Public,
300 "adminKey": adminPublicKey,
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400301 "reservedIP": bootstrapFlags.softServeIP,
giolekva8aa73e82022-07-09 11:34:39 +0400302 }
303 installer := action.NewInstall(config)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400304 installer.Namespace = bootstrapFlags.pcloudEnvName
giolekva8aa73e82022-07-09 11:34:39 +0400305 installer.CreateNamespace = true
306 installer.ReleaseName = "soft-serve"
307 installer.Wait = true
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400308 installer.WaitForJobs = true
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400309 installer.Timeout = 20 * time.Minute
giolekva8aa73e82022-07-09 11:34:39 +0400310 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
311 return err
312 }
313 return nil
314}
315
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400316func installFluxcd(ss *soft.Client, pcloudEnvName string) error {
317 keys, err := installer.NewSSHKeyPair()
318 if err != nil {
319 return err
320 }
321 if err := ss.AddUser("flux", keys.Public); err != nil {
322 return err
323 }
324 if err := ss.MakeUserAdmin("flux"); err != nil {
325 return err
326 }
327 fmt.Printf("Creating /%s repo", pcloudEnvName)
328 if err := ss.AddRepository(pcloudEnvName, "# PCloud Systems"); err != nil {
329 return err
330 }
331 fmt.Println("Installing Flux")
332 ssPublic, err := ss.GetPublicKey()
333 if err != nil {
334 return err
335 }
336 if err := installFluxBootstrap(
337 ss.GetRepoAddress(pcloudEnvName),
338 ss.IP,
339 string(ssPublic),
340 keys.Private,
341 ); err != nil {
342 return err
343 }
344 return nil
345}
346
347func installFluxBootstrap(repoAddr, repoHost, repoHostPubKey, privateKey string) error {
348 config, err := createActionConfig(bootstrapFlags.pcloudEnvName)
giolekva8aa73e82022-07-09 11:34:39 +0400349 if err != nil {
350 return err
351 }
352 chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "flux-bootstrap"))
353 if err != nil {
354 return err
355 }
356 values := map[string]interface{}{
357 "repositoryAddress": repoAddr,
358 "repositoryHost": repoHost,
359 "repositoryHostPublicKey": repoHostPubKey,
360 "privateKey": privateKey,
361 }
362 installer := action.NewInstall(config)
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400363 installer.Namespace = bootstrapFlags.pcloudEnvName
giolekva8aa73e82022-07-09 11:34:39 +0400364 installer.CreateNamespace = true
365 installer.ReleaseName = "flux"
366 installer.Wait = true
367 installer.WaitForJobs = true
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400368 installer.Timeout = 20 * time.Minute
giolekva8aa73e82022-07-09 11:34:39 +0400369 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
370 return err
371 }
372 return nil
373}
374
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400375func installInfrastructureServices(repo installer.RepoIO, global map[string]any) error {
376 values := map[string]any{
377 "Global": global,
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400378 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400379 appRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
380 install := func(name string) error {
381 app, err := appRepo.Find(name)
382 if err != nil {
383 return err
384 }
385 return repo.InstallApp(*app, "infrastructure", values)
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400386 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400387 appsToInstall := []string{
388 "resource-renderer-controller",
389 "headscale-controller",
390 "csi-driver-smb",
391 "ingress-public",
392 "cert-manager",
393 "cert-manager-webhook-gandi",
394 "cert-manager-webhook-gandi-role",
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400395 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400396 for _, name := range appsToInstall {
397 if err := install(name); err != nil {
398 return err
399 }
Giorgi Lekveishvilid6e80cc2023-06-09 17:38:49 +0400400 }
401 return nil
402}
403
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400404func configurePCloudRepo(repo installer.RepoIO) error {
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400405 {
406 kust := installer.NewKustomization()
407 kust.AddResources("pcloud-flux", "infrastructure", "environments")
408 if err := repo.WriteKustomization("kustomization.yaml", kust); err != nil {
409 return err
410 }
411 {
412 out, err := repo.Writer("infrastructure/pcloud-charts.yaml")
413 if err != nil {
414 return err
415 }
416 defer out.Close()
417 _, err = out.Write([]byte(`
418apiVersion: source.toolkit.fluxcd.io/v1beta2
419kind: GitRepository
420metadata:
421 name: pcloud # TODO(giolekva): use more generic name
422 namespace: pcloud # TODO(giolekva): configurable
423spec:
424 interval: 1m0s
425 url: https://github.com/giolekva/pcloud
426 ref:
427 branch: main
428`))
429 if err != nil {
430 return err
431 }
432 }
433 infraKust := installer.NewKustomization()
434 infraKust.AddResources("pcloud-charts.yaml")
435 if err := repo.WriteKustomization("infrastructure/kustomization.yaml", infraKust); err != nil {
436 return err
437 }
438 if err := repo.WriteKustomization("environments/kustomization.yaml", installer.NewKustomization()); err != nil {
439 return err
440 }
441 if err := repo.CommitAndPush("initialize pcloud directory structure"); err != nil {
442 return err
443 }
444 }
445 return nil
446}
447
448func installEnvManager(ss *soft.Client, repo installer.RepoIO, global map[string]any) error {
449 keys, err := installer.NewSSHKeyPair()
450 if err != nil {
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400451 return err
452 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400453 user := fmt.Sprintf("%s-env-manager", bootstrapFlags.pcloudEnvName)
454 if err := ss.AddUser(user, keys.Public); err != nil {
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400455 return err
456 }
Giorgi Lekveishvili0ccd1482023-06-21 15:02:24 +0400457 if err := ss.MakeUserAdmin(user); err != nil {
458 return err
459 }
460 appRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
461 envManager, err := appRepo.Find("env-manager")
462 if err != nil {
463 return err
464 }
465 return repo.InstallApp(*envManager, "infrastructure", map[string]any{
466 "Global": global,
467 "Values": map[string]any{
468 "RepoIP": bootstrapFlags.softServeIP,
469 "SSHPrivateKey": keys.Private,
470 },
471 })
Giorgi Lekveishvili3550b432023-06-09 19:37:51 +0400472}
473
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400474func createActionConfig(namespace string) (*action.Configuration, error) {
giolekva8aa73e82022-07-09 11:34:39 +0400475 config := new(action.Configuration)
476 if err := config.Init(
Giorgi Lekveishvili23ef7f82023-05-26 11:57:48 +0400477 kube.GetConfig(rootFlags.kubeConfig, "", namespace),
478 namespace,
giolekva8aa73e82022-07-09 11:34:39 +0400479 "",
480 func(fmtString string, args ...interface{}) {
481 fmt.Printf(fmtString, args...)
482 fmt.Println()
483 },
484 ); err != nil {
485 return nil, err
486 }
487 return config, nil
488}