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