rework graceful shutdown + rework golangci lint

This commit is contained in:
rmanach 2025-04-03 16:34:47 +02:00
parent 541a671bc2
commit 35ac9a21ef
12 changed files with 658 additions and 121 deletions

View File

@ -1,33 +1,372 @@
run: # Defines the configuration version.
timeout: 5m # The only possible value is "2".
modules-download-mode: readonly version: "2"
linters: linters:
# Default set of linters.
# The value can be: `standard`, `all`, `none`, or `fast`.
# Default: standard
default: all
# Enable specific linter.
# https://golangci-lint.run/usage/linters/#enabled-by-default
enable: enable:
- gofmt - asasalint
- govet - asciicheck
- goimports - bidichk
- bodyclose
- canonicalheader
- containedctx
- contextcheck
- copyloopvar
- cyclop
- decorder
- depguard
- dogsled
- dupl
- dupword
- durationcheck
- err113
- errcheck - errcheck
- staticcheck - errchkjson
- unused - errname
- gosimple - errorlint
- exhaustive
- exhaustruct
- exptostd
- fatcontext
- forbidigo
- forcetypeassert
- funlen
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gochecksumtype
- gocognit
- goconst
- gocritic
- gocyclo
- godot
- godox
- goheader
- gomoddirectives
- gomodguard
- goprintffuncname
- gosec
- gosmopolitan
- govet
- grouper
- iface
- importas
- inamedparam
- ineffassign - ineffassign
- typecheck - interfacebloat
- intrange
- ireturn
- lll
- loggercheck
- maintidx
- makezero
- mirror
- misspell
- mnd
- musttag
- nakedret
- nestif
- nilerr
- nilnesserr
- nilnil
- nlreturn
- noctx
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- recvcheck
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- staticcheck
- tagalign
- tagliatelle
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert
- unparam
- unused
- usestdlibvars
- usetesting
- varnamelen
- wastedassign
- whitespace
- wrapcheck
- wsl
- zerologlint
- misspell
- mnd
- funlen
- goconst
- gocritic
- gocyclo
- gosec
- govet
- unparam
- unused
- errname
# Disable specific linter
# https://golangci-lint.run/usage/linters/#disabled-by-default
disable:
- varnamelen
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- containedctx
- contextcheck
- copyloopvar
- cyclop
- decorder
- depguard
- dogsled
- dupl
- dupword
- durationcheck
- err113
- errcheck
- errchkjson
- errorlint
- exhaustive
- exhaustruct
- exptostd
- fatcontext
- forbidigo
- forcetypeassert
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gochecksumtype
- gocognit
- godot
- godox
- gosmopolitan
- goheader
- gomoddirectives
- gomodguard
- goprintffuncname
- grouper
- iface
- importas
- inamedparam
- ineffassign
- interfacebloat
- intrange
- ireturn
- lll
- loggercheck
- maintidx
- makezero
- mirror
- musttag
- nakedret
- nestif
- nilerr
- nilnesserr
- nilnil
- nlreturn
- noctx
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- recvcheck
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- staticcheck
- tagalign
- tagliatelle
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert
- usestdlibvars
- usetesting
- wastedassign
- whitespace
- wrapcheck
- wsl
- zerologlint
# All available settings of specific linters.
settings:
# See the dedicated "linters.settings" documentation section.
option: value
# Defines a set of rules to ignore issues.
# It does not skip the analysis, and so does not ignore "typecheck" errors.
exclusions:
# Mode of the generated files analysis.
#
# - `strict`: sources are excluded by strictly following the Go generated file convention.
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
# This line must appear before the first non-comment, non-blank text in the file.
# https://go.dev/s/generatedcode
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
# - `disable`: disable the generated files exclusion.
#
# Default: lax
generated: strict
# Log a warning if an exclusion rule is unused.
# Default: false
warn-unused: true
# Predefined exclusion rules.
# Default: []
presets:
- comments
- std-error-handling
- common-false-positives
- legacy
# Excluding configuration per-path, per-linter, per-text and per-source.
# rules:
# # Exclude some linters from running on tests files.
# - path: _test\.go
# linters:
# - gocyclo
# - errcheck
# - dupl
# - gosec
# # Run some linter only for test files by excluding its issues for everything else.
# - path-except: _test\.go
# linters:
# - forbidigo
# # Exclude known linters from partially hard-vendored code,
# # which is impossible to exclude via `nolint` comments.
# # `/` will be replaced by the current OS file path separator to properly work on Windows.
# - path: internal/hmac/
# text: "weak cryptographic primitive"
# linters:
# - gosec
# # Exclude some `staticcheck` messages.
# - linters:
# - staticcheck
# text: "SA9003:"
# # Exclude `lll` issues for long lines with `go:generate`.
# - linters:
# - lll
# source: "^//go:generate "
# # Which file paths to exclude: they will be analyzed, but issues from them won't be reported.
# # "/" will be replaced by the current OS file path separator to properly work on Windows.
# # Default: []
# paths:
# - ".*\\.my\\.go$"
# - lib/bad.go
# # Which file paths to not exclude.
# # Default: []
# paths-except:
# - ".*\\.my\\.go$"
# - lib/bad.go
linters-settings: formatters:
gofmt: # Enable specific formatter.
simplify: true # Default: [] (uses standard Go formatting)
goimports: enable:
local-prefixes: gitea.thegux.fr - gci
- gofmt
- gofumpt
- goimports
- golines
# Formatters settings.
settings:
# See the dedicated "formatters.settings" documentation section.
option: value
# exclusions:
# # Mode of the generated files analysis.
# #
# # - `strict`: sources are excluded by strictly following the Go generated file convention.
# # Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
# # This line must appear before the first non-comment, non-blank text in the file.
# # https://go.dev/s/generatedcode
# # - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
# # - `disable`: disable the generated files exclusion.
# #
# # Default: lax
# generated: strict
# # Which file paths to exclude.
# # Default: []
# paths:
# - ".*\\.my\\.go$"
# - lib/bad.go
issues: # Options for analysis running.
exclude-rules: run:
- path: _test\.go # Timeout for total work, e.g. 30s, 5m, 5m30s.
linters: # If the value is lower or equal to 0, the timeout is disabled.
- errcheck # Default: 0 (disabled)
- staticcheck timeout: 5m
exclude-dirs: # The mode used to evaluate relative paths.
- .. # It's used by exclusions, Go plugins, and some linters.
# The value can be:
service: # - `gomod`: the paths will be relative to the directory of the `go.mod` file.
golangci-lint-version: 1.54.x # - `gitroot`: the paths will be relative to the git root (the parent directory of `.git`).
# - `cfg`: the paths will be relative to the configuration file.
# - `wd` (NOT recommended): the paths will be relative to the place where golangci-lint is run.
# Default: cfg
relative-path-mode: gomod
# Exit code when at least one issue was found.
# Default: 1
issues-exit-code: 2
# Include test files or not.
# Default: true
tests: false
# # List of build tags, all linters use it.
# # Default: []
# build-tags:
# - mytag
# If set, we pass it to "go list -mod={option}". From "go help modules":
# If invoked with -mod=readonly, the go command is disallowed from the implicit
# automatic updating of go.mod described above. Instead, it fails when any changes
# to go.mod are needed. This setting is most useful to check that go.mod does
# not need updates, such as in a continuous integration and testing system.
# If invoked with -mod=vendor, the go command assumes that the vendor
# directory holds the correct copies of dependencies and ignores
# the dependency descriptions in go.mod.
#
# Allowed values: readonly|vendor|mod
# Default: ""
modules-download-mode: readonly
# Allow multiple parallel golangci-lint instances running.
# If false, golangci-lint acquires file lock on start.
# Default: false
allow-parallel-runners: true
# Allow multiple golangci-lint instances running, but serialize them around a lock.
# If false, golangci-lint exits with an error if it fails to acquire file lock on start.
# Default: false
allow-serial-runners: true
# Define the Go version limit.
# Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.22.
go: '1.23'
# Number of operating system threads (`GOMAXPROCS`) that can execute golangci-lint simultaneously.
# Default: 0 (automatically set to match Linux container CPU quota and
# fall back to the number of logical CPUs in the machine)
concurrency: 4

