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  }