Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit 7d43c90

Browse files
Merge pull request #514 from silvin-lubecki/installation-error
Installation is no longer blocked in case of previous failure
2 parents 94a99e5 + 2c98fb3 commit 7d43c90

File tree

7 files changed

+143
-59
lines changed

7 files changed

+143
-59
lines changed

e2e/commands_test.go

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -305,53 +305,35 @@ func testDockerAppLifecycle(t *testing.T, useBindMount bool) {
305305
cmd, cleanup := dockerCli.createTestCmd()
306306
defer cleanup()
307307
appName := strings.Replace(t.Name(), "/", "_", 1)
308-
309308
tmpDir := fs.NewDir(t, appName)
310309
defer tmpDir.Remove()
311-
312-
cmd.Env = append(cmd.Env, "DOCKER_TARGET_CONTEXT=swarm-target-context")
313-
314310
// Running a swarm using docker in docker to install the application
315311
// and run the invocation image
316312
swarm := NewContainer("docker:18.09-dind", 2375)
317313
swarm.Start(t)
318314
defer swarm.Stop(t)
315+
initializeDockerAppEnvironment(t, &cmd, tmpDir, swarm, useBindMount)
319316

320-
// The dind doesn't have the cnab-app-base image so we save it in order to load it later
321-
icmd.RunCommand(dockerCli.path, "save", fmt.Sprintf("docker/cnab-app-base:%s", internal.Version), "--output", tmpDir.Join("cnab-app-base.tar.gz")).Assert(t, icmd.Success)
322-
323-
// We need two contexts:
324-
// - one for `docker` so that it connects to the dind swarm created before
325-
// - the target context for the invocation image to install within the swarm
326-
cmd.Command = dockerCli.Command("context", "create", "swarm-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, swarm.GetAddress(t)), "--default-stack-orchestrator", "swarm")
327-
icmd.RunCmd(cmd).Assert(t, icmd.Success)
328-
329-
// When creating a context on a Windows host we cannot use
330-
// the unix socket but it's needed inside the invocation image.
331-
// The workaround is to create a context with an empty host.
332-
// This host will default to the unix socket inside the
333-
// invocation image
334-
host := "host="
335-
if !useBindMount {
336-
host += fmt.Sprintf("tcp://%s", swarm.GetPrivateAddress(t))
337-
}
338-
339-
cmd.Command = dockerCli.Command("context", "create", "swarm-target-context", "--docker", host, "--default-stack-orchestrator", "swarm")
340-
icmd.RunCmd(cmd).Assert(t, icmd.Success)
341-
342-
// Initialize the swarm
343-
cmd.Env = append(cmd.Env, "DOCKER_CONTEXT=swarm-context")
344-
cmd.Command = dockerCli.Command("swarm", "init")
345-
icmd.RunCmd(cmd).Assert(t, icmd.Success)
317+
// Install an illformed Docker Application Package
318+
cmd.Command = dockerCli.Command("app", "install", "testdata/simple/simple.dockerapp", "--set", "web_port=-1", "--name", appName)
319+
icmd.RunCmd(cmd).Assert(t, icmd.Expected{
320+
ExitCode: 1,
321+
Err: "error decoding 'Ports': Invalid hostPort: -1",
322+
})
323+
// TODO: List the installation and check the failed status
346324

347-
// Load the needed base cnab image into the swarm docker engine
348-
cmd.Command = dockerCli.Command("load", "--input", tmpDir.Join("cnab-app-base.tar.gz"))
349-
icmd.RunCmd(cmd).Assert(t, icmd.Success)
325+
// Upgrading a failed installation is not allowed
326+
cmd.Command = dockerCli.Command("app", "upgrade", appName)
327+
icmd.RunCmd(cmd).Assert(t, icmd.Expected{
328+
ExitCode: 1,
329+
Err: fmt.Sprintf("Installation %q has failed and cannot be upgraded, reinstall it using 'docker app install'", appName),
330+
})
350331

351-
// Install a Docker Application Package
332+
// Install a Docker Application Package with an existing failed installation is fine
352333
cmd.Command = dockerCli.Command("app", "install", "testdata/simple/simple.dockerapp", "--name", appName)
353334
checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(),
354335
[]string{
336+
fmt.Sprintf("WARNING: installing over previously failed installation %q", appName),
355337
fmt.Sprintf("Creating network %s_back", appName),
356338
fmt.Sprintf("Creating network %s_front", appName),
357339
fmt.Sprintf("Creating service %s_db", appName),
@@ -368,6 +350,13 @@ func testDockerAppLifecycle(t *testing.T, useBindMount bool) {
368350
fmt.Sprintf("[[:alnum:]]+ %s_api replicated [0-1]/1 python:3.6", appName),
369351
})
370352

353+
// Installing again the same application is forbidden
354+
cmd.Command = dockerCli.Command("app", "install", "testdata/simple/simple.dockerapp", "--name", appName)
355+
icmd.RunCmd(cmd).Assert(t, icmd.Expected{
356+
ExitCode: 1,
357+
Err: fmt.Sprintf("Installation %q already exists, use 'docker app upgrade' instead", appName),
358+
})
359+
371360
// Upgrade the application, changing the port
372361
cmd.Command = dockerCli.Command("app", "upgrade", appName, "--set", "web_port=8081")
373362
checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(),
@@ -393,6 +382,41 @@ func testDockerAppLifecycle(t *testing.T, useBindMount bool) {
393382
})
394383
}
395384