View File

@ -1,8 +1,9 @@
run: lint run: lint
go run main.go @go run main.go
build: lint build: lint
go build -o hmdeploy main.go @go build -o hmdeploy main.go
lint: lint:
golangci-lint run ./... @golangci-lint fmt ./...
@golangci-lint run ./...

View File

@ -54,7 +54,7 @@ func NewSSHConn(addr, user string, port int, privkey string) (SSHConn, error) {
sshConfig := ssh.ClientConfig{ sshConfig := ssh.ClientConfig{
User: user, User: user,
HostKeyCallback: ssh.InsecureIgnoreHostKey(), HostKeyCallback: ssh.InsecureIgnoreHostKey(), //nolint:gosec // no need
Auth: []ssh.AuthMethod{ Auth: []ssh.AuthMethod{
ssh.PublicKeys(sshPrivKey), ssh.PublicKeys(sshPrivKey),
}, },
@ -80,7 +80,7 @@ func (c *SSHConn) CopyFile(src, dest string) error {
if err != nil { if err != nil {
return fmt.Errorf("%w, addr=%s, err=%v", ErrSSHSession, c.addr, err) return fmt.Errorf("%w, addr=%s, err=%v", ErrSSHSession, c.addr, err)
} }
defer sshSession.Close() defer sshSession.Close() //nolint: errcheck // defered
fileInfo, err := os.Stat(src) fileInfo, err := os.Stat(src)
if err != nil { if err != nil {
@ -91,24 +91,44 @@ func (c *SSHConn) CopyFile(src, dest string) error {
if err != nil { if err != nil {
return fmt.Errorf("unable to open scp source file src=%s, err=%v", src, err) return fmt.Errorf("unable to open scp source file src=%s, err=%v", src, err)
} }
defer file.Close() defer file.Close() //nolint: errcheck // defered
go func() { go func() {
w, _ := sshSession.StdinPipe() w, _ := sshSession.StdinPipe()
defer w.Close() defer w.Close() //nolint: errcheck // defered
fmt.Fprintf(w, "C0644 %d %s\n", fileInfo.Size(), filepath.Base(dest)) if _, err := fmt.Fprintf(w, "C0644 %d %s\n", fileInfo.Size(), filepath.Base(dest)); err != nil {
log.Debug().
Err(err).
Str("src", src).
Str("dest", dest).
Msg("unable to write file info to scp")
return
}
if _, err := io.Copy(w, file); err != nil { if _, err := io.Copy(w, file); err != nil {
log.Debug().Err(err).Str("src", src).Str("dest", dest).Msg("unable to scp src to dest") log.Debug().Err(err).Str("src", src).Str("dest", dest).Msg("unable to scp src to dest")
return return
} }
fmt.Fprint(w, "\x00") if _, err := fmt.Fprint(w, "\x00"); err != nil {
log.Debug().
Err(err).
Str("src", src).
Str("dest", dest).
Msg("unable to write scp termination string")
}
}() }()
if err := sshSession.Run(fmt.Sprintf("scp -t %s", dest)); err != nil { if err := sshSession.Run(fmt.Sprintf("scp -t %s", dest)); err != nil {
return fmt.Errorf("%w, addr=%s, src=%s, dest=%s, err=%v", ErrSShCopy, c.addr, src, dest, err) return fmt.Errorf(
"%w, addr=%s, src=%s, dest=%s, err=%v",
ErrSShCopy,
c.addr,
src,
dest,
err,
)
} }
return nil return nil
@ -119,7 +139,7 @@ func (c *SSHConn) Execute(cmd string) (string, error) {
if err != nil { if err != nil {
return "", fmt.Errorf("%w, addr=%s, err=%v", ErrSSHSession, c.addr, err) return "", fmt.Errorf("%w, addr=%s, err=%v", ErrSSHSession, c.addr, err)
} }
defer sshSession.Close() defer sshSession.Close() //nolint: errcheck // defered
var buf bytes.Buffer var buf bytes.Buffer
sshSession.Stdout = &buf sshSession.Stdout = &buf

View File

@ -1,10 +1,11 @@
package deployers package deployers
var ErrContextDone = "unable to execute, context done" import "errors"
var ErrContextDone = errors.New("unable to execute, context done")
type IDeployer interface { type IDeployer interface {
Deploy() error Deploy() error
Build() error Build() error
Clear() error Clear() error
Done() <-chan struct{}
} }

View File

@ -4,11 +4,12 @@ import (
"context" "context"
"fmt" "fmt"
"path/filepath" "path/filepath"
"sync/atomic"
"github.com/rs/zerolog/log" "time"
"gitea.thegux.fr/hmdeploy/connection" "gitea.thegux.fr/hmdeploy/connection"
"gitea.thegux.fr/hmdeploy/models" "gitea.thegux.fr/hmdeploy/models"
"github.com/rs/zerolog/log"
) )
type NginxDeployer struct { type NginxDeployer struct {
@ -17,25 +18,38 @@ type NginxDeployer struct {
conn connection.IConnection conn connection.IConnection
project *models.Project project *models.Project
chDone chan struct{} processing atomic.Bool
chDone chan struct{}
errFlag error
} }
var _ IDeployer = (*NginxDeployer)(nil) var _ IDeployer = (*NginxDeployer)(nil)
func NewNginxDeployer(ctx context.Context, netInfo *models.HMNetInfo, project *models.Project) (NginxDeployer, error) { func NewNginxDeployer(
ctx context.Context,
netInfo *models.HMNetInfo,
project *models.Project,
) (*NginxDeployer, error) {
var nd NginxDeployer var nd NginxDeployer
conn, err := connection.NewSSHConn(netInfo.IP.String(), netInfo.SSH.User, netInfo.SSH.Port, netInfo.SSH.PrivKey) conn, err := connection.NewSSHConn(
netInfo.IP.String(),
netInfo.SSH.User,
netInfo.SSH.Port,
netInfo.SSH.PrivKey,
)
if err != nil { if err != nil {
return nd, err return &nd, err
} }
nd.conn = &conn nd.conn = &conn
nd.project = project nd.project = project
nd.chDone = make(chan struct{}, 5)
nd.ctx = ctx nd.ctx = ctx
nd.processing = atomic.Bool{}
nd.processing.Store(false)
nd.chDone = make(chan struct{}, 1)
return nd, nil return &nd, nil
} }
func (nd *NginxDeployer) close() error { func (nd *NginxDeployer) close() error {
@ -47,19 +61,46 @@ func (nd *NginxDeployer) clean() (err error) {
return return
} }
func (nd *NginxDeployer) setDone() { func (nd *NginxDeployer) setDone(err error) {
nd.chDone <- struct{}{} nd.chDone <- struct{}{}
nd.errFlag = err
}
func (nd *NginxDeployer) Error() error {
return nd.errFlag
} }
func (nd *NginxDeployer) Done() <-chan struct{} { func (nd *NginxDeployer) Done() <-chan struct{} {
chDone := make(chan struct{}) chDone := make(chan struct{})
go func() { go func() {
defer func() {
close(chDone)
}()
for { for {
select { select {
case <-nd.chDone:
chDone <- struct{}{}
return
case <-nd.ctx.Done(): case <-nd.ctx.Done():
log.Warn().Str("deployer", "swarm").Msg("context done catch")
timeout := time.NewTicker(10 * time.Second) //nolint:mnd //TODO: to refactor
tick := time.NewTicker(time.Second)
for {
select {
case <-timeout.C:
log.Error().
Msg("timeout while waiting for graceful swarm deployer shutdown")
chDone <- struct{}{}
return
case <-tick.C:
if !nd.processing.Load() {
chDone <- struct{}{}
return
}
tick.Reset(1 * time.Second)
}
}
case <-nd.chDone:
log.Info().Str("deployer", "nginx").Msg("terminated")
chDone <- struct{}{} chDone <- struct{}{}
return return
} }
@ -84,9 +125,12 @@ func (nd *NginxDeployer) Clear() error {
} }
func (nd *NginxDeployer) Build() error { func (nd *NginxDeployer) Build() error {
nd.processing.Store(true)
defer nd.processing.Store(false)
select { select {
case <-nd.ctx.Done(): case <-nd.ctx.Done():
nd.setDone() nd.errFlag = ErrContextDone
return fmt.Errorf("%w, build nginx archive skipped", ErrContextDone) return fmt.Errorf("%w, build nginx archive skipped", ErrContextDone)
default: default:
} }
@ -94,22 +138,24 @@ func (nd *NginxDeployer) Build() error {
nginxPath := filepath.Join(nd.project.Dir, filepath.Base(nd.project.Deps.NginxFile)) nginxPath := filepath.Join(nd.project.Dir, filepath.Base(nd.project.Deps.NginxFile))
nginxConf := nd.project.Name + ".conf" nginxConf := nd.project.Name + ".conf"
log.Info().Str("nginx", nginxConf).Msg("transfering nginx conf...") log.Info().Str("nginx", nginxConf).Msg("transferring nginx conf...")
if err := nd.conn.CopyFile(nginxPath, nginxConf); err != nil { if err := nd.conn.CopyFile(nginxPath, nginxConf); err != nil {
nd.setDone() nd.setDone(err)
return err return err
} }
log.Info().Str("nginx", nginxConf).Msg("nginx conf transfered with success") log.Info().Str("nginx", nginxConf).Msg("nginx conf transferred with success")
return nil return nil
} }
func (nd *NginxDeployer) Deploy() (err error) { func (nd *NginxDeployer) Deploy() (err error) {
defer nd.setDone() nd.processing.Store(true)
defer nd.processing.Store(false)
select { select {
case <-nd.ctx.Done(): case <-nd.ctx.Done():
nd.errFlag = ErrContextDone
return fmt.Errorf("%w, nginx deployment skipped", ErrContextDone) return fmt.Errorf("%w, nginx deployment skipped", ErrContextDone)
default: default:
} }
@ -121,10 +167,16 @@ func (nd *NginxDeployer) Deploy() (err error) {
_, err = nd.conn.Execute( _, err = nd.conn.Execute(
fmt.Sprintf( fmt.Sprintf(
"cp %s /etc/nginx/sites-available && ln -sf /etc/nginx/sites-available/%s /etc/nginx/sites-enabled/%s", "cp %s /etc/nginx/sites-available && ln -sf /etc/nginx/sites-available/%s /etc/nginx/sites-enabled/%s",
nginxConf, nginxConf, nginxConf, nginxConf,
nginxConf,
nginxConf,
), ),
) )
nd.setDone(err)
if err == nil {
log.Info().Str("nginx", nginxConf).Msg("nginx conf successfully deployed")
}
log.Info().Str("nginx", nginxConf).Msg("nginx conf successfully deployed")
return err return err
} }

View File

@ -2,18 +2,22 @@ package deployers
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"sync/atomic"
"github.com/rs/zerolog/log" "time"
"gitea.thegux.fr/hmdeploy/connection" "gitea.thegux.fr/hmdeploy/connection"
"gitea.thegux.fr/hmdeploy/docker" "gitea.thegux.fr/hmdeploy/docker"
"gitea.thegux.fr/hmdeploy/models" "gitea.thegux.fr/hmdeploy/models"
"gitea.thegux.fr/hmdeploy/utils" "gitea.thegux.fr/hmdeploy/utils"
"github.com/rs/zerolog/log"
) )
var ErrSwarmDeployerNoArchive = errors.New("no archive found to be deployed")
type SwarmDeployer struct { type SwarmDeployer struct {
ctx context.Context ctx context.Context
@ -23,26 +27,40 @@ type SwarmDeployer struct {
project *models.Project project *models.Project
archivePath string archivePath string
chDone chan struct{} processing atomic.Bool
chDone chan struct{}
errFlag error
} }
var _ IDeployer = (*SwarmDeployer)(nil) var _ IDeployer = (*SwarmDeployer)(nil)
func NewSwarmDeployer(ctx context.Context, dockerClient docker.IClient, netInfo *models.HMNetInfo, project *models.Project) (SwarmDeployer, error) { func NewSwarmDeployer(
ctx context.Context,
dockerClient docker.IClient,
netInfo *models.HMNetInfo,
project *models.Project,
) (*SwarmDeployer, error) {
var sd SwarmDeployer var sd SwarmDeployer
conn, err := connection.NewSSHConn(netInfo.IP.String(), netInfo.SSH.User, netInfo.SSH.Port, netInfo.SSH.PrivKey) conn, err := connection.NewSSHConn(
netInfo.IP.String(),
netInfo.SSH.User,
netInfo.SSH.Port,
netInfo.SSH.PrivKey,
)
if err != nil { if err != nil {
return sd, err return &sd, err
} }
sd.ctx = ctx sd.ctx = ctx
sd.conn = &conn sd.conn = &conn
sd.dcli = dockerClient sd.dcli = dockerClient
sd.project = project sd.project = project
sd.chDone = make(chan struct{}, 5) sd.processing = atomic.Bool{}
sd.processing.Store(false)
sd.chDone = make(chan struct{}, 1)
return sd, nil return &sd, nil
} }
func (sd *SwarmDeployer) close() error { func (sd *SwarmDeployer) close() error {
@ -50,24 +68,52 @@ func (sd *SwarmDeployer) close() error {
} }
func (sd *SwarmDeployer) clean() (err error) { func (sd *SwarmDeployer) clean() (err error) {
defer os.Remove(sd.archivePath) defer os.Remove(sd.archivePath) //nolint: errcheck // defered
_, err = sd.conn.Execute(fmt.Sprintf("rm -f %s %s *.tar.gz *.tar", models.ComposeFile, models.EnvFile)) _, err = sd.conn.Execute(
fmt.Sprintf("rm -f %s %s *.tar.gz *.tar", models.ComposeFile, models.EnvFile),
)
return return
} }
func (sd *SwarmDeployer) setDone() { func (sd *SwarmDeployer) setDone(err error) {
sd.chDone <- struct{}{} sd.chDone <- struct{}{}
sd.errFlag = err
}
func (sd *SwarmDeployer) Error() error {
return sd.errFlag
} }
func (sd *SwarmDeployer) Done() <-chan struct{} { func (sd *SwarmDeployer) Done() <-chan struct{} {
chDone := make(chan struct{}) chDone := make(chan struct{})
go func() { go func() {
defer func() {
close(chDone)
}()
for { for {
select { select {
case <-sd.chDone:
chDone <- struct{}{}
return
case <-sd.ctx.Done(): case <-sd.ctx.Done():
log.Warn().Str("deployer", "swarm").Msg("context done catch")
timeout := time.NewTicker(10 * time.Second) //nolint:mnd //TODO: to refactor
tick := time.NewTicker(time.Second)
for {
select {
case <-timeout.C:
log.Error().
Msg("timeout while waiting for graceful swarm deployer shutdown")
chDone <- struct{}{}
return
case <-tick.C:
if !sd.processing.Load() {
chDone <- struct{}{}
return
}
tick.Reset(1 * time.Second)
}
}
case <-sd.chDone:
log.Info().Str("deployer", "swarm").Msg("terminated")
chDone <- struct{}{} chDone <- struct{}{}
return return
} }
@ -93,9 +139,12 @@ func (sd *SwarmDeployer) Clear() error {
} }
func (sd *SwarmDeployer) Build() error { func (sd *SwarmDeployer) Build() error {
sd.processing.Store(true)
defer sd.processing.Store(false)
select { select {
case <-sd.ctx.Done(): case <-sd.ctx.Done():
sd.setDone() sd.errFlag = ErrContextDone
return fmt.Errorf("%w, swarm project build skipped", ErrContextDone) return fmt.Errorf("%w, swarm project build skipped", ErrContextDone)
default: default:
} }
@ -106,11 +155,11 @@ func (sd *SwarmDeployer) Build() error {
if imageName := sd.project.ImageName; imageName != "" { if imageName := sd.project.ImageName; imageName != "" {
tarFile, err := sd.dcli.Save(imageName, sd.project.Dir) tarFile, err := sd.dcli.Save(imageName, sd.project.Dir)
if err != nil { if err != nil {
sd.setDone() sd.setDone(err)
return err return err
} }
defer os.Remove(tarFile) defer os.Remove(tarFile) //nolint: errcheck // defered
filesToArchive = append(filesToArchive, tarFile) filesToArchive = append(filesToArchive, tarFile)
@ -118,16 +167,22 @@ func (sd *SwarmDeployer) Build() error {
} }
if envFilePath := sd.project.Deps.EnvFile; envFilePath != "" { if envFilePath := sd.project.Deps.EnvFile; envFilePath != "" {
filesToArchive = append(filesToArchive, filepath.Join(sd.project.Dir, filepath.Base(envFilePath))) filesToArchive = append(
filesToArchive,
filepath.Join(sd.project.Dir, filepath.Base(envFilePath)),
)
log.Info().Msg(".env file added to the archive for deployment") log.Info().Msg(".env file added to the archive for deployment")
} }
composeFileBase := filepath.Base(sd.project.Deps.ComposeFile) composeFileBase := filepath.Base(sd.project.Deps.ComposeFile)
filesToArchive = append(filesToArchive, filepath.Join(sd.project.Dir, composeFileBase)) filesToArchive = append(filesToArchive, filepath.Join(sd.project.Dir, composeFileBase))
archivePath, err := utils.CreateArchive(sd.project.Dir, fmt.Sprintf("%s-%s", sd.project.Name, "swarm"), filesToArchive...) archivePath, err := utils.CreateArchive(
sd.project.Dir,
fmt.Sprintf("%s-%s", sd.project.Name, "swarm"),
filesToArchive...)
if err != nil { if err != nil {
sd.setDone() sd.setDone(err)
return err return err
} }
@ -138,36 +193,46 @@ func (sd *SwarmDeployer) Build() error {
} }
func (sd *SwarmDeployer) Deploy() error { func (sd *SwarmDeployer) Deploy() error {
defer sd.setDone() sd.processing.Store(true)
defer sd.processing.Store(false)
select { select {
case <-sd.ctx.Done(): case <-sd.ctx.Done():
sd.errFlag = ErrContextDone
return fmt.Errorf("%w, swarm deployment skipped", ErrContextDone) return fmt.Errorf("%w, swarm deployment skipped", ErrContextDone)
default: default:
} }
if sd.archivePath == "" { if sd.archivePath == "" {
return fmt.Errorf("unable to deploy, no archive to deploy") sd.setDone(ErrSwarmDeployerNoArchive)
return ErrSwarmDeployerNoArchive
} }
log.Info().Str("archive", sd.archivePath).Msg("deploying archive to swarm...") log.Info().Str("archive", sd.archivePath).Msg("deploying archive to swarm...")
archiveDestPath := filepath.Base(sd.archivePath) archiveDestPath := filepath.Base(sd.archivePath)
log.Info().Str("archive", sd.archivePath).Msg("archive built with success, tranfering to swarm for deployment...") log.Info().
Str("archive", sd.archivePath).
Msg("archive built with success, tranfering to swarm for deployment...")
if err := sd.conn.CopyFile(sd.archivePath, archiveDestPath); err != nil { if err := sd.conn.CopyFile(sd.archivePath, archiveDestPath); err != nil {
sd.setDone(err)
return err return err
} }
if _, err := sd.conn.Execute(fmt.Sprintf("tar xzvf %s", archiveDestPath)); err != nil { if _, err := sd.conn.Execute(fmt.Sprintf("tar xzvf %s", archiveDestPath)); err != nil {
sd.setDone(err)
return err return err
} }
log.Info().Str("project", sd.project.Name).Msg("deploying swarm project...") log.Info().Str("project", sd.project.Name).Msg("deploying swarm project...")
composeFileBase := filepath.Base(sd.project.Deps.ComposeFile) composeFileBase := filepath.Base(sd.project.Deps.ComposeFile)
if _, err := sd.conn.Execute(fmt.Sprintf("docker stack deploy -c %s %s", composeFileBase, sd.project.Name)); err != nil { if _, err := sd.conn.Execute(fmt.Sprintf("docker stack deploy -c %s %s", composeFileBase, sd.project.Name)); err != nil {
sd.setDone(err)
return err return err
} }
log.Info().Msg("swarm deployment done with success") log.Info().Msg("swarm deployment done with success")
sd.setDone(nil)
return nil return nil
} }

View File

@ -37,7 +37,13 @@ func (c *Client) Save(imageName, dest string) (string, error) {
cmd := exec.Command("docker", "save", "-o", tarFile, imageName) cmd := exec.Command("docker", "save", "-o", tarFile, imageName)
cmd.Dir = dest cmd.Dir = dest
if _, err := cmd.Output(); err != nil { if _, err := cmd.Output(); err != nil {
return "", fmt.Errorf("%w, dir=%s, image=%s, err=%v", ErrDockerClientSave, dest, imageName, err) return "", fmt.Errorf(
"%w, dir=%s, image=%s, err=%v",
ErrDockerClientSave,
dest,
imageName,
err,
)
} }
return filepath.Join(dest, tarFile), nil return filepath.Join(dest, tarFile), nil

62
main.go
View File

@ -8,17 +8,21 @@ import (
"os/signal" "os/signal"
"path" "path"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gitea.thegux.fr/hmdeploy/deployers" "gitea.thegux.fr/hmdeploy/deployers"
"gitea.thegux.fr/hmdeploy/docker" "gitea.thegux.fr/hmdeploy/docker"
"gitea.thegux.fr/hmdeploy/models" "gitea.thegux.fr/hmdeploy/models"
"gitea.thegux.fr/hmdeploy/scheduler" "gitea.thegux.fr/hmdeploy/scheduler"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
) )
const HMDEPLOY_DIRNAME = ".homeserver" const (
const NETWORK_FILENAME = "map.json" HMDeployDirname string = ".homeserver"
NetworkFilename string = "map.json"
SchedulerNbWorkers uint8 = 4
SchedulerQueueCapacity uint32 = 30
)
var HOME_PATH = os.Getenv("HOME") var HOME_PATH = os.Getenv("HOME")
@ -27,13 +31,12 @@ func initLogger() {
log.Logger = log.With().Caller().Logger().Output(zerolog.ConsoleWriter{Out: os.Stderr}) log.Logger = log.With().Caller().Logger().Output(zerolog.ConsoleWriter{Out: os.Stderr})
} }
func main() { func main() { //nolint: funlen // TODO: to rework
ctx, stop := signal.NotifyContext( ctx, _ := signal.NotifyContext(
context.Background(), context.Background(),
os.Interrupt, os.Interrupt,
os.Kill, os.Kill,
) )
defer stop()
initLogger() initLogger()
log.Info().Msg("hmdeploy started") log.Info().Msg("hmdeploy started")
@ -41,7 +44,7 @@ func main() {
projectDir := flag.String("path", ".", "define the .homeserver project root dir") projectDir := flag.String("path", ".", "define the .homeserver project root dir")
flag.Parse() flag.Parse()
hmmap_path := path.Join(HOME_PATH, HMDEPLOY_DIRNAME, NETWORK_FILENAME) hmmap_path := path.Join(HOME_PATH, HMDeployDirname, NetworkFilename)
c, err := os.ReadFile(hmmap_path) c, err := os.ReadFile(hmmap_path)
if err != nil { if err != nil {
log.Fatal().Err(err).Str("conf", hmmap_path).Msg("unable to load configuration") log.Fatal().Err(err).Str("conf", hmmap_path).Msg("unable to load configuration")
@ -57,7 +60,10 @@ func main() {
if err != nil { if err != nil {
log.Fatal().Str("dir", *projectDir).Err(err).Msg("unable to init project from directory") log.Fatal().Str("dir", *projectDir).Err(err).Msg("unable to init project from directory")
} }
log.Info().Str("dir", project.Dir).Str("name", project.Name).Msg("project initialized with success") log.Info().
Str("dir", project.Dir).
Str("name", project.Name).
Msg("project initialized with success")
swarmNet := hmmap.GetSwarmNetInfo() swarmNet := hmmap.GetSwarmNetInfo()
if swarmNet == nil { if swarmNet == nil {
@ -70,7 +76,7 @@ func main() {
log.Fatal().Err(err).Msg("unable to init swarm deployer") log.Fatal().Err(err).Msg("unable to init swarm deployer")
} }
var nd deployers.IDeployer var nd *deployers.NginxDeployer
if project.Deps.NginxFile != "" { if project.Deps.NginxFile != "" {
nginxNet := hmmap.GetNginxNetInfo() nginxNet := hmmap.GetNginxNetInfo()
if nginxNet == nil { if nginxNet == nil {
@ -84,24 +90,40 @@ func main() {
return return
} }
nd = &d nd = d
} }
deployNginx := scheduler.NewTask("nginx-deploy", nd.Deploy) var deploySwarm *scheduler.Task
deploySwarm := scheduler.NewTask("swarm-deploy", sd.Deploy, deployNginx) if nd != nil {
deployNginx := scheduler.NewTask("nginx-deploy", nd.Deploy)
deploySwarm = scheduler.NewTask("swarm-deploy", sd.Deploy, deployNginx)
} else {
deploySwarm = scheduler.NewTask("swarm-deploy", sd.Deploy)
}
s := scheduler.NewScheduler(context.Background(), 30, 4) s := scheduler.NewScheduler(
s.Submit(scheduler.NewTask("swarm-build", sd.Build, deploySwarm)) context.Background(),
s.Submit(scheduler.NewTask("nginx-build", nd.Build)) SchedulerQueueCapacity,
SchedulerNbWorkers,
)
s.Submit(scheduler.NewTask("swarm-build", sd.Build, deploySwarm)) //nolint: errcheck // TODO
if nd != nil {
s.Submit(scheduler.NewTask("nginx-build", nd.Build)) //nolint: errcheck // TODO
}
<-nd.Done()
<-sd.Done() <-sd.Done()
<-nd.Done()
s.Submit(scheduler.NewTask("nginx-clear", nd.Clear)) s.Submit(scheduler.NewTask("nginx-clear", nd.Clear)) //nolint: errcheck // TODO
s.Submit(scheduler.NewTask("swarm-clear", sd.Clear)) s.Submit(scheduler.NewTask("swarm-clear", sd.Clear)) //nolint: errcheck // TODO
s.Stop() s.Stop()
<-s.Done() <-s.Done()
if sd.Error() != nil || nd.Error() != nil {
log.Error().Str("name", project.Name).Msg("unable to deploy project, see logs for details")
return
}
log.Info().Str("name", project.Name).Msg("project deployed successfully") log.Info().Str("name", project.Name).Msg("project deployed successfully")
} }

View File

@ -14,8 +14,10 @@ type HMNetInfo struct {
} `json:"ssh,omitempty"` } `json:"ssh,omitempty"`
} }
type HMVM map[string]*HMNetInfo type (
type HMLXC map[string]*HMNetInfo HMVM map[string]*HMNetInfo
HMLXC map[string]*HMNetInfo
)
type HMMap struct { type HMMap struct {
*HMNetInfo *HMNetInfo

View File

@ -15,7 +15,7 @@ const (
MainDir string = ".homeserver" MainDir string = ".homeserver"
ComposeFile string = "docker-compose.deploy.yml" ComposeFile string = "docker-compose.deploy.yml"
EnvFile = ".env" EnvFile string = ".env"
NginxFile = "nginx.conf" NginxFile = "nginx.conf"
ConfFile = "hmdeploy.json" ConfFile = "hmdeploy.json"
@ -36,7 +36,12 @@ func getFileInfo(baseDir, filePath string) (fs.FileInfo, error) {
fInf, err = os.Stat(fileAbsPath) fInf, err = os.Stat(fileAbsPath)
if err != nil { if err != nil {
return fInf, fmt.Errorf("%w, unable to stat file=%s, err=%v", ErrProjectConfFile, fileAbsPath, err) return fInf, fmt.Errorf(
"%w, unable to stat file=%s, err=%v",
ErrProjectConfFile,
fileAbsPath,
err,
)
} }
return fInf, nil return fInf, nil
@ -92,15 +97,31 @@ func ProjectFromDir(dir string) (Project, error) {
content, err := os.ReadFile(filepath.Join(dir, ConfFile)) content, err := os.ReadFile(filepath.Join(dir, ConfFile))
if err != nil { if err != nil {
return p, fmt.Errorf("%w, unable to read conf file=%s, err=%v", ErrProjectConfFile, ConfFile, err) return p, fmt.Errorf(
"%w, unable to read conf file=%s, err=%v",
ErrProjectConfFile,
ConfFile,
err,
)
} }
if err := json.Unmarshal(content, &p); err != nil { if err := json.Unmarshal(content, &p); err != nil {
return p, fmt.Errorf("%w, unable to parse conf file=%s, err=%v", ErrProjectConfFile, ConfFile, err) return p, fmt.Errorf(
"%w, unable to parse conf file=%s, err=%v",
ErrProjectConfFile,
ConfFile,
err,
)
} }
if err := p.validate(); err != nil { if err := p.validate(); err != nil {
return p, fmt.Errorf("%w, unable to validate project, name=%s, dir=%s, err=%v", ErrProjectConfFile, p.Name, p.Dir, err) return p, fmt.Errorf(
"%w, unable to validate project, name=%s, dir=%s, err=%v",
ErrProjectConfFile,
p.Name,
p.Dir,
err,
)
} }
return p, nil return p, nil

View File

@ -84,13 +84,13 @@ type Scheduler struct {
wg sync.WaitGroup wg sync.WaitGroup
capacity atomic.Uint32 capacity atomic.Uint32
workers int workers uint8
chTasks chan *Task chTasks chan *Task
tasks taskStore tasks taskStore
} }
func NewScheduler(ctx context.Context, capacity, workers int) *Scheduler { func NewScheduler(ctx context.Context, capacity uint32, workers uint8) *Scheduler {
ctxChild, fnCancel := context.WithCancel(ctx) ctxChild, fnCancel := context.WithCancel(ctx)
s := Scheduler{ s := Scheduler{
ctx: ctxChild, ctx: ctxChild,
@ -101,14 +101,14 @@ func NewScheduler(ctx context.Context, capacity, workers int) *Scheduler {
tasks: newTaskStore(), tasks: newTaskStore(),
wg: sync.WaitGroup{}, wg: sync.WaitGroup{},
} }
s.capacity.Add(uint32(capacity)) s.capacity.Add(capacity)
s.run() s.run()
return &s return &s
} }
func (s *Scheduler) run() { func (s *Scheduler) run() {
for i := 0; i < s.workers; i++ { for i := 0; i < int(s.workers); i++ {
s.wg.Add(1) s.wg.Add(1)
go func() { go func() {
defer s.wg.Done() defer s.wg.Done()
@ -126,7 +126,7 @@ func (s *Scheduler) run() {
s.tasks.setStatus(t, Success) s.tasks.setStatus(t, Success)
for _, nt := range t.Next { for _, nt := range t.Next {
s.Submit(nt) s.Submit(nt) //nolint: errcheck // TODO
} }
case <-s.ctx.Done(): case <-s.ctx.Done():
log.Warn().Msg("context done, stopping worker...") log.Warn().Msg("context done, stopping worker...")
@ -162,7 +162,7 @@ func (s *Scheduler) Submit(task *Task) error {
func (s *Scheduler) Done() <-chan struct{} { func (s *Scheduler) Done() <-chan struct{} {
chDone := make(chan struct{}) chDone := make(chan struct{})
go func() { go func() {
for { for { //nolint: staticcheck // no
select { select {
case <-s.ctx.Done(): case <-s.ctx.Done():
log.Info().Msg("waiting for scheduler task completion...") log.Info().Msg("waiting for scheduler task completion...")

View File

@ -16,7 +16,7 @@ func addToArchive(tw *tar.Writer, filename string) error {
if err != nil { if err != nil {
return err return err
} }
defer file.Close() defer file.Close() //nolint: errcheck // defered
info, err := file.Stat() info, err := file.Stat()
if err != nil { if err != nil {
@ -39,23 +39,31 @@ func addToArchive(tw *tar.Writer, filename string) error {
func CreateArchive(destDir, name string, files ...string) (string, error) { func CreateArchive(destDir, name string, files ...string) (string, error) {
now := time.Now().UTC() now := time.Now().UTC()
archivePath := filepath.Join(destDir, fmt.Sprintf("%s-%s.tar.gz", name, strings.Replace(now.Format(time.RFC3339), ":", "-", -1))) archivePath := filepath.Join(
destDir,
fmt.Sprintf("%s-%s.tar.gz", name, strings.ReplaceAll(now.Format(time.RFC3339), ":", "-")),
)
file, err := os.Create(archivePath) file, err := os.Create(archivePath)
if err != nil { if err != nil {
return "", fmt.Errorf("unable to create archive=%s, err=%v", archivePath, err) return "", fmt.Errorf("unable to create archive=%s, err=%v", archivePath, err)
} }
defer file.Close() defer file.Close() //nolint: errcheck // defered
gw := gzip.NewWriter(file) gw := gzip.NewWriter(file)
defer gw.Close() defer gw.Close() //nolint: errcheck // defered
tw := tar.NewWriter(gw) tw := tar.NewWriter(gw)
defer tw.Close() defer tw.Close() //nolint: errcheck // defered
for _, f := range files { for _, f := range files {
if err := addToArchive(tw, f); err != nil { if err := addToArchive(tw, f); err != nil {
return "", fmt.Errorf("unable to add file=%s to archive=%s, err=%v", f, archivePath, err) return "", fmt.Errorf(
"unable to add file=%s to archive=%s, err=%v",
f,
archivePath,
err,
)
} }
} }