From 983c9d2d8e0c716e8b3cb10a66091fb6556549a2 Mon Sep 17 00:00:00 2001 From: Grant Arnold Date: Fri, 24 Oct 2025 16:57:55 +0000 Subject: [PATCH 1/5] Update machinekey endpoint to use v4 --- cmd/machinekey.go | 41 +++++++++++++++++++++++++--- cmd/machinekey_test.go | 62 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 88 insertions(+), 15 deletions(-) diff --git a/cmd/machinekey.go b/cmd/machinekey.go index 34c54e0..6a6cd4d 100644 --- a/cmd/machinekey.go +++ b/cmd/machinekey.go @@ -8,29 +8,62 @@ import ( "net/http" "github.com/spf13/cobra" + "github.com/spf13/viper" ) type MachineKey struct { MachineKey string `json:"machineKey"` } +type machineKeyFlags struct { + apiVersion apiVersionFlag +} + +var machineKeyV4BuildThreshold = 23319 + func newMachineKeyCmd() *cobra.Command { - return &cobra.Command{ + f := machineKeyFlags{} + cmd := &cobra.Command{ Use: "machinekey", Short: "Retrieves machine key of the machine running FME Flow.", Long: `Retrieves machine key of the machine running FME Flow.`, Args: NoArgs, - RunE: machineKeyRun(), + RunE: machineKeyRun(&f), } + + 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) + + return cmd } -func machineKeyRun() func(cmd *cobra.Command, args []string) error { +func machineKeyRun(f *machineKeyFlags) 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 2023.0+ and later can use v4. Otherwise fall back to v3 + if f.apiVersion == "" { + fmeflowBuild := viper.GetInt("build") + if fmeflowBuild < machineKeyV4BuildThreshold { + f.apiVersion = apiVersionFlagV3 + } else { + f.apiVersion = apiVersionFlagV4 + } + } + + // v3 and v4 work exactly the same, so we can just change the endpoint + var endpoint string + if f.apiVersion == "v4" { + endpoint = "/fmeapiv4/license/machinekey" + } else { + endpoint = "/fmerest/v3/licensing/machinekey" + } + // call the status endpoint to see if it is finished - request, err := buildFmeFlowRequest("/fmerest/v3/licensing/machinekey", "GET", nil) + request, err := buildFmeFlowRequest(endpoint, "GET", nil) if err != nil { return err } diff --git a/cmd/machinekey_test.go b/cmd/machinekey_test.go index 39963d2..63b3fd2 100644 --- a/cmd/machinekey_test.go +++ b/cmd/machinekey_test.go @@ -6,8 +6,8 @@ import ( ) func TestMachineKey(t *testing.T) { - // standard responses for v3 - responseV3 := `{ + // standard response for both v3 and v4 (both use the same JSON format) + response := `{ "machineKey": "3096247551" }` @@ -19,30 +19,70 @@ func TestMachineKey(t *testing.T) { wantErrOutputRegex: "unknown flag: --badflag", }, { - name: "500 bad status code", + name: "500 bad status code v3", statusCode: http.StatusInternalServerError, wantErrText: "500 Internal Server Error", - args: []string{"license", "machinekey"}, + args: []string{"license", "machinekey", "--api-version", "v3"}, }, { - name: "404 bad status code", + name: "404 bad status code v3", statusCode: http.StatusNotFound, wantErrText: "404 Not Found", - args: []string{"license", "machinekey"}, + args: []string{"license", "machinekey", "--api-version", "v3"}, }, { - name: "get license machinekey", + name: "get license machinekey v3", + statusCode: http.StatusOK, + args: []string{"license", "machinekey", "--api-version", "v3"}, + wantOutputRegex: "^3096247551[\\s]*$", + body: response, + }, + { + name: "get license machinekey json v3", + statusCode: http.StatusOK, + args: []string{"license", "machinekey", "--json", "--api-version", "v3"}, + body: response, + wantOutputJson: response, + }, + { + name: "500 bad status code v4", + statusCode: http.StatusInternalServerError, + wantErrText: "500 Internal Server Error", + args: []string{"license", "machinekey", "--api-version", "v4"}, + }, + { + name: "404 bad status code v4", + statusCode: http.StatusNotFound, + wantErrText: "404 Not Found", + args: []string{"license", "machinekey", "--api-version", "v4"}, + }, + { + name: "get license machinekey v4", + statusCode: http.StatusOK, + args: []string{"license", "machinekey", "--api-version", "v4"}, + wantOutputRegex: "^3096247551[\\s]*$", + body: response, + }, + { + name: "get license machinekey json v4", + statusCode: http.StatusOK, + args: []string{"license", "machinekey", "--json", "--api-version", "v4"}, + body: response, + wantOutputJson: response, + }, + { + name: "get license machinekey (no explicit version)", statusCode: http.StatusOK, args: []string{"license", "machinekey"}, wantOutputRegex: "^3096247551[\\s]*$", - body: responseV3, + body: response, }, { - name: "get license machinekey json", + name: "get license machinekey json (no explicit version)", statusCode: http.StatusOK, args: []string{"license", "machinekey", "--json"}, - body: responseV3, - wantOutputJson: responseV3, + body: response, + wantOutputJson: response, }, } From b1b9b57ef963f0aa9bb61a72d575060f02caf582 Mon Sep 17 00:00:00 2001 From: Grant Arnold Date: Fri, 24 Oct 2025 18:06:38 +0000 Subject: [PATCH 2/5] Update license refresh to v4. --- cmd/refresh.go | 43 +++++++++++++++++--- cmd/refreshStatus.go | 25 +++++++++++- cmd/refreshStatus_test.go | 84 +++++++++++++++++++++++++++++++++++---- cmd/refresh_test.go | 62 +++++++++++++++++++++++++---- 4 files changed, 193 insertions(+), 21 deletions(-) diff --git a/cmd/refresh.go b/cmd/refresh.go index 378fed7..fe6ed86 100644 --- a/cmd/refresh.go +++ b/cmd/refresh.go @@ -10,6 +10,7 @@ import ( "time" "github.com/spf13/cobra" + "github.com/spf13/viper" ) type RefreshStatus struct { @@ -18,9 +19,12 @@ type RefreshStatus struct { } type refreshFlags struct { - wait bool + wait bool + apiVersion apiVersionFlag } +var refreshV4BuildThreshold = 23319 + func newRefreshCmd() *cobra.Command { f := refreshFlags{} cmd := &cobra.Command{ @@ -34,6 +38,9 @@ func newRefreshCmd() *cobra.Command { RunE: refreshRun(&f), } cmd.Flags().BoolVar(&f.wait, "wait", false, "Wait for licensing refresh to finish") + 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.AddCommand(newRefreshStatusCmd()) return cmd @@ -45,7 +52,27 @@ func refreshRun(f *refreshFlags) func(cmd *cobra.Command, args []string) error { client := &http.Client{} http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - request, err := buildFmeFlowRequest("/fmerest/v3/licensing/refresh", "POST", nil) + // get build to decide if we should use v3 or v4 + // FME Server 2023.0+ and later can use v4. Otherwise fall back to v3 + if f.apiVersion == "" { + fmeflowBuild := viper.GetInt("build") + if fmeflowBuild < refreshV4BuildThreshold { + f.apiVersion = apiVersionFlagV3 + } else { + f.apiVersion = apiVersionFlagV4 + } + } + + var refreshEndpoint, statusEndpoint string + if f.apiVersion == "v4" { + refreshEndpoint = "/fmeapiv4/license/refresh" + statusEndpoint = "/fmeapiv4/license/refresh/status" + } else { + refreshEndpoint = "/fmerest/v3/licensing/refresh" + statusEndpoint = "/fmerest/v3/licensing/refresh/status" + } + + request, err := buildFmeFlowRequest(refreshEndpoint, "POST", nil) if err != nil { return err } @@ -68,7 +95,7 @@ func refreshRun(f *refreshFlags) func(cmd *cobra.Command, args []string) error { fmt.Print(".") time.Sleep(1 * time.Second) // call the status endpoint to see if it is finished - request, err := buildFmeFlowRequest("/fmerest/v3/licensing/refresh/status", "GET", nil) + request, err := buildFmeFlowRequest(statusEndpoint, "GET", nil) if err != nil { return err } @@ -85,9 +112,13 @@ func refreshRun(f *refreshFlags) func(cmd *cobra.Command, args []string) error { var result RefreshStatus if err := json.Unmarshal(responseData, &result); err != nil { return err - } else if result.Status != "REQUESTING" { - complete = true - fmt.Fprintln(cmd.OutOrStdout(), result.Message) + } else { + // v3 uses uppercase "REQUESTING", v4 uses lowercase "requesting" + isRequesting := result.Status == "REQUESTING" || result.Status == "requesting" + if !isRequesting { + complete = true + fmt.Fprintln(cmd.OutOrStdout(), result.Message) + } } if complete { diff --git a/cmd/refreshStatus.go b/cmd/refreshStatus.go index b488ef4..e03b36a 100644 --- a/cmd/refreshStatus.go +++ b/cmd/refreshStatus.go @@ -9,11 +9,13 @@ import ( "strings" "github.com/spf13/cobra" + "github.com/spf13/viper" ) type licenseRefreshStatusFlags struct { outputType string noHeaders bool + apiVersion apiVersionFlag } func newRefreshStatusCmd() *cobra.Command { @@ -36,6 +38,9 @@ func newRefreshStatusCmd() *cobra.Command { } cmd.Flags().StringVarP(&f.outputType, "output", "o", "table", "Specify the output type. Should be one of table, json, or custom-columns") cmd.Flags().BoolVar(&f.noHeaders, "no-headers", false, "Don't print column headers") + 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) return cmd } @@ -47,11 +52,29 @@ func refreshStatusRun(f *licenseRefreshStatusFlags) func(cmd *cobra.Command, arg f.outputType = "json" } + // get build to decide if we should use v3 or v4 + // FME Server 2023.0+ and later can use v4. Otherwise fall back to v3 + if f.apiVersion == "" { + fmeflowBuild := viper.GetInt("build") + if fmeflowBuild < refreshV4BuildThreshold { + f.apiVersion = apiVersionFlagV3 + } else { + f.apiVersion = apiVersionFlagV4 + } + } + + var statusEndpoint string + if f.apiVersion == "v4" { + statusEndpoint = "/fmeapiv4/license/refresh/status" + } else { + statusEndpoint = "/fmerest/v3/licensing/refresh/status" + } + // set up http client := &http.Client{} // call the status endpoint to see if it is finished - request, err := buildFmeFlowRequest("/fmerest/v3/licensing/refresh/status", "GET", nil) + request, err := buildFmeFlowRequest(statusEndpoint, "GET", nil) if err != nil { return err } diff --git a/cmd/refreshStatus_test.go b/cmd/refreshStatus_test.go index 3e6bd11..7d1edc3 100644 --- a/cmd/refreshStatus_test.go +++ b/cmd/refreshStatus_test.go @@ -6,12 +6,28 @@ import ( ) func TestRefreshStatus(t *testing.T) { - // standard responses for v3 + // standard responses for v3 (uppercase status) statusV3 := `{ "message":"License refresh completed. License file was updated.", "status":"SUCCESS" }` + // standard responses for v4 (lowercase status) + statusV4 := `{ + "message":"License refresh completed. License file was updated.", + "status":"success" + }` + + requestingV3 := `{ + "message":"License refresh in progress.", + "status":"REQUESTING" + }` + + requestingV4 := `{ + "message":"License refresh in progress.", + "status":"requesting" + }` + cases := []testCase{ { name: "unknown flag", @@ -20,26 +36,80 @@ func TestRefreshStatus(t *testing.T) { wantErrOutputRegex: "unknown flag: --badflag", }, { - name: "500 bad status code", + name: "500 bad status code v3", statusCode: http.StatusInternalServerError, wantErrText: "500 Internal Server Error", - args: []string{"license", "refresh", "status"}, + args: []string{"license", "refresh", "status", "--api-version", "v3"}, }, { - name: "404 bad status code", + name: "404 bad status code v3", statusCode: http.StatusNotFound, wantErrText: "404 Not Found", - args: []string{"license", "refresh", "status"}, + args: []string{"license", "refresh", "status", "--api-version", "v3"}, + }, + { + name: "get refresh status v3", + statusCode: http.StatusOK, + args: []string{"license", "refresh", "status", "--api-version", "v3"}, + wantOutputRegex: "^[\\s]*STATUS[\\s]*MESSAGE[\\s]*SUCCESS[\\s]*License refresh completed. License file was updated.[\\s]*$", + body: statusV3, + }, + { + name: "get refresh status json v3", + statusCode: http.StatusOK, + body: statusV3, + args: []string{"license", "refresh", "status", "--json", "--api-version", "v3"}, + wantOutputJson: statusV3, + }, + { + name: "get refresh requesting status v3", + statusCode: http.StatusOK, + args: []string{"license", "refresh", "status", "--api-version", "v3"}, + wantOutputRegex: "^[\\s]*STATUS[\\s]*MESSAGE[\\s]*REQUESTING[\\s]*License refresh in progress.[\\s]*$", + body: requestingV3, + }, + { + name: "500 bad status code v4", + statusCode: http.StatusInternalServerError, + wantErrText: "500 Internal Server Error", + args: []string{"license", "refresh", "status", "--api-version", "v4"}, + }, + { + name: "404 bad status code v4", + statusCode: http.StatusNotFound, + wantErrText: "404 Not Found", + args: []string{"license", "refresh", "status", "--api-version", "v4"}, + }, + { + name: "get refresh status v4", + statusCode: http.StatusOK, + args: []string{"license", "refresh", "status", "--api-version", "v4"}, + wantOutputRegex: "^[\\s]*STATUS[\\s]*MESSAGE[\\s]*success[\\s]*License refresh completed. License file was updated.[\\s]*$", + body: statusV4, + }, + { + name: "get refresh status json v4", + statusCode: http.StatusOK, + body: statusV4, + args: []string{"license", "refresh", "status", "--json", "--api-version", "v4"}, + wantOutputJson: statusV4, + }, + { + name: "get refresh requesting status v4", + statusCode: http.StatusOK, + args: []string{"license", "refresh", "status", "--api-version", "v4"}, + wantOutputRegex: "^[\\s]*STATUS[\\s]*MESSAGE[\\s]*requesting[\\s]*License refresh in progress.[\\s]*$", + body: requestingV4, }, { - name: "get refresh status", + name: "get refresh status (no explicit version)", statusCode: http.StatusOK, args: []string{"license", "refresh", "status"}, wantOutputRegex: "^[\\s]*STATUS[\\s]*MESSAGE[\\s]*SUCCESS[\\s]*License refresh completed. License file was updated.[\\s]*$", body: statusV3, }, { - name: "get refresh status json", + name: "get refresh status json (no explicit version)", statusCode: http.StatusOK, body: statusV3, args: []string{"license", "refresh", "status", "--json"}, diff --git a/cmd/refresh_test.go b/cmd/refresh_test.go index a03b575..810771a 100644 --- a/cmd/refresh_test.go +++ b/cmd/refresh_test.go @@ -6,10 +6,18 @@ import ( ) func TestLicenseRefresh(t *testing.T) { + // v3 responses (uppercase status) statusV3 := `{ "message":"License refresh completed. License file was updated.", "status":"SUCCESS" }` + + // v4 responses (lowercase status) + statusV4 := `{ + "message":"License refresh completed. License file was updated.", + "status":"success" + }` + cases := []testCase{ { name: "unknown flag", @@ -18,30 +26,70 @@ func TestLicenseRefresh(t *testing.T) { wantErrOutputRegex: "unknown flag: --badflag", }, { - name: "500 bad status code", + name: "500 bad status code v3", + statusCode: http.StatusInternalServerError, + wantErrText: "500 Internal Server Error", + args: []string{"license", "refresh", "--api-version", "v3"}, + }, + { + name: "404 bad status code v3", + statusCode: http.StatusNotFound, + wantErrText: "404 Not Found", + args: []string{"license", "refresh", "--api-version", "v3"}, + }, + { + name: "refresh license v3", + statusCode: http.StatusAccepted, + args: []string{"license", "refresh", "--api-version", "v3"}, + wantOutputRegex: "^License Refresh Successfully sent\\.[\\s]*$", + body: "", + }, + { + name: "refresh license wait v3", + statusCode: http.StatusAccepted, + args: []string{"license", "refresh", "--wait", "--api-version", "v3"}, + wantOutputRegex: "^License Refresh Successfully sent\\.[\\s]*License refresh completed. License file was updated.[\\s]*$", + body: statusV3, + }, + { + name: "500 bad status code v4", statusCode: http.StatusInternalServerError, wantErrText: "500 Internal Server Error", - args: []string{"license", "refresh"}, + args: []string{"license", "refresh", "--api-version", "v4"}, }, { - name: "404 bad status code", + name: "404 bad status code v4", statusCode: http.StatusNotFound, wantErrText: "404 Not Found", - args: []string{"license", "refresh"}, + args: []string{"license", "refresh", "--api-version", "v4"}, + }, + { + name: "refresh license v4", + statusCode: http.StatusAccepted, + args: []string{"license", "refresh", "--api-version", "v4"}, + wantOutputRegex: "^License Refresh Successfully sent\\.[\\s]*$", + body: "", + }, + { + name: "refresh license wait v4", + statusCode: http.StatusAccepted, + args: []string{"license", "refresh", "--wait", "--api-version", "v4"}, + wantOutputRegex: "^License Refresh Successfully sent\\.[\\s]*License refresh completed. License file was updated.[\\s]*$", + body: statusV4, }, { - name: "refresh license", + name: "refresh license (no explicit version)", statusCode: http.StatusAccepted, args: []string{"license", "refresh"}, wantOutputRegex: "^License Refresh Successfully sent\\.[\\s]*$", body: "", }, { - name: "refresh license wait", + name: "refresh license wait (no explicit version)", statusCode: http.StatusAccepted, args: []string{"license", "refresh", "--wait"}, wantOutputRegex: "^License Refresh Successfully sent\\.[\\s]*License refresh completed. License file was updated.[\\s]*$", - body: statusV3, + body: statusV4, }, } From 23ca4ee2f1a204ef4c26cc071a30536e5ac064af Mon Sep 17 00:00:00 2001 From: Grant Arnold Date: Fri, 24 Oct 2025 18:42:09 +0000 Subject: [PATCH 3/5] Implement license status in v4 --- cmd/licenseStatus.go | 97 ++++++++++++++++++++++++++++++++++---- cmd/licenseStatus_test.go | 99 +++++++++++++++++++++++++++++++++------ 2 files changed, 173 insertions(+), 23 deletions(-) diff --git a/cmd/licenseStatus.go b/cmd/licenseStatus.go index daefa02..61249b4 100644 --- a/cmd/licenseStatus.go +++ b/cmd/licenseStatus.go @@ -9,9 +9,10 @@ import ( "strings" "github.com/spf13/cobra" + "github.com/spf13/viper" ) -type LicenseStatus struct { +type LicenseStatusV3 struct { ExpiryDate string `json:"expiryDate"` MaximumEngines int `json:"maximumEngines"` SerialNumber string `json:"serialNumber"` @@ -20,11 +21,23 @@ type LicenseStatus struct { MaximumAuthors int `json:"maximumAuthors"` } +type LicenseStatusV4 struct { + Licensed bool `json:"licensed"` + Expiration string `json:"expiration"` + MaximumEngines int `json:"maximumEngines"` + Expired bool `json:"expired"` + SerialNumber string `json:"serialNumber"` + MaximumAuthors int `json:"maximumAuthors"` +} + type licenseStatusFlags struct { outputType string noHeaders bool + apiVersion apiVersionFlag } +var licenseStatusV4BuildThreshold = 23319 + func newLicenseStatusCmd() *cobra.Command { f := licenseStatusFlags{} cmd := &cobra.Command{ @@ -36,6 +49,9 @@ func newLicenseStatusCmd() *cobra.Command { } cmd.Flags().StringVarP(&f.outputType, "output", "o", "table", "Specify the output type. Should be one of table, json, or custom-columns") cmd.Flags().BoolVar(&f.noHeaders, "no-headers", false, "Don't print column headers") + 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) return cmd } @@ -45,11 +61,30 @@ func licenseStatusRun(f *licenseStatusFlags) func(cmd *cobra.Command, args []str if jsonOutput { f.outputType = "json" } + + // get build to decide if we should use v3 or v4 + // FME Server 2023.0+ and later can use v4. Otherwise fall back to v3 + if f.apiVersion == "" { + fmeflowBuild := viper.GetInt("build") + if fmeflowBuild < licenseStatusV4BuildThreshold { + f.apiVersion = apiVersionFlagV3 + } else { + f.apiVersion = apiVersionFlagV4 + } + } + + var endpoint string + if f.apiVersion == "v4" { + endpoint = "/fmeapiv4/license/status" + } else { + endpoint = "/fmerest/v3/licensing/license/status" + } + // set up http client := &http.Client{} // call the status endpoint to see if it is finished - request, err := buildFmeFlowRequest("/fmerest/v3/licensing/license/status", "GET", nil) + request, err := buildFmeFlowRequest(endpoint, "GET", nil) if err != nil { return err } @@ -65,10 +100,12 @@ func licenseStatusRun(f *licenseStatusFlags) func(cmd *cobra.Command, args []str return err } - var result LicenseStatus - if err := json.Unmarshal(responseData, &result); err != nil { - return err - } else { + if f.apiVersion == "v4" { + var result LicenseStatusV4 + if err := json.Unmarshal(responseData, &result); err != nil { + return err + } + if f.outputType == "table" { // output all values returned by the JSON in a table t := createTableWithDefaultColumns(result) @@ -93,8 +130,6 @@ func licenseStatusRun(f *licenseStatusFlags) func(cmd *cobra.Command, args []str return errors.New("custom-columns format specified but no custom columns given") } - // we have to marshal the Items array, then create an array of marshalled items - // to pass to the creation of the table. marshalledItems := [][]byte{} mJson, err := json.Marshal(result) if err != nil { @@ -112,13 +147,57 @@ func licenseStatusRun(f *licenseStatusFlags) func(cmd *cobra.Command, args []str } fmt.Fprintln(cmd.OutOrStdout(), t.Render()) } else { + return errors.New("invalid output format specified") + } + } else { + var result LicenseStatusV3 + if err := json.Unmarshal(responseData, &result); err != nil { + return err + } + + if f.outputType == "table" { + // output all values returned by the JSON in a table + t := createTableWithDefaultColumns(result) + + if f.noHeaders { + t.ResetHeaders() + } + fmt.Fprintln(cmd.OutOrStdout(), t.Render()) + } else if f.outputType == "json" { prettyJSON, err := prettyPrintJSON(responseData) if err != nil { return err } fmt.Fprintln(cmd.OutOrStdout(), prettyJSON) - } + } else if strings.HasPrefix(f.outputType, "custom-columns") { + // parse the columns and json queries + columnsString := "" + if strings.HasPrefix(f.outputType, "custom-columns=") { + columnsString = f.outputType[len("custom-columns="):] + } + if len(columnsString) == 0 { + return errors.New("custom-columns format specified but no custom columns given") + } + + marshalledItems := [][]byte{} + mJson, err := json.Marshal(result) + if err != nil { + return err + } + marshalledItems = append(marshalledItems, mJson) + columnsInput := strings.Split(columnsString, ",") + t, err := createTableFromCustomColumns(marshalledItems, columnsInput) + if err != nil { + return err + } + if f.noHeaders { + t.ResetHeaders() + } + fmt.Fprintln(cmd.OutOrStdout(), t.Render()) + } else { + return errors.New("invalid output format specified") + } } return nil diff --git a/cmd/licenseStatus_test.go b/cmd/licenseStatus_test.go index 5b06f4f..88bdcf5 100644 --- a/cmd/licenseStatus_test.go +++ b/cmd/licenseStatus_test.go @@ -16,6 +16,16 @@ func TestLicenseStatus(t *testing.T) { "maximumAuthors": 10 }` + // standard responses for v4 (different field names) + responseV4 := `{ + "licensed": true, + "expiration": "PERMANENT", + "maximumEngines": 10, + "expired": false, + "serialNumber": "AAAA-AAAA-AAAA", + "maximumAuthors": 10 + }` + cases := []testCase{ { name: "unknown flag", @@ -24,52 +34,113 @@ func TestLicenseStatus(t *testing.T) { wantErrOutputRegex: "unknown flag: --badflag", }, { - name: "500 bad status code", + name: "500 bad status code v3", statusCode: http.StatusInternalServerError, wantErrText: "500 Internal Server Error", - args: []string{"license", "status"}, + args: []string{"license", "status", "--api-version", "v3"}, }, { - name: "404 bad status code", + name: "404 bad status code v3", statusCode: http.StatusNotFound, wantErrText: "404 Not Found", - args: []string{"license", "status"}, + args: []string{"license", "status", "--api-version", "v3"}, }, { - name: "get license status table output", + name: "get license status table output v3", statusCode: http.StatusOK, - args: []string{"license", "status"}, + args: []string{"license", "status", "--api-version", "v3"}, wantOutputRegex: "^[\\s]*EXPIRY DATE[\\s]*MAXIMUM ENGINES[\\s]*SERIAL NUMBER[\\s]*IS LICENSE EXPIRED[\\s]*IS LICENSED[\\s]*MAXIMUM AUTHORS[\\s]*PERMANENT[\\s]*10[\\s]*AAAA-AAAA-AAAA[\\s]*false[\\s]*true[\\s]*10[\\s]*$", body: responseV3, }, { - name: "get license status no headers", + name: "get license status no headers v3", statusCode: http.StatusOK, body: responseV3, - args: []string{"license", "status", "--no-headers"}, + args: []string{"license", "status", "--no-headers", "--api-version", "v3"}, wantOutputRegex: "^[\\s]*PERMANENT[\\s]*10[\\s]*AAAA-AAAA-AAAA[\\s]*false[\\s]*true[\\s]*10[\\s]*$", }, { - name: "get license status json", + name: "get license status json v3", statusCode: http.StatusOK, - args: []string{"license", "status", "--json"}, + args: []string{"license", "status", "--json", "--api-version", "v3"}, body: responseV3, wantOutputJson: responseV3, }, { - name: "get license status json", + name: "get license status json output flag v3", statusCode: http.StatusOK, - args: []string{"license", "status", "--output=json"}, + args: []string{"license", "status", "--output=json", "--api-version", "v3"}, body: responseV3, wantOutputJson: responseV3, }, { - name: "get license status custom columns", + name: "get license status custom columns v3", statusCode: http.StatusOK, body: responseV3, - args: []string{"license", "status", "--output", "custom-columns=SERIAL:.serialNumber,LICENSED:.isLicensed"}, + args: []string{"license", "status", "--output", "custom-columns=SERIAL:.serialNumber,LICENSED:.isLicensed", "--api-version", "v3"}, wantOutputRegex: "^[\\s]*SERIAL[\\s]*LICENSED[\\s]*AAAA-AAAA-AAAA[\\s]*true[\\s]*$", }, + { + name: "500 bad status code v4", + statusCode: http.StatusInternalServerError, + wantErrText: "500 Internal Server Error", + args: []string{"license", "status", "--api-version", "v4"}, + }, + { + name: "404 bad status code v4", + statusCode: http.StatusNotFound, + wantErrText: "404 Not Found", + args: []string{"license", "status", "--api-version", "v4"}, + }, + { + name: "get license status table output v4", + statusCode: http.StatusOK, + args: []string{"license", "status", "--api-version", "v4"}, + wantOutputRegex: "^[\\s]*LICENSED[\\s]*EXPIRATION[\\s]*MAXIMUM ENGINES[\\s]*EXPIRED[\\s]*SERIAL NUMBER[\\s]*MAXIMUM AUTHORS[\\s]*true[\\s]*PERMANENT[\\s]*10[\\s]*false[\\s]*AAAA-AAAA-AAAA[\\s]*10[\\s]*$", + body: responseV4, + }, + { + name: "get license status no headers v4", + statusCode: http.StatusOK, + body: responseV4, + args: []string{"license", "status", "--no-headers", "--api-version", "v4"}, + wantOutputRegex: "^[\\s]*true[\\s]*PERMANENT[\\s]*10[\\s]*false[\\s]*AAAA-AAAA-AAAA[\\s]*10[\\s]*$", + }, + { + name: "get license status json v4", + statusCode: http.StatusOK, + args: []string{"license", "status", "--json", "--api-version", "v4"}, + body: responseV4, + wantOutputJson: responseV4, + }, + { + name: "get license status json output flag v4", + statusCode: http.StatusOK, + args: []string{"license", "status", "--output=json", "--api-version", "v4"}, + body: responseV4, + wantOutputJson: responseV4, + }, + { + name: "get license status custom columns v4", + statusCode: http.StatusOK, + body: responseV4, + args: []string{"license", "status", "--output", "custom-columns=SERIAL:.serialNumber,LICENSED:.licensed", "--api-version", "v4"}, + wantOutputRegex: "^[\\s]*SERIAL[\\s]*LICENSED[\\s]*AAAA-AAAA-AAAA[\\s]*true[\\s]*$", + }, + { + name: "get license status table output (no explicit version)", + statusCode: http.StatusOK, + args: []string{"license", "status"}, + wantOutputRegex: "^[\\s]*LICENSED[\\s]*EXPIRATION[\\s]*MAXIMUM ENGINES[\\s]*EXPIRED[\\s]*SERIAL NUMBER[\\s]*MAXIMUM AUTHORS[\\s]*true[\\s]*PERMANENT[\\s]*10[\\s]*false[\\s]*AAAA-AAAA-AAAA[\\s]*10[\\s]*$", + body: responseV4, + }, + { + name: "get license status json (no explicit version)", + statusCode: http.StatusOK, + args: []string{"license", "status", "--json"}, + body: responseV4, + wantOutputJson: responseV4, + }, } runTests(cases, t) From 6adb61daafc3b6bca2292aa405db2ee1d2783e1c Mon Sep 17 00:00:00 2001 From: Grant Arnold Date: Fri, 24 Oct 2025 22:32:31 +0000 Subject: [PATCH 4/5] Update systemcode command to not run on older builds. --- cmd/systemcode.go | 9 +++++++++ cmd/systemcode_test.go | 28 ++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/cmd/systemcode.go b/cmd/systemcode.go index 8e95f45..c193fbb 100644 --- a/cmd/systemcode.go +++ b/cmd/systemcode.go @@ -8,12 +8,15 @@ import ( "net/http" "github.com/spf13/cobra" + "github.com/spf13/viper" ) type SystemCode struct { SystemCode string `json:"systemCode"` } +var systemCodeDeprecatedBuildThreshold = 26000 + func newSystemCodeCmd() *cobra.Command { cmd := &cobra.Command{ Use: "systemcode", @@ -27,6 +30,12 @@ func newSystemCodeCmd() *cobra.Command { func systemCodeRun() func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { + // Check if systemcode is available in this version of FME Flow + fmeflowBuild := viper.GetInt("build") + if fmeflowBuild >= systemCodeDeprecatedBuildThreshold { + return errors.New("systemcode is not available in this version of FME Flow. The systemcode command was removed in FME Flow 2026.1+") + } + // set up http client := &http.Client{} diff --git a/cmd/systemcode_test.go b/cmd/systemcode_test.go index 79eee9e..bd27caf 100644 --- a/cmd/systemcode_test.go +++ b/cmd/systemcode_test.go @@ -17,18 +17,28 @@ func TestSystemCode(t *testing.T) { statusCode: http.StatusOK, args: []string{"license", "systemcode", "--badflag"}, wantErrOutputRegex: "unknown flag: --badflag", + fmeflowBuild: 25000, // Use older build that supports systemcode }, { - name: "500 bad status code", - statusCode: http.StatusInternalServerError, - wantErrText: "500 Internal Server Error", - args: []string{"license", "systemcode"}, + name: "systemcode not available in newer builds", + statusCode: http.StatusOK, + args: []string{"license", "systemcode"}, + wantErrText: "systemcode is not available in this version of FME Flow. The systemcode command was removed in FME Flow 2024.0+", + fmeflowBuild: 26000, // Use build >= 26000 to trigger deprecation error }, { - name: "404 bad status code", - statusCode: http.StatusNotFound, - wantErrText: "404 Not Found", - args: []string{"license", "systemcode"}, + name: "500 bad status code", + statusCode: http.StatusInternalServerError, + wantErrText: "500 Internal Server Error", + args: []string{"license", "systemcode"}, + fmeflowBuild: 25000, // Use older build that supports systemcode + }, + { + name: "404 bad status code", + statusCode: http.StatusNotFound, + wantErrText: "404 Not Found", + args: []string{"license", "systemcode"}, + fmeflowBuild: 25000, // Use older build that supports systemcode }, { name: "get license systemcode", @@ -36,6 +46,7 @@ func TestSystemCode(t *testing.T) { args: []string{"license", "systemcode"}, wantOutputRegex: "^fc1e6bdd-3ccd-4749-a9aa-7f4ef9039c06[\\s]*$", body: responseV3, + fmeflowBuild: 25000, // Use older build that supports systemcode }, { name: "get license systemcode json", @@ -43,6 +54,7 @@ func TestSystemCode(t *testing.T) { args: []string{"license", "systemcode", "--json"}, body: responseV3, wantOutputJson: responseV3, + fmeflowBuild: 25000, // Use older build that supports systemcode }, } From dc0422e590aceee64e20887ed94b0fded52d717a Mon Sep 17 00:00:00 2001 From: Grant Arnold Date: Sat, 25 Oct 2025 00:09:10 +0000 Subject: [PATCH 5/5] Update licensing request commands to v4. --- cmd/request.go | 317 ++++++++++++++++++++++++++++---------- cmd/requestStatus.go | 229 +++++++++++++++++++-------- cmd/requestStatus_test.go | 34 +++- cmd/request_test.go | 46 +++++- cmd/root.go | 5 +- cmd/systemcode_test.go | 2 +- cmd/testFunctions.go | 6 + 7 files changed, 476 insertions(+), 163 deletions(-) diff --git a/cmd/request.go b/cmd/request.go index 88d1307..2dbcdde 100644 --- a/cmd/request.go +++ b/cmd/request.go @@ -11,6 +11,7 @@ import ( "time" "github.com/spf13/cobra" + "github.com/spf13/viper" ) type licenseRequestFlags struct { @@ -24,13 +25,35 @@ type licenseRequestFlags struct { salesSource string subscribeToUpdates bool wait bool + apiVersion apiVersionFlag } -type RequestStatus struct { +// v3 response structure +type RequestStatusV3 struct { Status string `json:"status"` Message string `json:"message"` } +// v4 request structure +type LicenseRequestV4 struct { + JobTitle string `json:"jobTitle,omitempty"` + Company string `json:"company,omitempty"` + Industry string `json:"industry,omitempty"` + Email string `json:"email"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + SerialNumber string `json:"serialNumber,omitempty"` + SubscribeToUpdates bool `json:"subscribeToUpdates"` +} + +// v4 response structure +type RequestStatusV4 struct { + Status string `json:"status"` + Message string `json:"message"` +} + +var licenseRequestV4BuildThreshold = 23319 + func newLicenseRequestCmd() *cobra.Command { f := licenseRequestFlags{} cmd := &cobra.Command{ @@ -54,10 +77,13 @@ func newLicenseRequestCmd() *cobra.Command { cmd.Flags().StringVar(&f.serialNumber, "serial-number", "", "Serial Number for the license request.") cmd.Flags().StringVar(&f.company, "company", "", "Company for the licensing request") cmd.Flags().StringVar(&f.industry, "industry", "", "Industry for the licensing request") - cmd.Flags().StringVar(&f.category, "category", "", "License Category") - cmd.Flags().StringVar(&f.salesSource, "sales-source", "", "Sales source") + cmd.Flags().StringVar(&f.category, "category", "", "License Category (v3 API only)") + cmd.Flags().StringVar(&f.salesSource, "sales-source", "", "Sales source (v3 API only)") cmd.Flags().BoolVar(&f.subscribeToUpdates, "subscribe-to-updates", false, "Subscribe to Updates") cmd.Flags().BoolVar(&f.wait, "wait", false, "Wait for licensing request to finish") + 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("first-name") cmd.MarkFlagRequired("last-name") cmd.MarkFlagRequired("email") @@ -68,104 +94,233 @@ func newLicenseRequestCmd() *cobra.Command { func licenseRequestRun(f *licenseRequestFlags) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { - // set up http - client := &http.Client{} - - // add mandatory values - data := url.Values{ - "firstName": {f.firstName}, - "lastName": {f.lastName}, - "email": {f.email}, + // get build to decide if we should use v3 or v4 + // FME Server 2023.0+ and later can use v4. Otherwise fall back to v3 + if f.apiVersion == "" { + fmeflowBuild := viper.GetInt("build") + if fmeflowBuild < licenseRequestV4BuildThreshold { + f.apiVersion = apiVersionFlagV3 + } else { + f.apiVersion = apiVersionFlagV4 + } } - // add optional values - if f.serialNumber != "" { - data.Add("serialNumber", f.serialNumber) - } - if f.company != "" { - data.Add("company", f.company) - } - if f.industry != "" { - data.Add("industry", f.industry) - } - if f.category != "" { - data.Add("category", f.category) - } - if f.salesSource != "" { - data.Add("salesSource", f.salesSource) - } - if f.subscribeToUpdates { - data.Add("subscribeToUpdates", "true") + // Validate v3-only flags are not used with v4 + if f.apiVersion == "v4" { + if f.category != "" { + return fmt.Errorf("the --category flag is only supported with v3 API. Use --api-version v3 or remove the --category flag") + } + if f.salesSource != "" { + return fmt.Errorf("the --sales-source flag is only supported with v3 API. Use --api-version v3 or remove the --sales-source flag") + } } - request, err := buildFmeFlowRequest("/fmerest/v3/licensing/request", "POST", strings.NewReader(data.Encode())) - if err != nil { - return err + if f.apiVersion == "v4" { + return licenseRequestRunV4(f, cmd) + } else { + return licenseRequestRunV3(f, cmd) } + } +} - request.Header.Add("Content-Type", "application/x-www-form-urlencoded") +func licenseRequestRunV3(f *licenseRequestFlags, cmd *cobra.Command) error { + // set up http + client := &http.Client{} + + // add mandatory values + data := url.Values{ + "firstName": {f.firstName}, + "lastName": {f.lastName}, + "email": {f.email}, + } - response, err := client.Do(&request) - if err != nil { - return err - } else if response.StatusCode != 202 { - return errors.New(response.Status) + // add optional values + if f.serialNumber != "" { + data.Add("serialNumber", f.serialNumber) + } + if f.company != "" { + data.Add("company", f.company) + } + if f.industry != "" { + data.Add("industry", f.industry) + } + if f.category != "" { + data.Add("category", f.category) + } + if f.salesSource != "" { + data.Add("salesSource", f.salesSource) + } + if f.subscribeToUpdates { + data.Add("subscribeToUpdates", "true") + } + + request, err := buildFmeFlowRequest("/fmerest/v3/licensing/request", "POST", strings.NewReader(data.Encode())) + if err != nil { + return err + } + + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + response, err := client.Do(&request) + if err != nil { + return err + } else if response.StatusCode != 202 { + return errors.New(response.Status) + } + + if !jsonOutput { + fmt.Fprintln(cmd.OutOrStdout(), "License Request Successfully sent.") + } else { + if !f.wait { + fmt.Fprintln(cmd.OutOrStdout(), "{}") } + } - if !jsonOutput { - fmt.Fprintln(cmd.OutOrStdout(), "License Request Successfully sent.") - } else { - if !f.wait { - fmt.Fprintln(cmd.OutOrStdout(), "{}") + if f.wait { + // check the license status until it is finished + complete := false + for { + if !jsonOutput { + fmt.Print(".") + } + + time.Sleep(1 * time.Second) + // call the status endpoint to see if it is finished + request, err := buildFmeFlowRequest("/fmerest/v3/licensing/request/status", "GET", nil) + if err != nil { + return err + } + response, err := client.Do(&request) + if err != nil { + return err + } + + responseData, err := io.ReadAll(response.Body) + if err != nil { + return err } - } - if f.wait { - // check the license status until it is finished - complete := false - for { + var result RequestStatusV3 + if err := json.Unmarshal(responseData, &result); err != nil { + return err + } else if result.Status != "REQUESTING" { + complete = true if !jsonOutput { - fmt.Print(".") + fmt.Fprintln(cmd.OutOrStdout(), result.Message) + } else { + prettyJSON, err := prettyPrintJSON(responseData) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), prettyJSON) } + } - time.Sleep(1 * time.Second) - // call the status endpoint to see if it is finished - request, err := buildFmeFlowRequest("/fmerest/v3/licensing/request/status", "GET", nil) - if err != nil { - return err - } - response, err := client.Do(&request) - if err != nil { - return err - } + if complete { + break + } + } + } - responseData, err := io.ReadAll(response.Body) - if err != nil { - return err - } + return nil +} - var result RequestStatus - if err := json.Unmarshal(responseData, &result); err != nil { - return err - } else if result.Status != "REQUESTING" { - complete = true - if !jsonOutput { - fmt.Fprintln(cmd.OutOrStdout(), result.Message) - } else { - prettyJSON, err := prettyPrintJSON(responseData) - if err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), prettyJSON) +func licenseRequestRunV4(f *licenseRequestFlags, cmd *cobra.Command) error { + // set up http + client := &http.Client{} + + // create request body + requestBody := LicenseRequestV4{ + FirstName: f.firstName, + LastName: f.lastName, + Email: f.email, + SubscribeToUpdates: f.subscribeToUpdates, + } + + // add optional values + if f.serialNumber != "" { + requestBody.SerialNumber = f.serialNumber + } + if f.company != "" { + requestBody.Company = f.company + } + if f.industry != "" { + requestBody.Industry = f.industry + } + + // marshal JSON + jsonData, err := json.Marshal(requestBody) + if err != nil { + return err + } + + request, err := buildFmeFlowRequest("/fmeapiv4/license/request", "POST", strings.NewReader(string(jsonData))) + if err != nil { + return err + } + + request.Header.Add("Content-Type", "application/json") + + response, err := client.Do(&request) + if err != nil { + return err + } else if response.StatusCode != 202 { + return errors.New(response.Status) + } + + if !jsonOutput { + fmt.Fprintln(cmd.OutOrStdout(), "License Request Successfully sent.") + } else { + if !f.wait { + fmt.Fprintln(cmd.OutOrStdout(), "{}") + } + } + + if f.wait { + // check the license status until it is finished + complete := false + for { + if !jsonOutput { + fmt.Print(".") + } + + time.Sleep(1 * time.Second) + // call the status endpoint to see if it is finished + request, err := buildFmeFlowRequest("/fmeapiv4/license/request/status", "GET", nil) + if err != nil { + return err + } + response, err := client.Do(&request) + if err != nil { + return err + } + + responseData, err := io.ReadAll(response.Body) + if err != nil { + return err + } + + var result RequestStatusV4 + if err := json.Unmarshal(responseData, &result); err != nil { + return err + } else if result.Status != "requesting" { + complete = true + if !jsonOutput { + fmt.Fprintln(cmd.OutOrStdout(), result.Message) + } else { + prettyJSON, err := prettyPrintJSON(responseData) + if err != nil { + return err } + fmt.Fprintln(cmd.OutOrStdout(), prettyJSON) } + } - if complete { - break - } + if complete { + break } } - - return nil } + + return nil } diff --git a/cmd/requestStatus.go b/cmd/requestStatus.go index 73efde5..7bf4290 100644 --- a/cmd/requestStatus.go +++ b/cmd/requestStatus.go @@ -9,11 +9,15 @@ import ( "strings" "github.com/spf13/cobra" + "github.com/spf13/viper" ) +const requestStatusV4BuildThreshold = 23319 + type licenseRequestStatusFlags struct { outputType string noHeaders bool + apiVersion apiVersionFlag } func newLicenseRequestStatusCmd() *cobra.Command { @@ -36,6 +40,9 @@ func newLicenseRequestStatusCmd() *cobra.Command { } cmd.Flags().StringVarP(&f.outputType, "output", "o", "table", "Specify the output type. Should be one of table, json, or custom-columns") cmd.Flags().BoolVar(&f.noHeaders, "no-headers", false, "Don't print column headers") + cmd.Flags().Var(&f.apiVersion, "api-version", "The api version to use when contacting FME Flow. Must be one of v3 or v4") + cmd.Flags().MarkHidden("api-version") + cmd.RegisterFlagCompletionFunc("api-version", apiVersionFlagCompletion) return cmd } @@ -46,79 +53,173 @@ func licenseRequestStatusRun(f *licenseRequestStatusFlags) func(cmd *cobra.Comma f.outputType = "json" } - // set up http - client := &http.Client{} - - // call the status endpoint to see if it is finished - request, err := buildFmeFlowRequest("/fmerest/v3/licensing/request/status", "GET", nil) - if err != nil { - return err - } - response, err := client.Do(&request) - if err != nil { - return err - } else if response.StatusCode != 200 { - return errors.New(response.Status) + // get build to decide if we should use v3 or v4 + // FME Server 2023.0+ and later can use v4. Otherwise fall back to v3 + if f.apiVersion == "" { + fmeflowBuild := viper.GetInt("build") + if fmeflowBuild < requestStatusV4BuildThreshold { + f.apiVersion = apiVersionFlagV3 + } else { + f.apiVersion = apiVersionFlagV4 + } } - responseData, err := io.ReadAll(response.Body) - if err != nil { - return err + if f.apiVersion == "v4" { + return licenseRequestStatusRunV4(f, cmd) + } else { + return licenseRequestStatusRunV3(f, cmd) } + } +} + +func licenseRequestStatusRunV3(f *licenseRequestStatusFlags, cmd *cobra.Command) error { + // set up http + client := &http.Client{} + + // call the status endpoint to see if it is finished + request, err := buildFmeFlowRequest("/fmerest/v3/licensing/request/status", "GET", nil) + if err != nil { + return err + } + response, err := client.Do(&request) + if err != nil { + return err + } else if response.StatusCode != 200 { + return errors.New(response.Status) + } + + responseData, err := io.ReadAll(response.Body) + if err != nil { + return err + } + + var result RequestStatusV3 + if err := json.Unmarshal(responseData, &result); err != nil { + return err + } else { + if f.outputType == "table" { + t := createTableWithDefaultColumns(result) + + if f.noHeaders { + t.ResetHeaders() + } + fmt.Fprintln(cmd.OutOrStdout(), t.Render()) + + } else if f.outputType == "json" { + prettyJSON, err := prettyPrintJSON(responseData) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), prettyJSON) + } else if strings.HasPrefix(f.outputType, "custom-columns") { + // parse the columns and json queries + columnsString := "" + if strings.HasPrefix(f.outputType, "custom-columns=") { + columnsString = f.outputType[len("custom-columns="):] + } + if len(columnsString) == 0 { + return errors.New("custom-columns format specified but no custom columns given") + } + + // we have to marshal the Items array, then create an array of marshalled items + // to pass to the creation of the table. + marshalledItems := [][]byte{} + mJson, err := json.Marshal(result) + if err != nil { + return err + } + marshalledItems = append(marshalledItems, mJson) + + columnsInput := strings.Split(columnsString, ",") + t, err := createTableFromCustomColumns(marshalledItems, columnsInput) + if err != nil { + return err + } + if f.noHeaders { + t.ResetHeaders() + } + fmt.Fprintln(cmd.OutOrStdout(), t.Render()) - var result RequestStatus - if err := json.Unmarshal(responseData, &result); err != nil { - return err } else { - if f.outputType == "table" { - t := createTableWithDefaultColumns(result) - - if f.noHeaders { - t.ResetHeaders() - } - fmt.Fprintln(cmd.OutOrStdout(), t.Render()) - - } else if f.outputType == "json" { - prettyJSON, err := prettyPrintJSON(responseData) - if err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), prettyJSON) - } else if strings.HasPrefix(f.outputType, "custom-columns") { - // parse the columns and json queries - columnsString := "" - if strings.HasPrefix(f.outputType, "custom-columns=") { - columnsString = f.outputType[len("custom-columns="):] - } - if len(columnsString) == 0 { - return errors.New("custom-columns format specified but no custom columns given") - } - - // we have to marshal the Items array, then create an array of marshalled items - // to pass to the creation of the table. - marshalledItems := [][]byte{} - mJson, err := json.Marshal(result) - if err != nil { - return err - } - marshalledItems = append(marshalledItems, mJson) - - columnsInput := strings.Split(columnsString, ",") - t, err := createTableFromCustomColumns(marshalledItems, columnsInput) - if err != nil { - return err - } - if f.noHeaders { - t.ResetHeaders() - } - fmt.Fprintln(cmd.OutOrStdout(), t.Render()) + return errors.New("invalid output format specified") + } - } else { - return errors.New("invalid output format specified") + } + return nil +} + +func licenseRequestStatusRunV4(f *licenseRequestStatusFlags, cmd *cobra.Command) error { + // set up http + client := &http.Client{} + + // call the status endpoint to see if it is finished + request, err := buildFmeFlowRequest("/fmeapiv4/license/request/status", "GET", nil) + if err != nil { + return err + } + response, err := client.Do(&request) + if err != nil { + return err + } else if response.StatusCode != 200 { + return errors.New(response.Status) + } + + responseData, err := io.ReadAll(response.Body) + if err != nil { + return err + } + + var result RequestStatusV4 + if err := json.Unmarshal(responseData, &result); err != nil { + return err + } else { + if f.outputType == "table" { + t := createTableWithDefaultColumns(result) + + if f.noHeaders { + t.ResetHeaders() + } + fmt.Fprintln(cmd.OutOrStdout(), t.Render()) + + } else if f.outputType == "json" { + prettyJSON, err := prettyPrintJSON(responseData) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), prettyJSON) + } else if strings.HasPrefix(f.outputType, "custom-columns") { + // parse the columns and json queries + columnsString := "" + if strings.HasPrefix(f.outputType, "custom-columns=") { + columnsString = f.outputType[len("custom-columns="):] + } + if len(columnsString) == 0 { + return errors.New("custom-columns format specified but no custom columns given") } + // we have to marshal the Items array, then create an array of marshalled items + // to pass to the creation of the table. + marshalledItems := [][]byte{} + mJson, err := json.Marshal(result) + if err != nil { + return err + } + marshalledItems = append(marshalledItems, mJson) + + columnsInput := strings.Split(columnsString, ",") + t, err := createTableFromCustomColumns(marshalledItems, columnsInput) + if err != nil { + return err + } + if f.noHeaders { + t.ResetHeaders() + } + fmt.Fprintln(cmd.OutOrStdout(), t.Render()) + + } else { + return errors.New("invalid output format specified") } - return nil } + return nil } diff --git a/cmd/requestStatus_test.go b/cmd/requestStatus_test.go index 6e3ef96..28715c0 100644 --- a/cmd/requestStatus_test.go +++ b/cmd/requestStatus_test.go @@ -7,7 +7,7 @@ import ( func TestRequestStatus(t *testing.T) { // standard responses for v3 - statusV3 := `{ + status := `{ "message": "Success! Your FME Server has now been licensed.", "status": "SUCCESS" }` @@ -32,18 +32,36 @@ func TestRequestStatus(t *testing.T) { args: []string{"license", "request", "status"}, }, { - name: "get request status", + name: "get request status v3", statusCode: http.StatusOK, - args: []string{"license", "request", "status"}, + args: []string{"license", "request", "status", "--api-version", "v3"}, wantOutputRegex: "^[\\s]*STATUS[\\s]*MESSAGE[\\s]*SUCCESS[\\s]*Success! Your FME Server has now been licensed\\.[\\s]*$", - body: statusV3, + body: status, + fmeflowBuild: 20000, // Force v3 }, { - name: "get request status json", + name: "get request status v3 json", statusCode: http.StatusOK, - body: statusV3, - args: []string{"license", "request", "status", "--json"}, - wantOutputJson: statusV3, + body: status, + args: []string{"license", "request", "status", "--api-version", "v3", "--json"}, + wantOutputJson: status, + fmeflowBuild: 20000, // Force v3 + }, + { + name: "get request status v4", + statusCode: http.StatusOK, + args: []string{"license", "request", "status", "--api-version", "v4"}, + wantOutputRegex: "^[\\s]*STATUS[\\s]*MESSAGE[\\s]*SUCCESS[\\s]*Success! Your FME Server has now been licensed\\.[\\s]*$", + body: status, + fmeflowBuild: 25000, // Force v4 + }, + { + name: "get request status v4 json", + statusCode: http.StatusOK, + body: status, + args: []string{"license", "request", "status", "--api-version", "v4", "--json"}, + wantOutputJson: status, + fmeflowBuild: 25000, // Force v4 }, } diff --git a/cmd/request_test.go b/cmd/request_test.go index c37d701..c3a17c5 100644 --- a/cmd/request_test.go +++ b/cmd/request_test.go @@ -11,7 +11,7 @@ import ( func TestLicenseRequest(t *testing.T) { // standard responses for v3 - responseV3Status := `{ + responseStatus := `{ "message": "Success! Your FME Server has now been licensed.", "status": "SUCCESS" }` @@ -26,7 +26,18 @@ func TestLicenseRequest(t *testing.T) { } if strings.Contains(r.URL.Path, "/fmerest/v3/licensing/request/status") { w.WriteHeader(http.StatusOK) - _, err := w.Write([]byte(responseV3Status)) + _, err := w.Write([]byte(responseStatus)) + require.NoError(t, err) + } + if strings.Contains(r.URL.Path, "/fmeapiv4/license/request") { + w.WriteHeader(http.StatusAccepted) + _, err := w.Write([]byte("")) + require.NoError(t, err) + + } + if strings.Contains(r.URL.Path, "/fmeapiv4/license/request/status") { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(responseStatus)) require.NoError(t, err) } @@ -70,24 +81,43 @@ func TestLicenseRequest(t *testing.T) { wantErrText: "required flag(s) \"first-name\" not set", }, { - name: "request license", - args: []string{"license", "request", "--first-name", "Billy", "--last-name", "Bob", "--email", "billy.bob@example.com"}, + name: "request license v3", + args: []string{"license", "request", "--first-name", "Billy", "--last-name", "Bob", "--email", "billy.bob@example.com", "--api-version", "v3"}, wantOutputRegex: "^License Request Successfully sent\\.[\\s]*$", httpServer: httptest.NewServer(http.HandlerFunc(customHttpServerHandler)), }, { - name: "request license and wait", + name: "request license v4", + args: []string{"license", "request", "--first-name", "Billy", "--last-name", "Bob", "--email", "billy.bob@example.com", "--api-version", "v4"}, + wantOutputRegex: "^License Request Successfully sent\\.[\\s]*$", + httpServer: httptest.NewServer(http.HandlerFunc(customHttpServerHandler)), + }, + { + name: "request license and wait v3", statusCode: http.StatusOK, - args: []string{"license", "request", "--first-name", "Billy", "--last-name", "Bob", "--email", "billy.bob@example.com", "--wait"}, + args: []string{"license", "request", "--first-name", "Billy", "--last-name", "Bob", "--email", "billy.bob@example.com", "--wait", "--api-version", "v3"}, wantOutputRegex: "^License Request Successfully sent\\.[\\s]*Success! Your FME Server has now been licensed\\.[\\s]*$", httpServer: httptest.NewServer(http.HandlerFunc(customHttpServerHandler)), }, { - name: "request license check form params", + name: "request license and wait v4", + statusCode: http.StatusOK, + args: []string{"license", "request", "--first-name", "Billy", "--last-name", "Bob", "--email", "billy.bob@example.com", "--wait", "--api-version", "v4"}, + wantOutputRegex: "^License Request Successfully sent\\.[\\s]*Success! Your FME Server has now been licensed\\.[\\s]*$", + httpServer: httptest.NewServer(http.HandlerFunc(customHttpServerHandler)), + }, + { + name: "request license check form params v3", statusCode: http.StatusAccepted, - args: []string{"license", "request", "--first-name", "Billy", "--last-name", "Bob", "--email", "billy.bob@example.com", "--serial-number", "AAAA-AAAA-AAAA", "--company", "Example Inc.", "--industry", "Industry", "--sales-source", "source", "--subscribe-to-updates", "--category", "Category"}, + args: []string{"license", "request", "--first-name", "Billy", "--last-name", "Bob", "--email", "billy.bob@example.com", "--serial-number", "AAAA-AAAA-AAAA", "--company", "Example Inc.", "--industry", "Industry", "--sales-source", "source", "--subscribe-to-updates", "--category", "Category", "--api-version", "v3"}, wantFormParams: map[string]string{"firstName": "Billy", "lastName": "Bob", "email": "billy.bob@example.com", "serialNumber": "AAAA-AAAA-AAAA", "company": "Example Inc.", "category": "Category", "industry": "Industry", "salesSource": "source", "subscribeToUpdates": "true"}, }, + { + name: "request license check body params v4", + statusCode: http.StatusAccepted, + args: []string{"license", "request", "--first-name", "Billy", "--last-name", "Bob", "--email", "billy.bob@example.com", "--serial-number", "AAAA-AAAA-AAAA", "--company", "Example Inc.", "--industry", "Industry", "--subscribe-to-updates", "--api-version", "v4"}, + wantBodyJson: `{"firstName":"Billy","lastName":"Bob","email":"billy.bob@example.com","serialNumber":"AAAA-AAAA-AAAA","company":"Example Inc.","industry":"Industry","subscribeToUpdates":true}`, + }, } runTests(cases, t) diff --git a/cmd/root.go b/cmd/root.go index 2196d17..91b4f7b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -102,7 +102,10 @@ func initConfig() { //viper.AutomaticEnv() // read in environment variables that match // If a config file is found, read it in. - viper.ReadInConfig() + err := viper.ReadInConfig() + if err != nil { // Handle errors reading the config file + cobra.CheckErr(err) + } } diff --git a/cmd/systemcode_test.go b/cmd/systemcode_test.go index bd27caf..a8a0fe1 100644 --- a/cmd/systemcode_test.go +++ b/cmd/systemcode_test.go @@ -23,7 +23,7 @@ func TestSystemCode(t *testing.T) { name: "systemcode not available in newer builds", statusCode: http.StatusOK, args: []string{"license", "systemcode"}, - wantErrText: "systemcode is not available in this version of FME Flow. The systemcode command was removed in FME Flow 2024.0+", + wantErrText: "systemcode is not available in this version of FME Flow. The systemcode command was removed in FME Flow 2026.1+", fmeflowBuild: 26000, // Use build >= 26000 to trigger deprecation error }, { diff --git a/cmd/testFunctions.go b/cmd/testFunctions.go index 6d24f3e..4a9e00f 100644 --- a/cmd/testFunctions.go +++ b/cmd/testFunctions.go @@ -32,6 +32,7 @@ type testCase struct { wantURLContains string // check the URL contains a certain string wantFileContents fileContents // check file contents wantBodyRegEx string // check the contents of the body sent + wantBodyJson string // check the JSON body sent fmeflowBuild int // build to pretend we are contacting args []string // flags to pass into the command httpServer *httptest.Server // custom http test server if needed @@ -69,6 +70,11 @@ func runTests(tcs []testCase, t *testing.T) { require.NoError(t, err) require.Regexp(t, regexp.MustCompile(tc.wantBodyRegEx), string(body)) } + if tc.wantBodyJson != "" { + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + require.JSONEq(t, tc.wantBodyJson, string(body)) + } w.WriteHeader(tc.statusCode) _, err := w.Write([]byte(tc.body)) require.NoError(t, err)