github.com/pluralsh/plural-cli@v0.9.5/pkg/cd/control_plane_install.go (about) 1 package cd 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "net/http" 8 "path/filepath" 9 10 "gopkg.in/yaml.v3" 11 12 "github.com/AlecAivazis/survey/v2" 13 "github.com/osteele/liquid" 14 "github.com/pluralsh/plural-cli/pkg/api" 15 "github.com/pluralsh/plural-cli/pkg/bundle" 16 "github.com/pluralsh/plural-cli/pkg/config" 17 "github.com/pluralsh/plural-cli/pkg/crypto" 18 "github.com/pluralsh/plural-cli/pkg/manifest" 19 "github.com/pluralsh/plural-cli/pkg/provider" 20 "github.com/pluralsh/plural-cli/pkg/template" 21 "github.com/pluralsh/plural-cli/pkg/utils" 22 "github.com/pluralsh/plural-cli/pkg/utils/git" 23 ) 24 25 var ( 26 liquidEngine = liquid.NewEngine() 27 ) 28 29 const ( 30 templateUrl = "https://raw.githubusercontent.com/pluralsh/console/master/templates/values.yaml.liquid" 31 tplUrl = "https://raw.githubusercontent.com/pluralsh/console/master/templates/values.yaml.tpl" 32 ) 33 34 type secrets struct { 35 AesKey string `yaml:"aes_key"` 36 Erlang string `yaml:"erlang"` 37 } 38 39 type ingress struct { 40 ConsoleDns string `yaml:"console_dns"` 41 KasDns string `yaml:"kas_dns"` 42 } 43 44 type consoleValues struct { 45 Ingress ingress `yaml:"ingress"` 46 Secrets secrets `yaml:"secrets"` 47 } 48 49 func ControlPlaneValues(conf config.Config, file, domain, dsn, name string) (string, error) { 50 consoleDns := fmt.Sprintf("console.%s", domain) 51 kasDns := fmt.Sprintf("kas.%s", domain) 52 randoms := map[string]string{} 53 existing := consoleValues{} 54 if utils.Exists(file) { 55 if d, err := utils.ReadFile(file); err == nil { 56 if err := yaml.Unmarshal([]byte(d), &existing); err == nil { 57 if existing.Ingress.ConsoleDns != "" { 58 consoleDns = existing.Ingress.ConsoleDns 59 } 60 if existing.Ingress.KasDns != "" { 61 kasDns = existing.Ingress.KasDns 62 } 63 } 64 } 65 } 66 for _, key := range []string{"jwt", "erlang", "adminPassword", "kasApi", "kasPrivateApi", "kasRedis"} { 67 rand, err := crypto.RandStr(32) 68 if err != nil { 69 return "", err 70 } 71 randoms[key] = rand 72 } 73 74 if existing.Secrets.Erlang != "" { 75 randoms["erlang"] = existing.Secrets.Erlang 76 } 77 78 client := api.FromConfig(&conf) 79 me, err := client.Me() 80 if err != nil { 81 return "", fmt.Errorf("you must run `plural login` before installing") 82 } 83 84 root, err := git.Root() 85 if err != nil { 86 return "", err 87 } 88 89 project, err := manifest.ReadProject(filepath.Join(root, "workspace.yaml")) 90 if err != nil { 91 return "", err 92 } 93 94 prov, err := provider.FromManifest(project) 95 if err != nil { 96 return "", err 97 } 98 99 configuration := map[string]interface{}{ 100 "consoleDns": consoleDns, 101 "kasDns": kasDns, 102 "aesKey": utils.GenAESKey(), 103 "adminName": me.Email, 104 "adminEmail": me.Email, 105 "clusterName": name, 106 "pluralToken": conf.Token, 107 "postgresUrl": dsn, 108 "provider": prov.Name(), 109 "clusterIssuer": "plural", 110 } 111 112 if existing.Secrets.AesKey != "" { 113 configuration["aesKey"] = existing.Secrets.AesKey 114 } 115 116 for k, v := range randoms { 117 configuration[k] = v 118 } 119 120 cryptos, err := cryptoVals() 121 if err != nil { 122 return "", err 123 } 124 125 for k, v := range cryptos { 126 configuration[k] = v 127 } 128 129 clientId, clientSecret, err := ensureInstalledAndOidc(client, consoleDns) 130 if err != nil { 131 return "", err 132 } 133 configuration["pluralClientId"] = clientId 134 configuration["pluralClientSecret"] = clientSecret 135 136 tpl, err := fetchTemplate(tplUrl) 137 if err != nil { 138 return "", err 139 } 140 141 return template.RenderString(string(tpl), configuration) 142 } 143 144 func cryptoVals() (map[string]string, error) { 145 res := make(map[string]string) 146 keyFile, err := config.PluralDir("key") 147 if err != nil { 148 return res, err 149 } 150 151 aes, err := utils.ReadFile(keyFile) 152 if err != nil { 153 return res, err 154 } 155 res["key"] = aes 156 157 identityFile, err := config.PluralDir("identity") 158 if err != nil { 159 return res, nil 160 } 161 162 identity, err := utils.ReadFile(identityFile) 163 if err != nil { 164 return res, nil 165 } 166 res["identity"] = identity 167 return res, nil 168 } 169 170 func CreateControlPlane(conf config.Config) (string, error) { 171 client := api.FromConfig(&conf) 172 me, err := client.Me() 173 if err != nil { 174 return "", fmt.Errorf("you must run `plural login` before installing") 175 } 176 177 azureSurvey := []*survey.Question{ 178 { 179 Name: "console", 180 Prompt: &survey.Input{Message: "Enter a dns name for your installation of the console (eg console.your.domain):"}, 181 }, 182 { 183 Name: "kubeProxy", 184 Prompt: &survey.Input{Message: "Enter a dns name for the kube proxy (eg kas.your.domain), this is used for dashboarding functionality:"}, 185 }, 186 { 187 Name: "clusterName", 188 Prompt: &survey.Input{Message: "Enter a name for this cluster:"}, 189 }, 190 { 191 Name: "postgresDsn", 192 Prompt: &survey.Input{Message: "Enter a postgres connection string for the underlying database (should be postgres://<user>:<password>@<host>:5432/<database>):"}, 193 }, 194 } 195 var resp struct { 196 Console string 197 KubeProxy string 198 ClusterName string 199 PostgresDsn string 200 } 201 if err := survey.Ask(azureSurvey, &resp); err != nil { 202 return "", err 203 } 204 205 randoms := map[string]string{} 206 for _, key := range []string{"jwt", "erlang", "adminPassword", "kasApi", "kasPrivateApi", "kasRedis"} { 207 rand, err := crypto.RandStr(32) 208 if err != nil { 209 return "", err 210 } 211 randoms[key] = rand 212 } 213 214 configuration := map[string]string{ 215 "consoleDns": resp.Console, 216 "kasDns": resp.KubeProxy, 217 "aesKey": utils.GenAESKey(), 218 "adminName": me.Email, 219 "adminEmail": me.Email, 220 "clusterName": resp.ClusterName, 221 "pluralToken": conf.Token, 222 "postgresUrl": resp.PostgresDsn, 223 } 224 for k, v := range randoms { 225 configuration[k] = v 226 } 227 228 clientId, clientSecret, err := ensureInstalledAndOidc(client, resp.Console) 229 if err != nil { 230 return "", err 231 } 232 configuration["pluralClientId"] = clientId 233 configuration["pluralClientSecret"] = clientSecret 234 235 tpl, err := fetchTemplate(templateUrl) 236 if err != nil { 237 return "", err 238 } 239 240 bindings := map[string]interface{}{ 241 "configuration": configuration, 242 } 243 244 res, err := liquidEngine.ParseAndRender(tpl, bindings) 245 return string(res), err 246 } 247 248 func fetchTemplate(url string) (res []byte, err error) { 249 resp, err := http.Get(url) 250 if err != nil { 251 return 252 } 253 defer resp.Body.Close() 254 var out bytes.Buffer 255 _, err = io.Copy(&out, resp.Body) 256 return out.Bytes(), err 257 } 258 259 func ensureInstalledAndOidc(client api.Client, dns string) (clientId string, clientSecret string, err error) { 260 inst, err := client.GetInstallation("console") 261 if err != nil || inst == nil { 262 repo, err := client.GetRepository("console") 263 if err != nil { 264 return "", "", err 265 } 266 _, err = client.CreateInstallation(repo.Id) 267 if err != nil { 268 return "", "", err 269 } 270 } 271 272 redirectUris := []string{fmt.Sprintf("https://%s/oauth/callback", dns)} 273 err = bundle.SetupOIDC("console", client, redirectUris, "POST") 274 if err != nil { 275 return 276 } 277 278 inst, err = client.GetInstallation("console") 279 if err != nil { 280 return 281 } 282 283 return inst.OIDCProvider.ClientId, inst.OIDCProvider.ClientSecret, nil 284 }