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  }