package deployers import ( "context" "errors" "sync/atomic" "time" "gitea.thegux.fr/hmdeploy/models" "github.com/rs/zerolog/log" ) var ErrContextDone = errors.New("unable to execute, context done") type IDeployer interface { Type() DeployerType Deploy() error Build() error Destroy() error Clear() error Error() error Done() <-chan struct{} } type DeployerType string const ( Nginx DeployerType = "nginx" Swarm DeployerType = "swarm" GracefulTimeout = 10 * time.Second DefaultStateTimeout = 30 * time.Second ) type checkStateOption struct { timeout *time.Duration } type fnStateOption func(c *checkStateOption) func WithTimeout(duration time.Duration) fnStateOption { return func(c *checkStateOption) { c.timeout = &duration } } // Base struct of the deployers. // It handles the main informations to build a deployer. // // "Inherited" deployers must implement three methods in order // to satify the `IDeployer` contract: // - `Deploy() error`: run shell command to deploy the archive remotly // - `Build() error`: build the archive // - `Clear() error`: clean all the ressources locally and remotly type deployer struct { //nolint:govet // ll ctx context.Context fnCancel context.CancelFunc chDone chan struct{} processing atomic.Bool type_ DeployerType errFlag error project *models.Project } func newDeployer(ctx context.Context, type_ DeployerType, project *models.Project) *deployer { d := &deployer{ ctx: ctx, type_: type_, project: project, processing: atomic.Bool{}, chDone: make(chan struct{}, 1), } d.processing.Store(false) return d } func (d *deployer) setDone(err error) { d.chDone <- struct{}{} d.errFlag = errors.Join(d.ctx.Err(), err) if err != nil && d.fnCancel != nil { d.fnCancel() } } // SetCancellationFunc sets a context cancellation function for the deployer. // // If two deployers are related on the same context, one failed and you want // to stop the execution of the others. Then, associate a cancel function // for the deployer and the cancel func will be fired if error occurred // during a deployment step. Stopping all the deployers. func (d *deployer) SetCancellationFunc(fnCancel context.CancelFunc) { d.fnCancel = fnCancel } func (d *deployer) Type() DeployerType { return d.type_ } func (d *deployer) Error() error { return d.errFlag } // Done returns a channel providing the shutdown or the termination // of the deployer. // // If the context is done, it will wait until all the current actions are done // for a graceful shutdown. It has a graceful timeout (see: `GracefulTimeout`). // // If the deployer is done, succeed or failed, it simply returns. func (d *deployer) Done() <-chan struct{} { chDone := make(chan struct{}) go func() { defer func() { close(chDone) }() for { select { case <-d.ctx.Done(): log.Debug().Str("deployer", string(d.type_)).Msg("context done catch") timeout := time.NewTicker(GracefulTimeout) tick := time.NewTicker(time.Second) defer tick.Stop() defer timeout.Stop() for { select { case <-timeout.C: log.Error(). Str("deployer", string(d.type_)). Msg("timeout while waiting for graceful shutdown") chDone <- struct{}{} return case <-tick.C: if !d.processing.Load() { chDone <- struct{}{} return } tick.Reset(1 * time.Second) } } case <-d.chDone: log.Debug().Str("deployer", string(d.type_)).Msg("terminated") chDone <- struct{}{} return } } }() return chDone }