Skip to content

Commit 9fc2deb

Browse files
authored
Merge pull request #35 from safesoftware/healthcheck-no-auth
Allow healthcheck to be run without logging in first.
2 parents baedf48 + 3f9e936 commit 9fc2deb

File tree

8 files changed

+139
-54
lines changed

8 files changed

+139
-54
lines changed

.vscode/tasks.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,19 @@
2222
"fmeserver"
2323
]
2424
},
25-
"problemMatcher": []
25+
"problemMatcher": [],
26+
"group": {
27+
"kind": "build",
28+
"isDefault": true
29+
}
30+
},
31+
{
32+
"label": "Build Docker Image",
33+
"type": "shell",
34+
"command": "docker build -t safesoftware/fmeserver-cli .",
35+
"group": {
36+
"kind": "build"
37+
}
2638
}
2739
]
2840
}

cmd/functions.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ package cmd
33
import (
44
"bytes"
55
"encoding/json"
6+
"fmt"
67
"io"
78
"net/http"
9+
"net/url"
10+
"os"
811
"reflect"
912
"strings"
1013
"unicode"
@@ -38,7 +41,9 @@ func buildFmeServerRequest(endpoint string, method string, body io.Reader) (http
3841
fmeserverToken := viper.GetString("token")
3942

4043
req, err := http.NewRequest(method, fmeserverUrl+endpoint, body)
41-
req.Header.Set("Authorization", "fmetoken token="+fmeserverToken)
44+
if fmeserverToken != "" {
45+
req.Header.Set("Authorization", "fmetoken token="+fmeserverToken)
46+
}
4247
return *req, err
4348
}
4449

@@ -163,3 +168,34 @@ func isEmpty(object interface{}) bool {
163168
}
164169
return false
165170
}
171+
172+
func checkConfigFile(requireToken bool) error {
173+
// make sure the config file is set up correctly
174+
_, err := os.Stat(viper.ConfigFileUsed())
175+
if err != nil {
176+
return fmt.Errorf("could not open the config file " + viper.ConfigFileUsed() + ". Have you called the login command? ")
177+
}
178+
fmeserverUrl := viper.GetString("url")
179+
180+
// check the fme server URL is valid
181+
_, err = url.ParseRequestURI(fmeserverUrl)
182+
if err != nil {
183+
return fmt.Errorf("invalid FME Server url in config file " + viper.ConfigFileUsed() + ". Have you called the login command? ")
184+
}
185+
186+
if requireToken {
187+
// check there is a token to use for auth
188+
fmeserverToken := viper.GetString("token")
189+
if fmeserverToken == "" {
190+
return fmt.Errorf("no token found in config file " + viper.ConfigFileUsed() + ". Have you called the login command? ")
191+
}
192+
}
193+
194+
// check there is a build set in the config file
195+
fmeserverBuild := viper.GetString("build")
196+
if fmeserverBuild == "" {
197+
return fmt.Errorf("no build found in config file " + viper.ConfigFileUsed() + ". Have you called the login command? ")
198+
}
199+
return nil
200+
201+
}

cmd/healthcheck.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88
"net/http"
9+
"net/url"
910
"os"
1011
"strings"
1112

