blob: bc1ca717c877e4d1cc1416a8d9401fd633bfe8c3 [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
148 installer.ReleaseName = "flux"
149 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
320func bootstrapCmdRun(cmd *cobra.Command, args []string) error {
321 adminPubKey, adminPrivKey, err := readAdminKeys()
322 if err != nil {
323 return err
324 }
325 auth, err := createSSHAuthMethod(adminPrivKey)
326 if err != nil {
327 return err
328 }
329 fluxPub, fluxPriv, err := generateSSHKeys()
330 if err != nil {
331 return err
332 }
333 config, err := generateConfig([]string{string(adminPubKey), fluxPub})
334 if err != nil {
335 return err
336 }
337 softServePub, softServePriv, err := generateSSHKeys()
338 if err != nil {
339 return err
340 }
341 auth.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
342 fmt.Printf("-- %s || %s -- \n", softServePub, ssh.MarshalAuthorizedKey(key))
343 return nil
344 }
345 if err := installSoftServe(softServePub, softServePriv, string(adminPubKey)); err != nil {
346 return err
347 }
348 time.Sleep(10 * time.Second)
349 if err := overwriteConfigRepo("ssh://192.168.0.208:22/config", auth, config); err != nil {
350 return err
351 }
352 if err := createRepo("ssh://192.168.0.208:22/pcloud", "PCloud System\n", auth); err != nil {
353 return err
354 }
355 if err := installFlux("ssh://soft-serve.pcloud.svc.cluster.local:22/pcloud", "soft-serve.pcloud.svc.cluster.local", softServePub, fluxPriv); err != nil {
356 return err
357 }
358 return nil
359}
360
361func installCmd() *cobra.Command {
362 cmd := &cobra.Command{
giolekva050609f2021-12-29 15:51:40 +0400363 Use: "install",
364 RunE: installCmdRun,
365 }
giolekva716efb92022-05-07 23:08:58 +0400366 cmd.Flags().StringVar(
giolekva050609f2021-12-29 15:51:40 +0400367 &installFlags.config,
368 "config",
369 "",
370 "",
371 )
giolekva716efb92022-05-07 23:08:58 +0400372 cmd.Flags().StringVar(
giolekva050609f2021-12-29 15:51:40 +0400373 &installFlags.appName,
374 "app",
375 "",
376 "",
377 )
giolekva716efb92022-05-07 23:08:58 +0400378 cmd.Flags().StringVar(
giolekva050609f2021-12-29 15:51:40 +0400379 &installFlags.outputDir,
380 "output-dir",
381 "",
382 "",
383 )
giolekva716efb92022-05-07 23:08:58 +0400384 return cmd
385}
386
387func init() {
388 rootCmd = &cobra.Command{
389 Use: "pcloud",
390 }
391 rootCmd.PersistentFlags().StringVar(
392 &rootFlags.kubeConfig,
393 "kubeconfig",
394 "",
395 "",
396 )
397 rootCmd.AddCommand(bootstrapCmd())
398 rootCmd.AddCommand(installCmd())
giolekva050609f2021-12-29 15:51:40 +0400399}
400
401func readConfig(config string) (Config, error) {
402 var cfg Config
403 inp, err := ioutil.ReadFile(config)
404 if err != nil {
405 return cfg, err
406 }
407 err = yaml.UnmarshalStrict(inp, &cfg)
408 return cfg, err
409}
410
411func installCmdRun(cmd *cobra.Command, args []string) error {
412 cfg, err := readConfig(installFlags.config)
413 if err != nil {
414 return err
415 }
416 tmpls, err := template.ParseFS(valuesTmpls, "values-tmpl/*.yaml")
417 if err != nil {
418 log.Fatal(err)
419 }
giolekvaef76a3e2022-01-10 12:22:28 +0400420 apps := CreateAllApps(tmpls)
giolekva050609f2021-12-29 15:51:40 +0400421 for _, a := range apps {
422 if a.Name == installFlags.appName {
423 for _, t := range a.Templates {
424 out, err := os.Create(filepath.Join(installFlags.outputDir, t.Name()))
425 if err != nil {
426 return err
427 }
428 defer out.Close()
429 if err := t.Execute(out, cfg); err != nil {
430 return err
431 }
432 }
433 break
434 }
435 }
436 return nil
437}
438
439func main() {
440 if err := rootCmd.Execute(); err != nil {
441 log.Fatal(err)
442 }
443}