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  }