Merge pull request #5019 from taylorsilva/wall-3636

Add API endpoints for broadcasting a message to a concourse cluster
This commit is contained in:
Taylor Silva 2020-01-21 12:12:13 -05:00 committed by GitHub
commit 602b60c7fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 991 additions and 10 deletions

View File

@ -6,7 +6,6 @@ import (
"net/http/httptest"
"sync"
"testing"
"time"
"code.cloudfoundry.org/lager"
"code.cloudfoundry.org/lager/lagertest"
@ -52,12 +51,12 @@ var (
dbUserFactory *dbfakes.FakeUserFactory
dbCheckFactory *dbfakes.FakeCheckFactory
dbTeam *dbfakes.FakeTeam
dbWall *dbfakes.FakeWall
fakeSecretManager *credsfakes.FakeSecrets
fakeVarSourcePool *credsfakes.FakeVarSourcePool
credsManagers creds.Managers
interceptTimeoutFactory *containerserverfakes.FakeInterceptTimeoutFactory
interceptTimeout *containerserverfakes.FakeInterceptTimeout
expire time.Duration
isTLSEnabled bool
cliDownloadsDir string
logger *lagertest.TestLogger
@ -97,6 +96,7 @@ var _ = BeforeEach(func() {
dbBuildFactory = new(dbfakes.FakeBuildFactory)
dbUserFactory = new(dbfakes.FakeUserFactory)
dbCheckFactory = new(dbfakes.FakeCheckFactory)
dbWall = new(dbfakes.FakeWall)
interceptTimeoutFactory = new(containerserverfakes.FakeInterceptTimeoutFactory)
interceptTimeout = new(containerserverfakes.FakeInterceptTimeout)
@ -137,8 +137,6 @@ var _ = BeforeEach(func() {
sink = lager.NewReconfigurableSink(lager.NewPrettySink(GinkgoWriter, lager.DEBUG), lager.DEBUG)
expire = 24 * time.Hour
isTLSEnabled = false
build = new(dbfakes.FakeBuild)
@ -192,6 +190,7 @@ var _ = BeforeEach(func() {
fakeVarSourcePool,
credsManagers,
interceptTimeoutFactory,
dbWall,
)
Expect(err).NotTo(HaveOccurred())

View File

@ -4,8 +4,6 @@ import (
"net/http"
"path/filepath"
"github.com/concourse/concourse/atc/api/usersserver"
"code.cloudfoundry.org/lager"
"github.com/concourse/concourse/atc"
"github.com/concourse/concourse/atc/api/artifactserver"
@ -22,7 +20,9 @@ import (
"github.com/concourse/concourse/atc/api/resourceserver"
"github.com/concourse/concourse/atc/api/resourceserver/versionserver"
"github.com/concourse/concourse/atc/api/teamserver"
"github.com/concourse/concourse/atc/api/usersserver"
"github.com/concourse/concourse/atc/api/volumeserver"
"github.com/concourse/concourse/atc/api/wallserver"
"github.com/concourse/concourse/atc/api/workerserver"
"github.com/concourse/concourse/atc/creds"
"github.com/concourse/concourse/atc/db"
@ -69,6 +69,7 @@ func NewHandler(
varSourcePool creds.VarSourcePool,
credsManagers creds.Managers,
interceptTimeoutFactory containerserver.InterceptTimeoutFactory,
dbWall db.Wall,
) (http.Handler, error) {
absCLIDownloadsDir, err := filepath.Abs(cliDownloadsDir)
@ -98,6 +99,7 @@ func NewHandler(
infoServer := infoserver.NewServer(logger, version, workerVersion, externalURL, clusterName, credsManagers)
artifactServer := artifactserver.NewServer(logger, workerClient)
usersServer := usersserver.NewServer(logger, dbUserFactory)
wallServer := wallserver.NewServer(dbWall, logger)
handlers := map[string]http.Handler{
atc.GetConfig: http.HandlerFunc(configServer.GetConfig),
@ -204,6 +206,10 @@ func NewHandler(
atc.CreateArtifact: teamHandlerFactory.HandlerFor(artifactServer.CreateArtifact),
atc.GetArtifact: teamHandlerFactory.HandlerFor(artifactServer.GetArtifact),
atc.GetWall: http.HandlerFunc(wallServer.GetWall),
atc.SetWall: http.HandlerFunc(wallServer.SetWall),
atc.ClearWall: http.HandlerFunc(wallServer.ClearWall),
}
return rata.NewRouter(atc.Routes, wrapper.Wrap(handlers))

View File

@ -14,7 +14,8 @@ func (s *Server) Info(w http.ResponseWriter, r *http.Request) {
err := json.NewEncoder(w).Encode(atc.Info{Version: s.version,
WorkerVersion: s.workerVersion,
ExternalURL: s.externalURL,
ClusterName: s.clusterName})
ClusterName: s.clusterName,
})
if err != nil {
logger.Error("failed-to-encode-info", err)
w.WriteHeader(http.StatusInternalServerError)

185
atc/api/wall_test.go Normal file
View File

@ -0,0 +1,185 @@
package api_test
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"time"
"github.com/concourse/concourse/atc"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Wall API", func() {
var response *http.Response
Context("Gets a wall message", func() {
BeforeEach(func() {
dbWall.GetWallReturns(atc.Wall{Message: "test message"}, nil)
})
JustBeforeEach(func() {
req, err := http.NewRequest("GET", server.URL+"/api/v1/wall", nil)
Expect(err).NotTo(HaveOccurred())
response, err = client.Do(req)
Expect(err).NotTo(HaveOccurred())
})
It("returns 200", func() {
Expect(response.StatusCode).To(Equal(http.StatusOK))
})
It("returns Content-Type 'application/json'", func() {
Expect(response.Header.Get("Content-Type")).To(Equal("application/json"))
})
Context("the message does not expire", func() {
It("returns only message", func() {
Expect(dbWall.GetWallCallCount()).To(Equal(1))
Expect(ioutil.ReadAll(response.Body)).To(MatchJSON(`{"message":"test message"}`))
})
})
Context("and the message does expire", func() {
var (
expectedDuration time.Duration
)
BeforeEach(func() {
expiresAt := time.Now().Add(time.Minute)
expectedDuration = time.Until(expiresAt)
dbWall.GetWallReturns(atc.Wall{Message: "test message", TTL: expectedDuration}, nil)
})
It("returns the expiration with the message", func() {
Expect(dbWall.GetWallCallCount()).To(Equal(1))
var msg atc.Wall
err := json.NewDecoder(response.Body).Decode(&msg)
Expect(err).ToNot(HaveOccurred())
Expect(msg).To(Equal(atc.Wall{
Message: "test message",
TTL: expectedDuration,
}))
})
})
})
Context("Sets a wall message", func() {
var expectedWall atc.Wall
BeforeEach(func() {
expectedWall = atc.Wall{
Message: "test message",
TTL: time.Minute,
}
dbWall.SetWallReturns(nil)
})
JustBeforeEach(func() {
payload, err := json.Marshal(expectedWall)
Expect(err).NotTo(HaveOccurred())
req, err := http.NewRequest("PUT", server.URL+"/api/v1/wall",
ioutil.NopCloser(bytes.NewBuffer(payload)))
Expect(err).NotTo(HaveOccurred())
response, err = client.Do(req)
Expect(err).NotTo(HaveOccurred())
})
Context("when authenticated", func() {
BeforeEach(func() {
fakeAccess.IsAuthenticatedReturns(true)
})
Context("and is admin", func() {
BeforeEach(func() {
fakeAccess.IsAdminReturns(true)
})
It("returns 200", func() {
Expect(response.StatusCode).To(Equal(http.StatusOK))
})
It("sets the message and expiration", func() {
Expect(dbWall.SetWallCallCount()).To(Equal(1))
Expect(dbWall.SetWallArgsForCall(0)).To(Equal(expectedWall))
})
})
Context("and is not admin", func() {
BeforeEach(func() {
fakeAccess.IsAdminReturns(false)
})
It("returns 403", func() {
Expect(response.StatusCode).To(Equal(http.StatusForbidden))
})
})
})
Context("when not authenticated", func() {
BeforeEach(func() {
fakeAccess.IsAuthenticatedReturns(false)
})
It("returns 401", func() {
Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
})
})
})
Context("Clears the wall message", func() {
JustBeforeEach(func() {
req, err := http.NewRequest("DELETE", server.URL+"/api/v1/wall", nil)
Expect(err).NotTo(HaveOccurred())
response, err = client.Do(req)
Expect(err).NotTo(HaveOccurred())
})
Context("when authenticated", func() {
BeforeEach(func() {
fakeAccess.IsAuthenticatedReturns(true)
})
Context("is an admin", func() {
BeforeEach(func() {
fakeAccess.IsAdminReturns(true)
})
It("returns 200", func() {
Expect(response.StatusCode).To(Equal(http.StatusOK))
})
It("makes the Clear database call", func() {
Expect(dbWall.ClearCallCount()).To(Equal(1))
})
})
Context("is not an admin", func() {
BeforeEach(func() {
fakeAccess.IsAdminReturns(false)
})
It("returns 403", func() {
Expect(response.StatusCode).To(Equal(http.StatusForbidden))
})
})
})
Context("when not authenticated", func() {
BeforeEach(func() {
fakeAccess.IsAuthenticatedReturns(false)
})
It("returns 401", func() {
Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
})
})
})
})

View File

@ -0,0 +1,18 @@
package wallserver
import (
"code.cloudfoundry.org/lager"
"github.com/concourse/concourse/atc/db"
)
type Server struct {
wall db.Wall
logger lager.Logger
}
func NewServer(wall db.Wall, logger lager.Logger) *Server {
return &Server{
wall: wall,
logger: logger,
}
}

View File

@ -0,0 +1,56 @@
package wallserver
import (
"encoding/json"
"net/http"
"github.com/concourse/concourse/atc"
)
func (s *Server) GetWall(w http.ResponseWriter, r *http.Request) {
logger := s.logger.Session("wall")
w.Header().Set("Content-Type", "application/json")
wall, err := s.wall.GetWall()
if err != nil {
logger.Error("failed-to-get-message", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
err = json.NewEncoder(w).Encode(wall)
if err != nil {
logger.Error("failed-to-encode-json", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (s *Server) SetWall(w http.ResponseWriter, r *http.Request) {
logger := s.logger.Session("wall")
var wall atc.Wall
err := json.NewDecoder(r.Body).Decode(&wall)
if err != nil {
logger.Error("failed-to-decode-json", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
err = s.wall.SetWall(wall)
if err != nil {
logger.Error("failed-to-set-wall-message", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (s *Server) ClearWall(w http.ResponseWriter, r *http.Request) {
logger := s.logger.Session("wall")
err := s.wall.Clear()
if err != nil {
logger.Error("failed-to-clear-the-wall", err)
w.WriteHeader(http.StatusInternalServerError)
}
}

View File

@ -615,6 +615,8 @@ func (cmd *RunCommand) constructAPIMembers(
gcContainerDestroyer := gc.NewDestroyer(logger, dbContainerRepository, dbVolumeRepository)
dbBuildFactory := db.NewBuildFactory(dbConn, lockFactory, cmd.GC.OneOffBuildGracePeriod)
dbCheckFactory := db.NewCheckFactory(dbConn, lockFactory, secretManager, cmd.varSourcePool, cmd.GlobalResourceCheckTimeout)
dbClock := db.NewClock()
dbWall := db.NewWall(dbConn, &dbClock)
accessFactory := accessor.NewAccessFactory(authHandler.PublicKey())
customActionRoleMap := accessor.CustomActionRoleMap{}
@ -646,6 +648,7 @@ func (cmd *RunCommand) constructAPIMembers(
secretManager,
credsManagers,
accessFactory,
dbWall,
)
if err != nil {
@ -1537,6 +1540,7 @@ func (cmd *RunCommand) constructAPIHandler(
secretManager creds.Secrets,
credsManagers creds.Managers,
accessFactory accessor.AccessFactory,
dbWall db.Wall,
) (http.Handler, error) {
checkPipelineAccessHandlerFactory := auth.NewCheckPipelineAccessHandlerFactory(teamFactory)
@ -1603,6 +1607,7 @@ func (cmd *RunCommand) constructAPIHandler(
cmd.varSourcePool,
credsManagers,
containerserver.NewInterceptTimeoutFactory(cmd.InterceptIdleTimeout),
dbWall,
)
}

View File

@ -129,7 +129,10 @@ func (a *auditor) ValidateAction(action string) bool {
atc.DownloadCLI,
atc.GetInfo,
atc.GetInfoCreds,
atc.ListActiveUsersSince:
atc.ListActiveUsersSince,
atc.GetWall,
atc.SetWall,
atc.ClearWall:
return a.EnableSystemAuditLog
case atc.ListTeams,
atc.SetTeam,

24
atc/db/clock.go Normal file
View File

@ -0,0 +1,24 @@
package db
import "time"
//go:generate counterfeiter . Clock
type Clock interface {
Now() time.Time
Until(time.Time) time.Duration
}
type clock struct{}
func NewClock() clock {
return clock{}
}
func (c *clock) Now() time.Time {
return time.Now()
}
func (c *clock) Until(t time.Time) time.Duration {
return time.Until(t)
}

View File

@ -14,6 +14,7 @@ import (
"github.com/concourse/concourse/atc"
"github.com/concourse/concourse/atc/creds/credsfakes"
"github.com/concourse/concourse/atc/db"
"github.com/concourse/concourse/atc/db/dbfakes"
"github.com/concourse/concourse/atc/db/lock"
"github.com/concourse/concourse/atc/metric"
"github.com/concourse/concourse/atc/postgresrunner"
@ -47,6 +48,8 @@ var (
workerBaseResourceTypeFactory db.WorkerBaseResourceTypeFactory
workerTaskCacheFactory db.WorkerTaskCacheFactory
userFactory db.UserFactory
dbWall db.Wall
fakeClock dbfakes.FakeClock
defaultWorkerResourceType atc.WorkerResourceType
defaultTeam db.Team
@ -116,6 +119,7 @@ var _ = BeforeEach(func() {
workerBaseResourceTypeFactory = db.NewWorkerBaseResourceTypeFactory(dbConn)
workerTaskCacheFactory = db.NewWorkerTaskCacheFactory(dbConn)
userFactory = db.NewUserFactory(dbConn)
dbWall = db.NewWall(dbConn, &fakeClock)
var err error
defaultTeam, err = teamFactory.CreateTeam(atc.Team{Name: "default-team"})

View File

@ -0,0 +1,175 @@
// Code generated by counterfeiter. DO NOT EDIT.
package dbfakes
import (
"sync"
"time"
"github.com/concourse/concourse/atc/db"
)
type FakeClock struct {
NowStub func() time.Time
nowMutex sync.RWMutex
nowArgsForCall []struct {
}
nowReturns struct {
result1 time.Time
}
nowReturnsOnCall map[int]struct {
result1 time.Time
}
UntilStub func(time.Time) time.Duration
untilMutex sync.RWMutex
untilArgsForCall []struct {
arg1 time.Time
}
untilReturns struct {
result1 time.Duration
}
untilReturnsOnCall map[int]struct {
result1 time.Duration
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeClock) Now() time.Time {
fake.nowMutex.Lock()
ret, specificReturn := fake.nowReturnsOnCall[len(fake.nowArgsForCall)]
fake.nowArgsForCall = append(fake.nowArgsForCall, struct {
}{})
fake.recordInvocation("Now", []interface{}{})
fake.nowMutex.Unlock()
if fake.NowStub != nil {
return fake.NowStub()
}
if specificReturn {
return ret.result1
}
fakeReturns := fake.nowReturns
return fakeReturns.result1
}
func (fake *FakeClock) NowCallCount() int {
fake.nowMutex.RLock()
defer fake.nowMutex.RUnlock()
return len(fake.nowArgsForCall)
}
func (fake *FakeClock) NowCalls(stub func() time.Time) {
fake.nowMutex.Lock()
defer fake.nowMutex.Unlock()
fake.NowStub = stub
}
func (fake *FakeClock) NowReturns(result1 time.Time) {
fake.nowMutex.Lock()
defer fake.nowMutex.Unlock()
fake.NowStub = nil
fake.nowReturns = struct {
result1 time.Time
}{result1}
}
func (fake *FakeClock) NowReturnsOnCall(i int, result1 time.Time) {
fake.nowMutex.Lock()
defer fake.nowMutex.Unlock()
fake.NowStub = nil
if fake.nowReturnsOnCall == nil {
fake.nowReturnsOnCall = make(map[int]struct {
result1 time.Time
})
}
fake.nowReturnsOnCall[i] = struct {
result1 time.Time
}{result1}
}
func (fake *FakeClock) Until(arg1 time.Time) time.Duration {
fake.untilMutex.Lock()
ret, specificReturn := fake.untilReturnsOnCall[len(fake.untilArgsForCall)]
fake.untilArgsForCall = append(fake.untilArgsForCall, struct {
arg1 time.Time
}{arg1})
fake.recordInvocation("Until", []interface{}{arg1})
fake.untilMutex.Unlock()
if fake.UntilStub != nil {
return fake.UntilStub(arg1)
}
if specificReturn {
return ret.result1
}
fakeReturns := fake.untilReturns
return fakeReturns.result1
}
func (fake *FakeClock) UntilCallCount() int {
fake.untilMutex.RLock()
defer fake.untilMutex.RUnlock()
return len(fake.untilArgsForCall)
}
func (fake *FakeClock) UntilCalls(stub func(time.Time) time.Duration) {
fake.untilMutex.Lock()
defer fake.untilMutex.Unlock()
fake.UntilStub = stub
}
func (fake *FakeClock) UntilArgsForCall(i int) time.Time {
fake.untilMutex.RLock()
defer fake.untilMutex.RUnlock()
argsForCall := fake.untilArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeClock) UntilReturns(result1 time.Duration) {
fake.untilMutex.Lock()
defer fake.untilMutex.Unlock()
fake.UntilStub = nil
fake.untilReturns = struct {
result1 time.Duration
}{result1}
}
func (fake *FakeClock) UntilReturnsOnCall(i int, result1 time.Duration) {
fake.untilMutex.Lock()
defer fake.untilMutex.Unlock()
fake.UntilStub = nil
if fake.untilReturnsOnCall == nil {
fake.untilReturnsOnCall = make(map[int]struct {
result1 time.Duration
})
}
fake.untilReturnsOnCall[i] = struct {
result1 time.Duration
}{result1}
}
func (fake *FakeClock) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.nowMutex.RLock()
defer fake.nowMutex.RUnlock()
fake.untilMutex.RLock()
defer fake.untilMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeClock) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ db.Clock = new(FakeClock)

244
atc/db/dbfakes/fake_wall.go Normal file
View File

@ -0,0 +1,244 @@
// Code generated by counterfeiter. DO NOT EDIT.
package dbfakes
import (
"sync"
"github.com/concourse/concourse/atc"
"github.com/concourse/concourse/atc/db"
)
type FakeWall struct {
ClearStub func() error
clearMutex sync.RWMutex
clearArgsForCall []struct {
}
clearReturns struct {
result1 error
}
clearReturnsOnCall map[int]struct {
result1 error
}
GetWallStub func() (atc.Wall, error)
getWallMutex sync.RWMutex
getWallArgsForCall []struct {
}
getWallReturns struct {
result1 atc.Wall
result2 error
}
getWallReturnsOnCall map[int]struct {
result1 atc.Wall
result2 error
}
SetWallStub func(atc.Wall) error
setWallMutex sync.RWMutex
setWallArgsForCall []struct {
arg1 atc.Wall
}
setWallReturns struct {
result1 error
}
setWallReturnsOnCall map[int]struct {
result1 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeWall) Clear() error {
fake.clearMutex.Lock()
ret, specificReturn := fake.clearReturnsOnCall[len(fake.clearArgsForCall)]
fake.clearArgsForCall = append(fake.clearArgsForCall, struct {
}{})
fake.recordInvocation("Clear", []interface{}{})
fake.clearMutex.Unlock()
if fake.ClearStub != nil {
return fake.ClearStub()
}
if specificReturn {
return ret.result1
}
fakeReturns := fake.clearReturns
return fakeReturns.result1
}
func (fake *FakeWall) ClearCallCount() int {
fake.clearMutex.RLock()
defer fake.clearMutex.RUnlock()
return len(fake.clearArgsForCall)
}
func (fake *FakeWall) ClearCalls(stub func() error) {
fake.clearMutex.Lock()
defer fake.clearMutex.Unlock()
fake.ClearStub = stub
}
func (fake *FakeWall) ClearReturns(result1 error) {
fake.clearMutex.Lock()
defer fake.clearMutex.Unlock()
fake.ClearStub = nil
fake.clearReturns = struct {
result1 error
}{result1}
}
func (fake *FakeWall) ClearReturnsOnCall(i int, result1 error) {
fake.clearMutex.Lock()
defer fake.clearMutex.Unlock()
fake.ClearStub = nil
if fake.clearReturnsOnCall == nil {
fake.clearReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.clearReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeWall) GetWall() (atc.Wall, error) {
fake.getWallMutex.Lock()
ret, specificReturn := fake.getWallReturnsOnCall[len(fake.getWallArgsForCall)]
fake.getWallArgsForCall = append(fake.getWallArgsForCall, struct {
}{})
fake.recordInvocation("GetWall", []interface{}{})
fake.getWallMutex.Unlock()
if fake.GetWallStub != nil {
return fake.GetWallStub()
}
if specificReturn {
return ret.result1, ret.result2
}
fakeReturns := fake.getWallReturns
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeWall) GetWallCallCount() int {
fake.getWallMutex.RLock()
defer fake.getWallMutex.RUnlock()
return len(fake.getWallArgsForCall)
}
func (fake *FakeWall) GetWallCalls(stub func() (atc.Wall, error)) {
fake.getWallMutex.Lock()
defer fake.getWallMutex.Unlock()
fake.GetWallStub = stub
}
func (fake *FakeWall) GetWallReturns(result1 atc.Wall, result2 error) {
fake.getWallMutex.Lock()
defer fake.getWallMutex.Unlock()
fake.GetWallStub = nil
fake.getWallReturns = struct {
result1 atc.Wall
result2 error
}{result1, result2}
}
func (fake *FakeWall) GetWallReturnsOnCall(i int, result1 atc.Wall, result2 error) {
fake.getWallMutex.Lock()
defer fake.getWallMutex.Unlock()
fake.GetWallStub = nil
if fake.getWallReturnsOnCall == nil {
fake.getWallReturnsOnCall = make(map[int]struct {
result1 atc.Wall
result2 error
})
}
fake.getWallReturnsOnCall[i] = struct {
result1 atc.Wall
result2 error
}{result1, result2}
}
func (fake *FakeWall) SetWall(arg1 atc.Wall) error {
fake.setWallMutex.Lock()
ret, specificReturn := fake.setWallReturnsOnCall[len(fake.setWallArgsForCall)]
fake.setWallArgsForCall = append(fake.setWallArgsForCall, struct {
arg1 atc.Wall
}{arg1})
fake.recordInvocation("SetWall", []interface{}{arg1})
fake.setWallMutex.Unlock()
if fake.SetWallStub != nil {
return fake.SetWallStub(arg1)
}
if specificReturn {
return ret.result1
}
fakeReturns := fake.setWallReturns
return fakeReturns.result1
}
func (fake *FakeWall) SetWallCallCount() int {
fake.setWallMutex.RLock()
defer fake.setWallMutex.RUnlock()
return len(fake.setWallArgsForCall)
}
func (fake *FakeWall) SetWallCalls(stub func(atc.Wall) error) {
fake.setWallMutex.Lock()
defer fake.setWallMutex.Unlock()
fake.SetWallStub = stub
}
func (fake *FakeWall) SetWallArgsForCall(i int) atc.Wall {
fake.setWallMutex.RLock()
defer fake.setWallMutex.RUnlock()
argsForCall := fake.setWallArgsForCall[i]
return argsForCall.arg1
}
func (fake *FakeWall) SetWallReturns(result1 error) {
fake.setWallMutex.Lock()
defer fake.setWallMutex.Unlock()
fake.SetWallStub = nil
fake.setWallReturns = struct {
result1 error
}{result1}
}
func (fake *FakeWall) SetWallReturnsOnCall(i int, result1 error) {
fake.setWallMutex.Lock()
defer fake.setWallMutex.Unlock()
fake.SetWallStub = nil
if fake.setWallReturnsOnCall == nil {
fake.setWallReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.setWallReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeWall) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.clearMutex.RLock()
defer fake.clearMutex.RUnlock()
fake.getWallMutex.RLock()
defer fake.getWallMutex.RUnlock()
fake.setWallMutex.RLock()
defer fake.setWallMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeWall) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ db.Wall = new(FakeWall)

View File

@ -0,0 +1,5 @@
BEGIN;
DROP TABLE IF EXISTS wall;
COMMIT;

View File

@ -0,0 +1,8 @@
BEGIN;
CREATE TABLE wall (
message text NOT NULL,
expires_at timestamp with time zone
);
COMMIT;

107
atc/db/wall.go Normal file
View File

@ -0,0 +1,107 @@
package db
import (
"database/sql"
"github.com/concourse/concourse/atc"
sq "github.com/Masterminds/squirrel"
)
//go:generate counterfeiter . Wall
type Wall interface {
SetWall(atc.Wall) error
GetWall() (atc.Wall, error)
Clear() error
}
type wall struct {
conn Conn
clock Clock
}
func NewWall(conn Conn, clock Clock) Wall {
return &wall{
conn: conn,
clock: clock,
}
}
func (w wall) SetWall(wall atc.Wall) error {
tx, err := w.conn.Begin()
if err != nil {
return err
}
defer Rollback(tx)
_, err = psql.Delete("wall").RunWith(tx).Exec()
if err != nil {
return err
}
query := psql.Insert("wall").
Columns("message")
if wall.TTL != 0 {
expiresAt := w.clock.Now().Add(wall.TTL)
query = query.Columns("expires_at").Values(wall.Message, expiresAt)
} else {
query = query.Values(wall.Message)
}
_, err = query.RunWith(tx).Exec()
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (w wall) GetWall() (atc.Wall, error) {
var wall atc.Wall
row := psql.Select("message", "expires_at").
From("wall").
Where(sq.Or{
sq.Gt{"expires_at": w.clock.Now()},
sq.Eq{"expires_at": nil},
}).
RunWith(w.conn).QueryRow()
err := w.scanWall(&wall, row)
if err != nil && err != sql.ErrNoRows {
return atc.Wall{}, err
}
return wall, nil
}
func (w *wall) scanWall(wall *atc.Wall, scan scannable) error {
var expiresAt sql.NullTime
err := scan.Scan(&wall.Message, &expiresAt)
if err != nil {
return err
}
if expiresAt.Valid {
wall.TTL = w.clock.Until(expiresAt.Time)
}
return nil
}
func (w wall) Clear() error {
_, err := psql.Delete("wall").RunWith(w.conn).Exec()
if err != nil {
return err
}
return nil
}

119
atc/db/wall_test.go Normal file
View File

@ -0,0 +1,119 @@
package db_test
import (
"time"
"github.com/concourse/concourse/atc"
"github.com/concourse/concourse/atc/db/dbfakes"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Wall", func() {
var (
msgOnly = atc.Wall{Message: "this is a test message!"}
msgWithTTL = atc.Wall{Message: "this is a test message!", TTL: time.Minute}
startTime = time.Now()
)
Context(" a message is set", func() {
BeforeEach(func() {
fakeClock = dbfakes.FakeClock{}
fakeClock.NowReturns(startTime)
})
Context("with no expiration", func() {
It("successfully sets the wall", func() {
err := dbWall.SetWall(msgOnly)
Expect(err).ToNot(HaveOccurred())
Expect(fakeClock.NowCallCount()).To(Equal(0))
})
It("successfully gets the wall", func() {
_ = dbWall.SetWall(msgOnly)
actualWall, err := dbWall.GetWall()
Expect(err).ToNot(HaveOccurred())
Expect(fakeClock.NowCallCount()).To(Equal(1))
Expect(actualWall).To(Equal(msgOnly))
})
})
Context("with an expiration", func() {
It("successfully sets the wall", func() {
err := dbWall.SetWall(msgWithTTL)
Expect(err).ToNot(HaveOccurred())
Expect(fakeClock.NowCallCount()).To(Equal(1))
})
Context("the message has not expired", func() {
Context("and gets a wall", func() {
BeforeEach(func() {
fakeClock.NowReturns(startTime.Add(time.Second))
fakeClock.UntilReturns(30 * time.Second)
})
Specify("successfully", func() {
_ = dbWall.SetWall(msgWithTTL)
_, err := dbWall.GetWall()
Expect(err).ToNot(HaveOccurred())
Expect(fakeClock.NowCallCount()).To(Equal(2))
Expect(fakeClock.UntilCallCount()).To(Equal(1))
})
Specify("with the TTL field set", func() {
_ = dbWall.SetWall(msgWithTTL)
actualWall, _ := dbWall.GetWall()
msgWithTTL.TTL = 30 * time.Second
Expect(actualWall).To(Equal(msgWithTTL))
})
})
})
Context("the message has expired", func() {
It("returns no message", func() {
_ = dbWall.SetWall(msgWithTTL)
fakeClock.NowReturns(startTime.Add(time.Hour))
actualWall, err := dbWall.GetWall()
Expect(err).ToNot(HaveOccurred())
Expect(fakeClock.NowCallCount()).To(Equal(2))
Expect(actualWall).To(Equal(atc.Wall{}))
})
})
})
})
Context("multiple messages are set", func() {
It("returns the last message that was set", func() {
expectedWall := atc.Wall{Message: "test 3"}
dbWall.SetWall(atc.Wall{Message: "test 1"})
dbWall.SetWall(atc.Wall{Message: "test 2"})
dbWall.SetWall(expectedWall)
actualWall, err := dbWall.GetWall()
Expect(err).ToNot(HaveOccurred())
Expect(actualWall).To(Equal(expectedWall))
})
})
Context("clearing the wall", func() {
BeforeEach(func() {
dbWall.SetWall(msgOnly)
actualWall, err := dbWall.GetWall()
Expect(err).ToNot(HaveOccurred())
Expect(actualWall).To(Equal(msgOnly), "ensure the message has been set before proceeding")
})
It("returns no error", func() {
err := dbWall.Clear()
Expect(err).ToNot(HaveOccurred())
})
It("GetWall returns no message after clearing the wall", func() {
_ = dbWall.Clear()
actualWall, err := dbWall.GetWall()
Expect(err).ToNot(HaveOccurred())
Expect(actualWall).To(Equal(atc.Wall{}))
})
})
})

View File

@ -104,6 +104,10 @@ const (
ListBuildArtifacts = "ListBuildArtifacts"
ListActiveUsersSince = "ListActiveUsersSince"
SetWall = "SetWall"
GetWall = "GetWall"
ClearWall = "ClearWall"
)
const (
@ -214,4 +218,8 @@ var Routes = rata.Routes([]rata.Route{
{Path: "/api/v1/teams/:team_name/artifacts", Method: "POST", Name: CreateArtifact},
{Path: "/api/v1/teams/:team_name/artifacts/:artifact_id", Method: "GET", Name: GetArtifact},
{Path: "/api/v1/wall", Method: "GET", Name: GetWall},
{Path: "/api/v1/wall", Method: "PUT", Name: SetWall},
{Path: "/api/v1/wall", Method: "DELETE", Name: ClearWall},
})

8
atc/wall.go Normal file
View File

@ -0,0 +1,8 @@
package atc
import "time"
type Wall struct {
Message string `json:"message,omitempty"`
TTL time.Duration `json:"TTL,omitempty"`
}

View File

@ -109,13 +109,16 @@ func (wrappa *APIAuthWrappa) Wrap(handlers rata.Handlers) rata.Handlers {
atc.ListAllJobs,
atc.ListAllResources,
atc.ListBuilds,
atc.MainJobBadge:
atc.MainJobBadge,
atc.GetWall:
newHandler = auth.CheckAuthenticationIfProvidedHandler(handler, rejector)
case atc.GetLogLevel,
atc.ListActiveUsersSince,
atc.SetLogLevel,
atc.GetInfoCreds:
atc.GetInfoCreds,
atc.SetWall,
atc.ClearWall:
newHandler = auth.CheckAdminHandler(handler, rejector)
// authorized (requested team matches resource team)

View File

@ -211,12 +211,15 @@ var _ = Describe("APIAuthWrappa", func() {
atc.ListAllResources: authenticateIfTokenProvided(inputHandlers[atc.ListAllResources]),
atc.ListTeams: authenticateIfTokenProvided(inputHandlers[atc.ListTeams]),
atc.MainJobBadge: authenticateIfTokenProvided(inputHandlers[atc.MainJobBadge]),
atc.GetWall: authenticateIfTokenProvided(inputHandlers[atc.GetWall]),
// authenticated and is admin
atc.GetLogLevel: authenticatedAndAdmin(inputHandlers[atc.GetLogLevel]),
atc.SetLogLevel: authenticatedAndAdmin(inputHandlers[atc.SetLogLevel]),
atc.GetInfoCreds: authenticatedAndAdmin(inputHandlers[atc.GetInfoCreds]),
atc.ListActiveUsersSince: authenticatedAndAdmin(inputHandlers[atc.ListActiveUsersSince]),
atc.SetWall: authenticatedAndAdmin(inputHandlers[atc.SetWall]),
atc.ClearWall: authenticatedAndAdmin(inputHandlers[atc.ClearWall]),
// authorized (requested team matches resource team)
atc.CheckResource: authorized(inputHandlers[atc.CheckResource]),