From 1dc8fee569860aaaa2c19eaeb08b6c19878d517b Mon Sep 17 00:00:00 2001 From: Johann Weging Date: Thu, 14 Mar 2019 14:24:27 +0100 Subject: [PATCH 01/11] add(application): Use structs to represent the Gate API schema --- spinnaker/resource_application.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/spinnaker/resource_application.go b/spinnaker/resource_application.go index 5b58fe7..f6a580e 100644 --- a/spinnaker/resource_application.go +++ b/spinnaker/resource_application.go @@ -27,11 +27,24 @@ func resourceApplication() *schema.Resource { } } +// application represents the Gate API schema +// +// HINT: to extend this schema have a look at the output +// of the spin (https://github.com/spinnaker/spin) +// application get command. +type application struct { + Name string `json:"name"` + Email string `json:"email"` + InstancePort int `json:"instancePort"` + Permissions map[string][]string `json:"permissions,omitempty"` +} + +// applicationRead represents the Gate API schema of an application +// get request. The relevenat part of the schema is identical with +// the application struct, it's just wrapped in an attributes field. type applicationRead struct { - Name string `json:"name"` - Attributes struct { - Email string `json:"email"` - } `json:"attributes"` + Name string `json:"name"` + Attributes *application `json:"attributes"` } func resourceApplicationCreate(data *schema.ResourceData, meta interface{}) error { From ae37f2040d8853b43aa86c821e97ce3f58dceaf7 Mon Sep 17 00:00:00 2001 From: Johann Weging Date: Thu, 14 Mar 2019 14:33:46 +0100 Subject: [PATCH 02/11] update(API): improve error message for GetApplication --- spinnaker/api/application.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spinnaker/api/application.go b/spinnaker/api/application.go index 012a49a..afc9f3c 100644 --- a/spinnaker/api/application.go +++ b/spinnaker/api/application.go @@ -16,7 +16,7 @@ func GetApplication(client *gate.GatewayClient, applicationName string, dest int if resp.StatusCode == http.StatusNotFound { return fmt.Errorf("Application '%s' not found\n", applicationName) } else if resp.StatusCode != http.StatusOK { - return fmt.Errorf("Encountered an error getting application, status code: %d\n", resp.StatusCode) + return fmt.Errorf("Encountered an error getting application: %s status code: %d\n", err, resp.StatusCode) } } From bf26f7f8ca3fa6eb7b50dd24a3acfc325d1df700 Mon Sep 17 00:00:00 2001 From: Johann Weging Date: Thu, 14 Mar 2019 14:40:33 +0100 Subject: [PATCH 03/11] update(API): Use application schema in CreateApplication --- spinnaker/api/application.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/spinnaker/api/application.go b/spinnaker/api/application.go index afc9f3c..a1b02ab 100644 --- a/spinnaker/api/application.go +++ b/spinnaker/api/application.go @@ -31,16 +31,10 @@ func GetApplication(client *gate.GatewayClient, applicationName string, dest int return nil } -func CreateApplication(client *gate.GatewayClient, applicationName, email string) error { - - app := map[string]interface{}{ - "instancePort": 80, - "name": applicationName, - "email": email, - } +func CreateApplication(client *gate.GatewayClient, applicationName string, application interface{}) error { createAppTask := map[string]interface{}{ - "job": []interface{}{map[string]interface{}{"type": "createApplication", "application": app}}, + "job": []interface{}{map[string]interface{}{"type": "createApplication", "application": application}}, "application": applicationName, "description": fmt.Sprintf("Create Application: %s", applicationName), } From 8336ba34b13fee510a2c3616fa2481a98081f0cc Mon Sep 17 00:00:00 2001 From: Johann Weging Date: Thu, 14 Mar 2019 14:45:32 +0100 Subject: [PATCH 04/11] update(application): Use application struct for CreateApplication API call --- spinnaker/resource_application.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spinnaker/resource_application.go b/spinnaker/resource_application.go index f6a580e..9a9a78b 100644 --- a/spinnaker/resource_application.go +++ b/spinnaker/resource_application.go @@ -50,10 +50,9 @@ type applicationRead struct { func resourceApplicationCreate(data *schema.ResourceData, meta interface{}) error { clientConfig := meta.(gateConfig) client := clientConfig.client - application := data.Get("application").(string) - email := data.Get("email").(string) - if err := api.CreateApplication(client, application, email); err != nil { + app := applicationFromResource(data) + if err := api.CreateApplication(client, app.Name, app); err != nil { return err } From 780ff48cd9917eb9a8100f8c22b4e9faba67e62c Mon Sep 17 00:00:00 2001 From: Johann Weging Date: Thu, 14 Mar 2019 14:51:59 +0100 Subject: [PATCH 05/11] update(application): Create applicationRead as pointer --- spinnaker/resource_application.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spinnaker/resource_application.go b/spinnaker/resource_application.go index 9a9a78b..3732926 100644 --- a/spinnaker/resource_application.go +++ b/spinnaker/resource_application.go @@ -62,9 +62,10 @@ func resourceApplicationCreate(data *schema.ResourceData, meta interface{}) erro func resourceApplicationRead(data *schema.ResourceData, meta interface{}) error { clientConfig := meta.(gateConfig) client := clientConfig.client + applicationName := data.Get("application").(string) - var app applicationRead - if err := api.GetApplication(client, applicationName, &app); err != nil { + app := &applicationRead{} + if err := api.GetApplication(client, applicationName, app); err != nil { return err } From d4c8fb2c3bda75e4dbda653c7e651570e735f2e0 Mon Sep 17 00:00:00 2001 From: Johann Weging Date: Thu, 14 Mar 2019 14:54:03 +0100 Subject: [PATCH 06/11] update(application): Add instance_port and permissions to the application schema --- spinnaker/resource_application.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/spinnaker/resource_application.go b/spinnaker/resource_application.go index 3732926..0a5c083 100644 --- a/spinnaker/resource_application.go +++ b/spinnaker/resource_application.go @@ -13,11 +13,25 @@ func resourceApplication() *schema.Resource { "application": { Type: schema.TypeString, Required: true, + ForceNew: true, }, "email": { Type: schema.TypeString, Required: true, }, + "instance_port": { + Type: schema.TypeInt, + Required: false, + Optional: true, + Default: 80, + }, + "permissions": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, }, Create: resourceApplicationCreate, Read: resourceApplicationRead, From 7201eb876baf2b4d67d43ea88cb0e6d5bd94ad44 Mon Sep 17 00:00:00 2001 From: Johann Weging Date: Thu, 14 Mar 2019 14:58:21 +0100 Subject: [PATCH 07/11] add(application): Add validation functions in the schema --- Gopkg.lock | 5 ++++- spinnaker/resource_application.go | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Gopkg.lock b/Gopkg.lock index 33c415d..a05f518 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -240,7 +240,7 @@ revision = "fa9f258a92500514cc8e9c67020487709df92432" [[projects]] - digest = "1:8ee6b299ec5e027376522696c4a2d236c5d4da19ccc23d8cc31a3530ce9cb26b" + digest = "1:55226d01af9764e7eb44603845b02dfac641b15489ab1b8a285cad577daef85f" name = "github.com/hashicorp/terraform" packages = [ "config", @@ -252,6 +252,8 @@ "helper/hashcode", "helper/hilmapstructure", "helper/schema", + "helper/structure", + "helper/validation", "httpclient", "moduledeps", "plugin", @@ -615,6 +617,7 @@ input-imports = [ "github.com/ghodss/yaml", "github.com/hashicorp/terraform/helper/schema", + "github.com/hashicorp/terraform/helper/validation", "github.com/hashicorp/terraform/plugin", "github.com/hashicorp/terraform/terraform", "github.com/mitchellh/mapstructure", diff --git a/spinnaker/resource_application.go b/spinnaker/resource_application.go index 0a5c083..d33184f 100644 --- a/spinnaker/resource_application.go +++ b/spinnaker/resource_application.go @@ -5,6 +5,7 @@ import ( "github.com/armory-io/terraform-provider-spinnaker/spinnaker/api" "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" ) func resourceApplication() *schema.Resource { @@ -24,12 +25,14 @@ func resourceApplication() *schema.Resource { Required: false, Optional: true, Default: 80, + ValidateFunc: validation.IntBetween(1, 65535), }, "permissions": { Type: schema.TypeMap, Optional: true, Elem: &schema.Schema{ Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"read", "write", "read_write"}, false), }, }, }, From 154b6aeada59572934f49d132a50fe26bcef74c4 Mon Sep 17 00:00:00 2001 From: Johann Weging Date: Thu, 14 Mar 2019 15:02:16 +0100 Subject: [PATCH 08/11] update(application): applicationFromResource reflects schema changes --- spinnaker/resource_application.go | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/spinnaker/resource_application.go b/spinnaker/resource_application.go index d33184f..14c21a1 100644 --- a/spinnaker/resource_application.go +++ b/spinnaker/resource_application.go @@ -122,7 +122,35 @@ func resourceApplicationExists(data *schema.ResourceData, meta interface{}) (boo return true, nil } -func readApplication(data *schema.ResourceData, application applicationRead) error { +func applicationFromResource(data *schema.ResourceData) *application { + app := &application{ + Name: data.Get("application").(string), + Email: data.Get("email").(string), + InstancePort: data.Get("instance_port").(int), + Permissions: make(map[string][]string), + } + + // convert {"team_name": "read_write"} to {"READ": ["team_name"], "WRITE": ["team_name"]} + // for the spinnaker API + readPerms := []string{} + writePerms := []string{} + for team, permI := range data.Get("permissions").(map[string]interface{}) { + perm := permI.(string) + if strings.HasPrefix(perm, "read") { + readPerms = append(readPerms, team) + } + if strings.HasSuffix(perm, "write") { + writePerms = append(writePerms, team) + } + + } + app.Permissions["READ"] = readPerms + app.Permissions["WRITE"] = writePerms + + return app +} + +func readApplication(data *schema.ResourceData, application *applicationRead) error { data.SetId(application.Name) return nil } From cd62d554c9bfd6395255d404fbea5d68e110786b Mon Sep 17 00:00:00 2001 From: Johann Weging Date: Thu, 14 Mar 2019 15:05:45 +0100 Subject: [PATCH 09/11] update(application): readApplication reflects schema changes --- spinnaker/resource_application.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spinnaker/resource_application.go b/spinnaker/resource_application.go index 14c21a1..3fac1e0 100644 --- a/spinnaker/resource_application.go +++ b/spinnaker/resource_application.go @@ -152,5 +152,25 @@ func applicationFromResource(data *schema.ResourceData) *application { func readApplication(data *schema.ResourceData, application *applicationRead) error { data.SetId(application.Name) + data.Set("name", application.Name) + data.Set("email", application.Attributes.Email) + data.Set("instance_port", application.Attributes.InstancePort) + + // convert {"READ": ["team_name"], "WRITE": ["team_name"]} to {"team_name": "read_write"} + // for the spinnaker API + perms := make(map[string]string) + for _, team := range application.Attributes.Permissions["READ"] { + perms[team] = "read" + } + for _, team := range application.Attributes.Permissions["WRITE"] { + perm, ok := perms[team] + if ok { + // perms contains "read", append undescore to create "read_write" + perm += "_" + } + perm += "write" + perms[team] = perm + } + data.Set("permissions", perms) return nil } From 5d3a4616788428b610570ad2138c71c0dbbdb186 Mon Sep 17 00:00:00 2001 From: Johann Weging Date: Thu, 14 Mar 2019 15:07:45 +0100 Subject: [PATCH 10/11] add(application): Implement resource update function --- spinnaker/resource_application.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/spinnaker/resource_application.go b/spinnaker/resource_application.go index 3fac1e0..f9eb89e 100644 --- a/spinnaker/resource_application.go +++ b/spinnaker/resource_application.go @@ -90,7 +90,16 @@ func resourceApplicationRead(data *schema.ResourceData, meta interface{}) error } func resourceApplicationUpdate(data *schema.ResourceData, meta interface{}) error { - return nil + // the application update in spinnaker is an simple upsert + clientConfig := meta.(gateConfig) + client := clientConfig.client + + app := applicationFromResource(data) + if err := api.CreateApplication(client, app.Name, app); err != nil { + return err + } + + return resourceApplicationRead(data, meta) } func resourceApplicationDelete(data *schema.ResourceData, meta interface{}) error { From 1a9d913863b9e388d0bc7f5ea17341a56b8702d7 Mon Sep 17 00:00:00 2001 From: Johann Weging Date: Thu, 14 Mar 2019 15:38:34 +0100 Subject: [PATCH 11/11] hack(API): Retry reading the application after creation on 403 --- spinnaker/api/application.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spinnaker/api/application.go b/spinnaker/api/application.go index a1b02ab..b6a7934 100644 --- a/spinnaker/api/application.go +++ b/spinnaker/api/application.go @@ -68,6 +68,26 @@ func CreateApplication(client *gate.GatewayClient, applicationName string, appli return fmt.Errorf("Encountered an error saving application, task output was: %v\n", task) } + // HACK: + // When creating an application with group permissions terraform fails to + // read the application due to a HTTP 403. It looks like the permissions + // are set without refreshing the cache. + // + // It looks like the responses of the gate API are cached. + // Try accessing the application until the cache timeout is reached + // and the API allows access to the application. + // The default redis cache timeout is 30 seconds. + attempts = 0 + for attempts < 10 { + _, resp, _ := client.ApplicationControllerApi.GetApplicationUsingGET(client.Context, applicationName, map[string]interface{}{}) + if resp != nil && resp.StatusCode != 403 { + break + } + + attempts += 1 + time.Sleep(time.Duration(10) * time.Second) + } + return nil }