From 82f4d0fc23e907d922c60d318ed72753cd4c5eee Mon Sep 17 00:00:00 2001 From: michaeljguarino Date: Sun, 5 Nov 2023 12:22:45 -0500 Subject: [PATCH] BYOK installer for plural console --- cmd/plural/cd.go | 23 ++++++ go.mod | 4 +- go.sum | 8 +- pkg/api/client.go | 1 + pkg/api/installations.go | 8 ++ pkg/bundle/oidc.go | 23 ++++++ pkg/cd/control_plane_install.go | 140 ++++++++++++++++++++++++++++++++ 7 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 pkg/cd/control_plane_install.go diff --git a/cmd/plural/cd.go b/cmd/plural/cd.go index b761a5bdf..07fe2998a 100644 --- a/cmd/plural/cd.go +++ b/cmd/plural/cd.go @@ -10,6 +10,7 @@ import ( "github.com/AlecAivazis/survey/v2" gqlclient "github.com/pluralsh/console-client-go" "github.com/pluralsh/plural/pkg/cd" + "github.com/pluralsh/plural/pkg/config" "github.com/pluralsh/plural/pkg/console" "github.com/pluralsh/plural/pkg/utils" "github.com/pluralsh/polly/containers" @@ -74,6 +75,11 @@ func (p *Plural) cdCommands() []cli.Command { cli.StringFlag{Name: "token", Usage: "console token", Required: true}, }, }, + { + Name: "control-plane", + Action: p.handleInstallControlPlane, + Usage: "sets up the plural console in an existing k8s cluster", + }, { Name: "uninstall", Action: p.handleUninstallOperator, @@ -1000,6 +1006,23 @@ func (p *Plural) handleCreateCluster(c *cli.Context) error { return nil } +func (p *Plural) handleInstallControlPlane(c *cli.Context) error { + conf := config.Read() + vals, err := cd.CreateControlPlane(conf) + if err != nil { + return err + } + + utils.Highlight("writing values.secret.yaml, you should keep this in a secure location for future helm installs") + if err := os.WriteFile("values.secret.yaml", []byte(vals), 0644); err != nil { + return err + } + + fmt.Println("After confirming everything looks correct in values.secret.yaml, run the following command to install:") + utils.Highlight("helm upgrade --install --create-namespace -f values.secret.yaml console -n plrl-console") + return nil +} + func (p *Plural) handleCreateProvider(existingProviders []string) (*gqlclient.CreateClusterProvider, error) { provider := "" var resp struct { diff --git a/go.mod b/go.mod index effa9b570..6e44eae21 100644 --- a/go.mod +++ b/go.mod @@ -50,11 +50,12 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/norwoodj/helm-docs v1.11.2 github.com/olekukonko/tablewriter v0.0.5 + github.com/osteele/liquid v1.3.2 github.com/packethost/packngo v0.29.0 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/pluralsh/cluster-api-migration v0.2.15 github.com/pluralsh/console-client-go v0.0.31 - github.com/pluralsh/gqlclient v1.10.0 + github.com/pluralsh/gqlclient v1.11.0 github.com/pluralsh/plural-operator v0.5.5 github.com/pluralsh/polly v0.1.1 github.com/pluralsh/terraform-delinker v0.0.1 @@ -220,6 +221,7 @@ require ( github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 // indirect github.com/onsi/gomega v1.27.6 // indirect github.com/orcaman/concurrent-map v1.0.0 // indirect + github.com/osteele/tuesday v1.0.3 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect diff --git a/go.sum b/go.sum index 7ca180e19..19d95946a 100644 --- a/go.sum +++ b/go.sum @@ -1380,6 +1380,10 @@ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY= github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= +github.com/osteele/liquid v1.3.2 h1:G+MvVYt1HX2xuv99JgdrhV7zRVdlvFnNi8M5rN8gQmI= +github.com/osteele/liquid v1.3.2/go.mod h1:VmzQQHa5v4E0GvGzqccfAfLgMwRk2V+s1QbxYx9dGak= +github.com/osteele/tuesday v1.0.3 h1:SrCmo6sWwSgnvs1bivmXLvD7Ko9+aJvvkmDjB5G4FTU= +github.com/osteele/tuesday v1.0.3/go.mod h1:pREKpE+L03UFuR+hiznj3q7j3qB1rUZ4XfKejwWFF2M= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= @@ -1423,8 +1427,8 @@ github.com/pluralsh/console-client-go v0.0.31 h1:M8NPmVmuL4sH8/gC+WlgbXmxOp/y3pX github.com/pluralsh/console-client-go v0.0.31/go.mod h1:kZjk0pXAWnvyj+miXveCho4kKQaX1Tm3CGAM+iwurWU= github.com/pluralsh/controller-reconcile-helper v0.0.4 h1:1o+7qYSyoeqKFjx+WgQTxDz4Q2VMpzprJIIKShxqG0E= github.com/pluralsh/controller-reconcile-helper v0.0.4/go.mod h1:AfY0gtteD6veBjmB6jiRx/aR4yevEf6K0M13/pGan/s= -github.com/pluralsh/gqlclient v1.10.0 h1:ccYB+A0JbPYkEeVzdfajd29l65N6x/buSKPMMxM8OIA= -github.com/pluralsh/gqlclient v1.10.0/go.mod h1:qSXKUlio1F2DRPy8el4oFYsmpKbkUYspgPB87T4it5I= +github.com/pluralsh/gqlclient v1.11.0 h1:FfXW7FiEJLHOfTAa7NxDb8jb3aMZNIpCAcG+bg8uHYA= +github.com/pluralsh/gqlclient v1.11.0/go.mod h1:qSXKUlio1F2DRPy8el4oFYsmpKbkUYspgPB87T4it5I= github.com/pluralsh/helm-docs v1.11.3-0.20230914191425-6d14ebab8817 h1:J7SGxH6nJGdRoNtqdzhyr2VMpbl4asolul7xqqW++EA= github.com/pluralsh/helm-docs v1.11.3-0.20230914191425-6d14ebab8817/go.mod h1:rLqec59NO7YF57Rq9VlubQHMp7wcRTJhzpkcgs4lOG4= github.com/pluralsh/oauth v0.9.2 h1:tM9hBK4tCnJUeCOgX0ctxBBCS3hiCDPoxkJLODtedmQ= diff --git a/pkg/api/client.go b/pkg/api/client.go index a4e0c30df..8e0e90971 100644 --- a/pkg/api/client.go +++ b/pkg/api/client.go @@ -59,6 +59,7 @@ type Client interface { GetPackageInstallations(repoId string) (charts []*ChartInstallation, tfs []*TerraformInstallation, err error) CreateCrd(repo string, chart string, file string) error CreateDomain(name string) error + CreateInstallation(id string) (string, error) GetInstallation(name string) (*Installation, error) GetInstallationById(id string) (*Installation, error) GetInstallations() ([]*Installation, error) diff --git a/pkg/api/installations.go b/pkg/api/installations.go index b0127f1ad..96c8054c3 100644 --- a/pkg/api/installations.go +++ b/pkg/api/installations.go @@ -39,6 +39,14 @@ func (client *client) DeleteInstallation(id string) error { return err } +func (client *client) CreateInstallation(id string) (string, error) { + resp, err := client.pluralClient.CreateInstallation(client.ctx, id) + if err != nil { + return "", err + } + return resp.CreateInstallation.ID, err +} + func convertInstallation(installation *gqlclient.InstallationFragment) *Installation { if installation == nil { return nil diff --git a/pkg/bundle/oidc.go b/pkg/bundle/oidc.go index 9d8f538aa..be11f6228 100644 --- a/pkg/bundle/oidc.go +++ b/pkg/bundle/oidc.go @@ -55,6 +55,29 @@ func ConfigureOidc(repo string, client api.Client, recipe *api.Recipe, ctx map[s return api.GetErrorResponse(err, "OIDCProvider") } +func SetupOIDC(repo string, client api.Client, redirectUris []string, authMethod string) error { + inst, err := client.GetInstallation(repo) + if err != nil { + return api.GetErrorResponse(err, "GetInstallation") + } + + me, err := client.Me() + if err != nil { + return api.GetErrorResponse(err, "Me") + } + + oidcSettings := &api.OidcProviderAttributes{ + RedirectUris: redirectUris, + AuthMethod: authMethod, + Bindings: []api.Binding{ + {UserId: me.Id}, + }, + } + mergeOidcAttributes(inst, oidcSettings) + err = client.OIDCProvider(inst.Id, oidcSettings) + return api.GetErrorResponse(err, "OIDCProvider") +} + func mergeOidcAttributes(inst *api.Installation, attributes *api.OidcProviderAttributes) { if inst.OIDCProvider == nil { return diff --git a/pkg/cd/control_plane_install.go b/pkg/cd/control_plane_install.go new file mode 100644 index 000000000..054b1c922 --- /dev/null +++ b/pkg/cd/control_plane_install.go @@ -0,0 +1,140 @@ +package cd + +import ( + "bytes" + "fmt" + "io" + "net/http" + + "github.com/AlecAivazis/survey/v2" + "github.com/osteele/liquid" + "github.com/pluralsh/plural/pkg/api" + "github.com/pluralsh/plural/pkg/bundle" + "github.com/pluralsh/plural/pkg/config" + "github.com/pluralsh/plural/pkg/crypto" + "github.com/pluralsh/plural/pkg/utils" +) + +var ( + liquidEngine = liquid.NewEngine() +) + +const ( + templateUrl = "https://raw.githubusercontent.com/pluralsh/console/cd-scaffolding/charts/console/values.yaml.liquid" +) + +func CreateControlPlane(conf config.Config) (string, error) { + client := api.FromConfig(&conf) + me, err := client.Me() + if err != nil { + return "", fmt.Errorf("you must run `plural login` before installing") + } + + azureSurvey := []*survey.Question{ + { + Name: "console", + Prompt: &survey.Input{Message: "Enter a dns name for your installation of the console (eg console.your.domain):"}, + }, + { + Name: "kubeProxy", + Prompt: &survey.Input{Message: "Enter a dns name for the kube proxy (eg kas.your.domain), this is used for dashboarding functionality:"}, + }, + { + Name: "clusterName", + Prompt: &survey.Input{Message: "Enter a name for this cluster:"}, + }, + { + Name: "postgresDsn", + Prompt: &survey.Input{Message: "Enter a postgres connection string for the underlying database (should be postgres://:@:5432/):"}, + }, + } + var resp struct { + Console string + KubeProxy string + ClusterName string + PostgresDsn string + } + if err := survey.Ask(azureSurvey, &resp); err != nil { + return "", err + } + + randoms := map[string]string{} + for _, key := range []string{"jwt", "erlang", "adminPassword", "kasApi", "kasPrivateApi", "kasRedis"} { + rand, err := crypto.RandStr(32) + if err != nil { + return "", err + } + randoms[key] = rand + } + + configuration := map[string]string{ + "consoleDns": resp.Console, + "kasDns": resp.KubeProxy, + "aesKey": utils.GenAESKey(), + "adminName": me.Email, + "adminEmail": me.Email, + "clusterName": resp.ClusterName, + "pluralToken": conf.Token, + "postgresUrl": resp.PostgresDsn, + } + for k, v := range randoms { + configuration[k] = v + } + + clientId, clientSecret, err := ensureInstalledAndOidc(client, resp.Console) + if err != nil { + return "", err + } + configuration["pluralClientId"] = clientId + configuration["pluralClientSecret"] = clientSecret + + tpl, err := fetchTemplate() + if err != nil { + return "", err + } + + bindings := map[string]interface{}{ + "configuration": configuration, + } + + res, err := liquidEngine.ParseAndRender(tpl, bindings) + return string(res), err +} + +func fetchTemplate() (res []byte, err error) { + resp, err := http.Get(templateUrl) + if err != nil { + return + } + defer resp.Body.Close() + var out bytes.Buffer + _, err = io.Copy(&out, resp.Body) + return out.Bytes(), err +} + +func ensureInstalledAndOidc(client api.Client, dns string) (clientId string, clientSecret string, err error) { + inst, err := client.GetInstallation("console") + if err != nil || inst == nil { + repo, err := client.GetRepository("console") + if err != nil { + return "", "", err + } + _, err = client.CreateInstallation(repo.Id) + if err != nil { + return "", "", err + } + } + + redirectUris := []string{fmt.Sprintf("https://%s/oauth/callback", dns)} + err = bundle.SetupOIDC("console", client, redirectUris, "POST") + if err != nil { + return + } + + inst, err = client.GetInstallation("console") + if err != nil { + return + } + + return inst.OIDCProvider.ClientId, inst.OIDCProvider.ClientSecret, nil +}