mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-30 17:38:51 +00:00
352 lines
9.6 KiB
Go
352 lines
9.6 KiB
Go
package jobs
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/runatlantis/atlantis/server/events/command"
|
|
"github.com/runatlantis/atlantis/server/events/models"
|
|
"github.com/runatlantis/atlantis/server/logging"
|
|
)
|
|
|
|
type OutputBuffer struct {
|
|
OperationComplete bool
|
|
Buffer []string
|
|
}
|
|
|
|
type PullInfo struct {
|
|
PullNum int
|
|
Repo string
|
|
RepoFullName string
|
|
ProjectName string
|
|
Path string
|
|
Workspace string
|
|
}
|
|
|
|
type JobIDInfo struct {
|
|
JobID string
|
|
JobIDUrl string
|
|
JobDescription string
|
|
Time time.Time
|
|
TimeFormatted string
|
|
JobStep string
|
|
}
|
|
|
|
type PullInfoWithJobIDs struct {
|
|
Pull PullInfo
|
|
JobIDInfos []JobIDInfo
|
|
}
|
|
|
|
type JobInfo struct {
|
|
PullInfo
|
|
HeadCommit string
|
|
JobDescription string
|
|
JobStep string
|
|
}
|
|
|
|
type ProjectCmdOutputLine struct {
|
|
JobID string
|
|
JobInfo JobInfo
|
|
Line string
|
|
OperationComplete bool
|
|
}
|
|
|
|
// AsyncProjectCommandOutputHandler is a handler to transport terraform client
|
|
// outputs to the front end.
|
|
type AsyncProjectCommandOutputHandler struct {
|
|
projectCmdOutput chan *ProjectCmdOutputLine
|
|
|
|
projectOutputBuffers map[string]OutputBuffer
|
|
projectOutputBuffersLock sync.RWMutex
|
|
|
|
receiverBuffers map[string]map[chan string]bool
|
|
receiverBuffersLock sync.RWMutex
|
|
|
|
logger logging.SimpleLogging
|
|
|
|
// Tracks all the jobs for a pull request which is used for clean up after a pull request is closed.
|
|
pullToJobMapping sync.Map
|
|
}
|
|
|
|
//go:generate pegomock generate --package mocks -o mocks/mock_project_command_output_handler.go ProjectCommandOutputHandler
|
|
|
|
type ProjectCommandOutputHandler interface {
|
|
// Send will enqueue the msg and wait for Handle() to receive the message.
|
|
Send(ctx command.ProjectContext, msg string, operationComplete bool)
|
|
|
|
SendWorkflowHook(ctx models.WorkflowHookCommandContext, msg string, operationComplete bool)
|
|
|
|
// Register registers a channel and blocks until it is caught up. Callers should call this asynchronously when attempting
|
|
// to read the channel in the same goroutine
|
|
Register(jobID string, receiver chan string)
|
|
|
|
// Deregister removes a channel from successive updates and closes it.
|
|
Deregister(jobID string, receiver chan string)
|
|
|
|
IsKeyExists(key string) bool
|
|
|
|
// Listens for msg from channel
|
|
Handle()
|
|
|
|
// Cleans up resources for a pull
|
|
CleanUp(pullInfo PullInfo)
|
|
|
|
// Returns a map from Pull Requests to Jobs
|
|
GetPullToJobMapping() []PullInfoWithJobIDs
|
|
}
|
|
|
|
func NewAsyncProjectCommandOutputHandler(
|
|
projectCmdOutput chan *ProjectCmdOutputLine,
|
|
logger logging.SimpleLogging,
|
|
) ProjectCommandOutputHandler {
|
|
return &AsyncProjectCommandOutputHandler{
|
|
projectCmdOutput: projectCmdOutput,
|
|
logger: logger,
|
|
receiverBuffers: map[string]map[chan string]bool{},
|
|
projectOutputBuffers: map[string]OutputBuffer{},
|
|
pullToJobMapping: sync.Map{},
|
|
}
|
|
}
|
|
|
|
func (p *AsyncProjectCommandOutputHandler) GetPullToJobMapping() []PullInfoWithJobIDs {
|
|
|
|
pullToJobMappings := []PullInfoWithJobIDs{}
|
|
i := 0
|
|
|
|
p.pullToJobMapping.Range(func(key, value interface{}) bool {
|
|
pullInfo := key.(PullInfo)
|
|
jobIDMap := value.(map[string]JobIDInfo)
|
|
|
|
p := PullInfoWithJobIDs{
|
|
Pull: pullInfo,
|
|
JobIDInfos: make([]JobIDInfo, 0, len(jobIDMap)),
|
|
}
|
|
|
|
for _, JobIDInfo := range jobIDMap {
|
|
p.JobIDInfos = append(p.JobIDInfos, JobIDInfo)
|
|
}
|
|
|
|
pullToJobMappings = append(pullToJobMappings, p)
|
|
i++
|
|
return true
|
|
})
|
|
|
|
return pullToJobMappings
|
|
}
|
|
|
|
func (p *AsyncProjectCommandOutputHandler) IsKeyExists(key string) bool {
|
|
p.projectOutputBuffersLock.RLock()
|
|
defer p.projectOutputBuffersLock.RUnlock()
|
|
_, ok := p.projectOutputBuffers[key]
|
|
return ok
|
|
}
|
|
|
|
func (p *AsyncProjectCommandOutputHandler) Send(ctx command.ProjectContext, msg string, operationComplete bool) {
|
|
p.projectCmdOutput <- &ProjectCmdOutputLine{
|
|
JobID: ctx.JobID,
|
|
JobInfo: JobInfo{
|
|
HeadCommit: ctx.Pull.HeadCommit,
|
|
PullInfo: PullInfo{
|
|
PullNum: ctx.Pull.Num,
|
|
Repo: ctx.BaseRepo.Name,
|
|
RepoFullName: ctx.BaseRepo.FullName,
|
|
ProjectName: ctx.ProjectName,
|
|
Path: ctx.RepoRelDir,
|
|
Workspace: ctx.Workspace,
|
|
},
|
|
JobStep: ctx.CommandName.String(),
|
|
},
|
|
Line: msg,
|
|
OperationComplete: operationComplete,
|
|
}
|
|
}
|
|
|
|
func (p *AsyncProjectCommandOutputHandler) SendWorkflowHook(ctx models.WorkflowHookCommandContext, msg string, operationComplete bool) {
|
|
p.projectCmdOutput <- &ProjectCmdOutputLine{
|
|
JobID: ctx.HookID,
|
|
JobInfo: JobInfo{
|
|
HeadCommit: ctx.Pull.HeadCommit,
|
|
PullInfo: PullInfo{
|
|
PullNum: ctx.Pull.Num,
|
|
Repo: ctx.BaseRepo.Name,
|
|
RepoFullName: ctx.BaseRepo.FullName,
|
|
},
|
|
JobDescription: ctx.HookDescription,
|
|
JobStep: ctx.HookStepName,
|
|
},
|
|
Line: msg,
|
|
OperationComplete: operationComplete,
|
|
}
|
|
}
|
|
|
|
func (p *AsyncProjectCommandOutputHandler) Register(jobID string, receiver chan string) {
|
|
p.addChan(receiver, jobID)
|
|
}
|
|
|
|
func (p *AsyncProjectCommandOutputHandler) Handle() {
|
|
for msg := range p.projectCmdOutput {
|
|
if msg.OperationComplete {
|
|
p.completeJob(msg.JobID)
|
|
continue
|
|
}
|
|
|
|
// Add job to pullToJob mapping
|
|
if _, ok := p.pullToJobMapping.Load(msg.JobInfo.PullInfo); !ok {
|
|
p.pullToJobMapping.Store(msg.JobInfo.PullInfo, map[string]JobIDInfo{})
|
|
}
|
|
value, _ := p.pullToJobMapping.Load(msg.JobInfo.PullInfo)
|
|
jobMapping := value.(map[string]JobIDInfo)
|
|
jobMapping[msg.JobID] = JobIDInfo{
|
|
JobID: msg.JobID,
|
|
JobDescription: msg.JobInfo.JobDescription,
|
|
Time: time.Now(),
|
|
JobStep: msg.JobInfo.JobStep,
|
|
}
|
|
|
|
// Forward new message to all receiver channels and output buffer
|
|
p.writeLogLine(msg.JobID, msg.Line)
|
|
}
|
|
}
|
|
|
|
func (p *AsyncProjectCommandOutputHandler) completeJob(jobID string) {
|
|
p.projectOutputBuffersLock.Lock()
|
|
p.receiverBuffersLock.Lock()
|
|
defer func() {
|
|
p.projectOutputBuffersLock.Unlock()
|
|
p.receiverBuffersLock.Unlock()
|
|
}()
|
|
|
|
// Update operation status to complete
|
|
if outputBuffer, ok := p.projectOutputBuffers[jobID]; ok {
|
|
outputBuffer.OperationComplete = true
|
|
p.projectOutputBuffers[jobID] = outputBuffer
|
|
}
|
|
|
|
// Close active receiver channels
|
|
if openChannels, ok := p.receiverBuffers[jobID]; ok {
|
|
for ch := range openChannels {
|
|
close(ch)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func (p *AsyncProjectCommandOutputHandler) addChan(ch chan string, jobID string) {
|
|
p.projectOutputBuffersLock.RLock()
|
|
outputBuffer := p.projectOutputBuffers[jobID]
|
|
p.projectOutputBuffersLock.RUnlock()
|
|
|
|
for _, line := range outputBuffer.Buffer {
|
|
ch <- line
|
|
}
|
|
|
|
// No need register receiver since all the logs have been streamed
|
|
if outputBuffer.OperationComplete {
|
|
close(ch)
|
|
return
|
|
}
|
|
|
|
// add the channel to our registry after we backfill the contents of the buffer,
|
|
// to prevent new messages coming in interleaving with this backfill.
|
|
p.receiverBuffersLock.Lock()
|
|
if p.receiverBuffers[jobID] == nil {
|
|
p.receiverBuffers[jobID] = map[chan string]bool{}
|
|
}
|
|
p.receiverBuffers[jobID][ch] = true
|
|
p.receiverBuffersLock.Unlock()
|
|
}
|
|
|
|
// Add log line to buffer and send to all current channels
|
|
func (p *AsyncProjectCommandOutputHandler) writeLogLine(jobID string, line string) {
|
|
p.receiverBuffersLock.Lock()
|
|
for ch := range p.receiverBuffers[jobID] {
|
|
select {
|
|
case ch <- line:
|
|
default:
|
|
// Delete buffered channel if it's blocking.
|
|
delete(p.receiverBuffers[jobID], ch)
|
|
}
|
|
}
|
|
p.receiverBuffersLock.Unlock()
|
|
|
|
p.projectOutputBuffersLock.Lock()
|
|
if _, ok := p.projectOutputBuffers[jobID]; !ok {
|
|
p.projectOutputBuffers[jobID] = OutputBuffer{
|
|
Buffer: []string{},
|
|
}
|
|
}
|
|
outputBuffer := p.projectOutputBuffers[jobID]
|
|
outputBuffer.Buffer = append(outputBuffer.Buffer, line)
|
|
p.projectOutputBuffers[jobID] = outputBuffer
|
|
|
|
p.projectOutputBuffersLock.Unlock()
|
|
}
|
|
|
|
// Remove channel, so client no longer receives Terraform output
|
|
func (p *AsyncProjectCommandOutputHandler) Deregister(jobID string, ch chan string) {
|
|
p.logger.Debug("Removing channel for %s", jobID)
|
|
p.receiverBuffersLock.Lock()
|
|
delete(p.receiverBuffers[jobID], ch)
|
|
p.receiverBuffersLock.Unlock()
|
|
}
|
|
|
|
func (p *AsyncProjectCommandOutputHandler) GetReceiverBufferForPull(jobID string) map[chan string]bool {
|
|
return p.receiverBuffers[jobID]
|
|
}
|
|
|
|
func (p *AsyncProjectCommandOutputHandler) GetProjectOutputBuffer(jobID string) OutputBuffer {
|
|
return p.projectOutputBuffers[jobID]
|
|
}
|
|
|
|
func (p *AsyncProjectCommandOutputHandler) GetJobIDMapForPull(pullInfo PullInfo) map[string]JobIDInfo {
|
|
if value, ok := p.pullToJobMapping.Load(pullInfo); ok {
|
|
return value.(map[string]JobIDInfo)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *AsyncProjectCommandOutputHandler) CleanUp(pullInfo PullInfo) {
|
|
if value, ok := p.pullToJobMapping.Load(pullInfo); ok {
|
|
jobMapping := value.(map[string]JobIDInfo)
|
|
for jobID := range jobMapping {
|
|
p.projectOutputBuffersLock.Lock()
|
|
delete(p.projectOutputBuffers, jobID)
|
|
p.projectOutputBuffersLock.Unlock()
|
|
|
|
p.receiverBuffersLock.Lock()
|
|
delete(p.receiverBuffers, jobID)
|
|
p.receiverBuffersLock.Unlock()
|
|
}
|
|
|
|
// Remove job mapping
|
|
p.pullToJobMapping.Delete(pullInfo)
|
|
}
|
|
}
|
|
|
|
// NoopProjectOutputHandler is a mock that doesn't do anything
|
|
type NoopProjectOutputHandler struct{}
|
|
|
|
func (p *NoopProjectOutputHandler) Send(_ command.ProjectContext, _ string, _ bool) {
|
|
}
|
|
|
|
func (p *NoopProjectOutputHandler) SendWorkflowHook(_ models.WorkflowHookCommandContext, _ string, _ bool) {
|
|
}
|
|
|
|
func (p *NoopProjectOutputHandler) Register(_ string, _ chan string) {}
|
|
|
|
func (p *NoopProjectOutputHandler) Deregister(_ string, _ chan string) {}
|
|
|
|
func (p *NoopProjectOutputHandler) Handle() {
|
|
}
|
|
|
|
func (p *NoopProjectOutputHandler) CleanUp(_ PullInfo) {
|
|
}
|
|
|
|
func (p *NoopProjectOutputHandler) IsKeyExists(_ string) bool {
|
|
return false
|
|
}
|
|
|
|
func (p *NoopProjectOutputHandler) GetPullToJobMapping() []PullInfoWithJobIDs {
|
|
return []PullInfoWithJobIDs{}
|
|
}
|