diff --git a/e2e/cnab_test.go b/e2e/cnab_test.go index 2ee333237..c9e941ebb 100644 --- a/e2e/cnab_test.go +++ b/e2e/cnab_test.go @@ -20,15 +20,23 @@ func TestCallCustomStatusAction(t *testing.T) { cnab string }{ { - name: "validCustomStatusAction", + name: "validCustomDockerStatusAction", exitCode: 0, - expectedOutput: "Status action", - cnab: "cnab-with-status", + expectedOutput: "com.docker.app.status", + cnab: "cnab-with-docker-status", }, + { + name: "validCustomStandardStatusAction", + exitCode: 0, + expectedOutput: "io.cnab.status", + cnab: "cnab-with-standard-status", + }, + // A CNAB bundle without standard or docker status action still can output + // some informations about the installation. { name: "missingCustomStatusAction", - exitCode: 1, - expectedOutput: "status failed: action not defined for bundle", + exitCode: 0, + expectedOutput: "Name: missingCustomStatusAction", cnab: "cnab-without-status", }, } diff --git a/e2e/commands_test.go b/e2e/commands_test.go index 9ee2fba55..77667a0cf 100644 --- a/e2e/commands_test.go +++ b/e2e/commands_test.go @@ -352,6 +352,30 @@ func testDockerAppLifecycle(t *testing.T, useBindMount bool) { cmd.Command = dockerCli.Command("app", "status", appName) checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), []string{ + `INSTALLATION +------------ +Name: TestDockerAppLifecycle_.* +Created: .* +Modified: .* +Revision: .* +Last Action: install +Result: SUCCESS +Orchestrator: swarm + +APPLICATION +----------- +Name: simple +Version: 1.1.0-beta1 +Reference:.* + +PARAMETERS +---------- +api_host: example.com +static_subdir: data/static +web_port: 8082 + +STATUS +------`, fmt.Sprintf("[[:alnum:]]+ %s_db replicated [0-1]/1 postgres:9.3", appName), fmt.Sprintf(`[[:alnum:]]+ %s_web replicated [0-1]/1 nginx:latest \*:8082->80/tcp`, appName), fmt.Sprintf("[[:alnum:]]+ %s_api replicated [0-1]/1 python:3.6", appName), diff --git a/e2e/testdata/cnab-with-status/bundle.json b/e2e/testdata/cnab-with-docker-status/bundle.json similarity index 66% rename from e2e/testdata/cnab-with-status/bundle.json rename to e2e/testdata/cnab-with-docker-status/bundle.json index 8da8f23f9..9cac002d8 100644 --- a/e2e/testdata/cnab-with-status/bundle.json +++ b/e2e/testdata/cnab-with-docker-status/bundle.json @@ -1,10 +1,10 @@ { - "name": "cnab-with-status", + "name": "cnab-with-docker-status", "version": "0.1.0", "invocationImages": [ { "imageType": "docker", - "image": "e2e/cnab-with-status:v0.1.0" + "image": "e2e/cnab-with-docker-status:v0.1.0" } ], "actions": { diff --git a/e2e/testdata/cnab-with-status/cnab/app/run b/e2e/testdata/cnab-with-docker-status/cnab/app/run similarity index 100% rename from e2e/testdata/cnab-with-status/cnab/app/run rename to e2e/testdata/cnab-with-docker-status/cnab/app/run diff --git a/e2e/testdata/cnab-with-status/cnab/build/Dockerfile b/e2e/testdata/cnab-with-docker-status/cnab/build/Dockerfile similarity index 100% rename from e2e/testdata/cnab-with-status/cnab/build/Dockerfile rename to e2e/testdata/cnab-with-docker-status/cnab/build/Dockerfile diff --git a/e2e/testdata/cnab-with-standard-status/bundle.json b/e2e/testdata/cnab-with-standard-status/bundle.json new file mode 100644 index 000000000..65ec46709 --- /dev/null +++ b/e2e/testdata/cnab-with-standard-status/bundle.json @@ -0,0 +1,15 @@ +{ + "name": "cnab-with-standard-status", + "version": "0.1.0", + "invocationImages": [ + { + "imageType": "docker", + "image": "e2e/cnab-with-standard-status:v0.1.0" + } + ], + "actions": { + "io.cnab.status": { + "modifies": false + } + } +} diff --git a/e2e/testdata/cnab-with-standard-status/cnab/app/run b/e2e/testdata/cnab-with-standard-status/cnab/app/run new file mode 100755 index 000000000..e063557b6 --- /dev/null +++ b/e2e/testdata/cnab-with-standard-status/cnab/app/run @@ -0,0 +1,23 @@ +#!/bin/sh + +action=$CNAB_ACTION +name=$CNAB_INSTALLATION_NAME + +case $action in + install) + echo "Install action" + ;; + uninstall) + echo "uninstall action" + ;; + upgrade) + echo "Upgrade action" + ;; + io.cnab.status) + echo "Status action" + ;; + *) + echo "No action for $action" + ;; +esac +echo "Action $action complete for $name" diff --git a/e2e/testdata/cnab-with-standard-status/cnab/build/Dockerfile b/e2e/testdata/cnab-with-standard-status/cnab/build/Dockerfile new file mode 100644 index 000000000..3059afdca --- /dev/null +++ b/e2e/testdata/cnab-with-standard-status/cnab/build/Dockerfile @@ -0,0 +1,7 @@ +ARG ALPINE_VERSION=3.9.2 + +FROM alpine:${ALPINE_VERSION} + +COPY cnab/app/run /cnab/app/run + +CMD /cnab/app/run diff --git a/internal/commands/install.go b/internal/commands/install.go index e8ec47312..65a3cae6b 100644 --- a/internal/commands/install.go +++ b/internal/commands/install.go @@ -136,7 +136,7 @@ func runInstall(dockerCli command.Cli, appname string, opts installOptions) erro // so any installation needs a clean uninstallation. err2 := installationStore.Store(installation) if err != nil { - return fmt.Errorf("Installation failed: %s", errBuf) + return fmt.Errorf("Installation failed: %s\n%s", errBuf, err) } if err2 != nil { return err2 diff --git a/internal/commands/status.go b/internal/commands/status.go index ed9164d98..18774ceba 100644 --- a/internal/commands/status.go +++ b/internal/commands/status.go @@ -2,15 +2,31 @@ package commands import ( "fmt" + "io" + "os" + "sort" + "strings" + "text/tabwriter" + "time" "github.com/deislabs/duffle/pkg/action" "github.com/deislabs/duffle/pkg/credentials" "github.com/docker/app/internal" + "github.com/docker/app/internal/store" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + units "github.com/docker/go-units" "github.com/spf13/cobra" ) +var ( + knownStatusActions = []string{ + internal.ActionStatusName, + // TODO: Extract this constant to the cnab-go library + "io.cnab.status", + } +) + func statusCmd(dockerCli command.Cli) *cobra.Command { var opts credentialOptions @@ -42,6 +58,14 @@ func runStatus(dockerCli command.Cli, installationName string, opts credentialOp if err != nil { return err } + displayInstallationStatus(os.Stdout, installation) + + // Check if the bundle knows the docker app status action, if not just exit without error. + statusAction := resolveStatusAction(installation) + if statusAction == "" { + return nil + } + bind, err := requiredClaimBindMount(installation.Claim, opts.targetContext, dockerCli) if err != nil { return err @@ -62,8 +86,9 @@ func runStatus(dockerCli command.Cli, installationName string, opts credentialOp if err := credentials.Validate(creds, installation.Bundle.Credentials); err != nil { return err } + printHeader(os.Stdout, "STATUS") status := &action.RunCustom{ - Action: internal.ActionStatusName, + Action: statusAction, Driver: driverImpl, } if err := status.Run(&installation.Claim, creds, dockerCli.Out()); err != nil { @@ -71,3 +96,76 @@ func runStatus(dockerCli command.Cli, installationName string, opts credentialOp } return nil } + +func displayInstallationStatus(w io.Writer, installation *store.Installation) { + printHeader(w, "INSTALLATION") + tab := tabwriter.NewWriter(w, 0, 0, 1, ' ', 0) + printValue(tab, "Name", installation.Name) + printValue(tab, "Created", units.HumanDuration(time.Since(installation.Created))) + printValue(tab, "Modified", units.HumanDuration(time.Since(installation.Modified))) + printValue(tab, "Revision", installation.Revision) + printValue(tab, "Last Action", installation.Result.Action) + printValue(tab, "Result", strings.ToUpper(installation.Result.Status)) + if o, ok := installation.Parameters[internal.ParameterOrchestratorName]; ok { + orchestrator := fmt.Sprintf("%v", o) + if orchestrator == "" { + orchestrator = string(command.OrchestratorSwarm) + } + printValue(tab, "Orchestrator", orchestrator) + if kubeNamespace, ok := installation.Parameters[internal.ParameterKubernetesNamespaceName]; ok && orchestrator == string(command.OrchestratorKubernetes) { + printValue(tab, "Kubernetes namespace", fmt.Sprintf("%v", kubeNamespace)) + } + } + + tab.Flush() + fmt.Fprintln(w) + + printHeader(w, "APPLICATION") + tab = tabwriter.NewWriter(w, 0, 0, 1, ' ', 0) + printValue(tab, "Name", installation.Bundle.Name) + printValue(tab, "Version", installation.Bundle.Version) + printValue(tab, "Reference", installation.Reference) + tab.Flush() + fmt.Fprintln(w) + + if len(installation.Parameters) > 0 { + printHeader(w, "PARAMETERS") + tab = tabwriter.NewWriter(w, 0, 0, 1, ' ', 0) + params := sortParameters(installation) + for _, param := range params { + if !strings.HasPrefix(param, internal.Namespace) { + // TODO: Trim long []byte parameters, maybe add type too (string, int...) + printValue(tab, param, fmt.Sprintf("%v", installation.Parameters[param])) + } + } + tab.Flush() + fmt.Fprintln(w) + } +} + +func sortParameters(installation *store.Installation) []string { + var params []string + for name := range installation.Parameters { + params = append(params, name) + } + sort.Strings(params) + return params +} + +func printHeader(w io.Writer, header string) { + fmt.Fprintln(w, header) + fmt.Fprintln(w, strings.Repeat("-", len(header))) +} + +func printValue(w io.Writer, key, value string) { + fmt.Fprintf(w, "%s:\t%s\n", key, value) +} + +func resolveStatusAction(installation *store.Installation) string { + for _, name := range knownStatusActions { + if _, ok := installation.Bundle.Actions[name]; ok { + return name + } + } + return "" +}