diff --git a/README.md b/README.md index 6c045c8..98fe561 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ hmdeploy --path /path/my-project --destroy * ~~Improve the CLI arguments~~ * ~~Destroy~~ * ~~Check deployment/undeployment state~~ -* Create a deployment temp dir (lock to avoid concurrent deployments) +* ~~Create a deployment temp dir (lock to avoid concurrent deployments)~~ * Deals with bugs * Tests 😮‍💨 diff --git a/deployers/commons.go b/deployers/commons.go index d5b90fa..e6b1eaf 100644 --- a/deployers/commons.go +++ b/deployers/commons.go @@ -3,9 +3,13 @@ package deployers import ( "context" "errors" + "fmt" + "os" + "path/filepath" "sync/atomic" "time" + "gitea.thegux.fr/hmdeploy/connection" "gitea.thegux.fr/hmdeploy/models" "github.com/rs/zerolog/log" ) @@ -51,21 +55,43 @@ type deployer struct { //nolint:govet // ll type_ DeployerType errFlag error - project *models.Project - archivePath string + conn connection.IConnection + project *models.Project + archivePath string + deploymentDir string } -func newDeployer(ctx context.Context, type_ DeployerType, project *models.Project) *deployer { +func newDeployer( + ctx context.Context, + type_ DeployerType, + project *models.Project, + netInfo *models.HMNetInfo, +) (*deployer, error) { + conn, err := connection.NewSSHConn( + netInfo.IP.String(), + netInfo.SSH.User, + netInfo.SSH.Port, + netInfo.SSH.PrivKey, + ) + if err != nil { + return nil, err + } + d := &deployer{ ctx: ctx, type_: type_, project: project, processing: atomic.Bool{}, chDone: make(chan struct{}, 1), + conn: &conn, } d.processing.Store(false) - return d + return d, nil +} + +func (d *deployer) close() error { + return d.conn.Close() } func (d *deployer) setDone(err error) { @@ -76,6 +102,50 @@ func (d *deployer) setDone(err error) { } } +func (d *deployer) clean() { + if err := os.Remove(d.archivePath); err != nil { + log.Err(err).Str("archive", d.archivePath).Msg("unable to clean local archive file") + } + + if d.deploymentDir != "" { + if _, err := d.conn.Execute("rm -rf " + d.deploymentDir); err != nil { + log.Err(err). + Str("dir", d.deploymentDir). + Str("type", string(d.type_)). + Msg("unable to clean deployment dir") + } + } +} + +func (d *deployer) copyUntarArchive() error { + deploymentDir := "." + d.project.Name + + // TODO(rmanach): check cmd error output to check if's not an other error + if _, err := d.conn.Execute("mkdir " + deploymentDir); err != nil { + log.Error(). + Str("dir", deploymentDir). + Msg("deployment dir already exists, unable to deploy now") + d.setDone(err) + return err + } + + d.deploymentDir = deploymentDir + + archiveName := filepath.Base(d.archivePath) + archiveDestPath := filepath.Join(deploymentDir, archiveName) + if err := d.conn.CopyFile(d.archivePath, archiveDestPath); err != nil { + d.setDone(err) + return err + } + + if _, err := d.conn.Execute(fmt.Sprintf("cd %s && tar xzvf %s", deploymentDir, archiveName)); err != nil { + d.setDone(err) + return err + } + + return nil +} + // SetCancellationFunc sets a context cancellation function for the deployer. // // If two deployers are related on the same context, one failed and you want @@ -94,6 +164,18 @@ func (d *deployer) Error() error { return d.errFlag } +func (d *deployer) Clear() error { + log.Debug().Str("type", string(d.type_)).Msg("clearing deployment...") + d.clean() + + if err := d.close(); err != nil { + log.Err(err).Msg("unable to close conn") + } + + log.Debug().Str("type", string(d.type_)).Msg("clear deployment done") + return nil +} + // Done returns a channel providing the shutdown or the termination // of the deployer. // diff --git a/deployers/nginx.go b/deployers/nginx.go index ab46427..ae84c60 100644 --- a/deployers/nginx.go +++ b/deployers/nginx.go @@ -3,10 +3,8 @@ package deployers import ( "context" "fmt" - "os" "path/filepath" - "gitea.thegux.fr/hmdeploy/connection" "gitea.thegux.fr/hmdeploy/models" "gitea.thegux.fr/hmdeploy/utils" "github.com/rs/zerolog/log" @@ -15,7 +13,6 @@ import ( // NginxDeployer handles the deployment of an Nginx configuration. type NginxDeployer struct { *deployer - conn connection.IConnection } var _ IDeployer = (*NginxDeployer)(nil) @@ -27,18 +24,11 @@ func NewNginxDeployer( ) (NginxDeployer, error) { var nd NginxDeployer - conn, err := connection.NewSSHConn( - netInfo.IP.String(), - netInfo.SSH.User, - netInfo.SSH.Port, - netInfo.SSH.PrivKey, - ) + deployer, err := newDeployer(ctx, Nginx, project, netInfo) if err != nil { return nd, err } - - nd.conn = &conn - nd.deployer = newDeployer(ctx, Nginx, project) + nd.deployer = deployer return nd, nil } @@ -51,38 +41,6 @@ func (nd NginxDeployer) getAssetsPath() string { return nd.project.GetNginxAssetsPath() } -func (nd *NginxDeployer) close() error { - return nd.conn.Close() -} - -func (nd *NginxDeployer) clean() (err error) { - if err = os.Remove(nd.archivePath); err != nil { - log.Err(err).Str("archive", nd.archivePath).Msg("unable to clean local nginx archive file") - } - - cmd := "rm -rf nginx.conf build/ *.tar.gz" - if ap := nd.getAssetsPath(); ap != "" { - cmd += " " + filepath.Base(nd.getAssetsPath()) - } - _, err = nd.conn.Execute(cmd) - return -} - -func (nd *NginxDeployer) Clear() error { - log.Debug().Msg("clearing nginx deployment...") - - if err := nd.clean(); err != nil { - log.Err(err).Msg("unable to clean nginx conf remotly") - } - - if err := nd.close(); err != nil { - log.Err(err).Msg("unable to close nginx conn") - } - - log.Debug().Msg("clear nginx deployment done") - return nil -} - func (nd *NginxDeployer) Build() error { nd.processing.Store(true) defer nd.processing.Store(false) @@ -134,14 +92,7 @@ func (nd *NginxDeployer) Deploy() error { default: } - archiveDestPath := filepath.Base(nd.archivePath) - if err := nd.conn.CopyFile(nd.archivePath, archiveDestPath); err != nil { - nd.setDone(err) - return err - } - - if _, err := nd.conn.Execute(fmt.Sprintf("tar xzvf %s", archiveDestPath)); err != nil { - nd.setDone(err) + if err := nd.copyUntarArchive(); err != nil { return err } @@ -153,7 +104,7 @@ func (nd *NginxDeployer) Deploy() error { "rm -rf /var/www/static/%s/* && mkdir -p /var/www/static/%s && mv %s/* /var/www/static/%s", nd.project.Name, nd.project.Name, - filepath.Base(nd.getAssetsPath()), + filepath.Join(nd.deploymentDir, filepath.Base(nd.getAssetsPath())), nd.project.Name, ), ); err != nil { @@ -169,7 +120,7 @@ func (nd *NginxDeployer) Deploy() error { if _, err := nd.conn.Execute( fmt.Sprintf( "mv %s /etc/nginx/sites-available/%s && ln -sf /etc/nginx/sites-available/%s /etc/nginx/sites-enabled/%s", - filepath.Base(cp), + filepath.Join(nd.deploymentDir, filepath.Base(cp)), nginxConf, nginxConf, nginxConf, diff --git a/deployers/swarm.go b/deployers/swarm.go index 746e212..e2918d6 100644 --- a/deployers/swarm.go +++ b/deployers/swarm.go @@ -35,20 +35,14 @@ func NewSwarmDeployer( ) (SwarmDeployer, error) { var sd SwarmDeployer - conn, err := connection.NewSSHConn( - netInfo.IP.String(), - netInfo.SSH.User, - netInfo.SSH.Port, - netInfo.SSH.PrivKey, - ) - if err != nil { - return sd, nil - } - - sd.conn = &conn sd.dloc = dloc sd.drem = drem - sd.deployer = newDeployer(ctx, Swarm, project) + + deployer, err := newDeployer(ctx, Swarm, project, netInfo) + if err != nil { + return sd, err + } + sd.deployer = deployer return sd, nil } @@ -61,36 +55,6 @@ func (sd SwarmDeployer) getEnvPath() string { return sd.project.GetEnvPath() } -func (sd *SwarmDeployer) close() error { - return sd.conn.Close() -} - -func (sd *SwarmDeployer) clean() (err error) { - if err = os.Remove(sd.archivePath); err != nil { - log.Err(err).Str("archive", sd.archivePath).Msg("unable to clean local swarm archive file") - } - _, err = sd.conn.Execute( - fmt.Sprintf("rm -f %s %s *.tar.gz *.tar", models.ComposeFile, models.EnvFile), - ) - return -} - -func (sd *SwarmDeployer) Clear() error { - log.Debug().Msg("clearing swarm deployment...") - - if err := sd.clean(); err != nil { - log.Err(err).Msg("unable to clean swarm conf remotly") - } - - if err := sd.close(); err != nil { - log.Err(err).Msg("unable to close swarm conn") - } - - log.Debug().Msg("clear swarm deployment done") - - return nil -} - // Build builds the archive with mandatory files to deploy a swarm service. // // After the build, the path of the local archive built is set in @@ -177,20 +141,13 @@ func (sd *SwarmDeployer) Deploy() error { return err } - archiveDestPath := filepath.Base(sd.archivePath) - if err := sd.conn.CopyFile(sd.archivePath, archiveDestPath); err != nil { - sd.setDone(err) - return err - } - - if _, err := sd.conn.Execute(fmt.Sprintf("tar xzvf %s", archiveDestPath)); err != nil { - sd.setDone(err) + if err := sd.copyUntarArchive(); err != nil { return err } log.Info().Str("project", sd.project.Name).Msg("deploying swarm project...") composeFileBase := filepath.Base(sd.getComposePath()) - if err := sd.drem.DeployStack(sd.ctx, sd.project.Name, composeFileBase, docker.WithCheckState()); err != nil { + if err := sd.drem.DeployStack(sd.ctx, sd.project.Name, composeFileBase, docker.WithCheckState(), docker.WithBaseDir(sd.deploymentDir)); err != nil { sd.setDone(err) return err } diff --git a/docker/client.go b/docker/client.go index 3f05032..0a86b94 100644 --- a/docker/client.go +++ b/docker/client.go @@ -31,6 +31,7 @@ var ( ) type stackOption struct { + baseDir string checkState bool } @@ -42,6 +43,12 @@ func WithCheckState() fnStackOption { } } +func WithBaseDir(dir string) fnStackOption { + return func(s *stackOption) { + s.baseDir = dir + } +} + func parseIDs(cmdOutput string) []string { ids := []string{} bufLine := []rune{} @@ -201,15 +208,19 @@ func (c *RemoteClient) DeployStack( projectName, composeFilepath string, options ...fnStackOption, ) error { - if _, err := c.conn.Execute(fmt.Sprintf("docker stack deploy -c %s %s --with-registry-auth", composeFilepath, projectName)); err != nil { - return err - } - var opts stackOption for _, opt := range options { opt(&opts) } + if bd := opts.baseDir; bd != "" { + composeFilepath = filepath.Join(bd, composeFilepath) + } + + if _, err := c.conn.Execute(fmt.Sprintf("docker stack deploy -c %s %s --with-registry-auth", composeFilepath, projectName)); err != nil { + return err + } + if opts.checkState { return c.checkState(ctx, projectName, Running) }