Skip to content

Commit

Permalink
feat(*): add installation delete options/flags to relevant commands (#…
Browse files Browse the repository at this point in the history
…1189)

* feat(*): add installation delete options/flags to relevant commands

Signed-off-by: Vaughn Dice <vadice@microsoft.com>

* Apply suggestions from code review

Co-authored-by: Carolyn Van Slyck <me@carolynvanslyck.com>

* incorporate add'l review feedback

Signed-off-by: Vaughn Dice <vadice@microsoft.com>

* add'l review feedback

Signed-off-by: Vaughn Dice <vadice@microsoft.com>

* refactor the parent/dep uninstall scenarios

Signed-off-by: Vaughn Dice <vadice@microsoft.com>

* update dependencies_test.go

Signed-off-by: Vaughn Dice <vadice@microsoft.com>

* use BundleAction interface to enable action-specific options when executing dependencies

Signed-off-by: Vaughn Dice <vadice@microsoft.com>

* ref(tests/dependencies_test.go): set mysql dep namespace param as well

Signed-off-by: Vaughn Dice <vadice@microsoft.com>

* fix cmd example spacing

Signed-off-by: Vaughn Dice <vadice@microsoft.com>

* add comments around uninstallOpts when empty

Signed-off-by: Vaughn Dice <vadice@microsoft.com>

* remove ToActionArgs from BundleAction interface

Signed-off-by: Vaughn Dice <vadice@microsoft.com>

Co-authored-by: Carolyn Van Slyck <me@carolynvanslyck.com>
  • Loading branch information
vdice and carolynvs committed Aug 19, 2020
1 parent 7ae632d commit c525954
Show file tree
Hide file tree
Showing 22 changed files with 633 additions and 56 deletions.
6 changes: 6 additions & 0 deletions cmd/porter/bundle.go
Expand Up @@ -249,6 +249,8 @@ For example, the 'debug' driver may be specified, which simply logs the info giv
porter bundle uninstall --parameter-set azure --param test-mode=true --param header-color=blue
porter bundle uninstall --cred azure --cred kubernetes
porter bundle uninstall --driver debug
porter bundle uninstall --delete
porter bundle uninstall --force-delete
`,
PreRunE: func(cmd *cobra.Command, args []string) error {
return opts.Validate(args, p)
Expand All @@ -273,6 +275,10 @@ For example, the 'debug' driver may be specified, which simply logs the info giv
"Credential to use when uninstalling the bundle. May be either a named set of credentials or a filepath, and specified multiple times.")
f.StringVarP(&opts.Driver, "driver", "d", porter.DefaultDriver,
"Specify a driver to use. Allowed values: docker, debug")
f.BoolVar(&opts.Delete, "delete", false,
"Delete all records associated with the installation, assuming the uninstall action succeeds")
f.BoolVar(&opts.ForceDelete, "force-delete", false,
"UNSAFE. Delete all records associated with the installation, even if uninstall fails. This is intended for cleaning up test data and is not recommended for production environments.")
addBundlePullFlags(f, &opts.BundlePullOptions)

return cmd
Expand Down
29 changes: 28 additions & 1 deletion cmd/porter/installations.go
Expand Up @@ -19,6 +19,7 @@ func buildInstallationCommands(p *porter.Porter) *cobra.Command {
cmd.AddCommand(buildInstallationsListCommand(p))
cmd.AddCommand(buildInstallationShowCommand(p))
cmd.AddCommand(buildInstallationOutputsCommands(p))
cmd.AddCommand(buildInstallationDeleteCommand(p))

return cmd
}
Expand Down Expand Up @@ -59,7 +60,7 @@ func buildInstallationShowCommand(p *porter.Porter) *cobra.Command {
Short: "Show an installation of a bundle",
Long: "Displays info relating to an installation of a bundle, including status and a listing of outputs.",
Example: ` porter installation show
porter installation show another-bundle
porter installation show another-bundle
Optional output formats include json and yaml.
`,
Expand All @@ -77,3 +78,29 @@ Optional output formats include json and yaml.

return &cmd
}

func buildInstallationDeleteCommand(p *porter.Porter) *cobra.Command {
opts := porter.DeleteOptions{}

cmd := cobra.Command{
Use: "delete [INSTALLATION]",
Short: "Delete an installation",
Long: "Deletes all records and outputs associated with an installation",
Example: ` porter installation delete
porter installation delete wordpress
porter installation delete --force
`,
PreRunE: func(cmd *cobra.Command, args []string) error {
return opts.Validate(args, p.Context)
},
RunE: func(cmd *cobra.Command, args []string) error {
return p.DeleteInstallation(opts)
},
}

f := cmd.Flags()
f.BoolVar(&opts.Force, "force", false,
"Force a delete the installation, regardless of last completed action")

return &cmd
}
4 changes: 4 additions & 0 deletions docs/content/cli/bundles_uninstall.md
Expand Up @@ -30,6 +30,8 @@ porter bundles uninstall [INSTALLATION] [flags]
porter bundle uninstall --parameter-set azure --param test-mode=true --param header-color=blue
porter bundle uninstall --cred azure --cred kubernetes
porter bundle uninstall --driver debug
porter bundle uninstall --delete
porter bundle uninstall --force-delete
```

