diff --git a/README.md b/README.md index 9111da4..8b359e2 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,9 @@ To get started with Ploy, follow these steps: 3. **Customize Configuration**: Edit the generated `configuration.json` to suit your deployment and task requirements. 4. **Run Ploy**: Execute your configurations using `ploy run [pipelines (run multiple separated by a space)]`. +### Recipes +- [Zero Downtime Deployments](https://github.com/davesavic/ploy/recipes/zero-downtime-deployment.json) + ### Contributing Contributions to Ploy are welcome! If you have suggestions, bug reports, or want to contribute code, please visit create the relevant report. \ No newline at end of file diff --git a/cmd/run.go b/cmd/run.go index fc248eb..ac0ead4 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -19,7 +19,8 @@ var runCmd = &cobra.Command{ Short: "Run a ploy pipeline", Long: `Run a ploy pipeline`, Run: func(cmd *cobra.Command, args []string) { - cf, err := os.ReadFile("configuration.json") + configPath, _ := cmd.Flags().GetString("config") + cf, err := os.ReadFile(configPath) if err != nil { log.Fatal(err) } @@ -37,11 +38,12 @@ var runCmd = &cobra.Command{ for _, arg := range args { var executor ploy.PipelineExecutor local, _ := cmd.Flags().GetBool("local") + verbose, _ := cmd.Flags().GetBool("verbose") if local { - executor = &ploy.LocalPipelineExecutor{Config: cfg} + executor = &ploy.LocalPipelineExecutor{Config: cfg, Verbose: verbose} } else { - executor = &ploy.RemotePipelineExecutor{Config: cfg} + executor = &ploy.RemotePipelineExecutor{Config: cfg, Verbose: verbose} } out, err := executor.Execute(arg) @@ -58,4 +60,6 @@ func init() { rootCmd.AddCommand(runCmd) runCmd.Flags().BoolP("local", "l", false, "Run the pipeline locally") + runCmd.Flags().String("config", "configuration.json", "Path to configuration file") + runCmd.Flags().BoolP("verbose", "v", false, "Verbose output") } diff --git a/coverage.out b/coverage.out index a1de68f..bbecca1 100644 --- a/coverage.out +++ b/coverage.out @@ -1,38 +1,46 @@ mode: set -github.com/davesavic/ploy/pkg/ploy/ploy.go:49.75,51.13 2 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:51.13,53.3 1 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:55.2,57.26 2 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:57.26,59.3 1 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:61.2,61.31 1 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:61.31,65.14 3 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:65.14,67.4 1 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:69.3,70.17 2 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:70.17,72.4 1 0 -github.com/davesavic/ploy/pkg/ploy/ploy.go:74.3,75.17 2 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:75.17,77.4 1 0 -github.com/davesavic/ploy/pkg/ploy/ploy.go:79.3,88.17 3 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:88.17,90.4 1 0 -github.com/davesavic/ploy/pkg/ploy/ploy.go:92.3,92.30 1 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:92.30,94.15 2 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:94.15,96.5 1 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:98.4,98.31 1 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:98.31,101.19 3 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:101.19,103.6 1 0 -github.com/davesavic/ploy/pkg/ploy/ploy.go:105.5,106.19 2 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:106.19,108.6 1 0 -github.com/davesavic/ploy/pkg/ploy/ploy.go:110.5,112.60 2 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:112.60,114.6 1 0 -github.com/davesavic/ploy/pkg/ploy/ploy.go:118.3,118.40 1 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:118.40,120.4 1 0 -github.com/davesavic/ploy/pkg/ploy/ploy.go:123.2,123.26 1 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:126.53,130.27 3 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:130.27,132.3 1 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:139.74,143.13 3 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:143.13,145.3 1 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:147.2,147.29 1 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:147.29,149.14 2 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:149.14,151.4 1 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:153.3,153.26 1 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:153.26,160.18 6 1 -github.com/davesavic/ploy/pkg/ploy/ploy.go:160.18,162.5 1 0 -github.com/davesavic/ploy/pkg/ploy/ploy.go:166.2,166.26 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:50.75,52.13 2 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:52.13,54.3 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:56.2,58.26 2 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:58.26,60.3 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:62.2,62.31 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:62.31,66.14 3 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:66.14,68.4 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:70.3,71.17 2 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:71.17,73.4 1 0 +github.com/davesavic/ploy/pkg/ploy/ploy.go:75.3,76.17 2 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:76.17,78.4 1 0 +github.com/davesavic/ploy/pkg/ploy/ploy.go:80.3,80.16 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:80.16,82.4 1 0 +github.com/davesavic/ploy/pkg/ploy/ploy.go:84.3,93.17 3 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:93.17,95.4 1 0 +github.com/davesavic/ploy/pkg/ploy/ploy.go:97.3,97.16 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:97.16,99.4 1 0 +github.com/davesavic/ploy/pkg/ploy/ploy.go:101.3,101.30 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:101.30,103.15 2 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:103.15,105.5 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:107.4,109.31 2 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:109.31,112.18 2 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:112.18,114.6 1 0 +github.com/davesavic/ploy/pkg/ploy/ploy.go:116.5,117.19 2 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:117.19,119.6 1 0 +github.com/davesavic/ploy/pkg/ploy/ploy.go:121.5,122.19 2 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:122.19,124.6 1 0 +github.com/davesavic/ploy/pkg/ploy/ploy.go:126.5,126.18 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:126.18,128.6 1 0 +github.com/davesavic/ploy/pkg/ploy/ploy.go:130.5,130.60 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:130.60,132.6 1 0 +github.com/davesavic/ploy/pkg/ploy/ploy.go:136.3,136.40 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:136.40,138.4 1 0 +github.com/davesavic/ploy/pkg/ploy/ploy.go:141.2,141.26 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:144.53,148.27 3 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:148.27,150.3 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:158.74,162.13 3 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:162.13,164.3 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:166.2,166.29 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:166.29,168.14 2 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:168.14,170.4 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:172.3,172.26 1 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:172.26,179.18 6 1 +github.com/davesavic/ploy/pkg/ploy/ploy.go:179.18,181.5 1 0 +github.com/davesavic/ploy/pkg/ploy/ploy.go:185.2,185.26 1 1 diff --git a/pkg/ploy/ploy.go b/pkg/ploy/ploy.go index fb6593d..a42d1b9 100644 --- a/pkg/ploy/ploy.go +++ b/pkg/ploy/ploy.go @@ -43,7 +43,8 @@ type PipelineExecutor interface { } type RemotePipelineExecutor struct { - Config Config + Config Config + Verbose bool } func (r *RemotePipelineExecutor) Execute(pipeline string) (string, error) { @@ -59,7 +60,7 @@ func (r *RemotePipelineExecutor) Execute(pipeline string) (string, error) { } for _, s := range pl.Servers { - out.Write([]byte(fmt.Sprintf("Running pipeline %s on server %s\n", pipeline, s))) + out.Write([]byte(fmt.Sprintf("Running pipeline [%s] on server [%s]\n", pipeline, s))) server, exists := r.Config.Servers[s] if !exists { @@ -76,6 +77,10 @@ func (r *RemotePipelineExecutor) Execute(pipeline string) (string, error) { return "", fmt.Errorf("error parsing private key (%s): %v", server.PrivateKey, err) } + if r.Verbose { + out.Write([]byte(" Private key successfully loaded\n")) + } + sshCfg := ssh.ClientConfig{ User: server.User, Auth: []ssh.AuthMethod{ @@ -89,14 +94,25 @@ func (r *RemotePipelineExecutor) Execute(pipeline string) (string, error) { return "", fmt.Errorf("error dialing SSH server (%s): %v", s, err) } + if r.Verbose { + out.Write([]byte(" SSH connection established\n")) + } + for _, t := range pl.Tasks { commands, exists := r.Config.Tasks[t] if !exists { return "", fmt.Errorf("task %s is not defined", t) } + out.Write([]byte(fmt.Sprintf(" Running task: %s\n", t))) + for _, c := range commands { populatePlaceholders(&c, r.Config.Params) + + if r.Verbose { + out.Write([]byte(fmt.Sprintf(" Executing: %s\n", c))) + } + session, err := client.NewSession() if err != nil { return "", fmt.Errorf("error creating SSH session: %v", err) @@ -107,7 +123,9 @@ func (r *RemotePipelineExecutor) Execute(pipeline string) (string, error) { return "", fmt.Errorf("error running command (%s): %v", c, err) } - out.Write(op) + if r.Verbose { + out.Write([]byte(fmt.Sprintf(" Output: %s\n\n", op))) + } if err := session.Close(); err != nil && err != io.EOF { return "", fmt.Errorf("error closing SSH session: %v", err) @@ -133,7 +151,8 @@ func populatePlaceholders(s *string, params Params) { } type LocalPipelineExecutor struct { - Config Config + Config Config + Verbose bool } func (l *LocalPipelineExecutor) Execute(pipeline string) (string, error) { diff --git a/pkg/ploy/ploy_test.go b/pkg/ploy/ploy_test.go index 4d2a184..8448425 100644 --- a/pkg/ploy/ploy_test.go +++ b/pkg/ploy/ploy_test.go @@ -52,7 +52,7 @@ func TestRemotePipelineExecutor_Execute2(t *testing.T) { { name: "valid execution", pipeline: "test", - expectedOutput: "Hello, World!", + expectedOutput: "Running task", prepareFunc: func() (*SSHTestServer, ploy.Config) { s := NewSSHTestServer("localhost:2222") s.SetOutputString("Hello, World!") diff --git a/recipes/zero-downtime-deployment.json b/recipes/zero-downtime-deployment.json new file mode 100644 index 0000000..6d1e234 --- /dev/null +++ b/recipes/zero-downtime-deployment.json @@ -0,0 +1,58 @@ +{ + "params": { + "deployment-dir": "/srv/www/app/", + "serving-dir": "/srv/www/app/current", + "retention": "5", + "timestamp-file": "/tmp/deployment_timestamp" + }, + "servers": { + "staging": { + "host": "111.111.111.111", + "port": 22, + "user": "ploy", + "private-key": "/home/user/.ssh/id_rsa" + } + }, + "tasks": { + "generate-timestamp": [ + "echo '{{timestamp}}' > {{timestamp-file}}" + ], + "create-deployment-dir": [ + "timestamp=$(cat {{timestamp-file}}) && mkdir -p {{deployment-dir}}$timestamp" + ], + "deploy-app": [ + "timestamp=$(cat {{timestamp-file}}) && touch {{deployment-dir}}$timestamp/deployed.txt" + ], + "update-symlink": [ + "timestamp=$(cat {{timestamp-file}}) && ln -sfn {{deployment-dir}}$timestamp {{serving-dir}}" + ], + "cleanup-old-deployments": [ + "ls -1 {{deployment-dir}} | grep -P '^\\d{14}$' | head -n -5 | xargs -I {} rm -rf {{deployment-dir}}{}" + ], + "rollback-to-previous": [ + "prev_deployment=$(ls -1 {{deployment-dir}} | grep -P '^\\d{14}$' | tail -n 2 | head -n 1) && ln -sfn {{deployment-dir}}$prev_deployment {{serving-dir}} && echo $prev_deployment > /tmp/current_deployment" + ] + }, + "pipelines": { + "deploy": { + "tasks": [ + "generate-timestamp", + "create-deployment-dir", + "deploy-app", + "update-symlink", + "cleanup-old-deployments" + ], + "servers": [ + "staging" + ] + }, + "rollback": { + "tasks": [ + "rollback-to-previous" + ], + "servers": [ + "staging" + ] + } + } +}