385+
func initializeDockerAppEnvironment(t *testing.T, cmd *icmd.Cmd, tmpDir *fs.Dir, swarm *Container, useBindMount bool) {
386+
cmd.Env = append(cmd.Env, "DOCKER_TARGET_CONTEXT=swarm-target-context")
387+
388+
// The dind doesn't have the cnab-app-base image so we save it in order to load it later
389+
icmd.RunCommand(dockerCli.path, "save", fmt.Sprintf("docker/cnab-app-base:%s", internal.Version), "--output", tmpDir.Join("cnab-app-base.tar.gz")).Assert(t, icmd.Success)
390+
391+
// We need two contexts:
392+
// - one for `docker` so that it connects to the dind swarm created before
393+
// - the target context for the invocation image to install within the swarm
394+
cmd.Command = dockerCli.Command("context", "create", "swarm-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, swarm.GetAddress(t)), "--default-stack-orchestrator", "swarm")
395+
icmd.RunCmd(*cmd).Assert(t, icmd.Success)
396+
397+
// When creating a context on a Windows host we cannot use
398+
// the unix socket but it's needed inside the invocation image.
399+
// The workaround is to create a context with an empty host.
400+
// This host will default to the unix socket inside the
401+
// invocation image
402+
host := "host="
403+
if !useBindMount {
404+
host += fmt.Sprintf("tcp://%s", swarm.GetPrivateAddress(t))
405+
}
406+
407+
cmd.Command = dockerCli.Command("context", "create", "swarm-target-context", "--docker", host, "--default-stack-orchestrator", "swarm")
408+
icmd.RunCmd(*cmd).Assert(t, icmd.Success)
409+
410+
// Initialize the swarm
411+
cmd.Env = append(cmd.Env, "DOCKER_CONTEXT=swarm-context")
412+
cmd.Command = dockerCli.Command("swarm", "init")
413+
icmd.RunCmd(*cmd).Assert(t, icmd.Success)
414+
415+
// Load the needed base cnab image into the swarm docker engine
416+
cmd.Command = dockerCli.Command("load", "--input", tmpDir.Join("cnab-app-base.tar.gz"))
417+
icmd.RunCmd(*cmd).Assert(t, icmd.Success)
418+
}
419+
396420
func checkContains(t *testing.T, combined string, expectedLines []string) {
397421
for _, expected := range expectedLines {
398422
exp := regexp.MustCompile(expected)

internal/commands/cnab.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,8 @@ func prepareCustomAction(actionName string, dockerCli command.Cli, appname strin
335335
}
336336
return a, c, errBuf, nil
337337
}
338+
339+
func isInstallationFailed(installation *claim.Claim) bool {
340+
return installation.Result.Action == claim.ActionInstall &&
341+
installation.Result.Status == claim.StatusFailure
342+
}

internal/commands/install.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package commands
22

33
import (
44
"fmt"
5+
"os"
56

67
"github.com/deislabs/duffle/pkg/action"
78
"github.com/deislabs/duffle/pkg/claim"
@@ -90,8 +91,15 @@ func runInstall(dockerCli command.Cli, appname string, opts installOptions) erro
9091
if installationName == "" {
9192
installationName = bndl.Name
9293
}
93-
if _, err = installationStore.Read(installationName); err == nil {
94-
return fmt.Errorf("installation %q already exists", installationName)
94+
if installation, err := installationStore.Read(installationName); err == nil {
95+
// A failed installation can be overridden, but with a warning
96+
if isInstallationFailed(&installation) {
97+
fmt.Fprintf(os.Stderr, "WARNING: installing over previously failed installation %q\n", installationName)
98+
} else {
99+
// Return an error in case of successful installation, or even failed upgrade, which means
100+
// their was already a successful installation.
101+
return fmt.Errorf("Installation %q already exists, use 'docker app upgrade' instead", installationName)
102+
}
95103
}
96104
c, err := claim.New(installationName)
97105
if err != nil {
@@ -123,12 +131,17 @@ func runInstall(dockerCli command.Cli, appname string, opts installOptions) erro
123131
inst := &action.Install{
124132
Driver: driverImpl,
125133
}
126-
err = inst.Run(c, creds, dockerCli.Out())
134+
err = inst.Run(c, creds, os.Stdout)
127135
// Even if the installation failed, the installation is persisted with its failure status,
128136
// so any installation needs a clean uninstallation.
129137
err2 := installationStore.Store(*c)
130138
if err != nil {
131-
return fmt.Errorf("install failed: %s", errBuf)
139+
return fmt.Errorf("Installation failed: %s", errBuf)
132140
}
133-
return err2
141+
if err2 != nil {
142+
return err2
143+
}
144+
145+
fmt.Printf("Application %q installed on context %q\n", installationName, opts.targetContext)
146+
return nil
134147
}

internal/commands/uninstall.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package commands
22

33
import (
44
"fmt"
5+
"os"
56

67
"github.com/deislabs/duffle/pkg/action"
78
"github.com/deislabs/duffle/pkg/credentials"
@@ -58,12 +59,15 @@ func runUninstall(dockerCli command.Cli, installationName string, opts credentia
5859
uninst := &action.Uninstall{
5960
Driver: driverImpl,
6061
}
61-
err = uninst.Run(&c, creds, dockerCli.Out())
62-
if err == nil {
63-
return installationStore.Delete(installationName)
62+
if err := uninst.Run(&c, creds, os.Stdout); err != nil {
63+
if err2 := installationStore.Store(c); err2 != nil {
64+
return fmt.Errorf("%s while %s", err2, errBuf)
65+
}
66+
return fmt.Errorf("Uninstall failed: %s", errBuf)
6467
}
65-
if err2 := installationStore.Store(c); err2 != nil {
66-
fmt.Fprintf(dockerCli.Err(), "failed to update installation: %s\n", err2)
68+
if err := installationStore.Delete(installationName); err != nil {
69+
return fmt.Errorf("Failed to delete installation %q from the installation store: %s", installationName, err)
6770
}
68-
return fmt.Errorf("uninstall failed: %s", errBuf)
71+
fmt.Printf("Application %q uninstalled on context %q\n", installationName, opts.targetContext)
72+
return nil
6973
}

internal/commands/upgrade.go

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package commands
22

33
import (
44
"fmt"
5+
"os"
56

67
"github.com/deislabs/duffle/pkg/action"
78
"github.com/deislabs/duffle/pkg/credentials"
@@ -46,48 +47,56 @@ func runUpgrade(dockerCli command.Cli, installationName string, opts upgradeOpti
4647
return err
4748
}
4849

49-
c, err := installationStore.Read(installationName)
50+
installation, err := installationStore.Read(installationName)
5051
if err != nil {
5152
return err
5253
}
5354

55+
if isInstallationFailed(&installation) {
56+
return fmt.Errorf("Installation %q has failed and cannot be upgraded, reinstall it using 'docker app install'", installationName)
57+
}
58+
5459
if opts.bundleOrDockerApp != "" {
5560
b, err := resolveBundle(dockerCli, bundleStore, opts.bundleOrDockerApp, opts.pull, opts.insecureRegistries)
5661
if err != nil {
5762
return err
5863
}
59-
c.Bundle = b
64+
installation.Bundle = b
6065
}
61-
if err := mergeBundleParameters(&c,
66+
if err := mergeBundleParameters(&installation,
6267
withFileParameters(opts.parametersFiles),
6368
withCommandLineParameters(opts.overrides),
6469
withSendRegistryAuth(opts.sendRegistryAuth),
6570
); err != nil {
6671
return err
6772
}
6873

69-
bind, err := requiredClaimBindMount(c, opts.targetContext, dockerCli)
74+
bind, err := requiredClaimBindMount(installation, opts.targetContext, dockerCli)
7075
if err != nil {
7176
return err
7277
}
7378
driverImpl, errBuf, err := prepareDriver(dockerCli, bind, nil)
7479
if err != nil {
7580
return err
7681
}
77-
creds, err := prepareCredentialSet(c.Bundle, opts.CredentialSetOpts(dockerCli, credentialStore)...)
82+
creds, err := prepareCredentialSet(installation.Bundle, opts.CredentialSetOpts(dockerCli, credentialStore)...)
7883
if err != nil {
7984
return err
8085
}
81-
if err := credentials.Validate(creds, c.Bundle.Credentials); err != nil {
86+
if err := credentials.Validate(creds, installation.Bundle.Credentials); err != nil {
8287
return err
8388
}
8489
u := &action.Upgrade{
8590
Driver: driverImpl,
8691
}
87-
err = u.Run(&c, creds, dockerCli.Out())
88-
err2 := installationStore.Store(c)
92+
err = u.Run(&installation, creds, os.Stdout)
93+
err2 := installationStore.Store(installation)
8994
if err != nil {
90-
return fmt.Errorf("upgrade failed: %s", errBuf)
95+
return fmt.Errorf("Upgrade failed: %s", errBuf)
96+
}
97+
if err2 != nil {
98+
return err2
9199
}
92-
return err2
100+
fmt.Printf("Application %q upgraded on context %q\n", installationName, opts.targetContext)
101+
return nil
93102
}

internal/store/app.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ func (a ApplicationStore) InstallationStore(context string) (InstallationStore,
5757
if err := os.MkdirAll(path, 0755); err != nil {
5858
return nil, errors.Wrapf(err, "failed to create installation store directory for context %q", context)
5959
}
60-
return claim.NewClaimStore(crud.NewFileSystemStore(path, "json")), nil
60+
claimStore := claim.NewClaimStore(crud.NewFileSystemStore(path, "json"))
61+
return &installationStore{claimStore: claimStore}, nil
6162
}
6263

6364
// CredentialStore initializes and returns a context based credential store

internal/store/installation.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,41 @@
11
package store
22

3-
import "github.com/deislabs/duffle/pkg/claim"
3+
import (
4+
"fmt"
5+
6+
"github.com/deislabs/duffle/pkg/claim"
7+
)
48

59
// InstallationStore is an interface to persist claims.
610
type InstallationStore interface {
711
List() ([]string, error)
8-
Store(installationName claim.Claim) error
12+
Store(installation claim.Claim) error
913
Read(installationName string) (claim.Claim, error)
1014
Delete(installationName string) error
1115
}
1216

13-
var _ InstallationStore = &claim.Store{}
17+
var _ InstallationStore = &installationStore{}
18+
19+
type installationStore struct {
20+
claimStore claim.Store
21+
}
22+
23+
func (i installationStore) List() ([]string, error) {
24+
return i.claimStore.List()
25+
}
26+
27+
func (i installationStore) Store(installation claim.Claim) error {
28+
return i.claimStore.Store(installation)
29+
}
30+
31+
func (i installationStore) Read(installationName string) (claim.Claim, error) {
32+
c, err := i.claimStore.Read(installationName)
33+
if err == claim.ErrClaimNotFound {
34+
return claim.Claim{}, fmt.Errorf("Installation %q not found", installationName)
35+
}
36+
return c, err
37+
}
38+
39+
func (i installationStore) Delete(installationName string) error {
40+
return i.claimStore.Delete(installationName)
41+
}

0 commit comments

Comments
 (0)