@@ -15,6 +16,7 @@ import (
1516

1617
type healthcheckFlags struct {
1718
ready bool
19+
url string
1820
outputType string
1921
noHeaders bool
2022
apiVersion apiVersionFlag
@@ -37,7 +39,7 @@ func newHealthcheckCmd() *cobra.Command {
3739
cmd := &cobra.Command{
3840
Use: "healthcheck",
3941
Short: "Retrieves the health status of FME Server",
40-
Long: "Retrieves the health status of FME Server. The health status is normal if the FME Server REST API is responsive. Note that this endpoint does not require authentication. Load balancer or other systems can monitor FME Server using this endpoint without supplying token or password credentials.",
42+
Long: "Retrieves the health status of FME Server. The health status is normal if the FME Server REST API is responsive. Note that this endpoint does not require authentication. This command can be used without calling the login command first. The FME Server url can be passed in using the --url flag without needing a config file. A config file without a token can also be used.",
4143
Example: `
4244
# Check if the FME Server is healthy and accepting requests
4345
fmeserver healthcheck
@@ -49,11 +51,38 @@ func newHealthcheckCmd() *cobra.Command {
4951
fmeserver healthcheck --json
5052
5153
# Check that the FME Server is healthy and output just the status
52-
fmeserver healthcheck --output=custom-columns=STATUS:.status`,
54+
fmeserver healthcheck --output=custom-columns=STATUS:.status
55+
56+
# Check the FME Server is healthy without needing a config file
57+
fmeserver healthcheck --url https://my-fmeserver.internal
58+
59+
# Check the FME Server is healthy with a manually created config file
60+
cat << EOF >fmeserver-cli.yaml
61+
build: 23235
62+
url: https://my-fmeserver.internal
63+
EOF
64+
fmeserver healthcheck --config fmeserver-cli.yaml`,
5365
Args: NoArgs,
66+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
67+
// only check config if we didn't specify a url
68+
if f.url == "" {
69+
return checkConfigFile(false)
70+
} else {
71+
var err error
72+
url, err := url.ParseRequestURI(f.url)
73+
if err != nil {
74+
return fmt.Errorf(urlErrorMsg)
75+
}
76+
if url.Path != "" {
77+
return fmt.Errorf(urlErrorMsg)
78+
}
79+
}
80+
return nil
81+
},
5482
RunE: healthcheckRun(&f),
5583
}
5684
cmd.Flags().BoolVar(&f.ready, "ready", false, "The health check will report the status of FME Server if it is ready to process jobs.")
85+
cmd.Flags().StringVar(&f.url, "url", "", "The base URL of the FME Server to check the health of. Pass this in if checking the health of an FME Server that you haven't called the login command for.")
5786
cmd.Flags().Var(&f.apiVersion, "api-version", "The api version to use when contacting FME Server. Must be one of v3 or v4")
5887
cmd.Flags().StringVarP(&f.outputType, "output", "o", "table", "Specify the output type. Should be one of table, json, or custom-columns")
5988
cmd.Flags().BoolVar(&f.noHeaders, "no-headers", false, "Don't print column headers")
@@ -74,6 +103,7 @@ func healthcheckRun(f *healthcheckFlags) func(cmd *cobra.Command, args []string)
74103

75104
// get build to decide if we should use v3 or v4
76105
// FME Server 2023.0 and later can use v4. Otherwise fall back to v3
106+
// If called without a config file and thus no build number, default to v3
77107
if f.apiVersion == "" {
78108
fmeserverBuild := viper.GetInt("build")
79109
if fmeserverBuild < healthcheckV4BuildThreshold {
@@ -92,6 +122,10 @@ func healthcheckRun(f *healthcheckFlags) func(cmd *cobra.Command, args []string)
92122
endpoint += "/liveness"
93123
}
94124

125+
if f.url != "" {
126+
viper.Set("url", f.url)
127+
}
128+
95129
request, err := buildFmeServerRequest(endpoint, "GET", nil)
96130
if err != nil {
97131
return err
@@ -169,7 +203,12 @@ func healthcheckRun(f *healthcheckFlags) func(cmd *cobra.Command, args []string)
169203
endpoint += "?ready=true"
170204
}
171205

206+
if f.url != "" {
207+
viper.Set("url", f.url)
208+
}
209+
172210
request, err := buildFmeServerRequest(endpoint, "GET", nil)
211+
173212
if err != nil {
174213
return err
175214
}

cmd/healthcheck_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,22 @@ func TestHealthcheck(t *testing.T) {
104104
wantOutputJson: okResponseV3,
105105
args: []string{"healthcheck", "--json", "--api-version", "v3"},
106106
},
107+
{
108+
name: "v4 health check with url flag",
109+
statusCode: http.StatusOK,
110+
body: okResponseV4,
111+
wantOutputRegex: "STATUS[\\s]*MESSAGE[\\s]*[\\s]*ok[\\s]*FME Server is healthy",
112+
args: []string{"healthcheck", "--url", urlPlaceholder},
113+
omitConfig: true,
114+
},
115+
{
116+
name: "v4 health check with no token in config file",
117+
statusCode: http.StatusOK,
118+
body: okResponseV4,
119+
wantOutputRegex: "STATUS[\\s]*MESSAGE[\\s]*[\\s]*ok[\\s]*FME Server is healthy",
120+
args: []string{"healthcheck"},
121+
omitConfigToken: true,
122+
},
107123
}
108124
runTests(cases, t)
109125
}

cmd/login.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ type loginFlags struct {
5050
expiration int
5151
}
5252

53+
var urlErrorMsg = "invalid FME Server URL specified. URL should be of the form https://myfmeserverhostname.com"
54+
5355
func newLoginCmd() *cobra.Command {
5456
f := loginFlags{}
5557
cmd := &cobra.Command{
@@ -80,7 +82,7 @@ func newLoginCmd() *cobra.Command {
8082
cmd.Usage()
8183
return fmt.Errorf("accepts at most 1 argument, received %d", len(args))
8284
}
83-
urlErrorMsg := "invalid FME Server URL specified. URL should be of the form https://myfmeserverhostname.com"
85+
8486
url, err := url.ParseRequestURI(args[0])
8587
if err != nil {
8688
return fmt.Errorf(urlErrorMsg)

cmd/login_test.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import (
1111
"github.com/stretchr/testify/require"
1212
)
1313

14-
var testURL = "https://myfmeserver.example.com"
15-
1614
func TestLogin(t *testing.T) {
1715
tokenResponse := `{
1816
"lastSaveDate": "2022-11-17T19:30:44Z",
@@ -71,20 +69,20 @@ func TestLogin(t *testing.T) {
7169
{
7270
name: "unknown flag",
7371
statusCode: http.StatusOK,
74-
args: []string{"login", testURL, "--badflag"},
72+
args: []string{"login", urlPlaceholder, "--badflag"},
7573
wantErrOutputRegex: "unknown flag: --badflag",
7674
},
7775
{
7876
name: "500 bad status code",
7977
statusCode: http.StatusInternalServerError,
8078
wantErrText: "500 Internal Server Error",
81-
args: []string{"login", testURL, "--user", "admin", "--password-file", passwordFile.Name()},
79+
args: []string{"login", urlPlaceholder, "--user", "admin", "--password-file", passwordFile.Name()},
8280
},
8381
{
8482
name: "422 bad status code",
8583
statusCode: http.StatusNotFound,
8684
wantErrText: "404 Not Found",
87-
args: []string{"login", testURL, "--user", "admin", "--password-file", passwordFile.Name()},
85+
args: []string{"login", urlPlaceholder, "--user", "admin", "--password-file", passwordFile.Name()},
8886
},
8987
{
9088
name: "login with user and password",
@@ -117,19 +115,19 @@ url: %s
117115
{
118116
name: "missing password flag",
119117
statusCode: http.StatusOK,
120-
args: []string{"login", testURL, "--user", "admin"},
118+
args: []string{"login", urlPlaceholder, "--user", "admin"},
121119
wantErrText: "if any flags in the group [user password-file] are set they must all be set; missing [password-file]",
122120
},
123121
{
124122
name: "missing user flag",
125123
statusCode: http.StatusOK,
126-
args: []string{"login", testURL, "--password-file", passwordFile.Name()},
124+
args: []string{"login", urlPlaceholder, "--password-file", passwordFile.Name()},
127125
wantErrText: "if any flags in the group [user password-file] are set they must all be set; missing [user]",
128126
},
129127
{
130128
name: "token and password mutually exclusive",
131129
statusCode: http.StatusOK,
132-
args: []string{"login", testURL, "--user", "admin", "--password-file", passwordFile.Name(), "--token", "5ba5e0fd15c2403bc8b2e3aa1dfb975ca2197fbf"},
130+
args: []string{"login", urlPlaceholder, "--user", "admin", "--password-file", passwordFile.Name(), "--token", "5ba5e0fd15c2403bc8b2e3aa1dfb975ca2197fbf"},
133131
wantErrText: "if any flags in the group [token password-file] are set none of the others can be; [password-file token] were all set",
134132
},
135133
}

cmd/root.go

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"errors"
66
"fmt"
77
"net/http"
8-
"net/url"
98
"os"
109
"path/filepath"
1110

@@ -36,32 +35,7 @@ func NewRootCommand() *cobra.Command {
3635
SilenceUsage: true,
3736
DisableAutoGenTag: true,
3837
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
39-
// make sure the config file is set up correctly
40-
_, err := os.Stat(viper.ConfigFileUsed())
41-
if err != nil {
42-
return fmt.Errorf("could not open the config file " + viper.ConfigFileUsed() + ". Have you called the login command? ")
43-
}
44-
fmeserverUrl := viper.GetString("url")
45-
46-
// check the fme server URL is valid
47-
_, err = url.ParseRequestURI(fmeserverUrl)
48-
if err != nil {
49-
return fmt.Errorf("invalid FME Server url in config file " + viper.ConfigFileUsed() + ". Have you called the login command? ")
50-
}
51-
52-
// check there is a token to use for auth
53-
fmeserverToken := viper.GetString("token")
54-
if fmeserverToken == "" {
55-
return fmt.Errorf("no token found in config file " + viper.ConfigFileUsed() + ". Have you called the login command? ")
56-
}
57-
58-
// check there is a build set in the config file
59-
fmeserverBuild := viper.GetString("build")
60-
if fmeserverBuild == "" {
61-
return fmt.Errorf("no build found in config file " + viper.ConfigFileUsed() + ". Have you called the login command? ")
62-
}
63-
64-
return nil
38+
return checkConfigFile(true)
6539
},
6640
}
6741
cmds.ResetFlags()

cmd/testFunctions.go

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http/httptest"
88
"os"
99
"regexp"
10+
"strings"
1011
"testing"
1112

1213
"github.com/spf13/viper"
@@ -24,21 +25,25 @@ type testCase struct {
2425
body string // the body of the request that the test server should return
2526
wantErrText string // the expected text in the error object to be returned
2627
wantOutputRegex string // regex of the expected stdout to be returned
27-
wantOutputJson string // regex of the expected stdout to be returned
28+
wantOutputJson string // the expected json to be returned
2829
wantErrOutputRegex string // regex of the expected stderr to be returned
2930
wantFormParams map[string]string // array to ensure that all required URL form parameters exist
3031
wantFormParamsList map[string][]string // for URL forms with multiple values
3132
wantFileContents fileContents // check file contents
3233
wantBodyRegEx string // check the contents of the body sent
3334
fmeserverBuild int // build to pretend we are contacting
3435
args []string // flags to pass into the command
35-
36-
httpServer *httptest.Server // custom http test server if needed
36+
httpServer *httptest.Server // custom http test server if needed
37+
omitConfig bool // set this to true if testing a command with no config file set up
38+
omitConfigToken bool // set this to true if testing a command that reads from the config file but doesn't require a token
3739
}
3840

3941
// random token to use for testing
4042
var testToken = "57463e1b143db046ef3f4ae8ba1b0233e32ee9dd"
4143

44+
// string for when we need the test http server in a command
45+
var urlPlaceholder = "[url]"
46+
4247
func runTests(tcs []testCase, t *testing.T) {
4348
for _, tc := range tcs {
4449
tc := tc
@@ -68,13 +73,17 @@ func runTests(tcs []testCase, t *testing.T) {
6873

6974
defer tc.httpServer.Close()
7075

71-
// set up the config file
72-
viper.Set("url", tc.httpServer.URL)
73-
viper.Set("token", testToken)
74-
if tc.fmeserverBuild != 0 {
75-
viper.Set("build", tc.fmeserverBuild)
76-
} else {
77-
viper.Set("build", 23159)
76+
if !tc.omitConfig {
77+
// set up the config file
78+
viper.Set("url", tc.httpServer.URL)
79+
if !tc.omitConfigToken {
80+
viper.Set("token", testToken)
81+
}
82+
if tc.fmeserverBuild != 0 {
83+
viper.Set("build", tc.fmeserverBuild)
84+
} else {
85+
viper.Set("build", 23159)
86+
}
7887
}
7988

8089
// create a new copy of the command for each test
@@ -86,10 +95,9 @@ func runTests(tcs []testCase, t *testing.T) {
8695
cmd.SetOut(stdOut)
8796
cmd.SetErr(stdErr)
8897

89-
// a bit of a hack to make login work
90-
// requires that URL is passed in first in testing before flags
91-
if tc.args[0] == "login" {
92-
tc.args[1] = tc.httpServer.URL
98+
// a bit of a hack to make login work as it needs the URL of the test server
99+
for i, s := range tc.args {
100+
tc.args[i] = strings.Replace(s, urlPlaceholder, tc.httpServer.URL, -1)
93101
}
94102

95103
// if a config file isn't specified, generate a random file and set the config file flag

0 commit comments

Comments
 (0)