1220 lines
33 KiB
Go
1220 lines
33 KiB
Go
package api_test
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
|
|
"code.cloudfoundry.org/garden"
|
|
gfakes "code.cloudfoundry.org/garden/gardenfakes"
|
|
"github.com/concourse/concourse/atc"
|
|
"github.com/concourse/concourse/atc/api/accessor/accessorfakes"
|
|
"github.com/concourse/concourse/atc/db"
|
|
"github.com/concourse/concourse/atc/db/dbfakes"
|
|
"github.com/concourse/concourse/atc/worker/workerfakes"
|
|
"github.com/gorilla/websocket"
|
|
. "github.com/onsi/ginkgo"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("Containers API", func() {
|
|
var (
|
|
stepType = db.ContainerTypeTask
|
|
fakeaccess *accessorfakes.FakeAccess
|
|
stepName = "some-step"
|
|
pipelineID = 1111
|
|
jobID = 2222
|
|
buildID = 3333
|
|
workingDirectory = "/tmp/build/my-favorite-guid"
|
|
attempt = "1.5"
|
|
user = "snoopy"
|
|
|
|
req *http.Request
|
|
|
|
fakeContainer1 *dbfakes.FakeContainer
|
|
fakeContainer2 *dbfakes.FakeContainer
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
fakeaccess = new(accessorfakes.FakeAccess)
|
|
fakeContainer1 = new(dbfakes.FakeContainer)
|
|
fakeContainer1.HandleReturns("some-handle")
|
|
fakeContainer1.StateReturns("container-state")
|
|
fakeContainer1.WorkerNameReturns("some-worker-name")
|
|
fakeContainer1.MetadataReturns(db.ContainerMetadata{
|
|
Type: stepType,
|
|
|
|
StepName: stepName,
|
|
Attempt: attempt,
|
|
|
|
PipelineID: pipelineID,
|
|
JobID: jobID,
|
|
BuildID: buildID,
|
|
|
|
WorkingDirectory: workingDirectory,
|
|
User: user,
|
|
})
|
|
|
|
fakeContainer2 = new(dbfakes.FakeContainer)
|
|
fakeContainer2.HandleReturns("some-other-handle")
|
|
fakeContainer2.WorkerNameReturns("some-other-worker-name")
|
|
fakeContainer2.MetadataReturns(db.ContainerMetadata{
|
|
Type: stepType,
|
|
|
|
StepName: stepName + "-other",
|
|
Attempt: attempt + ".1",
|
|
|
|
PipelineID: pipelineID + 1,
|
|
JobID: jobID + 1,
|
|
BuildID: buildID + 1,
|
|
|
|
WorkingDirectory: workingDirectory + "/other",
|
|
User: user + "-other",
|
|
})
|
|
})
|
|
JustBeforeEach(func() {
|
|
fakeAccessor.CreateReturns(fakeaccess)
|
|
})
|
|
|
|
Describe("GET /api/v1/teams/a-team/containers", func() {
|
|
BeforeEach(func() {
|
|
var err error
|
|
req, err = http.NewRequest("GET", server.URL+"/api/v1/teams/a-team/containers", nil)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
req.Header.Set("Content-Type", "application/json")
|
|
})
|
|
|
|
Context("when not authenticated", func() {
|
|
BeforeEach(func() {
|
|
fakeaccess.IsAuthenticatedReturns(false)
|
|
})
|
|
|
|
It("returns 401 Unauthorized", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
|
|
})
|
|
})
|
|
|
|
Context("when authenticated", func() {
|
|
BeforeEach(func() {
|
|
fakeaccess.IsAuthenticatedReturns(true)
|
|
fakeaccess.IsAuthorizedReturns(true)
|
|
})
|
|
|
|
Context("with no params", func() {
|
|
Context("when no errors are returned", func() {
|
|
BeforeEach(func() {
|
|
dbTeam.ContainersReturns([]db.Container{fakeContainer1, fakeContainer2}, nil)
|
|
})
|
|
|
|
It("returns 200", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(response.StatusCode).To(Equal(http.StatusOK))
|
|
})
|
|
|
|
It("returns Content-Type application/json", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(response.Header.Get("Content-Type")).To(Equal("application/json"))
|
|
})
|
|
|
|
It("returns all containers", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(body).To(MatchJSON(`
|
|
[
|
|
{
|
|
"id": "some-handle",
|
|
"worker_name": "some-worker-name",
|
|
"type": "task",
|
|
"step_name": "some-step",
|
|
"attempt": "1.5",
|
|
"pipeline_id": 1111,
|
|
"job_id": 2222,
|
|
"state": "container-state",
|
|
"build_id": 3333,
|
|
"working_directory": "/tmp/build/my-favorite-guid",
|
|
"user": "snoopy"
|
|
},
|
|
{
|
|
"id": "some-other-handle",
|
|
"worker_name": "some-other-worker-name",
|
|
"type": "task",
|
|
"step_name": "some-step-other",
|
|
"attempt": "1.5.1",
|
|
"pipeline_id": 1112,
|
|
"job_id": 2223,
|
|
"build_id": 3334,
|
|
"working_directory": "/tmp/build/my-favorite-guid/other",
|
|
"user": "snoopy-other"
|
|
}
|
|
]
|
|
`))
|
|
})
|
|
})
|
|
|
|
Context("when no containers are found", func() {
|
|
BeforeEach(func() {
|
|
dbTeam.ContainersReturns([]db.Container{}, nil)
|
|
})
|
|
|
|
It("returns 200", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(response.StatusCode).To(Equal(http.StatusOK))
|
|
})
|
|
|
|
It("returns an empty array", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(body).To(MatchJSON(`
|
|
[]
|
|
`))
|
|
})
|
|
})
|
|
|
|
Context("when there is an error", func() {
|
|
var (
|
|
expectedErr error
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
expectedErr = errors.New("some error")
|
|
dbTeam.ContainersReturns(nil, expectedErr)
|
|
})
|
|
|
|
It("returns 500", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("querying with pipeline id", func() {
|
|
BeforeEach(func() {
|
|
req.URL.RawQuery = url.Values{
|
|
"pipeline_id": []string{strconv.Itoa(pipelineID)},
|
|
}.Encode()
|
|
})
|
|
|
|
It("queries with it in the metadata", func() {
|
|
_, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(dbTeam.FindContainersByMetadataCallCount()).To(Equal(1))
|
|
|
|
meta := dbTeam.FindContainersByMetadataArgsForCall(0)
|
|
Expect(meta).To(Equal(db.ContainerMetadata{
|
|
PipelineID: pipelineID,
|
|
}))
|
|
})
|
|
})
|
|
|
|
Describe("querying with job id", func() {
|
|
BeforeEach(func() {
|
|
req.URL.RawQuery = url.Values{
|
|
"job_id": []string{strconv.Itoa(jobID)},
|
|
}.Encode()
|
|
})
|
|
|
|
It("queries with it in the metadata", func() {
|
|
_, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(dbTeam.FindContainersByMetadataCallCount()).To(Equal(1))
|
|
|
|
meta := dbTeam.FindContainersByMetadataArgsForCall(0)
|
|
Expect(meta).To(Equal(db.ContainerMetadata{
|
|
JobID: jobID,
|
|
}))
|
|
})
|
|
})
|
|
|
|
Describe("querying with type", func() {
|
|
BeforeEach(func() {
|
|
req.URL.RawQuery = url.Values{
|
|
"type": []string{string(stepType)},
|
|
}.Encode()
|
|
})
|
|
|
|
It("queries with it in the metadata", func() {
|
|
_, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
meta := dbTeam.FindContainersByMetadataArgsForCall(0)
|
|
Expect(meta).To(Equal(db.ContainerMetadata{
|
|
Type: stepType,
|
|
}))
|
|
})
|
|
})
|
|
|
|
Describe("querying with step name", func() {
|
|
BeforeEach(func() {
|
|
req.URL.RawQuery = url.Values{
|
|
"step_name": []string{stepName},
|
|
}.Encode()
|
|
})
|
|
|
|
It("queries with it in the metadata", func() {
|
|
_, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
meta := dbTeam.FindContainersByMetadataArgsForCall(0)
|
|
Expect(meta).To(Equal(db.ContainerMetadata{
|
|
StepName: stepName,
|
|
}))
|
|
})
|
|
})
|
|
|
|
Describe("querying with build id", func() {
|
|
Context("when the buildID can be parsed as an int", func() {
|
|
BeforeEach(func() {
|
|
buildIDString := strconv.Itoa(buildID)
|
|
|
|
req.URL.RawQuery = url.Values{
|
|
"build_id": []string{buildIDString},
|
|
}.Encode()
|
|
})
|
|
|
|
It("queries with it in the metadata", func() {
|
|
_, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
meta := dbTeam.FindContainersByMetadataArgsForCall(0)
|
|
Expect(meta).To(Equal(db.ContainerMetadata{
|
|
BuildID: buildID,
|
|
}))
|
|
})
|
|
|
|
Context("when the buildID fails to be parsed as an int", func() {
|
|
BeforeEach(func() {
|
|
req.URL.RawQuery = url.Values{
|
|
"build_id": []string{"not-an-int"},
|
|
}.Encode()
|
|
})
|
|
|
|
It("returns 400 Bad Request", func() {
|
|
response, _ := client.Do(req)
|
|
Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
|
|
})
|
|
|
|
It("does not lookup containers", func() {
|
|
_, _ = client.Do(req)
|
|
Expect(dbTeam.FindContainersByMetadataCallCount()).To(Equal(0))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("querying with attempts", func() {
|
|
Context("when the attempts can be parsed as a slice of int", func() {
|
|
BeforeEach(func() {
|
|
req.URL.RawQuery = url.Values{
|
|
"attempt": []string{attempt},
|
|
}.Encode()
|
|
})
|
|
|
|
It("queries with it in the metadata", func() {
|
|
_, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
meta := dbTeam.FindContainersByMetadataArgsForCall(0)
|
|
Expect(meta).To(Equal(db.ContainerMetadata{
|
|
Attempt: attempt,
|
|
}))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("querying with type 'check'", func() {
|
|
BeforeEach(func() {
|
|
req.URL.RawQuery = url.Values{
|
|
"type": []string{"check"},
|
|
"resource_name": []string{"some-resource"},
|
|
"pipeline_name": []string{"some-pipeline"},
|
|
}.Encode()
|
|
})
|
|
|
|
It("queries with check properties", func() {
|
|
_, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
pipelineName, resourceName, secretManager := dbTeam.FindCheckContainersArgsForCall(0)
|
|
Expect(pipelineName).To(Equal("some-pipeline"))
|
|
Expect(resourceName).To(Equal("some-resource"))
|
|
Expect(secretManager).To(Equal(fakeSecretManager))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("GET /api/v1/containers/:id", func() {
|
|
var handle = "some-handle"
|
|
|
|
BeforeEach(func() {
|
|
dbTeam.FindContainerByHandleReturns(fakeContainer1, true, nil)
|
|
|
|
var err error
|
|
req, err = http.NewRequest("GET", server.URL+"/api/v1/teams/a-team/containers/"+handle, nil)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
req.Header.Set("Content-Type", "application/json")
|
|
})
|
|
|
|
Context("when not authenticated", func() {
|
|
BeforeEach(func() {
|
|
fakeaccess.IsAuthenticatedReturns(false)
|
|
})
|
|
|
|
It("returns 401 Unauthorized", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
|
|
})
|
|
})
|
|
|
|
Context("when authenticated", func() {
|
|
BeforeEach(func() {
|
|
fakeaccess.IsAuthenticatedReturns(true)
|
|
fakeaccess.IsAuthorizedReturns(true)
|
|
})
|
|
|
|
Context("when the container is not found", func() {
|
|
BeforeEach(func() {
|
|
dbTeam.FindContainerByHandleReturns(nil, false, nil)
|
|
})
|
|
|
|
It("returns 404 Not Found", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(response.StatusCode).To(Equal(http.StatusNotFound))
|
|
})
|
|
})
|
|
|
|
Context("when the container is found", func() {
|
|
BeforeEach(func() {
|
|
dbTeam.FindContainerByHandleReturns(fakeContainer1, true, nil)
|
|
})
|
|
|
|
Context("when the container is within the team", func() {
|
|
BeforeEach(func() {
|
|
dbTeam.IsCheckContainerReturns(false, nil)
|
|
dbTeam.IsContainerWithinTeamReturns(true, nil)
|
|
})
|
|
|
|
It("returns 200 OK", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(response.StatusCode).To(Equal(http.StatusOK))
|
|
})
|
|
|
|
It("returns Content-Type application/json", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(response.Header.Get("Content-Type")).To(Equal("application/json"))
|
|
})
|
|
|
|
It("performs lookup by id", func() {
|
|
_, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(dbTeam.FindContainerByHandleCallCount()).To(Equal(1))
|
|
Expect(dbTeam.FindContainerByHandleArgsForCall(0)).To(Equal(handle))
|
|
})
|
|
|
|
It("returns the container", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(body).To(MatchJSON(`
|
|
{
|
|
"id": "some-handle",
|
|
"state": "container-state",
|
|
"worker_name": "some-worker-name",
|
|
"type": "task",
|
|
"step_name": "some-step",
|
|
"attempt": "1.5",
|
|
"pipeline_id": 1111,
|
|
"job_id": 2222,
|
|
"build_id": 3333,
|
|
"working_directory": "/tmp/build/my-favorite-guid",
|
|
"user": "snoopy"
|
|
}
|
|
`))
|
|
})
|
|
})
|
|
|
|
Context("when the container is not within the team", func() {
|
|
BeforeEach(func() {
|
|
dbTeam.IsCheckContainerReturns(false, nil)
|
|
dbTeam.IsContainerWithinTeamReturns(false, nil)
|
|
})
|
|
|
|
It("returns 404 Not Found", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(response.StatusCode).To(Equal(http.StatusNotFound))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when there is an error", func() {
|
|
var (
|
|
expectedErr error
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
expectedErr = errors.New("some error")
|
|
dbTeam.FindContainerByHandleReturns(nil, false, expectedErr)
|
|
})
|
|
|
|
It("returns 500", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("GET /api/v1/containers/:id/hijack", func() {
|
|
var (
|
|
handle = "some-handle"
|
|
|
|
requestPayload string
|
|
|
|
conn *websocket.Conn
|
|
response *http.Response
|
|
|
|
expectBadHandshake bool
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
expectBadHandshake = false
|
|
requestPayload = `{"path":"ls", "user": "snoopy"}`
|
|
})
|
|
|
|
JustBeforeEach(func() {
|
|
wsURL, err := url.Parse(server.URL)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
wsURL.Scheme = "ws"
|
|
wsURL.Path = "/api/v1/teams/a-team/containers/" + handle + "/hijack"
|
|
|
|
dialer := websocket.Dialer{}
|
|
conn, response, err = dialer.Dial(wsURL.String(), nil)
|
|
if !expectBadHandshake {
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
writer, err := conn.NextWriter(websocket.TextMessage)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
_, err = writer.Write([]byte(requestPayload))
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
err = writer.Close()
|
|
Expect(err).NotTo(HaveOccurred())
|
|
}
|
|
})
|
|
|
|
AfterEach(func() {
|
|
if !expectBadHandshake {
|
|
_ = conn.Close()
|
|
}
|
|
})
|
|
|
|
Context("when authenticated", func() {
|
|
BeforeEach(func() {
|
|
fakeaccess.IsAuthenticatedReturns(true)
|
|
fakeaccess.IsAuthorizedReturns(true)
|
|
})
|
|
|
|
Context("and the worker client returns a container", func() {
|
|
var (
|
|
fakeDBContainer *dbfakes.FakeCreatedContainer
|
|
fakeContainer *workerfakes.FakeContainer
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
fakeDBContainer = new(dbfakes.FakeCreatedContainer)
|
|
dbTeam.FindContainerByHandleReturns(fakeDBContainer, true, nil)
|
|
fakeDBContainer.HandleReturns("some-handle")
|
|
|
|
fakeContainer = new(workerfakes.FakeContainer)
|
|
fakeWorkerClient.FindContainerReturns(fakeContainer, true, nil)
|
|
})
|
|
|
|
Context("when the container is a check container", func() {
|
|
BeforeEach(func() {
|
|
dbTeam.IsCheckContainerReturns(true, nil)
|
|
})
|
|
|
|
Context("when the user is not admin", func() {
|
|
BeforeEach(func() {
|
|
expectBadHandshake = true
|
|
|
|
fakeaccess.IsAdminReturns(false)
|
|
})
|
|
|
|
It("returns Forbidden", func() {
|
|
Expect(response.StatusCode).To(Equal(http.StatusForbidden))
|
|
})
|
|
})
|
|
|
|
Context("when the user is an admin", func() {
|
|
BeforeEach(func() {
|
|
fakeaccess.IsAdminReturns(true)
|
|
})
|
|
|
|
Context("when the container is not within the team", func() {
|
|
BeforeEach(func() {
|
|
expectBadHandshake = true
|
|
|
|
dbTeam.IsContainerWithinTeamReturns(false, nil)
|
|
})
|
|
|
|
It("returns 404 not found", func() {
|
|
Expect(response.StatusCode).To(Equal(http.StatusNotFound))
|
|
})
|
|
})
|
|
|
|
Context("when the container is within the team", func() {
|
|
var (
|
|
fakeProcess *gfakes.FakeProcess
|
|
processExit chan int
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
dbTeam.IsContainerWithinTeamReturns(true, nil)
|
|
|
|
exit := make(chan int)
|
|
processExit = exit
|
|
|
|
fakeProcess = new(gfakes.FakeProcess)
|
|
fakeProcess.WaitStub = func() (int, error) {
|
|
return <-exit, nil
|
|
}
|
|
|
|
fakeContainer.RunReturns(fakeProcess, nil)
|
|
})
|
|
|
|
AfterEach(func() {
|
|
close(processExit)
|
|
})
|
|
|
|
It("should try to hijack the container", func() {
|
|
Eventually(fakeContainer.RunCallCount).Should(Equal(1))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when the container is a build step container", func() {
|
|
BeforeEach(func() {
|
|
dbTeam.IsCheckContainerReturns(false, nil)
|
|
})
|
|
|
|
Context("when the container is not within the team", func() {
|
|
BeforeEach(func() {
|
|
expectBadHandshake = true
|
|
|
|
dbTeam.IsContainerWithinTeamReturns(false, nil)
|
|
})
|
|
|
|
It("returns 404 not found", func() {
|
|
Expect(response.StatusCode).To(Equal(http.StatusNotFound))
|
|
})
|
|
})
|
|
|
|
Context("when the container is within the team", func() {
|
|
BeforeEach(func() {
|
|
dbTeam.IsContainerWithinTeamReturns(true, nil)
|
|
})
|
|
|
|
Context("when the call to lookup the container returns an error", func() {
|
|
BeforeEach(func() {
|
|
expectBadHandshake = true
|
|
|
|
fakeWorkerClient.FindContainerReturns(nil, false, errors.New("nope"))
|
|
})
|
|
|
|
It("returns 500 internal error", func() {
|
|
Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
|
|
})
|
|
})
|
|
|
|
Context("when the container could not be found on the worker client", func() {
|
|
BeforeEach(func() {
|
|
expectBadHandshake = true
|
|
|
|
fakeWorkerClient.FindContainerReturns(nil, false, nil)
|
|
})
|
|
|
|
It("returns 404 Not Found", func() {
|
|
Expect(response.StatusCode).To(Equal(http.StatusNotFound))
|
|
})
|
|
})
|
|
|
|
Context("when the request payload is invalid", func() {
|
|
BeforeEach(func() {
|
|
requestPayload = "ß"
|
|
})
|
|
|
|
It("closes the connection with an error", func() {
|
|
_, _, err := conn.ReadMessage()
|
|
|
|
Expect(websocket.IsCloseError(err, 1003)).To(BeTrue()) // unsupported data
|
|
Expect(err).To(MatchError(ContainSubstring("malformed process spec")))
|
|
})
|
|
})
|
|
|
|
Context("when running the process fails", func() {
|
|
var containerRunError = errors.New("container-run-error")
|
|
|
|
BeforeEach(func() {
|
|
fakeContainer.RunReturns(nil, containerRunError)
|
|
})
|
|
|
|
It("receives the error in the output", func() {
|
|
Eventually(fakeContainer.RunCallCount).Should(Equal(1))
|
|
|
|
expectedHijackOutput := atc.HijackOutput{
|
|
Error: containerRunError.Error(),
|
|
}
|
|
|
|
var hijackOutput atc.HijackOutput
|
|
err := conn.ReadJSON(&hijackOutput)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(hijackOutput).To(Equal(expectedHijackOutput))
|
|
})
|
|
})
|
|
|
|
Context("when running the process succeeds", func() {
|
|
var (
|
|
fakeProcess *gfakes.FakeProcess
|
|
processExit chan int
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
exit := make(chan int)
|
|
processExit = exit
|
|
|
|
fakeProcess = new(gfakes.FakeProcess)
|
|
fakeProcess.WaitStub = func() (int, error) {
|
|
return <-exit, nil
|
|
}
|
|
|
|
fakeContainer.RunReturns(fakeProcess, nil)
|
|
})
|
|
|
|
AfterEach(func() {
|
|
close(processExit)
|
|
})
|
|
|
|
It("did not check if the user is admin", func() {
|
|
Expect(fakeaccess.IsAdminCallCount()).To(Equal(0))
|
|
})
|
|
|
|
It("hijacks the build", func() {
|
|
Eventually(fakeContainer.RunCallCount).Should(Equal(1))
|
|
|
|
_, lookedUpTeamID, lookedUpHandle := fakeWorkerClient.FindContainerArgsForCall(0)
|
|
Expect(lookedUpTeamID).To(Equal(734))
|
|
Expect(lookedUpHandle).To(Equal(handle))
|
|
|
|
spec, io := fakeContainer.RunArgsForCall(0)
|
|
Expect(spec).To(Equal(garden.ProcessSpec{
|
|
Path: "ls",
|
|
User: "snoopy",
|
|
}))
|
|
|
|
Expect(io.Stdin).NotTo(BeNil())
|
|
Expect(io.Stdout).NotTo(BeNil())
|
|
Expect(io.Stderr).NotTo(BeNil())
|
|
})
|
|
|
|
It("marks container as hijacked", func() {
|
|
Eventually(fakeContainer.RunCallCount).Should(Equal(1))
|
|
|
|
Expect(fakeContainer.MarkAsHijackedCallCount()).To(Equal(1))
|
|
})
|
|
|
|
Context("when stdin is sent over the API", func() {
|
|
JustBeforeEach(func() {
|
|
err := conn.WriteJSON(atc.HijackInput{
|
|
Stdin: []byte("some stdin\n"),
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
})
|
|
|
|
It("forwards the payload to the process", func() {
|
|
Eventually(fakeContainer.RunCallCount).Should(Equal(1))
|
|
|
|
_, io := fakeContainer.RunArgsForCall(0)
|
|
Expect(bufio.NewReader(io.Stdin).ReadBytes('\n')).To(Equal([]byte("some stdin\n")))
|
|
|
|
Expect(interceptTimeout.ResetCallCount()).To(Equal(1))
|
|
})
|
|
})
|
|
|
|
Context("when stdin is closed via the API", func() {
|
|
JustBeforeEach(func() {
|
|
err := conn.WriteJSON(atc.HijackInput{
|
|
Closed: true,
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
})
|
|
|
|
It("closes the process's stdin", func() {
|
|
Eventually(fakeContainer.RunCallCount).Should(Equal(1))
|
|
|
|
_, ioConfig := fakeContainer.RunArgsForCall(0)
|
|
_, err := ioConfig.Stdin.Read(make([]byte, 10))
|
|
Expect(err).To(Equal(io.EOF))
|
|
})
|
|
})
|
|
|
|
Context("when the process prints to stdout", func() {
|
|
JustBeforeEach(func() {
|
|
Eventually(fakeContainer.RunCallCount).Should(Equal(1))
|
|
|
|
_, io := fakeContainer.RunArgsForCall(0)
|
|
|
|
_, err := fmt.Fprintf(io.Stdout, "some stdout\n")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
})
|
|
|
|
It("forwards it to the response", func() {
|
|
var hijackOutput atc.HijackOutput
|
|
err := conn.ReadJSON(&hijackOutput)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(hijackOutput).To(Equal(atc.HijackOutput{
|
|
Stdout: []byte("some stdout\n"),
|
|
}))
|
|
})
|
|
})
|
|
|
|
Context("when the process prints to stderr", func() {
|
|
JustBeforeEach(func() {
|
|
Eventually(fakeContainer.RunCallCount).Should(Equal(1))
|
|
|
|
_, io := fakeContainer.RunArgsForCall(0)
|
|
|
|
_, err := fmt.Fprintf(io.Stderr, "some stderr\n")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
})
|
|
|
|
It("forwards it to the response", func() {
|
|
var hijackOutput atc.HijackOutput
|
|
err := conn.ReadJSON(&hijackOutput)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(hijackOutput).To(Equal(atc.HijackOutput{
|
|
Stderr: []byte("some stderr\n"),
|
|
}))
|
|
})
|
|
})
|
|
|
|
Context("when the process exits", func() {
|
|
JustBeforeEach(func() {
|
|
Eventually(processExit).Should(BeSent(123))
|
|
})
|
|
|
|
It("forwards its exit status to the response", func() {
|
|
var hijackOutput atc.HijackOutput
|
|
err := conn.ReadJSON(&hijackOutput)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
exitStatus := 123
|
|
Expect(hijackOutput).To(Equal(atc.HijackOutput{
|
|
ExitStatus: &exitStatus,
|
|
}))
|
|
})
|
|
|
|
It("closes the process' stdin pipe", func() {
|
|
_, io := fakeContainer.RunArgsForCall(0)
|
|
|
|
c := make(chan bool, 1)
|
|
|
|
go func() {
|
|
var b []byte
|
|
_, err := io.Stdin.Read(b)
|
|
if err != nil {
|
|
c <- true
|
|
}
|
|
}()
|
|
|
|
Eventually(c, 2*time.Second).Should(Receive())
|
|
})
|
|
})
|
|
|
|
Context("when new tty settings are sent over the API", func() {
|
|
JustBeforeEach(func() {
|
|
err := conn.WriteJSON(atc.HijackInput{
|
|
TTYSpec: &atc.HijackTTYSpec{
|
|
WindowSize: atc.HijackWindowSize{
|
|
Columns: 123,
|
|
Rows: 456,
|
|
},
|
|
},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
})
|
|
|
|
It("forwards it to the process", func() {
|
|
Eventually(fakeProcess.SetTTYCallCount).Should(Equal(1))
|
|
|
|
Expect(fakeProcess.SetTTYArgsForCall(0)).To(Equal(garden.TTYSpec{
|
|
WindowSize: &garden.WindowSize{
|
|
Columns: 123,
|
|
Rows: 456,
|
|
},
|
|
}))
|
|
})
|
|
|
|
Context("and setting the TTY on the process fails", func() {
|
|
BeforeEach(func() {
|
|
fakeProcess.SetTTYReturns(errors.New("oh no!"))
|
|
})
|
|
|
|
It("forwards the error to the response", func() {
|
|
var hijackOutput atc.HijackOutput
|
|
err := conn.ReadJSON(&hijackOutput)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(hijackOutput).To(Equal(atc.HijackOutput{
|
|
Error: "oh no!",
|
|
}))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when waiting on the process fails", func() {
|
|
BeforeEach(func() {
|
|
fakeProcess.WaitReturns(0, errors.New("oh no!"))
|
|
})
|
|
|
|
It("forwards the error to the response", func() {
|
|
var hijackOutput atc.HijackOutput
|
|
err := conn.ReadJSON(&hijackOutput)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(hijackOutput).To(Equal(atc.HijackOutput{
|
|
Error: "oh no!",
|
|
}))
|
|
})
|
|
})
|
|
|
|
Context("when intercept timeout channel sends a value", func() {
|
|
var (
|
|
interceptTimeoutChannel chan time.Time
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
interceptTimeoutChannel = make(chan time.Time)
|
|
interceptTimeout.ChannelReturns(interceptTimeoutChannel)
|
|
})
|
|
|
|
It("exits with timeout error", func() {
|
|
interceptTimeout.ErrorReturns(errors.New("too slow"))
|
|
interceptTimeoutChannel <- time.Time{}
|
|
|
|
var hijackOutput atc.HijackOutput
|
|
err := conn.ReadJSON(&hijackOutput)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(hijackOutput.Error).To(Equal("too slow"))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when not authenticated", func() {
|
|
BeforeEach(func() {
|
|
expectBadHandshake = true
|
|
|
|
fakeaccess.IsAuthenticatedReturns(false)
|
|
})
|
|
|
|
It("returns 401 Unauthorized", func() {
|
|
Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("GET /api/v1/containers/destroying", func() {
|
|
BeforeEach(func() {
|
|
var err error
|
|
req, err = http.NewRequest("GET", server.URL+"/api/v1/containers/destroying", nil)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
fakeaccess.IsAuthenticatedReturns(true)
|
|
})
|
|
|
|
Context("when not authenticated", func() {
|
|
BeforeEach(func() {
|
|
fakeaccess.IsAuthenticatedReturns(false)
|
|
})
|
|
|
|
It("returns 401 Unauthorized", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
|
|
})
|
|
|
|
It("does not attempt to find the worker", func() {
|
|
Expect(dbWorkerFactory.GetWorkerCallCount()).To(BeZero())
|
|
})
|
|
})
|
|
|
|
Context("when authenticated as system", func() {
|
|
BeforeEach(func() {
|
|
fakeaccess.IsSystemReturns(true)
|
|
})
|
|
|
|
Context("with no params", func() {
|
|
It("returns 400 Bad Request", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeContainerRepository.FindDestroyingContainersCallCount()).To(Equal(0))
|
|
Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
|
|
})
|
|
})
|
|
|
|
Context("querying with worker name", func() {
|
|
BeforeEach(func() {
|
|
req.URL.RawQuery = url.Values{
|
|
"worker_name": []string{"some-worker-name"},
|
|
}.Encode()
|
|
})
|
|
|
|
Context("when there is an error", func() {
|
|
BeforeEach(func() {
|
|
fakeContainerRepository.FindDestroyingContainersReturns(nil, errors.New("some error"))
|
|
})
|
|
|
|
It("returns 500", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
|
|
})
|
|
})
|
|
|
|
Context("when no containers are found", func() {
|
|
BeforeEach(func() {
|
|
fakeContainerRepository.FindDestroyingContainersReturns([]string{}, nil)
|
|
})
|
|
|
|
It("returns 200", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(response.StatusCode).To(Equal(http.StatusOK))
|
|
})
|
|
|
|
It("returns an empty array", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(body).To(MatchJSON(`
|
|
[]
|
|
`))
|
|
})
|
|
|
|
Context("when containers are found", func() {
|
|
BeforeEach(func() {
|
|
fakeContainerRepository.FindDestroyingContainersReturns([]string{
|
|
"handle1",
|
|
"handle2",
|
|
}, nil)
|
|
})
|
|
It("returns container handles array", func() {
|
|
response, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(body).To(MatchJSON(`
|
|
["handle1", "handle2"]
|
|
`))
|
|
})
|
|
})
|
|
})
|
|
|
|
It("queries with it in the worker name", func() {
|
|
_, err := client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeContainerRepository.FindDestroyingContainersCallCount()).To(Equal(1))
|
|
|
|
workerName := fakeContainerRepository.FindDestroyingContainersArgsForCall(0)
|
|
Expect(workerName).To(Equal("some-worker-name"))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("PUT /api/v1/containers/report", func() {
|
|
var response *http.Response
|
|
var body io.Reader
|
|
var err error
|
|
|
|
BeforeEach(func() {
|
|
body = bytes.NewBufferString(`
|
|
[
|
|
"handle1",
|
|
"handle2"
|
|
]
|
|
`)
|
|
})
|
|
|
|
JustBeforeEach(func() {
|
|
req, err = http.NewRequest("PUT", server.URL+"/api/v1/containers/report", body)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
req.Header.Set("Content-Type", "application/json")
|
|
})
|
|
|
|
Context("when not authenticated", func() {
|
|
BeforeEach(func() {
|
|
fakeaccess.IsAuthenticatedReturns(false)
|
|
})
|
|
|
|
It("returns 401 Unauthorized", func() {
|
|
response, err = client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(response.StatusCode).To(Equal(http.StatusUnauthorized))
|
|
})
|
|
})
|
|
|
|
Context("when authenticated as system", func() {
|
|
BeforeEach(func() {
|
|
fakeaccess.IsAuthenticatedReturns(true)
|
|
fakeaccess.IsSystemReturns(true)
|
|
})
|
|
|
|
Context("with no params", func() {
|
|
It("returns 404", func() {
|
|
response, err = client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(fakeDestroyer.DestroyContainersCallCount()).To(Equal(0))
|
|
Expect(response.StatusCode).To(Equal(http.StatusNotFound))
|
|
})
|
|
|
|
It("returns Content-Type application/json", func() {
|
|
response, err = client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(response.StatusCode).To(Equal(http.StatusNotFound))
|
|
Expect(response.Header.Get("Content-Type")).To(Equal("application/json"))
|
|
})
|
|
})
|
|
|
|
Context("querying with worker name", func() {
|
|
JustBeforeEach(func() {
|
|
req.URL.RawQuery = url.Values{
|
|
"worker_name": []string{"some-worker-name"},
|
|
}.Encode()
|
|
})
|
|
|
|
Context("with invalid json", func() {
|
|
BeforeEach(func() {
|
|
body = bytes.NewBufferString(`{}`)
|
|
})
|
|
|
|
It("returns 400", func() {
|
|
response, err = client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(response.StatusCode).To(Equal(http.StatusBadRequest))
|
|
})
|
|
})
|
|
|
|
Context("when there is an error", func() {
|
|
BeforeEach(func() {
|
|
fakeDestroyer.DestroyContainersReturns(errors.New("some error"))
|
|
})
|
|
|
|
It("returns 500", func() {
|
|
response, err = client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(response.StatusCode).To(Equal(http.StatusInternalServerError))
|
|
})
|
|
})
|
|
|
|
Context("when containers are destroyed", func() {
|
|
BeforeEach(func() {
|
|
fakeDestroyer.DestroyContainersReturns(nil)
|
|
})
|
|
|
|
It("returns 204", func() {
|
|
response, err = client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(response.StatusCode).To(Equal(http.StatusNoContent))
|
|
})
|
|
})
|
|
|
|
It("queries with it in the worker name", func() {
|
|
_, err = client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(fakeDestroyer.DestroyContainersCallCount()).To(Equal(1))
|
|
|
|
workerName, handles := fakeDestroyer.DestroyContainersArgsForCall(0)
|
|
Expect(workerName).To(Equal("some-worker-name"))
|
|
Expect(handles).To(Equal([]string{"handle1", "handle2"}))
|
|
})
|
|
|
|
It("marks containers as missing", func() {
|
|
_, err = client.Do(req)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(fakeContainerRepository.UpdateContainersMissingSinceCallCount()).To(Equal(1))
|
|
|
|
workerName, handles := fakeContainerRepository.UpdateContainersMissingSinceArgsForCall(0)
|
|
Expect(workerName).To(Equal("some-worker-name"))
|
|
Expect(handles).To(Equal([]string{"handle1", "handle2"}))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|