restructure json conf + deploy nginx assets

This commit is contained in:
rmanach 2025-05-02 09:56:00 +02:00
parent 0dae6ae400
commit 07f096a0a5
9 changed files with 311 additions and 96 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
map.json
hmdeploy
hmdeploy
hmdeploy-linux*

View File

@ -10,7 +10,10 @@ import (
"github.com/rs/zerolog/log"
)
var ErrContextDone = errors.New("unable to execute, context done")
var (
ErrContextDone = errors.New("unable to execute, context done")
ErrEmptyArchive = errors.New("no file to add to the archive")
)
type IDeployer interface {
Type() DeployerType
@ -48,7 +51,8 @@ type deployer struct { //nolint:govet // ll
type_ DeployerType
errFlag error
project *models.Project
project *models.Project
archivePath string
}
func newDeployer(ctx context.Context, type_ DeployerType, project *models.Project) *deployer {

View File

@ -3,9 +3,12 @@ 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"
)
@ -40,12 +43,28 @@ func NewNginxDeployer(
return nd, nil
}
func (nd NginxDeployer) getConfPath() string {
return nd.project.GetNginxConfPath()
}
func (nd NginxDeployer) getAssetsPath() string {
return nd.project.GetNginxAssetsPath()
}
func (nd *NginxDeployer) close() error {
return nd.conn.Close()
}
func (nd *NginxDeployer) clean() (err error) {
_, err = nd.conn.Execute("rm -f " + nd.project.Name + ".conf")
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
}
@ -75,20 +94,36 @@ func (nd *NginxDeployer) Build() error {
default:
}
nginxConf := nd.project.Name + ".conf"
log.Info().Msg("building nginx archive for deployment...")
log.Info().Str("nginx", nginxConf).Msg("transferring nginx conf...")
filesToArchive := []string{}
if ap := nd.getAssetsPath(); ap != "" {
filesToArchive = append(filesToArchive, ap)
}
if cp := nd.getConfPath(); cp != "" {
filesToArchive = append(filesToArchive, cp)
}
if err := nd.conn.CopyFile(nd.project.Deps.NginxFile, nginxConf); err != nil {
nd.setDone(err)
if len(filesToArchive) == 0 {
return ErrEmptyArchive
}
archivePath, err := utils.CreateArchive(
nd.project.Dir,
fmt.Sprintf("%s-%s", nd.project.Name, "nginx"),
filesToArchive...,
)
if err != nil {
return err
}
log.Info().Str("nginx", nginxConf).Msg("nginx conf transferred with success")
nd.archivePath = archivePath
log.Info().Str("archive", archivePath).Msg("nginx archive built")
return nil
}
func (nd *NginxDeployer) Deploy() (err error) {
func (nd *NginxDeployer) Deploy() error {
nd.processing.Store(true)
defer nd.processing.Store(false)
@ -99,25 +134,55 @@ func (nd *NginxDeployer) Deploy() (err error) {
default:
}
nginxConf := nd.project.Name + ".conf"
log.Info().Str("nginx", nginxConf).Msg("deploying nginx conf...")
_, err = nd.conn.Execute(
fmt.Sprintf(
"cp %s /etc/nginx/sites-available && ln -sf /etc/nginx/sites-available/%s /etc/nginx/sites-enabled/%s",
nginxConf,
nginxConf,
nginxConf,
),
)
nd.setDone(err)
if err == nil {
log.Info().Str("nginx", nginxConf).Msg("nginx conf successfully deployed")
archiveDestPath := filepath.Base(nd.archivePath)
if err := nd.conn.CopyFile(nd.archivePath, archiveDestPath); err != nil {
nd.setDone(err)
return err
}
return err
if _, err := nd.conn.Execute(fmt.Sprintf("tar xzvf %s", archiveDestPath)); err != nil {
nd.setDone(err)
return err
}
if ap := nd.getAssetsPath(); ap != "" {
log.Info().Msg("deploying nginx assets...")
if _, err := nd.conn.Execute(
fmt.Sprintf(
"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()),
nd.project.Name,
),
); err != nil {
nd.setDone(err)
return err
}
}
if cp := nd.getConfPath(); cp != "" {
nginxConf := nd.project.Name + ".conf"
log.Info().Str("nginx", nginxConf).Msg("deploying nginx conf...")
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),
nginxConf,
nginxConf,
nginxConf,
),
); err != nil {
nd.setDone(err)
return err
}
}
log.Info().Msg("nginx project successfully deployed")
nd.setDone(nil)
return nil
}
func (nd *NginxDeployer) Destroy() (err error) {

View File

@ -19,10 +19,9 @@ var ErrSwarmDeployerNoArchive = errors.New("no archive found to be deployed")
// SwarmDeployer handles the deployment of a Docker service on the swarm instance.
type SwarmDeployer struct {
*deployer
conn connection.IConnection
dloc docker.IClient
drem *docker.RemoteClient
archivePath string
conn connection.IConnection
dloc docker.IClient
drem *docker.RemoteClient
}
var _ IDeployer = (*SwarmDeployer)(nil)
@ -54,6 +53,14 @@ func NewSwarmDeployer(
return sd, nil
}
func (sd SwarmDeployer) getComposePath() string {
return sd.project.GetComposePath()
}
func (sd SwarmDeployer) getEnvPath() string {
return sd.project.GetEnvPath()
}
func (sd *SwarmDeployer) close() error {
return sd.conn.Close()
}
@ -119,7 +126,7 @@ func (sd *SwarmDeployer) Build() error {
log.Info().Str("image", tarFile).Msg("image transferred with success")
}
if envFilePath := sd.project.Deps.EnvFile; envFilePath != "" {
if envFilePath := sd.getEnvPath(); envFilePath != "" {
filesToArchive = append(
filesToArchive,
envFilePath,
@ -129,7 +136,7 @@ func (sd *SwarmDeployer) Build() error {
filesToArchive = append(
filesToArchive,
sd.project.Deps.ComposeFile,
sd.getComposePath(),
)
archivePath, err := utils.CreateArchive(
@ -171,9 +178,6 @@ func (sd *SwarmDeployer) Deploy() error {
}
archiveDestPath := filepath.Base(sd.archivePath)
log.Info().
Str("archive", sd.archivePath).
Msg("archive built with success, tranferring to swarm for deployment...")
if err := sd.conn.CopyFile(sd.archivePath, archiveDestPath); err != nil {
sd.setDone(err)
return err
@ -185,7 +189,7 @@ func (sd *SwarmDeployer) Deploy() error {
}
log.Info().Str("project", sd.project.Name).Msg("deploying swarm project...")
composeFileBase := filepath.Base(sd.project.Deps.ComposeFile)
composeFileBase := filepath.Base(sd.getComposePath())
if err := sd.drem.DeployStack(sd.ctx, sd.project.Name, composeFileBase, docker.WithCheckState()); err != nil {
sd.setDone(err)
return err

View File

@ -18,7 +18,7 @@ import (
const (
stateTickDuration = 4 * time.Second
defaultStateTimeout = 30 * time.Second
defaultStateTimeout = 10 * time.Minute
)
var (

13
hmdeploy.example.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "my-api",
"dependencies": {
"swarm": {
"env": ".env",
"compose": "docker-compose.deploy.yml"
},
"nginx": {
"conf": "nginx.conf",
"assets": "assets/"
}
}
}

66
main.go
View File

@ -136,10 +136,13 @@ func (d *Deployers) generateTasksTree() scheduler.Tasks {
func (d *Deployers) waitForCompletion(s *scheduler.Scheduler) error {
var wg sync.WaitGroup
deps := []deployers.IDeployer{d.sd}
deps := []deployers.IDeployer{}
if d.nd != nil {
deps = append(deps, d.nd)
}
if d.sd != nil {
deps = append(deps, d.sd)
}
for idx := range deps {
if d := deps[idx]; d != nil {
@ -238,14 +241,40 @@ func loadHMMap() (models.HMMap, error) {
return hmmap, nil
}
func initSwarmDeployer(
ctx context.Context,
project *models.Project,
swarmNet *models.HMNetInfo,
fnCancel context.CancelFunc,
) (deployers.SwarmDeployer, error) {
dloc := docker.NewLocalClient()
drem, err := docker.NewRemoteClient(swarmNet)
if err != nil {
return deployers.SwarmDeployer{}, err
}
sd, err := deployers.NewSwarmDeployer(ctx, project, swarmNet, &dloc, &drem)
if err != nil {
return deployers.SwarmDeployer{}, fmt.Errorf(
"%w, unable to init swarm deployer, err=%v",
ErrDeployerInit,
err,
)
}
if fnCancel != nil {
sd.SetCancellationFunc(fnCancel)
}
return sd, nil
}
// initDeployers instanciates from `Project` and `HMMap` needed deployers and returns them.
//
// You can provide as an optional arg:
// - WithGlobalCancellation(fnCancel context.CancelFunc): close the global context, notifying all deployers to stop
// - WithNoSwarm(): disable Swarm deployment
// - WithNoNginx(): disable Nginx deployment
//
//nolint:funlen // not that so much...
func initDeployers(
ctx context.Context,
hmmap *models.HMMap,
@ -261,34 +290,21 @@ func initDeployers(
destroy: opt.destroy,
}
swarmNet := hmmap.GetSwarmNetInfo()
if swarmNet == nil {
return deps, fmt.Errorf("%w, swarm net info does not exist", ErrNetInfoNotFound)
}
if !opt.noSwarm && project.GetComposePath() != "" {
swarmNet := hmmap.GetSwarmNetInfo()
if swarmNet == nil {
return deps, fmt.Errorf("%w, swarm net info does not exist", ErrNetInfoNotFound)
}
dloc := docker.NewLocalClient()
drem, err := docker.NewRemoteClient(swarmNet)
if err != nil {
return deps, err
}
if !opt.noSwarm {
sd, err := deployers.NewSwarmDeployer(ctx, project, swarmNet, &dloc, &drem)
sd, err := initSwarmDeployer(ctx, project, swarmNet, opt.fnCancel)
if err != nil {
return deps, fmt.Errorf(
"%w, unable to init swarm deployer, err=%v",
ErrDeployerInit,
err,
)
return deps, err
}
deps.sd = &sd
if opt.fnCancel != nil {
sd.SetCancellationFunc(opt.fnCancel)
}
deps.sd = &sd
}
if !opt.noNginx && project.Deps.NginxFile != "" {
if !opt.noNginx && project.GetNginxConfPath() != "" {
nginxNet := hmmap.GetNginxNetInfo()
if nginxNet == nil {
return deps, fmt.Errorf("%w, nginx net info does not exist", ErrNetInfoNotFound)

View File

@ -22,10 +22,26 @@ const (
var ErrProjectConfFile = errors.New("project error")
func getFilepath(baseDir, filePath string) (string, error) {
filePath = filepath.Join(baseDir, filePath)
type filepathOption struct {
isDir bool
}
type fnFilepathOption func(*filepathOption)
func IsDir() fnFilepathOption {
return func(fo *filepathOption) {
fo.isDir = true
}
}
func getFilepath(baseDir, filePath string, options ...fnFilepathOption) (string, error) {
var opts filepathOption
for _, opt := range options {
opt(&opts)
}
if !filepath.IsAbs(filePath) {
filePath = filepath.Join(baseDir, filePath)
filePath, err := filepath.Abs(filePath) //nolint: govet
if err != nil {
return filePath, fmt.Errorf(
@ -47,7 +63,7 @@ func getFilepath(baseDir, filePath string) (string, error) {
)
}
if fileInfo.IsDir() {
if fileInfo.IsDir() && !opts.isDir {
return filePath, fmt.Errorf(
"%w, file=%s, err=%s",
ErrProjectConfFile,
@ -56,6 +72,15 @@ func getFilepath(baseDir, filePath string) (string, error) {
)
}
if !fileInfo.IsDir() && opts.isDir {
return filePath, fmt.Errorf(
"%w, file=%s, err=%s",
ErrProjectConfFile,
filePath,
"must be a dir",
)
}
return filePath, nil
}
@ -64,43 +89,73 @@ type Project struct {
Name string `json:"name"`
Dir string
Deps struct {
EnvFile string `json:"env"`
ComposeFile string `json:"compose"`
NginxFile string `json:"nginx"`
Swarm struct {
EnvFile string `json:"env"`
ComposeFile string `json:"compose"`
} `json:"swarm"`
Nginx struct {
Conf string `json:"conf"`
Assets string `json:"assets"`
}
} `json:"dependencies"`
ImageNames []string `json:"images"`
}
func (p *Project) validate() error {
cpath, err := getFilepath(p.Dir, p.Deps.ComposeFile)
if err != nil {
return err
}
p.Deps.ComposeFile = cpath
if p.Deps.EnvFile != "" {
epath, err := getFilepath(p.Dir, p.Deps.EnvFile)
if compf := p.Deps.Swarm.ComposeFile; compf != "" {
cpath, err := getFilepath(p.Dir, compf)
if err != nil {
return err
}
p.Deps.EnvFile = epath
p.Deps.Swarm.ComposeFile = cpath
} else {
log.Warn().Msg("no .env file provided, hoping one it's set elsewhere...")
log.Warn().Msg("no docker-compose file provided, Swarm deployment discards")
}
if p.Deps.NginxFile != "" {
npath, err := getFilepath(p.Dir, p.Deps.NginxFile)
if env := p.Deps.Swarm.EnvFile; env != "" {
epath, err := getFilepath(p.Dir, env)
if err != nil {
return err
}
p.Deps.NginxFile = npath
} else {
log.Warn().Msg("no Nginx conf file provided, Nginx deployment discarded")
p.Deps.Swarm.EnvFile = epath
}
if conf := p.Deps.Nginx.Conf; conf != "" {
npath, err := getFilepath(p.Dir, conf)
if err != nil {
return err
}
p.Deps.Nginx.Conf = npath
}
if assets := p.Deps.Nginx.Assets; assets != "" {
apath, err := getFilepath(p.Dir, assets, IsDir())
if err != nil {
return err
}
p.Deps.Nginx.Assets = apath
}
return nil
}
func (p Project) GetComposePath() string {
return p.Deps.Swarm.ComposeFile
}
func (p Project) GetEnvPath() string {
return p.Deps.Swarm.EnvFile
}
func (p Project) GetNginxConfPath() string {
return p.Deps.Nginx.Conf
}
func (p Project) GetNginxAssetsPath() string {
return p.Deps.Nginx.Assets
}
// ProjectFromDir instantiates a new project from a directory path.
//
// The directory path must refers to the path including the `.homeserver` dir not

View File

@ -17,25 +17,75 @@ import (
const confirmChar = "Y"
func addToArchive(tw *tar.Writer, filename string) error {
file, err := os.Open(filename)
// addToArchive adds a file or directory (recursively) to the tar writer.
// The baseName is the desired root name in the archive.
//
//nolint:funlen // it's ok
func addToArchive(tw *tar.Writer, filename, baseName, basePath string) error {
// resolve the absolute path to eliminate relative components like ../
absPath, err := filepath.Abs(filename)
if err != nil {
return fmt.Errorf("unable to resolve absolute path for %s: %v", filename, err)
}
file, err := os.Open(absPath)
if err != nil {
return err
}
defer file.Close() //nolint: errcheck // defered
defer file.Close() //nolint: errcheck // deferred
info, err := file.Stat()
if err != nil {
return err
}
header, err := tar.FileInfoHeader(info, info.Name())
// compute the relative path within the archive
// start with baseName and append the relative path from basePath
relPath, err := filepath.Rel(basePath, absPath)
if err != nil {
return fmt.Errorf("unable to compute relative path for %s: %v", absPath, err)
}
// combine baseName with the relative path
if relPath == "." {
relPath = baseName
} else {
relPath = filepath.Join(baseName, relPath)
}
relPath = filepath.ToSlash(relPath)
if info.IsDir() {
header, err := tar.FileInfoHeader(info, "") //nolint:govet // shadow ok
if err != nil {
return err
}
header.Name = relPath
if err := tw.WriteHeader(header); err != nil { //nolint:govet // shadow ok
return err
}
entries, err := os.ReadDir(absPath)
if err != nil {
return err
}
for _, entry := range entries {
entryPath := filepath.Join(absPath, entry.Name())
if err := addToArchive(tw, entryPath, baseName, basePath); err != nil {
return err
}
}
return nil
}
header, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
header.Name = filepath.Base(file.Name())
if err = tw.WriteHeader(header); err != nil {
header.Name = relPath
if err := tw.WriteHeader(header); err != nil { //nolint:govet // shadow ok
return err
}
@ -43,7 +93,8 @@ func addToArchive(tw *tar.Writer, filename string) error {
return err
}
// CreateArchive creates a gzip tar archive in the `destDir` path including `files`.
// CreateArchive creates a gzip tar archive in the `destDir` path including `files` and returns
// the generated archive path.
func CreateArchive(destDir, name string, files ...string) (string, error) {
now := time.Now().UTC()
archivePath := filepath.Join(
@ -55,16 +106,22 @@ func CreateArchive(destDir, name string, files ...string) (string, error) {
if err != nil {
return "", fmt.Errorf("unable to create archive=%s, err=%v", archivePath, err)
}
defer file.Close() //nolint: errcheck // defered
defer file.Close() //nolint: errcheck // deferred
gw := gzip.NewWriter(file)
defer gw.Close() //nolint: errcheck // defered
defer gw.Close() //nolint: errcheck // deferred
tw := tar.NewWriter(gw)
defer tw.Close() //nolint: errcheck // defered
defer tw.Close() //nolint: errcheck // deferred
for _, f := range files {
if err := addToArchive(tw, f); err != nil {
absPath, err := filepath.Abs(f)
if err != nil {
return "", fmt.Errorf("unable to resolve absolute path for %s: %v", f, err)
}
baseName := filepath.Base(f)
if err := addToArchive(tw, f, baseName, absPath); err != nil {
return "", fmt.Errorf(
"unable to add file=%s to archive=%s, err=%v",
f,