github.com/skyscape-cloud-services/terraform@v0.9.2-0.20170609144644-7ece028a1747/builtin/providers/rancher/resource_rancher_stack.go (about) 1 package rancher 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "log" 7 "net/http" 8 "reflect" 9 "strings" 10 "time" 11 12 compose "github.com/docker/libcompose/config" 13 "github.com/hashicorp/terraform/helper/resource" 14 "github.com/hashicorp/terraform/helper/schema" 15 "github.com/hashicorp/terraform/helper/validation" 16 "github.com/rancher/go-rancher/catalog" 17 rancherClient "github.com/rancher/go-rancher/v2" 18 ) 19 20 func resourceRancherStack() *schema.Resource { 21 return &schema.Resource{ 22 Create: resourceRancherStackCreate, 23 Read: resourceRancherStackRead, 24 Update: resourceRancherStackUpdate, 25 Delete: resourceRancherStackDelete, 26 Importer: &schema.ResourceImporter{ 27 State: resourceRancherStackImport, 28 }, 29 30 Schema: map[string]*schema.Schema{ 31 "id": &schema.Schema{ 32 Type: schema.TypeString, 33 Computed: true, 34 }, 35 "name": &schema.Schema{ 36 Type: schema.TypeString, 37 Required: true, 38 }, 39 "description": &schema.Schema{ 40 Type: schema.TypeString, 41 Optional: true, 42 }, 43 "environment_id": { 44 Type: schema.TypeString, 45 Required: true, 46 ForceNew: true, 47 }, 48 "docker_compose": { 49 Type: schema.TypeString, 50 Optional: true, 51 DiffSuppressFunc: suppressComposeDiff, 52 }, 53 "rancher_compose": { 54 Type: schema.TypeString, 55 Optional: true, 56 DiffSuppressFunc: suppressComposeDiff, 57 }, 58 "environment": { 59 Type: schema.TypeMap, 60 Optional: true, 61 }, 62 "catalog_id": { 63 Type: schema.TypeString, 64 Optional: true, 65 }, 66 "scope": { 67 Type: schema.TypeString, 68 Default: "user", 69 Optional: true, 70 ValidateFunc: validation.StringInSlice([]string{"user", "system"}, true), 71 }, 72 "start_on_create": { 73 Type: schema.TypeBool, 74 Optional: true, 75 Computed: true, 76 }, 77 "finish_upgrade": { 78 Type: schema.TypeBool, 79 Optional: true, 80 }, 81 "rendered_docker_compose": { 82 Type: schema.TypeString, 83 Computed: true, 84 }, 85 "rendered_rancher_compose": { 86 Type: schema.TypeString, 87 Computed: true, 88 }, 89 }, 90 } 91 } 92 93 func resourceRancherStackCreate(d *schema.ResourceData, meta interface{}) error { 94 log.Printf("[INFO] Creating Stack: %s", d.Id()) 95 client, err := meta.(*Config).EnvironmentClient(d.Get("environment_id").(string)) 96 if err != nil { 97 return err 98 } 99 100 data, err := makeStackData(d, meta) 101 if err != nil { 102 return err 103 } 104 105 var newStack rancherClient.Stack 106 if err := client.Create("stack", data, &newStack); err != nil { 107 return err 108 } 109 110 stateConf := &resource.StateChangeConf{ 111 Pending: []string{"activating", "active", "removed", "removing"}, 112 Target: []string{"active"}, 113 Refresh: StackStateRefreshFunc(client, newStack.Id), 114 Timeout: 10 * time.Minute, 115 Delay: 1 * time.Second, 116 MinTimeout: 3 * time.Second, 117 } 118 _, waitErr := stateConf.WaitForState() 119 if waitErr != nil { 120 return fmt.Errorf( 121 "Error waiting for stack (%s) to be created: %s", newStack.Id, waitErr) 122 } 123 124 d.SetId(newStack.Id) 125 log.Printf("[INFO] Stack ID: %s", d.Id()) 126 127 return resourceRancherStackRead(d, meta) 128 } 129 130 func resourceRancherStackRead(d *schema.ResourceData, meta interface{}) error { 131 log.Printf("[INFO] Refreshing Stack: %s", d.Id()) 132 client, err := meta.(*Config).EnvironmentClient(d.Get("environment_id").(string)) 133 if err != nil { 134 return err 135 } 136 137 stack, err := client.Stack.ById(d.Id()) 138 if err != nil { 139 return err 140 } 141 142 if stack == nil { 143 log.Printf("[INFO] Stack %s not found", d.Id()) 144 d.SetId("") 145 return nil 146 } 147 148 if removed(stack.State) { 149 log.Printf("[INFO] Stack %s was removed on %v", d.Id(), stack.Removed) 150 d.SetId("") 151 return nil 152 } 153 154 config, err := client.Stack.ActionExportconfig(stack, &rancherClient.ComposeConfigInput{}) 155 if err != nil { 156 return err 157 } 158 159 log.Printf("[INFO] Stack Name: %s", stack.Name) 160 161 d.Set("description", stack.Description) 162 d.Set("name", stack.Name) 163 dockerCompose := strings.Replace(config.DockerComposeConfig, "\r", "", -1) 164 rancherCompose := strings.Replace(config.RancherComposeConfig, "\r", "", -1) 165 166 catalogID := d.Get("catalog_id") 167 if catalogID == "" { 168 d.Set("docker_compose", dockerCompose) 169 d.Set("rancher_compose", rancherCompose) 170 } else { 171 d.Set("docker_compose", "") 172 d.Set("rancher_compose", "") 173 } 174 d.Set("rendered_docker_compose", dockerCompose) 175 d.Set("rendered_rancher_compose", rancherCompose) 176 d.Set("environment_id", stack.AccountId) 177 d.Set("environment", stack.Environment) 178 179 if stack.ExternalId == "" { 180 d.Set("scope", "user") 181 d.Set("catalog_id", "") 182 } else { 183 trimmedID := strings.TrimPrefix(stack.ExternalId, "system-") 184 if trimmedID == stack.ExternalId { 185 d.Set("scope", "user") 186 } else { 187 d.Set("scope", "system") 188 } 189 d.Set("catalog_id", strings.TrimPrefix(trimmedID, "catalog://")) 190 } 191 192 d.Set("start_on_create", stack.StartOnCreate) 193 d.Set("finish_upgrade", d.Get("finish_upgrade").(bool)) 194 195 return nil 196 } 197 198 func resourceRancherStackUpdate(d *schema.ResourceData, meta interface{}) error { 199 log.Printf("[INFO] Updating Stack: %s", d.Id()) 200 client, err := meta.(*Config).EnvironmentClient(d.Get("environment_id").(string)) 201 if err != nil { 202 return err 203 } 204 d.Partial(true) 205 206 data, err := makeStackData(d, meta) 207 if err != nil { 208 return err 209 } 210 211 stack, err := client.Stack.ById(d.Id()) 212 if err != nil { 213 return err 214 } 215 216 var newStack rancherClient.Stack 217 if err = client.Update(stack.Type, &stack.Resource, data, &newStack); err != nil { 218 return err 219 } 220 221 stateConf := &resource.StateChangeConf{ 222 Pending: []string{"active", "active-updating"}, 223 Target: []string{"active"}, 224 Refresh: StackStateRefreshFunc(client, newStack.Id), 225 Timeout: 10 * time.Minute, 226 Delay: 1 * time.Second, 227 MinTimeout: 3 * time.Second, 228 } 229 s, waitErr := stateConf.WaitForState() 230 stack = s.(*rancherClient.Stack) 231 if waitErr != nil { 232 return fmt.Errorf( 233 "Error waiting for stack (%s) to be updated: %s", stack.Id, waitErr) 234 } 235 236 d.SetPartial("name") 237 d.SetPartial("description") 238 d.SetPartial("scope") 239 240 if d.HasChange("docker_compose") || 241 d.HasChange("rancher_compose") || 242 d.HasChange("environment") || 243 d.HasChange("catalog_id") { 244 245 envMap := make(map[string]interface{}) 246 for key, value := range *data["environment"].(*map[string]string) { 247 envValue := value 248 envMap[key] = &envValue 249 } 250 stack, err = client.Stack.ActionUpgrade(stack, &rancherClient.StackUpgrade{ 251 DockerCompose: *data["dockerCompose"].(*string), 252 RancherCompose: *data["rancherCompose"].(*string), 253 Environment: envMap, 254 ExternalId: *data["externalId"].(*string), 255 }) 256 if err != nil { 257 return err 258 } 259 260 stateConf := &resource.StateChangeConf{ 261 Pending: []string{"active", "upgrading", "upgraded"}, 262 Target: []string{"upgraded"}, 263 Refresh: StackStateRefreshFunc(client, stack.Id), 264 Timeout: 10 * time.Minute, 265 Delay: 1 * time.Second, 266 MinTimeout: 3 * time.Second, 267 } 268 s, waitErr := stateConf.WaitForState() 269 if waitErr != nil { 270 return fmt.Errorf( 271 "Error waiting for stack (%s) to be upgraded: %s", stack.Id, waitErr) 272 } 273 stack = s.(*rancherClient.Stack) 274 275 if d.Get("finish_upgrade").(bool) { 276 stack, err = client.Stack.ActionFinishupgrade(stack) 277 if err != nil { 278 return err 279 } 280 281 stateConf = &resource.StateChangeConf{ 282 Pending: []string{"active", "upgraded", "finishing-upgrade"}, 283 Target: []string{"active"}, 284 Refresh: StackStateRefreshFunc(client, stack.Id), 285 Timeout: 10 * time.Minute, 286 Delay: 1 * time.Second, 287 MinTimeout: 3 * time.Second, 288 } 289 _, waitErr = stateConf.WaitForState() 290 if waitErr != nil { 291 return fmt.Errorf( 292 "Error waiting for stack (%s) to be upgraded: %s", stack.Id, waitErr) 293 } 294 } 295 296 d.SetPartial("rendered_docker_compose") 297 d.SetPartial("rendered_rancher_compose") 298 d.SetPartial("docker_compose") 299 d.SetPartial("rancher_compose") 300 d.SetPartial("environment") 301 d.SetPartial("catalog_id") 302 } 303 304 d.Partial(false) 305 306 return resourceRancherStackRead(d, meta) 307 } 308 309 func resourceRancherStackDelete(d *schema.ResourceData, meta interface{}) error { 310 log.Printf("[INFO] Deleting Stack: %s", d.Id()) 311 id := d.Id() 312 client, err := meta.(*Config).EnvironmentClient(d.Get("environment_id").(string)) 313 if err != nil { 314 return err 315 } 316 317 stack, err := client.Stack.ById(id) 318 if err != nil { 319 return err 320 } 321 322 if err := client.Stack.Delete(stack); err != nil { 323 return fmt.Errorf("Error deleting Stack: %s", err) 324 } 325 326 log.Printf("[DEBUG] Waiting for stack (%s) to be removed", id) 327 328 stateConf := &resource.StateChangeConf{ 329 Pending: []string{"active", "removed", "removing"}, 330 Target: []string{"removed"}, 331 Refresh: StackStateRefreshFunc(client, id), 332 Timeout: 10 * time.Minute, 333 Delay: 1 * time.Second, 334 MinTimeout: 3 * time.Second, 335 } 336 337 _, waitErr := stateConf.WaitForState() 338 if waitErr != nil { 339 return fmt.Errorf( 340 "Error waiting for stack (%s) to be removed: %s", id, waitErr) 341 } 342 343 d.SetId("") 344 return nil 345 } 346 347 func resourceRancherStackImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { 348 envID, resourceID := splitID(d.Id()) 349 d.SetId(resourceID) 350 if envID != "" { 351 d.Set("environment_id", envID) 352 } else { 353 client, err := meta.(*Config).GlobalClient() 354 if err != nil { 355 return []*schema.ResourceData{}, err 356 } 357 stack, err := client.Stack.ById(d.Id()) 358 if err != nil { 359 return []*schema.ResourceData{}, err 360 } 361 d.Set("environment_id", stack.AccountId) 362 } 363 return []*schema.ResourceData{d}, nil 364 } 365 366 // StackStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch 367 // a Rancher Stack. 368 func StackStateRefreshFunc(client *rancherClient.RancherClient, stackID string) resource.StateRefreshFunc { 369 return func() (interface{}, string, error) { 370 stack, err := client.Stack.ById(stackID) 371 372 if err != nil { 373 return nil, "", err 374 } 375 376 return stack, stack.State, nil 377 } 378 } 379 380 func environmentFromMap(m map[string]interface{}) map[string]string { 381 result := make(map[string]string) 382 for k, v := range m { 383 result[k] = v.(string) 384 } 385 return result 386 } 387 388 func makeStackData(d *schema.ResourceData, meta interface{}) (data map[string]interface{}, err error) { 389 name := d.Get("name").(string) 390 description := d.Get("description").(string) 391 392 var externalID string 393 var dockerCompose string 394 var rancherCompose string 395 var environment map[string]string 396 if c, ok := d.GetOk("catalog_id"); ok { 397 if scope, ok := d.GetOk("scope"); ok && scope.(string) == "system" { 398 externalID = "system-" 399 } 400 catalogID := c.(string) 401 externalID += "catalog://" + catalogID 402 403 catalogClient, err := meta.(*Config).CatalogClient() 404 if err != nil { 405 return data, err 406 } 407 408 templateVersion, err := getCatalogTemplateVersion(catalogClient, catalogID) 409 if err != nil { 410 return data, err 411 } 412 413 if templateVersion.Id != catalogID { 414 return data, fmt.Errorf("Did not find template %s", catalogID) 415 } 416 417 dockerCompose = templateVersion.Files["docker-compose.yml"].(string) 418 rancherCompose = templateVersion.Files["rancher-compose.yml"].(string) 419 } 420 421 if c, ok := d.GetOk("docker_compose"); ok { 422 dockerCompose = c.(string) 423 } 424 if c, ok := d.GetOk("rancher_compose"); ok { 425 rancherCompose = c.(string) 426 } 427 428 environment = environmentFromMap(d.Get("environment").(map[string]interface{})) 429 430 startOnCreate := d.Get("start_on_create") 431 system := systemScope(d.Get("scope").(string)) 432 433 data = map[string]interface{}{ 434 "name": &name, 435 "description": &description, 436 "dockerCompose": &dockerCompose, 437 "rancherCompose": &rancherCompose, 438 "environment": &environment, 439 "externalId": &externalID, 440 "startOnCreate": &startOnCreate, 441 "system": &system, 442 } 443 444 return data, nil 445 } 446 447 func suppressComposeDiff(k, old, new string, d *schema.ResourceData) bool { 448 cOld, err := compose.CreateConfig([]byte(old)) 449 if err != nil { 450 // TODO: log? 451 return false 452 } 453 454 cNew, err := compose.CreateConfig([]byte(new)) 455 if err != nil { 456 // TODO: log? 457 return false 458 } 459 460 return reflect.DeepEqual(cOld, cNew) 461 } 462 463 func getCatalogTemplateVersion(c *catalog.RancherClient, catalogID string) (*catalog.TemplateVersion, error) { 464 templateVersion := &catalog.TemplateVersion{} 465 466 namesAndFolder := strings.SplitN(catalogID, ":", 3) 467 if len(namesAndFolder) != 3 { 468 return templateVersion, fmt.Errorf("catalog_id: %s not in 'catalog:name:N' format", catalogID) 469 } 470 471 template, err := c.Template.ById(namesAndFolder[0] + ":" + namesAndFolder[1]) 472 if err != nil { 473 return templateVersion, fmt.Errorf("Failed to get catalog template: %s at url %s", err, c.GetOpts().Url) 474 } 475 476 if template == nil { 477 return templateVersion, fmt.Errorf("Unknown catalog template %s", catalogID) 478 } 479 480 for _, versionLink := range template.VersionLinks { 481 if strings.HasSuffix(versionLink.(string), catalogID) { 482 client := &http.Client{} 483 req, err := http.NewRequest("GET", fmt.Sprint(versionLink), nil) 484 req.SetBasicAuth(c.GetOpts().AccessKey, c.GetOpts().SecretKey) 485 resp, err := client.Do(req) 486 if err != nil { 487 return templateVersion, err 488 } 489 defer resp.Body.Close() 490 491 if resp.StatusCode != 200 { 492 return templateVersion, fmt.Errorf("Bad Response %d lookup up %s", resp.StatusCode, versionLink) 493 } 494 495 err = json.NewDecoder(resp.Body).Decode(templateVersion) 496 return templateVersion, err 497 } 498 } 499 500 return templateVersion, nil 501 } 502 503 func systemScope(scope string) bool { 504 return scope == "system" 505 }