Expand All @@ -39,9 +41,11 @@ porter bundles uninstall [INSTALLATION] [flags]
--allow-docker-host-access Controls if the bundle should have access to the host's Docker daemon with elevated privileges. See https://porter.sh/configuration/#allow-docker-host-access for the full implications of this flag.
--cnab-file string Path to the CNAB bundle.json file.
-c, --cred strings Credential to use when uninstalling the bundle. May be either a named set of credentials or a filepath, and specified multiple times.
--delete Delete all records associated with the installation, assuming the uninstall action succeeds
-d, --driver string Specify a driver to use. Allowed values: docker, debug (default "docker")
-f, --file string Path to the porter manifest file. Defaults to the bundle in the current directory. Optional unless a newer version of the bundle should be used to uninstall the bundle.
--force Force a fresh pull of the bundle
--force-delete UNSAFE. Delete all records associated with the installation, even if uninstall fails. This is intended for cleaning up test data and is not recommended for production environments.
-h, --help help for uninstall
--insecure-registry Don't require TLS for the registry
--param strings Define an individual parameter in the form NAME=VALUE. Overrides parameters otherwise set via --parameter-set. May be specified multiple times.
Expand Down
1 change: 1 addition & 0 deletions docs/content/cli/installations.md
Expand Up @@ -27,6 +27,7 @@ Commands for working with installations of a bundle
### SEE ALSO

* [porter](/cli/porter/) - I am porter 👩🏽‍✈️, the friendly neighborhood CNAB authoring tool
* [porter installations delete](/cli/porter_installations_delete/) - Delete an installation
* [porter installations list](/cli/porter_installations_list/) - List installed bundles
* [porter installations output](/cli/porter_installations_output/) - Output commands
* [porter installations show](/cli/porter_installations_show/) - Show an installation of a bundle
Expand Down
44 changes: 44 additions & 0 deletions docs/content/cli/installations_delete.md
@@ -0,0 +1,44 @@
---
title: "porter installations delete"
slug: porter_installations_delete
url: /cli/porter_installations_delete/
---
## porter installations delete

Delete an installation

### Synopsis

Deletes all records and outputs associated with an installation

```
porter installations delete [INSTALLATION] [flags]
```

### Examples

```
porter installation delete
porter installation delete wordpress
porter installation delete --force
```

### Options

```
--force Force a delete the installation, regardless of last completed action
-h, --help help for delete
```

### Options inherited from parent commands

```
--debug Enable debug logging
--debug-plugins Enable plugin debug logging
```

### SEE ALSO

* [porter installations](/cli/porter_installations/) - Installation commands

