github.com/bendemaree/terraform@v0.5.4-0.20150613200311-f50d97d6eee6/builtin/providers/azure/resource_azure_instance.go (about)

     1  package azure
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"fmt"
     7  	"log"
     8  	"strings"
     9  
    10  	"github.com/Azure/azure-sdk-for-go/management"
    11  	"github.com/Azure/azure-sdk-for-go/management/hostedservice"
    12  	"github.com/Azure/azure-sdk-for-go/management/osimage"
    13  	"github.com/Azure/azure-sdk-for-go/management/virtualmachine"
    14  	"github.com/Azure/azure-sdk-for-go/management/virtualmachineimage"
    15  	"github.com/Azure/azure-sdk-for-go/management/vmutils"
    16  	"github.com/hashicorp/terraform/helper/hashcode"
    17  	"github.com/hashicorp/terraform/helper/schema"
    18  )
    19  
    20  const (
    21  	linux                = "Linux"
    22  	windows              = "Windows"
    23  	osDiskBlobStorageURL = "http://%s.blob.core.windows.net/vhds/%s.vhd"
    24  )
    25  
    26  func resourceAzureInstance() *schema.Resource {
    27  	return &schema.Resource{
    28  		Create: resourceAzureInstanceCreate,
    29  		Read:   resourceAzureInstanceRead,
    30  		Update: resourceAzureInstanceUpdate,
    31  		Delete: resourceAzureInstanceDelete,
    32  
    33  		Schema: map[string]*schema.Schema{
    34  			"name": &schema.Schema{
    35  				Type:     schema.TypeString,
    36  				Required: true,
    37  				ForceNew: true,
    38  			},
    39  
    40  			"description": &schema.Schema{
    41  				Type:     schema.TypeString,
    42  				Optional: true,
    43  				Computed: true,
    44  				ForceNew: true,
    45  			},
    46  
    47  			"image": &schema.Schema{
    48  				Type:     schema.TypeString,
    49  				Required: true,
    50  				ForceNew: true,
    51  			},
    52  
    53  			"size": &schema.Schema{
    54  				Type:     schema.TypeString,
    55  				Required: true,
    56  			},
    57  
    58  			"subnet": &schema.Schema{
    59  				Type:     schema.TypeString,
    60  				Optional: true,
    61  				Computed: true,
    62  				ForceNew: true,
    63  			},
    64  
    65  			"virtual_network": &schema.Schema{
    66  				Type:     schema.TypeString,
    67  				Optional: true,
    68  				ForceNew: true,
    69  			},
    70  
    71  			"storage_service_name": &schema.Schema{
    72  				Type:     schema.TypeString,
    73  				Optional: true,
    74  				ForceNew: true,
    75  			},
    76  
    77  			"reverse_dns": &schema.Schema{
    78  				Type:     schema.TypeString,
    79  				Optional: true,
    80  				ForceNew: true,
    81  			},
    82  
    83  			"location": &schema.Schema{
    84  				Type:     schema.TypeString,
    85  				Required: true,
    86  				ForceNew: true,
    87  			},
    88  
    89  			"automatic_updates": &schema.Schema{
    90  				Type:     schema.TypeBool,
    91  				Optional: true,
    92  				Default:  false,
    93  				ForceNew: true,
    94  			},
    95  
    96  			"time_zone": &schema.Schema{
    97  				Type:     schema.TypeString,
    98  				Optional: true,
    99  				ForceNew: true,
   100  			},
   101  
   102  			"username": &schema.Schema{
   103  				Type:     schema.TypeString,
   104  				Required: true,
   105  				ForceNew: true,
   106  			},
   107  
   108  			"password": &schema.Schema{
   109  				Type:     schema.TypeString,
   110  				Optional: true,
   111  				ForceNew: true,
   112  			},
   113  
   114  			"ssh_key_thumbprint": &schema.Schema{
   115  				Type:     schema.TypeString,
   116  				Optional: true,
   117  				ForceNew: true,
   118  			},
   119  
   120  			"endpoint": &schema.Schema{
   121  				Type:     schema.TypeSet,
   122  				Optional: true,
   123  				Computed: true,
   124  				Elem: &schema.Resource{
   125  					Schema: map[string]*schema.Schema{
   126  						"name": &schema.Schema{
   127  							Type:     schema.TypeString,
   128  							Required: true,
   129  						},
   130  
   131  						"protocol": &schema.Schema{
   132  							Type:     schema.TypeString,
   133  							Optional: true,
   134  							Default:  "tcp",
   135  						},
   136  
   137  						"public_port": &schema.Schema{
   138  							Type:     schema.TypeInt,
   139  							Required: true,
   140  						},
   141  
   142  						"private_port": &schema.Schema{
   143  							Type:     schema.TypeInt,
   144  							Required: true,
   145  						},
   146  					},
   147  				},
   148  				Set: resourceAzureEndpointHash,
   149  			},
   150  
   151  			"security_group": &schema.Schema{
   152  				Type:     schema.TypeString,
   153  				Optional: true,
   154  				Computed: true,
   155  			},
   156  
   157  			"ip_address": &schema.Schema{
   158  				Type:     schema.TypeString,
   159  				Computed: true,
   160  			},
   161  
   162  			"vip_address": &schema.Schema{
   163  				Type:     schema.TypeString,
   164  				Computed: true,
   165  			},
   166  		},
   167  	}
   168  }
   169  
   170  func resourceAzureInstanceCreate(d *schema.ResourceData, meta interface{}) (err error) {
   171  	mc := meta.(*Client).mgmtClient
   172  
   173  	name := d.Get("name").(string)
   174  
   175  	// Compute/set the description
   176  	description := d.Get("description").(string)
   177  	if description == "" {
   178  		description = name
   179  	}
   180  
   181  	// Retrieve the needed details of the image
   182  	configureForImage, osType, err := retrieveImageDetails(
   183  		mc,
   184  		d.Get("image").(string),
   185  		name,
   186  		d.Get("storage_service_name").(string),
   187  	)
   188  	if err != nil {
   189  		return err
   190  	}
   191  
   192  	// Verify if we have all required parameters
   193  	if err := verifyInstanceParameters(d, osType); err != nil {
   194  		return err
   195  	}
   196  
   197  	p := hostedservice.CreateHostedServiceParameters{
   198  		ServiceName:    name,
   199  		Label:          base64.StdEncoding.EncodeToString([]byte(name)),
   200  		Description:    fmt.Sprintf("Cloud Service created automatically for instance %s", name),
   201  		Location:       d.Get("location").(string),
   202  		ReverseDNSFqdn: d.Get("reverse_dns").(string),
   203  	}
   204  
   205  	log.Printf("[DEBUG] Creating Cloud Service for instance: %s", name)
   206  	err = hostedservice.NewClient(mc).CreateHostedService(p)
   207  	if err != nil {
   208  		return fmt.Errorf("Error creating Cloud Service for instance %s: %s", name, err)
   209  	}
   210  
   211  	// Put in this defer here, so we are sure to cleanup already created parts
   212  	// when we exit with an error
   213  	defer func(mc management.Client) {
   214  		if err != nil {
   215  			req, err := hostedservice.NewClient(mc).DeleteHostedService(name, true)
   216  			if err != nil {
   217  				log.Printf("[DEBUG] Error cleaning up Cloud Service of instance %s: %s", name, err)
   218  			}
   219  
   220  			// Wait until the Cloud Service is deleted
   221  			if err := mc.WaitForOperation(req, nil); err != nil {
   222  				log.Printf(
   223  					"[DEBUG] Error waiting for Cloud Service of instance %s to be deleted: %s", name, err)
   224  			}
   225  		}
   226  	}(mc)
   227  
   228  	// Create a new role for the instance
   229  	role := vmutils.NewVMConfiguration(name, d.Get("size").(string))
   230  
   231  	log.Printf("[DEBUG] Configuring deployment from image...")
   232  	err = configureForImage(&role)
   233  	if err != nil {
   234  		return fmt.Errorf("Error configuring the deployment for %s: %s", name, err)
   235  	}
   236  
   237  	if osType == linux {
   238  		// This is pretty ugly, but the Azure SDK leaves me no other choice...
   239  		if tp, ok := d.GetOk("ssh_key_thumbprint"); ok {
   240  			err = vmutils.ConfigureForLinux(
   241  				&role,
   242  				name,
   243  				d.Get("username").(string),
   244  				d.Get("password").(string),
   245  				tp.(string),
   246  			)
   247  		} else {
   248  			err = vmutils.ConfigureForLinux(
   249  				&role,
   250  				name,
   251  				d.Get("username").(string),
   252  				d.Get("password").(string),
   253  			)
   254  		}
   255  		if err != nil {
   256  			return fmt.Errorf("Error configuring %s for Linux: %s", name, err)
   257  		}
   258  	}
   259  
   260  	if osType == windows {
   261  		err = vmutils.ConfigureForWindows(
   262  			&role,
   263  			name,
   264  			d.Get("username").(string),
   265  			d.Get("password").(string),
   266  			d.Get("automatic_updates").(bool),
   267  			d.Get("time_zone").(string),
   268  		)
   269  		if err != nil {
   270  			return fmt.Errorf("Error configuring %s for Windows: %s", name, err)
   271  		}
   272  	}
   273  
   274  	if s := d.Get("endpoint").(*schema.Set); s.Len() > 0 {
   275  		for _, v := range s.List() {
   276  			m := v.(map[string]interface{})
   277  			err := vmutils.ConfigureWithExternalPort(
   278  				&role,
   279  				m["name"].(string),
   280  				m["private_port"].(int),
   281  				m["public_port"].(int),
   282  				endpointProtocol(m["protocol"].(string)),
   283  			)
   284  			if err != nil {
   285  				return fmt.Errorf(
   286  					"Error adding endpoint %s for instance %s: %s", m["name"].(string), name, err)
   287  			}
   288  		}
   289  	}
   290  
   291  	if subnet, ok := d.GetOk("subnet"); ok {
   292  		err = vmutils.ConfigureWithSubnet(&role, subnet.(string))
   293  		if err != nil {
   294  			return fmt.Errorf(
   295  				"Error associating subnet %s with instance %s: %s", d.Get("subnet").(string), name, err)
   296  		}
   297  	}
   298  
   299  	if sg, ok := d.GetOk("security_group"); ok {
   300  		err = vmutils.ConfigureWithSecurityGroup(&role, sg.(string))
   301  		if err != nil {
   302  			return fmt.Errorf(
   303  				"Error associating security group %s with instance %s: %s", sg.(string), name, err)
   304  		}
   305  	}
   306  
   307  	options := virtualmachine.CreateDeploymentOptions{
   308  		VirtualNetworkName: d.Get("virtual_network").(string),
   309  	}
   310  
   311  	log.Printf("[DEBUG] Creating the new instance...")
   312  	req, err := virtualmachine.NewClient(mc).CreateDeployment(role, name, options)
   313  	if err != nil {
   314  		return fmt.Errorf("Error creating instance %s: %s", name, err)
   315  	}
   316  
   317  	log.Printf("[DEBUG] Waiting for the new instance to be created...")
   318  	if err := mc.WaitForOperation(req, nil); err != nil {
   319  		return fmt.Errorf(
   320  			"Error waiting for instance %s to be created: %s", name, err)
   321  	}
   322  
   323  	d.SetId(name)
   324  
   325  	return resourceAzureInstanceRead(d, meta)
   326  }
   327  
   328  func resourceAzureInstanceRead(d *schema.ResourceData, meta interface{}) error {
   329  	mc := meta.(*Client).mgmtClient
   330  
   331  	log.Printf("[DEBUG] Retrieving Cloud Service for instance: %s", d.Id())
   332  	cs, err := hostedservice.NewClient(mc).GetHostedService(d.Id())
   333  	if err != nil {
   334  		return fmt.Errorf("Error retrieving Cloud Service of instance %s: %s", d.Id(), err)
   335  	}
   336  
   337  	d.Set("reverse_dns", cs.ReverseDNSFqdn)
   338  	d.Set("location", cs.Location)
   339  
   340  	log.Printf("[DEBUG] Retrieving instance: %s", d.Id())
   341  	dpmt, err := virtualmachine.NewClient(mc).GetDeployment(d.Id(), d.Id())
   342  	if err != nil {
   343  		if management.IsResourceNotFoundError(err) {
   344  			d.SetId("")
   345  			return nil
   346  		}
   347  		return fmt.Errorf("Error retrieving instance %s: %s", d.Id(), err)
   348  	}
   349  
   350  	if len(dpmt.RoleList) != 1 {
   351  		return fmt.Errorf(
   352  			"Instance %s has an unexpected number of roles: %d", d.Id(), len(dpmt.RoleList))
   353  	}
   354  
   355  	d.Set("size", dpmt.RoleList[0].RoleSize)
   356  
   357  	if len(dpmt.RoleInstanceList) != 1 {
   358  		return fmt.Errorf(
   359  			"Instance %s has an unexpected number of role instances: %d",
   360  			d.Id(), len(dpmt.RoleInstanceList))
   361  	}
   362  	d.Set("ip_address", dpmt.RoleInstanceList[0].IPAddress)
   363  
   364  	if len(dpmt.RoleInstanceList[0].InstanceEndpoints) > 0 {
   365  		d.Set("vip_address", dpmt.RoleInstanceList[0].InstanceEndpoints[0].Vip)
   366  	}
   367  
   368  	// Find the network configuration set
   369  	for _, c := range dpmt.RoleList[0].ConfigurationSets {
   370  		if c.ConfigurationSetType == virtualmachine.ConfigurationSetTypeNetwork {
   371  			// Create a new set to hold all configured endpoints
   372  			endpoints := &schema.Set{
   373  				F: resourceAzureEndpointHash,
   374  			}
   375  
   376  			// Loop through all endpoints
   377  			for _, ep := range c.InputEndpoints {
   378  				endpoint := map[string]interface{}{}
   379  
   380  				// Update the values
   381  				endpoint["name"] = ep.Name
   382  				endpoint["protocol"] = string(ep.Protocol)
   383  				endpoint["public_port"] = ep.Port
   384  				endpoint["private_port"] = ep.LocalPort
   385  				endpoints.Add(endpoint)
   386  			}
   387  			d.Set("endpoint", endpoints)
   388  
   389  			// Update the subnet
   390  			switch len(c.SubnetNames) {
   391  			case 1:
   392  				d.Set("subnet", c.SubnetNames[0])
   393  			case 0:
   394  				d.Set("subnet", "")
   395  			default:
   396  				return fmt.Errorf(
   397  					"Instance %s has an unexpected number of associated subnets %d",
   398  					d.Id(), len(dpmt.RoleInstanceList))
   399  			}
   400  
   401  			// Update the security group
   402  			d.Set("security_group", c.NetworkSecurityGroup)
   403  		}
   404  	}
   405  
   406  	connType := "ssh"
   407  	if dpmt.RoleList[0].OSVirtualHardDisk.OS == windows {
   408  		connType = "winrm"
   409  	}
   410  
   411  	// Set the connection info for any configured provisioners
   412  	d.SetConnInfo(map[string]string{
   413  		"type":     connType,
   414  		"host":     dpmt.VirtualIPs[0].Address,
   415  		"user":     d.Get("username").(string),
   416  		"password": d.Get("password").(string),
   417  	})
   418  
   419  	return nil
   420  }
   421  
   422  func resourceAzureInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
   423  	mc := meta.(*Client).mgmtClient
   424  
   425  	// First check if anything we can update changed, and if not just return
   426  	if !d.HasChange("size") && !d.HasChange("endpoint") && !d.HasChange("security_group") {
   427  		return nil
   428  	}
   429  
   430  	// Get the current role
   431  	role, err := virtualmachine.NewClient(mc).GetRole(d.Id(), d.Id(), d.Id())
   432  	if err != nil {
   433  		return fmt.Errorf("Error retrieving role of instance %s: %s", d.Id(), err)
   434  	}
   435  
   436  	// Verify if we have all required parameters
   437  	if err := verifyInstanceParameters(d, role.OSVirtualHardDisk.OS); err != nil {
   438  		return err
   439  	}
   440  
   441  	if d.HasChange("size") {
   442  		role.RoleSize = d.Get("size").(string)
   443  	}
   444  
   445  	if d.HasChange("endpoint") {
   446  		_, n := d.GetChange("endpoint")
   447  
   448  		// Delete the existing endpoints
   449  		for i, c := range role.ConfigurationSets {
   450  			if c.ConfigurationSetType == virtualmachine.ConfigurationSetTypeNetwork {
   451  				c.InputEndpoints = nil
   452  				role.ConfigurationSets[i] = c
   453  			}
   454  		}
   455  
   456  		// And add the ones we still want
   457  		if s := n.(*schema.Set); s.Len() > 0 {
   458  			for _, v := range s.List() {
   459  				m := v.(map[string]interface{})
   460  				err := vmutils.ConfigureWithExternalPort(
   461  					role,
   462  					m["name"].(string),
   463  					m["private_port"].(int),
   464  					m["public_port"].(int),
   465  					endpointProtocol(m["protocol"].(string)),
   466  				)
   467  				if err != nil {
   468  					return fmt.Errorf(
   469  						"Error adding endpoint %s for instance %s: %s", m["name"].(string), d.Id(), err)
   470  				}
   471  			}
   472  		}
   473  	}
   474  
   475  	if d.HasChange("security_group") {
   476  		sg := d.Get("security_group").(string)
   477  		err := vmutils.ConfigureWithSecurityGroup(role, sg)
   478  		if err != nil {
   479  			return fmt.Errorf(
   480  				"Error associating security group %s with instance %s: %s", sg, d.Id(), err)
   481  		}
   482  	}
   483  
   484  	// Update the adjusted role
   485  	req, err := virtualmachine.NewClient(mc).UpdateRole(d.Id(), d.Id(), d.Id(), *role)
   486  	if err != nil {
   487  		return fmt.Errorf("Error updating role of instance %s: %s", d.Id(), err)
   488  	}
   489  
   490  	if err := mc.WaitForOperation(req, nil); err != nil {
   491  		return fmt.Errorf(
   492  			"Error waiting for role of instance %s to be updated: %s", d.Id(), err)
   493  	}
   494  
   495  	return resourceAzureInstanceRead(d, meta)
   496  }
   497  
   498  func resourceAzureInstanceDelete(d *schema.ResourceData, meta interface{}) error {
   499  	mc := meta.(*Client).mgmtClient
   500  
   501  	log.Printf("[DEBUG] Deleting instance: %s", d.Id())
   502  	req, err := hostedservice.NewClient(mc).DeleteHostedService(d.Id(), true)
   503  	if err != nil {
   504  		return fmt.Errorf("Error deleting instance %s: %s", d.Id(), err)
   505  	}
   506  
   507  	// Wait until the instance is deleted
   508  	if err := mc.WaitForOperation(req, nil); err != nil {
   509  		return fmt.Errorf(
   510  			"Error waiting for instance %s to be deleted: %s", d.Id(), err)
   511  	}
   512  
   513  	d.SetId("")
   514  
   515  	return nil
   516  }
   517  
   518  func resourceAzureEndpointHash(v interface{}) int {
   519  	var buf bytes.Buffer
   520  	m := v.(map[string]interface{})
   521  	buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
   522  	buf.WriteString(fmt.Sprintf("%s-", m["protocol"].(string)))
   523  	buf.WriteString(fmt.Sprintf("%d-", m["public_port"].(int)))
   524  	buf.WriteString(fmt.Sprintf("%d-", m["private_port"].(int)))
   525  
   526  	return hashcode.String(buf.String())
   527  }
   528  
   529  func retrieveImageDetails(
   530  	mc management.Client,
   531  	label string,
   532  	name string,
   533  	storage string) (func(*virtualmachine.Role) error, string, error) {
   534  	configureForImage, osType, VMLabels, err := retrieveVMImageDetails(mc, label)
   535  	if err == nil {
   536  		return configureForImage, osType, nil
   537  	}
   538  
   539  	configureForImage, osType, OSLabels, err := retrieveOSImageDetails(mc, label, name, storage)
   540  	if err == nil {
   541  		return configureForImage, osType, nil
   542  	}
   543  
   544  	return nil, "", fmt.Errorf("Could not find image with label '%s'. Available images are: %s",
   545  		label, strings.Join(append(VMLabels, OSLabels...), ", "))
   546  }
   547  
   548  func retrieveVMImageDetails(
   549  	mc management.Client,
   550  	label string) (func(*virtualmachine.Role) error, string, []string, error) {
   551  	imgs, err := virtualmachineimage.NewClient(mc).ListVirtualMachineImages()
   552  	if err != nil {
   553  		return nil, "", nil, fmt.Errorf("Error retrieving image details: %s", err)
   554  	}
   555  
   556  	var labels []string
   557  	for _, img := range imgs.VMImages {
   558  		if img.Label == label {
   559  			if img.OSDiskConfiguration.OS != linux && img.OSDiskConfiguration.OS != windows {
   560  				return nil, "", nil, fmt.Errorf("Unsupported image OS: %s", img.OSDiskConfiguration.OS)
   561  			}
   562  
   563  			configureForImage := func(role *virtualmachine.Role) error {
   564  				return vmutils.ConfigureDeploymentFromVMImage(
   565  					role,
   566  					img.Name,
   567  					"",
   568  					true,
   569  				)
   570  			}
   571  
   572  			return configureForImage, img.OSDiskConfiguration.OS, nil, nil
   573  		}
   574  
   575  		labels = append(labels, img.Label)
   576  	}
   577  
   578  	return nil, "", labels, fmt.Errorf("Could not find image with label '%s'", label)
   579  }
   580  
   581  func retrieveOSImageDetails(
   582  	mc management.Client,
   583  	label string,
   584  	name string,
   585  	storage string) (func(*virtualmachine.Role) error, string, []string, error) {
   586  	imgs, err := osimage.NewClient(mc).ListOSImages()
   587  	if err != nil {
   588  		return nil, "", nil, fmt.Errorf("Error retrieving image details: %s", err)
   589  	}
   590  
   591  	var labels []string
   592  	for _, img := range imgs.OSImages {
   593  		if img.Label == label {
   594  			if img.OS != linux && img.OS != windows {
   595  				return nil, "", nil, fmt.Errorf("Unsupported image OS: %s", img.OS)
   596  			}
   597  			if img.MediaLink == "" {
   598  				if storage == "" {
   599  					return nil, "", nil,
   600  						fmt.Errorf("When using a platform image, the 'storage' parameter is required")
   601  				}
   602  				img.MediaLink = fmt.Sprintf(osDiskBlobStorageURL, storage, name)
   603  			}
   604  
   605  			configureForImage := func(role *virtualmachine.Role) error {
   606  				return vmutils.ConfigureDeploymentFromPlatformImage(
   607  					role,
   608  					img.Name,
   609  					img.MediaLink,
   610  					label,
   611  				)
   612  			}
   613  
   614  			return configureForImage, img.OS, nil, nil
   615  		}
   616  
   617  		labels = append(labels, img.Label)
   618  	}
   619  
   620  	return nil, "", labels, fmt.Errorf("Could not find image with label '%s'", label)
   621  }
   622  
   623  func endpointProtocol(p string) virtualmachine.InputEndpointProtocol {
   624  	if p == "tcp" {
   625  		return virtualmachine.InputEndpointProtocolTCP
   626  	}
   627  
   628  	return virtualmachine.InputEndpointProtocolUDP
   629  }
   630  
   631  func verifyInstanceParameters(d *schema.ResourceData, osType string) error {
   632  	if osType == linux {
   633  		_, pass := d.GetOk("password")
   634  		_, key := d.GetOk("ssh_key_thumbprint")
   635  
   636  		if !pass && !key {
   637  			return fmt.Errorf(
   638  				"You must supply a 'password' and/or a 'ssh_key_thumbprint' when using a Linux image")
   639  		}
   640  	}
   641  
   642  	if osType == windows {
   643  		if _, ok := d.GetOk("password"); !ok {
   644  			return fmt.Errorf("You must supply a 'password' when using a Windows image")
   645  		}
   646  
   647  		if _, ok := d.GetOk("time_zone"); !ok {
   648  			return fmt.Errorf("You must supply a 'time_zone' when using a Windows image")
   649  		}
   650  	}
   651  
   652  	if _, ok := d.GetOk("subnet"); ok {
   653  		if _, ok := d.GetOk("virtual_network"); !ok {
   654  			return fmt.Errorf("You must also supply a 'virtual_network' when supplying a 'subnet'")
   655  		}
   656  	}
   657  
   658  	if s := d.Get("endpoint").(*schema.Set); s.Len() > 0 {
   659  		for _, v := range s.List() {
   660  			protocol := v.(map[string]interface{})["protocol"].(string)
   661  
   662  			if protocol != "tcp" && protocol != "udp" {
   663  				return fmt.Errorf(
   664  					"Invalid endpoint protocol %s! Valid options are 'tcp' and 'udp'.", protocol)
   665  			}
   666  		}
   667  	}
   668  
   669  	return nil
   670  }