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