Skip to content

Commit

Permalink
Add bootstrap command for gitops cli to bootstrap WGE (#3371)
Browse files Browse the repository at this point in the history
* init gitops bootstrap command for wge

* add checks for entitlement and flux

* Add wge version choise list

* add create admin username and password secret

* add install wge

* fix adding admin password

* fix linting

* run go mod tidy

* enable ingress

* add domain type selector

* add option to bootstrap flux

* adjust bootstraping flux and domain

* add install extra controllers

* fix lint

* rename package checks to commands

* refactor packages

* early exit

* refactor errors and add utils for git repos

* refactor wge installation to use files

* refactor extra controllers

* remove extra unneeded values

* Add OIDC

* update portforward

* add admin password revert

* refactor error handeling

* fix unhandeled errors

* refactor creating helmrepos and helmreleases

* refactor styling

* go mod tidy

* split utils

* Add OIDC

* refactor creating extra controllers

* move install extra controller to commands

* add oidc

* Add OIDC

* install capi controller

* add terraform controller

* refactor OIDC cli

* fix linting

* fix lintting

* fix lint

* fix lint

* update OIDC CLI

* handle error msgs

* move check mark to method

* reformat error

* cleanup

* update CLI OIDC

* fix confirm input

* type

* Add localhost portforward info

* add gitopssets controller

* enable pipelines controller and cluster-controller and gitopssets by default

* remove gitops sets

* handle existing secrets

* Update onboarding CLI messages

* cleanup utils

* cleanup utils

* cleanup domain

* cleanup utils

* cleanup input

* cleanup input

* edit messages and variableNames

* update cli messages

* cleanup variables

* cleanup constants

* cleanup constants

* add unit test for k8s utils and refactor accordingly

* add unit tests for flux

* add unit test for admin password

* Add unit -test for adding capi & policy-agent

* move controllers under gitops add

* add unit test for wge version

* test oidc get issuer

* Move controllers under gitops add

* Move controllers under gitops add

* pause add controllers tests

* restore install controller function after setup

* fix controllers

* pass opts to controllers

* refactor git utilits

* refactor git utilities

* prepare bootstrap bommand for release1

* address messages and languague

* clean extra variables

* remove oidc flow to another branch

* Update cmd/gitops/app/bootstrap/cmd.go

Co-authored-by: Eneko Fernández <[email protected]>

* Update cmd/gitops/app/bootstrap/cmd.go

Co-authored-by: Eneko Fernández <[email protected]>

* refactor git utility & add unit-test

* remove out of scope componenets

* apply code review comments to refactor file names and error messages

* move bootstrap package under pkg

* refactor using the k8s client and add silent mode

refactor git utils

* fix admin password

* verify username and password

* remove unused methods

* fix spelling

* remove aws related stuff

* use git library

add check for previous installation

* fix lint

* imporve error messages to stage failures

* adjust gitopssets values and installation checks

* clean silent mode

* add checks for kubeconfig

* adjust admin password

* refactor commands to use config interface struct

* improve error messages

* remove unused variable

* wip adding cli design doc

* wip adding cli design doc

* add error guidance

* rename config to bootstrapper to be more clear

* refactor commands to use steps pattern

* continue on refactor commands to use steps pattern

* fix ssh authenticate by explictly asking for private key file

in case the key is not loaded in ssh agent. this usually happens on macos

* added integration test, refactored configuration and design (#3458)

* Changes after the review to enhance the following aspects

Testing:

- Added integration test so we could test the functionality e2e: it uses some local configuration that we need to test but already provides the acceptance layer that we were missing:

Design:

- Bootstrap workflow moved to the domain layer within `pkg` so it could be presented in different forms.
- Integrated configuration chain of responsibility into a single a builder pattern, so we have configurability in this layer. As a result:
  - we dont need to pass the flags to the steps
  - we config the stepsbefore the workflow is executed which seems the right moment.

Other refactors:
- Moved steps to package `steps` from `command`  for consistency

* integrated ssh key management

* add events and error messages and fix domain bug

* add current context

* fix lint

* add entitlement expiration message

* fix entitlement expiration

* fix entitlement expiration

* seperate entitlement secret validations

* add messages for flux

* edit messages to small letter and update success messages

* add validation for password and wge version

* add test cases for admin password create creds

* add test cases for domain type

* update entitlement test

* add validation on password input

* remove aws related message

* panic in case of casting error to give more context about the error

* handle portforward and error messages

* fix external dns spacing

* add validation on domain type

* Cli eneko review (#3474)

* reviewed documentation

* updated docs with waleed input

* reviewed TBD

* latest set of changes

* removing commented

* remove debugging

* removed stale documentation

* removed withe space

* review

* removed unused

---------

Co-authored-by: Ahmad Samir <[email protected]>
Co-authored-by: Eneko Fernández <[email protected]>
Co-authored-by: Eneko Fernandez <[email protected]>
  • Loading branch information
4 people authored Oct 12, 2023
1 parent 6f30d56 commit c574e5f
Show file tree
Hide file tree
Showing 30 changed files with 3,119 additions and 10 deletions.
91 changes: 91 additions & 0 deletions cmd/gitops/app/bootstrap/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package bootstrap

import (
"fmt"
"os"

"github.com/spf13/cobra"
. "github.com/weaveworks/weave-gitops-enterprise/pkg/bootstrap"
"github.com/weaveworks/weave-gitops-enterprise/pkg/bootstrap/steps"
"github.com/weaveworks/weave-gitops/cmd/gitops/config"
"github.com/weaveworks/weave-gitops/pkg/logger"
)

const (
cmdName = "bootstrap"
cmdShortDescription = `gitops bootstrap installs Weave GitOps Enterprise in simple steps:
- Entitlements: check that you have valid entitlements.
- Flux: check or bootstrap Flux.
- Weave Gitops: check or install a supported Weave GitOps version with default configuration.
- Authentication: check or setup cluster user authentication to access the dashboard.
`
cmdExamples = `
# Start WGE installation from the current kubeconfig
gitops bootstrap
# Start WGE installation from a specific kubeconfig
gitops bootstrap --kubeconfig <your-kubeconfig-location>
# Start WGE installation with given 'username' and 'password'
gitops bootstrap --username wego-admin --password=hell0!
`
)

type bootstrapFlags struct {
username string
password string
version string
domainType string
domain string
privateKeyPath string
privateKeyPassword string
}

var flags bootstrapFlags

func Command(opts *config.Options) *cobra.Command {
cmd := &cobra.Command{
Use: cmdName,
Short: cmdShortDescription,
Example: cmdExamples,
RunE: getBootstrapCmdRun(opts),
}

cmd.Flags().StringVarP(&flags.username, "username", "u", "", "dashboard admin username")
cmd.Flags().StringVarP(&flags.password, "password", "p", "", "dashboard admin password")
cmd.Flags().StringVarP(&flags.version, "version", "v", "", "version of Weave GitOps Enterprise (should be from the latest 3 versions)")
cmd.Flags().StringVarP(&flags.domainType, "domain-type", "t", "", "dashboard domain type: could be 'localhost' or 'externaldns'")
cmd.Flags().StringVarP(&flags.domain, "domain", "d", "", "indicate the domain to use in case of using `externaldns`")
cmd.Flags().StringVarP(&flags.privateKeyPath, "private-key", "k", "", "private key path. This key will be used to push the Weave GitOps Enterprise's resources to the default cluster repository")
cmd.Flags().StringVarP(&flags.privateKeyPassword, "private-key-password", "c", "", "private key password. If the private key is encrypted using password")
return cmd
}

func getBootstrapCmdRun(opts *config.Options) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {

cliLogger := logger.NewCLILogger(os.Stdout)

// create config from flags
c, err := steps.NewConfigBuilder().
WithLogWriter(cliLogger).
WithKubeconfig(opts.Kubeconfig).
WithUsername(flags.username).
WithPassword(flags.password).
WithVersion(flags.version).
WithDomainType(flags.domainType).
WithDomain(flags.domain).
WithPrivateKey(flags.privateKeyPath, flags.privateKeyPassword).
Build()

if err != nil {
return fmt.Errorf("cannot config bootstrap: %v", err)
}

err = Bootstrap(c)
if err != nil {
return fmt.Errorf("cannot execute bootstrap: %v", err)
}
return nil
}
}
291 changes: 291 additions & 0 deletions cmd/gitops/app/bootstrap/cmd_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
//go:build integration
// +build integration

package bootstrap_test

import (
"context"
"fmt"
"os"
"sync"
"testing"
"time"

"github.com/go-logr/logr"
"github.com/go-logr/logr/testr"
"github.com/stretchr/testify/require"
"github.com/weaveworks/weave-gitops-enterprise/cmd/gitops/app/root"
"github.com/weaveworks/weave-gitops-enterprise/cmd/gitops/pkg/adapters"
"github.com/weaveworks/weave-gitops/pkg/runner"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

. "github.com/onsi/gomega"
)

const (
defaultTimeout = time.Second * 5
defaultInterval = time.Second
)

var fluxSystemNamespace = corev1.Namespace{
TypeMeta: metav1.TypeMeta{
Kind: "Namespace",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "flux-system",
},
}

func createEntitlementSecretFromEnv(t *testing.T) corev1.Secret {

username := os.Getenv("WGE_ENTITLEMENT_USERNAME")
require.NotEmpty(t, username)
password := os.Getenv("WGE_ENTITLEMENT_PASSWORD")
require.NotEmpty(t, password)
entitlement := os.Getenv("WGE_ENTITLEMENT_ENTITLEMENT")
require.NotEmpty(t, entitlement)

return corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "weave-gitops-enterprise-credentials",
Namespace: "flux-system",
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
"username": []byte(username),
"password": []byte(password),
"entitlement": []byte(entitlement),
},
}
}

