Skip to content

Commit

Permalink
feat: add rollback support
Browse files Browse the repository at this point in the history
It is really important to use different tags of your docker image, or else this command will not work.
  • Loading branch information
bjarneo committed Nov 13, 2024
1 parent 1cfcde9 commit b4c92f7
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 21 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ FROM nginx:alpine
RUN echo '<!DOCTYPE html>\
<html>\
<head>\
<title>Welcome to copepod</title>\
<title>Welcome to copepod3</title>\
</head>\
<body>\
<div class="container">\
<h1>Welcome to Copepod!</h1>\
<h1>Welcome to Copepod3!</h1>\
<p>If you see this page, the nginx web server is successfully installed and working.</p>\
<p>This page was created from within the Dockerfile.</p>\
</div>\
Expand Down
39 changes: 23 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,19 @@ go build -o copepod
### Command Line Options

| Option | Environment Variable | Default | Description |
|-----------------|---------------------|------------------|--------------------------------|
| --host | DEPLOY_HOST | | Remote host to deploy to |
| --user | DEPLOY_USER | | SSH user for remote host |
| --image | DEPLOY_IMAGE | copepod_app | Docker image name |
| --tag | DEPLOY_TAG | latest | Docker image tag |
| --platform | DEPLOY_PLATFORM | linux/amd64 | Docker platform |
| --ssh-key | SSH_KEY_PATH | | Path to SSH key |
| --container-name| CONTAINER_NAME | copepod_app | Name for the container |
| --container-port| CONTAINER_PORT | 3000 | Container port |
| --host-port | HOST_PORT | 3000 | Host port |
| --env-file | ENV_FILE | | Environment file |
| --build-arg | BUILD_ARGS | | Build arguments (KEY=VALUE) |
|-----------------|---------------------|------------------|----------------------------------|
| --host | DEPLOY_HOST | | Remote host to deploy to |
| --user | DEPLOY_USER | | SSH user for remote host |
| --image | DEPLOY_IMAGE | copepod_app | Docker image name |
| --tag | DEPLOY_TAG | latest | Docker image tag |
| --platform | DEPLOY_PLATFORM | linux/amd64 | Docker platform |
| --ssh-key | SSH_KEY_PATH | | Path to SSH key |
| --container-name| CONTAINER_NAME | copepod_app | Name for the container |
| --container-port| CONTAINER_PORT | 3000 | Container port |
| --host-port | HOST_PORT | 3000 | Host port |
| --env-file | ENV_FILE | | Environment file |
| --build-arg | | | Build arguments (KEY=VALUE) |
| --rollback | | | Rollback to the previous instance |

### Example Commands

Expand All @@ -102,6 +103,13 @@ Using environment file:
./copepod --env-file .env.production
```

Rollback:

```bash
# For rollback to work you need to deploy using different tags, and not override the same tag each deploy
./codepod --host example.com --user deploy --container-name myapp --container-port 8080 --host-port 80 --rollback
```

Using build arguments:

```bash
Expand Down Expand Up @@ -170,10 +178,9 @@ The tool includes error handling for common scenarios:
## Known Limitations

1. Limited error recovery mechanisms
2. No rollback functionality
3. Basic container health checking
4. No support for complex Docker network configurations
5. No Docker Compose support
2. Basic container health checking
3. No support for complex Docker network configurations
4. No Docker Compose support

## Contributing

Expand Down
137 changes: 134 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Config struct {
ContainerPort string `json:"containerPort"`
HostPort string `json:"hostPort"`
EnvFile string `json:"envFile"`
Rollback bool `json:"rollback"`
BuildArgs map[string]string `json:"buildArgs"`
}

