Skip to content

Commit

Permalink
Add support for cancel/run stage and run/runfailed jobs APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
nikhilsbhat committed Jul 17, 2023
1 parent 5a0872a commit 4fa4e65
Show file tree
Hide file tree
Showing 12 changed files with 472 additions and 88 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Below are the list of supported APIs:
- [x] Delete Agents bulk
- [x] Kill running tasks iin Agent
- [x] Agent job run history
- [ ] [ConfigRepo](https://api.gocd.org/current/#config-repo)
- [x] [ConfigRepo](https://api.gocd.org/current/#config-repo)
- [x] Get All Config repo
- [x] Get Specific Config repo
- [x] Create Config repo
Expand Down Expand Up @@ -120,13 +120,13 @@ Below are the list of supported APIs:
- [x] Extract template from pipeline
- [x] Validate pipeline config syntax
- [ ] [Stage Instances](https://api.gocd.org/current/#stage-instances)
- [ ] Cancel stage
- [x] Cancel stage
- [ ] Get stage instance
- [ ] Get stage history
- [ ] Run failed jobs
- [ ] Run selected jobs
- [ ] [Stages](https://api.gocd.org/current/#stages)
- [ ] Run stage
- [x] Run failed jobs
- [x] Run selected jobs
- [x] [Stages](https://api.gocd.org/current/#stages)
- [x] Run stage
- [ ] [Jobs](https://api.gocd.org/current/#jobs)
- [ ] Get job instance
- [ ] Get job history
Expand Down
2 changes: 2 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const (
MaintenanceEndpoint = "/api/admin/maintenance_mode"
APIFeedPipelineEndpoint = "/api/feed/pipelines.xml"
APIJobFeedEndpoint = "/api/feed/jobs/scheduled.xml"
JobsAPIEndpoint = "/api/jobs"
StageEndpoint = "/api/stages"
PipelineStatus = "/api/pipelines/%s/status"
EncryptEndpoint = "/api/admin/encrypt"
ArtifactInfoEndpoint = "/api/admin/config/server/artifact_config"
Expand Down
4 changes: 4 additions & 0 deletions gocd.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ type GoCd interface {
DeletePipeline(name string) error
ExportPipelineToConfigRepoFormat(pipelineName, pluginID string) (PipelineExport, error)
GetScheduledJobs() (ScheduledJobs, error)
RunFailedJobs(stage Stage) (string, error)
RunJobs(stage Stage) (string, error)
RunStage(stage Stage) (string, error)
CancelStage(stage Stage) (string, error)
ExtractTemplatePipeline(pipeline, template string) (PipelineConfig, error)
EncryptText(value string) (Encrypted, error)
DecryptText(value, cipherKey string) (string, error)
Expand Down
4 changes: 2 additions & 2 deletions gocd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
func TestGetGoCDMethodNames(t *testing.T) {
t.Run("should list all method names", func(t *testing.T) {
response := gocd.GetGoCDMethodNames()
assert.Equal(t, 133, len(response))
assert.Equal(t, 137, len(response))
assert.Equal(t, "AgentKillTask", response[0])
assert.Equal(t, "ValidatePipelineSyntax", response[132])
assert.Equal(t, "ValidatePipelineSyntax", response[136])
})
}
96 changes: 96 additions & 0 deletions jobs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package gocd

import (
"encoding/json"
"encoding/xml"
"net/http"
"path/filepath"

"github.com/jinzhu/copier"
"github.com/nikhilsbhat/gocd-sdk-go/pkg/errors"
)

// GetScheduledJobs returns all scheduled jobs from GoCD.
func (conf *client) GetScheduledJobs() (ScheduledJobs, error) {
var scheduledJobs ScheduledJobs
newClient := &client{}
if err := copier.CopyWithOption(newClient, conf, copier.Option{IgnoreEmpty: true, DeepCopy: true}); err != nil {
return scheduledJobs, err
}

resp, err := newClient.httpClient.R().
Get(APIJobFeedEndpoint)
if err != nil {
return scheduledJobs, &errors.APIError{Err: err, Message: "get scheduled jobs"}
}
if resp.StatusCode() != http.StatusOK {
return scheduledJobs, &errors.NonOkError{Code: resp.StatusCode(), Response: resp}
}

if err = xml.Unmarshal(resp.Body(), &scheduledJobs); err != nil {
return scheduledJobs, &errors.MarshalError{Err: err}
}

return scheduledJobs, nil
}

// RunFailedJobs runs all failed jobs from a selected pipeline.
func (conf *client) RunFailedJobs(stage Stage) (string, error) {
newClient := &client{}
if err := copier.CopyWithOption(newClient, conf, copier.Option{IgnoreEmpty: true, DeepCopy: true}); err != nil {
return "", err
}

resp, err := newClient.httpClient.R().
SetHeaders(map[string]string{
"Accept": HeaderVersionThree,
HeaderConfirm: "true",
}).
Post(filepath.Join(StageEndpoint, stage.Pipeline, stage.PipelineInstance, stage.Name, stage.StageCounter, "run-failed-jobs"))
if err != nil {
return "", &errors.APIError{Err: err, Message: "run failed jobs"}
}

if resp.StatusCode() != http.StatusAccepted {
return "", &errors.NonOkError{Code: resp.StatusCode(), Response: resp}
}

var message map[string]string

if err = json.Unmarshal(resp.Body(), &message); err != nil {
return "", &errors.MarshalError{Err: err}
}

return message["message"], nil
}

// RunJobs runs all selected jobs from a selected pipeline.
func (conf *client) RunJobs(stage Stage) (string, error) {
newClient := &client{}
if err := copier.CopyWithOption(newClient, conf, copier.Option{IgnoreEmpty: true, DeepCopy: true}); err != nil {
return "", err
}

resp, err := newClient.httpClient.R().
SetBody(stage).
SetHeaders(map[string]string{
"Accept": HeaderVersionThree,
HeaderConfirm: "true",
}).
Post(filepath.Join(StageEndpoint, stage.Pipeline, stage.PipelineInstance, stage.Name, stage.StageCounter, "run-selected-jobs"))
if err != nil {
return "", &errors.APIError{Err: err, Message: "run selected jobs"}
}

if resp.StatusCode() != http.StatusAccepted {
return "", &errors.NonOkError{Code: resp.StatusCode(), Response: resp}
}

var message map[string]string

if err = json.Unmarshal(resp.Body(), &message); err != nil {
return "", &errors.MarshalError{Err: err}
}

return message["message"], nil
}
169 changes: 169 additions & 0 deletions jobs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package gocd_test

import (
_ "embed"
"net/http"
"testing"

"github.com/nikhilsbhat/gocd-sdk-go"
"github.com/stretchr/testify/assert"
)

//go:embed internal/fixtures/scheduled_jobs.xml
var scheduledJobJSON string

func Test_client_ScheduledJobs(t *testing.T) {
t.Run("should error out while fetching scheduled jobs from server", func(t *testing.T) {
client := gocd.NewClient("http://localhost:8156/go", auth, "info", nil)
client.SetRetryCount(1)
client.SetRetryWaitTime(1)

actual, err := client.GetScheduledJobs()
assert.EqualError(t, err, "call made to get scheduled jobs errored with: "+
"Get \"http://localhost:8156/go/api/feed/jobs/scheduled.xml\": dial tcp [::1]:8156: connect: connection refused")
assert.Equal(t, gocd.ScheduledJobs{}, actual)
})

t.Run("should error out while fetching pipelines as server returned non 200 status code", func(t *testing.T) {
server := mockServer([]byte("scheduledJobJSON"), http.StatusBadGateway, nil, true, nil)
client := gocd.NewClient(server.URL, auth, "info", nil)

actual, err := client.GetScheduledJobs()
assert.EqualError(t, err, "got 502 from GoCD while making GET call for "+server.URL+
"/api/feed/jobs/scheduled.xml\nwith BODY:scheduledJobJSON")
assert.Equal(t, gocd.ScheduledJobs{}, actual)
})

t.Run("should error out while fetching pipelines as server returned malformed response", func(t *testing.T) {
server := mockServer([]byte(`{"email_on_failure"`), http.StatusOK, nil, true, nil)
client := gocd.NewClient(server.URL, auth, "info", nil)

actual, err := client.GetScheduledJobs()
assert.EqualError(t, err, "reading response body errored with: EOF")
assert.Equal(t, gocd.ScheduledJobs{}, actual)
})

t.Run("should be able to fetch the pipelines present in GoCD", func(t *testing.T) {
server := mockServer([]byte(scheduledJobJSON), http.StatusOK, nil, true, nil)
client := gocd.NewClient(server.URL, auth, "info", nil)

expected := gocd.ScheduledJobs{
Job: []gocd.Job{
{
Name: "job1",
ID: "6",
BuildLocator: "mypipeline/5/defaultStage/1/job1",
Environment: "sample_environment",
},
},
}

actual, err := client.GetScheduledJobs()
assert.NoError(t, err)
assert.Equal(t, expected, actual)
})
}

func Test_client_RunJobs(t *testing.T) {
correctJobsHeader := map[string]string{"Accept": gocd.HeaderVersionThree, gocd.HeaderConfirm: "true"}
t.Run("should error out while running selected jobs of a pipeline from server", func(t *testing.T) {
client := gocd.NewClient("http://localhost:8156/go", auth, "info", nil)
client.SetRetryCount(1)
client.SetRetryWaitTime(1)

actual, err := client.RunJobs(gocd.Stage{})
assert.EqualError(t, err, "call made to run selected jobs errored with: "+
"Post \"http://localhost:8156/go/api/stages/run-selected-jobs\": dial tcp [::1]:8156: connect: connection refused")
assert.Equal(t, "", actual)
})

t.Run("should error out while running selected jobs as server returned non 200 status code", func(t *testing.T) {
server := mockServer([]byte("scheduledJobJSON"), http.StatusBadGateway, correctJobsHeader, true, nil)
client := gocd.NewClient(server.URL, auth, "info", nil)

actual, err := client.RunJobs(gocd.Stage{})
assert.EqualError(t, err, "got 502 from GoCD while making POST call for "+server.URL+
"/api/stages/run-selected-jobs\nwith BODY:scheduledJobJSON")
assert.Equal(t, "", actual)
})

t.Run("should error out while running selected jobs as server returned malformed response", func(t *testing.T) {
server := mockServer([]byte(`{"email_on_failure"`), http.StatusAccepted, correctJobsHeader, true, nil)
client := gocd.NewClient(server.URL, auth, "info", nil)

actual, err := client.RunJobs(gocd.Stage{})
assert.EqualError(t, err, "reading response body errored with: unexpected end of JSON input")
assert.Equal(t, "", actual)
})

t.Run("should be able to run jobs from a pipeline present in GoCD", func(t *testing.T) {
runJobResponse := `{"message": "Request to rerun jobs accepted"}`
server := mockServer([]byte(runJobResponse), http.StatusAccepted, correctJobsHeader, true, nil)
client := gocd.NewClient(server.URL, auth, "info", nil)

stage := gocd.Stage{
Pipeline: "pipeline1",
PipelineInstance: "2",
Name: "myStage1",
StageCounter: "3",
Jobs: []string{"myJob1", "myJob2"},
}
expected := "Request to rerun jobs accepted"

actual, err := client.RunJobs(stage)
assert.NoError(t, err)
assert.Equal(t, expected, actual)
})
}

func Test_client_RunFailedJobs(t *testing.T) {
correctJobsHeader := map[string]string{"Accept": gocd.HeaderVersionThree, gocd.HeaderConfirm: "true"}
t.Run("should error out while running failed jobs from server", func(t *testing.T) {
client := gocd.NewClient("http://localhost:8156/go", auth, "info", nil)
client.SetRetryCount(1)
client.SetRetryWaitTime(1)

actual, err := client.RunFailedJobs(gocd.Stage{})
assert.EqualError(t, err, "call made to run failed jobs errored with: "+
"Post \"http://localhost:8156/go/api/stages/run-failed-jobs\": dial tcp [::1]:8156: connect: connection refused")
assert.Equal(t, "", actual)
})

t.Run("should error out while running failed jobs as server returned non 200 status code", func(t *testing.T) {
server := mockServer([]byte("scheduledJobJSON"), http.StatusBadGateway, correctJobsHeader, true, nil)
client := gocd.NewClient(server.URL, auth, "info", nil)

actual, err := client.RunFailedJobs(gocd.Stage{})
assert.EqualError(t, err, "got 502 from GoCD while making POST call for "+server.URL+
"/api/stages/run-failed-jobs\nwith BODY:scheduledJobJSON")
assert.Equal(t, "", actual)
})

t.Run("should error out while running failed jobs as server returned malformed response", func(t *testing.T) {
server := mockServer([]byte(`{"email_on_failure"`), http.StatusAccepted, correctJobsHeader, true, nil)
client := gocd.NewClient(server.URL, auth, "info", nil)

actual, err := client.RunFailedJobs(gocd.Stage{})
assert.EqualError(t, err, "reading response body errored with: unexpected end of JSON input")
assert.Equal(t, "", actual)
})

t.Run("should be able to run failed jobs present in GoCD", func(t *testing.T) {
runFailedJobResponse := `{"message": "Request to rerun jobs accepted"}`
server := mockServer([]byte(runFailedJobResponse), http.StatusAccepted, correctJobsHeader, true, nil)
client := gocd.NewClient(server.URL, auth, "info", nil)

stage := gocd.Stage{
Pipeline: "pipeline1",
PipelineInstance: "2",
Name: "mystage",
StageCounter: "3",
Jobs: nil,
}

expected := "Request to rerun jobs accepted"
actual, err := client.RunFailedJobs(stage)
assert.NoError(t, err)
assert.Equal(t, expected, actual)
})
}
4 changes: 2 additions & 2 deletions maintenance.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ func (conf *client) DisableMaintenanceMode() error {

resp, err := newClient.httpClient.R().
SetHeaders(map[string]string{
"Accept": HeaderVersionOne,
"X-GoCD-Confirm": "true",
"Accept": HeaderVersionOne,
HeaderConfirm: "true",
}).
Post(filepath.Join(MaintenanceEndpoint, "disable"))
if err != nil {
Expand Down
24 changes: 0 additions & 24 deletions pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,30 +332,6 @@ func (conf *client) GetPipelineInstance(pipeline PipelineObject) (map[string]int
return pipelineInstance, nil
}

// GetScheduledJobs returns all scheduled jobs from GoCD.
func (conf *client) GetScheduledJobs() (ScheduledJobs, error) {
var scheduledJobs ScheduledJobs
newClient := &client{}
if err := copier.CopyWithOption(newClient, conf, copier.Option{IgnoreEmpty: true, DeepCopy: true}); err != nil {
return scheduledJobs, err
}

resp, err := newClient.httpClient.R().
Get(APIJobFeedEndpoint)
if err != nil {
return scheduledJobs, &errors.APIError{Err: err, Message: "get scheduled jobs"}
}
if resp.StatusCode() != http.StatusOK {
return scheduledJobs, &errors.NonOkError{Code: resp.StatusCode(), Response: resp}
}

if err = xml.Unmarshal(resp.Body(), &scheduledJobs); err != nil {
return scheduledJobs, &errors.MarshalError{Err: err}
}

return scheduledJobs, nil
}

func (conf *client) ExportPipelineToConfigRepoFormat(pipelineName, pluginID string) (PipelineExport, error) {
newClient := &client{}
if err := copier.CopyWithOption(newClient, conf, copier.Option{IgnoreEmpty: true, DeepCopy: true}); err != nil {
Expand Down
Loading

0 comments on commit 4fa4e65

Please sign in to comment.