blob: 4cee2c6f687f6ea8d7bf964a212b3763c64df98e [file] [log] [blame]
giolekva050609f2021-12-29 15:51:40 +04001package main
2
3import (
giolekva716efb92022-05-07 23:08:58 +04004 "context"
5 "crypto/ed25519"
6 "crypto/rand"
7 "crypto/x509"
giolekva050609f2021-12-29 15:51:40 +04008 "embed"
giolekva716efb92022-05-07 23:08:58 +04009 "encoding/pem"
10 "fmt"
11 "golang.org/x/crypto/ssh"
12 "io"
giolekva050609f2021-12-29 15:51:40 +040013 "io/ioutil"
14 "log"
giolekva716efb92022-05-07 23:08:58 +040015 "net"
giolekva050609f2021-12-29 15:51:40 +040016 "os"
17 "path/filepath"
giolekva716efb92022-05-07 23:08:58 +040018 "strings"
giolekva050609f2021-12-29 15:51:40 +040019 "text/template"
giolekva716efb92022-05-07 23:08:58 +040020 "time"
giolekva050609f2021-12-29 15:51:40 +040021
giolekva716efb92022-05-07 23:08:58 +040022 "github.com/go-git/go-billy/v5/memfs"
23 "github.com/go-git/go-git/v5"
24 "github.com/go-git/go-git/v5/config"
25 "github.com/go-git/go-git/v5/plumbing/object"
26 "github.com/go-git/go-git/v5/plumbing/transport"
27 gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
28 "github.com/go-git/go-git/v5/storage/memory"
giolekva050609f2021-12-29 15:51:40 +040029 "github.com/spf13/cobra"
giolekva716efb92022-05-07 23:08:58 +040030 "helm.sh/helm/v3/pkg/action"
31 "helm.sh/helm/v3/pkg/chart/loader"
32 "helm.sh/helm/v3/pkg/kube"
giolekva050609f2021-12-29 15:51:40 +040033 "sigs.k8s.io/yaml"
34)
35
giolekva050609f2021-12-29 15:51:40 +040036//go:embed values-tmpl
37var valuesTmpls embed.FS
38
giolekva716efb92022-05-07 23:08:58 +040039//go:embed config.yaml
40var configTmpl string
41
giolekva050609f2021-12-29 15:51:40 +040042var rootCmd *cobra.Command
43
giolekva716efb92022-05-07 23:08:58 +040044var rootFlags struct {
45 kubeConfig string
46}
47
giolekva050609f2021-12-29 15:51:40 +040048var installFlags struct {
49 config string
50 appName string
51 outputDir string
52}
53
giolekva716efb92022-05-07 23:08:58 +040054var bootstrapFlags struct {
55 chartsDir string
56 adminPubKey string
57 adminPrivKey string
58}
59
60func bootstrapCmd() *cobra.Command {
61 cmd := &cobra.Command{
62 Use: "bootstrap",
63 RunE: bootstrapCmdRun,
giolekva050609f2021-12-29 15:51:40 +040064 }
giolekva716efb92022-05-07 23:08:58 +040065 cmd.Flags().StringVar(
66 &bootstrapFlags.chartsDir,
67 "charts-dir",
68 "",
69 "",
70 )
71 cmd.Flags().StringVar(
72 &bootstrapFlags.adminPubKey,
73 "admin-pub-key",
74 "",
75 "",
76 )
77 cmd.Flags().StringVar(
78 &bootstrapFlags.adminPrivKey,
79 "admin-priv-key",
80 "",
81 "",
82 )
83 return cmd
84}
85
86func createActionConfig() (*action.Configuration, error) {
87 config := new(action.Configuration)
88 if err := config.Init(
89 kube.GetConfig(rootFlags.kubeConfig, "", ""),
90 "pcloud",
91 "",
92 func(fmtString string, args ...interface{}) {
93 fmt.Printf(fmtString, args...)
94 fmt.Println()
95 },
96 ); err != nil {
97 return nil, err
98 }
99 return config, nil
100}
101
102func installSoftServe(pubKey, privKey, adminKey string) error {
103 config, err := createActionConfig()
104 if err != nil {
105 return err
106 }
107 chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "soft-serve"))
108 if err != nil {
109 return err
110 }
111 values := map[string]interface{}{
112 "privateKey": privKey,
113 "publicKey": pubKey,
114 "adminKey": adminKey,
115 }
116 installer := action.NewInstall(config)
117 installer.Namespace = "pcloud"
118 installer.CreateNamespace = true
119 installer.ReleaseName = "soft-serve"
120 installer.Wait = true
121 installer.Timeout = 5 * time.Minute
122 // installer.DryRun = true
123 // installer.OutputDir = "/tmp/rr"
124 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
125 return err
126 }
127 return nil
128}
129
130func installFlux(repoAddr, repoHost, repoHostPubKey, privateKey string) error {
131 config, err := createActionConfig()
132 if err != nil {
133 return err
134 }
135 chart, err := loader.Load(filepath.Join(bootstrapFlags.chartsDir, "flux-bootstrap"))
136 if err != nil {
137 return err
138 }
139 values := map[string]interface{}{
140 "repositoryAddress": repoAddr,
141 "repositoryHost": repoHost,
142 "repositoryHostPublicKey": repoHostPubKey,
143 "privateKey": privateKey,
144 }
145 installer := action.NewInstall(config)
146 installer.Namespace = "pcloud"
147 installer.CreateNamespace = true
giolekva9eacb1a2022-05-21 13:57:19 +0400148 installer.ReleaseName = "flux4"
giolekva716efb92022-05-07 23:08:58 +0400149 installer.Wait = true
150 installer.WaitForJobs = true
151 installer.Timeout = 5 * time.Minute
152 // installer.DryRun = true
153 // installer.OutputDir = "/tmp/ee"
154 if _, err := installer.RunWithContext(context.TODO(), chart, values); err != nil {
155 return err
156 }
157 return nil
158}
159
160func overwriteConfigRepo(address string, auth transport.AuthMethod, cfg string) error {
161 repo, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
162 URL: address,
163 Auth: auth,
164 RemoteName: "soft",
165 InsecureSkipTLS: true,
166 })
167 if err != nil {
168 return err
169 }
170 wt, err := repo.Worktree()
171 if err != nil {
172 return err
173 }
174 if err := func() error {
175 f, err := wt.Filesystem.Create("config.yaml")
176 if err != nil {
177 return nil
178 }
179 defer f.Close()
180 if _, err := io.WriteString(f, cfg); err != nil {
181 return err
182 }
183 return nil
184
185 }(); err != nil {
186 return err
187 }
188 if _, err := wt.Add("config.yaml"); err != nil {
189 return err
190 }
191 if _, err := wt.Commit("initial overwrite to give access to fluxcd", &git.CommitOptions{
192 Author: &object.Signature{
193 Name: "pcloud",
194 Email: "pcloud@installer",
195 When: time.Now(),
196 },
197 }); err != nil {
198 return err
199 }
200 if err = repo.Push(&git.PushOptions{
201 RemoteName: "soft",
202 Auth: auth,
203 }); err != nil {
204 return err
205 }
206 return nil
207}
208
209func createRepo(address string, readme string, auth transport.AuthMethod) error {
210 repo, err := git.Init(memory.NewStorage(), memfs.New())
211 if err != nil {
212 return err
213 }
214 wt, err := repo.Worktree()
215 if err != nil {
216 return err
217 }
218 if err := func() error {
219 f, err := wt.Filesystem.Create("README.md")
220 if err != nil {
221 return nil
222 }
223 defer f.Close()
224 if _, err := io.WriteString(f, readme); err != nil {
225 return err
226 }
227 return nil
228
229 }(); err != nil {
230 return err
231 }
232 if _, err := wt.Add("README.md"); err != nil {
233 return err
234 }
235 if _, err := wt.Commit("init", &git.CommitOptions{
236 Author: &object.Signature{
237 Name: "pcloud",
238 Email: "pcloud@installer",
239 When: time.Now(),
240 },
241 }); err != nil {
242 return err
243 }
244 if _, err := repo.CreateRemote(&config.RemoteConfig{
245 Name: "soft",
246 URLs: []string{address},
247 }); err != nil {
248 return err
249 }
250 if err = repo.Push(&git.PushOptions{
251 RemoteName: "soft",
252 Auth: auth,
253 }); err != nil {
254 return err
255 }
256 return nil
257}
258
259func generateSSHKeys() (string, string, error) {
260 pub, priv, err := ed25519.GenerateKey(rand.Reader)
261 if err != nil {
262 return "", "", err
263 }
264 privEnc, err := x509.MarshalPKCS8PrivateKey(priv)
265 if err != nil {
266 return "", "", err
267 }
268 privPem := pem.EncodeToMemory(
269 &pem.Block{
270 Type: "PRIVATE KEY",
271 Bytes: privEnc,
272 },
273 )
274 pubKey, err := ssh.NewPublicKey(pub)
275 if err != nil {
276 return "", "", err
277 }
278 return string(ssh.MarshalAuthorizedKey(pubKey)), string(privPem), nil
279}
280
281func generateConfig(adminKeys []string) (string, error) {
282 keys := make([]string, len(adminKeys))
283 for i, key := range adminKeys {
284 keys[i] = strings.Trim(key, " \n")
285 }
286 configT, err := template.New("config").Parse(configTmpl)
287 if err != nil {
288 return "", err
289 }
290 var configB strings.Builder
291 if err := configT.Execute(&configB, keys); err != nil {
292 return "", err
293 }
294 return configB.String(), nil
295}
296
297func readAdminKeys() ([]byte, []byte, error) {
298 pubKey, err := os.ReadFile(bootstrapFlags.adminPubKey)
299 if err != nil {
300 return nil, nil, err
301 }
302 privKey, err := os.ReadFile(bootstrapFlags.adminPrivKey)
303 if err != nil {
304 return nil, nil, err
305 }
306 return pubKey, privKey, nil
307}
308
309func createSSHAuthMethod(key []byte) (*gitssh.PublicKeys, error) {
310 signer, err := ssh.ParsePrivateKey(key)
311 if err != nil {
312 return nil, err
313 }
314 return &gitssh.PublicKeys{
315 User: "pcloud",
316 Signer: signer,
317 }, nil
318}
319
giolekva9eacb1a2022-05-21 13:57:19 +0400320func reloadConfig(addr string, clientPrivKey []byte, serverPubKey string) error {
321 signer, err := ssh.ParsePrivateKey(clientPrivKey)
322 if err != nil {
323 return err
324 }
325 config := &ssh.ClientConfig{
326 Auth: []ssh.AuthMethod{
327 ssh.PublicKeys(signer),
328 },
329 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
330 fmt.Printf("## %s || %s -- \n", serverPubKey, ssh.MarshalAuthorizedKey(key))
331 return nil
332 },
333 }
334 client, err := ssh.Dial("tcp", addr, config)
335 if err != nil {
336 return err
337 }
338 session, err := client.NewSession()
339 if err != nil {
340 return err
341 }
342 defer session.Close()
343 return session.Run("reload")
344}
345
giolekva716efb92022-05-07 23:08:58 +0400346func bootstrapCmdRun(cmd *cobra.Command, args []string) error {
347 adminPubKey, adminPrivKey, err := readAdminKeys()
348 if err != nil {
349 return err
350 }
351 auth, err := createSSHAuthMethod(adminPrivKey)
352 if err != nil {
353 return err
354 }
355 fluxPub, fluxPriv, err := generateSSHKeys()
356 if err != nil {
357 return err
358 }
359 config, err := generateConfig([]string{string(adminPubKey), fluxPub})
360 if err != nil {
361 return err
362 }
363 softServePub, softServePriv, err := generateSSHKeys()
364 if err != nil {
365 return err
366 }
367 auth.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
368 fmt.Printf("-- %s || %s -- \n", softServePub, ssh.MarshalAuthorizedKey(key))
369 return nil
370 }
giolekva9eacb1a2022-05-21 13:57:19 +0400371 fmt.Println("Installing SoftServe")
giolekva716efb92022-05-07 23:08:58 +0400372 if err := installSoftServe(softServePub, softServePriv, string(adminPubKey)); err != nil {
373 return err
374 }
giolekva9eacb1a2022-05-21 13:57:19 +0400375 time.Sleep(30 * time.Second)
376 fmt.Println("Overwriting config")
giolekva716efb92022-05-07 23:08:58 +0400377 if err := overwriteConfigRepo("ssh://192.168.0.208:22/config", auth, config); err != nil {
378 return err
379 }
giolekva9eacb1a2022-05-21 13:57:19 +0400380 fmt.Println("Reloading config")
381 if err := reloadConfig("192.168.0.208:22", adminPrivKey, softServePub); err != nil {
382 return err
383 }
384 fmt.Println("Creating /pcloud repo")
giolekva716efb92022-05-07 23:08:58 +0400385 if err := createRepo("ssh://192.168.0.208:22/pcloud", "PCloud System\n", auth); err != nil {
386 return err
387 }
giolekva9eacb1a2022-05-21 13:57:19 +0400388 fmt.Println("Installing Flux")
giolekva716efb92022-05-07 23:08:58 +0400389 if err := installFlux("ssh://soft-serve.pcloud.svc.cluster.local:22/pcloud", "soft-serve.pcloud.svc.cluster.local", softServePub, fluxPriv); err != nil {
390 return err
391 }
392 return nil
393}
394
395func installCmd() *cobra.Command {
396 cmd := &cobra.Command{
giolekva050609f2021-12-29 15:51:40 +0400397 Use: "install",
398 RunE: installCmdRun,
399 }
giolekva716efb92022-05-07 23:08:58 +0400400 cmd.Flags().StringVar(
giolekva050609f2021-12-29 15:51:40 +0400401 &installFlags.config,
402 "config",
403 "",
404 "",
405 )
giolekva716efb92022-05-07 23:08:58 +0400406 cmd.Flags().StringVar(
giolekva050609f2021-12-29 15:51:40 +0400407 &installFlags.appName,
408 "app",
409 "",
410 "",
411 )
giolekva716efb92022-05-07 23:08:58 +0400412 cmd.Flags().StringVar(
giolekva050609f2021-12-29 15:51:40 +0400413 &installFlags.outputDir,
414 "output-dir",
415 "",
416 "",
417 )
giolekva716efb92022-05-07 23:08:58 +0400418 return cmd
419}
420
421func init() {
422 rootCmd = &cobra.Command{
423 Use: "pcloud",
424 }
425 rootCmd.PersistentFlags().StringVar(
426 &rootFlags.kubeConfig,
427 "kubeconfig",
428 "",
429 "",
430 )
431 rootCmd.AddCommand(bootstrapCmd())
432 rootCmd.AddCommand(installCmd())
giolekva050609f2021-12-29 15:51:40 +0400433}
434
435func readConfig(config string) (Config, error) {
436 var cfg Config
437 inp, err := ioutil.ReadFile(config)
438 if err != nil {
439 return cfg, err
440 }
441 err = yaml.UnmarshalStrict(inp, &cfg)
442 return cfg, err
443}
444
445func installCmdRun(cmd *cobra.Command, args []string) error {
446 cfg, err := readConfig(installFlags.config)
447 if err != nil {
448 return err
449 }
450 tmpls, err := template.ParseFS(valuesTmpls, "values-tmpl/*.yaml")
451 if err != nil {
452 log.Fatal(err)
453 }
giolekvaef76a3e2022-01-10 12:22:28 +0400454 apps := CreateAllApps(tmpls)
giolekva050609f2021-12-29 15:51:40 +0400455 for _, a := range apps {
456 if a.Name == installFlags.appName {
457 for _, t := range a.Templates {
458 out, err := os.Create(filepath.Join(installFlags.outputDir, t.Name()))
459 if err != nil {
460 return err
461 }
462 defer out.Close()
463 if err := t.Execute(out, cfg); err != nil {
464 return err
465 }
466 }
467 break
468 }
469 }
470 return nil
471}
472
473func main() {
474 if err := rootCmd.Execute(); err != nil {
475 log.Fatal(err)
476 }
477}