From 399b7388053c1f6a214a4dbfe811d91a1f262bfa Mon Sep 17 00:00:00 2001 From: Richard Case Date: Wed, 2 Nov 2016 12:51:32 +0000 Subject: [PATCH] Merge of development for release (#83) New features/functionality: * [#60] Webhook functionality added to the CLI. * [#76] Project changed to use semantic versioning. See http://semver.org/ for further details. * Added a get baremetal capabilities command. * Added a version command to return build version, build date and git commit hash, os and Go version * Changed the integration tests to use 'go test' instead of a seperate executable. The tests utilise the new subtest functionality in go 1.7. * API file generation now has been separated out into a different command line. Bug fixes: * [#55] Remove premium storage type as its deprecated. The --storage-type command line option has been removed completely for server create, server import. * [#64] Listing cross DC policies didn't filter as expected. * [#67] Getting server details doesn't return IsManaged or IsManagdBackup * [#69] Updating the server disks fails. * [#70] Integration tests where failing due to multiple issues. * [#75] Creating bare metal servers shouldn't require CPU, MemoryGB or template. Additionally the configuration-id and os-type parameters should indicate how to get the allowed values. Resolves #55, #60, #64, #67, #69, #70, #75, #76 Signed-off-by: Richard Case --- .gitignore | 1 + README.md | 4 + base/build.go | 5 + base/constants.go | 6 +- cmd/genapi/main.go | 36 +++ commands/version.go | 48 ++++ connection/connection.go | 2 +- init.go | 140 +++++++-- integration_tests/api.json | 130 +-------- integration_tests/api_parser.go | 2 +- integration_tests/api_storage.go | 2 +- integration_tests/integration_test.go | 40 +++ integration_tests/logger.go | 8 +- integration_tests/main.go | 51 ---- integration_tests/objects.go | 2 +- integration_tests/runner.go | 266 ++++++++++-------- models/autoscale/remove_on_server.go | 2 +- models/balancer/create.go | 2 +- models/balancer/delete_pool.go | 2 +- models/balancer/update.go | 2 +- .../datacenter/get_baremetal_capabilities.go | 41 +++ .../datacenter/get_deployment_capabilities.go | 1 - models/firewall/delete.go | 4 +- models/server/create.go | 33 ++- models/server/delete.go | 2 +- models/server/delete_snapshot.go | 4 +- models/server/entities.go | 32 ++- models/server/get.go | 2 +- models/server/import.go | 2 +- models/server/remove_ip_address.go | 4 +- models/server/secondary_network.go | 9 +- models/server/update.go | 3 +- models/webhook/add-targeturi.go | 23 ++ models/webhook/delete-targeturi.go | 23 ++ models/webhook/delete.go | 17 ++ models/webhook/entities.go | 20 ++ models/webhook/list.go | 6 + models/webhook/update.go | 31 ++ models/webhook/validation.go | 48 ++++ run_integraion_tests | 6 +- scripts/build_releases | 17 +- scripts/generate_api | 4 +- 42 files changed, 707 insertions(+), 376 deletions(-) create mode 100644 base/build.go create mode 100644 cmd/genapi/main.go create mode 100644 commands/version.go create mode 100644 integration_tests/integration_test.go delete mode 100644 integration_tests/main.go create mode 100644 models/datacenter/get_baremetal_capabilities.go create mode 100644 models/webhook/add-targeturi.go create mode 100644 models/webhook/delete-targeturi.go create mode 100644 models/webhook/delete.go create mode 100644 models/webhook/entities.go create mode 100644 models/webhook/list.go create mode 100644 models/webhook/update.go create mode 100644 models/webhook/validation.go diff --git a/.gitignore b/.gitignore index d38fafa..ce7e35e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ out/* *.swp .DS_Store release/* +.vscode/ diff --git a/README.md b/README.md index 22c175f..381afce 100644 --- a/README.md +++ b/README.md @@ -318,6 +318,10 @@ instructions below may not work properly on Windows. * If you want to make an executable, simply run `./scripts/build`. The binary will appear in the `./out` folder. +* The integration tests can be running `./run_integration_tests`. + +* The API file can be regenerated by running `./scripts/generate_api`. + ### Building the releases Generally, any Linux/Darwin machine should work for building the releases. A Darwin machine is required though if you want to build a `MacOS .pkg`. diff --git a/base/build.go b/base/build.go new file mode 100644 index 0000000..7f06561 --- /dev/null +++ b/base/build.go @@ -0,0 +1,5 @@ +package base + +var BuildVersion = "built-from-source" +var BuildGitCommit = "No git commit provided" +var BuildDate = "No build date supplied" diff --git a/base/constants.go b/base/constants.go index b80ce26..09203e2 100644 --- a/base/constants.go +++ b/base/constants.go @@ -1,11 +1,9 @@ package base -const ( - VERSION = "built-from-source" -) - var ( URL = "https://api.ctl.io" + CTL_URL = "http://www.ctl.io" + PROJ_URL = "https://github.com/CenturyLinkCloud/clc-go-cli" TIME_FORMAT = "2006-01-02 15:04:05" SERVER_TIME_FORMAT = "2006-01-02T15:04:05Z" TIME_FORMAT_REPR = "YYYY-MM-DD hh:mm:ss" diff --git a/cmd/genapi/main.go b/cmd/genapi/main.go new file mode 100644 index 0000000..a26a38a --- /dev/null +++ b/cmd/genapi/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "flag" + "os" + + integration "github.com/centurylinkcloud/clc-go-cli/integration_tests" +) + +func main() { + logger := integration.NewLogger() + + var apiPath = flag.String("api-path", "", "The path to the API file") + flag.Parse() + + if *apiPath == "" { + logger.Logf("ERROR: The api-path command line argument must be specified.") + os.Exit(-1) + } + + parser := integration.NewParser(logger) + + apiDef, err := parser.ParseApi() + if err != nil { + logger.Logf("Error while parsing API definition: %v", err) + os.Exit(-2) + } + + err = integration.StoreApi(apiDef, *apiPath) + if err != nil { + logger.Logf("Error while storing API definition: %v", err) + os.Exit(-3) + } + + os.Exit(0) +} diff --git a/commands/version.go b/commands/version.go new file mode 100644 index 0000000..71feddd --- /dev/null +++ b/commands/version.go @@ -0,0 +1,48 @@ +package commands + +import ( + "fmt" + "runtime" + + "github.com/centurylinkcloud/clc-go-cli/base" +) + +var banner = ` +------------------------------------------------------------- + + _____ __ __ _ __ + / ___/___ ___ / /_ __ __ ____ __ __ / / (_)___ / /__ + / /__ / -_)/ _ \/ __// // // __// // // /__ / // _ \ / '_/ + \___/ \__//_//_/\__/ \_,_//_/ \_, //____//_//_//_//_/\_\ + /___/ + +------------------------------------------------------------- +` + +type Version struct { + CommandBase +} + +func NewVersion(info CommandExcInfo) *Version { + v := Version{} + v.ExcInfo = info + return &v +} + +func (v *Version) IsOffline() bool { + return true +} + +func (v *Version) ExecuteOffline() (string, error) { + fmt.Printf("%s", banner) + fmt.Printf("CenturyLink Cloud CLI (Version %s)\n", base.BuildVersion) + fmt.Printf("%s\n", base.PROJ_URL) + fmt.Printf("\n") + fmt.Printf("Go Version: %s %s/%s\n", runtime.Version(), runtime.GOOS, runtime.GOARCH) + fmt.Printf("Built on: %s\n", base.BuildDate) + fmt.Printf("Git Commit: %s\n", base.BuildGitCommit) + fmt.Printf("\n") + fmt.Printf("For more information on CenturyLink Cloud visit: %s\n", base.CTL_URL) + + return "", nil +} diff --git a/connection/connection.go b/connection/connection.go index e3bc9a8..27005cd 100644 --- a/connection/connection.go +++ b/connection/connection.go @@ -22,7 +22,7 @@ import ( const OriginalBaseUrl = "https://api.ctl.io/" var ( - userAgent = fmt.Sprintf("clc-go-cli-%s-%s", base.VERSION, runtime.GOOS) + userAgent = fmt.Sprintf("clc-go-cli-%s-%s", base.BuildVersion, runtime.GOOS) //this made a variable instead of a constant for testing purpoises BaseUrl = OriginalBaseUrl ) diff --git a/init.go b/init.go index 5a4f25a..e680ccf 100644 --- a/init.go +++ b/init.go @@ -24,6 +24,7 @@ import ( "github.com/centurylinkcloud/clc-go-cli/models/ospatch" "github.com/centurylinkcloud/clc-go-cli/models/server" "github.com/centurylinkcloud/clc-go-cli/models/vpn" + "github.com/centurylinkcloud/clc-go-cli/models/webhook" ) var AllCommands []base.Command = make([]base.Command, 0) @@ -157,15 +158,6 @@ func init() { "--type", []string{"Required. Whether to create a standard, hyperscale, or bareMetal server."}, }, - { - "--storage-type", - []string{ - "For standard servers, whether to use standard or premium storage.", - "If not provided, will default to premium storage.", - "For hyperscale servers, storage type must be hyperscale.", - "Ignored for bare metal servers.", - }, - }, { "--anti-affinity-policy-id", []string{ @@ -202,14 +194,15 @@ func init() { "--configuration-id", []string{ "Only required for bare metal servers. Specifies the identifier for the specific configuration type of bare metal server to deploy.", - "Ignored for standard and hyperscale servers.", + "The list of valid bare metal configuration id's can be found by calling the 'clc data-center get-baremetal-capabilities' command.", + "Ignored for standard and hyperscale servers. ", }, }, { "--os-type", []string{ - "Only required for bare metal servers. Specifies the OS to provision with the bare metal server. Currently, the only supported OS types", - "are redHat6_64Bit, centOS6_64Bit, windows2012R2Standard_64Bit.", + "Only required for bare metal servers. Specifies the OS to provision with the bare metal server. The list of valid operating", + "systems can be found by calling the 'clc data-center get-baremetal-capabilities' command.", "Ignored for standard and hyperscale servers.", }, }, @@ -791,13 +784,6 @@ func init() { "--type", []string{"Required. Whether to create standard or hyperscale server"}, }, - { - "--storage-type", - []string{ - "For standard servers, whether to use standard or premium storage. If not provided, will default to premium storage.", - "For hyperscale servers, storage type must be hyperscale.", - }, - }, { "--custom-fields", []string{ @@ -1380,7 +1366,25 @@ func init() { Brief: []string{ "Gets the list of capabilities that a specific data center supports for a given account,", "including the deployable networks, OS templates, and whether features like", - "premium storage and shared load balancer configuration are available.", + "bare metal servers and shared load balancer configuration are available.", + }, + Arguments: []help.Argument{ + { + "--data-center", + []string{"Required. Short string representing the data center you are querying."}, + }, + }, + }, + }) + registerCommandBase(&datacenter.GetBMCapReq{}, &datacenter.GetBMCapRes{}, commands.CommandExcInfo{ + Verb: "GET", + Url: "https://api.ctl.io/v2/datacenters/{accountAlias}/{DataCenter}/bareMetalCapabilities", + Resource: "data-center", + Command: "get-baremetal-capabilities", + Help: help.Command{ + Brief: []string{ + "Gets the list of bare metal capabilities that a specific data center supports for a given account,", + "including the list of configuration types and the list of supported operating systems.", }, Arguments: []help.Argument{ { @@ -1473,7 +1477,7 @@ func init() { }, }, }) - registerCommandBase(&network.CreateReq{}, &models.LinkEntity{}, commands.CommandExcInfo{ + registerCommandBase(&network.CreateReq{}, &models.Status{}, commands.CommandExcInfo{ Verb: "POST", Url: "https://api.ctl.io/v2-experimental/networks/{accountAlias}/{DataCenter}/claim", Resource: "network", @@ -1939,7 +1943,7 @@ func init() { }) registerCommandBase(&crossdc_firewall.ListReq{}, &[]crossdc_firewall.Entity{}, commands.CommandExcInfo{ Verb: "GET", - Url: "https://api.ctl.io/v2-experimental/crossDcFirewallPolicies/{accountAlias}/{DataCenter}?destinationAccount={DestinationAccountAlias}", + Url: "https://api.ctl.io/v2-experimental/crossDcFirewallPolicies/{accountAlias}/{DataCenter}?destinationAccountId={DestinationAccountAlias}", Resource: "crossdc-firewall-policy", Command: "list", Help: help.Command{ @@ -2435,6 +2439,13 @@ func init() { AccountAgnostic: true, }, })) + registerCustomCommand(commands.NewVersion(commands.CommandExcInfo{ + Resource: "version", + Help: help.Command{ + Brief: []string{"Shows version information about the cli."}, + AccountAgnostic: true, + }, + })) registerCustomCommand(commands.NewLogin(commands.CommandExcInfo{ Resource: "login", Help: help.Command{ @@ -3737,6 +3748,91 @@ func init() { }, }, }) + registerCommandBase(nil, &webhook.ListRes{}, commands.CommandExcInfo{ + Verb: "GET", + Url: "https://api.ctl.io/v2/webhooks/{accountAlias}", + Resource: "webhook", + Command: "list", + Help: help.Command{ + Brief: []string{"Gets a list of the webhooks configured for a given account."}, + }, + }) + registerCommandBase(&webhook.DeleteReq{}, new(string), commands.CommandExcInfo{ + Verb: "DELETE", + Url: "https://api.ctl.io/v2/webhooks/{accountAlias}/{Event}/configuration", + Resource: "webhook", + Command: "delete", + Help: help.Command{ + Brief: []string{"Deletes a given alert policy by ID."}, + Arguments: []help.Argument{ + { + "--event", + []string{"Required. Name of the event for which the webhook will be deleted."}, + }, + }, + }, + }) + registerCommandBase(&webhook.DeleteTargetURIReq{}, new(string), commands.CommandExcInfo{ + Verb: "DELETE", + Url: "https://api.ctl.io/v2/webhooks/{accountAlias}/{Event}/configuration/targetUris?targetUri={TargetUri}", + Resource: "webhook", + Command: "delete-targeturi", + Help: help.Command{ + Brief: []string{"Deletes a target URI from a webhook."}, + Arguments: []help.Argument{ + { + "--event", + []string{"Required. Name of the event for which the target URI will be deleted."}, + }, + { + "--target-uri", + []string{"The URI of the target to remove from the webhook."}, + }, + }, + }, + }) + registerCommandBase(&webhook.AddTargetURIReq{}, new(string), commands.CommandExcInfo{ + Verb: "POST", + Url: "https://api.ctl.io/v2/webhooks/{accountAlias}/{Event}/configuration/targetUris", + Resource: "webhook", + Command: "add-targeturi", + Help: help.Command{ + Brief: []string{"Add a target uri to the webhook for a specified event."}, + Arguments: []help.Argument{ + { + "--event", + []string{"Required. Name of the event for which the target URI will be added."}, + }, + { + "--target-uri", + []string{"Required. A uri that will be called when the event occurs."}, + }, + }, + }, + }) + registerCommandBase(&webhook.UpdateReq{}, new(string), commands.CommandExcInfo{ + Verb: "PUT", + Url: "https://api.ctl.io/v2/webhooks/{accountAlias}/{Event}/configuration", + Resource: "webhook", + Command: "update", + Help: help.Command{ + Brief: []string{"Change the configuration of a webhook for a specific event."}, + Arguments: []help.Argument{ + { + "--event", + []string{"Required. Name of the event for which to update the webhook."}, + }, + { + "--recursive", + []string{"Required. If true, the webhook is called when the event occurs in sub-accounts."}, + }, + { + "--target-uri", + []string{"A uri that will be called when the event occurs."}, + }, + }, + }, + }) } func registerCommandBase(inputModel interface{}, outputModel interface{}, info commands.CommandExcInfo) { diff --git a/integration_tests/api.json b/integration_tests/api.json index c6c4696..db50e1f 100644 --- a/integration_tests/api.json +++ b/integration_tests/api.json @@ -1842,7 +1842,6 @@ } ], "supportsBareMetalServers": false, - "supportsPremiumStorage": true, "supportsSharedLoadBalancer": true, "templates": [ { @@ -1939,13 +1938,6 @@ ] }, "ResParameters": [ - { - "Name": "supportsPremiumStorage", - "Type": "boolean", - "Description": "Whether or not this data center provides support for servers with premium storage", - "IsRequired": false, - "Children": null - }, { "Name": "supportsSharedLoadBalancer", "Type": "boolean", @@ -3848,100 +3840,20 @@ "ContentParameters": null, "ContentExample": null, "ResExample": { - "cidr": "11.22.33.0/24", - "description": "vlan_9999_11.22.33", - "gateway": "11.22.33.1", - "id": "e1c1ab9ba070486bbbf114d7bbf556c4", - "links": [ - { - "href": "http://api.ctl.io/v2-experimental/networks/ALIAS/WA1/e1c1ab9ba070486bbbf114d7bbf556c4", - "rel": "self", - "verbs": [ - "GET", - "PUT" - ] - }, - { - "href": "http://api.ctl.io/v2-experimental/networks/ALIAS/WA1/e1c1ab9ba070486bbbf114d7bbf556c4/ipAddresses", - "rel": "ipAddresses", - "verbs": [ - "GET" - ] - }, - { - "href": "http://api.ctl.io/v2-experimental/networks/ALIAS/WA1/e1c1ab9ba070486bbbf114d7bbf556c4/release", - "rel": "release", - "verbs": [ - "POST" - ] - } - ], - "name": "vlan_9999_11.22.33", - "netmask": "255.255.255.0", - "type": "private", - "vlan": 9999 - }, + "operationId":"3b4aeaf059c545958b7d96093103d7fc", + "uri":"/v2-experimental/operations/ALIAS/status/3b4aeaf059c545958b7d96093103d7fc"}, "ResParameters": [ { - "Name": "id", - "Type": "string", - "Description": "ID of the network", - "IsRequired": false, - "Children": null - }, - { - "Name": "cidr", - "Type": "string", - "Description": "The network address, specified using ", - "IsRequired": false, - "Children": null - }, - { - "Name": "description", - "Type": "string", - "Description": "Description of VLAN, a free text field that defaults to the VLAN number combined with the network address", - "IsRequired": false, - "Children": null - }, - { - "Name": "gateway", - "Type": "string", - "Description": "Gateway IP address of the network", - "IsRequired": false, - "Children": null - }, - { - "Name": "name", - "Type": "string", - "Description": "User-defined name of the network; the default is the VLAN number combined with the network address", - "IsRequired": false, - "Children": null - }, - { - "Name": "netmask", + "Name": "operationId", "Type": "string", - "Description": "A screen of numbers used for routing traffic within a subnet", + "Description": "GUID for the item in the queue for completion", "IsRequired": false, "Children": null }, { - "Name": "type", + "Name": "uri", "Type": "string", - "Description": "Network type, usually ", - "IsRequired": false, - "Children": null - }, - { - "Name": "vlan", - "Type": "integer", - "Description": "Unique number assigned to the VLAN", - "IsRequired": false, - "Children": null - }, - { - "Name": "links", - "Type": "array", - "Description": "Collection of ", + "Description": "Link to review status of the operation", "IsRequired": false, "Children": null } @@ -5719,7 +5631,6 @@ "name": "WA1ALIASWB01", "osType": "Windows 2008 64-bit", "status": "active", - "storageType": "standard", "type": "standard" }, "ResParameters": [ @@ -5789,14 +5700,7 @@ { "Name": "type", "Type": "string", - "Description": "Whether a standard or premium server", - "IsRequired": false, - "Children": null - }, - { - "Name": "storageType", - "Type": "string", - "Description": "Whether it uses standard or premium storage", + "Description": "Whether a standard, hyperscale or bareMetal server", "IsRequired": false, "Children": null }, @@ -5999,17 +5903,10 @@ { "Name": "type", "Type": "string", - "Description": "Whether to create a ", + "Description": "Whether to create a standard, hyperscale or bareMetal server", "IsRequired": true, "Children": null }, - { - "Name": "storageType", - "Type": "string", - "Description": "For standard servers, whether to use standard or premium storage. If not provided, will default to premium storage. For hyperscale servers, storage type must be hyperscale. (Ignored for bare metal servers.)", - "IsRequired": false, - "Children": null - }, { "Name": "antiAffinityPolicyId", "Type": "string", @@ -6138,7 +6035,6 @@ "primaryDns": "172.17.1.26", "secondaryDns": "172.17.1.27", "sourceServerId": "RHEL-6-64-TEMPLATE", - "storageType": "standard", "ttl": "2014-12-17T01:17:17Z", "type": "standard" }, @@ -6455,17 +6351,10 @@ { "Name": "type", "Type": "string", - "Description": "Whether to create standard or hyperscale server", + "Description": "Whether to create standard, hyperscale or bareMetal server", "IsRequired": true, "Children": null }, - { - "Name": "storageType", - "Type": "string", - "Description": "For standard servers, whether to use standard or premium storage. If not provided, will default to premium storage. For hyperscale servers, storage type must be hyperscale.", - "IsRequired": false, - "Children": null - }, { "Name": "customFields", "Type": "complex", @@ -6521,7 +6410,6 @@ "password": "P@ssw0rd1", "primaryDns": "172.17.1.26", "secondaryDns": "172.17.1.27", - "storageType": "standard", "type": "standard" }, "ResExample": { diff --git a/integration_tests/api_parser.go b/integration_tests/api_parser.go index ea50069..b79a38d 100644 --- a/integration_tests/api_parser.go +++ b/integration_tests/api_parser.go @@ -1,4 +1,4 @@ -package main +package integration_tests import ( "encoding/json" diff --git a/integration_tests/api_storage.go b/integration_tests/api_storage.go index 340ff3c..426be51 100644 --- a/integration_tests/api_storage.go +++ b/integration_tests/api_storage.go @@ -1,4 +1,4 @@ -package main +package integration_tests import ( "encoding/json" diff --git a/integration_tests/integration_test.go b/integration_tests/integration_test.go new file mode 100644 index 0000000..e836d63 --- /dev/null +++ b/integration_tests/integration_test.go @@ -0,0 +1,40 @@ +// +build integration + +package integration_tests + +import ( + "flag" + "os" + "testing" +) + +var ( + apiPath = flag.String("api-path", "", "The path to the API file") + clcTrace = flag.Bool("clc-trace", false, "Output trace statements for calls to CLC") +) + +func TestMain(m *testing.M) { + flag.Parse() + + result := m.Run() + + os.Exit(result) +} + +func TestCommands(t *testing.T) { + api, err := LoadApi(*apiPath) + if err != nil { + t.Errorf("Error while loading API: %v", err) + t.Fail() + return + } + t.Logf("Api def loaded, count: %d", len(api)) + + runner := NewRunner(api, *clcTrace) + err = runner.RunTests(t) + + if err != nil { + t.Errorf("%s\n", err.Error()) + t.Fail() + } +} diff --git a/integration_tests/logger.go b/integration_tests/logger.go index de33a6d..768bbed 100644 --- a/integration_tests/logger.go +++ b/integration_tests/logger.go @@ -1,13 +1,15 @@ -package main +package integration_tests import ( "fmt" + "golang.org/x/net/html" ) type Logger interface { Logf(format string, a ...interface{}) LogNode(message string, n *html.Node) + Warnf(format string, a ...interface{}) } type logger struct{} @@ -20,6 +22,10 @@ func (l *logger) Logf(format string, a ...interface{}) { fmt.Printf(format+"\n", a...) } +func (l *logger) Warnf(format string, a ...interface{}) { + fmt.Printf("WARN: "+format+"\n", a...) +} + func (l *logger) LogNode(message string, n *html.Node) { if n == nil { fmt.Printf("%s: \n", message) diff --git a/integration_tests/main.go b/integration_tests/main.go deleted file mode 100644 index e0a30d4..0000000 --- a/integration_tests/main.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" -) - -func main() { - var apiPath string - flag.StringVar(&apiPath, "api-path", "", "Api path") - var generate bool - flag.BoolVar(&generate, "generate", false, "Generate API") - - flag.Parse() - - logger := NewLogger() - if generate { - parser := NewParser(logger) - apiDef, err := parser.ParseApi() - if err != nil { - showError("Error while parsing API definition", err) - return - } - err = StoreApi(apiDef, apiPath) - if err != nil { - showError("Error while storing API definition", err) - } - } else { - api, err := LoadApi(apiPath) - if err != nil { - showError("Error while loadin API", err) - return - } - logger.Logf("Api def loaded, count: %d", len(api)) - runner := NewRunner(api, logger) - err = runner.RunTests() - if err != nil { - showError("", err) - } - } -} - -func showError(prefix string, err error) { - if err != nil { - fmt.Printf("%s: %s\n", prefix, err.Error()) - } else { - fmt.Print(prefix + "\n") - } - os.Exit(1) -} diff --git a/integration_tests/objects.go b/integration_tests/objects.go index 026891d..c1e7973 100644 --- a/integration_tests/objects.go +++ b/integration_tests/objects.go @@ -1,4 +1,4 @@ -package main +package integration_tests type ApiDef struct { Method string diff --git a/integration_tests/runner.go b/integration_tests/runner.go index 7170ee2..2002f6a 100644 --- a/integration_tests/runner.go +++ b/integration_tests/runner.go @@ -1,4 +1,4 @@ -package main +package integration_tests import ( "encoding/json" @@ -9,6 +9,7 @@ import ( "os" "strconv" "strings" + "testing" "time" cli "github.com/centurylinkcloud/clc-go-cli" @@ -20,132 +21,78 @@ import ( arg_parser "github.com/centurylinkcloud/clc-go-cli/parser" ) +// Runner represents a integration test suite type Runner interface { - RunTests() error -} - -func NewRunner(api []*ApiDef, logger Logger) Runner { - r := &runner{} - r.logger = logger - r.api = api - r.addServeMux() - return r + RunTests(t *testing.T) error } type runner struct { api []*ApiDef - logger Logger serveMux *http.ServeMux server *httptest.Server + trace bool } -func (r *runner) addServeMux() { - baseMux := http.NewServeMux() - r.serveMux = http.NewServeMux() - r.server = httptest.NewServer(baseMux) - connection.BaseUrl = r.server.URL + "/" - - baseMux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - req.URL.Path = strings.ToLower(req.URL.Path) - r.serveMux.ServeHTTP(w, req) - })) +// NewRunner will create a new instance of a Runner +// for a supplied api definition. +func NewRunner(api []*ApiDef, trace bool) Runner { + r := &runner{} + r.api = api + r.addServeMux() + r.trace = trace + return r } -func (r *runner) RunTests() error { - os.Setenv("CLC_TRACE", "true") +func (r *runner) RunTests(t *testing.T) error { + if r.trace { + os.Setenv("CLC_TRACE", "true") + } + for _, command := range cli.AllCommands { if cmdBase, ok := command.(*commands.CommandBase); ok { - err := r.TestCommand(cmdBase) - if err != nil { - return err - } - } - } - r.logger.Logf("Test execution finished succcessfully") - return nil -} + var subTestName = fmt.Sprintf("%s %s", cmdBase.ExcInfo.Resource, cmdBase.ExcInfo.Command) + t.Run(subTestName, func(t *testing.T) { + err := r.TestCommand(cmdBase, t) + if err != nil { + t.Errorf("Error executing test: %v", err) + return + } -func (r *runner) addLoginHandler() error { - resModel := &authentication.LoginRes{AccountAlias: "ALIAS", BearerToken: "token"} - response, err := json.Marshal(resModel) - if err != nil { - return err - } - checker := func(req string) error { - reqModel := &authentication.LoginReq{} - err := json.Unmarshal([]byte(req), &reqModel) - if err != nil { - return err - } - if reqModel.Username != "user" || reqModel.Password != "password" { - return fmt.Errorf("Incorrect request model: %#v", reqModel) + t.Logf("Test completed without error") + + }) } - return nil } - r.addHandlerBase("/v2/authentication/login", string(response), checker) - return nil -} -func (r *runner) addHandler(url string, response string, checker func(string) error) error { - r.logger.Logf("Adding httpHandler for url: %s", url) - r.addServeMux() - err := r.addLoginHandler() - if err != nil { - return err - } - r.addHandlerBase(url, response, checker) return nil } -func (r *runner) addHandlerBase(url string, response string, checker func(string) error) { - url = strings.ToLower(url) - r.serveMux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { - reqContent, err := ioutil.ReadAll(req.Body) - if err != nil { - panic(err) - } - if checker != nil { - err := checker(string(reqContent)) - if err != nil { - panic(err) - } - } - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(response)) - }) -} - -func (r *runner) findApiDef(url, method string) (*ApiDef, error) { - if method == "PATCH" { - return nil, nil - } - if strings.Contains(url, "?") { - return nil, nil +func (r *runner) TestCommand(cmd *commands.CommandBase, t *testing.T) (err error) { + if cmd.ExcInfo.Verb == "PATCH" { + t.Skipf("HTTP PATCH isn't supported by the integration tests") + return nil } - for _, apiDef := range r.api { - apiDef.Url = strings.Replace(apiDef.Url, "{sourceAccountAlias}", "{accountAlias}", -1) - apiUrl := strings.Replace(apiDef.Url, "locationId", "DataCenter", -1) - apiUrl = strings.Replace(apiUrl, "Network", "NetworkId", -1) - if strings.EqualFold(apiUrl, url) && apiDef.Method == method { - return apiDef, nil - } + if strings.Contains(cmd.ExcInfo.Url, "?") { + t.Skipf("URLs with a query string aren't supported by the integration tests") + return nil } - return nil, fmt.Errorf("Api definition for url %s and method %s not found", url, method) -} -func (r *runner) TestCommand(cmd *commands.CommandBase) (err error) { - r.logger.Logf("------- Testing command %s %s", cmd.ExcInfo.Resource, cmd.ExcInfo.Command) - apiDef, err := r.findApiDef(cmd.ExcInfo.Url, cmd.ExcInfo.Verb) - if err != nil { - return err - } - //skip patch operations for now + apiDef := r.findApiDef(cmd.ExcInfo.Url, cmd.ExcInfo.Verb) if apiDef == nil { + t.Skipf("Api definition for url %s and method %s not found", cmd.ExcInfo.Url, cmd.ExcInfo.Verb) + return nil + } + + //TODO: Handle commands that are wrappers around execute package. Ignore the commands for now + if r.isExecutePackageWrapper(cmd) { + t.Skipf("Commands that are a wrapper around execute package aren't supported by the integration tests.") return nil } + args := []string{cmd.ExcInfo.Resource, cmd.ExcInfo.Command} - defaultId := "some-id" + defaultID := "some-id" r.initialModifyContent(apiDef) + t.Logf("API url: %s\n", apiDef.Url) url := apiDef.Url url = strings.Replace(url, "https://api.ctl.io", "", -1) url = strings.Replace(url, "{accountAlias}", "ALIAS", -1) @@ -176,24 +123,26 @@ func (r *runner) TestCommand(cmd *commands.CommandBase) (err error) { paramName := strings.Replace(param.Name, "IP", "Ip", -1) paramName = strings.Replace(paramName, "ID", "Id", -1) if paramName != "AccountAlias" && paramName != "LocationId" && !strings.EqualFold(paramName, "sourceAccountAlias") { - args = append(args, arg_parser.DenormalizePropertyName(paramName), defaultId) - url = strings.Replace(url, "{"+strings.ToLower(paramName)+"}", defaultId, -1) + args = append(args, arg_parser.DenormalizePropertyName(paramName), defaultID) + url = strings.Replace(url, "{"+strings.ToLower(paramName)+"}", defaultID, -1) } else if paramName == "LocationId" { - args = append(args, "--data-center", defaultId) - url = strings.Replace(url, "{locationid}", defaultId, -1) + args = append(args, "--data-center", defaultID) + url = strings.Replace(url, "{locationid}", defaultID, -1) } } args = append(args, "--user", "user", "--password", "password") err = r.addHandler(url, string(resExampleString), func(req string) error { + t.Logf("Json1: %s", string(contentExampleString)) + t.Logf("Json2: %s", req) return r.compareJson(string(contentExampleString), req) }) if err != nil { return err } - r.logger.Logf("Args: %v", args) + t.Logf("Args: %v", args) res := clc.Run(args) - r.logger.Logf("Result received: %s", res) + t.Logf("Result received: %s", res) if res == "" { return nil } @@ -206,6 +155,79 @@ func (r *runner) TestCommand(cmd *commands.CommandBase) (err error) { return r.deepCompareObjects("", r.postModifyResExample(apiDef.ResExample), *obj) } +func (r *runner) addServeMux() { + baseMux := http.NewServeMux() + r.serveMux = http.NewServeMux() + r.server = httptest.NewServer(baseMux) + connection.BaseUrl = r.server.URL + "/" + + baseMux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + req.URL.Path = strings.ToLower(req.URL.Path) + r.serveMux.ServeHTTP(w, req) + })) +} + +func (r *runner) addLoginHandler() error { + resModel := &authentication.LoginRes{AccountAlias: "ALIAS", BearerToken: "token"} + response, err := json.Marshal(resModel) + if err != nil { + return err + } + checker := func(req string) error { + reqModel := &authentication.LoginReq{} + err := json.Unmarshal([]byte(req), &reqModel) + if err != nil { + return err + } + if reqModel.Username != "user" || reqModel.Password != "password" { + return fmt.Errorf("Incorrect request model: %#v", reqModel) + } + return nil + } + r.addHandlerBase("/v2/authentication/login", string(response), checker) + return nil +} + +func (r *runner) addHandler(url string, response string, checker func(string) error) error { + r.addServeMux() + err := r.addLoginHandler() + if err != nil { + return err + } + r.addHandlerBase(url, response, checker) + return nil +} + +func (r *runner) addHandlerBase(url string, response string, checker func(string) error) { + url = strings.ToLower(url) + r.serveMux.HandleFunc(url, func(w http.ResponseWriter, req *http.Request) { + reqContent, err := ioutil.ReadAll(req.Body) + if err != nil { + panic(err) + } + if checker != nil { + err := checker(string(reqContent)) + if err != nil { + panic(err) + } + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(response)) + }) +} + +func (r *runner) findApiDef(url, method string) *ApiDef { + for _, apiDef := range r.api { + apiDef.Url = strings.Replace(apiDef.Url, "{sourceAccountAlias}", "{accountAlias}", -1) + apiURL := strings.Replace(apiDef.Url, "locationId", "DataCenter", -1) + apiURL = strings.Replace(apiURL, "Network", "NetworkId", -1) + if strings.EqualFold(apiURL, url) && apiDef.Method == method { + return apiDef + } + } + return nil +} + func (r *runner) postModifyResExample(obj interface{}) interface{} { switch obj.(type) { case map[string]interface{}: @@ -229,7 +251,7 @@ func (r *runner) postModifyResExample(obj interface{}) interface{} { } func (r *runner) modifyResExample(apiDef *ApiDef) { - additionalProperties := []AdditionalProperty{ + additionalProperties := []additionalProperty{ {"POST", "https://api.ctl.io/v2/groups/{accountAlias}", "serversCount", 1}, {"GET", "https://api.ctl.io/v2/servers/{accountAlias}/{serverId}", "os", "some-os"}, } @@ -248,7 +270,7 @@ func (r *runner) modifyResExample(apiDef *ApiDef) { } func (r *runner) initialModifyContent(apiDef *ApiDef) { - additionalProperties := []AdditionalProperty{ + additionalProperties := []additionalProperty{ {"POST", "https://api.ctl.io/v2/servers/{accountAlias}", "isManagedBackup", true}, } for _, prop := range additionalProperties { @@ -257,7 +279,7 @@ func (r *runner) initialModifyContent(apiDef *ApiDef) { } } - missedExamples := []MissedExample{ + missedExamples := []missedExample{ {"POST", "https://api.ctl.io/v2/operations/{accountAlias}/servers/startMaintenance", []interface{}{"WA1ALIASWB01", "WA1ALIASWB02"}}, {"POST", "https://api.ctl.io/v2/groups/{accountAlias}/{groupId}/restore", map[string]interface{}{"targetGroupId": "WA1ALIASWB02"}}, } @@ -283,6 +305,19 @@ func (r *runner) initialModifyContent(apiDef *ApiDef) { } } +func (r *runner) isExecutePackageWrapper(cmd *commands.CommandBase) bool { + wrappers := []ExecutePackageWrapper{ + {"os-patch", "apply"}, + } + + for _, wrapper := range wrappers { + if cmd.ExcInfo.Resource == wrapper.Resource && cmd.ExcInfo.Command == wrapper.Command { + return true + } + } + return false +} + func (r *runner) postModifyContent(apiDef *ApiDef) (string, error) { contentExample := apiDef.ContentExample if array, ok := contentExample.([]interface{}); ok { @@ -321,6 +356,7 @@ func (r *runner) postModifyContent(apiDef *ApiDef) (string, error) { {"PUT", "https://api.ctl.io/v2/servers/{accountAlias}/{serverId}/cpuAutoscalePolicy", "id", "policyId"}, {"PUT", "https://api.ctl.io/v2/groups/{accountAlias}/{groupId}/horizontalAutoscalePolicy/", "loadBalancerPool", "loadBalancer"}, {"POST", "https://api.ctl.io/v2/groups/{accountAlias}/{groupId}/defaults", "memoryGB", "memoryGb"}, + {"POST", "https://api.ctl.io/v2/operations/{accountAlias}/servers/executePackage", "servers", "serverIds"}, } data, err := json.Marshal(contentExample) if err != nil { @@ -336,8 +372,6 @@ func (r *runner) postModifyContent(apiDef *ApiDef) (string, error) { } func (r *runner) compareJson(json1, json2 string) error { - r.logger.Logf("Json1: %s", json1) - r.logger.Logf("Json2: %s", json2) if strings.TrimSpace(json1) == "" && strings.TrimSpace(json2) == "" { return nil } @@ -365,12 +399,12 @@ func (r *runner) deepCompareObjects(prefix string, obj1 interface{}, obj2 interf if array, ok := obj2.([]interface{}); ok && len(array) == 0 { return nil } - return fmt.Errorf("Mistmatch in property %s. Values: \n%v \n%v", prefix, obj1, obj2) + return fmt.Errorf("Mismatch in property %s. Values: \n%v \n%v", prefix, obj1, obj2) } switch obj1.(type) { case string, float64, bool: if fmt.Sprintf("%v", obj1) != fmt.Sprintf("%v", obj2) { - return fmt.Errorf("Mistmatch in property %s. Values: %v %v", prefix, obj1, obj2) + return fmt.Errorf("Mismatch in property %s. Values: %v %v", prefix, obj1, obj2) } return nil case []interface{}: @@ -426,12 +460,16 @@ type convertProperty struct { Method, Url, OldName, NewName string } -type AdditionalProperty struct { +type additionalProperty struct { Method, Url, Name string Value interface{} } -type MissedExample struct { +type missedExample struct { Method, Url string Example interface{} } + +type ExecutePackageWrapper struct { + Resource, Command string +} diff --git a/models/autoscale/remove_on_server.go b/models/autoscale/remove_on_server.go index b32abb1..3d7d876 100644 --- a/models/autoscale/remove_on_server.go +++ b/models/autoscale/remove_on_server.go @@ -5,5 +5,5 @@ import ( ) type RemoveOnServerReq struct { - server.Server `argument:"composed" URIParam:"ServerId"` + server.Server `argument:"composed" URIParam:"ServerId" json:"-"` } diff --git a/models/balancer/create.go b/models/balancer/create.go index 5985dfb..fe4f2a9 100644 --- a/models/balancer/create.go +++ b/models/balancer/create.go @@ -4,5 +4,5 @@ type Create struct { DataCenter string `json:"-" valid:"required" URIParam:"yes"` Name string `valid:"required"` Description string `valid:"required"` - Status string `json:",omitempty" oneOf:"enabled,disabled"` + Status string `json:",omitempty" oneOf:"enabled,disabled,optional"` } diff --git a/models/balancer/delete_pool.go b/models/balancer/delete_pool.go index ad96803..2dea609 100644 --- a/models/balancer/delete_pool.go +++ b/models/balancer/delete_pool.go @@ -2,5 +2,5 @@ package balancer type DeletePool struct { LoadBalancer `argument:"composed" URIParam:"LoadBalancerId,DataCenter" json:"-"` - PoolId string `valid:"required" URIParam:"yes"` + PoolId string `valid:"required" URIParam:"yes" json:"-"` } diff --git a/models/balancer/update.go b/models/balancer/update.go index 0009401..2ba4a57 100644 --- a/models/balancer/update.go +++ b/models/balancer/update.go @@ -4,5 +4,5 @@ type Update struct { LoadBalancer `argument:"composed" URIParam:"LoadBalancerId,DataCenter" json:"-"` Name string `valid:"required"` Description string `valid:"required"` - Status string `json:",omitempty" oneOf:"enabled,disabled"` + Status string `json:",omitempty" oneOf:"enabled,disabled,optional"` } diff --git a/models/datacenter/get_baremetal_capabilities.go b/models/datacenter/get_baremetal_capabilities.go new file mode 100644 index 0000000..b5d5270 --- /dev/null +++ b/models/datacenter/get_baremetal_capabilities.go @@ -0,0 +1,41 @@ +package datacenter + +type GetBMCapReq struct { + DataCenter string `valid:"required" URIParam:"yes"` +} + +type GetBMCapRes struct { + SKUs []SKU `json:"skus"` + OperatingSystems []OperatingSystem `json:"operatingSystems"` +} + +type SKU struct { + ID string `json:"id"` + HourlyRate float32 `json:"hourlyRate"` + Availability string `json:"availability"` + Memory []Memory `json:"memory"` + Processor Processor `json:"processor"` + Storage []Storage `json:"storage"` +} + +type Memory struct { + CapacityInGB int `json:"capacityGB"` +} + +type Processor struct { + Sockets int `json:"sockets"` + CoresPerSocket int `json:"coresPerSocket"` + Description string `json:"description"` +} + +type Storage struct { + Type string `json:"type"` + CapacityInGB int `json:"capacityGB"` + SpeedInRPM int `json:"speedRpm"` +} + +type OperatingSystem struct { + Type string `json:"type"` + Description string `json:"description"` + HourlyRatePerSocket float32 `json:"hourlyRatePerSocket"` +} diff --git a/models/datacenter/get_deployment_capabilities.go b/models/datacenter/get_deployment_capabilities.go index 63566d6..af6cdd4 100644 --- a/models/datacenter/get_deployment_capabilities.go +++ b/models/datacenter/get_deployment_capabilities.go @@ -5,7 +5,6 @@ type GetDCReq struct { } type GetDCRes struct { - SupportsPremiumStorage bool SupportsSharedLoadBalancer bool SupportsBareMetalServers bool DeployableNetworks []DeployableNetwork diff --git a/models/firewall/delete.go b/models/firewall/delete.go index 229415f..0a770d4 100644 --- a/models/firewall/delete.go +++ b/models/firewall/delete.go @@ -1,6 +1,6 @@ package firewall type DeleteReq struct { - DataCenter string `valid:"required" URIParam:"yes"` - FirewallPolicy string `valid:"required" URIParam:"yes"` + DataCenter string `valid:"required" URIParam:"yes" json:"-"` + FirewallPolicy string `valid:"required" URIParam:"yes" json:"-"` } diff --git a/models/server/create.go b/models/server/create.go index e175631..9204130 100644 --- a/models/server/create.go +++ b/models/server/create.go @@ -2,12 +2,13 @@ package server import ( "fmt" + "time" + "github.com/centurylinkcloud/clc-go-cli/base" "github.com/centurylinkcloud/clc-go-cli/models/affinity" "github.com/centurylinkcloud/clc-go-cli/models/customfields" "github.com/centurylinkcloud/clc-go-cli/models/group" "github.com/centurylinkcloud/clc-go-cli/models/network" - "time" ) type CreateReq struct { @@ -27,11 +28,10 @@ type CreateReq struct { IpAddress string `json:",omitempty"` RootPassword string `json:"Password,omitempty"` SourceServerPassword string `json:",omitempty"` - Cpu int64 `valid:"required"` + Cpu int64 `json:",omitempty` CpuAutoscalePolicyId string `json:",omitempty"` - MemoryGb int64 `valid:"required"` + MemoryGb int64 `json:",omitempty` Type string `valid:"required" oneOf:"standard,hyperscale,bareMetal"` - StorageType string `json:",omitempty" oneOf:"standard,premium,hyperscale"` AntiAffinityPolicyId string `json:",omitempty"` AntiAffinityPolicyName string `json:",omitempty"` CustomFields []customfields.Def `json:",omitempty"` @@ -44,15 +44,24 @@ type CreateReq struct { } func (c *CreateReq) Validate() error { - serverIdValues := []string{c.SourceServerId, c.SourceServerName, c.TemplateName} - numNonEmpty := 0 - for _, item := range serverIdValues { - if item != "" { - numNonEmpty++ + if c.Type == "standard" || c.Type == "hyperscale" { + if c.Cpu == 0 { + return fmt.Errorf("Cpu: required for standard and hyperscale servers.") + } + if c.MemoryGb == 0 { + return fmt.Errorf("MemoryGb: required for standard and hyperscale servers.") + } + + serverIdValues := []string{c.SourceServerId, c.SourceServerName, c.TemplateName} + numNonEmpty := 0 + for _, item := range serverIdValues { + if item != "" { + numNonEmpty++ + } + } + if numNonEmpty > 1 || numNonEmpty == 0 { + return fmt.Errorf("Exactly one parameter from the following: source-server-id, source-server-name, template-name must be specified.") } - } - if numNonEmpty > 1 || numNonEmpty == 0 { - return fmt.Errorf("Exactly one parameter from the following: source-server-id, source-server-name, template-name must be specified.") } if (c.GroupName == "") == (c.GroupId == "") { diff --git a/models/server/delete.go b/models/server/delete.go index e9189d3..686ce72 100644 --- a/models/server/delete.go +++ b/models/server/delete.go @@ -1,5 +1,5 @@ package server type DeleteReq struct { - Server `argument:"composed" URIParam:"ServerId"` + Server `argument:"composed" URIParam:"ServerId" json:"-"` } diff --git a/models/server/delete_snapshot.go b/models/server/delete_snapshot.go index 5c5509b..c5db8b0 100644 --- a/models/server/delete_snapshot.go +++ b/models/server/delete_snapshot.go @@ -1,6 +1,6 @@ package server type DeleteSnapshotReq struct { - Server `argument:"composed" URIParam:"ServerId"` - SnapshotId string `valid:"required" URIParam:"yes"` + Server `argument:"composed" URIParam:"ServerId" json:"-"` + SnapshotId string `valid:"required" URIParam:"yes" json:"-"` } diff --git a/models/server/entities.go b/models/server/entities.go index c068d8c..474e0d3 100644 --- a/models/server/entities.go +++ b/models/server/entities.go @@ -37,23 +37,27 @@ type PackageDef struct { } type Details struct { - IpAddresses []IPAddresses - AlertPolicies []alert.AlertPolicy - Cpu int64 - DiskCount int64 - HostName string - InMaintenanceMode bool - MemoryMB int64 - PowerState string - StorageGB int64 - Disks []Disk - Partitions []Partition - Snapshots []Snapshot - CustomFields []customfields.FullDef + IpAddresses []IPAddresses + SecondaryIPAddresses []IPAddresses + AlertPolicies []alert.AlertPolicy + Cpu int64 + DiskCount int64 + HostName string + InMaintenanceMode bool + MemoryMB int64 + PowerState string + StorageGB int64 + Disks []Disk + Partitions []Partition + Snapshots []Snapshot + CustomFields []customfields.FullDef + ProcessorDescription string `json:",omitempty"` + StorageDescription string `json:",omitempty"` + IsManagedBackup bool `json:",omitempty"` } type IPAddresses struct { - Public string + Public string `json:",omitempty"` Internal string } diff --git a/models/server/get.go b/models/server/get.go index 813641e..ec96fcc 100644 --- a/models/server/get.go +++ b/models/server/get.go @@ -17,11 +17,11 @@ type GetRes struct { IsTemplate bool LocationId string OsType string + IsManagedOS bool Os string Status string Details Details Type string - StorageType string ChangeInfo models.ChangeInfo Links []models.LinkEntity } diff --git a/models/server/import.go b/models/server/import.go index 482af8e..cc9a6c7 100644 --- a/models/server/import.go +++ b/models/server/import.go @@ -2,6 +2,7 @@ package server import ( "fmt" + "github.com/centurylinkcloud/clc-go-cli/base" "github.com/centurylinkcloud/clc-go-cli/models/customfields" "github.com/centurylinkcloud/clc-go-cli/models/group" @@ -21,7 +22,6 @@ type Import struct { Cpu int64 `valid:"required"` MemoryGb int64 `valid:"required"` Type string `valid:"required" oneOf:"standard,hyperscale"` - StorageType string `json:",omitempty" oneOf:"standard,premium,hyperscale"` CustomFields []customfields.Def `json:",omitempty"` OvfId string `valid:"required"` OvfOsType string `valid:"required"` diff --git a/models/server/remove_ip_address.go b/models/server/remove_ip_address.go index 634e048..3325d4a 100644 --- a/models/server/remove_ip_address.go +++ b/models/server/remove_ip_address.go @@ -1,6 +1,6 @@ package server type RemoveIPAddressReq struct { - Server `argument:"composed" URIParam:"ServerId"` - PublicIp string `valid:"required" URIParam:"yes"` + Server `argument:"composed" URIParam:"ServerId" json:"-"` + PublicIp string `valid:"required" URIParam:"yes" json:"-"` } diff --git a/models/server/secondary_network.go b/models/server/secondary_network.go index 8d6acc1..a78e914 100644 --- a/models/server/secondary_network.go +++ b/models/server/secondary_network.go @@ -2,6 +2,7 @@ package server import ( "fmt" + "github.com/centurylinkcloud/clc-go-cli/base" "github.com/centurylinkcloud/clc-go-cli/models/network" ) @@ -15,10 +16,10 @@ type AddNetwork struct { } type RemoveNetwork struct { - ServerId string `URIParam:"yes"` - ServerName string - NetworkId string `URIParam:"yes"` - NetworkName string + ServerId string `URIParam:"yes" json:"-"` + ServerName string `json:"-"` + NetworkId string `URIParam:"yes" json:"-"` + NetworkName string `json:"-"` } func (a *AddNetwork) Validate() error { diff --git a/models/server/update.go b/models/server/update.go index 4b5f67e..7b32ec0 100644 --- a/models/server/update.go +++ b/models/server/update.go @@ -3,6 +3,7 @@ package server import ( "encoding/json" "fmt" + "github.com/centurylinkcloud/clc-go-cli/base" "github.com/centurylinkcloud/clc-go-cli/models/customfields" "github.com/centurylinkcloud/clc-go-cli/models/group" @@ -141,7 +142,7 @@ func (ur *UpdateReq) ApplyDefaultBehaviour() error { } ur.PatchOperation = append(ur.PatchOperation, op) } - if len(ur.Disks.Add) != 0 && len(ur.Disks.Keep) != 0 { + if len(ur.Disks.Add) != 0 || len(ur.Disks.Keep) != 0 { op := ServerPatchOperation{ Op: "set", Member: "disks", diff --git a/models/webhook/add-targeturi.go b/models/webhook/add-targeturi.go new file mode 100644 index 0000000..a3753fb --- /dev/null +++ b/models/webhook/add-targeturi.go @@ -0,0 +1,23 @@ +package webhook + +// AddTargetURIReq represents a request to add a webhook. It will add a target uri to be called on a specific event. +type AddTargetURIReq struct { + Event string `json:"-" valid:"required" URIParam:"yes"` + TargetUri string `valid:"required" json:"targetUri"` +} + +// Validate provides custom validation for the AddTargetURIReq request +func (c *AddTargetURIReq) Validate() error { + + err := ValidateTargetURI(c.TargetUri) + if err != nil { + return err + } + + err = ValidateEvent(c.Event) + if err != nil { + return err + } + + return nil +} diff --git a/models/webhook/delete-targeturi.go b/models/webhook/delete-targeturi.go new file mode 100644 index 0000000..28ed2fe --- /dev/null +++ b/models/webhook/delete-targeturi.go @@ -0,0 +1,23 @@ +package webhook + +// DeleteTargetURIReq represents a request to delete a specific target uri associated with a webhook for a given event +type DeleteTargetURIReq struct { + Event string `json:"-" valid:"required" URIParam:"yes"` + TargetUri string `URIParam:"yes" valid:"required"` +} + +// Validate provides custom validation for the DeleteTargetURIReq request +func (c *DeleteTargetURIReq) Validate() error { + + err := ValidateTargetURI(c.TargetUri) + if err != nil { + return err + } + + err = ValidateEvent(c.Event) + if err != nil { + return err + } + + return nil +} diff --git a/models/webhook/delete.go b/models/webhook/delete.go new file mode 100644 index 0000000..19b5201 --- /dev/null +++ b/models/webhook/delete.go @@ -0,0 +1,17 @@ +package webhook + +// DeleteReq represents a request to delete all the target uris associated with a webhook for a given event +type DeleteReq struct { + Event string `json:"-" valid:"required" URIParam:"yes"` +} + +// Validate provides custom validation for the AddTargetURIReq request +func (c *DeleteReq) Validate() error { + + err := ValidateEvent(c.Event) + if err != nil { + return err + } + + return nil +} diff --git a/models/webhook/entities.go b/models/webhook/entities.go new file mode 100644 index 0000000..210f2af --- /dev/null +++ b/models/webhook/entities.go @@ -0,0 +1,20 @@ +package webhook + +import ( + "github.com/centurylinkcloud/clc-go-cli/models" +) + +type Webhook struct { + Name string + Configuration Configuration + Links []models.LinkEntity +} + +type Configuration struct { + Recursive bool + TargetUris []TargetUri +} + +type TargetUri struct { + TargetUri string +} diff --git a/models/webhook/list.go b/models/webhook/list.go new file mode 100644 index 0000000..5f7bedf --- /dev/null +++ b/models/webhook/list.go @@ -0,0 +1,6 @@ +package webhook + +// ListRes represents a list of webhooks +type ListRes struct { + Items []Webhook +} diff --git a/models/webhook/update.go b/models/webhook/update.go new file mode 100644 index 0000000..6553abb --- /dev/null +++ b/models/webhook/update.go @@ -0,0 +1,31 @@ +package webhook + +// UpdateReq represents a request to update a webhook for a given event +type UpdateReq struct { + Event string `json:"-" valid:"required" URIParam:"yes"` + Recursive string `json:"recursive" oneOf:"true,false"` + TargetUris []TargetUri `json:"targetUris,omitempty"` +} + +// Validate provides custom validation for the UpdateReq request +func (c *UpdateReq) Validate() error { + + if c.TargetUris != nil { + if len(c.TargetUris) > 0 { + + for _, targetURI := range c.TargetUris { + err := ValidateTargetURI(targetURI.TargetUri) + if err != nil { + return err + } + } + } + } + + err := ValidateEvent(c.Event) + if err != nil { + return err + } + + return nil +} diff --git a/models/webhook/validation.go b/models/webhook/validation.go new file mode 100644 index 0000000..8806f32 --- /dev/null +++ b/models/webhook/validation.go @@ -0,0 +1,48 @@ +package webhook + +import ( + "fmt" + "net/url" + "strings" + + "github.com/asaskevich/govalidator" +) + +var allowedEvents = []string{ + "Account.Created", "Account.Deleted", "Account.Updated", "Alert.Notification", "Server.Created", "Server.Deleted", "Server.Updated", + "User.Created", "User.Deleted", "User.Updated"} + +// ValidateTargetURI provides validation checks for the targetUri of a webhook +func ValidateTargetURI(targetURI string) error { + + validURL := govalidator.IsURL(targetURI) + if !validURL { + return fmt.Errorf("TargetUri: invalid URI.") + } + + url, _ := url.Parse(targetURI) + if strings.ToLower(url.Scheme) != "https" { + return fmt.Errorf("TargetUri: must be an HTTPS endpoint.") + } + + return nil +} + +// ValidateEvent provides validation checks for the event name of a webhook +func ValidateEvent(eventName string) error { + + if stringInSlice(eventName, allowedEvents) == false { + return fmt.Errorf("Event: %s is an invalid event. Allowed events are: %s", eventName, allowedEvents) + } + + return nil +} + +func stringInSlice(str string, list []string) bool { + for _, v := range list { + if v == str { + return true + } + } + return false +} diff --git a/run_integraion_tests b/run_integraion_tests index 9c351d4..380ba6e 100755 --- a/run_integraion_tests +++ b/run_integraion_tests @@ -2,8 +2,6 @@ set -e -echo -e "Generating Binary..." -go build -o ./out/tests integration_tests/*.go - echo -e "Run tests..." -./out/tests -api-path $(pwd -P)/integration_tests/api.json +go test ./integration_tests -v --tags=integration -api-path $(pwd -P)/integration_tests/api.json + diff --git a/scripts/build_releases b/scripts/build_releases index 80e0d6d..3592ebb 100755 --- a/scripts/build_releases +++ b/scripts/build_releases @@ -11,9 +11,13 @@ fi echo "Building the version $version" echo -echo "Overriding the user agent" -sed "s/VERSION = \".*\"/VERSION = \"$version\"/g" base/constants.go > _constants && mv _constants base/constants.go -echo +echo "Generating build variables" +buildDate=`date -u '+%Y-%m-%d_%I:%M:%S%p'` +buildCommit=`git rev-parse HEAD` +echo "Build version: $version" +echo "Build date: $buildDate" +echo "Build commit: $buildCommit" + release_folder=release mkdir -p $release_folder @@ -38,7 +42,7 @@ do echo "Building $binary for $GOOS/$GOARCH.." - GO_ENABLED=0 GOOS=$GOOS GOARCH=$GOARCH go build -o $binary cmd/clc/* + GO_ENABLED=0 GOOS=$GOOS GOARCH=$GOARCH go build -o $binary -ldflags "-X github.com/centurylinkcloud/clc-go-cli/base.BuildVersion=$version -X github.com/centurylinkcloud/clc-go-cli/base.BuildDate=$buildDate -X github.com/centurylinkcloud/clc-go-cli/base.BuildGitCommit=$buildCommit" cmd/clc/* echo "Adding autocomplete files for $GOOS/$GOARCH.." @@ -76,7 +80,4 @@ do echo echo done -done - -echo "Reverting the user agent" -git checkout base/constants.go +done \ No newline at end of file diff --git a/scripts/generate_api b/scripts/generate_api index 58ce909..29b5921 100755 --- a/scripts/generate_api +++ b/scripts/generate_api @@ -3,7 +3,7 @@ set -e echo -e "Generating Binary..." -go build -o ./out/tests integration_tests/*.go +go build -o ./out/genapi cmd/genapi/*.go echo -e "Generating API" -./out/tests -generate -api-path $(readlink -f '.')/integration_tests/api.json +./out/genapi -api-path $(readlink -f '.')/integration_tests/api_GENERATED.json