Skip to content

Commit

Permalink
Merge pull request #13 from safesoftware/job-cancellation
Browse files Browse the repository at this point in the history
Implement Job cancellation
  • Loading branch information
garnold54 authored Dec 9, 2022
2 parents 1b30b2c + 43a7966 commit 49a0195
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 0 deletions.
140 changes: 140 additions & 0 deletions cmd/cancel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"

"github.com/spf13/cobra"
"github.com/spf13/viper"
)

type CancelMessage struct {
Message string `json:"message"`
}

type cancelFlags struct {
id string
apiVersion apiVersionFlag
}

var cancelV4BuildThreshold = 22337

func newCancelCmd() *cobra.Command {
f := cancelFlags{}
cmd := &cobra.Command{
Use: "cancel",
Short: "Cancel a running job on FME Server",
Long: `Cancels the job and marks it as aborted in the completed jobs section, but does not remove it from the database.`,
Example: `
# Cancel a job with id 42
fmeserver cancel --id 42
`,
Args: NoArgs,
RunE: runCancel(&f),
}

cmd.Flags().StringVar(&f.id, "id", "", " The ID of the job to cancel.")
cmd.Flags().Var(&f.apiVersion, "api-version", "The api version to use when contacting FME Server. Must be one of v3 or v4")
cmd.Flags().MarkHidden("api-version")
cmd.RegisterFlagCompletionFunc("api-version", apiVersionFlagCompletion)
cmd.MarkFlagRequired("id")

return cmd
}

func runCancel(f *cancelFlags) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
// set up http
client := &http.Client{}

// get build to decide if we should use v3 or v4
// FME Server 2022.0 and later can use v4. Otherwise fall back to v3
if f.apiVersion == "" {
fmeserverBuild := viper.GetInt("build")
if fmeserverBuild < healthcheckV4BuildThreshold {
f.apiVersion = apiVersionFlagV3
} else {
f.apiVersion = apiVersionFlagV4
}
}

if f.apiVersion == "v4" {
endpoint := "/fmeapiv4/jobs/" + f.id + "/cancel"

request, err := buildFmeServerRequest(endpoint, "POST", nil)
if err != nil {
return err
}
response, err := client.Do(&request)
if err != nil {
return err
} else if response.StatusCode != 204 {
// attempt to parse the body into JSON as there could be a valuable message in there
// if fail, just output the status code
responseData, err := io.ReadAll(response.Body)
if err == nil {

var responseMessage CancelMessage
if err := json.Unmarshal(responseData, &responseMessage); err == nil {

// if json output is requested, output the JSON to stdout before erroring
if jsonOutput {
prettyJSON, err := prettyPrintJSON(responseData)
if err == nil {
fmt.Fprintln(cmd.OutOrStdout(), prettyJSON)
} else {
return errors.New(response.Status)
}
}
return errors.New(responseMessage.Message)
} else {
return errors.New(response.Status)
}
} else {
return errors.New(response.Status)
}

}

if jsonOutput {
// This endpoint returns no content if successful. Just output empty JSON if requested.
fmt.Fprintln(cmd.OutOrStdout(), "{}")
} else {
fmt.Fprintln(cmd.OutOrStdout(), "Success. The job with id "+f.id+" was cancelled.")
}

return nil

} else if f.apiVersion == "v3" {

// call the status endpoint to see if it is finished
request, err := buildFmeServerRequest("/fmerest/v3/transformations/jobs/running/"+f.id, "DELETE", nil)
if err != nil {
return err
}
response, err := client.Do(&request)
if err != nil {
return err
} else if response.StatusCode == 404 {
return errors.New("the specified job ID was not found")
} else if response.StatusCode != 204 {
return errors.New(response.Status)
}

if jsonOutput {
fmt.Fprintln(cmd.OutOrStdout(), "{}")
} else {
fmt.Fprintln(cmd.OutOrStdout(), "Success. The job with id "+f.id+" was cancelled.")
}

return nil
}
return nil
}
}
86 changes: 86 additions & 0 deletions cmd/cancel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package cmd

import (
"net/http"
"testing"
)

func TestCancel(t *testing.T) {
cases := []testCase{
{
name: "unknown flag",
statusCode: http.StatusOK,
args: []string{"cancel", "--badflag"},
wantErrOutputRegex: "unknown flag: --badflag",
},
{
name: "500 bad status code",
statusCode: http.StatusInternalServerError,
wantErrText: "500 Internal Server Error",
args: []string{"cancel", "--id", "1"},
},
{
name: "invalid job id v3",
statusCode: http.StatusNotFound,
wantErrText: "the specified job ID was not found",
args: []string{"cancel", "--id", "1", "--api-version", "v3"},
},
{
name: "cancel valid job v3",
statusCode: http.StatusNoContent,
args: []string{"cancel", "--id", "1234", "--api-version", "v3"},
wantOutputRegex: "Success. The job with id 1234 was cancelled.",
},
{
name: "cancel valid job json v3",
statusCode: http.StatusNoContent,
args: []string{"cancel", "--id", "1234", "--json", "--api-version", "v3"},
wantOutputRegex: "{}",
},
{
name: "job already complete",
statusCode: http.StatusUnprocessableEntity,
body: `{"message": "Job \"1234\" is already complete and cannot be cancelled."}`,
args: []string{"cancel", "--id", "1234", "--api-version", "v4"},
wantErrText: "Job \"1234\" is already complete and cannot be cancelled.",
},
{
name: "job id does not exist",
statusCode: http.StatusUnprocessableEntity,
body: `{"message": "The job for ID \"55\" does not exist."}`,
args: []string{"cancel", "--id", "1234", "--api-version", "v4"},
wantErrText: "The job for ID \"55\" does not exist.",
},
{
name: "job already complete json",
statusCode: http.StatusUnprocessableEntity,
body: `{"message": "Job \"1234\" is already complete and cannot be cancelled."}`,
args: []string{"cancel", "--id", "1234", "--json", "--api-version", "v4"},
wantErrText: "Job \"1234\" is already complete and cannot be cancelled.",
wantOutputJson: `{"message": "Job \"1234\" is already complete and cannot be cancelled."}`,
},
{
name: "job id does not exist json",
statusCode: http.StatusUnprocessableEntity,
body: `{"message": "The job for ID \"55\" does not exist."}`,
args: []string{"cancel", "--id", "1234", "--json", "--api-version", "v4"},
wantErrText: "The job for ID \"55\" does not exist.",
wantOutputJson: `{"message": "The job for ID \"55\" does not exist."}`,
},
{
name: "cancel valid job",
statusCode: http.StatusNoContent,
args: []string{"cancel", "--id", "1234", "--api-version", "v4"},
wantOutputRegex: "Success. The job with id 1234 was cancelled.",
},
{
name: "cancel valid job json",
statusCode: http.StatusNoContent,
args: []string{"cancel", "--id", "1234", "--json", "--api-version", "v4"},
wantOutputRegex: "{}",
},
}

runTests(cases, t)

}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func NewRootCommand() *cobra.Command {
cmds.AddCommand(newMigrationCmd())
cmds.AddCommand(newRestoreCmd())
cmds.AddCommand(newRunCmd())
cmds.AddCommand(newCancelCmd())
cmds.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
cmd.PrintErrln(err)
cmd.PrintErrln(cmd.UsageString())
Expand Down

0 comments on commit 49a0195

Please sign in to comment.