Expand Down Expand Up @@ -63,6 +64,7 @@ Options:
--host-port Host port (default: 3000)
--env-file Environment file (default: "")
--build-arg Build arguments (can be specified multiple times, format: KEY=VALUE)
--rollback Rollback to the previous version
--help Show this help message
Environment Variables:
Expand Down Expand Up @@ -157,6 +159,7 @@ func LoadConfig() Config {
flag.StringVar(&config.EnvFile, "env-file", getEnv("ENV_FILE", ""), "Environment file")
flag.Var(&buildArgs, "build-arg", "Build argument in KEY=VALUE format (can be specified multiple times)")
flag.BoolVar(&showHelp, "help", false, "Show help message")
flag.BoolVar(&config.Rollback, "rollback", false, "Rollback to previous version")

// Custom usage message
flag.Usage = func() {
Expand Down Expand Up @@ -275,6 +278,126 @@ func CheckSSH(config *Config, logger *Logger) error {
return err
}

// Rollback performs a rollback to the previous version by inspecting the current container
// TODO: fix this mess
// Rollback performs a rollback to the previous version using docker images history
func Rollback(config *Config, logger *Logger) error {
if err := logger.Info("Starting rollback process..."); err != nil {
return err
}

// Validate configuration
if err := config.ValidateConfig(); err != nil {
return err
}

// Check SSH connection
if err := CheckSSH(config, logger); err != nil {
return err
}

// Get current container image
getCurrentImageCmd := fmt.Sprintf("%s \"docker inspect --format='{{.Config.Image}}' %s\"",
getSSHCommand(config), config.ContainerName)
result, err := ExecuteCommand(logger, getCurrentImageCmd, "Getting current container information")
if err != nil {
return fmt.Errorf("failed to get current container information: %v", err)
}
currentImage := strings.TrimSpace(result.Stdout)

// Get image history sorted by creation time
// Format: repository, tag, image ID, created, size
getImagesCmd := fmt.Sprintf("%s \"docker images %s --format '{{.Repository}}:{{.Tag}}___{{.CreatedAt}}' | sort -k2 -r\"",
getSSHCommand(config), config.Image)
history, err := ExecuteCommand(logger, getImagesCmd, "Getting image history")
if err != nil {
return fmt.Errorf("failed to get image history: %v", err)
}

// Parse and sort images by creation time
images := strings.Split(strings.TrimSpace(history.Stdout), "\n")
if len(images) < 2 {
return fmt.Errorf("no previous version found to rollback to")
}

// Find current image and get the next one
var previousImage string
for i, img := range images {
imageName := strings.Split(img, "___")[0]
if imageName == currentImage && i+1 < len(images) {
previousImage = strings.Split(images[i+1], "___")[0]
break
}
}

if previousImage == "" {
return fmt.Errorf("could not find previous version to rollback to")
}

// Log the versions involved
if err := logger.Info(fmt.Sprintf("Found previous version: %s", previousImage)); err != nil {
return err
}

// Prepare rollback commands
envFileFlag := ""
if config.EnvFile != "" {
envFileFlag = fmt.Sprintf("--env-file ~/%s", config.EnvFile)
}

rollbackCommands := strings.Join([]string{
// Stop and rename current container (for backup)
fmt.Sprintf("docker stop %s", config.ContainerName),
fmt.Sprintf("docker rename %s %s_backup", config.ContainerName, config.ContainerName),

// Start container with previous version
fmt.Sprintf("docker run -d --name %s --restart unless-stopped -p %s:%s %s %s",
config.ContainerName, config.HostPort, config.ContainerPort,
envFileFlag, previousImage),
}, " && ")

// Execute rollback
rollbackCmd := fmt.Sprintf("%s \"%s\"", getSSHCommand(config), rollbackCommands)
if _, err := ExecuteCommand(logger, rollbackCmd, "Rolling back to previous version"); err != nil {
// If rollback fails, attempt to restore the backup
restoreCmd := fmt.Sprintf("%s \"docker stop %s || true && docker rm %s || true && docker rename %s_backup %s && docker start %s\"",
getSSHCommand(config), config.ContainerName, config.ContainerName,
config.ContainerName, config.ContainerName, config.ContainerName)
if _, restoreErr := ExecuteCommand(logger, restoreCmd, "Restoring previous version after failed rollback"); restoreErr != nil {
return fmt.Errorf("rollback failed and restore failed: %v (original error: %v)", restoreErr, err)
}
return fmt.Errorf("rollback failed, restored previous version: %v", err)
}

// Verify new container is running
verifyCmd := fmt.Sprintf("%s \"docker ps --filter name=%s --format '{{.Status}}'\"",
getSSHCommand(config), config.ContainerName)
result, err = ExecuteCommand(logger, verifyCmd, "Verifying rollback container status")
if err != nil {
return err
}

if !strings.Contains(result.Stdout, "Up") {
// If verification fails, attempt to restore the backup
restoreCmd := fmt.Sprintf("%s \"docker stop %s || true && docker rm %s || true && docker rename %s_backup %s && docker start %s\"",
getSSHCommand(config), config.ContainerName, config.ContainerName,
config.ContainerName, config.ContainerName, config.ContainerName)
if _, restoreErr := ExecuteCommand(logger, restoreCmd, "Restoring previous version after failed verification"); restoreErr != nil {
return fmt.Errorf("rollback verification failed and restore failed: %v", restoreErr)
}
return fmt.Errorf("rollback verification failed, restored previous version")
}

// Log the rollback details
logger.Info(fmt.Sprintf("Successfully rolled back from %s to %s", currentImage, previousImage))

// If everything is successful, remove the backup container
cleanupCmd := fmt.Sprintf("%s \"docker rm %s_backup\"", getSSHCommand(config), config.ContainerName)
_, _ = ExecuteCommand(logger, cleanupCmd, "Cleaning up backup container")

return logger.Info("Rollback completed successfully! 🔄")
}

// Deploy performs the main deployment process
func Deploy(config *Config, logger *Logger) error {
// Log start of deployment
Expand Down Expand Up @@ -379,8 +502,16 @@ func main() {
defer logger.Close()

config := LoadConfig()
if err := Deploy(&config, logger); err != nil {
logger.Error("Deployment failed", err)
os.Exit(1)

if config.Rollback {
if err := Rollback(&config, logger); err != nil {
logger.Error("Rollback failed", err)
os.Exit(1)
}
} else {
if err := Deploy(&config, logger); err != nil {
logger.Error("Deployment failed", err)
os.Exit(1)
}
}
}

0 comments on commit b4c92f7

Please sign in to comment.