diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml deleted file mode 100644 index c60baa82..00000000 --- a/.github/workflows/e2e.yaml +++ /dev/null @@ -1,87 +0,0 @@ -name: E2E -on: - pull_request: - branches: - - main -jobs: - create-cluster-capi: - name: Create cluster with Cluster API - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Install GO - uses: actions/setup-go@v4 - with: - go-version-file: go.mod - cache: false - - name: Create kind cluster - uses: helm/kind-action@v1.9.0 - with: - install_only: true - - run: | - wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg - echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list - sudo apt-get update - sudo apt-get install -y terraform - - run: | - GOBIN="$HOME"/bin make build-cli - chmod 755 plural.o - mv plural.o /usr/local/bin/plural - - run: hack/e2e/kind-install-for-capd.sh - - run: hack/e2e/setup-plural.sh - env: - CLI_E2E_CONF: ${{ secrets.CLI_E2E_CONF }} - CLI_E2E_IDENTITY_FILE: ${{ secrets.CLI_E2E_IDENTITY_FILE }} - CLI_E2E_KEY_FILE: ${{ secrets.CLI_E2E_KEY_FILE }} - CLI_E2E_PUBLIC_KEY: ${{ secrets.CLI_E2E_PUBLIC_KEY }} - CLI_E2E_PRIVATE_KEY: ${{ secrets.CLI_E2E_PRIVATE_KEY }} - CLI_E2E_SHARING_PRIVATE_KEY: ${{ secrets.CLI_E2E_SHARING_PRIVATE_KEY }} - CLI_E2E_SHARING_PUBLIC_KEY: ${{ secrets.CLI_E2E_SHARING_PUBLIC_KEY }} - USE_CLUSTER_API: true - - run: go test -v -race ./pkg/test/e2eclusterapi/... -tags="e2e" - - run: | - cd $HOME/test - plural destroy --force --all --commit="" - env: - PLURAL_DESTROY_CONFIRM: true - PLURAL_DESTROY_AFFIRM_UNINSTALL_APPS: true - PLURAL_DISABLE_MP_TABLE_VIEW: true - create-cluster: - if: false - name: Create cluster - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Create kind cluster - uses: helm/kind-action@v1.8.0 - with: - install_only: true - - run: | - wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg - echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list - sudo apt-get update - sudo apt-get install -y terraform - - run: | - GOBIN="$HOME"/bin make build-cli - chmod 755 plural.o - mv plural.o /usr/local/bin/plural - - run: hack/e2e/setup-plural.sh - env: - CLI_E2E_CONF: ${{ secrets.CLI_E2E_CONF }} - CLI_E2E_IDENTITY_FILE: ${{ secrets.CLI_E2E_IDENTITY_FILE }} - CLI_E2E_KEY_FILE: ${{ secrets.CLI_E2E_KEY_FILE }} - CLI_E2E_PUBLIC_KEY: ${{ secrets.CLI_E2E_PUBLIC_KEY }} - CLI_E2E_PRIVATE_KEY: ${{ secrets.CLI_E2E_PRIVATE_KEY }} - CLI_E2E_SHARING_PRIVATE_KEY: ${{ secrets.CLI_E2E_SHARING_PRIVATE_KEY }} - CLI_E2E_SHARING_PUBLIC_KEY: ${{ secrets.CLI_E2E_SHARING_PUBLIC_KEY }} - INSTALL_APP: console - INSTALL_RECIPE: console-kind - - run: go test -v -race ./pkg/test/e2e/... -tags="e2e" - - run: | - cd $HOME/test - plural destroy --force --all --commit="" - env: - PLURAL_DESTROY_CONFIRM: true - PLURAL_DESTROY_AFFIRM_UNINSTALL_APPS: true \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 3fb3010f..fc77b0e6 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -11,6 +11,7 @@ before: builds: - id: plural-cli + main: ./cmd/plural targets: - linux_amd64 - linux_arm64 diff --git a/Dockerfile b/Dockerfile index 063eeba3..97dd53ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,6 @@ COPY go.sum go.sum RUN go mod download # Copy the go source -COPY main.go main.go COPY cmd/ cmd/ COPY pkg/ pkg/ @@ -30,7 +29,7 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} \ -X "github.com/pluralsh/plural-cli/cmd/plural.Version=${APP_VSN}" \ -X "github.com/pluralsh/plural-cli/cmd/plural.Commit=${APP_COMMIT}" \ -X "github.com/pluralsh/plural-cli/cmd/plural.Date=${APP_DATE}"' \ - -o plural . + -o plural ./cmd/plural FROM golang:1.20-alpine3.17 diff --git a/Makefile b/Makefile index f47cae42..f12d90aa 100644 --- a/Makefile +++ b/Makefile @@ -39,15 +39,15 @@ git-push: .PHONY: install install: - go build -ldflags '$(LDFLAGS)' -o $(GOBIN)/plural . + go build -ldflags '$(LDFLAGS)' -o $(GOBIN)/plural ./cmd/plural .PHONY: build-cli build-cli: ## Build a CLI binary for the host architecture without embedded UI - go build -ldflags='$(LDFLAGS)' -o $(OUTFILE) . + go build -ldflags='$(LDFLAGS)' -o $(OUTFILE) ./cmd/plural .PHONY: build-cli-ui build-cli-ui: $(PRE) generate-bindings ## Build a CLI binary for the host architecture with embedded UI - CGO_LDFLAGS=$(CGO_LDFLAGS) go build -tags $(WAILS_TAGS) -ldflags='$(LDFLAGS)' -o $(OUTFILE) . + CGO_LDFLAGS=$(CGO_LDFLAGS) go build -tags $(WAILS_TAGS) -ldflags='$(LDFLAGS)' -o $(OUTFILE) ./cmd/plural .PHONY: build-web build-web: ## Build just the embedded UI @@ -70,7 +70,7 @@ generate-bindings: build-web ## Generate backend bindings for the embedded UI .PHONY: release release: - GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags='$(LDFLAGS)' -o $(OUTFILE) . + GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags='$(LDFLAGS)' -o $(OUTFILE) ./cmd/plural .PHONY: setup setup: ## sets up your local env (for mac only) @@ -159,7 +159,7 @@ setup-tests: .PHONY: test test: setup-tests - gotestsum --format testname -- -v -race ./pkg/... ./cmd/... + gotestsum --format testname -- -v -race ./pkg/... ./cmd/command/... .PHONY: format format: # formats all go code to prep for linting diff --git a/cmd/plural/help.go b/cmd/command/ai/help.go similarity index 74% rename from cmd/plural/help.go rename to cmd/command/ai/help.go index 128989cc..608f9671 100644 --- a/cmd/plural/help.go +++ b/cmd/command/ai/help.go @@ -1,9 +1,11 @@ -package plural +package ai import ( "fmt" "time" + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/briandowns/spinner" "github.com/fatih/color" "github.com/pluralsh/plural-cli/pkg/api" @@ -13,6 +15,22 @@ import ( const intro = "What can we do to help you with Plural, using open source, or kubernetes?" +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "ai", + Usage: "utilize openai to get help with your setup", + Action: p.aiHelp, + Category: "Debugging", + } +} + func (p *Plural) aiHelp(c *cli.Context) error { p.InitPluralClient() chat := []*api.ChatMessage{{Role: "system", Content: intro}} diff --git a/cmd/plural/api.go b/cmd/command/api/api.go similarity index 82% rename from cmd/plural/api.go rename to cmd/command/api/api.go index 9c12bfa6..9e15a98b 100644 --- a/cmd/plural/api.go +++ b/cmd/command/api/api.go @@ -1,13 +1,30 @@ -package plural +package api import ( - "github.com/pluralsh/polly/algorithms" - "github.com/urfave/cli" - "github.com/pluralsh/plural-cli/pkg/api" + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" "github.com/pluralsh/plural-cli/pkg/utils" + "github.com/pluralsh/polly/algorithms" + "github.com/urfave/cli" ) +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + plural := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "api", + Usage: "inspect the plural api", + Subcommands: plural.apiCommands(), + Category: "API", + } +} + func (p *Plural) apiCommands() []cli.Command { return []cli.Command{ { @@ -18,45 +35,45 @@ func (p *Plural) apiCommands() []cli.Command { Name: "installations", Usage: "lists your installations", ArgsUsage: "", - Action: latestVersion(p.handleInstallations), + Action: common.LatestVersion(p.handleInstallations), }, { Name: "charts", Usage: "lists charts for a repository", ArgsUsage: "REPO_ID", - Action: latestVersion(requireArgs(p.handleCharts, []string{"REPO_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleCharts, []string{"REPO_ID"})), }, { Name: "terraform", Usage: "lists terraform modules for a repository", ArgsUsage: "REPO_ID", - Action: latestVersion(requireArgs(p.handleTerraforma, []string{"REPO_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleTerraforma, []string{"REPO_ID"})), }, { Name: "versions", Usage: "lists versions of a chart", ArgsUsage: "CHART_ID", - Action: latestVersion(requireArgs(p.handleVersions, []string{"CHART_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleVersions, []string{"CHART_ID"})), }, { Name: "chartinstallations", Aliases: []string{"ci"}, Usage: "lists chart installations for a repository", ArgsUsage: "REPO_ID", - Action: latestVersion(requireArgs(p.handleChartInstallations, []string{"REPO_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleChartInstallations, []string{"REPO_ID"})), }, { Name: "terraforminstallations", Aliases: []string{"ti"}, Usage: "lists terraform installations for a repository", ArgsUsage: "REPO_ID", - Action: latestVersion(requireArgs(p.handleTerraformInstallations, []string{"REPO_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleTerraformInstallations, []string{"REPO_ID"})), }, { Name: "artifacts", Usage: "Lists artifacts for a repository", ArgsUsage: "REPO_ID", - Action: latestVersion(requireArgs(p.handleArtifacts, []string{"REPO_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleArtifacts, []string{"REPO_ID"})), }, }, }, @@ -68,7 +85,7 @@ func (p *Plural) apiCommands() []cli.Command { Name: "domain", Usage: "creates a new domain for your account", ArgsUsage: "DOMAIN", - Action: latestVersion(p.handleCreateDomain), + Action: common.LatestVersion(p.handleCreateDomain), }, }, }, diff --git a/cmd/plural/api_test.go b/cmd/command/api/api_test.go similarity index 90% rename from cmd/plural/api_test.go rename to cmd/command/api/api_test.go index 1e0b61ee..d83750a7 100644 --- a/cmd/plural/api_test.go +++ b/cmd/command/api/api_test.go @@ -1,15 +1,16 @@ -package plural_test +package api_test import ( "os" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - - "github.com/pluralsh/plural-cli/cmd/plural" + "github.com/pluralsh/plural-cli/cmd/command/plural" "github.com/pluralsh/plural-cli/pkg/api" + pluralclient "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" "github.com/pluralsh/plural-cli/pkg/test/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func TestListArtifacts(t *testing.T) { @@ -50,10 +51,12 @@ func TestListArtifacts(t *testing.T) { if test.expectedError == "" { client.On("ListArtifacts", mock.AnythingOfType("string")).Return(test.artifacts, nil) } - app := plural.CreateNewApp(&plural.Plural{Client: client}) + app := plural.CreateNewApp(&plural.Plural{Plural: pluralclient.Plural{ + Client: client, + }}) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) if test.expectedError != "" { assert.Equal(t, err.Error(), test.expectedError) } else { @@ -113,10 +116,12 @@ func TestGetInstallations(t *testing.T) { t.Run(test.name, func(t *testing.T) { client := mocks.NewClient(t) client.On("GetInstallations").Return(test.installations, nil) - app := plural.CreateNewApp(&plural.Plural{Client: client}) + app := plural.CreateNewApp(&plural.Plural{Plural: pluralclient.Plural{ + Client: client, + }}) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) assert.NoError(t, err) assert.Equal(t, test.expectedResponse, res) @@ -161,10 +166,12 @@ func TestGetCharts(t *testing.T) { if test.expectedError == "" { client.On("GetCharts", mock.AnythingOfType("string")).Return(test.charts, nil) } - app := plural.CreateNewApp(&plural.Plural{Client: client}) + app := plural.CreateNewApp(&plural.Plural{Plural: pluralclient.Plural{ + Client: client, + }}) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) if test.expectedError != "" { assert.Equal(t, err.Error(), test.expectedError) } else { @@ -220,10 +227,12 @@ func TestGetTerraform(t *testing.T) { client.On("GetTerraform", mock.AnythingOfType("string")).Return(test.terraform, nil) } - app := plural.CreateNewApp(&plural.Plural{Client: client}) + app := plural.CreateNewApp(&plural.Plural{Plural: pluralclient.Plural{ + Client: client, + }}) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) if test.expectedError != "" { assert.Equal(t, err.Error(), test.expectedError) } else { @@ -276,10 +285,12 @@ func TestGetVersons(t *testing.T) { if test.expectedError == "" { client.On("GetVersions", mock.AnythingOfType("string")).Return(test.versions, nil) } - app := plural.CreateNewApp(&plural.Plural{Client: client}) + app := plural.CreateNewApp(&plural.Plural{Plural: pluralclient.Plural{ + Client: client, + }}) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) if test.expectedError != "" { assert.Equal(t, err.Error(), test.expectedError) } else { @@ -344,10 +355,12 @@ func TestGetChartInstallations(t *testing.T) { if test.expectedError == "" { client.On("GetChartInstallations", mock.AnythingOfType("string")).Return(test.chartInstallations, nil) } - app := plural.CreateNewApp(&plural.Plural{Client: client}) + app := plural.CreateNewApp(&plural.Plural{Plural: pluralclient.Plural{ + Client: client, + }}) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) if test.expectedError != "" { assert.Equal(t, err.Error(), test.expectedError) } else { @@ -406,10 +419,12 @@ func TestGetTerraformInstallations(t *testing.T) { if test.expectedError == "" { client.On("GetTerraformInstallations", mock.AnythingOfType("string")).Return(test.terraformInstallations, nil) } - app := plural.CreateNewApp(&plural.Plural{Client: client}) + app := plural.CreateNewApp(&plural.Plural{Plural: pluralclient.Plural{ + Client: client, + }}) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) if test.expectedError != "" { assert.Equal(t, err.Error(), test.expectedError) } else { diff --git a/cmd/plural/auth.go b/cmd/command/auth/auth.go similarity index 89% rename from cmd/plural/auth.go rename to cmd/command/auth/auth.go index 215c1941..d74bbc1f 100644 --- a/cmd/plural/auth.go +++ b/cmd/command/auth/auth.go @@ -1,16 +1,31 @@ -package plural +package auth import ( "fmt" "strings" "github.com/pluralsh/gqlclient" - "github.com/urfave/cli" - + "github.com/pluralsh/plural-cli/pkg/client" "github.com/pluralsh/plural-cli/pkg/config" "github.com/pluralsh/plural-cli/pkg/utils" + "github.com/urfave/cli" ) +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "auth", + Usage: "Handles authentication to the plural api", + Subcommands: p.authCommands(), + } +} + func (p *Plural) authCommands() []cli.Command { return []cli.Command{ { diff --git a/cmd/plural/bootstrap.go b/cmd/command/bootstrap/bootstrap.go similarity index 90% rename from cmd/plural/bootstrap.go rename to cmd/command/bootstrap/bootstrap.go index c4cfb4d6..47df494d 100644 --- a/cmd/plural/bootstrap.go +++ b/cmd/command/bootstrap/bootstrap.go @@ -1,4 +1,4 @@ -package plural +package bootstrap import ( "context" @@ -8,6 +8,10 @@ import ( "strings" "time" + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/pluralsh/plural-cli/pkg/exp" + "github.com/pluralsh/plural-cli/pkg/kubernetes" "github.com/pluralsh/plural-cli/pkg/manifest" "github.com/pluralsh/plural-cli/pkg/provider" @@ -49,6 +53,23 @@ nodes: containerPath: /var/run/docker.sock` ) +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "bootstrap", + Usage: "Commands for bootstrapping cluster", + Subcommands: p.bootstrapCommands(), + Category: "Bootstrap", + Hidden: !exp.IsFeatureEnabled(exp.EXP_PLURAL_CAPI), + } +} + func (p *Plural) bootstrapCommands() []cli.Command { return []cli.Command{ { @@ -76,7 +97,7 @@ func (p *Plural) namespaceCommands() []cli.Command { Usage: "skip creating when namespace exists", }, }, - Action: latestVersion(initKubeconfig(requireArgs(p.handleCreateNamespace, []string{"NAME"}))), + Action: common.LatestVersion(common.InitKubeconfig(common.RequireArgs(p.handleCreateNamespace, []string{"NAME"}))), }, } } @@ -97,13 +118,13 @@ func (p *Plural) bootstrapClusterCommands() []cli.Command { Usage: "skip creating when cluster exists", }, }, - Action: latestVersion(requireKind(requireArgs(handleCreateCluster, []string{"NAME"}))), + Action: common.LatestVersion(common.RequireKind(common.RequireArgs(handleCreateCluster, []string{"NAME"}))), }, { Name: "delete", ArgsUsage: "NAME", Usage: "Deletes bootstrap cluster", - Action: latestVersion(requireKind(requireArgs(handleDeleteCluster, []string{"NAME"}))), + Action: common.LatestVersion(common.RequireKind(common.RequireArgs(handleDeleteCluster, []string{"NAME"}))), }, { Name: "move", @@ -126,13 +147,13 @@ func (p *Plural) bootstrapClusterCommands() []cli.Command { Usage: "Context to be used within the kubeconfig file for the destination management cluster. If empty, current context will be used.", }, }, - Action: latestVersion(p.handleMoveCluster), + Action: common.LatestVersion(p.handleMoveCluster), }, { Name: "destroy-cluster-api", ArgsUsage: "NAME", Usage: "Destroy cluster API", - Action: latestVersion(requireArgs(p.handleDestroyClusterAPI, []string{"NAME"})), + Action: common.LatestVersion(common.RequireArgs(p.handleDestroyClusterAPI, []string{"NAME"})), }, } } diff --git a/cmd/command/bounce/bounce.go b/cmd/command/bounce/bounce.go new file mode 100644 index 00000000..32d9f0e4 --- /dev/null +++ b/cmd/command/bounce/bounce.go @@ -0,0 +1,77 @@ +package bounce + +import ( + "os" + "path/filepath" + + "github.com/pluralsh/plural-cli/pkg/utils/git" + "github.com/pluralsh/plural-cli/pkg/utils/pathing" + "github.com/pluralsh/plural-cli/pkg/wkspace" + + "github.com/pluralsh/plural-cli/pkg/api" + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/pluralsh/plural-cli/pkg/utils" + "github.com/urfave/cli" +) + +type Plural struct { + Plural client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "bounce", + Aliases: []string{"b"}, + Usage: "redeploys the charts in a workspace", + ArgsUsage: "APP", + Action: common.LatestVersion(common.InitKubeconfig(common.Owned(p.bounce))), + } +} + +func (p *Plural) bounce(c *cli.Context) error { + p.Plural.InitPluralClient() + repoRoot, err := git.Root() + if err != nil { + return err + } + repoName := c.Args().Get(0) + + if repoName != "" { + installation, err := p.Plural.GetInstallation(repoName) + if err != nil { + return api.GetErrorResponse(err, "GetInstallation") + } + return p.doBounce(repoRoot, installation) + } + + installations, err := client.GetSortedInstallations(p.Plural, repoName) + if err != nil { + return err + } + + for _, installation := range installations { + if err := p.doBounce(repoRoot, installation); err != nil { + return err + } + } + return nil +} + +func (p *Plural) doBounce(repoRoot string, installation *api.Installation) error { + p.Plural.InitPluralClient() + repoName := installation.Repository.Name + utils.Warn("bouncing deployments in %s\n", repoName) + workspace, err := wkspace.New(p.Plural.Client, installation) + if err != nil { + return err + } + + if err := os.Chdir(pathing.SanitizeFilepath(filepath.Join(repoRoot, repoName))); err != nil { + return err + } + return workspace.Bounce() +} diff --git a/cmd/command/buildcmd/build.go b/cmd/command/buildcmd/build.go new file mode 100644 index 00000000..72e1c29d --- /dev/null +++ b/cmd/command/buildcmd/build.go @@ -0,0 +1,70 @@ +package buildcmd + +import ( + "fmt" + + "github.com/pluralsh/plural-cli/cmd/command/crypto" + "github.com/pluralsh/plural-cli/pkg/api" + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/pluralsh/plural-cli/pkg/utils" + "github.com/pluralsh/plural-cli/pkg/utils/errors" + "github.com/urfave/cli" +) + +type Plural struct { + Plural client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "build", + Aliases: []string{"bld"}, + Usage: "builds your workspace", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "only", + Usage: "repository to (re)build", + }, + cli.BoolFlag{ + Name: "force", + Usage: "force workspace to build even if remote is out of sync", + }, + }, + Action: common.Tracked(common.Rooted(common.LatestVersion(common.Owned(common.UpstreamSynced(p.build)))), "cli.build"), + } +} + +func (p *Plural) build(c *cli.Context) error { + p.Plural.InitPluralClient() + force := c.Bool("force") + if err := crypto.CheckGitCrypt(c); err != nil { + return errors.ErrorWrap(common.ErrNoGit, "Failed to scan your repo for secrets to encrypt them") + } + + if c.IsSet("only") { + installation, err := p.Plural.GetInstallation(c.String("only")) + if err != nil { + return api.GetErrorResponse(err, "GetInstallation") + } else if installation == nil { + return utils.HighlightError(fmt.Errorf("%s is not installed. Please install it with `plural bundle install`", c.String("only"))) + } + + return common.DoBuild(p.Plural.Client, installation, force) + } + + installations, err := client.GetSortedInstallations(p.Plural, "") + if err != nil { + return err + } + + for _, installation := range installations { + if err := common.DoBuild(p.Plural.Client, installation, force); err != nil { + return err + } + } + return nil +} diff --git a/cmd/plural/bundle.go b/cmd/command/bundle/bundle.go similarity index 57% rename from cmd/plural/bundle.go rename to cmd/command/bundle/bundle.go index 516c0062..463a3099 100644 --- a/cmd/plural/bundle.go +++ b/cmd/command/bundle/bundle.go @@ -1,8 +1,10 @@ -package plural +package bundle import ( "fmt" + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" "github.com/urfave/cli" "github.com/pluralsh/plural-cli/pkg/api" @@ -11,13 +13,28 @@ import ( "github.com/pluralsh/plural-cli/pkg/utils" ) +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "bundle", + Usage: "Commands for installing and discovering installation bundles", + Subcommands: p.bundleCommands(), + } +} + func (p *Plural) bundleCommands() []cli.Command { return []cli.Command{ { Name: "list", Usage: "lists bundles for a repository", ArgsUsage: "REPO", - Action: latestVersion(rooted(requireArgs(p.bundleList, []string{"repo"}))), + Action: common.LatestVersion(common.Rooted(common.RequireArgs(p.bundleList, []string{"repo"}))), }, { Name: "install", @@ -29,35 +46,7 @@ func (p *Plural) bundleCommands() []cli.Command { Usage: "re-enter the configuration for this bundle", }, }, - Action: tracked(latestVersion(rooted(p.bundleInstall)), "bundle.install"), - }, - } -} - -func (p *Plural) stackCommands() []cli.Command { - return []cli.Command{ - { - Name: "install", - Usage: "installs a plural stack for your current provider", - ArgsUsage: "NAME", - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "refresh", - Usage: "re-enter the configuration for all bundles", - }, - }, - Action: tracked(latestVersion(rooted(requireArgs(p.stackInstall, []string{"stack-name"}))), "stack.install"), - }, - { - Name: "list", - Usage: "lists stacks to potentially install", - Flags: []cli.Flag{ - cli.BoolTFlag{ - Name: "account", - Usage: "only list stacks within your account", - }, - }, - Action: latestVersion(rooted(p.stackList)), + Action: common.Tracked(common.LatestVersion(common.Rooted(p.bundleInstall)), "bundle.install"), }, } } @@ -107,32 +96,6 @@ func (p *Plural) bundleInstall(c *cli.Context) (err error) { return } -func (p *Plural) stackInstall(c *cli.Context) (err error) { - name := c.Args().Get(0) - man, err := manifest.FetchProject() - if err != nil { - return - } - - p.InitPluralClient() - err = bundle.Stack(p.Client, name, man.Provider, c.Bool("refresh")) - utils.Note("To edit the configuration you've just entered, edit the context.yaml file at the root of your repo, or run with the --refresh flag\n") - return -} - -func (p *Plural) stackList(c *cli.Context) (err error) { - p.InitPluralClient() - stacks, err := p.ListStacks(c.Bool("account")) - if err != nil { - return api.GetErrorResponse(err, "ListStacks") - } - - headers := []string{"Name", "Description", "Featured"} - return utils.PrintTable(stacks, headers, func(s *api.Stack) ([]string, error) { - return []string{s.Name, s.Description, fmt.Sprintf("%v", s.Featured)}, nil - }) -} - func (p *Plural) listRecipes(repo string) (res []*api.Recipe, err error) { man, err := manifest.FetchProject() if err != nil { diff --git a/cmd/plural/bundle_test.go b/cmd/command/bundle/bundle_test.go similarity index 87% rename from cmd/plural/bundle_test.go rename to cmd/command/bundle/bundle_test.go index cc0ffff5..80286781 100644 --- a/cmd/plural/bundle_test.go +++ b/cmd/command/bundle/bundle_test.go @@ -1,13 +1,16 @@ -package plural_test +package bundle_test import ( "os" "testing" + pluralclient "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "github.com/pluralsh/plural-cli/cmd/plural" + "github.com/pluralsh/plural-cli/cmd/command/plural" "github.com/pluralsh/plural-cli/pkg/api" "github.com/pluralsh/plural-cli/pkg/test/mocks" "github.com/pluralsh/plural-cli/pkg/utils/git" @@ -59,10 +62,10 @@ func TestBundleList(t *testing.T) { client := mocks.NewClient(t) client.On("ListRecipes", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(test.recipe, nil) - app := plural.CreateNewApp(&plural.Plural{Client: client}) + app := plural.CreateNewApp(&plural.Plural{Plural: pluralclient.Plural{Client: client}}) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) assert.NoError(t, err) assert.Equal(t, test.expectedResponse, res) @@ -85,10 +88,10 @@ func TestBundleInstallNoGitRootDirectory(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { client := mocks.NewClient(t) - app := plural.CreateNewApp(&plural.Plural{Client: client}) + app := plural.CreateNewApp(&plural.Plural{Plural: pluralclient.Plural{Client: client}}) app.HelpName = plural.ApplicationName os.Args = test.args - _, err := captureStdout(app, os.Args) + _, err := common.CaptureStdout(app, os.Args) assert.Error(t, err) assert.Equal(t, test.expectedResponse, err.Error()) @@ -146,10 +149,10 @@ func TestBundleInstall(t *testing.T) { client := mocks.NewClient(t) client.On("GetRecipe", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(test.recipe, nil) client.On("InstallRecipe", mock.AnythingOfType("string")).Return(nil) - app := plural.CreateNewApp(&plural.Plural{Client: client}) + app := plural.CreateNewApp(&plural.Plural{Plural: pluralclient.Plural{Client: client}}) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) assert.NoError(t, err) assert.Equal(t, test.expectedResponse, res) }) diff --git a/cmd/plural/cd.go b/cmd/command/cd/cd.go similarity index 84% rename from cmd/plural/cd.go rename to cmd/command/cd/cd.go index 02b2c6ca..c7c29e60 100644 --- a/cmd/plural/cd.go +++ b/cmd/command/cd/cd.go @@ -1,19 +1,21 @@ -package plural +package cd import ( "fmt" "os" "strings" - "github.com/pluralsh/polly/algorithms" - "github.com/urfave/cli" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "sigs.k8s.io/yaml" - "github.com/pluralsh/plural-cli/pkg/cd" + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" "github.com/pluralsh/plural-cli/pkg/config" "github.com/pluralsh/plural-cli/pkg/console" "github.com/pluralsh/plural-cli/pkg/utils" + "github.com/pluralsh/polly/algorithms" + "github.com/urfave/cli" + "helm.sh/helm/v3/pkg/action" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/yaml" ) func init() { @@ -24,6 +26,39 @@ func init() { var consoleToken string var consoleURL string +type Plural struct { + client.Plural + HelmConfiguration *action.Configuration +} + +func Command(clients client.Plural, helmConfiguration *action.Configuration) cli.Command { + plural := Plural{ + Plural: clients, + HelmConfiguration: helmConfiguration, + } + return cli.Command{ + Name: "deployments", + Aliases: []string{"cd"}, + Usage: "view and manage plural deployments", + Subcommands: plural.cdCommands(), + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "token", + Usage: "console token", + EnvVar: "PLURAL_CONSOLE_TOKEN", + Destination: &consoleToken, + }, + cli.StringFlag{ + Name: "url", + Usage: "console url address", + EnvVar: "PLURAL_CONSOLE_URL", + Destination: &consoleURL, + }, + }, + Category: "CD", + } +} + func (p *Plural) cdCommands() []cli.Command { return []cli.Command{ p.cdProviders(), @@ -123,7 +158,7 @@ func (p *Plural) doInstallOperator(url, token, values string) error { if err != nil { return err } - if alreadyExists && !confirm("the deployment operator is already installed. Do you want to replace it", "PLURAL_INSTALL_AGENT_CONFIRM_IF_EXISTS") { + if alreadyExists && !common.Confirm("the deployment operator is already installed. Do you want to replace it", "PLURAL_INSTALL_AGENT_CONFIRM_IF_EXISTS") { utils.Success("deployment operator is already installed, skip installation\n") return nil } @@ -186,7 +221,7 @@ func confirmCluster(url, token string) (bool, error) { if clusterFragment.Provider != nil { provider = clusterFragment.Provider.Name } - return confirm(fmt.Sprintf("Are you sure you want to install deploy operator for the cluster:\nName: %s\nHandle: %s\nProvider: %s\n", myCluster.MyCluster.Name, handle, provider), "PLURAL_INSTALL_AGENT_CONFIRM"), nil + return common.Confirm(fmt.Sprintf("Are you sure you want to install deploy operator for the cluster:\nName: %s\nHandle: %s\nProvider: %s\n", myCluster.MyCluster.Name, handle, provider), "PLURAL_INSTALL_AGENT_CONFIRM"), nil } func (p *Plural) handleCdLogin(c *cli.Context) (err error) { diff --git a/cmd/plural/cd_clusters.go b/cmd/command/cd/cd_clusters.go similarity index 93% rename from cmd/plural/cd_clusters.go rename to cmd/command/cd/cd_clusters.go index 1f77e0c3..b06a738b 100644 --- a/cmd/plural/cd_clusters.go +++ b/cmd/command/cd/cd_clusters.go @@ -1,4 +1,4 @@ -package plural +package cd import ( "fmt" @@ -7,6 +7,7 @@ import ( "github.com/AlecAivazis/survey/v2" gqlclient "github.com/pluralsh/console/go/client" "github.com/pluralsh/plural-cli/pkg/cd" + "github.com/pluralsh/plural-cli/pkg/common" "github.com/pluralsh/plural-cli/pkg/console" "github.com/pluralsh/plural-cli/pkg/console/errors" "github.com/pluralsh/plural-cli/pkg/kubernetes/config" @@ -39,12 +40,12 @@ func (p *Plural) cdClusterCommands() []cli.Command { return []cli.Command{ { Name: "list", - Action: latestVersion(p.handleListClusters), + Action: common.LatestVersion(p.handleListClusters), Usage: "list clusters", }, { Name: "describe", - Action: latestVersion(requireArgs(p.handleDescribeCluster, []string{"CLUSTER_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleDescribeCluster, []string{"CLUSTER_ID"})), Usage: "describe cluster", ArgsUsage: "CLUSTER_ID", Flags: []cli.Flag{ @@ -53,7 +54,7 @@ func (p *Plural) cdClusterCommands() []cli.Command { }, { Name: "update", - Action: latestVersion(requireArgs(p.handleUpdateCluster, []string{"CLUSTER_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleUpdateCluster, []string{"CLUSTER_ID"})), Usage: "update cluster", ArgsUsage: "CLUSTER_ID", Flags: []cli.Flag{ @@ -64,7 +65,7 @@ func (p *Plural) cdClusterCommands() []cli.Command { }, { Name: "delete", - Action: latestVersion(requireArgs(p.handleDeleteCluster, []string{"CLUSTER_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleDeleteCluster, []string{"CLUSTER_ID"})), Usage: "deregisters a cluster in plural cd, and drains all services (unless --soft is specified)", ArgsUsage: "CLUSTER_ID", Flags: []cli.Flag{ @@ -77,13 +78,13 @@ func (p *Plural) cdClusterCommands() []cli.Command { { Name: "get-credentials", Aliases: []string{"kubeconfig"}, - Action: latestVersion(requireArgs(p.handleGetClusterCredentials, []string{"CLUSTER_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleGetClusterCredentials, []string{"CLUSTER_ID"})), Usage: "updates kubeconfig file with appropriate credentials to point to specified cluster", ArgsUsage: "CLUSTER_ID", }, { Name: "create", - Action: latestVersion(requireArgs(p.handleCreateCluster, []string{"CLUSTER_NAME"})), + Action: common.LatestVersion(common.RequireArgs(p.handleCreateCluster, []string{"CLUSTER_NAME"})), Usage: "create cluster", ArgsUsage: "CLUSTER_NAME", Flags: []cli.Flag{ @@ -93,7 +94,7 @@ func (p *Plural) cdClusterCommands() []cli.Command { }, { Name: "bootstrap", - Action: latestVersion(p.handleClusterBootstrap), + Action: common.LatestVersion(p.handleClusterBootstrap), Usage: "creates a new BYOK cluster and installs the agent onto it using the current kubeconfig", Flags: []cli.Flag{ cli.StringFlag{Name: "name", Usage: "The name you'll give the cluster", Required: true}, @@ -108,7 +109,7 @@ func (p *Plural) cdClusterCommands() []cli.Command { }, { Name: "reinstall", - Action: latestVersion(p.handleClusterReinstall), + Action: common.LatestVersion(p.handleClusterReinstall), Flags: []cli.Flag{ cli.StringFlag{Name: "values", Usage: "values file to use for the deployment agent helm chart", Required: false}, }, @@ -450,7 +451,7 @@ func (p *Plural) handleClusterBootstrap(c *cli.Context) error { existing, err := p.ConsoleClient.CreateCluster(attrs) if err != nil { - if errors.Like(err, "handle") && affirm("Do you want to reinstall the deployment operator?", "PLURAL_INSTALL_AGENT_CONFIRM_IF_EXISTS") { + if errors.Like(err, "handle") && common.Affirm("Do you want to reinstall the deployment operator?", "PLURAL_INSTALL_AGENT_CONFIRM_IF_EXISTS") { handle := lo.ToPtr(attrs.Name) if attrs.Handle != nil { handle = attrs.Handle diff --git a/cmd/plural/cd_contexts.go b/cmd/command/cd/cd_contexts.go similarity index 90% rename from cmd/plural/cd_contexts.go rename to cmd/command/cd/cd_contexts.go index f2faa79d..bdfa16c3 100644 --- a/cmd/plural/cd_contexts.go +++ b/cmd/command/cd/cd_contexts.go @@ -1,9 +1,11 @@ -package plural +package cd import ( "encoding/json" "fmt" + "github.com/pluralsh/plural-cli/pkg/common" + gqlclient "github.com/pluralsh/console/go/client" "github.com/pluralsh/plural-cli/pkg/console" "github.com/pluralsh/plural-cli/pkg/utils" @@ -27,13 +29,13 @@ func (p *Plural) cdServiceContextCommands() []cli.Command { cli.StringFlag{Name: "config-file", Usage: "path for json configuration file with the context blob", Required: true}, cli.StringFlag{Name: "name", Usage: "context name", Required: true}, }, - Action: latestVersion(requireArgs(p.handleUpsertServiceContext, []string{"NAME"})), + Action: common.LatestVersion(common.RequireArgs(p.handleUpsertServiceContext, []string{"NAME"})), Usage: "upsert service context", }, { Name: "get", ArgsUsage: "NAME", - Action: latestVersion(requireArgs(p.handleGetServiceContext, []string{"NAME"})), + Action: common.LatestVersion(common.RequireArgs(p.handleGetServiceContext, []string{"NAME"})), Usage: "get service context", }, } diff --git a/cmd/plural/cd_credentials.go b/cmd/command/cd/cd_credentials.go similarity index 87% rename from cmd/plural/cd_credentials.go rename to cmd/command/cd/cd_credentials.go index dc68c0a4..2ae8b193 100644 --- a/cmd/plural/cd_credentials.go +++ b/cmd/command/cd/cd_credentials.go @@ -1,8 +1,10 @@ -package plural +package cd import ( "fmt" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/pkg/errors" gqlclient "github.com/pluralsh/console/go/client" "github.com/pluralsh/plural-cli/pkg/utils" @@ -22,13 +24,13 @@ func (p *Plural) cdCredentialsCommands() []cli.Command { { Name: "create", ArgsUsage: "PROVIDER_NAME", - Action: latestVersion(requireArgs(p.handleCreateProviderCredentials, []string{"PROVIDER_NAME"})), + Action: common.LatestVersion(common.RequireArgs(p.handleCreateProviderCredentials, []string{"PROVIDER_NAME"})), Usage: "create provider credentials", }, { Name: "delete", ArgsUsage: "ID", - Action: latestVersion(requireArgs(p.handleDeleteProviderCredentials, []string{"ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleDeleteProviderCredentials, []string{"ID"})), Usage: "delete provider credentials", }, } diff --git a/cmd/plural/cd_notifications.go b/cmd/command/cd/cd_notifications.go similarity index 94% rename from cmd/plural/cd_notifications.go rename to cmd/command/cd/cd_notifications.go index bacceea0..ffcf54e8 100644 --- a/cmd/plural/cd_notifications.go +++ b/cmd/command/cd/cd_notifications.go @@ -1,8 +1,9 @@ -package plural +package cd import ( "github.com/AlecAivazis/survey/v2" consoleclient "github.com/pluralsh/console/go/client" + "github.com/pluralsh/plural-cli/pkg/common" "github.com/pluralsh/plural-cli/pkg/utils" "github.com/pluralsh/polly/algorithms" "github.com/urfave/cli" @@ -28,13 +29,13 @@ func (p *Plural) cdNotificationSinkCommands() []cli.Command { return []cli.Command{ { Name: "list", - Action: latestVersion(p.handleListNotificationSinks), + Action: common.LatestVersion(p.handleListNotificationSinks), Usage: "list notification sinks", }, { Name: "upsert", ArgsUsage: "NAME", - Action: latestVersion(requireArgs(p.handleCreateNotificationSinks, []string{"NAME"})), + Action: common.LatestVersion(common.RequireArgs(p.handleCreateNotificationSinks, []string{"NAME"})), Usage: "upsert notification sink", }, } diff --git a/cmd/plural/cd_pipelines.go b/cmd/command/cd/cd_pipelines.go similarity index 88% rename from cmd/plural/cd_pipelines.go rename to cmd/command/cd/cd_pipelines.go index 283919c4..45466232 100644 --- a/cmd/plural/cd_pipelines.go +++ b/cmd/command/cd/cd_pipelines.go @@ -1,9 +1,11 @@ -package plural +package cd import ( "io" "os" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/pluralsh/plural-cli/pkg/console" "github.com/pluralsh/plural-cli/pkg/utils" "github.com/urfave/cli" @@ -21,7 +23,7 @@ func (p *Plural) pipelineCommands() []cli.Command { return []cli.Command{ { Name: "create", - Action: latestVersion(requireArgs(p.handleCreatePipeline, []string{})), + Action: common.LatestVersion(common.RequireArgs(p.handleCreatePipeline, []string{})), Flags: []cli.Flag{ cli.StringFlag{ Name: "file", diff --git a/cmd/plural/cd_providers.go b/cmd/command/cd/cd_providers.go similarity index 97% rename from cmd/plural/cd_providers.go rename to cmd/command/cd/cd_providers.go index 13b499af..452b3175 100644 --- a/cmd/plural/cd_providers.go +++ b/cmd/command/cd/cd_providers.go @@ -1,10 +1,12 @@ -package plural +package cd import ( "fmt" "strconv" "strings" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/AlecAivazis/survey/v2" gqlclient "github.com/pluralsh/console/go/client" "github.com/urfave/cli" @@ -29,7 +31,7 @@ func (p *Plural) cdProvidersCommands() []cli.Command { return []cli.Command{ { Name: "list", - Action: latestVersion(p.handleListProviders), + Action: common.LatestVersion(p.handleListProviders), Usage: "list providers", }, } diff --git a/cmd/plural/cd_repositories.go b/cmd/command/cd/cd_repositories.go similarity index 92% rename from cmd/plural/cd_repositories.go rename to cmd/command/cd/cd_repositories.go index 162f893e..baec0d4c 100644 --- a/cmd/plural/cd_repositories.go +++ b/cmd/command/cd/cd_repositories.go @@ -1,8 +1,10 @@ -package plural +package cd import ( "fmt" + "github.com/pluralsh/plural-cli/pkg/common" + gqlclient "github.com/pluralsh/console/go/client" "github.com/pluralsh/plural-cli/pkg/utils" "github.com/samber/lo" @@ -21,12 +23,12 @@ func (p *Plural) cdRepositoriesCommands() []cli.Command { return []cli.Command{ { Name: "list", - Action: latestVersion(p.handleListCDRepositories), + Action: common.LatestVersion(p.handleListCDRepositories), Usage: "list repositories", }, { Name: "create", - Action: latestVersion(p.handleCreateCDRepository), + Action: common.LatestVersion(p.handleCreateCDRepository), Flags: []cli.Flag{ cli.StringFlag{Name: "url", Usage: "git repo url", Required: true}, cli.StringFlag{Name: "private-key", Usage: "git repo private key"}, @@ -39,7 +41,7 @@ func (p *Plural) cdRepositoriesCommands() []cli.Command { { Name: "update", ArgsUsage: "REPO_ID", - Action: latestVersion(requireArgs(p.handleUpdateCDRepository, []string{"REPO_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleUpdateCDRepository, []string{"REPO_ID"})), Flags: []cli.Flag{ cli.StringFlag{Name: "url", Usage: "git repo url", Required: true}, cli.StringFlag{Name: "private-key", Usage: "git repo private key"}, diff --git a/cmd/plural/cd_services.go b/cmd/command/cd/cd_services.go similarity index 95% rename from cmd/plural/cd_services.go rename to cmd/command/cd/cd_services.go index 1067ff74..d20f2392 100644 --- a/cmd/plural/cd_services.go +++ b/cmd/command/cd/cd_services.go @@ -1,9 +1,11 @@ -package plural +package cd import ( "fmt" "strings" + "github.com/pluralsh/plural-cli/pkg/common" + gqlclient "github.com/pluralsh/console/go/client" "github.com/pluralsh/plural-cli/pkg/cd/template" "github.com/pluralsh/plural-cli/pkg/console" @@ -27,7 +29,7 @@ func (p *Plural) cdServiceCommands() []cli.Command { { Name: "list", ArgsUsage: "CLUSTER_ID", - Action: latestVersion(requireArgs(p.handleListClusterServices, []string{"CLUSTER_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleListClusterServices, []string{"CLUSTER_ID"})), Usage: "list cluster services", }, { @@ -48,13 +50,13 @@ func (p *Plural) cdServiceCommands() []cli.Command { }, cli.StringFlag{Name: "config-file", Usage: "path for configuration file"}, }, - Action: latestVersion(requireArgs(p.handleCreateClusterService, []string{"CLUSTER_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleCreateClusterService, []string{"CLUSTER_ID"})), Usage: "create cluster service", }, { Name: "update", ArgsUsage: "SERVICE_ID", - Action: latestVersion(requireArgs(p.handleUpdateClusterService, []string{"SERVICE_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleUpdateClusterService, []string{"SERVICE_ID"})), Usage: "update cluster service", Flags: []cli.Flag{ cli.StringFlag{Name: "version", Usage: "service version"}, @@ -76,7 +78,7 @@ func (p *Plural) cdServiceCommands() []cli.Command { { Name: "clone", ArgsUsage: "CLUSTER SERVICE", - Action: latestVersion(requireArgs(p.handleCloneClusterService, []string{"CLUSTER", "SERVICE"})), + Action: common.LatestVersion(common.RequireArgs(p.handleCloneClusterService, []string{"CLUSTER", "SERVICE"})), Flags: []cli.Flag{ cli.StringFlag{Name: "name", Usage: "the name for the cloned service", Required: true}, cli.StringFlag{Name: "namespace", Usage: "the namespace for this cloned service", Required: true}, @@ -90,7 +92,7 @@ func (p *Plural) cdServiceCommands() []cli.Command { { Name: "describe", ArgsUsage: "SERVICE_ID", - Action: latestVersion(requireArgs(p.handleDescribeClusterService, []string{"SERVICE_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleDescribeClusterService, []string{"SERVICE_ID"})), Flags: []cli.Flag{ cli.StringFlag{Name: "o", Usage: "output format"}, }, @@ -117,13 +119,13 @@ func (p *Plural) cdServiceCommands() []cli.Command { { Name: "delete", ArgsUsage: "SERVICE_ID", - Action: latestVersion(requireArgs(p.handleDeleteClusterService, []string{"SERVICE_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleDeleteClusterService, []string{"SERVICE_ID"})), Usage: "delete cluster service", }, { Name: "kick", ArgsUsage: "SERVICE_ID", - Action: latestVersion(requireArgs(p.handleKickClusterService, []string{"SERVICE_ID"})), + Action: common.LatestVersion(common.RequireArgs(p.handleKickClusterService, []string{"SERVICE_ID"})), Usage: "force sync cluster service", }, } diff --git a/cmd/plural/cd_settings.go b/cmd/command/cd/cd_settings.go similarity index 86% rename from cmd/plural/cd_settings.go rename to cmd/command/cd/cd_settings.go index ec845e13..3b84a66d 100644 --- a/cmd/plural/cd_settings.go +++ b/cmd/command/cd/cd_settings.go @@ -1,7 +1,8 @@ -package plural +package cd import ( consoleclient "github.com/pluralsh/console/go/client" + "github.com/pluralsh/plural-cli/pkg/common" "github.com/pluralsh/plural-cli/pkg/utils" "github.com/urfave/cli" ) @@ -19,7 +20,7 @@ func (p *Plural) cdSettingsCommands() []cli.Command { { Name: "agents", ArgsUsage: "FILENAME", - Action: latestVersion(requireArgs(p.handleUpdateAgents, []string{"FILENAME"})), + Action: common.LatestVersion(common.RequireArgs(p.handleUpdateAgents, []string{"FILENAME"})), Usage: "update agents settings", }, } diff --git a/cmd/plural/cd_stacks.go b/cmd/command/cd/cd_stacks.go similarity index 95% rename from cmd/plural/cd_stacks.go rename to cmd/command/cd/cd_stacks.go index 0b575413..228755a3 100644 --- a/cmd/plural/cd_stacks.go +++ b/cmd/command/cd/cd_stacks.go @@ -1,8 +1,10 @@ -package plural +package cd import ( "fmt" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/AlecAivazis/survey/v2" "github.com/samber/lo" "github.com/urfave/cli" @@ -24,7 +26,7 @@ func (p *Plural) cdStacksCommands() []cli.Command { return []cli.Command{ { Name: "gen-backend", - Action: latestVersion(p.handleGenerateBackend), + Action: common.LatestVersion(p.handleGenerateBackend), Usage: "generate '_override.tf' to configure a custom terraform backend", Flags: []cli.Flag{ cli.StringFlag{Name: "address", Usage: "terraform backend address", Required: false}, diff --git a/cmd/plural/cd_test.go b/cmd/command/cd/cd_test.go similarity index 86% rename from cmd/plural/cd_test.go rename to cmd/command/cd/cd_test.go index 859b6e61..9419bc08 100644 --- a/cmd/plural/cd_test.go +++ b/cmd/command/cd/cd_test.go @@ -1,14 +1,18 @@ -package plural_test +package cd_test import ( + "bytes" + "io" "os" "testing" consoleclient "github.com/pluralsh/console/go/client" - plural "github.com/pluralsh/plural-cli/cmd/plural" + "github.com/pluralsh/plural-cli/cmd/command/plural" + pluralclient "github.com/pluralsh/plural-cli/pkg/client" "github.com/pluralsh/plural-cli/pkg/test/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/urfave/cli" ) func TestListCDClusters(t *testing.T) { @@ -30,9 +34,9 @@ func TestListCDClusters(t *testing.T) { client := mocks.NewConsoleClient(t) client.On("ListClusters").Return(test.result, nil) app := plural.CreateNewApp(&plural.Plural{ - Client: nil, - ConsoleClient: client, - Kube: nil, + Plural: pluralclient.Plural{ + ConsoleClient: client, + }, HelmConfiguration: nil, }) app.HelpName = plural.ApplicationName @@ -76,9 +80,9 @@ Name: test client := mocks.NewConsoleClient(t) client.On("GetCluster", mock.AnythingOfType("*string"), mock.AnythingOfType("*string")).Return(test.result, nil) app := plural.CreateNewApp(&plural.Plural{ - Client: nil, - ConsoleClient: client, - Kube: nil, + Plural: pluralclient.Plural{ + ConsoleClient: client, + }, HelmConfiguration: nil, }) app.HelpName = plural.ApplicationName @@ -116,9 +120,9 @@ func TestListCDRepositories(t *testing.T) { client := mocks.NewConsoleClient(t) client.On("ListRepositories").Return(test.result, nil) app := plural.CreateNewApp(&plural.Plural{ - Client: nil, - ConsoleClient: client, - Kube: nil, + Plural: pluralclient.Plural{ + ConsoleClient: client, + }, HelmConfiguration: nil, }) app.HelpName = plural.ApplicationName @@ -156,9 +160,9 @@ func TestListCDServices(t *testing.T) { client := mocks.NewConsoleClient(t) client.On("ListClusterServices", mock.AnythingOfType("*string"), mock.AnythingOfType("*string")).Return(test.result, nil) app := plural.CreateNewApp(&plural.Plural{ - Client: nil, - ConsoleClient: client, - Kube: nil, + Plural: pluralclient.Plural{ + ConsoleClient: client, + }, HelmConfiguration: nil, }) app.HelpName = plural.ApplicationName @@ -175,3 +179,23 @@ func TestListCDServices(t *testing.T) { }) } } + +func captureStdout(app *cli.App, arg []string) (string, error) { + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := app.Run(arg) + if err != nil { + return "", err + } + + w.Close() + os.Stdout = old + + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err != nil { + return "", err + } + return buf.String(), nil +} diff --git a/cmd/plural/clusters.go b/cmd/command/clusters/clusters.go similarity index 77% rename from cmd/plural/clusters.go rename to cmd/command/clusters/clusters.go index 7dbcfcd0..448cfa04 100644 --- a/cmd/plural/clusters.go +++ b/cmd/command/clusters/clusters.go @@ -1,35 +1,50 @@ -package plural +package clusters import ( "fmt" + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/urfave/cli" "sigs.k8s.io/yaml" "github.com/pluralsh/plural-cli/pkg/api" - "github.com/pluralsh/plural-cli/pkg/bootstrap" "github.com/pluralsh/plural-cli/pkg/bootstrap/aws" - "github.com/pluralsh/plural-cli/pkg/bootstrap/validation" "github.com/pluralsh/plural-cli/pkg/cluster" "github.com/pluralsh/plural-cli/pkg/config" - "github.com/pluralsh/plural-cli/pkg/exp" "github.com/pluralsh/plural-cli/pkg/kubernetes" "github.com/pluralsh/plural-cli/pkg/machinepool" "github.com/pluralsh/plural-cli/pkg/manifest" "github.com/pluralsh/plural-cli/pkg/utils" ) +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "clusters", + Usage: "commands related to managing plural clusters", + Subcommands: p.clusterCommands(), + } +} + func (p *Plural) clusterCommands() []cli.Command { return []cli.Command{ { Name: "list", Usage: "lists clusters accessible to your user", - Action: latestVersion(p.listClusters), + Action: common.LatestVersion(p.listClusters), }, { Name: "transfer", Usage: "transfers ownership of the current cluster to another", - Action: latestVersion(rooted(p.transferOwnership)), + Action: common.LatestVersion(common.Rooted(p.transferOwnership)), Flags: []cli.Flag{ cli.StringFlag{ Name: "email", @@ -46,7 +61,7 @@ func (p *Plural) clusterCommands() []cli.Command { Usage: "the id of the source cluster", }, }, - Action: latestVersion(p.showCluster), + Action: common.LatestVersion(p.showCluster), }, { Name: "depend", @@ -61,34 +76,34 @@ func (p *Plural) clusterCommands() []cli.Command { Usage: "the id of the cluster waiting for promotion", }, }, - Action: latestVersion(p.dependCluster), + Action: common.LatestVersion(p.dependCluster), }, { Name: "promote", Usage: "promote pending upgrades to your cluster", - Action: latestVersion(p.promoteCluster), + Action: common.LatestVersion(p.promoteCluster), }, { Name: "wait", Usage: "waits on a cluster until it becomes ready", ArgsUsage: "NAMESPACE NAME", - Action: latestVersion(initKubeconfig(requireArgs(handleClusterWait, []string{"NAMESPACE", "NAME"}))), + Action: common.LatestVersion(common.InitKubeconfig(common.RequireArgs(handleClusterWait, []string{"NAMESPACE", "NAME"}))), Category: "Debugging", }, { Name: "mpwait", Usage: "waits on a machine pool until it becomes ready", ArgsUsage: "NAMESPACE NAME", - Action: latestVersion(initKubeconfig(requireArgs(handleMPWait, []string{"NAMESPACE", "NAME"}))), + Action: common.LatestVersion(common.InitKubeconfig(common.RequireArgs(handleMPWait, []string{"NAMESPACE", "NAME"}))), Category: "Debugging", }, - { - Name: "migrate", - Usage: "migrate to Cluster API", - Action: latestVersion(rooted(initKubeconfig(p.handleMigration))), - Category: "Publishing", - Hidden: !exp.IsFeatureEnabled(exp.EXP_PLURAL_CAPI), - }, + // { + // Name: "migrate", + // Usage: "migrate to Cluster API", + // Action: common.LatestVersion(common.Rooted(common.InitKubeconfig(p.handleMigration))), + // Category: "Publishing", + // Hidden: !exp.IsFeatureEnabled(exp.EXP_PLURAL_CAPI), + // }, { Name: "aws-auth", Usage: "fetches the current state of your aws auth config map", @@ -97,24 +112,24 @@ func (p *Plural) clusterCommands() []cli.Command { } } -func (p *Plural) handleMigration(_ *cli.Context) error { - p.InitPluralClient() - if err := validation.ValidateMigration(p); err != nil { - return err - } - - project, err := manifest.FetchProject() - if err != nil { - return err - } - - if project.ClusterAPI { - utils.Success("Cluster already migrated.\n") - return nil - } - - return bootstrap.MigrateCluster(RunPlural) -} +// func (p *Plural) handleMigration(_ *cli.Context) error { +// p.InitPluralClient() +// if err := validation.ValidateMigration(p); err != nil { +// return err +// } +// +// project, err := manifest.FetchProject() +// if err != nil { +// return err +// } +// +// if project.ClusterAPI { +// utils.Success("Cluster already migrated.\n") +// return nil +// } +// +// return bootstrap.MigrateCluster(plural.RunPlural) +// } func awsAuthCommands() []cli.Command { return []cli.Command{ @@ -135,7 +150,7 @@ func awsAuthCommands() []cli.Command { } } -func handleAwsAuth(c *cli.Context) error { +func handleAwsAuth(_ *cli.Context) error { auth, err := aws.FetchAuth() if err != nil { return err @@ -216,7 +231,7 @@ func (p *Plural) transferOwnership(c *cli.Context) error { return err } - if err := p.assumeServiceAccount(config.Read(), man); err != nil { + if err := p.AssumeServiceAccount(config.Read(), man); err != nil { return err } @@ -230,13 +245,13 @@ func (p *Plural) transferOwnership(c *cli.Context) error { continue } - if err := p.doBuild(installation, false); err != nil { + if err := common.DoBuild(p.Client, installation, false); err != nil { return err } } utils.Highlight("deploying rebuilt applications\n") - if err := p.deploy(c); err != nil { + if err := p.Deploy(c); err != nil { return err } diff --git a/cmd/command/config/config.go b/cmd/command/config/config.go new file mode 100644 index 00000000..aac415cb --- /dev/null +++ b/cmd/command/config/config.go @@ -0,0 +1,66 @@ +package config + +import ( + "io" + "os" + + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/pluralsh/plural-cli/pkg/config" + "github.com/urfave/cli" +) + +func Command() cli.Command { + return cli.Command{ + Name: "config", + Aliases: []string{"conf"}, + Usage: "reads/modifies cli configuration", + Subcommands: configCommands(), + Category: "User Profile", + } +} + +func configCommands() []cli.Command { + return []cli.Command{ + { + Name: "amend", + Usage: "modify config", + ArgsUsage: "[key] [value]", + Action: common.LatestVersion(handleAmend), + }, + { + Name: "read", + Usage: "dumps config", + ArgsUsage: "", + Action: common.LatestVersion(handleRead), + }, + { + Name: "import", + Usage: "imports a new config from a given token", + Action: common.LatestVersion(handleConfigImport), + }, + } +} + +func handleAmend(c *cli.Context) error { + return config.Amend(c.Args().Get(0), c.Args().Get(1)) +} + +func handleRead(c *cli.Context) error { + conf := config.Read() + d, err := conf.Marshal() + if err != nil { + return err + } + + os.Stdout.Write(d) + return nil +} + +func handleConfigImport(c *cli.Context) error { + data, err := io.ReadAll(os.Stdin) + if err != nil { + return err + } + + return config.FromToken(string(data)) +} diff --git a/cmd/plural/config_test.go b/cmd/command/config/config_test.go similarity index 96% rename from cmd/plural/config_test.go rename to cmd/command/config/config_test.go index 6e1ddf1d..f92206fc 100644 --- a/cmd/plural/config_test.go +++ b/cmd/command/config/config_test.go @@ -1,4 +1,4 @@ -package plural_test +package config_test import ( "bytes" @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/urfave/cli" - "github.com/pluralsh/plural-cli/cmd/plural" + "github.com/pluralsh/plural-cli/cmd/command/plural" "github.com/pluralsh/plural-cli/pkg/config" pluraltest "github.com/pluralsh/plural-cli/pkg/test" ) diff --git a/cmd/plural/crypto.go b/cmd/command/crypto/crypto.go similarity index 72% rename from cmd/plural/crypto.go rename to cmd/command/crypto/crypto.go index b02e69ae..2ac7e69a 100644 --- a/cmd/plural/crypto.go +++ b/cmd/command/crypto/crypto.go @@ -1,4 +1,4 @@ -package plural +package crypto import ( "bytes" @@ -9,6 +9,9 @@ import ( "strconv" "strings" + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/AlecAivazis/survey/v2" "github.com/mitchellh/go-homedir" @@ -18,45 +21,25 @@ import ( "github.com/pluralsh/plural-cli/pkg/crypto" "github.com/pluralsh/plural-cli/pkg/scm" "github.com/pluralsh/plural-cli/pkg/utils" - "github.com/pluralsh/plural-cli/pkg/utils/git" ) var prefix = []byte("CHARTMART-ENCRYPTED") -const ( - GitAttributesFile = ".gitattributes" - GitIgnoreFile = ".gitignore" -) +type Plural struct { + client.Plural +} -const Gitattributes = `/**/helm/**/values.yaml filter=plural-crypt diff=plural-crypt -/**/helm/**/values.yaml* filter=plural-crypt diff=plural-crypt -/**/helm/**/README.md* filter=plural-crypt diff=plural-crypt -/**/helm/**/default-values.yaml* filter=plural-crypt diff=plural-crypt -/**/terraform/**/main.tf filter=plural-crypt diff=plural-crypt -/**/terraform/**/main.tf* filter=plural-crypt diff=plural-crypt -/**/manifest.yaml filter=plural-crypt diff=plural-crypt -/**/output.yaml filter=plural-crypt diff=plural-crypt -/diffs/**/* filter=plural-crypt diff=plural-crypt -context.yaml filter=plural-crypt diff=plural-crypt -workspace.yaml filter=plural-crypt diff=plural-crypt -context.yaml* filter=plural-crypt diff=plural-crypt -workspace.yaml* filter=plural-crypt diff=plural-crypt -helm-values/*.yaml filter=plural-crypt diff=plural-crypt -.env filter=plural-crypt diff=plural-crypt -.gitattributes !filter !diff -` - -const Gitignore = `/**/.terraform -/**/.terraform* -/**/terraform.tfstate* -/bin -*~ -.idea -*.swp -*.swo -.DS_STORE -.vscode -` +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "crypto", + Usage: "plural encryption utilities", + Subcommands: p.cryptoCommands(), + Category: "User Profile", + } +} // IMPORTANT // Repo cryptography relies on git smudge and clean filters, which pipe a file into stdin and respond with a new version @@ -76,12 +59,12 @@ func (p *Plural) cryptoCommands() []cli.Command { { Name: "init", Usage: "initializes git filters for you", - Action: cryptoInit, + Action: common.CryptoInit, }, { Name: "unlock", Usage: "auto-decrypts all affected files in the repo", - Action: handleUnlock, + Action: common.HandleUnlock, }, { Name: "import", @@ -91,7 +74,7 @@ func (p *Plural) cryptoCommands() []cli.Command { { Name: "recover", Usage: "recovers repo encryption keys from a working k8s cluster", - Action: initKubeconfig(p.handleRecover), + Action: common.InitKubeconfig(p.handleRecover), }, { Name: "random", @@ -108,7 +91,7 @@ func (p *Plural) cryptoCommands() []cli.Command { { Name: "ssh-keygen", Usage: "generate an ed5519 keypair for use in git ssh", - Action: affirmed(handleKeygen, "This command will autogenerate an ed5519 keypair, without passphrase. Sound good?", "PLURAL_CRYPTO_SSH_KEYGEN"), + Action: common.Affirmed(handleKeygen, "This command will autogenerate an ed5519 keypair, without passphrase. Sound good?", "PLURAL_CRYPTO_SSH_KEYGEN"), }, { Name: "export", @@ -163,13 +146,13 @@ func (p *Plural) backupCommands() []cli.Command { { Name: "create", Usage: "creates a backup for your current key", - Action: affirmed(p.createBackup, backupMsg, "PLURAL_BACKUPS_CREATE"), + Action: common.Affirmed(p.createBackup, common.BackupMsg, "PLURAL_BACKUPS_CREATE"), }, { Name: "restore", Usage: "restores a key backup as your current encryption key", ArgsUsage: "NAME", - Action: requireArgs(p.restoreBackup, []string{"NAME"}), + Action: common.RequireArgs(p.restoreBackup, []string{"NAME"}), }, } } @@ -255,10 +238,10 @@ func handleDecrypt(c *cli.Context) error { // CheckGitCrypt method checks if the .gitattributes and .gitignore files exist and have desired content. // Some old repos can have fewer files to encrypt and must be updated. func CheckGitCrypt(c *cli.Context) error { - if !utils.Exists(GitAttributesFile) || !utils.Exists(GitIgnoreFile) { - return cryptoInit(c) + if !utils.Exists(common.GitAttributesFile) || !utils.Exists(common.GitIgnoreFile) { + return common.CryptoInit(c) } - toCompare := map[string]string{GitAttributesFile: Gitattributes, GitIgnoreFile: Gitignore} + toCompare := map[string]string{common.GitAttributesFile: common.Gitattributes, common.GitIgnoreFile: common.Gitignore} for file, content := range toCompare { equal, err := utils.CompareFileContent(file, content) @@ -266,40 +249,13 @@ func CheckGitCrypt(c *cli.Context) error { return err } if !equal { - return cryptoInit(c) + return common.CryptoInit(c) } } return nil } -func cryptoInit(c *cli.Context) error { - encryptConfig := [][]string{ - {"filter.plural-crypt.smudge", "plural crypto decrypt"}, - {"filter.plural-crypt.clean", "plural crypto encrypt"}, - {"filter.plural-crypt.required", "true"}, - {"diff.plural-crypt.textconv", "plural crypto decrypt"}, - } - - utils.Highlight("Creating git encryption filters\n") - for _, conf := range encryptConfig { - if err := gitConfig(conf[0], conf[1]); err != nil { - return err - } - } - - if err := utils.WriteFile(GitAttributesFile, []byte(Gitattributes)); err != nil { - return err - } - - if err := utils.WriteFile(GitIgnoreFile, []byte(Gitignore)); err != nil { - return err - } - - _, err := crypto.Build() - return err -} - func (p *Plural) handleCryptoShare(c *cli.Context) error { p.InitPluralClient() emails := c.StringSlice("email") @@ -326,40 +282,6 @@ func (p *Plural) handleSetupKeys(c *cli.Context) error { return nil } -func handleUnlock(c *cli.Context) error { - _, err := crypto.Build() - if err != nil { - return err - } - - repoRoot, err := git.Root() - if err != nil { - return err - } - - // fixes Invalid cross-device link when using os.Rename - gitIndexDir, err := filepath.Abs(filepath.Join(repoRoot, ".git")) - if err != nil { - return err - } - gitIndex := filepath.Join(gitIndexDir, "index") - dump, err := os.CreateTemp(gitIndexDir, "index.bak") - if err != nil { - return err - } - if err := os.Rename(gitIndex, dump.Name()); err != nil { - return err - } - - if err := gitCommand("checkout", "HEAD", "--", repoRoot).Run(); err != nil { - _ = os.Rename(dump.Name(), gitIndex) - return errUnlock - } - - os.Remove(dump.Name()) - return nil -} - func exportKey(c *cli.Context) error { key, err := crypto.Materialize() if err != nil { diff --git a/cmd/plural/crypto_test.go b/cmd/command/crypto/crypto_test.go similarity index 88% rename from cmd/plural/crypto_test.go rename to cmd/command/crypto/crypto_test.go index a3eefa9d..9470fb74 100644 --- a/cmd/plural/crypto_test.go +++ b/cmd/command/crypto/crypto_test.go @@ -1,4 +1,4 @@ -package plural_test +package crypto_test import ( "encoding/base64" @@ -6,7 +6,11 @@ import ( "path" "testing" - "github.com/pluralsh/plural-cli/cmd/plural" + "github.com/pluralsh/plural-cli/cmd/command/crypto" + pluralclient "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" + + "github.com/pluralsh/plural-cli/cmd/command/plural" "github.com/pluralsh/plural-cli/pkg/api" "github.com/pluralsh/plural-cli/pkg/config" pluraltest "github.com/pluralsh/plural-cli/pkg/test" @@ -57,10 +61,12 @@ func TestSetupKeys(t *testing.T) { if test.expectedError == "" { client.On("CreateKey", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(nil) } - app := plural.CreateNewApp(&plural.Plural{Client: client}) + app := plural.CreateNewApp(&plural.Plural{Plural: pluralclient.Plural{ + Client: client, + }}) app.HelpName = plural.ApplicationName os.Args = test.args - _, err = captureStdout(app, os.Args) + _, err = common.CaptureStdout(app, os.Args) if test.expectedError != "" { assert.Equal(t, err.Error(), test.expectedError) } else { @@ -90,10 +96,12 @@ func TestRandom(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { client := mocks.NewClient(t) - app := plural.CreateNewApp(&plural.Plural{Client: client}) + app := plural.CreateNewApp(&plural.Plural{Plural: pluralclient.Plural{ + Client: client, + }}) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) assert.NoError(t, err) b, err := base64.StdEncoding.DecodeString(res) assert.NoError(t, err) @@ -164,10 +172,12 @@ func TestShare(t *testing.T) { if test.keys != nil { client.On("ListKeys", mock.Anything).Return(test.keys, nil) } - app := plural.CreateNewApp(&plural.Plural{Client: client}) + app := plural.CreateNewApp(&plural.Plural{Plural: pluralclient.Plural{ + Client: client, + }}) app.HelpName = plural.ApplicationName os.Args = test.args - _, err = captureStdout(app, os.Args) + _, err = common.CaptureStdout(app, os.Args) if test.expectedError != "" { assert.Equal(t, err.Error(), test.expectedError) } else { @@ -236,10 +246,13 @@ func TestRecover(t *testing.T) { kube.On("Secret", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(test.secret, nil) - app := plural.CreateNewApp(&plural.Plural{Client: client, Kube: kube}) + app := plural.CreateNewApp(&plural.Plural{Plural: pluralclient.Plural{ + Client: client, + Kube: kube, + }}) app.HelpName = plural.ApplicationName os.Args = test.args - _, err = captureStdout(app, os.Args) + _, err = common.CaptureStdout(app, os.Args) assert.NoError(t, err) b, err := os.ReadFile(path.Join(dir, ".plural", "key")) @@ -286,18 +299,18 @@ func TestCheckGitCrypt(t *testing.T) { _, err = git.Init() assert.NoError(t, err) - gitAttributes := path.Join(dir, plural.GitAttributesFile) - gitIgnore := path.Join(dir, plural.GitIgnoreFile) + gitAttributes := path.Join(dir, common.GitAttributesFile) + gitIgnore := path.Join(dir, common.GitIgnoreFile) if test.createFiles { - err = utils.WriteFile(gitIgnore, []byte(plural.Gitignore+"some extra")) + err = utils.WriteFile(gitIgnore, []byte(common.Gitignore+"some extra")) assert.NoError(t, err) - err = utils.WriteFile(gitAttributes, []byte(plural.Gitattributes+"abc")) + err = utils.WriteFile(gitAttributes, []byte(common.Gitattributes+"abc")) assert.NoError(t, err) } // test CheckGitCrypt - err = plural.CheckGitCrypt(&cli.Context{}) + err = crypto.CheckGitCrypt(&cli.Context{}) assert.NoError(t, err) // the files should exist @@ -306,11 +319,11 @@ func TestCheckGitCrypt(t *testing.T) { attributes, err := utils.ReadFile(gitAttributes) assert.NoError(t, err) - assert.Equal(t, attributes, plural.Gitattributes) + assert.Equal(t, attributes, common.Gitattributes) ignore, err := utils.ReadFile(gitIgnore) assert.NoError(t, err) - assert.Equal(t, ignore, plural.Gitignore) + assert.Equal(t, ignore, common.Gitignore) }) } } @@ -361,7 +374,7 @@ func TestCheckKeyFingerprint(t *testing.T) { app := plural.CreateNewApp(&plural.Plural{}) app.HelpName = plural.ApplicationName os.Args = test.args - _, err = captureStdout(app, os.Args) + _, err = common.CaptureStdout(app, os.Args) assert.NoError(t, err) utiltest.CheckFingerprint(t, keyFingerprint) diff --git a/cmd/command/deploy/deploy.go b/cmd/command/deploy/deploy.go new file mode 100644 index 00000000..ac4f830d --- /dev/null +++ b/cmd/command/deploy/deploy.go @@ -0,0 +1,54 @@ +package deploy + +import ( + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/urfave/cli" +) + +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "deploy", + Aliases: []string{"d"}, + Usage: "Deploys the current workspace. This command will first sniff out git diffs in workspaces, topsort them, then apply all changes.", + ArgsUsage: "Workspace", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "silence", + Usage: "don't display notes for deployed apps", + }, + cli.BoolFlag{ + Name: "verbose", + Usage: "show all command output during execution", + }, + cli.BoolFlag{ + Name: "ignore-console", + Usage: "don't deploy the plural console", + }, + cli.BoolFlag{ + Name: "all", + Usage: "deploy all repos irregardless of changes", + }, + cli.StringFlag{ + Name: "commit", + Usage: "commits your changes with this message", + }, + cli.StringSliceFlag{ + Name: "from", + Usage: "deploys only this application and its dependencies", + }, + cli.BoolFlag{ + Name: "force", + Usage: "use force push when pushing to git", + }, + }, + Action: common.Tracked(common.LatestVersion(common.Owned(common.Rooted(p.Deploy))), "cli.deploy"), + } +} diff --git a/cmd/command/destroy/destroy.go b/cmd/command/destroy/destroy.go new file mode 100644 index 00000000..577e65e0 --- /dev/null +++ b/cmd/command/destroy/destroy.go @@ -0,0 +1,180 @@ +package destroy + +import ( + "fmt" + "os" + + "github.com/pluralsh/plural-cli/pkg/api" + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/pluralsh/plural-cli/pkg/manifest" + "github.com/pluralsh/plural-cli/pkg/utils" + "github.com/pluralsh/plural-cli/pkg/utils/git" + "github.com/pluralsh/plural-cli/pkg/wkspace" + "github.com/urfave/cli" +) + +type Plural struct { + Plural client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "destroy", + Aliases: []string{"d"}, + Usage: "iterates through all installations in reverse topological order, deleting helm installations and terraform", + ArgsUsage: "APP", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "from", + Usage: "where to start your deploy command (useful when restarting interrupted destroys)", + }, + cli.StringFlag{ + Name: "commit", + Usage: "commits your changes with this message", + }, + cli.BoolFlag{ + Name: "force", + Usage: "use force push when pushing to git", + }, + cli.BoolFlag{ + Name: "all", + Usage: "tear down the entire cluster gracefully in one go", + }, + }, + Action: common.Tracked(common.LatestVersion(common.Owned(common.UpstreamSynced(p.destroy))), "cli.destroy"), + } +} + +func (p *Plural) destroy(c *cli.Context) error { + p.Plural.InitPluralClient() + repoName := c.Args().Get(0) + repoRoot, err := git.Root() + if err != nil { + return err + } + force := c.Bool("force") + all := c.Bool("all") + + project, err := manifest.FetchProject() + if err != nil { + return err + } + + infix := "this workspace" + if repoName != "" { + infix = repoName + } else if !all { + return fmt.Errorf("you must either specify an individual application or `--all` to destroy the entire workspace") + } + + if !force && !common.Confirm(fmt.Sprintf("Are you sure you want to destroy %s?", infix), "PLURAL_DESTROY_CONFIRM") { + return nil + } + + delete := force || common.Affirm("Do you want to uninstall your applications from the plural api as well?", "PLURAL_DESTROY_AFFIRM_UNINSTALL_APPS") + + if repoName != "" { + installation, err := p.Plural.GetInstallation(repoName) + if err != nil { + return api.GetErrorResponse(err, "GetInstallation") + } + + if installation == nil { + return fmt.Errorf("No installation for app %s to destroy, if the app is still in your repo, you can always run cd %s/terraform && terraform destroy", repoName, repoName) + } + + return p.doDestroy(repoRoot, installation, delete, project.ClusterAPI) + } + + installations, err := client.GetSortedInstallations(p.Plural, repoName) + if err != nil { + return err + } + + from := c.String("from") + started := from == "" + for i := len(installations) - 1; i >= 0; i-- { + installation := installations[i] + if installation.Repository.Name == from { + started = true + } + + if !started { + continue + } + + if err := p.doDestroy(repoRoot, installation, delete, project.ClusterAPI); err != nil { + return err + } + } + + man, _ := manifest.FetchProject() + if err := p.Plural.DeleteEabCredential(man.Cluster, man.Provider); err != nil { + fmt.Printf("no eab key to delete %s\n", err) + } + + if repoName == "" { + utils.Success("Finished destroying workspace\n") + utils.Note("if you want to recreate this workspace, be sure to rename the cluster to ensure a clean redeploy") + man, err := manifest.FetchProject() + if err != nil { + return err + } + if err := p.Plural.DestroyCluster(man.Network.Subdomain, man.Cluster, man.Provider); err != nil { + return api.GetErrorResponse(err, "DestroyCluster") + } + } + + utils.Highlight("\n==> Commit and push your changes to record your workspace changes\n\n") + + if commit := common.CommitMsg(c); commit != "" { + utils.Highlight("Pushing upstream...\n") + return git.Sync(repoRoot, commit, force) + } + + return nil +} + +func (p *Plural) doDestroy(repoRoot string, installation *api.Installation, delete, clusterAPI bool) error { + p.Plural.InitPluralClient() + if err := os.Chdir(repoRoot); err != nil { + return err + } + repo := installation.Repository.Name + if ctx, err := manifest.FetchContext(); err == nil && ctx.Protected(repo) { + return fmt.Errorf("This app is protected, you cannot plural destroy without updating context.yaml") + } + + utils.Error("\nDestroying application %s\n", repo) + workspace, err := wkspace.New(p.Plural.Client, installation) + if err != nil { + return err + } + + // TODO fix for clusterAPI + // if repo == Bootstrap && clusterAPI { + // if err = bootstrap.DestroyCluster(workspace.Destroy, plural.RunPlural); err != nil { + // return err + // } + // + // } else { + // if err := workspace.Destroy(); err != nil { + // return err + // } + // } + + if err := workspace.Destroy(); err != nil { + return err + } + + if delete { + utils.Highlight("Uninstalling %s from the plural api as well...\n", repo) + return p.Plural.Client.DeleteInstallation(installation.Id) + } + + return nil +} diff --git a/cmd/command/info/info.go b/cmd/command/info/info.go new file mode 100644 index 00000000..e681db41 --- /dev/null +++ b/cmd/command/info/info.go @@ -0,0 +1,40 @@ +package info + +import ( + "fmt" + + "github.com/pluralsh/plural-cli/pkg/api" + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/pluralsh/plural-cli/pkg/scaffold" + "github.com/urfave/cli" +) + +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "info", + Usage: "Get information for your installation of APP", + ArgsUsage: "APP", + Action: common.LatestVersion(common.Owned(common.Rooted(p.info))), + } +} +func (p *Plural) info(c *cli.Context) error { + p.InitPluralClient() + repo := c.Args().Get(0) + installation, err := p.GetInstallation(repo) + if err != nil { + return api.GetErrorResponse(err, "GetInstallation") + } + if installation == nil { + return fmt.Errorf("You have not installed %s", repo) + } + + return scaffold.Notes(installation) +} diff --git a/cmd/command/init/init.go b/cmd/command/init/init.go new file mode 100644 index 00000000..b297cab0 --- /dev/null +++ b/cmd/command/init/init.go @@ -0,0 +1,36 @@ +package init + +import ( + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/urfave/cli" +) + +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "init", + Usage: "initializes plural within a git repo", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "endpoint", + Usage: "the endpoint for the plural installation you're working with", + }, + cli.StringFlag{ + Name: "service-account", + Usage: "email for the service account you'd like to use for this workspace", + }, + cli.BoolFlag{ + Name: "ignore-preflights", + Usage: "whether to ignore preflight check failures prior to init", + }, + }, + Action: common.Tracked(common.LatestVersion(p.HandleInit), "cli.init"), + } +} diff --git a/cmd/plural/link.go b/cmd/command/link/link.go similarity index 96% rename from cmd/plural/link.go rename to cmd/command/link/link.go index 0579281d..5d288a14 100644 --- a/cmd/plural/link.go +++ b/cmd/command/link/link.go @@ -1,4 +1,4 @@ -package plural +package link import ( "path/filepath" @@ -7,7 +7,7 @@ import ( "github.com/urfave/cli" ) -func linkCommands() []cli.Command { +func Commands() []cli.Command { return []cli.Command{ { Name: "link", diff --git a/cmd/plural/logs.go b/cmd/command/log/logs.go similarity index 58% rename from cmd/plural/logs.go rename to cmd/command/log/logs.go index 0bb85a88..8ef95d8f 100644 --- a/cmd/plural/logs.go +++ b/cmd/command/log/logs.go @@ -1,24 +1,42 @@ -package plural +package log import ( + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" "github.com/pluralsh/plural-cli/pkg/config" "github.com/pluralsh/plural-cli/pkg/logs" "github.com/urfave/cli" ) +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "logs", + Usage: "Commands for tailing logs for specific apps", + Subcommands: p.logsCommands(), + Category: "Debugging", + } +} + func (p *Plural) logsCommands() []cli.Command { return []cli.Command{ { Name: "list", Usage: "lists log tails for a repo", ArgsUsage: "REPO", - Action: latestVersion(initKubeconfig(requireArgs(p.handleLogsList, []string{"REPO"}))), + Action: common.LatestVersion(common.InitKubeconfig(common.RequireArgs(p.handleLogsList, []string{"REPO"}))), }, { Name: "tail", Usage: "execs the specific logtail", ArgsUsage: "REPO NAME", - Action: latestVersion(initKubeconfig(requireArgs(p.handleLogTail, []string{"REPO", "NAME"}))), + Action: common.LatestVersion(common.InitKubeconfig(common.RequireArgs(p.handleLogTail, []string{"REPO", "NAME"}))), }, } } diff --git a/cmd/plural/logs_test.go b/cmd/command/log/logs_test.go similarity index 78% rename from cmd/plural/logs_test.go rename to cmd/command/log/logs_test.go index fcb3fa8c..134bb341 100644 --- a/cmd/plural/logs_test.go +++ b/cmd/command/log/logs_test.go @@ -1,10 +1,13 @@ -package plural_test +package log_test import ( "os" "testing" - plural "github.com/pluralsh/plural-cli/cmd/plural" + pluralclient "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" + + "github.com/pluralsh/plural-cli/cmd/command/plural" "github.com/pluralsh/plural-cli/pkg/test/mocks" "github.com/pluralsh/plural-operator/apis/platform/v1alpha1" "github.com/stretchr/testify/assert" @@ -34,10 +37,15 @@ func TestLogsList(t *testing.T) { if test.expectedError == "" { kube.On("LogTailList", mock.AnythingOfType("string")).Return(&v1alpha1.LogTailList{Items: []v1alpha1.LogTail{}}, nil) } - app := plural.CreateNewApp(&plural.Plural{Client: client, Kube: kube}) + app := plural.CreateNewApp(&plural.Plural{ + Plural: pluralclient.Plural{ + Client: client, + Kube: kube, + }, + }) app.HelpName = plural.ApplicationName os.Args = test.args - _, err := captureStdout(app, os.Args) + _, err := common.CaptureStdout(app, os.Args) if test.expectedError != "" { assert.Equal(t, test.expectedError, err.Error()) } else { diff --git a/cmd/plural/ops.go b/cmd/command/ops/ops.go similarity index 73% rename from cmd/plural/ops.go rename to cmd/command/ops/ops.go index 78484878..055dc317 100644 --- a/cmd/plural/ops.go +++ b/cmd/command/ops/ops.go @@ -1,26 +1,44 @@ -package plural +package ops import ( "fmt" + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" "github.com/pluralsh/plural-cli/pkg/provider" "github.com/pluralsh/plural-cli/pkg/utils" "github.com/urfave/cli" v1 "k8s.io/api/core/v1" ) +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "ops", + Usage: "Commands for simplifying cluster operations", + Subcommands: p.opsCommands(), + Category: "Debugging", + } +} + func (p *Plural) opsCommands() []cli.Command { return []cli.Command{ { Name: "terminate", Usage: "terminates a worker node in your cluster", ArgsUsage: "NAME", - Action: latestVersion(initKubeconfig(p.handleTerminateNode)), + Action: common.LatestVersion(common.InitKubeconfig(p.handleTerminateNode)), }, { Name: "cluster", Usage: "list the nodes in your cluster", - Action: latestVersion(initKubeconfig(p.handleListNodes)), + Action: common.LatestVersion(common.InitKubeconfig(p.handleListNodes)), }, } } diff --git a/cmd/plural/ops_test.go b/cmd/command/ops/ops_test.go similarity index 85% rename from cmd/plural/ops_test.go rename to cmd/command/ops/ops_test.go index 8b58f427..337d08d7 100644 --- a/cmd/plural/ops_test.go +++ b/cmd/command/ops/ops_test.go @@ -1,17 +1,19 @@ -package plural_test +package ops_test import ( "os" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "gopkg.in/yaml.v2" + "github.com/pluralsh/plural-cli/pkg/common" - "github.com/pluralsh/plural-cli/cmd/plural" + "github.com/pluralsh/plural-cli/cmd/command/plural" + clientcmd "github.com/pluralsh/plural-cli/pkg/client" "github.com/pluralsh/plural-cli/pkg/manifest" "github.com/pluralsh/plural-cli/pkg/test/mocks" "github.com/pluralsh/plural-cli/pkg/utils/git" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "gopkg.in/yaml.v2" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -55,10 +57,15 @@ func TestListNodes(t *testing.T) { client := mocks.NewClient(t) kube := mocks.NewKube(t) kube.On("Nodes").Return(test.nodes, nil) - app := plural.CreateNewApp(&plural.Plural{Client: client, Kube: kube}) + app := plural.CreateNewApp(&plural.Plural{ + Plural: clientcmd.Plural{ + Client: client, + Kube: kube, + }, + }) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) assert.NoError(t, err) assert.Equal(t, test.expectedResponse, res) @@ -116,10 +123,15 @@ func TestTerminate(t *testing.T) { client := mocks.NewClient(t) kube := mocks.NewKube(t) kube.On("Node", mock.AnythingOfType("string")).Return(test.node, nil) - app := plural.CreateNewApp(&plural.Plural{Client: client, Kube: kube}) + app := plural.CreateNewApp(&plural.Plural{ + Plural: clientcmd.Plural{ + Client: client, + Kube: kube, + }, + }) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) assert.NoError(t, err) assert.Equal(t, test.expectedResponse, res) diff --git a/cmd/plural/output.go b/cmd/command/output/output.go similarity index 72% rename from cmd/plural/output.go rename to cmd/command/output/output.go index 0d522ddb..8ed41409 100644 --- a/cmd/plural/output.go +++ b/cmd/command/output/output.go @@ -1,21 +1,32 @@ -package plural +package output import ( "path/filepath" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/pluralsh/plural-cli/pkg/output" "github.com/pluralsh/plural-cli/pkg/utils" "github.com/pluralsh/plural-cli/pkg/utils/pathing" "github.com/urfave/cli" ) +func Command() cli.Command { + return cli.Command{ + Name: "output", + Usage: "Commands for generating outputs from supported tools", + Subcommands: outputCommands(), + Category: "Workspace", + } +} + func outputCommands() []cli.Command { return []cli.Command{ { Name: "terraform", Usage: "generates terraform output", ArgsUsage: "REPO", - Action: latestVersion(handleTerraformOutput), + Action: common.LatestVersion(handleTerraformOutput), }, } } diff --git a/cmd/plural/packages.go b/cmd/command/packages/packages.go similarity index 80% rename from cmd/plural/packages.go rename to cmd/command/packages/packages.go index 64a63804..15d3d583 100644 --- a/cmd/plural/packages.go +++ b/cmd/command/packages/packages.go @@ -1,9 +1,13 @@ -package plural +package packages import ( "fmt" "os" + "github.com/pluralsh/plural-cli/pkg/client" + + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/pluralsh/plural-cli/pkg/api" "github.com/olekukonko/tablewriter" @@ -13,25 +17,40 @@ import ( "github.com/pluralsh/plural-cli/pkg/wkspace" ) +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "packages", + Usage: "Commands for managing your installed packages", + Subcommands: p.packagesCommands(), + } +} + func (p *Plural) packagesCommands() []cli.Command { return []cli.Command{ { Name: "install", Usage: "installs a package at a specific version", ArgsUsage: "helm|terraform REPO NAME VSN", - Action: affirmed(requireArgs(p.installPackage, []string{"TYPE", "REPO", "NAME", "VERSION"}), "Are you sure you want to install this package?", "PLURAL_PACKAGES_INSTALL"), + Action: common.Affirmed(common.RequireArgs(p.installPackage, []string{"TYPE", "REPO", "NAME", "VERSION"}), "Are you sure you want to install this package?", "PLURAL_PACKAGES_INSTALL"), }, { Name: "uninstall", Usage: "uninstall a helm or terraform package", ArgsUsage: "helm|terraform REPO NAME", - Action: latestVersion(affirmed(requireArgs(rooted(p.uninstallPackage), []string{"TYPE", "REPO", "NAME"}), "Are you sure you want to uninstall this package?", "PLURAL_PACKAGES_UNINSTALL")), + Action: common.LatestVersion(common.Affirmed(common.RequireArgs(common.Rooted(p.uninstallPackage), []string{"TYPE", "REPO", "NAME"}), "Are you sure you want to uninstall this package?", "PLURAL_PACKAGES_UNINSTALL")), }, { Name: "list", Usage: "lists the packages installed for a given repo", ArgsUsage: "REPO", - Action: latestVersion(requireArgs(rooted(p.listPackages), []string{"REPO"})), + Action: common.LatestVersion(common.RequireArgs(common.Rooted(p.listPackages), []string{"REPO"})), }, { Name: "show", @@ -47,13 +66,13 @@ func (p *Plural) showCommands() []cli.Command { Name: "helm", Usage: "list versions for a helm chart", ArgsUsage: "REPO NAME", - Action: requireArgs(p.showHelm, []string{"REPO", "NAME"}), + Action: common.RequireArgs(p.showHelm, []string{"REPO", "NAME"}), }, { Name: "terraform", Usage: "list versions for a terraform module", ArgsUsage: "REPO NAME", - Action: requireArgs(p.showTerraform, []string{"REPO", "NAME"}), + Action: common.RequireArgs(p.showTerraform, []string{"REPO", "NAME"}), }, } } diff --git a/cmd/command/plural/plural.go b/cmd/command/plural/plural.go new file mode 100644 index 00000000..32bf9479 --- /dev/null +++ b/cmd/command/plural/plural.go @@ -0,0 +1,276 @@ +package plural + +import ( + "github.com/pluralsh/plural-cli/cmd/command/ai" + "github.com/pluralsh/plural-cli/cmd/command/api" + "github.com/pluralsh/plural-cli/cmd/command/auth" + "github.com/pluralsh/plural-cli/cmd/command/bootstrap" + "github.com/pluralsh/plural-cli/cmd/command/bounce" + "github.com/pluralsh/plural-cli/cmd/command/buildcmd" + "github.com/pluralsh/plural-cli/cmd/command/bundle" + "github.com/pluralsh/plural-cli/cmd/command/cd" + "github.com/pluralsh/plural-cli/cmd/command/clusters" + "github.com/pluralsh/plural-cli/cmd/command/config" + cryptocmd "github.com/pluralsh/plural-cli/cmd/command/crypto" + "github.com/pluralsh/plural-cli/cmd/command/deploy" + "github.com/pluralsh/plural-cli/cmd/command/destroy" + "github.com/pluralsh/plural-cli/cmd/command/info" + cmdinit "github.com/pluralsh/plural-cli/cmd/command/init" + "github.com/pluralsh/plural-cli/cmd/command/link" + "github.com/pluralsh/plural-cli/cmd/command/log" + "github.com/pluralsh/plural-cli/cmd/command/ops" + "github.com/pluralsh/plural-cli/cmd/command/output" + "github.com/pluralsh/plural-cli/cmd/command/pr" + "github.com/pluralsh/plural-cli/cmd/command/profile" + "github.com/pluralsh/plural-cli/cmd/command/proxy" + "github.com/pluralsh/plural-cli/cmd/command/push" + "github.com/pluralsh/plural-cli/cmd/command/repo" + "github.com/pluralsh/plural-cli/cmd/command/stack" + "github.com/pluralsh/plural-cli/cmd/command/up" + "github.com/pluralsh/plural-cli/cmd/command/upgrade" + "github.com/pluralsh/plural-cli/cmd/command/vpn" + "github.com/pluralsh/plural-cli/cmd/command/workspace" + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" + conf "github.com/pluralsh/plural-cli/pkg/config" + "github.com/pluralsh/plural-cli/pkg/crypto" + "github.com/pluralsh/plural-cli/pkg/exp" + "github.com/pluralsh/plural-cli/pkg/utils" + "github.com/urfave/cli" + "helm.sh/helm/v3/pkg/action" +) + +func init() { + cli.BashCompletionFlag = cli.BoolFlag{Name: "compgen", Hidden: true} +} + +const ApplicationName = "plural" + +type Plural struct { + client.Plural + HelmConfiguration *action.Configuration +} + +func (p *Plural) getCommands() []cli.Command { + return []cli.Command{ + { + Name: "version", + Aliases: []string{"v", "vsn"}, + Usage: "Gets cli version info", + Action: common.VersionInfo, + }, + { + Name: "down", + Usage: "destroys your management cluster and any apps installed on it", + Action: common.LatestVersion(common.HandleDown), + }, + { + Name: "diff", + Aliases: []string{"df"}, + Usage: "diffs the state of the current workspace with the deployed version and dumps results to diffs/", + ArgsUsage: "APP", + Action: common.LatestVersion(common.HandleDiff), + }, + { + Name: "clone", + Usage: "clones and decrypts a plural repo", + ArgsUsage: "URL", + Action: common.HandleClone, + }, + { + Name: "create", + Usage: "scaffolds the resources needed to create a new plural repository", + Action: common.LatestVersion(common.HandleScaffold), + Category: "Workspace", + }, + { + Name: "watch", + Usage: "watches applications until they become ready", + ArgsUsage: "REPO", + Action: common.LatestVersion(common.InitKubeconfig(common.RequireArgs(common.HandleWatch, []string{"REPO"}))), + Category: "Debugging", + }, + { + Name: "wait", + Usage: "waits on applications until they become ready", + ArgsUsage: "REPO", + Action: common.LatestVersion(common.RequireArgs(common.HandleWait, []string{"REPO"})), + Category: "Debugging", + }, + { + Name: "info", + Usage: "generates a console dashboard for the namespace of this repo", + ArgsUsage: "REPO", + Action: common.LatestVersion(common.RequireArgs(common.HandleInfo, []string{"REPO"})), + Category: "Debugging", + }, + { + Name: "apply", + Usage: "applys the current pluralfile", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "file, f", + Usage: "pluralfile to use", + }, + }, + Action: common.LatestVersion(common.Apply), + Category: "Publishing", + }, + + { + Name: "readme", + Aliases: []string{"b"}, + Usage: "generates the readme for your installation repo", + Category: "Workspace", + Action: common.LatestVersion(common.DownloadReadme), + }, + { + Name: "preflights", + Usage: "runs provider preflight checks", + Category: "Workspace", + Action: common.LatestVersion(common.Preflights), + }, + { + Name: "login", + Usage: "logs into plural and saves credentials to the current config profile", + Action: common.LatestVersion(common.HandleLogin), + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "endpoint", + Usage: "the endpoint for the plural installation you're working with", + }, + cli.StringFlag{ + Name: "service-account", + Usage: "email for the service account you'd like to use for this workspace", + }, + }, + Category: "User Profile", + }, + { + Name: "import", + Usage: "imports plural config from another file", + Action: common.LatestVersion(common.HandleImport), + Category: "User Profile", + }, + { + Name: "repair", + Usage: "commits any new encrypted changes in your local workspace automatically", + Action: common.LatestVersion(common.HandleRepair), + Category: "Workspace", + }, + { + Name: "serve", + Usage: "launch the server", + Action: common.LatestVersion(common.HandleServe), + Category: "Workspace", + }, + { + Name: "test", + Usage: "validate a values templace", + Action: common.LatestVersion(common.TestTemplate), + Category: "Publishing", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "templateType", + Usage: "Determines the template type. Go template by default", + }, + }, + }, + { + Name: "template", + Aliases: []string{"tpl"}, + Usage: "templates a helm chart to be uploaded to plural", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "values", + Usage: "the values file", + }, + }, + Action: common.LatestVersion(common.HandleHelmTemplate), + Category: "Publishing", + }, + { + Name: "changed", + Usage: "shows repos with pending changes", + Action: common.LatestVersion(common.Diffed), + Category: "Workspace", + }, + } +} + +func globalFlags() []cli.Flag { + return []cli.Flag{ + cli.StringFlag{ + Name: "profile-file", + Usage: "configure your config.yml profile `FILE`", + EnvVar: "PLURAL_PROFILE_FILE", + Destination: &conf.ProfileFile, + }, + cli.StringFlag{ + Name: "encryption-key-file", + Usage: "configure your encryption key `FILE`", + EnvVar: "PLURAL_ENCRYPTION_KEY_FILE", + Destination: &crypto.EncryptionKeyFile, + }, + cli.BoolFlag{ + Name: "debug", + Usage: "enable debug mode", + EnvVar: "PLURAL_DEBUG_ENABLE", + Destination: &utils.EnableDebug, + }, + cli.BoolFlag{ + Name: "bootstrap", + Usage: "enable bootstrap mode", + Destination: &common.BootstrapMode, + Hidden: !exp.IsFeatureEnabled(exp.EXP_PLURAL_CAPI), + }, + } +} + +func CreateNewApp(plural *Plural) *cli.App { + if plural == nil { + plural = &Plural{} + } + app := cli.NewApp() + app.Name = ApplicationName + app.Usage = "Tooling to manage your installed plural applications" + app.EnableBashCompletion = true + app.Flags = globalFlags() + commands := []cli.Command{ + api.Command(plural.Plural), + auth.Command(plural.Plural), + ai.Command(plural.Plural), + bootstrap.Command(plural.Plural), + bounce.Command(plural.Plural), + bundle.Command(plural.Plural), + buildcmd.Command(plural.Plural), + cd.Command(plural.Plural, plural.HelmConfiguration), + config.Command(), + cryptocmd.Command(plural.Plural), + clusters.Command(plural.Plural), + deploy.Command(plural.Plural), + destroy.Command(plural.Plural), + output.Command(), + ops.Command(plural.Plural), + profile.Command(), + pr.Command(), + proxy.Command(plural.Plural), + push.Command(plural.Plural), + repo.Command(plural.Plural), + repo.APICommand(plural.Plural), + stack.Command(plural.Plural), + log.Command(plural.Plural), + info.Command(plural.Plural), + cmdinit.Command(plural.Plural), + up.Command(plural.Plural), + upgrade.Command(plural.Plural), + workspace.Command(plural.Plural, plural.HelmConfiguration), + vpn.Command(plural.Plural), + } + commands = append(commands, plural.getCommands()...) + app.Commands = commands + links := link.Commands() + app.Commands = append(app.Commands, links...) + + return app +} diff --git a/cmd/plural/pr.go b/cmd/command/pr/pr.go similarity index 78% rename from cmd/plural/pr.go rename to cmd/command/pr/pr.go index adf00a9e..98dc2ee2 100644 --- a/cmd/plural/pr.go +++ b/cmd/command/pr/pr.go @@ -1,10 +1,20 @@ -package plural +package pr import ( "github.com/pluralsh/plural-cli/pkg/pr" "github.com/urfave/cli" ) +func Command() cli.Command { + return cli.Command{ + Name: "pull-requests", + Aliases: []string{"pr"}, + Usage: "Generate and manage pull requests", + Subcommands: prCommands(), + Category: "CD", + } +} + func prCommands() []cli.Command { return []cli.Command{ { diff --git a/cmd/plural/config.go b/cmd/command/profile/profile.go similarity index 60% rename from cmd/plural/config.go rename to cmd/command/profile/profile.go index 24476c3b..90ed2f57 100644 --- a/cmd/plural/config.go +++ b/cmd/command/profile/profile.go @@ -1,33 +1,20 @@ -package plural +package profile import ( - "io" "os" + "github.com/pluralsh/plural-cli/pkg/common" "github.com/pluralsh/plural-cli/pkg/config" "github.com/pluralsh/plural-cli/pkg/utils" "github.com/urfave/cli" ) -func configCommands() []cli.Command { - return []cli.Command{ - { - Name: "amend", - Usage: "modify config", - ArgsUsage: "[key] [value]", - Action: latestVersion(handleAmend), - }, - { - Name: "read", - Usage: "dumps config", - ArgsUsage: "", - Action: latestVersion(handleRead), - }, - { - Name: "import", - Usage: "imports a new config from a given token", - Action: latestVersion(handleConfigImport), - }, +func Command() cli.Command { + return cli.Command{ + Name: "profile", + Usage: "Commands for managing config profiles for plural", + Subcommands: profileCommands(), + Category: "User Profile", } } @@ -37,53 +24,29 @@ func profileCommands() []cli.Command { Name: "use", Usage: "moves the config in PROFILE to the current config", ArgsUsage: "PROFILE", - Action: latestVersion(handleUseProfile), + Action: common.LatestVersion(handleUseProfile), }, { Name: "save", Usage: "saves the current config as PROFILE", ArgsUsage: "PROFILE", - Action: latestVersion(handleSaveProfile), + Action: common.LatestVersion(handleSaveProfile), }, { Name: "show", Usage: "displays the configuration for the current profile", ArgsUsage: "PROFILE", - Action: latestVersion(handleRead), + Action: common.LatestVersion(handleRead), }, { Name: "list", Usage: "lists all saved profiles", ArgsUsage: "", - Action: latestVersion(listProfiles), + Action: common.LatestVersion(listProfiles), }, } } -func handleAmend(c *cli.Context) error { - return config.Amend(c.Args().Get(0), c.Args().Get(1)) -} - -func handleRead(c *cli.Context) error { - conf := config.Read() - d, err := conf.Marshal() - if err != nil { - return err - } - - os.Stdout.Write(d) - return nil -} - -func handleConfigImport(c *cli.Context) error { - data, err := io.ReadAll(os.Stdin) - if err != nil { - return err - } - - return config.FromToken(string(data)) -} - func handleUseProfile(c *cli.Context) error { return config.Profile(c.Args().Get(0)) } @@ -104,3 +67,14 @@ func listProfiles(c *cli.Context) error { return []string{profile.Metadata.Name, profile.Spec.Email, profile.Spec.BaseUrl()}, nil }) } + +func handleRead(c *cli.Context) error { + conf := config.Read() + d, err := conf.Marshal() + if err != nil { + return err + } + + os.Stdout.Write(d) + return nil +} diff --git a/cmd/plural/proxy.go b/cmd/command/proxy/proxy.go similarity index 59% rename from cmd/plural/proxy.go rename to cmd/command/proxy/proxy.go index 7f2ded80..96cbb2df 100644 --- a/cmd/plural/proxy.go +++ b/cmd/command/proxy/proxy.go @@ -1,24 +1,42 @@ -package plural +package proxy import ( + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" "github.com/pluralsh/plural-cli/pkg/config" "github.com/pluralsh/plural-cli/pkg/proxy" "github.com/urfave/cli" ) +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "proxy", + Usage: "proxies into running processes in your cluster", + Subcommands: p.proxyCommands(), + Category: "Debugging", + } +} + func (p *Plural) proxyCommands() []cli.Command { return []cli.Command{ { Name: "list", Usage: "lists proxy plugins for a repo", ArgsUsage: "REPO", - Action: latestVersion(initKubeconfig(requireArgs(p.handleProxyList, []string{"REPO"}))), + Action: common.LatestVersion(common.InitKubeconfig(common.RequireArgs(p.handleProxyList, []string{"REPO"}))), }, { Name: "connect", Usage: "connects to a named proxy for a repo", ArgsUsage: "REPO NAME", - Action: latestVersion(initKubeconfig(requireArgs(p.handleProxyConnect, []string{"REPO", "NAME"}))), + Action: common.LatestVersion(common.InitKubeconfig(common.RequireArgs(p.handleProxyConnect, []string{"REPO", "NAME"}))), }, } } diff --git a/cmd/plural/proxy_test.go b/cmd/command/proxy/proxy_test.go similarity index 81% rename from cmd/plural/proxy_test.go rename to cmd/command/proxy/proxy_test.go index 85b6c08d..1a10d785 100644 --- a/cmd/plural/proxy_test.go +++ b/cmd/command/proxy/proxy_test.go @@ -1,15 +1,19 @@ -package plural_test +package proxy_test import ( "os" "testing" + clientcmd "github.com/pluralsh/plural-cli/pkg/client" + + "github.com/pluralsh/plural-cli/pkg/common" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/pluralsh/plural-operator/apis/platform/v1alpha1" "github.com/stretchr/testify/mock" - "github.com/pluralsh/plural-cli/cmd/plural" + "github.com/pluralsh/plural-cli/cmd/command/plural" "github.com/pluralsh/plural-cli/pkg/test/mocks" "github.com/stretchr/testify/assert" ) @@ -51,10 +55,15 @@ func TestProxyList(t *testing.T) { client := mocks.NewClient(t) kube := mocks.NewKube(t) kube.On("ProxyList", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(test.proxyList, nil) - app := plural.CreateNewApp(&plural.Plural{Client: client, Kube: kube}) + app := plural.CreateNewApp(&plural.Plural{ + Plural: clientcmd.Plural{ + Client: client, + Kube: kube, + }, + }) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) assert.NoError(t, err) assert.Equal(t, test.expectedResponse, res) diff --git a/cmd/plural/push.go b/cmd/command/push/push.go similarity index 74% rename from cmd/plural/push.go rename to cmd/command/push/push.go index e5bcc7fe..2fe32b33 100644 --- a/cmd/plural/push.go +++ b/cmd/command/push/push.go @@ -1,14 +1,17 @@ -package plural +package push import ( "fmt" "os" "path/filepath" + "github.com/pluralsh/plural-cli/pkg/client" + + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/pluralsh/plural-cli/pkg/api" "github.com/pluralsh/plural-cli/pkg/config" "github.com/pluralsh/plural-cli/pkg/helm" - "github.com/pluralsh/plural-cli/pkg/pluralfile" scftmpl "github.com/pluralsh/plural-cli/pkg/scaffold/template" "github.com/pluralsh/plural-cli/pkg/utils" "github.com/pluralsh/plural-cli/pkg/utils/pathing" @@ -16,31 +19,47 @@ import ( "sigs.k8s.io/yaml" ) +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "push", + Usage: "utilities for pushing tf or helm packages", + Subcommands: p.pushCommands(), + Category: "Publishing", + } +} + func (p *Plural) pushCommands() []cli.Command { return []cli.Command{ { Name: "terraform", Usage: "pushes a terraform module", ArgsUsage: "path/to/module REPO", - Action: latestVersion(p.handleTerraformUpload), + Action: common.LatestVersion(p.handleTerraformUpload), }, { Name: "helm", Usage: "pushes a helm chart", ArgsUsage: "path/to/chart REPO", - Action: latestVersion(handleHelmUpload), + Action: common.LatestVersion(handleHelmUpload), }, { Name: "recipe", Usage: "pushes a recipe", ArgsUsage: "path/to/recipe.yaml REPO", - Action: latestVersion(p.handleRecipeUpload), + Action: common.LatestVersion(p.handleRecipeUpload), }, { Name: "artifact", Usage: "creates an artifact for the repo", ArgsUsage: "path/to/def.yaml REPO", - Action: latestVersion(p.handleArtifact), + Action: common.LatestVersion(p.handleArtifact), Flags: []cli.Flag{ cli.StringFlag{ Name: "platform", @@ -58,69 +77,17 @@ func (p *Plural) pushCommands() []cli.Command { Name: "crd", Usage: "registers a new crd for a chart", ArgsUsage: "path/to/def.yaml REPO CHART", - Action: latestVersion(p.createCrd), + Action: common.LatestVersion(p.createCrd), }, } } -func apply(c *cli.Context) error { - path, _ := os.Getwd() - var file = pathing.SanitizeFilepath(filepath.Join(path, "Pluralfile")) - if c.IsSet("file") { - file, _ = filepath.Abs(c.String("file")) - } - - if err := os.Chdir(filepath.Dir(file)); err != nil { - return err - } - - plrl, err := pluralfile.Parse(file) - if err != nil { - return err - } - - lock, err := plrl.Lock(file) - if err != nil { - return err - } - return plrl.Execute(file, lock) -} - func (p *Plural) handleTerraformUpload(c *cli.Context) error { p.InitPluralClient() _, err := p.UploadTerraform(c.Args().Get(0), c.Args().Get(1)) return api.GetErrorResponse(err, "UploadTerraform") } -func handleHelmTemplate(c *cli.Context) error { - path := c.String("values") - f, err := scftmpl.TmpValuesFile(path) - if err != nil { - return err - } - - defer func(name string) { - _ = os.Remove(name) - }(f.Name()) - - name := "default" - namespace := "default" - actionConfig, err := helm.GetActionConfig(namespace) - if err != nil { - return err - } - values, err := getValues(f.Name()) - if err != nil { - return err - } - res, err := helm.Template(actionConfig, name, namespace, c.Args().Get(0), false, false, values) - if err != nil { - return err - } - fmt.Println(string(res)) - return nil -} - func handleHelmUpload(c *cli.Context) error { conf := config.Read() pth, repo := c.Args().Get(0), c.Args().Get(1) diff --git a/cmd/plural/repos.go b/cmd/command/repo/repos.go similarity index 78% rename from cmd/plural/repos.go rename to cmd/command/repo/repos.go index f0fc2de5..7a4649fb 100644 --- a/cmd/plural/repos.go +++ b/cmd/command/repo/repos.go @@ -1,9 +1,13 @@ -package plural +package repo import ( "fmt" "strings" + "github.com/pluralsh/plural-cli/pkg/client" + + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/urfave/cli" "github.com/pluralsh/plural-cli/pkg/api" @@ -14,13 +18,41 @@ import ( "github.com/pluralsh/plural-cli/pkg/utils" ) +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "repos", + Usage: "view and manage plural repositories", + Subcommands: p.reposCommands(), + Category: "API", + } +} + +func APICommand(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "apps", + Usage: "view and manage plural repositories", + Subcommands: p.reposCommands(), + Category: "API", + } +} + func (p *Plural) reposCommands() []cli.Command { return []cli.Command{ { Name: "unlock", Usage: "unlocks installations in a repo that have breaking changes", ArgsUsage: "APP", - Action: latestVersion(requireArgs(p.handleUnlockRepo, []string{"APP"})), + Action: common.LatestVersion(common.RequireArgs(p.handleUnlockRepo, []string{"APP"})), }, { Name: "release", @@ -32,7 +64,7 @@ func (p *Plural) reposCommands() []cli.Command { Usage: "tag name for a given release channel, eg stable, warm, dev, prod", }, }, - Action: latestVersion(requireArgs(p.handleRelease, []string{"APP"})), + Action: common.LatestVersion(common.RequireArgs(p.handleRelease, []string{"APP"})), }, { Name: "reinstall", @@ -48,7 +80,7 @@ func (p *Plural) reposCommands() []cli.Command { { Name: "reset", Usage: "eliminates your current plural installation set, to change cloud provider or eject from plural", - Action: latestVersion(p.handleResetInstallations), + Action: common.LatestVersion(p.handleResetInstallations), }, { Name: "synced", @@ -59,7 +91,7 @@ func (p *Plural) reposCommands() []cli.Command { Name: "uninstall", Usage: "uninstall an app from the plural api", ArgsUsage: "APP", - Action: latestVersion(requireArgs(p.handleUninstall, []string{"APP"})), + Action: common.LatestVersion(common.RequireArgs(p.handleUninstall, []string{"APP"})), }, { Name: "list", @@ -75,7 +107,7 @@ func (p *Plural) reposCommands() []cli.Command { Usage: "format to print the repositories out, eg csv or default is table", }, }, - Action: latestVersion(p.handleListRepositories), + Action: common.LatestVersion(p.handleListRepositories), }, } } @@ -175,7 +207,7 @@ func (p *Plural) handleMarkSynced(c *cli.Context) error { func (p *Plural) handleResetInstallations(c *cli.Context) error { p.InitPluralClient() conf := config.Read() - if !confirm(fmt.Sprintf("Are you sure you want to reset installations for %s? This will also wipe all oidc providers and any other associated state in the plural api", conf.Email), "PLURAL_REPOS_RESET_CONFIRM") { + if !common.Confirm(fmt.Sprintf("Are you sure you want to reset installations for %s? This will also wipe all oidc providers and any other associated state in the plural api", conf.Email), "PLURAL_REPOS_RESET_CONFIRM") { return nil } diff --git a/cmd/plural/repos_test.go b/cmd/command/repo/repos_test.go similarity index 88% rename from cmd/plural/repos_test.go rename to cmd/command/repo/repos_test.go index 59a9cf7d..c2dfa965 100644 --- a/cmd/plural/repos_test.go +++ b/cmd/command/repo/repos_test.go @@ -1,13 +1,17 @@ -package plural_test +package repo_test import ( "os" "testing" + clientcmd "github.com/pluralsh/plural-cli/pkg/client" + + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "github.com/pluralsh/plural-cli/cmd/plural" + "github.com/pluralsh/plural-cli/cmd/command/plural" "github.com/pluralsh/plural-cli/pkg/api" "github.com/pluralsh/plural-cli/pkg/test/mocks" ) @@ -55,10 +59,12 @@ func TestListRepositories(t *testing.T) { t.Run(test.name, func(t *testing.T) { client := mocks.NewClient(t) client.On("ListRepositories", mock.AnythingOfType("string")).Return(test.repos, nil) - app := plural.CreateNewApp(&plural.Plural{Client: client}) + app := plural.CreateNewApp(&plural.Plural{Plural: clientcmd.Plural{ + Client: client, + }}) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) assert.NoError(t, err) assert.Equal(t, test.expectedResponse, res) }) diff --git a/cmd/plural/shell.go b/cmd/command/shell/shell.go similarity index 70% rename from cmd/plural/shell.go rename to cmd/command/shell/shell.go index daea1d5e..162f1fe9 100644 --- a/cmd/plural/shell.go +++ b/cmd/command/shell/shell.go @@ -1,22 +1,32 @@ -package plural +package shell import ( "os" "github.com/pluralsh/plural-cli/pkg/api" + "github.com/pluralsh/plural-cli/pkg/common" "github.com/pluralsh/plural-cli/pkg/config" - "github.com/pluralsh/plural-cli/pkg/crypto" + pkgcrypto "github.com/pluralsh/plural-cli/pkg/crypto" "github.com/pluralsh/plural-cli/pkg/utils" "github.com/pluralsh/plural-cli/pkg/utils/git" "github.com/urfave/cli" ) +func Command() cli.Command { + return cli.Command{ + Name: "shell", + Usage: "manages your cloud shell", + Subcommands: shellCommands(), + Category: "Workspace", + } +} + func shellCommands() []cli.Command { return []cli.Command{ { Name: "sync", Usage: "syncs the setup in your cloud shell locally", - Action: latestVersion(handleShellSync), + Action: common.LatestVersion(handleShellSync), Flags: []cli.Flag{ cli.StringFlag{ Name: "endpoint", @@ -31,14 +41,14 @@ func shellCommands() []cli.Command { { Name: "purge", Usage: "deletes your cloud shell", - Action: latestVersion(handleShellPurge), + Action: common.LatestVersion(handleShellPurge), }, } } func handleShellSync(c *cli.Context) error { if !config.Exists() { - if err := handleLogin(c); err != nil { + if err := common.HandleLogin(c); err != nil { return err } } @@ -49,7 +59,7 @@ func handleShellSync(c *cli.Context) error { return api.GetErrorResponse(err, "GetShell") } - if err := crypto.Setup(shell.AesKey); err != nil { + if err := pkgcrypto.Setup(shell.AesKey); err != nil { return err } @@ -62,17 +72,17 @@ func handleShellSync(c *cli.Context) error { if err := os.Chdir(dir); err != nil { return err } - if err := cryptoInit(c); err != nil { + if err := common.CryptoInit(c); err != nil { return err } - return handleUnlock(c) + return common.HandleUnlock(c) } var destoryShellConfirm = "Are you sure you want to destroy your cloud shell (you should either `plural destroy` anything deployed or `plural shell sync` to sync the contents locally)?" func handleShellPurge(c *cli.Context) error { - if ok := confirm(destoryShellConfirm, "PLURAL_SHELL_PURGE_CONFIRM"); !ok { + if ok := common.Confirm(destoryShellConfirm, "PLURAL_SHELL_PURGE_CONFIRM"); !ok { return nil } diff --git a/cmd/command/stack/stacks.go b/cmd/command/stack/stacks.go new file mode 100644 index 00000000..27b0f16c --- /dev/null +++ b/cmd/command/stack/stacks.go @@ -0,0 +1,84 @@ +package stack + +import ( + "fmt" + + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" + + "github.com/urfave/cli" + + "github.com/pluralsh/plural-cli/pkg/api" + "github.com/pluralsh/plural-cli/pkg/bundle" + "github.com/pluralsh/plural-cli/pkg/manifest" + "github.com/pluralsh/plural-cli/pkg/utils" +) + +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "stack", + Usage: "Commands for installing and discovering plural stacks", + Subcommands: p.stackCommands(), + } +} + +func (p *Plural) stackCommands() []cli.Command { + return []cli.Command{ + { + Name: "install", + Usage: "installs a plural stack for your current provider", + ArgsUsage: "NAME", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "refresh", + Usage: "re-enter the configuration for all bundles", + }, + }, + Action: common.Tracked(common.LatestVersion(common.Rooted(common.RequireArgs(p.stackInstall, []string{"stack-name"}))), "stack.install"), + }, + { + Name: "list", + Usage: "lists stacks to potentially install", + Flags: []cli.Flag{ + cli.BoolTFlag{ + Name: "account", + Usage: "only list stacks within your account", + }, + }, + Action: common.LatestVersion(common.Rooted(p.stackList)), + }, + } +} + +func (p *Plural) stackInstall(c *cli.Context) (err error) { + name := c.Args().Get(0) + man, err := manifest.FetchProject() + if err != nil { + return + } + + p.InitPluralClient() + err = bundle.Stack(p.Client, name, man.Provider, c.Bool("refresh")) + utils.Note("To edit the configuration you've just entered, edit the context.yaml file at the root of your repo, or run with the --refresh flag\n") + return +} + +func (p *Plural) stackList(c *cli.Context) (err error) { + p.InitPluralClient() + stacks, err := p.ListStacks(c.Bool("account")) + if err != nil { + return api.GetErrorResponse(err, "ListStacks") + } + + headers := []string{"Name", "Description", "Featured"} + return utils.PrintTable(stacks, headers, func(s *api.Stack) ([]string, error) { + return []string{s.Name, s.Description, fmt.Sprintf("%v", s.Featured)}, nil + }) +} diff --git a/cmd/plural/up.go b/cmd/command/up/up.go similarity index 51% rename from cmd/plural/up.go rename to cmd/command/up/up.go index a78b83ed..4d5f9871 100644 --- a/cmd/plural/up.go +++ b/cmd/command/up/up.go @@ -1,23 +1,52 @@ -package plural +package up import ( "fmt" - "github.com/urfave/cli" - + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" "github.com/pluralsh/plural-cli/pkg/up" "github.com/pluralsh/plural-cli/pkg/utils" "github.com/pluralsh/plural-cli/pkg/utils/git" + "github.com/urfave/cli" ) -const ( - affirmUp = "Are you ready to set up your initial management cluster? You can check the generated terraform/helm to confirm everything looks good first" - affirmDown = "Are you ready to destroy your plural infrastructure? This will destroy all k8s clusters and any data stored within" -) +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "up", + Usage: "sets up your repository and an initial management cluster", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "endpoint", + Usage: "the endpoint for the plural installation you're working with", + }, + cli.StringFlag{ + Name: "service-account", + Usage: "email for the service account you'd like to use for this workspace", + }, + cli.BoolFlag{ + Name: "ignore-preflights", + Usage: "whether to ignore preflight check failures prior to init", + }, + cli.StringFlag{ + Name: "commit", + Usage: "commits your changes with this message", + }, + }, + Action: common.LatestVersion(p.handleUp), + } +} func (p *Plural) handleUp(c *cli.Context) error { // provider.IgnoreProviders([]string{"GENERIC", "KIND"}) - if err := p.handleInit(c); err != nil { + if err := p.HandleInit(c); err != nil { return err } p.InitPluralClient() @@ -40,13 +69,13 @@ func (p *Plural) handleUp(c *cli.Context) error { return err } - if !affirm(affirmUp, "PLURAL_UP_AFFIRM_DEPLOY") { + if !common.Affirm(common.AffirmUp, "PLURAL_UP_AFFIRM_DEPLOY") { return fmt.Errorf("cancelled deploy") } if err := ctx.Deploy(func() error { utils.Highlight("\n==> Commit and push your configuration\n\n") - if commit := commitMsg(c); commit != "" { + if commit := common.CommitMsg(c); commit != "" { utils.Highlight("Pushing upstream...\n") return git.Sync(repoRoot, commit, c.Bool("force")) } @@ -59,16 +88,3 @@ func (p *Plural) handleUp(c *cli.Context) error { utils.Highlight("Feel free to use `terrafrom` as you normally would, and leverage the gitops setup we've generated in the `apps/` subfolder\n") return nil } - -func (p *Plural) handleDown(c *cli.Context) error { - if !affirm(affirmDown, "PLURAL_DOWN_AFFIRM_DESTROY") { - return fmt.Errorf("cancelled destroy") - } - - ctx, err := up.Build() - if err != nil { - return err - } - - return ctx.Destroy() -} diff --git a/cmd/plural/upgrade.go b/cmd/command/upgrade/upgrade.go similarity index 50% rename from cmd/plural/upgrade.go rename to cmd/command/upgrade/upgrade.go index 3a91f891..44087bdc 100644 --- a/cmd/plural/upgrade.go +++ b/cmd/command/upgrade/upgrade.go @@ -1,13 +1,38 @@ -package plural +package upgrade import ( "io" "os" + "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/pluralsh/plural-cli/pkg/api" "github.com/urfave/cli" ) +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "upgrade", + Usage: "creates an upgrade in the upgrade queue QUEUE for application REPO", + ArgsUsage: "QUEUE REPO", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "f", + Usage: "file containing upgrade contents, use - for stdin", + }, + }, + Action: common.LatestVersion(common.RequireArgs(p.handleUpgrade, []string{"QUEUE", "REPO"})), + } +} + func (p *Plural) handleUpgrade(c *cli.Context) (err error) { p.InitPluralClient() queue, repo := c.Args().Get(0), c.Args().Get(1) diff --git a/cmd/plural/vpn.go b/cmd/command/vpn/vpn.go similarity index 85% rename from cmd/plural/vpn.go rename to cmd/command/vpn/vpn.go index 44266ff0..5ef6623d 100644 --- a/cmd/plural/vpn.go +++ b/cmd/command/vpn/vpn.go @@ -1,4 +1,4 @@ -package plural +package vpn import ( "fmt" @@ -6,6 +6,10 @@ import ( "path/filepath" "strconv" + "github.com/pluralsh/plural-cli/pkg/client" + + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/olekukonko/tablewriter" "github.com/pluralsh/plural-cli/pkg/config" "github.com/pluralsh/plural-cli/pkg/utils" @@ -25,6 +29,22 @@ const ( wireguardNotInstalledError = "wireguard is not installed. run `plural bundle list wireguard` to find the bundle to install" ) +type Plural struct { + client.Plural +} + +func Command(clients client.Plural) cli.Command { + p := Plural{ + Plural: clients, + } + return cli.Command{ + Name: "vpn", + Usage: "interacting with the plural vpn", + Subcommands: p.vpnCommands(), + Category: "Workspace", + } +} + func (p *Plural) vpnCommands() []cli.Command { return []cli.Command{ { @@ -46,7 +66,7 @@ func (p *Plural) vpnCommands() []cli.Command { Name: "client-config", ArgsUsage: "NAME", Usage: "get the config for a vpn client for a server", - Action: latestVersion(requireArgs(highlighted(p.vpnInstalled(initKubeconfig(p.handleWireguardPeerConfig))), []string{"NAME"})), + Action: common.LatestVersion(common.RequireArgs(common.Highlighted(p.vpnInstalled(common.InitKubeconfig(p.handleWireguardPeerConfig))), []string{"NAME"})), Flags: []cli.Flag{ cli.StringFlag{ Name: "server", @@ -66,12 +86,12 @@ func (p *Plural) vpnListCommands() []cli.Command { { Name: "servers", Usage: "lists vpn servers", - Action: latestVersion(highlighted(p.vpnInstalled(initKubeconfig(p.handleWireguardServerList)))), + Action: common.LatestVersion(common.Highlighted(p.vpnInstalled(common.InitKubeconfig(p.handleWireguardServerList)))), }, { Name: "clients", Usage: "lists vpn clients for a server", - Action: latestVersion(highlighted(p.vpnInstalled(initKubeconfig(p.handleWireguardPeerList)))), + Action: common.LatestVersion(common.Highlighted(p.vpnInstalled(common.InitKubeconfig(p.handleWireguardPeerList)))), Flags: []cli.Flag{ cli.StringFlag{ Name: "server", @@ -88,7 +108,7 @@ func (p *Plural) vpnCreateCommands() []cli.Command { Name: "client", ArgsUsage: "NAME", Usage: "create a new vpn client for a server", - Action: latestVersion(requireArgs(highlighted(p.vpnInstalled(initKubeconfig(p.handleWireguardPeerCreate))), []string{"NAME"})), + Action: common.LatestVersion(common.RequireArgs(common.Highlighted(p.vpnInstalled(common.InitKubeconfig(p.handleWireguardPeerCreate))), []string{"NAME"})), Flags: []cli.Flag{ cli.StringFlag{ Name: "server", @@ -105,7 +125,7 @@ func (p *Plural) vpnDeleteCommands() []cli.Command { Name: "client", ArgsUsage: "NAME", Usage: "delete a vpn client for a server", - Action: latestVersion(requireArgs(highlighted(p.vpnInstalled(initKubeconfig(p.handleWireguardPeerDelete))), []string{"NAME"})), + Action: common.LatestVersion(common.RequireArgs(common.Highlighted(p.vpnInstalled(common.InitKubeconfig(p.handleWireguardPeerDelete))), []string{"NAME"})), Flags: []cli.Flag{ cli.StringFlag{ Name: "server", diff --git a/cmd/plural/vpn_test.go b/cmd/command/vpn/vpn_test.go similarity index 94% rename from cmd/plural/vpn_test.go rename to cmd/command/vpn/vpn_test.go index d68e02db..ee745ba7 100644 --- a/cmd/plural/vpn_test.go +++ b/cmd/command/vpn/vpn_test.go @@ -1,11 +1,13 @@ -package plural_test +package vpn_test import ( "os" "testing" - "github.com/pluralsh/plural-cli/cmd/plural" + "github.com/pluralsh/plural-cli/cmd/command/plural" "github.com/pluralsh/plural-cli/pkg/api" + pluralclient "github.com/pluralsh/plural-cli/pkg/client" + "github.com/pluralsh/plural-cli/pkg/common" "github.com/pluralsh/plural-cli/pkg/test/mocks" vpnv1alpha1 "github.com/pluralsh/plural-operator/apis/vpn/v1alpha1" "github.com/stretchr/testify/assert" @@ -60,10 +62,15 @@ func TestServerList(t *testing.T) { kube := mocks.NewKube(t) client.On("GetInstallation", "wireguard").Return(test.installation, nil) kube.On("WireguardServerList", "wireguard").Return(test.servers, nil) - app := plural.CreateNewApp(&plural.Plural{Client: client, Kube: kube}) + app := plural.CreateNewApp(&plural.Plural{ + Plural: pluralclient.Plural{ + Client: client, + Kube: kube, + }, + }) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) assert.NoError(t, err) assert.Equal(t, test.expectedResponse, res) @@ -200,10 +207,15 @@ func TestClientList(t *testing.T) { kube := mocks.NewKube(t) client.On("GetInstallation", "wireguard").Return(test.installation, nil) kube.On("WireguardPeerList", "wireguard").Return(test.peers, nil) - app := plural.CreateNewApp(&plural.Plural{Client: client, Kube: kube}) + app := plural.CreateNewApp(&plural.Plural{ + Plural: pluralclient.Plural{ + Client: client, + Kube: kube, + }, + }) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) assert.NoError(t, err) assert.Equal(t, test.expectedResponse, res) @@ -327,10 +339,15 @@ func TestClientCreate(t *testing.T) { kube.On("WireguardServer", "wireguard", mock.AnythingOfType("string")).Return(test.server, nil) kube.On("WireguardPeerCreate", "wireguard", mock.AnythingOfType("*v1alpha1.WireguardPeer")).Return(test.peer, nil) } - app := plural.CreateNewApp(&plural.Plural{Client: client, Kube: kube}) + app := plural.CreateNewApp(&plural.Plural{ + Plural: pluralclient.Plural{ + Client: client, + Kube: kube, + }, + }) app.HelpName = plural.ApplicationName os.Args = test.args - res, err := captureStdout(app, os.Args) + res, err := common.CaptureStdout(app, os.Args) if test.expectedError != "" { assert.Equal(t, err.Error(), test.expectedError) } else { diff --git a/cmd/plural/workspace.go b/cmd/command/workspace/workspace.go similarity index 78% rename from cmd/plural/workspace.go rename to cmd/command/workspace/workspace.go index 5b1bdf3b..c763faf0 100644 --- a/cmd/plural/workspace.go +++ b/cmd/command/workspace/workspace.go @@ -1,4 +1,4 @@ -package plural +package workspace import ( "fmt" @@ -6,22 +6,44 @@ import ( "path/filepath" "strings" + "github.com/pluralsh/plural-cli/pkg/client" + "helm.sh/helm/v3/pkg/action" + + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/urfave/cli" "github.com/pluralsh/plural-cli/pkg/helm" "github.com/pluralsh/plural-cli/pkg/provider" - "github.com/pluralsh/plural-cli/pkg/scaffold" "github.com/pluralsh/plural-cli/pkg/utils" - "github.com/pluralsh/plural-cli/pkg/utils/git" "github.com/pluralsh/plural-cli/pkg/wkspace" ) +type Plural struct { + client.Plural + HelmConfiguration *action.Configuration +} + +func Command(clients client.Plural, helmConfiguration *action.Configuration) cli.Command { + p := Plural{ + Plural: clients, + HelmConfiguration: helmConfiguration, + } + return cli.Command{ + Name: "workspace", + Aliases: []string{"wkspace"}, + Usage: "Commands for managing installations in your workspace", + Subcommands: p.workspaceCommands(), + Category: "Workspace", + } +} + func (p *Plural) workspaceCommands() []cli.Command { return []cli.Command{ { Name: "kube-init", Usage: "generates kubernetes credentials for this subworkspace", - Action: latestVersion(kubeInit), + Action: common.LatestVersion(kubeInit), }, { Name: "readme", @@ -33,7 +55,7 @@ func (p *Plural) workspaceCommands() []cli.Command { Usage: "output to stdout instead of to a file", }, }, - Action: latestVersion(func(c *cli.Context) error { return appReadme(c.Args().Get(0), c.Bool("dry-run")) }), + Action: common.LatestVersion(func(c *cli.Context) error { return common.AppReadme(c.Args().Get(0), c.Bool("dry-run")) }), }, { Name: "helm", @@ -57,43 +79,43 @@ func (p *Plural) workspaceCommands() []cli.Command { Usage: "have helm wait until all pods are in ready state", }, }, - Action: latestVersion(initKubeconfig(p.bounceHelm)), + Action: common.LatestVersion(common.InitKubeconfig(p.bounceHelm)), }, { Name: "helm-diff", Usage: "diffs the helm release for this subworkspace", ArgsUsage: "NAME", - Action: latestVersion(p.diffHelm), + Action: common.LatestVersion(p.diffHelm), }, { Name: "helm-deps", Usage: "updates the helm dependencies for this workspace", ArgsUsage: "PATH", - Action: latestVersion(updateDeps), + Action: common.LatestVersion(updateDeps), }, { Name: "terraform-diff", Usage: "diffs the helm release for this subworkspace", ArgsUsage: "NAME", - Action: latestVersion(p.diffTerraform), + Action: common.LatestVersion(p.diffTerraform), }, { Name: "crds", Usage: "installs the crds for this repo", ArgsUsage: "NAME", - Action: latestVersion(initKubeconfig(p.createCrds)), + Action: common.LatestVersion(common.InitKubeconfig(p.createCrds)), }, { Name: "helm-template", Usage: "templates the helm values to stdout", ArgsUsage: "NAME", - Action: latestVersion(requireArgs(p.templateHelm, []string{"NAME"})), + Action: common.LatestVersion(common.RequireArgs(p.templateHelm, []string{"NAME"})), }, { Name: "helm-mapkubeapis", Usage: "updates in-place Helm release metadata that contains deprecated or removed Kubernetes APIs to a new instance with supported Kubernetes APIs", ArgsUsage: "NAME", - Action: latestVersion(requireArgs(p.mapkubeapis, []string{"NAME"})), + Action: common.LatestVersion(common.RequireArgs(p.mapkubeapis, []string{"NAME"})), }, } } @@ -218,13 +240,3 @@ func (p *Plural) mapkubeapis(c *cli.Context) error { return minimal.MapKubeApis() } - -func appReadme(name string, dryRun bool) error { - repoRoot, err := git.Root() - if err != nil { - return err - } - - dir := filepath.Join(repoRoot, name, "helm", name) - return scaffold.Readme(dir, dryRun) -} diff --git a/cmd/plural/workspace_test.go b/cmd/command/workspace/workspace_test.go similarity index 92% rename from cmd/plural/workspace_test.go rename to cmd/command/workspace/workspace_test.go index ad9133c5..316deb84 100644 --- a/cmd/plural/workspace_test.go +++ b/cmd/command/workspace/workspace_test.go @@ -1,4 +1,4 @@ -package plural_test +package workspace_test import ( "io" @@ -6,7 +6,9 @@ import ( "path/filepath" "testing" - "github.com/pluralsh/plural-cli/cmd/plural" + "github.com/pluralsh/plural-cli/pkg/common" + + "github.com/pluralsh/plural-cli/cmd/command/plural" "github.com/pluralsh/plural-cli/pkg/manifest" "github.com/pluralsh/plural-cli/pkg/utils" "github.com/pluralsh/plural-cli/pkg/utils/git" @@ -33,7 +35,7 @@ func TestHelmCommands(t *testing.T) { _ = os.RemoveAll(path) _ = os.Chdir(currentDir) }(dir, currentDir) - tFiles, err := filepath.Abs("../../pkg/test/helm") + tFiles, err := filepath.Abs("../../../pkg/test/helm") assert.NoError(t, err) err = utils.CopyDir(tFiles, filepath.Join(dir, subchart)) assert.NoError(t, err) @@ -101,10 +103,10 @@ func TestHelmCommands(t *testing.T) { assert.NoError(t, err) }() - app := plural.CreateNewApp(&plural.Plural{Client: nil, HelmConfiguration: actionConfig}) + app := plural.CreateNewApp(&plural.Plural{HelmConfiguration: actionConfig}) app.HelpName = plural.ApplicationName os.Args = test.args - output, err := captureStdout(app, os.Args) + output, err := common.CaptureStdout(app, os.Args) assert.NoError(t, err) if test.expectedOutput != "" { expected, err := utils.ReadFile(test.expectedOutput) diff --git a/cmd/plural/constants.go b/cmd/plural/constants.go deleted file mode 100644 index 62c5c569..00000000 --- a/cmd/plural/constants.go +++ /dev/null @@ -1,3 +0,0 @@ -package plural - -const backupMsg = "Would you like to back up your encryption key to plural? If you chose to manage it yourself, you can find it at ~/.plural/key" diff --git a/cmd/plural/deploy.go b/cmd/plural/deploy.go deleted file mode 100644 index 56d9a298..00000000 --- a/cmd/plural/deploy.go +++ /dev/null @@ -1,500 +0,0 @@ -package plural - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/AlecAivazis/survey/v2" - "github.com/pluralsh/plural-cli/pkg/api" - "github.com/pluralsh/plural-cli/pkg/application" - "github.com/pluralsh/plural-cli/pkg/bootstrap" - "github.com/pluralsh/plural-cli/pkg/diff" - "github.com/pluralsh/plural-cli/pkg/executor" - "github.com/pluralsh/plural-cli/pkg/kubernetes" - "github.com/pluralsh/plural-cli/pkg/manifest" - "github.com/pluralsh/plural-cli/pkg/scaffold" - "github.com/pluralsh/plural-cli/pkg/utils" - "github.com/pluralsh/plural-cli/pkg/utils/errors" - "github.com/pluralsh/plural-cli/pkg/utils/git" - "github.com/pluralsh/plural-cli/pkg/utils/pathing" - "github.com/pluralsh/plural-cli/pkg/wkspace" - "github.com/pluralsh/polly/algorithms" - "github.com/pluralsh/polly/containers" - "github.com/urfave/cli" -) - -const Bootstrap = "bootstrap" - -func (p *Plural) getSortedInstallations(repo string) ([]*api.Installation, error) { - p.InitPluralClient() - installations, err := p.GetInstallations() - if err != nil { - return installations, api.GetErrorResponse(err, "GetInstallations") - } - - if len(installations) == 0 { - return installations, fmt.Errorf("no installations present, run `plural bundle install ` to install your first app") - } - - sorted, err := wkspace.UntilRepo(p.Client, repo, installations) - if err != nil { - sorted = installations // we don't know all the dependencies yet - } - - return sorted, nil -} - -func (p *Plural) allSortedRepos() ([]string, error) { - p.InitPluralClient() - insts, err := p.GetInstallations() - if err != nil { - return nil, api.GetErrorResponse(err, "GetInstallations") - } - - return wkspace.SortAndFilter(insts) -} - -func getSortedNames(filter bool) ([]string, error) { - diffed, err := wkspace.DiffedRepos() - if err != nil { - return nil, err - } - - sorted, err := wkspace.TopSortNames(diffed) - if err != nil { - return nil, err - } - - if filter { - repos := containers.ToSet(diffed) - return algorithms.Filter(sorted, repos.Has), nil - } - - return sorted, nil -} - -func diffed(_ *cli.Context) error { - diffed, err := wkspace.DiffedRepos() - if err != nil { - return err - } - - for _, d := range diffed { - fmt.Println(d) - } - - return nil -} - -func (p *Plural) build(c *cli.Context) error { - p.InitPluralClient() - force := c.Bool("force") - if err := CheckGitCrypt(c); err != nil { - return errors.ErrorWrap(errNoGit, "Failed to scan your repo for secrets to encrypt them") - } - - if c.IsSet("only") { - installation, err := p.GetInstallation(c.String("only")) - if err != nil { - return api.GetErrorResponse(err, "GetInstallation") - } else if installation == nil { - return utils.HighlightError(fmt.Errorf("%s is not installed. Please install it with `plural bundle install`", c.String("only"))) - } - - return p.doBuild(installation, force) - } - - installations, err := p.getSortedInstallations("") - if err != nil { - return err - } - - for _, installation := range installations { - if err := p.doBuild(installation, force); err != nil { - return err - } - } - return nil -} - -func (p *Plural) doBuild(installation *api.Installation, force bool) error { - repoName := installation.Repository.Name - fmt.Printf("Building workspace for %s\n", repoName) - - if !wkspace.Configured(repoName) { - fmt.Printf("You have not locally configured %s but have it registered as an installation in our api, ", repoName) - fmt.Printf("either delete it with `plural apps uninstall %s` or install it locally via a bundle in `plural bundle list %s`\n", repoName, repoName) - return nil - } - - workspace, err := wkspace.New(p.Client, installation) - if err != nil { - return err - } - - vsn, ok := workspace.RequiredCliVsn() - if ok && !versionValid(vsn) { - return fmt.Errorf("Your cli version is not sufficient to complete this build, please update to at least %s", vsn) - } - - if err := workspace.Prepare(); err != nil { - return err - } - - build, err := scaffold.Scaffolds(workspace) - if err != nil { - return err - } - - err = build.Execute(workspace, force) - if err == nil { - utils.Success("Finished building %s\n\n", repoName) - } - - workspace.PrintLinks() - - appReadme(repoName, false) // nolint:errcheck - return err -} - -func (p *Plural) info(c *cli.Context) error { - p.InitPluralClient() - repo := c.Args().Get(0) - installation, err := p.GetInstallation(repo) - if err != nil { - return api.GetErrorResponse(err, "GetInstallation") - } - if installation == nil { - return fmt.Errorf("You have not installed %s", repo) - } - - return scaffold.Notes(installation) -} - -func (p *Plural) deploy(c *cli.Context) error { - p.InitPluralClient() - verbose := c.Bool("verbose") - repoRoot, err := git.Root() - if err != nil { - return err - } - - project, err := manifest.FetchProject() - if err != nil { - return err - } - - var sorted []string - switch { - case len(c.StringSlice("from")) > 0: - sorted, err = wkspace.AllDependencies(c.StringSlice("from")) - case c.Bool("all"): - sorted, err = p.allSortedRepos() - default: - sorted, err = getSortedNames(true) - } - if err != nil { - return err - } - - fmt.Printf("Deploying applications [%s] in topological order\n\n", strings.Join(sorted, ", ")) - - ignoreConsole := c.Bool("ignore-console") - for _, repo := range sorted { - if ignoreConsole && (repo == "console" || repo == Bootstrap) { - continue - } - - if repo == Bootstrap && project.ClusterAPI { - ready, err := bootstrap.CheckClusterReadiness(project.Cluster, Bootstrap) - - // Stop if cluster exists, but it is not ready yet. - if err != nil && err.Error() == bootstrap.ClusterNotReadyError { - return err - } - - // If cluster does not exist bootstrap needs to be done first. - if !ready { - err := bootstrap.BootstrapCluster(RunPlural) - if err != nil { - return err - } - } - } - - execution, err := executor.GetExecution(pathing.SanitizeFilepath(filepath.Join(repoRoot, repo)), "deploy") - if err != nil { - return err - } - - if err := execution.Execute("deploying", verbose); err != nil { - utils.Note("It looks like your deployment failed. This may be a transient issue and rerunning the `plural deploy` command may resolve it. Or, feel free to reach out to us on discord (https://discord.gg/bEBAMXV64s) or Intercom and we should be able to help you out\n") - return err - } - - fmt.Printf("\n") - - installation, err := p.GetInstallation(repo) - if err != nil { - return api.GetErrorResponse(err, "GetInstallation") - } - if installation == nil { - return fmt.Errorf("The %s was unistalled, run `plural bundle install %s ` ", repo, repo) - } - - if err := p.Client.MarkSynced(repo); err != nil { - utils.Warn("failed to mark %s as synced, this is not a critical error but might drift state in our api, you can run `plural repos synced %s` to mark it manually", repo, repo) - } - - if c.Bool("silence") { - continue - } - - if man, err := fetchManifest(repo); err == nil && man.Wait { - if kubeConf, err := kubernetes.KubeConfig(); err == nil { - fmt.Printf("Waiting for %s to become ready...\n", repo) - if err := application.SilentWait(kubeConf, repo); err != nil { - return err - } - fmt.Println("") - } - } - - if err := scaffold.Notes(installation); err != nil { - return err - } - } - - utils.Highlight("\n==> Commit and push your changes to record your deployment\n\n") - - if commit := commitMsg(c); commit != "" { - utils.Highlight("Pushing upstream...\n") - return git.Sync(repoRoot, commit, c.Bool("force")) - } - - return nil -} - -func commitMsg(c *cli.Context) string { - if commit := c.String("commit"); commit != "" { - return commit - } - - if !c.Bool("silence") { - var commit string - if err := survey.AskOne(&survey.Input{Message: "Enter a commit message (empty to not commit right now)"}, &commit); err != nil { - return "" - } - return commit - } - - return "" -} - -func handleDiff(_ *cli.Context) error { - repoRoot, err := git.Root() - if err != nil { - return err - } - - sorted, err := getSortedNames(true) - if err != nil { - return err - } - - fmt.Printf("Diffing applications [%s] in topological order\n\n", strings.Join(sorted, ", ")) - - for _, repo := range sorted { - d, err := diff.GetDiff(pathing.SanitizeFilepath(filepath.Join(repoRoot, repo)), "diff") - if err != nil { - return err - } - - if err := d.Execute(); err != nil { - return err - } - - fmt.Printf("\n") - } - return nil -} - -func (p *Plural) bounce(c *cli.Context) error { - p.InitPluralClient() - repoRoot, err := git.Root() - if err != nil { - return err - } - repoName := c.Args().Get(0) - - if repoName != "" { - installation, err := p.GetInstallation(repoName) - if err != nil { - return api.GetErrorResponse(err, "GetInstallation") - } - return p.doBounce(repoRoot, installation) - } - - installations, err := p.getSortedInstallations(repoName) - if err != nil { - return err - } - - for _, installation := range installations { - if err := p.doBounce(repoRoot, installation); err != nil { - return err - } - } - return nil -} - -func (p *Plural) doBounce(repoRoot string, installation *api.Installation) error { - p.InitPluralClient() - repoName := installation.Repository.Name - utils.Warn("bouncing deployments in %s\n", repoName) - workspace, err := wkspace.New(p.Client, installation) - if err != nil { - return err - } - - if err := os.Chdir(pathing.SanitizeFilepath(filepath.Join(repoRoot, repoName))); err != nil { - return err - } - return workspace.Bounce() -} - -func (p *Plural) destroy(c *cli.Context) error { - p.InitPluralClient() - repoName := c.Args().Get(0) - repoRoot, err := git.Root() - if err != nil { - return err - } - force := c.Bool("force") - all := c.Bool("all") - - project, err := manifest.FetchProject() - if err != nil { - return err - } - - infix := "this workspace" - if repoName != "" { - infix = repoName - } else if !all { - return fmt.Errorf("you must either specify an individual application or `--all` to destroy the entire workspace") - } - - if !force && !confirm(fmt.Sprintf("Are you sure you want to destroy %s?", infix), "PLURAL_DESTROY_CONFIRM") { - return nil - } - - delete := force || affirm("Do you want to uninstall your applications from the plural api as well?", "PLURAL_DESTROY_AFFIRM_UNINSTALL_APPS") - - if repoName != "" { - installation, err := p.GetInstallation(repoName) - if err != nil { - return api.GetErrorResponse(err, "GetInstallation") - } - - if installation == nil { - return fmt.Errorf("No installation for app %s to destroy, if the app is still in your repo, you can always run cd %s/terraform && terraform destroy", repoName, repoName) - } - - return p.doDestroy(repoRoot, installation, delete, project.ClusterAPI) - } - - installations, err := p.getSortedInstallations(repoName) - if err != nil { - return err - } - - from := c.String("from") - started := from == "" - for i := len(installations) - 1; i >= 0; i-- { - installation := installations[i] - if installation.Repository.Name == from { - started = true - } - - if !started { - continue - } - - if err := p.doDestroy(repoRoot, installation, delete, project.ClusterAPI); err != nil { - return err - } - } - - man, _ := manifest.FetchProject() - if err := p.DeleteEabCredential(man.Cluster, man.Provider); err != nil { - fmt.Printf("no eab key to delete %s\n", err) - } - - if repoName == "" { - utils.Success("Finished destroying workspace\n") - utils.Note("if you want to recreate this workspace, be sure to rename the cluster to ensure a clean redeploy") - man, err := manifest.FetchProject() - if err != nil { - return err - } - if err := p.DestroyCluster(man.Network.Subdomain, man.Cluster, man.Provider); err != nil { - return api.GetErrorResponse(err, "DestroyCluster") - } - } - - utils.Highlight("\n==> Commit and push your changes to record your workspace changes\n\n") - - if commit := commitMsg(c); commit != "" { - utils.Highlight("Pushing upstream...\n") - return git.Sync(repoRoot, commit, force) - } - - return nil -} - -func (p *Plural) doDestroy(repoRoot string, installation *api.Installation, delete, clusterAPI bool) error { - p.InitPluralClient() - if err := os.Chdir(repoRoot); err != nil { - return err - } - repo := installation.Repository.Name - if ctx, err := manifest.FetchContext(); err == nil && ctx.Protected(repo) { - return fmt.Errorf("This app is protected, you cannot plural destroy without updating context.yaml") - } - - utils.Error("\nDestroying application %s\n", repo) - workspace, err := wkspace.New(p.Client, installation) - if err != nil { - return err - } - - if repo == Bootstrap && clusterAPI { - if err = bootstrap.DestroyCluster(workspace.Destroy, RunPlural); err != nil { - return err - } - - } else { - if err := workspace.Destroy(); err != nil { - return err - } - } - - if delete { - utils.Highlight("Uninstalling %s from the plural api as well...\n", repo) - return p.Client.DeleteInstallation(installation.Id) - } - - return nil -} - -func fetchManifest(repo string) (*manifest.Manifest, error) { - p, err := manifest.ManifestPath(repo) - if err != nil { - return nil, err - } - - return manifest.Read(p) -} diff --git a/cmd/plural/errors.go b/cmd/plural/errors.go deleted file mode 100644 index 2b69f873..00000000 --- a/cmd/plural/errors.go +++ /dev/null @@ -1,11 +0,0 @@ -package plural - -import ( - "fmt" -) - -var ( - errNoGit = fmt.Errorf("Could not compare current workspace to origin. Do you have an `origin` remote configured, or does your repo not have an initial commit?") - errRemoteDiff = fmt.Errorf("Your local workspace is not in sync with remote. Either `git pull` recent changes or `git push` any missed changes. Also confirm you can authenticate to the origin remote, which you can see with `git remote -v`") - errUnlock = fmt.Errorf("could not decrypt your repo, this is likely due to using the wrong key at ~/.plural/key. The original key might be in a backup or on your previous machine.") -) diff --git a/main.go b/cmd/plural/main.go similarity index 85% rename from main.go rename to cmd/plural/main.go index 0320c874..32eaed26 100644 --- a/main.go +++ b/cmd/plural/main.go @@ -5,8 +5,7 @@ import ( "os" "github.com/fatih/color" - - "github.com/pluralsh/plural-cli/cmd/plural" + "github.com/pluralsh/plural-cli/cmd/command/plural" ) func main() { diff --git a/cmd/plural/plural.go b/cmd/plural/plural.go deleted file mode 100644 index 0cd61643..00000000 --- a/cmd/plural/plural.go +++ /dev/null @@ -1,588 +0,0 @@ -package plural - -import ( - "fmt" - "os" - - "github.com/pluralsh/plural-cli/pkg/api" - "github.com/pluralsh/plural-cli/pkg/config" - "github.com/pluralsh/plural-cli/pkg/console" - "github.com/pluralsh/plural-cli/pkg/crypto" - "github.com/pluralsh/plural-cli/pkg/exp" - "github.com/pluralsh/plural-cli/pkg/kubernetes" - "github.com/pluralsh/plural-cli/pkg/manifest" - "github.com/pluralsh/plural-cli/pkg/utils" - "github.com/urfave/cli" - "helm.sh/helm/v3/pkg/action" -) - -func init() { - cli.BashCompletionFlag = cli.BoolFlag{Name: "compgen", Hidden: true} -} - -const ApplicationName = "plural" - -type Plural struct { - api.Client - ConsoleClient console.ConsoleClient - kubernetes.Kube - HelmConfiguration *action.Configuration -} - -func (p *Plural) InitKube() error { - if p.Kube == nil { - kube, err := kubernetes.Kubernetes() - if err != nil { - return err - } - p.Kube = kube - } - return nil -} - -func (p *Plural) InitConsoleClient(token, url string) error { - if p.ConsoleClient == nil { - if token == "" { - conf := console.ReadConfig() - if conf.Token == "" { - return fmt.Errorf("you have not set up a console login, you can run `plural cd login` to save your credentials") - } - - token = conf.Token - url = conf.Url - } - consoleClient, err := console.NewConsoleClient(token, url) - if err != nil { - return err - } - p.ConsoleClient = consoleClient - } - return nil -} - -func (p *Plural) assumeServiceAccount(conf config.Config, man *manifest.ProjectManifest) error { - owner := man.Owner - jwt, email, err := api.FromConfig(&conf).ImpersonateServiceAccount(owner.Email) - if err != nil { - utils.Error("You (%s) are not the owner of this repo %s, %v \n", conf.Email, owner.Email, api.GetErrorResponse(err, "ImpersonateServiceAccount")) - return err - } - conf.Email = email - conf.Token = jwt - p.Client = api.FromConfig(&conf) - accessToken, err := p.GrabAccessToken() - if err != nil { - utils.Error("failed to create access token, bailing") - return api.GetErrorResponse(err, "GrabAccessToken") - } - conf.Token = accessToken - config.SetConfig(&conf) - return nil -} - -func (p *Plural) InitPluralClient() { - if p.Client == nil { - if project, err := manifest.FetchProject(); err == nil && config.Exists() { - conf := config.Read() - if owner := project.Owner; owner != nil && conf.Email != owner.Email { - utils.LogInfo().Printf("Trying to impersonate service account: %s \n", owner.Email) - if err := p.assumeServiceAccount(conf, project); err != nil { - os.Exit(1) - } - return - } - } - - p.Client = api.NewClient() - } -} - -func (p *Plural) getCommands() []cli.Command { - return []cli.Command{ - { - Name: "version", - Aliases: []string{"v", "vsn"}, - Usage: "Gets cli version info", - Action: versionInfo, - }, - { - Name: "up", - Usage: "sets up your repository and an initial management cluster", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "endpoint", - Usage: "the endpoint for the plural installation you're working with", - }, - cli.StringFlag{ - Name: "service-account", - Usage: "email for the service account you'd like to use for this workspace", - }, - cli.BoolFlag{ - Name: "ignore-preflights", - Usage: "whether to ignore preflight check failures prior to init", - }, - cli.StringFlag{ - Name: "commit", - Usage: "commits your changes with this message", - }, - }, - Action: latestVersion(p.handleUp), - }, - { - Name: "down", - Usage: "destroys your management cluster and any apps installed on it", - Action: latestVersion(p.handleDown), - }, - { - Name: "init", - Usage: "initializes plural within a git repo", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "endpoint", - Usage: "the endpoint for the plural installation you're working with", - }, - cli.StringFlag{ - Name: "service-account", - Usage: "email for the service account you'd like to use for this workspace", - }, - cli.BoolFlag{ - Name: "ignore-preflights", - Usage: "whether to ignore preflight check failures prior to init", - }, - }, - Action: tracked(latestVersion(p.handleInit), "cli.init"), - }, - { - Name: "build", - Aliases: []string{"bld"}, - Usage: "builds your workspace", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "only", - Usage: "repository to (re)build", - }, - cli.BoolFlag{ - Name: "force", - Usage: "force workspace to build even if remote is out of sync", - }, - }, - Action: tracked(rooted(latestVersion(owned(upstreamSynced(p.build)))), "cli.build"), - }, - { - Name: "info", - Usage: "Get information for your installation of APP", - ArgsUsage: "APP", - Action: latestVersion(owned(rooted(p.info))), - }, - { - Name: "deploy", - Aliases: []string{"d"}, - Usage: "Deploys the current workspace. This command will first sniff out git diffs in workspaces, topsort them, then apply all changes.", - ArgsUsage: "Workspace", - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "silence", - Usage: "don't display notes for deployed apps", - }, - cli.BoolFlag{ - Name: "verbose", - Usage: "show all command output during execution", - }, - cli.BoolFlag{ - Name: "ignore-console", - Usage: "don't deploy the plural console", - }, - cli.BoolFlag{ - Name: "all", - Usage: "deploy all repos irregardless of changes", - }, - cli.StringFlag{ - Name: "commit", - Usage: "commits your changes with this message", - }, - cli.StringSliceFlag{ - Name: "from", - Usage: "deploys only this application and its dependencies", - }, - cli.BoolFlag{ - Name: "force", - Usage: "use force push when pushing to git", - }, - }, - Action: tracked(latestVersion(owned(rooted(p.deploy))), "cli.deploy"), - }, - { - Name: "diff", - Aliases: []string{"df"}, - Usage: "diffs the state of the current workspace with the deployed version and dumps results to diffs/", - ArgsUsage: "APP", - Action: latestVersion(handleDiff), - }, - { - Name: "clone", - Usage: "clones and decrypts a plural repo", - ArgsUsage: "URL", - Action: handleClone, - }, - { - Name: "create", - Usage: "scaffolds the resources needed to create a new plural repository", - Action: latestVersion(handleScaffold), - Category: "Workspace", - }, - { - Name: "watch", - Usage: "watches applications until they become ready", - ArgsUsage: "REPO", - Action: latestVersion(initKubeconfig(requireArgs(handleWatch, []string{"REPO"}))), - Category: "Debugging", - }, - { - Name: "wait", - Usage: "waits on applications until they become ready", - ArgsUsage: "REPO", - Action: latestVersion(requireArgs(handleWait, []string{"REPO"})), - Category: "Debugging", - }, - { - Name: "info", - Usage: "generates a console dashboard for the namespace of this repo", - ArgsUsage: "REPO", - Action: latestVersion(requireArgs(handleInfo, []string{"REPO"})), - Category: "Debugging", - }, - { - Name: "apply", - Usage: "applys the current pluralfile", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "file, f", - Usage: "pluralfile to use", - }, - }, - Action: latestVersion(apply), - Category: "Publishing", - }, - { - Name: "bounce", - Aliases: []string{"b"}, - Usage: "redeploys the charts in a workspace", - ArgsUsage: "APP", - Action: latestVersion(initKubeconfig(owned(p.bounce))), - }, - { - Name: "readme", - Aliases: []string{"b"}, - Usage: "generates the readme for your installation repo", - Category: "Workspace", - Action: latestVersion(downloadReadme), - }, - { - Name: "destroy", - Aliases: []string{"d"}, - Usage: "iterates through all installations in reverse topological order, deleting helm installations and terraform", - ArgsUsage: "APP", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "from", - Usage: "where to start your deploy command (useful when restarting interrupted destroys)", - }, - cli.StringFlag{ - Name: "commit", - Usage: "commits your changes with this message", - }, - cli.BoolFlag{ - Name: "force", - Usage: "use force push when pushing to git", - }, - cli.BoolFlag{ - Name: "all", - Usage: "tear down the entire cluster gracefully in one go", - }, - }, - Action: tracked(latestVersion(owned(upstreamSynced(p.destroy))), "cli.destroy"), - }, - { - Name: "upgrade", - Usage: "creates an upgrade in the upgrade queue QUEUE for application REPO", - ArgsUsage: "QUEUE REPO", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "f", - Usage: "file containing upgrade contents, use - for stdin", - }, - }, - Action: latestVersion(requireArgs(p.handleUpgrade, []string{"QUEUE", "REPO"})), - }, - { - Name: "auth", - Usage: "Handles authentication to the plural api", - Subcommands: p.authCommands(), - }, - { - Name: "preflights", - Usage: "runs provider preflight checks", - Category: "Workspace", - Action: latestVersion(preflights), - }, - { - Name: "login", - Usage: "logs into plural and saves credentials to the current config profile", - Action: latestVersion(handleLogin), - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "endpoint", - Usage: "the endpoint for the plural installation you're working with", - }, - cli.StringFlag{ - Name: "service-account", - Usage: "email for the service account you'd like to use for this workspace", - }, - }, - Category: "User Profile", - }, - { - Name: "import", - Usage: "imports plural config from another file", - Action: latestVersion(handleImport), - Category: "User Profile", - }, - { - Name: "repair", - Usage: "commits any new encrypted changes in your local workspace automatically", - Action: latestVersion(handleRepair), - Category: "Workspace", - }, - { - Name: "serve", - Usage: "launch the server", - Action: latestVersion(handleServe), - Category: "Workspace", - }, - { - Name: "shell", - Usage: "manages your cloud shell", - Subcommands: shellCommands(), - Category: "Workspace", - }, - { - Name: "clusters", - Usage: "commands related to managing plural clusters", - Subcommands: p.clusterCommands(), - }, - { - Name: "repos", - Usage: "view and manage plural repositories", - Subcommands: p.reposCommands(), - Category: "API", - }, - { - Name: "apps", - Usage: "view and manage plural repositories", - Subcommands: p.reposCommands(), - Category: "API", - }, - { - Name: "test", - Usage: "validate a values templace", - Action: latestVersion(testTemplate), - Category: "Publishing", - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "templateType", - Usage: "Determines the template type. Go template by default", - }, - }, - }, - { - Name: "proxy", - Usage: "proxies into running processes in your cluster", - Subcommands: p.proxyCommands(), - Category: "Debugging", - }, - { - Name: "crypto", - Usage: "plural encryption utilities", - Subcommands: p.cryptoCommands(), - Category: "User Profile", - }, - { - Name: "push", - Usage: "utilities for pushing tf or helm packages", - Subcommands: p.pushCommands(), - Category: "Publishing", - }, - { - Name: "api", - Usage: "inspect the plural api", - Subcommands: p.apiCommands(), - Category: "API", - }, - { - Name: "config", - Aliases: []string{"conf"}, - Usage: "reads/modifies cli configuration", - Subcommands: configCommands(), - Category: "User Profile", - }, - { - Name: "workspace", - Aliases: []string{"wkspace"}, - Usage: "Commands for managing installations in your workspace", - Subcommands: p.workspaceCommands(), - Category: "Workspace", - }, - { - Name: "profile", - Usage: "Commands for managing config profiles for plural", - Subcommands: profileCommands(), - Category: "User Profile", - }, - { - Name: "output", - Usage: "Commands for generating outputs from supported tools", - Subcommands: outputCommands(), - Category: "Workspace", - }, - { - Name: "logs", - Usage: "Commands for tailing logs for specific apps", - Subcommands: p.logsCommands(), - Category: "Debugging", - }, - { - Name: "bundle", - Usage: "Commands for installing and discovering installation bundles", - Subcommands: p.bundleCommands(), - }, - { - Name: "stack", - Usage: "Commands for installing and discovering plural stacks", - Subcommands: p.stackCommands(), - }, - { - Name: "packages", - Usage: "Commands for managing your installed packages", - Subcommands: p.packagesCommands(), - }, - { - Name: "ops", - Usage: "Commands for simplifying cluster operations", - Subcommands: p.opsCommands(), - Category: "Debugging", - }, - { - Name: "vpn", - Usage: "interacting with the plural vpn", - Subcommands: p.vpnCommands(), - Category: "Workspace", - }, - { - Name: "ai", - Usage: "utilize openai to get help with your setup", - Action: p.aiHelp, - Category: "Debugging", - }, - { - Name: "deployments", - Aliases: []string{"cd"}, - Usage: "view and manage plural deployments", - Subcommands: p.cdCommands(), - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "token", - Usage: "console token", - EnvVar: "PLURAL_CONSOLE_TOKEN", - Destination: &consoleToken, - }, - cli.StringFlag{ - Name: "url", - Usage: "console url address", - EnvVar: "PLURAL_CONSOLE_URL", - Destination: &consoleURL, - }, - }, - Category: "CD", - }, - { - Name: "pull-requests", - Aliases: []string{"pr"}, - Usage: "Generate and manage pull requests", - Subcommands: prCommands(), - Category: "CD", - }, - { - Name: "template", - Aliases: []string{"tpl"}, - Usage: "templates a helm chart to be uploaded to plural", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "values", - Usage: "the values file", - }, - }, - Action: latestVersion(handleHelmTemplate), - Category: "Publishing", - }, - { - Name: "changed", - Usage: "shows repos with pending changes", - Action: latestVersion(diffed), - Category: "Workspace", - }, - { - Name: "bootstrap", - Usage: "Commands for bootstrapping cluster", - Subcommands: p.bootstrapCommands(), - Category: "Bootstrap", - Hidden: !exp.IsFeatureEnabled(exp.EXP_PLURAL_CAPI), - }, - p.uiCommands(), - } -} - -func globalFlags() []cli.Flag { - return []cli.Flag{ - cli.StringFlag{ - Name: "profile-file", - Usage: "configure your config.yml profile `FILE`", - EnvVar: "PLURAL_PROFILE_FILE", - Destination: &config.ProfileFile, - }, - cli.StringFlag{ - Name: "encryption-key-file", - Usage: "configure your encryption key `FILE`", - EnvVar: "PLURAL_ENCRYPTION_KEY_FILE", - Destination: &crypto.EncryptionKeyFile, - }, - cli.BoolFlag{ - Name: "debug", - Usage: "enable debug mode", - EnvVar: "PLURAL_DEBUG_ENABLE", - Destination: &utils.EnableDebug, - }, - cli.BoolFlag{ - Name: "bootstrap", - Usage: "enable bootstrap mode", - Destination: &bootstrapMode, - Hidden: !exp.IsFeatureEnabled(exp.EXP_PLURAL_CAPI), - }, - } -} - -func CreateNewApp(plural *Plural) *cli.App { - app := cli.NewApp() - app.Name = ApplicationName - app.Usage = "Tooling to manage your installed plural applications" - app.EnableBashCompletion = true - app.Flags = globalFlags() - app.Commands = plural.getCommands() - links := linkCommands() - app.Commands = append(app.Commands, links...) - - return app -} - -func RunPlural(arguments []string) error { - return CreateNewApp(&Plural{}).Run(arguments) -} diff --git a/cmd/plural/ui.go b/cmd/plural/ui.go deleted file mode 100644 index 4a3d1652..00000000 --- a/cmd/plural/ui.go +++ /dev/null @@ -1,39 +0,0 @@ -//go:build ui || generate - -package plural - -import ( - "github.com/urfave/cli" - - "github.com/pluralsh/plural-cli/pkg/manifest" - "github.com/pluralsh/plural-cli/pkg/ui" - "github.com/pluralsh/plural-cli/pkg/wkspace" -) - -func (p *Plural) uiCommands() cli.Command { - return cli.Command{ - Name: "install", - Usage: "opens installer UI that simplifies application configuration", - Action: tracked(rooted(p.run), "cli.install"), - } -} - -func (p *Plural) run(c *cli.Context) error { - _, err := wkspace.Preflight() - if err != nil { - return err - } - - _, err = manifest.FetchProject() - if err != nil { - return err - } - - _, err = manifest.FetchContext() - if err != nil { - return err - } - - p.InitPluralClient() - return ui.Run(p.Client, c) -} diff --git a/cmd/plural/ui_stub.go b/cmd/plural/ui_stub.go deleted file mode 100644 index 220bbd42..00000000 --- a/cmd/plural/ui_stub.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !ui && !generate - -package plural - -import ( - "github.com/urfave/cli" -) - -func (p *Plural) uiCommands() cli.Command { - return cli.Command{} -} diff --git a/dockerfiles/Dockerfile.cloud b/dockerfiles/Dockerfile.cloud index f016e614..eac0d97b 100644 --- a/dockerfiles/Dockerfile.cloud +++ b/dockerfiles/Dockerfile.cloud @@ -10,7 +10,6 @@ COPY go.sum go.sum RUN go mod download # Copy the go source -COPY main.go main.go COPY cmd/ cmd/ COPY pkg/ pkg/ @@ -25,7 +24,7 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} \ -X "github.com/pluralsh/plural-cli/cmd/plural.Version=${APP_VSN}" \ -X "github.com/pluralsh/plural-cli/cmd/plural.Commit=${APP_COMMIT}" \ -X "github.com/pluralsh/plural-cli/cmd/plural.Date=${APP_DATE}"' \ - -o plural . + -o plural ./cmd/plural FROM alpine:3.17.2 as tools diff --git a/pkg/client/build.go b/pkg/client/build.go new file mode 100644 index 00000000..7f9b6692 --- /dev/null +++ b/pkg/client/build.go @@ -0,0 +1,185 @@ +package client + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/pluralsh/plural-cli/pkg/application" + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/pluralsh/plural-cli/pkg/executor" + "github.com/pluralsh/plural-cli/pkg/kubernetes" + "github.com/pluralsh/plural-cli/pkg/manifest" + "github.com/pluralsh/plural-cli/pkg/scaffold" + "github.com/pluralsh/plural-cli/pkg/utils" + "github.com/pluralsh/plural-cli/pkg/utils/git" + "github.com/pluralsh/plural-cli/pkg/utils/pathing" + "github.com/pluralsh/polly/algorithms" + "github.com/pluralsh/polly/containers" + "github.com/urfave/cli" + + "github.com/pluralsh/plural-cli/pkg/api" + "github.com/pluralsh/plural-cli/pkg/wkspace" +) + +const Bootstrap = "bootstrap" + +func GetSortedInstallations(client Plural, repo string) ([]*api.Installation, error) { + client.InitPluralClient() + installations, err := client.GetInstallations() + if err != nil { + return installations, api.GetErrorResponse(err, "GetInstallations") + } + + if len(installations) == 0 { + return installations, fmt.Errorf("no installations present, run `plural bundle install ` to install your first app") + } + + sorted, err := wkspace.UntilRepo(client.Client, repo, installations) + if err != nil { + sorted = installations // we don't know all the dependencies yet + } + + return sorted, nil +} + +func (p *Plural) Deploy(c *cli.Context) error { + p.InitPluralClient() + verbose := c.Bool("verbose") + repoRoot, err := git.Root() + if err != nil { + return err + } + + // project, err := manifest.FetchProject() + // if err != nil { + // return err + // } + + var sorted []string + switch { + case len(c.StringSlice("from")) > 0: + sorted, err = wkspace.AllDependencies(c.StringSlice("from")) + case c.Bool("all"): + sorted, err = p.allSortedRepos() + default: + sorted, err = getSortedNames(true) + } + if err != nil { + return err + } + + fmt.Printf("Deploying applications [%s] in topological order\n\n", strings.Join(sorted, ", ")) + + ignoreConsole := c.Bool("ignore-console") + for _, repo := range sorted { + if ignoreConsole && (repo == "console" || repo == Bootstrap) { + continue + } + + // if repo == Bootstrap && project.ClusterAPI { + // ready, err := bootstrap.CheckClusterReadiness(project.Cluster, Bootstrap) + // + // // Stop if cluster exists, but it is not ready yet. + // if err != nil && err.Error() == bootstrap.ClusterNotReadyError { + // return err + // } + // + // // If cluster does not exist bootstrap needs to be done first. + // if !ready { + // err := bootstrap.BootstrapCluster(plural.RunPlural) + // if err != nil { + // return err + // } + // } + //} + + execution, err := executor.GetExecution(pathing.SanitizeFilepath(filepath.Join(repoRoot, repo)), "deploy") + if err != nil { + return err + } + + if err := execution.Execute("deploying", verbose); err != nil { + utils.Note("It looks like your deployment failed. This may be a transient issue and rerunning the `plural deploy` command may resolve it. Or, feel free to reach out to us on discord (https://discord.gg/bEBAMXV64s) or Intercom and we should be able to help you out\n") + return err + } + + fmt.Printf("\n") + + installation, err := p.GetInstallation(repo) + if err != nil { + return api.GetErrorResponse(err, "GetInstallation") + } + if installation == nil { + return fmt.Errorf("The %s was unistalled, run `plural bundle install %s ` ", repo, repo) + } + + if err := p.Client.MarkSynced(repo); err != nil { + utils.Warn("failed to mark %s as synced, this is not a critical error but might drift state in our api, you can run `plural repos synced %s` to mark it manually", repo, repo) + } + + if c.Bool("silence") { + continue + } + + if man, err := fetchManifest(repo); err == nil && man.Wait { + if kubeConf, err := kubernetes.KubeConfig(); err == nil { + fmt.Printf("Waiting for %s to become ready...\n", repo) + if err := application.SilentWait(kubeConf, repo); err != nil { + return err + } + fmt.Println("") + } + } + + if err := scaffold.Notes(installation); err != nil { + return err + } + } + + utils.Highlight("\n==> Commit and push your changes to record your deployment\n\n") + + if commit := common.CommitMsg(c); commit != "" { + utils.Highlight("Pushing upstream...\n") + return git.Sync(repoRoot, commit, c.Bool("force")) + } + + return nil +} + +func (p *Plural) allSortedRepos() ([]string, error) { + p.InitPluralClient() + insts, err := p.GetInstallations() + if err != nil { + return nil, api.GetErrorResponse(err, "GetInstallations") + } + + return wkspace.SortAndFilter(insts) +} +func getSortedNames(filter bool) ([]string, error) { + diffed, err := wkspace.DiffedRepos() + if err != nil { + return nil, err + } + + sorted, err := wkspace.TopSortNames(diffed) + if err != nil { + return nil, err + } + + if filter { + repos := containers.ToSet(diffed) + return algorithms.Filter(sorted, repos.Has), nil + } + + return sorted, nil +} + +func fetchManifest(repo string) (*manifest.Manifest, error) { + p, err := manifest.ManifestPath(repo) + if err != nil { + return nil, err + } + + return manifest.Read(p) +} diff --git a/pkg/client/plural.go b/pkg/client/plural.go new file mode 100644 index 00000000..58de041c --- /dev/null +++ b/pkg/client/plural.go @@ -0,0 +1,169 @@ +package client + +import ( + "fmt" + "os" + + "github.com/pluralsh/plural-cli/pkg/common" + "github.com/pluralsh/plural-cli/pkg/crypto" + "github.com/pluralsh/plural-cli/pkg/scm" + "github.com/pluralsh/plural-cli/pkg/wkspace" + "github.com/urfave/cli" + + "github.com/pluralsh/plural-cli/pkg/api" + "github.com/pluralsh/plural-cli/pkg/config" + "github.com/pluralsh/plural-cli/pkg/console" + "github.com/pluralsh/plural-cli/pkg/kubernetes" + "github.com/pluralsh/plural-cli/pkg/manifest" + "github.com/pluralsh/plural-cli/pkg/utils" +) + +const DemoingErrorMsg = "You're currently running a gcp demo cluster. Spin that down by deleting you shell at https://app.plural.sh/shell before beginning a local installation" + +type Plural struct { + api.Client + ConsoleClient console.ConsoleClient + kubernetes.Kube +} + +func (p *Plural) InitKube() error { + if p.Kube == nil { + kube, err := kubernetes.Kubernetes() + if err != nil { + return err + } + p.Kube = kube + } + return nil +} + +func (p *Plural) InitConsoleClient(token, url string) error { + if p.ConsoleClient == nil { + if token == "" { + conf := console.ReadConfig() + if conf.Token == "" { + return fmt.Errorf("you have not set up a console login, you can run `plural cd login` to save your credentials") + } + + token = conf.Token + url = conf.Url + } + consoleClient, err := console.NewConsoleClient(token, url) + if err != nil { + return err + } + p.ConsoleClient = consoleClient + } + return nil +} + +func (p *Plural) InitPluralClient() { + if p.Client == nil { + if project, err := manifest.FetchProject(); err == nil && config.Exists() { + conf := config.Read() + if owner := project.Owner; owner != nil && conf.Email != owner.Email { + utils.LogInfo().Printf("Trying to impersonate service account: %s \n", owner.Email) + if err := p.AssumeServiceAccount(conf, project); err != nil { + os.Exit(1) + } + return + } + } + + p.Client = api.NewClient() + } +} + +func (p *Plural) AssumeServiceAccount(conf config.Config, man *manifest.ProjectManifest) error { + owner := man.Owner + jwt, email, err := api.FromConfig(&conf).ImpersonateServiceAccount(owner.Email) + if err != nil { + utils.Error("You (%s) are not the owner of this repo %s, %v \n", conf.Email, owner.Email, api.GetErrorResponse(err, "ImpersonateServiceAccount")) + return err + } + conf.Email = email + conf.Token = jwt + p.Client = api.FromConfig(&conf) + accessToken, err := p.GrabAccessToken() + if err != nil { + utils.Error("failed to create access token, bailing") + return api.GetErrorResponse(err, "GrabAccessToken") + } + conf.Token = accessToken + config.SetConfig(&conf) + return nil +} + +func (p *Plural) HandleInit(c *cli.Context) error { + gitCreated := false + repo := "" + + if utils.Exists("./workspace.yaml") { + utils.Highlight("Found workspace.yaml, skipping init as this repo has already been initialized...\n") + return nil + } + + git, err := wkspace.Preflight() + if err != nil && git { + return err + } + + if err := common.HandleLogin(c); err != nil { + return err + } + p.InitPluralClient() + + me, err := p.Me() + if err != nil { + return api.GetErrorResponse(err, "Me") + } + if me.Demoing { + return fmt.Errorf(DemoingErrorMsg) + } + + if _, err := os.Stat(manifest.ProjectManifestPath()); err == nil && git && !common.Affirm("This repository's workspace.yaml already exists. Would you like to use it?", "PLURAL_INIT_AFFIRM_CURRENT_REPO") { + fmt.Println("Run `plural init` from empty repository or outside any in order to start from scratch.") + return nil + } + + prov, err := common.RunPreflights() + if err != nil && !c.Bool("ignore-preflights") { + return err + } + + if !git && common.Affirm("you're attempting to setup plural outside a git repository. would you like us to set one up for you here?", "PLURAL_INIT_AFFIRM_SETUP_REPO") { + repo, err = scm.Setup() + if err != nil { + return err + } + gitCreated = true + } + if !git && !gitCreated { + return fmt.Errorf("you're not in a git repository, either clone one directly or let us set it up for you by rerunning `plural init`") + } + + // create workspace.yaml when git repository is ready + if err := prov.Flush(); err != nil { + return err + } + if err := common.CryptoInit(c); err != nil { + return err + } + _ = wkspace.DownloadReadme() + + if common.Affirm(common.BackupMsg, "PLURAL_INIT_AFFIRM_BACKUP_KEY") { + if err := crypto.BackupKey(p.Client); err != nil { + return api.GetErrorResponse(err, "BackupKey") + } + } + + if err := crypto.CreateKeyFingerprintFile(); err != nil { + return err + } + + utils.Success("Workspace is properly configured!\n") + if gitCreated { + utils.Highlight("Be sure to `cd %s` to use your configured git repo\n", repo) + } + return nil +} diff --git a/cmd/plural/app.go b/pkg/common/app.go similarity index 76% rename from cmd/plural/app.go rename to pkg/common/app.go index 9159963e..3c2bd076 100644 --- a/cmd/plural/app.go +++ b/pkg/common/app.go @@ -1,10 +1,12 @@ -package plural +package common import ( "fmt" "os/exec" "strings" + "github.com/pluralsh/plural-cli/pkg/up" + tm "github.com/buger/goterm" "github.com/urfave/cli" @@ -16,7 +18,7 @@ import ( "sigs.k8s.io/application/api/v1beta1" ) -func handleWatch(c *cli.Context) error { +func HandleWatch(c *cli.Context) error { repo := c.Args().Get(0) kubeConf, err := kubernetes.KubeConfig() if err != nil { @@ -36,7 +38,7 @@ func handleWatch(c *cli.Context) error { }, timeout) } -func handleWait(c *cli.Context) error { +func HandleWait(c *cli.Context) error { repo := c.Args().Get(0) kubeConf, err := kubernetes.KubeConfig() if err != nil { @@ -46,7 +48,7 @@ func handleWait(c *cli.Context) error { return application.Wait(kubeConf, repo) } -func handleInfo(c *cli.Context) error { +func HandleInfo(c *cli.Context) error { repo := c.Args().Get(0) conf := config.Read() @@ -63,3 +65,16 @@ func handleInfo(c *cli.Context) error { cmd := exec.Command("k9s", "-n", conf.Namespace(repo)) return cmd.Run() } + +func HandleDown(_ *cli.Context) error { + if !Affirm(AffirmDown, "PLURAL_DOWN_AFFIRM_DESTROY") { + return fmt.Errorf("cancelled destroy") + } + + ctx, err := up.Build() + if err != nil { + return err + } + + return ctx.Destroy() +} diff --git a/pkg/common/apply.go b/pkg/common/apply.go new file mode 100644 index 00000000..82362e5a --- /dev/null +++ b/pkg/common/apply.go @@ -0,0 +1,33 @@ +package common + +import ( + "os" + "path/filepath" + + "github.com/pluralsh/plural-cli/pkg/pluralfile" + "github.com/pluralsh/plural-cli/pkg/utils/pathing" + "github.com/urfave/cli" +) + +func Apply(c *cli.Context) error { + path, _ := os.Getwd() + var file = pathing.SanitizeFilepath(filepath.Join(path, "Pluralfile")) + if c.IsSet("file") { + file, _ = filepath.Abs(c.String("file")) + } + + if err := os.Chdir(filepath.Dir(file)); err != nil { + return err + } + + plrl, err := pluralfile.Parse(file) + if err != nil { + return err + } + + lock, err := plrl.Lock(file) + if err != nil { + return err + } + return plrl.Execute(file, lock) +} diff --git a/cmd/plural/init.go b/pkg/common/common.go similarity index 57% rename from cmd/plural/init.go rename to pkg/common/common.go index 47ad4f31..bc5f8ad1 100644 --- a/cmd/plural/init.go +++ b/pkg/common/common.go @@ -1,4 +1,4 @@ -package plural +package common import ( "fmt" @@ -8,118 +8,71 @@ import ( "time" "github.com/pkg/browser" - "github.com/urfave/cli" - "github.com/pluralsh/plural-cli/pkg/api" "github.com/pluralsh/plural-cli/pkg/config" "github.com/pluralsh/plural-cli/pkg/crypto" - "github.com/pluralsh/plural-cli/pkg/manifest" "github.com/pluralsh/plural-cli/pkg/provider" - "github.com/pluralsh/plural-cli/pkg/scm" "github.com/pluralsh/plural-cli/pkg/server" "github.com/pluralsh/plural-cli/pkg/utils" - "github.com/pluralsh/plural-cli/pkg/utils/git" "github.com/pluralsh/plural-cli/pkg/utils/pathing" "github.com/pluralsh/plural-cli/pkg/wkspace" -) - -const DemoingErrorMsg = "You're currently running a gcp demo cluster. Spin that down by deleting you shell at https://app.plural.sh/shell before beginning a local installation" - -func (p *Plural) handleInit(c *cli.Context) error { - gitCreated := false - repo := "" + "github.com/urfave/cli" - if utils.Exists("./workspace.yaml") { - utils.Highlight("Found workspace.yaml, skipping init as this repo has already been initialized...\n") - return nil - } + "github.com/pluralsh/plural-cli/pkg/scaffold" + "github.com/pluralsh/plural-cli/pkg/utils/git" +) - git, err := wkspace.Preflight() - if err != nil && git { +func AppReadme(name string, dryRun bool) error { + repoRoot, err := git.Root() + if err != nil { return err } - if err := handleLogin(c); err != nil { - return err - } - p.InitPluralClient() + dir := filepath.Join(repoRoot, name, "helm", name) + return scaffold.Readme(dir, dryRun) +} - me, err := p.Me() - if err != nil { - return api.GetErrorResponse(err, "Me") - } - if me.Demoing { - return fmt.Errorf(DemoingErrorMsg) - } +func DoBuild(client api.Client, installation *api.Installation, force bool) error { + repoName := installation.Repository.Name + fmt.Printf("Building workspace for %s\n", repoName) - if _, err := os.Stat(manifest.ProjectManifestPath()); err == nil && git && !affirm("This repository's workspace.yaml already exists. Would you like to use it?", "PLURAL_INIT_AFFIRM_CURRENT_REPO") { - fmt.Println("Run `plural init` from empty repository or outside any in order to start from scratch.") + if !wkspace.Configured(repoName) { + fmt.Printf("You have not locally configured %s but have it registered as an installation in our api, ", repoName) + fmt.Printf("either delete it with `plural apps uninstall %s` or install it locally via a bundle in `plural bundle list %s`\n", repoName, repoName) return nil } - prov, err := runPreflights() - if err != nil && !c.Bool("ignore-preflights") { + workspace, err := wkspace.New(client, installation) + if err != nil { return err } - if !git && affirm("you're attempting to setup plural outside a git repository. would you like us to set one up for you here?", "PLURAL_INIT_AFFIRM_SETUP_REPO") { - repo, err = scm.Setup() - if err != nil { - return err - } - gitCreated = true - } - if !git && !gitCreated { - return fmt.Errorf("you're not in a git repository, either clone one directly or let us set it up for you by rerunning `plural init`") + vsn, ok := workspace.RequiredCliVsn() + if ok && !VersionValid(vsn) { + return fmt.Errorf("Your cli version is not sufficient to complete this build, please update to at least %s", vsn) } - // create workspace.yaml when git repository is ready - if err := prov.Flush(); err != nil { - return err - } - if err := cryptoInit(c); err != nil { + if err := workspace.Prepare(); err != nil { return err } - _ = wkspace.DownloadReadme() - - if affirm(backupMsg, "PLURAL_INIT_AFFIRM_BACKUP_KEY") { - if err := crypto.BackupKey(p.Client); err != nil { - return api.GetErrorResponse(err, "BackupKey") - } - } - if err := crypto.CreateKeyFingerprintFile(); err != nil { + build, err := scaffold.Scaffolds(workspace) + if err != nil { return err } - utils.Success("Workspace is properly configured!\n") - if gitCreated { - utils.Highlight("Be sure to `cd %s` to use your configured git repo\n", repo) + err = build.Execute(workspace, force) + if err == nil { + utils.Success("Finished building %s\n\n", repoName) } - return nil -} - -func preflights(c *cli.Context) error { - _, err := runPreflights() - return err -} -func runPreflights() (provider.Provider, error) { - prov, err := provider.GetProvider() - if err != nil { - return prov, err - } + workspace.PrintLinks() - for _, pre := range prov.Preflights() { - if err := pre.Validate(); err != nil { - return prov, err - } - } - - return prov, nil + AppReadme(repoName, false) // nolint:errcheck + return err } -func handleLogin(c *cli.Context) error { +func HandleLogin(c *cli.Context) error { conf := &config.Config{} conf.Token = "" conf.Endpoint = c.String("endpoint") @@ -128,7 +81,7 @@ func handleLogin(c *cli.Context) error { if config.Exists() { conf := config.Read() - if affirm(fmt.Sprintf("It looks like your current Plural user is %s, use this profile?", conf.Email), "PLURAL_LOGIN_AFFIRM_CURRENT_USER") { + if Affirm(fmt.Sprintf("It looks like your current Plural user is %s, use this profile?", conf.Email), "PLURAL_LOGIN_AFFIRM_CURRENT_USER") { client = api.FromConfig(&conf) return postLogin(&conf, client, c, persist) } @@ -156,38 +109,11 @@ func handleLogin(c *cli.Context) error { } conf.Token = jwt - conf.ReportErrors = affirm("Would you be willing to report any errors to Plural to help with debugging?", "PLURAL_LOGIN_AFFIRM_REPORT_ERRORS") + conf.ReportErrors = Affirm("Would you be willing to report any errors to Plural to help with debugging?", "PLURAL_LOGIN_AFFIRM_REPORT_ERRORS") client = api.FromConfig(conf) return postLogin(conf, client, c, persist) } -func handleClone(c *cli.Context) error { - url := c.Args().Get(0) - cmd := exec.Command("git", "clone", url) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return err - } - - repo := git.RepoName(url) - _ = os.Chdir(repo) - if err := cryptoInit(c); err != nil { - return err - } - - if err := handleUnlock(c); err != nil { - return err - } - - utils.Success("Your repo has been cloned and decrypted, cd %s to start working\n", repo) - return nil -} - -func downloadReadme(c *cli.Context) error { - return wkspace.DownloadReadme() -} - func postLogin(conf *config.Config, client api.Client, c *cli.Context, persist bool) error { me, err := client.Me() if err != nil { @@ -222,8 +148,54 @@ func postLogin(conf *config.Config, client api.Client, c *cli.Context, persist b conf.Token = accessToken return conf.Flush() } +func Preflights(c *cli.Context) error { + _, err := RunPreflights() + return err +} + +func RunPreflights() (provider.Provider, error) { + prov, err := provider.GetProvider() + if err != nil { + return prov, err + } + + for _, pre := range prov.Preflights() { + if err := pre.Validate(); err != nil { + return prov, err + } + } + + return prov, nil +} + +func HandleClone(c *cli.Context) error { + url := c.Args().Get(0) + cmd := exec.Command("git", "clone", url) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return err + } + + repo := git.RepoName(url) + _ = os.Chdir(repo) + if err := CryptoInit(c); err != nil { + return err + } + + if err := HandleUnlock(c); err != nil { + return err + } + + utils.Success("Your repo has been cloned and decrypted, cd %s to start working\n", repo) + return nil +} + +func DownloadReadme(c *cli.Context) error { + return wkspace.DownloadReadme() +} -func handleImport(c *cli.Context) error { +func HandleImport(c *cli.Context) error { dir, err := filepath.Abs(c.Args().Get(0)) if err != nil { return err @@ -234,7 +206,7 @@ func handleImport(c *cli.Context) error { return err } - if err := cryptoInit(c); err != nil { + if err := CryptoInit(c); err != nil { return err } @@ -255,6 +227,6 @@ func handleImport(c *cli.Context) error { return nil } -func handleServe(c *cli.Context) error { +func HandleServe(c *cli.Context) error { return server.Run() } diff --git a/pkg/common/constants.go b/pkg/common/constants.go new file mode 100644 index 00000000..072cce16 --- /dev/null +++ b/pkg/common/constants.go @@ -0,0 +1,16 @@ +package common + +import "fmt" + +const BackupMsg = "Would you like to back up your encryption key to plural? If you chose to manage it yourself, you can find it at ~/.plural/key" + +const ( + AffirmUp = "Are you ready to set up your initial management cluster? You can check the generated terraform/helm to confirm everything looks good first" + AffirmDown = "Are you ready to destroy your plural infrastructure? This will destroy all k8s clusters and any data stored within" +) + +var ( + ErrNoGit = fmt.Errorf("Could not compare current workspace to origin. Do you have an `origin` remote configured, or does your repo not have an initial commit?") + ErrRemoteDiff = fmt.Errorf("Your local workspace is not in sync with remote. Either `git pull` recent changes or `git push` any missed changes. Also confirm you can authenticate to the origin remote, which you can see with `git remote -v`") + ErrUnlock = fmt.Errorf("could not decrypt your repo, this is likely due to using the wrong key at ~/.plural/key. The original key might be in a backup or on your previous machine.") +) diff --git a/pkg/common/crypto.go b/pkg/common/crypto.go new file mode 100644 index 00000000..61275394 --- /dev/null +++ b/pkg/common/crypto.go @@ -0,0 +1,107 @@ +package common + +import ( + "os" + "path/filepath" + + "github.com/pluralsh/plural-cli/pkg/crypto" + "github.com/pluralsh/plural-cli/pkg/utils" + "github.com/pluralsh/plural-cli/pkg/utils/git" + "github.com/urfave/cli" +) + +const ( + GitAttributesFile = ".gitattributes" + GitIgnoreFile = ".gitignore" +) + +const Gitattributes = `/**/helm/**/values.yaml filter=plural-crypt diff=plural-crypt +/**/helm/**/values.yaml* filter=plural-crypt diff=plural-crypt +/**/helm/**/README.md* filter=plural-crypt diff=plural-crypt +/**/helm/**/default-values.yaml* filter=plural-crypt diff=plural-crypt +/**/terraform/**/main.tf filter=plural-crypt diff=plural-crypt +/**/terraform/**/main.tf* filter=plural-crypt diff=plural-crypt +/**/manifest.yaml filter=plural-crypt diff=plural-crypt +/**/output.yaml filter=plural-crypt diff=plural-crypt +/diffs/**/* filter=plural-crypt diff=plural-crypt +context.yaml filter=plural-crypt diff=plural-crypt +workspace.yaml filter=plural-crypt diff=plural-crypt +context.yaml* filter=plural-crypt diff=plural-crypt +workspace.yaml* filter=plural-crypt diff=plural-crypt +helm-values/*.yaml filter=plural-crypt diff=plural-crypt +.env filter=plural-crypt diff=plural-crypt +.gitattributes !filter !diff +` + +const Gitignore = `/**/.terraform +/**/.terraform* +/**/terraform.tfstate* +/bin +*~ +.idea +*.swp +*.swo +.DS_STORE +.vscode +` + +func CryptoInit(c *cli.Context) error { + encryptConfig := [][]string{ + {"filter.plural-crypt.smudge", "plural crypto decrypt"}, + {"filter.plural-crypt.clean", "plural crypto encrypt"}, + {"filter.plural-crypt.required", "true"}, + {"diff.plural-crypt.textconv", "plural crypto decrypt"}, + } + + utils.Highlight("Creating git encryption filters\n") + for _, conf := range encryptConfig { + if err := GitConfig(conf[0], conf[1]); err != nil { + return err + } + } + + if err := utils.WriteFile(GitAttributesFile, []byte(Gitattributes)); err != nil { + return err + } + + if err := utils.WriteFile(GitIgnoreFile, []byte(Gitignore)); err != nil { + return err + } + + _, err := crypto.Build() + return err +} + +func HandleUnlock(_ *cli.Context) error { + _, err := crypto.Build() + if err != nil { + return err + } + + repoRoot, err := git.Root() + if err != nil { + return err + } + + // fixes Invalid cross-device link when using os.Rename + gitIndexDir, err := filepath.Abs(filepath.Join(repoRoot, ".git")) + if err != nil { + return err + } + gitIndex := filepath.Join(gitIndexDir, "index") + dump, err := os.CreateTemp(gitIndexDir, "index.bak") + if err != nil { + return err + } + if err := os.Rename(gitIndex, dump.Name()); err != nil { + return err + } + + if err := GitCommand("checkout", "HEAD", "--", repoRoot).Run(); err != nil { + _ = os.Rename(dump.Name(), gitIndex) + return ErrUnlock + } + + os.Remove(dump.Name()) + return nil +} diff --git a/pkg/common/diff.go b/pkg/common/diff.go new file mode 100644 index 00000000..e611c870 --- /dev/null +++ b/pkg/common/diff.go @@ -0,0 +1,75 @@ +package common + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/pluralsh/plural-cli/pkg/diff" + "github.com/pluralsh/plural-cli/pkg/utils/git" + "github.com/pluralsh/plural-cli/pkg/utils/pathing" + "github.com/pluralsh/plural-cli/pkg/wkspace" + "github.com/pluralsh/polly/algorithms" + "github.com/pluralsh/polly/containers" + "github.com/urfave/cli" +) + +func Diffed(_ *cli.Context) error { + diffed, err := wkspace.DiffedRepos() + if err != nil { + return err + } + + for _, d := range diffed { + fmt.Println(d) + } + + return nil +} + +func getSortedNames(filter bool) ([]string, error) { + diffed, err := wkspace.DiffedRepos() + if err != nil { + return nil, err + } + + sorted, err := wkspace.TopSortNames(diffed) + if err != nil { + return nil, err + } + + if filter { + repos := containers.ToSet(diffed) + return algorithms.Filter(sorted, repos.Has), nil + } + + return sorted, nil +} + +func HandleDiff(_ *cli.Context) error { + repoRoot, err := git.Root() + if err != nil { + return err + } + + sorted, err := getSortedNames(true) + if err != nil { + return err + } + + fmt.Printf("Diffing applications [%s] in topological order\n\n", strings.Join(sorted, ", ")) + + for _, repo := range sorted { + d, err := diff.GetDiff(pathing.SanitizeFilepath(filepath.Join(repoRoot, repo)), "diff") + if err != nil { + return err + } + + if err := d.Execute(); err != nil { + return err + } + + fmt.Printf("\n") + } + return nil +} diff --git a/cmd/plural/git.go b/pkg/common/git.go similarity index 67% rename from cmd/plural/git.go rename to pkg/common/git.go index 224a5332..8321612b 100644 --- a/cmd/plural/git.go +++ b/pkg/common/git.go @@ -1,4 +1,4 @@ -package plural +package common import ( "fmt" @@ -9,7 +9,7 @@ import ( "github.com/urfave/cli" ) -func handleRepair(c *cli.Context) error { +func HandleRepair(c *cli.Context) error { repoRoot, err := git.Root() if err != nil { return err @@ -22,12 +22,12 @@ func handleRepair(c *cli.Context) error { return nil } -func gitConfig(name, val string) error { - cmd := gitCommand("config", name, val) +func GitConfig(name, val string) error { + cmd := GitCommand("config", name, val) return cmd.Run() } -func gitCommand(args ...string) *exec.Cmd { +func GitCommand(args ...string) *exec.Cmd { cmd := exec.Command("git", args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/pkg/common/helm.go b/pkg/common/helm.go new file mode 100644 index 00000000..459fd8d7 --- /dev/null +++ b/pkg/common/helm.go @@ -0,0 +1,52 @@ +package common + +import ( + "fmt" + "os" + + "github.com/pluralsh/plural-cli/pkg/helm" + scftmpl "github.com/pluralsh/plural-cli/pkg/scaffold/template" + "github.com/urfave/cli" + "sigs.k8s.io/yaml" +) + +func HandleHelmTemplate(c *cli.Context) error { + path := c.String("values") + f, err := scftmpl.TmpValuesFile(path) + if err != nil { + return err + } + + defer func(name string) { + _ = os.Remove(name) + }(f.Name()) + + name := "default" + namespace := "default" + actionConfig, err := helm.GetActionConfig(namespace) + if err != nil { + return err + } + values, err := getValues(f.Name()) + if err != nil { + return err + } + res, err := helm.Template(actionConfig, name, namespace, c.Args().Get(0), false, false, values) + if err != nil { + return err + } + fmt.Println(string(res)) + return nil +} + +func getValues(path string) (map[string]interface{}, error) { + values := make(map[string]interface{}) + valsContent, err := os.ReadFile(path) + if err != nil { + return nil, err + } + if err := yaml.Unmarshal(valsContent, &values); err != nil { + return nil, err + } + return values, nil +} diff --git a/cmd/plural/scaffold.go b/pkg/common/scaffold.go similarity index 77% rename from cmd/plural/scaffold.go rename to pkg/common/scaffold.go index 7025ad6c..1d09c4de 100644 --- a/cmd/plural/scaffold.go +++ b/pkg/common/scaffold.go @@ -1,4 +1,4 @@ -package plural +package common import ( "github.com/pluralsh/plural-cli/pkg/api" @@ -6,7 +6,7 @@ import ( "github.com/urfave/cli" ) -func handleScaffold(c *cli.Context) error { +func HandleScaffold(c *cli.Context) error { client := api.NewClient() return scaffold.ApplicationScaffold(client) } diff --git a/cmd/plural/template.go b/pkg/common/template.go similarity index 97% rename from cmd/plural/template.go rename to pkg/common/template.go index c9910729..7952e374 100644 --- a/cmd/plural/template.go +++ b/pkg/common/template.go @@ -1,4 +1,4 @@ -package plural +package common import ( "bytes" @@ -15,7 +15,7 @@ import ( "gopkg.in/yaml.v2" ) -func testTemplate(c *cli.Context) error { +func TestTemplate(c *cli.Context) error { conf := config.Read() client := api.NewClient() installations, _ := client.GetInstallations() diff --git a/pkg/common/tests.go b/pkg/common/tests.go new file mode 100644 index 00000000..8e16ca7e --- /dev/null +++ b/pkg/common/tests.go @@ -0,0 +1,29 @@ +package common + +import ( + "bytes" + "io" + "os" + + "github.com/urfave/cli" +) + +func CaptureStdout(app *cli.App, arg []string) (string, error) { + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := app.Run(arg) + if err != nil { + return "", err + } + + w.Close() + os.Stdout = old + + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err != nil { + return "", err + } + return buf.String(), nil +} diff --git a/cmd/plural/validation.go b/pkg/common/validation.go similarity index 74% rename from cmd/plural/validation.go rename to pkg/common/validation.go index badbeb61..a72cb88e 100644 --- a/cmd/plural/validation.go +++ b/pkg/common/validation.go @@ -1,30 +1,31 @@ -package plural +package common import ( "fmt" "os" + "github.com/pluralsh/plural-cli/pkg/provider" + "github.com/pluralsh/plural-cli/pkg/utils/errors" + "github.com/pluralsh/polly/algorithms" + "github.com/AlecAivazis/survey/v2" "github.com/pluralsh/plural-cli/pkg/api" "github.com/pluralsh/plural-cli/pkg/config" "github.com/pluralsh/plural-cli/pkg/executor" "github.com/pluralsh/plural-cli/pkg/manifest" - "github.com/pluralsh/plural-cli/pkg/provider" "github.com/pluralsh/plural-cli/pkg/utils" - "github.com/pluralsh/plural-cli/pkg/utils/errors" "github.com/pluralsh/plural-cli/pkg/utils/git" "github.com/pluralsh/plural-cli/pkg/utils/pathing" - "github.com/pluralsh/polly/algorithms" "github.com/urfave/cli" ) func init() { - bootstrapMode = false + BootstrapMode = false } -var bootstrapMode bool +var BootstrapMode bool -func requireArgs(fn func(*cli.Context) error, args []string) func(*cli.Context) error { +func RequireArgs(fn func(*cli.Context) error, args []string) func(*cli.Context) error { return func(c *cli.Context) error { nargs := c.NArg() if nargs > len(args) { @@ -39,9 +40,9 @@ func requireArgs(fn func(*cli.Context) error, args []string) func(*cli.Context) } } -func rooted(fn func(*cli.Context) error) func(*cli.Context) error { +func Rooted(fn func(*cli.Context) error) func(*cli.Context) error { return func(c *cli.Context) error { - if err := repoRoot(); err != nil { + if err := RepoRoot(); err != nil { return err } @@ -49,9 +50,9 @@ func rooted(fn func(*cli.Context) error) func(*cli.Context) error { } } -func owned(fn func(*cli.Context) error) func(*cli.Context) error { +func Owned(fn func(*cli.Context) error) func(*cli.Context) error { return func(c *cli.Context) error { - if err := validateOwner(); err != nil { + if err := ValidateOwner(); err != nil { return err } @@ -59,9 +60,9 @@ func owned(fn func(*cli.Context) error) func(*cli.Context) error { } } -func affirmed(fn func(*cli.Context) error, msg string, envKey string) func(*cli.Context) error { +func Affirmed(fn func(*cli.Context) error, msg string, envKey string) func(*cli.Context) error { return func(c *cli.Context) error { - if !affirm(msg, envKey) { + if !Affirm(msg, envKey) { return nil } @@ -69,13 +70,13 @@ func affirmed(fn func(*cli.Context) error, msg string, envKey string) func(*cli. } } -func highlighted(fn func(*cli.Context) error) func(*cli.Context) error { +func Highlighted(fn func(*cli.Context) error) func(*cli.Context) error { return func(c *cli.Context) error { return utils.HighlightError(fn(c)) } } -func tracked(fn func(*cli.Context) error, event string) func(*cli.Context) error { +func Tracked(fn func(*cli.Context) error, event string) func(*cli.Context) error { return func(c *cli.Context) error { event := api.UserEventAttributes{Data: "", Event: event, Status: "OK"} err := fn(c) @@ -99,30 +100,7 @@ func tracked(fn func(*cli.Context) error, event string) func(*cli.Context) error } } -func initKubeconfig(fn func(*cli.Context) error) func(*cli.Context) error { - return func(c *cli.Context) error { - _, found := utils.ProjectRoot() - if found { - prov, err := provider.GetProvider() - if err != nil { - return err - } - if bootstrapMode { - prov = &provider.KINDProvider{Clust: "bootstrap"} - } - if err := prov.KubeConfig(); err != nil { - return err - } - utils.LogInfo().Println("init", prov.Name(), "provider") - } else { - utils.LogInfo().Println("not found provider") - } - - return fn(c) - } -} - -func validateOwner() error { +func ValidateOwner() error { path := manifest.ProjectManifestPath() project, err := manifest.ReadProject(path) if err != nil { @@ -143,7 +121,7 @@ func validateOwner() error { return nil } -func confirm(msg string, envKey string) bool { +func Confirm(msg string, envKey string) bool { res := true conf, ok := utils.GetEnvBoolValue(envKey) if ok { @@ -156,7 +134,7 @@ func confirm(msg string, envKey string) bool { return res } -func affirm(msg string, envKey string) bool { +func Affirm(msg string, envKey string) bool { res := true conf, ok := utils.GetEnvBoolValue(envKey) if ok { @@ -169,7 +147,7 @@ func affirm(msg string, envKey string) bool { return res } -func repoRoot() error { +func RepoRoot() error { dir, err := os.Getwd() if err != nil { return err @@ -189,7 +167,7 @@ func repoRoot() error { return nil } -func latestVersion(fn func(*cli.Context) error) func(*cli.Context) error { +func LatestVersion(fn func(*cli.Context) error) func(*cli.Context) error { return func(c *cli.Context) error { if os.Getenv("PLURAL_CONSOLE") != "1" && os.Getenv("CLOUD_SHELL") != "1" && algorithms.Coinflip(1, 5) { utils.CheckLatestVersion(Version) @@ -199,24 +177,30 @@ func latestVersion(fn func(*cli.Context) error) func(*cli.Context) error { } } -func upstreamSynced(fn func(*cli.Context) error) func(*cli.Context) error { +func InitKubeconfig(fn func(*cli.Context) error) func(*cli.Context) error { return func(c *cli.Context) error { - changed, sha, err := git.HasUpstreamChanges() - if err != nil { - utils.LogError().Println(err) - return errors.ErrorWrap(errNoGit, "Failed to get git information") - } - - force := c.Bool("force") - if !changed && !force { - return errors.ErrorWrap(errRemoteDiff, fmt.Sprintf("Expecting HEAD at commit=%s", sha)) + _, found := utils.ProjectRoot() + if found { + prov, err := provider.GetProvider() + if err != nil { + return err + } + if BootstrapMode { + prov = &provider.KINDProvider{Clust: "bootstrap"} + } + if err := prov.KubeConfig(); err != nil { + return err + } + utils.LogInfo().Println("init", prov.Name(), "provider") + } else { + utils.LogInfo().Println("not found provider") } return fn(c) } } -func requireKind(fn func(*cli.Context) error) func(*cli.Context) error { +func RequireKind(fn func(*cli.Context) error) func(*cli.Context) error { return func(c *cli.Context) error { exists, _ := utils.Which("kind") if !exists { @@ -226,3 +210,36 @@ func requireKind(fn func(*cli.Context) error) func(*cli.Context) error { return fn(c) } } + +func CommitMsg(c *cli.Context) string { + if commit := c.String("commit"); commit != "" { + return commit + } + + if !c.Bool("silence") { + var commit string + if err := survey.AskOne(&survey.Input{Message: "Enter a commit message (empty to not commit right now)"}, &commit); err != nil { + return "" + } + return commit + } + + return "" +} + +func UpstreamSynced(fn func(*cli.Context) error) func(*cli.Context) error { + return func(c *cli.Context) error { + changed, sha, err := git.HasUpstreamChanges() + if err != nil { + utils.LogError().Println(err) + return errors.ErrorWrap(ErrNoGit, "Failed to get git information") + } + + force := c.Bool("force") + if !changed && !force { + return errors.ErrorWrap(ErrRemoteDiff, fmt.Sprintf("Expecting HEAD at commit=%s", sha)) + } + + return fn(c) + } +} diff --git a/cmd/plural/version.go b/pkg/common/version.go similarity index 92% rename from cmd/plural/version.go rename to pkg/common/version.go index 71a886fa..100e2396 100644 --- a/cmd/plural/version.go +++ b/pkg/common/version.go @@ -1,4 +1,4 @@ -package plural +package common import ( "fmt" @@ -22,7 +22,7 @@ var ( Date = "" ) -func versionValid(vsn string) bool { +func VersionValid(vsn string) bool { current := Version if !strings.HasPrefix(current, "v") { current = fmt.Sprintf("v%s", current) @@ -46,7 +46,7 @@ func checkRecency() error { return nil } -func versionInfo(c *cli.Context) error { +func VersionInfo(_ *cli.Context) error { fmt.Println("PLURAL CLI:") fmt.Printf(" version\t%s\n", Version) fmt.Printf(" git commit\t%s\n", Commit) diff --git a/pkg/up/ping.go b/pkg/up/ping.go index 8ce85c4a..7bc8a783 100644 --- a/pkg/up/ping.go +++ b/pkg/up/ping.go @@ -79,7 +79,7 @@ func retrier(retryMsg, successMsg string, f func() error) error { go func() { for { - fmt.Printf(retryMsg) + fmt.Print(retryMsg) err := f() if err == nil { utils.Success(successMsg)