4 changes: 4 additions & 0 deletions docs/content/cli/uninstall.md
Expand Up @@ -30,6 +30,8 @@ porter uninstall [INSTALLATION] [flags]
porter uninstall --parameter-set azure --param test-mode=true --param header-color=blue
porter uninstall --cred azure --cred kubernetes
porter uninstall --driver debug
porter uninstall --delete
porter uninstall --force-delete
```

Expand All @@ -39,9 +41,11 @@ porter uninstall [INSTALLATION] [flags]
--allow-docker-host-access Controls if the bundle should have access to the host's Docker daemon with elevated privileges. See https://porter.sh/configuration/#allow-docker-host-access for the full implications of this flag.
--cnab-file string Path to the CNAB bundle.json file.
-c, --cred strings Credential to use when uninstalling the bundle. May be either a named set of credentials or a filepath, and specified multiple times.
--delete Delete all records associated with the installation, assuming the uninstall action succeeds
-d, --driver string Specify a driver to use. Allowed values: docker, debug (default "docker")
-f, --file string Path to the porter manifest file. Defaults to the bundle in the current directory. Optional unless a newer version of the bundle should be used to uninstall the bundle.
--force Force a fresh pull of the bundle
--force-delete UNSAFE. Delete all records associated with the installation, even if uninstall fails. This is intended for cleaning up test data and is not recommended for production environments.
-h, --help help for uninstall
--insecure-registry Don't require TLS for the registry
--param strings Define an individual parameter in the form NAME=VALUE. Overrides parameters otherwise set via --parameter-set. May be specified multiple times.
Expand Down
4 changes: 3 additions & 1 deletion pkg/cnab/provider/helpers.go
Expand Up @@ -45,6 +45,8 @@ func (t *TestRuntime) LoadBundle(bundleFile string) (bundle.Bundle, error) {
}

func (t *TestRuntime) Execute(args ActionArguments) error {
args.Driver = debugDriver
if args.Driver == "" {
args.Driver = debugDriver
}
return t.Runtime.Execute(args)
}
45 changes: 45 additions & 0 deletions pkg/context/helpers.go
Expand Up @@ -135,6 +135,51 @@ func (c *TestContext) AddTestDirectory(srcDir, destDir string) {
}
}

func (c *TestContext) AddTestDriver(src, name string) string {
c.T.Helper()

data, err := ioutil.ReadFile(src)
if err != nil {
c.T.Fatal(err)
}

dirname, err := c.FileSystem.TempDir("", "porter")
if err != nil {
c.T.Fatal(err)
}

// filename in accordance with cnab-go's command driver expectations
filename := fmt.Sprintf("%s/cnab-%s", dirname, name)

newfile, err := c.FileSystem.Create(filename)
if err != nil {
c.T.Fatal(err)
}

if len(data) > 0 {
_, err := newfile.Write(data)
if err != nil {
c.T.Fatal(err)
}
}

err = c.FileSystem.Chmod(newfile.Name(), os.ModePerm)
if err != nil {
c.T.Fatal(err)
}
err = newfile.Close()
if err != nil {
c.T.Fatal(err)
}

path := os.Getenv("PATH")
pathlist := []string{dirname, path}
newpath := strings.Join(pathlist, string(os.PathListSeparator))
os.Setenv("PATH", newpath)

return dirname
}

// GetOutput returns all text printed to stdout.
func (c *TestContext) GetOutput() string {
return string(c.capturedOut.Bytes())
Expand Down
61 changes: 61 additions & 0 deletions pkg/porter/delete.go
@@ -0,0 +1,61 @@
package porter

import (
"fmt"

"get.porter.sh/porter/pkg/context"
claims "github.com/cnabio/cnab-go/claim"
"github.com/pkg/errors"
)

const installationDeleteTmpl = "deleting installation records for %s...\n"

var (
// ErrUnsafeInstallationDelete warns the user that deletion of an unsuccessfully uninstalled installation is unsafe
ErrUnsafeInstallationDelete = errors.New("it is unsafe to delete an installation when the last action wasn't a successful uninstall")

// ErrUnsafeInstallationDeleteRetryForce presents the ErrUnsafeInstallationDelete error and provides a retry option of --force
ErrUnsafeInstallationDeleteRetryForce = fmt.Errorf("%s; if you are sure it should be deleted, retry the last command with the --force flag", ErrUnsafeInstallationDelete)
)

// DeleteOptions represent options for Porter's installation delete command
type DeleteOptions struct {
sharedOptions
Force bool
}

// Validate prepares for an installation delete action and validates the args/options.
func (o *DeleteOptions) Validate(args []string, cxt *context.Context) error {
// Ensure only one argument exists (installation name) if args length non-zero
err := o.sharedOptions.validateInstallationName(args)
if err != nil {
return err
}

return o.sharedOptions.defaultBundleFiles(cxt)
}

// DeleteInstallation handles deletion of an installation
func (p *Porter) DeleteInstallation(opts DeleteOptions) error {
err := p.applyDefaultOptions(&opts.sharedOptions)
if err != nil {
return err
}

installation, err := p.Claims.ReadInstallationStatus(opts.Name)
if err != nil {
return errors.Wrapf(err, "unable to read status for installation %s", opts.Name)
}

claim, err := installation.GetLastClaim()
if err != nil {
return errors.Wrapf(err, "unable to read most recent record for installation %s", opts.Name)
}

if (claim.Action != claims.ActionUninstall || installation.GetLastStatus() != claims.StatusSucceeded) && !opts.Force {
return ErrUnsafeInstallationDeleteRetryForce
}

fmt.Fprintf(p.Out, installationDeleteTmpl, opts.Name)
return p.Claims.DeleteInstallation(opts.Name)
}
80 changes: 80 additions & 0 deletions pkg/porter/delete_test.go
@@ -0,0 +1,80 @@
package porter

import (
"testing"

"github.com/cnabio/cnab-go/bundle"
"github.com/cnabio/cnab-go/claim"
"github.com/stretchr/testify/require"
)

func TestDeleteInstallation(t *testing.T) {
testcases := []struct {
name string
lastAction string
lastActionStatus string
force bool
installationRemains bool
wantError string
}{
{
name: "not yet installed",
wantError: "unable to read status for installation test: Installation does not exist",
}, {
name: "last action not uninstall; no --force",
lastAction: "install",
lastActionStatus: claim.StatusSucceeded,
installationRemains: true,
wantError: ErrUnsafeInstallationDeleteRetryForce.Error(),
}, {
name: "last action failed uninstall; no --force",
lastAction: "uninstall",
lastActionStatus: claim.StatusFailed,
installationRemains: true,
wantError: ErrUnsafeInstallationDeleteRetryForce.Error(),
}, {
name: "last action not uninstall; --force",
lastAction: "install",
lastActionStatus: claim.StatusSucceeded,
force: true,
}, {
name: "last action failed uninstall; --force",
lastAction: "uninstall",
lastActionStatus: claim.StatusFailed,
force: true,
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
p := NewTestPorter(t)
p.TestConfig.SetupPorterHome()

var err error

// Create test claim
if tc.lastAction != "" {
c := p.TestClaims.CreateClaim("test", tc.lastAction, bundle.Bundle{}, nil)
_ = p.TestClaims.CreateResult(c, tc.lastActionStatus)
}

opts := DeleteOptions{}
opts.Name = "test"
opts.Force = tc.force

err = p.DeleteInstallation(opts)
if tc.wantError != "" {
require.EqualError(t, err, tc.wantError)
} else {
require.NoError(t, err, "expected DeleteInstallation to succeed")
}

_, err = p.Claims.ReadInstallation("test")
if tc.installationRemains {
require.NoError(t, err, "expected installation to exist")
} else {
require.EqualError(t, err, "Installation does not exist")
}
})
}
}

0 comments on commit c525954

Please sign in to comment.