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  }