// TestBootstrapCmd is an integration test for bootstrapping command.
// It uses envtest to simulate a cluster.
func TestBootstrapCmd(t *testing.T) {
g := NewGomegaWithT(t)
g.SetDefaultEventuallyTimeout(defaultTimeout)
g.SetDefaultEventuallyPollingInterval(defaultInterval)
testLog := testr.New(t)

lock := sync.Mutex{}

privateKeyFile := os.Getenv("GIT_PRIVATEKEY_PATH")
g.Expect(privateKeyFile).NotTo(BeEmpty())
privateKeyArg := fmt.Sprintf("--private-key=%s", privateKeyFile)

// ensure flux-system ns exists
_ = k8sClient.Create(context.Background(), &fluxSystemNamespace)

tests := []struct {
name string
flags []string
expectedErrorStr string
setup func(t *testing.T)
reset func(t *testing.T)
}{
{
name: "should fail without flux bootstrapped",
flags: []string{},
expectedErrorStr: "please bootstrap Flux in `flux-system` namespace: more info https://fluxcd.io/flux/installation",
},
{
name: "should fail without entitlements",
flags: []string{},
setup: func(t *testing.T) {
bootstrapFluxSsh(g)
},
reset: func(t *testing.T) {
uninstallFlux(g)
},

expectedErrorStr: "entitlement file is not found",
},
{
name: "should fail without private key",
flags: []string{},
setup: func(t *testing.T) {
bootstrapFluxSsh(g)
createEntitlements(t, testLog)
},
reset: func(t *testing.T) {
deleteEntitlements(t, testLog)
uninstallFlux(g)
},
expectedErrorStr: "cannot process input 'private key path and password",
},
{
name: "should fail without selected wge version",
flags: []string{
privateKeyArg,
"--private-key-password=\"\"",
},
setup: func(t *testing.T) {
bootstrapFluxSsh(g)
createEntitlements(t, testLog)
},
reset: func(t *testing.T) {
deleteEntitlements(t, testLog)
uninstallFlux(g)
},
expectedErrorStr: "cannot process input 'select WGE version'",
},
{
name: "should fail without user authentication",
flags: []string{"--version=0.33.0",
privateKeyArg,
"--private-key-password=\"\"",
},
setup: func(t *testing.T) {
bootstrapFluxSsh(g)
createEntitlements(t, testLog)
},
reset: func(t *testing.T) {
deleteEntitlements(t, testLog)
uninstallFlux(g)
},
expectedErrorStr: "cannot process input 'user authentication'",
},
{
name: "should fail without dashboard access",
flags: []string{"--version=0.33.0",
privateKeyArg,
"--private-key-password=\"\"",
"--username=admin",
"--password=admin123"},
setup: func(t *testing.T) {
bootstrapFluxSsh(g)
createEntitlements(t, testLog)
},
reset: func(t *testing.T) {
deleteClusterUser(t, testLog)
deleteEntitlements(t, testLog)
uninstallFlux(g)
},
expectedErrorStr: "cannot process input 'dashboard access'",
},
}
for _, tt := range tests {
lock.Lock()
t.Run(tt.name, func(t *testing.T) {

defer lock.Unlock()

if tt.setup != nil {
tt.setup(t)
}

if tt.reset != nil {
defer tt.reset(t)
}

client := adapters.NewHTTPClient()
cmd := root.Command(client)
bootstrapCmdArgs := []string{"bootstrap"}
bootstrapCmdArgs = append(bootstrapCmdArgs, tt.flags...)
cmd.SetArgs(bootstrapCmdArgs)

err := cmd.Execute()
if tt.expectedErrorStr != "" {
g.Expect(err.Error()).To(ContainSubstring(tt.expectedErrorStr))
return
}
g.Expect(err).To(BeNil())

})
}
}

