github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/builtin/provisioners/habitat/resource_provisioner.go (about)

     1  package habitat
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha256"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/url"
    10  	"strings"
    11  
    12  	version "github.com/hashicorp/go-version"
    13  	"github.com/hashicorp/terraform/communicator"
    14  	"github.com/hashicorp/terraform/communicator/remote"
    15  	"github.com/hashicorp/terraform/configs/hcl2shim"
    16  	"github.com/hashicorp/terraform/helper/schema"
    17  	"github.com/hashicorp/terraform/helper/validation"
    18  	"github.com/hashicorp/terraform/terraform"
    19  	"github.com/mitchellh/go-linereader"
    20  )
    21  
    22  type provisioner struct {
    23  	Version          string
    24  	AutoUpdate       bool
    25  	HttpDisable      bool
    26  	Services         []Service
    27  	PermanentPeer    bool
    28  	ListenCtl        string
    29  	ListenGossip     string
    30  	ListenHTTP       string
    31  	Peer             string
    32  	Peers            []string
    33  	RingKey          string
    34  	RingKeyContent   string
    35  	CtlSecret        string
    36  	SkipInstall      bool
    37  	UseSudo          bool
    38  	ServiceType      string
    39  	ServiceName      string
    40  	URL              string
    41  	Channel          string
    42  	Events           string
    43  	Organization     string
    44  	GatewayAuthToken string
    45  	BuilderAuthToken string
    46  	SupOptions       string
    47  	AcceptLicense    bool
    48  
    49  	installHabitat      provisionFn
    50  	startHabitat        provisionFn
    51  	uploadRingKey       provisionFn
    52  	uploadCtlSecret     provisionFn
    53  	startHabitatService provisionServiceFn
    54  
    55  	osType string
    56  }
    57  
    58  type provisionFn func(terraform.UIOutput, communicator.Communicator) error
    59  type provisionServiceFn func(terraform.UIOutput, communicator.Communicator, Service) error
    60  
    61  func Provisioner() terraform.ResourceProvisioner {
    62  	return &schema.Provisioner{
    63  		Schema: map[string]*schema.Schema{
    64  			"version": &schema.Schema{
    65  				Type:     schema.TypeString,
    66  				Optional: true,
    67  			},
    68  			"auto_update": &schema.Schema{
    69  				Type:     schema.TypeBool,
    70  				Optional: true,
    71  				Default:  false,
    72  			},
    73  			"http_disable": &schema.Schema{
    74  				Type:     schema.TypeBool,
    75  				Optional: true,
    76  				Default:  false,
    77  			},
    78  			"peer": &schema.Schema{
    79  				Type:     schema.TypeString,
    80  				Optional: true,
    81  			},
    82  			"peers": &schema.Schema{
    83  				Type:     schema.TypeList,
    84  				Elem:     &schema.Schema{Type: schema.TypeString},
    85  				Optional: true,
    86  			},
    87  			"service_type": &schema.Schema{
    88  				Type:         schema.TypeString,
    89  				Optional:     true,
    90  				Default:      "systemd",
    91  				ValidateFunc: validation.StringInSlice([]string{"systemd", "unmanaged"}, false),
    92  			},
    93  			"service_name": &schema.Schema{
    94  				Type:     schema.TypeString,
    95  				Optional: true,
    96  				Default:  "hab-supervisor",
    97  			},
    98  			"use_sudo": &schema.Schema{
    99  				Type:     schema.TypeBool,
   100  				Optional: true,
   101  				Default:  true,
   102  			},
   103  			"accept_license": &schema.Schema{
   104  				Type:     schema.TypeBool,
   105  				Required: true,
   106  			},
   107  			"permanent_peer": &schema.Schema{
   108  				Type:     schema.TypeBool,
   109  				Optional: true,
   110  				Default:  false,
   111  			},
   112  			"listen_ctl": &schema.Schema{
   113  				Type:     schema.TypeString,
   114  				Optional: true,
   115  			},
   116  			"listen_gossip": &schema.Schema{
   117  				Type:     schema.TypeString,
   118  				Optional: true,
   119  			},
   120  			"listen_http": &schema.Schema{
   121  				Type:     schema.TypeString,
   122  				Optional: true,
   123  			},
   124  			"ring_key": &schema.Schema{
   125  				Type:     schema.TypeString,
   126  				Optional: true,
   127  			},
   128  			"ring_key_content": &schema.Schema{
   129  				Type:     schema.TypeString,
   130  				Optional: true,
   131  			},
   132  			"ctl_secret": &schema.Schema{
   133  				Type:     schema.TypeString,
   134  				Optional: true,
   135  			},
   136  			"url": &schema.Schema{
   137  				Type:     schema.TypeString,
   138  				Optional: true,
   139  				ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
   140  					u, err := url.Parse(val.(string))
   141  					if err != nil {
   142  						errs = append(errs, fmt.Errorf("invalid URL specified for %q: %v", key, err))
   143  					}
   144  
   145  					if u.Scheme == "" {
   146  						errs = append(errs, fmt.Errorf("invalid URL specified for %q (scheme must be specified)", key))
   147  					}
   148  
   149  					return warns, errs
   150  				},
   151  			},
   152  			"channel": &schema.Schema{
   153  				Type:     schema.TypeString,
   154  				Optional: true,
   155  			},
   156  			"events": &schema.Schema{
   157  				Type:     schema.TypeString,
   158  				Optional: true,
   159  			},
   160  			"organization": &schema.Schema{
   161  				Type:     schema.TypeString,
   162  				Optional: true,
   163  			},
   164  			"gateway_auth_token": &schema.Schema{
   165  				Type:     schema.TypeString,
   166  				Optional: true,
   167  			},
   168  			"builder_auth_token": &schema.Schema{
   169  				Type:     schema.TypeString,
   170  				Optional: true,
   171  			},
   172  			"service": &schema.Schema{
   173  				Type: schema.TypeSet,
   174  				Elem: &schema.Resource{
   175  					Schema: map[string]*schema.Schema{
   176  						"name": &schema.Schema{
   177  							Type:     schema.TypeString,
   178  							Required: true,
   179  						},
   180  						"binds": &schema.Schema{
   181  							Type:     schema.TypeList,
   182  							Elem:     &schema.Schema{Type: schema.TypeString},
   183  							Optional: true,
   184  						},
   185  						"bind": &schema.Schema{
   186  							Type: schema.TypeSet,
   187  							Elem: &schema.Resource{
   188  								Schema: map[string]*schema.Schema{
   189  									"alias": &schema.Schema{
   190  										Type:     schema.TypeString,
   191  										Required: true,
   192  									},
   193  									"service": &schema.Schema{
   194  										Type:     schema.TypeString,
   195  										Required: true,
   196  									},
   197  									"group": &schema.Schema{
   198  										Type:     schema.TypeString,
   199  										Required: true,
   200  									},
   201  								},
   202  							},
   203  							Optional: true,
   204  						},
   205  						"topology": &schema.Schema{
   206  							Type:         schema.TypeString,
   207  							Optional:     true,
   208  							ValidateFunc: validation.StringInSlice([]string{"leader", "standalone"}, false),
   209  						},
   210  						"user_toml": &schema.Schema{
   211  							Type:     schema.TypeString,
   212  							Optional: true,
   213  						},
   214  						"strategy": &schema.Schema{
   215  							Type:         schema.TypeString,
   216  							Optional:     true,
   217  							ValidateFunc: validation.StringInSlice([]string{"none", "rolling", "at-once"}, false),
   218  						},
   219  						"channel": &schema.Schema{
   220  							Type:     schema.TypeString,
   221  							Optional: true,
   222  						},
   223  						"group": &schema.Schema{
   224  							Type:     schema.TypeString,
   225  							Optional: true,
   226  						},
   227  						"url": &schema.Schema{
   228  							Type:     schema.TypeString,
   229  							Optional: true,
   230  							ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
   231  								u, err := url.Parse(val.(string))
   232  								if err != nil {
   233  									errs = append(errs, fmt.Errorf("invalid URL specified for %q: %v", key, err))
   234  								}
   235  
   236  								if u.Scheme == "" {
   237  									errs = append(errs, fmt.Errorf("invalid URL specified for %q (scheme must be specified)", key))
   238  								}
   239  
   240  								return warns, errs
   241  							},
   242  						},
   243  						"application": &schema.Schema{
   244  							Type:     schema.TypeString,
   245  							Optional: true,
   246  						},
   247  						"environment": &schema.Schema{
   248  							Type:     schema.TypeString,
   249  							Optional: true,
   250  						},
   251  						"service_key": &schema.Schema{
   252  							Type:     schema.TypeString,
   253  							Optional: true,
   254  						},
   255  					},
   256  				},
   257  				Optional: true,
   258  			},
   259  		},
   260  		ApplyFunc:    applyFn,
   261  		ValidateFunc: validateFn,
   262  	}
   263  }
   264  
   265  func applyFn(ctx context.Context) error {
   266  	o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
   267  	s := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
   268  	d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
   269  
   270  	p, err := decodeConfig(d)
   271  	if err != nil {
   272  		return err
   273  	}
   274  
   275  	// Automatically determine the OS type
   276  	switch t := s.Ephemeral.ConnInfo["type"]; t {
   277  	case "ssh", "":
   278  		p.osType = "linux"
   279  	case "winrm":
   280  		p.osType = "windows"
   281  	default:
   282  		return fmt.Errorf("unsupported connection type: %s", t)
   283  	}
   284  
   285  	switch p.osType {
   286  	case "linux":
   287  		p.installHabitat = p.linuxInstallHabitat
   288  		p.uploadRingKey = p.linuxUploadRingKey
   289  		p.uploadCtlSecret = p.linuxUploadCtlSecret
   290  		p.startHabitat = p.linuxStartHabitat
   291  		p.startHabitatService = p.linuxStartHabitatService
   292  	case "windows":
   293  		return fmt.Errorf("windows is not supported yet for the habitat provisioner")
   294  	default:
   295  		return fmt.Errorf("unsupported os type: %s", p.osType)
   296  	}
   297  
   298  	// Get a new communicator
   299  	comm, err := communicator.New(s)
   300  	if err != nil {
   301  		return err
   302  	}
   303  
   304  	retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
   305  	defer cancel()
   306  
   307  	// Wait and retry until we establish the connection
   308  	err = communicator.Retry(retryCtx, func() error {
   309  		return comm.Connect(o)
   310  	})
   311  
   312  	if err != nil {
   313  		return err
   314  	}
   315  	defer comm.Disconnect()
   316  
   317  	if !p.SkipInstall {
   318  		o.Output("Installing habitat...")
   319  		if err := p.installHabitat(o, comm); err != nil {
   320  			return err
   321  		}
   322  	}
   323  
   324  	if p.RingKeyContent != "" {
   325  		o.Output("Uploading supervisor ring key...")
   326  		if err := p.uploadRingKey(o, comm); err != nil {
   327  			return err
   328  		}
   329  	}
   330  
   331  	if p.CtlSecret != "" {
   332  		o.Output("Uploading ctl secret...")
   333  		if err := p.uploadCtlSecret(o, comm); err != nil {
   334  			return err
   335  		}
   336  	}
   337  
   338  	o.Output("Starting the habitat supervisor...")
   339  	if err := p.startHabitat(o, comm); err != nil {
   340  		return err
   341  	}
   342  
   343  	if p.Services != nil {
   344  		for _, service := range p.Services {
   345  			o.Output("Starting service: " + service.Name)
   346  			if err := p.startHabitatService(o, comm, service); err != nil {
   347  				return err
   348  			}
   349  		}
   350  	}
   351  
   352  	return nil
   353  }
   354  
   355  func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
   356  	ringKeyContent, ok := c.Get("ring_key_content")
   357  	if ok && ringKeyContent != "" && ringKeyContent != hcl2shim.UnknownVariableValue {
   358  		ringKey, ringOk := c.Get("ring_key")
   359  		if ringOk && ringKey == "" {
   360  			es = append(es, errors.New("if ring_key_content is specified, ring_key must be specified as well"))
   361  		}
   362  	}
   363  
   364  	v, ok := c.Get("version")
   365  	if ok && v != nil && strings.TrimSpace(v.(string)) != "" {
   366  		if _, err := version.NewVersion(v.(string)); err != nil {
   367  			es = append(es, errors.New(v.(string)+" is not a valid version."))
   368  		}
   369  	}
   370  
   371  	acceptLicense, ok := c.Get("accept_license")
   372  	if ok && !acceptLicense.(bool) {
   373  		if v != nil && strings.TrimSpace(v.(string)) != "" {
   374  			versionOld, _ := version.NewVersion("0.79.0")
   375  			versionRequired, _ := version.NewVersion(v.(string))
   376  			if versionRequired.GreaterThan(versionOld) {
   377  				es = append(es, errors.New("Habitat end user license agreement needs to be accepted, set the accept_license argument to true to accept"))
   378  			}
   379  		} else { // blank means latest version
   380  			es = append(es, errors.New("Habitat end user license agreement needs to be accepted, set the accept_license argument to true to accept"))
   381  		}
   382  	}
   383  
   384  	// Validate service level configs
   385  	services, ok := c.Get("service")
   386  	if ok {
   387  		data, dataOk := services.(string)
   388  		if dataOk {
   389  			es = append(es, fmt.Errorf("service '%v': must be a block", data))
   390  		}
   391  	}
   392  
   393  	return ws, es
   394  }
   395  
   396  type Service struct {
   397  	Name            string
   398  	Strategy        string
   399  	Topology        string
   400  	Channel         string
   401  	Group           string
   402  	URL             string
   403  	Binds           []Bind
   404  	BindStrings     []string
   405  	UserTOML        string
   406  	AppName         string
   407  	Environment     string
   408  	ServiceGroupKey string
   409  }
   410  
   411  func (s *Service) getPackageName(fullName string) string {
   412  	return strings.Split(fullName, "/")[1]
   413  }
   414  
   415  func (s *Service) getServiceNameChecksum() string {
   416  	return fmt.Sprintf("%x", sha256.Sum256([]byte(s.Name)))
   417  }
   418  
   419  type Bind struct {
   420  	Alias   string
   421  	Service string
   422  	Group   string
   423  }
   424  
   425  func (b *Bind) toBindString() string {
   426  	return fmt.Sprintf("%s:%s.%s", b.Alias, b.Service, b.Group)
   427  }
   428  
   429  func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
   430  	p := &provisioner{
   431  		Version:          d.Get("version").(string),
   432  		AutoUpdate:       d.Get("auto_update").(bool),
   433  		HttpDisable:      d.Get("http_disable").(bool),
   434  		Peer:             d.Get("peer").(string),
   435  		Peers:            getPeers(d.Get("peers").([]interface{})),
   436  		Services:         getServices(d.Get("service").(*schema.Set).List()),
   437  		UseSudo:          d.Get("use_sudo").(bool),
   438  		AcceptLicense:    d.Get("accept_license").(bool),
   439  		ServiceType:      d.Get("service_type").(string),
   440  		ServiceName:      d.Get("service_name").(string),
   441  		RingKey:          d.Get("ring_key").(string),
   442  		RingKeyContent:   d.Get("ring_key_content").(string),
   443  		CtlSecret:        d.Get("ctl_secret").(string),
   444  		PermanentPeer:    d.Get("permanent_peer").(bool),
   445  		ListenCtl:        d.Get("listen_ctl").(string),
   446  		ListenGossip:     d.Get("listen_gossip").(string),
   447  		ListenHTTP:       d.Get("listen_http").(string),
   448  		URL:              d.Get("url").(string),
   449  		Channel:          d.Get("channel").(string),
   450  		Events:           d.Get("events").(string),
   451  		Organization:     d.Get("organization").(string),
   452  		BuilderAuthToken: d.Get("builder_auth_token").(string),
   453  		GatewayAuthToken: d.Get("gateway_auth_token").(string),
   454  	}
   455  
   456  	return p, nil
   457  }
   458  
   459  func getPeers(v []interface{}) []string {
   460  	peers := make([]string, 0, len(v))
   461  	for _, rawPeerData := range v {
   462  		peers = append(peers, rawPeerData.(string))
   463  	}
   464  	return peers
   465  }
   466  
   467  func getServices(v []interface{}) []Service {
   468  	services := make([]Service, 0, len(v))
   469  	for _, rawServiceData := range v {
   470  		serviceData := rawServiceData.(map[string]interface{})
   471  		name := (serviceData["name"].(string))
   472  		strategy := (serviceData["strategy"].(string))
   473  		topology := (serviceData["topology"].(string))
   474  		channel := (serviceData["channel"].(string))
   475  		group := (serviceData["group"].(string))
   476  		url := (serviceData["url"].(string))
   477  		app := (serviceData["application"].(string))
   478  		env := (serviceData["environment"].(string))
   479  		userToml := (serviceData["user_toml"].(string))
   480  		serviceGroupKey := (serviceData["service_key"].(string))
   481  		var bindStrings []string
   482  		binds := getBinds(serviceData["bind"].(*schema.Set).List())
   483  		for _, b := range serviceData["binds"].([]interface{}) {
   484  			bind, err := getBindFromString(b.(string))
   485  			if err != nil {
   486  				return nil
   487  			}
   488  			binds = append(binds, bind)
   489  		}
   490  
   491  		service := Service{
   492  			Name:            name,
   493  			Strategy:        strategy,
   494  			Topology:        topology,
   495  			Channel:         channel,
   496  			Group:           group,
   497  			URL:             url,
   498  			UserTOML:        userToml,
   499  			BindStrings:     bindStrings,
   500  			Binds:           binds,
   501  			AppName:         app,
   502  			Environment:     env,
   503  			ServiceGroupKey: serviceGroupKey,
   504  		}
   505  		services = append(services, service)
   506  	}
   507  	return services
   508  }
   509  
   510  func getBinds(v []interface{}) []Bind {
   511  	binds := make([]Bind, 0, len(v))
   512  	for _, rawBindData := range v {
   513  		bindData := rawBindData.(map[string]interface{})
   514  		alias := bindData["alias"].(string)
   515  		service := bindData["service"].(string)
   516  		group := bindData["group"].(string)
   517  		bind := Bind{
   518  			Alias:   alias,
   519  			Service: service,
   520  			Group:   group,
   521  		}
   522  		binds = append(binds, bind)
   523  	}
   524  	return binds
   525  }
   526  
   527  func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader) {
   528  	lr := linereader.New(r)
   529  	for line := range lr.Ch {
   530  		o.Output(line)
   531  	}
   532  }
   533  
   534  func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communicator, command string) error {
   535  	outR, outW := io.Pipe()
   536  	errR, errW := io.Pipe()
   537  
   538  	go p.copyOutput(o, outR)
   539  	go p.copyOutput(o, errR)
   540  	defer outW.Close()
   541  	defer errW.Close()
   542  
   543  	cmd := &remote.Cmd{
   544  		Command: command,
   545  		Stdout:  outW,
   546  		Stderr:  errW,
   547  	}
   548  
   549  	if err := comm.Start(cmd); err != nil {
   550  		return fmt.Errorf("error executing command %q: %v", cmd.Command, err)
   551  	}
   552  
   553  	if err := cmd.Wait(); err != nil {
   554  		return err
   555  	}
   556  
   557  	return nil
   558  }
   559  
   560  func getBindFromString(bind string) (Bind, error) {
   561  	t := strings.FieldsFunc(bind, func(d rune) bool {
   562  		switch d {
   563  		case ':', '.':
   564  			return true
   565  		}
   566  		return false
   567  	})
   568  	if len(t) != 3 {
   569  		return Bind{}, errors.New("invalid bind specification: " + bind)
   570  	}
   571  	return Bind{Alias: t[0], Service: t[1], Group: t[2]}, nil
   572  }