func bootstrapFluxSsh(g *WithT) {
var runner runner.CLIRunner

repoUrl := os.Getenv("GIT_URL_SSH")
g.Expect(repoUrl).NotTo(BeEmpty())
fmt.Println(repoUrl)

privateKeyFile := os.Getenv("GIT_PRIVATEKEY_PATH")
g.Expect(privateKeyFile).NotTo(BeEmpty())
fmt.Println(privateKeyFile)

args := []string{"bootstrap", "git", "-s", fmt.Sprintf("--url=%s", repoUrl), fmt.Sprintf("--private-key-file=%s", privateKeyFile), "--path=clusters/management"}
fmt.Println(args)

s, err := runner.Run("flux", args...)
fmt.Println(string(s))
g.Expect(err).To(BeNil())

}

func uninstallFlux(g *WithT) {
var runner runner.CLIRunner
args := []string{"uninstall", "-s", "--keep-namespace"}
_, err := runner.Run("flux", args...)
g.Expect(err).To(BeNil())
}

func createEntitlements(t *testing.T, testLog logr.Logger) {
secret := createEntitlementSecretFromEnv(t)
objects := []client.Object{
&secret,
}
createResources(testLog, t, k8sClient, objects...)
}

func deleteEntitlements(t *testing.T, testLog logr.Logger) {
secret := createEntitlementSecretFromEnv(t)
objects := []client.Object{
&secret,
}
deleteResources(testLog, t, k8sClient, objects...)
}

func deleteClusterUser(t *testing.T, testLog logr.Logger) {
secret := corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
Type: corev1.SecretTypeOpaque,
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-user-auth",
Namespace: "flux-system",
},
Data: map[string][]byte{},
}

objects := []client.Object{
&secret,
}
deleteResources(testLog, t, k8sClient, objects...)
}

func createResources(log logr.Logger, t *testing.T, k client.Client, objects ...client.Object) {
ctx := context.Background()
t.Helper()
for _, o := range objects {
err := k.Create(ctx, o)
if err != nil {
t.Errorf("failed to create object: %s", err)
}
log.Info("created object", "name", o.GetName(), "ns", o.GetNamespace(), "kind", o.GetObjectKind().GroupVersionKind().Kind)
}
}

func deleteResources(log logr.Logger, t *testing.T, k client.Client, objects ...client.Object) {
ctx := context.Background()
t.Helper()
for _, o := range objects {
err := k.Delete(ctx, o)
if err != nil {
t.Logf("failed to cleanup object: %s", err)
}
log.Info("deleted object", "name", o.GetName(), "ns", o.GetNamespace(), "kind", o.GetObjectKind().GroupVersionKind().Kind)

}
}
Loading

0 comments on commit c574e5f

Please sign in to comment.