github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/ec2/environ_vpc.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package ec2
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/juju/collections/set"
    11  	"github.com/juju/errors"
    12  	"gopkg.in/amz.v3/ec2"
    13  
    14  	"github.com/juju/juju/environs"
    15  	"github.com/juju/juju/environs/context"
    16  	"github.com/juju/juju/network"
    17  )
    18  
    19  const (
    20  	activeState           = "active"
    21  	availableState        = "available"
    22  	localRouteGatewayID   = "local"
    23  	defaultRouteCIDRBlock = "0.0.0.0/0"
    24  	vpcIDNone             = "none"
    25  )
    26  
    27  var (
    28  	vpcNotUsableForBootstrapErrorPrefix = `
    29  Juju cannot use the given vpc-id for bootstrapping a controller
    30  instance. Please, double check the given VPC ID is correct, and that
    31  the VPC contains at least one subnet.
    32  
    33  Error details`[1:]
    34  
    35  	vpcNotUsableForModelErrorPrefix = `
    36  Juju cannot use the given vpc-id for the model being added.
    37  Please double check the given VPC ID is correct, and that
    38  the VPC contains at least one subnet.
    39  
    40  Error details`[1:]
    41  
    42  	vpcNotRecommendedErrorPrefix = `
    43  The given vpc-id does not meet one or more of the following minimum
    44  Juju requirements:
    45  
    46  1. VPC should be in "available" state and contain one or more subnets.
    47  2. An Internet Gateway (IGW) should be attached to the VPC.
    48  3. The main route table of the VPC should have both a default route
    49     to the attached IGW and a local route matching the VPC CIDR block.
    50  4. At least one of the VPC subnets should have MapPublicIPOnLaunch
    51     attribute enabled (i.e. at least one subnet needs to be 'public').
    52  5. All subnets should be implicitly associated to the VPC main route
    53     table, rather than explicitly to per-subnet route tables.
    54  
    55  A default VPC already satisfies all of the requirements above. If you
    56  still want to use the VPC, try running 'juju bootstrap' again with:
    57  
    58    --config vpc-id=%s --config vpc-id-force=true
    59  
    60  to force Juju to bypass the requirements check (NOT recommended unless
    61  you understand the implications: most importantly, not being able to
    62  access the Juju controller, likely causing bootstrap to fail, or trying
    63  to deploy exposed workloads on instances started in private or isolated
    64  subnets).
    65  
    66  Error details`[1:]
    67  
    68  	cannotValidateVPCErrorPrefix = `
    69  Juju could not verify whether the given vpc-id meets the minimum Juju
    70  connectivity requirements. Please, double check the VPC ID is correct,
    71  you have a working connection to the Internet, your AWS credentials are
    72  sufficient to access VPC features, or simply retry bootstrapping again.
    73  
    74  Error details`[1:]
    75  
    76  	vpcNotRecommendedButForcedWarning = `
    77  WARNING! The specified vpc-id does not satisfy the minimum Juju requirements,
    78  but will be used anyway because vpc-id-force=true is also specified.
    79  
    80  `[1:]
    81  )
    82  
    83  // vpcNotUsableError indicates a user-specified VPC cannot be used either
    84  // because it is missing or because it contains no subnets.
    85  type vpcNotUsableError struct {
    86  	errors.Err
    87  }
    88  
    89  // vpcNotUsablef returns an error which satisfies isVPCNotUsableError().
    90  func vpcNotUsablef(optionalCause error, format string, args ...interface{}) error {
    91  	outerErr := errors.Errorf(format, args...)
    92  	if optionalCause != nil {
    93  		outerErr = errors.Maskf(optionalCause, format, args...)
    94  	}
    95  
    96  	innerErr, _ := outerErr.(*errors.Err) // cannot fail.
    97  	return &vpcNotUsableError{*innerErr}
    98  }
    99  
   100  // isVPCNotUsableError reports whether err was created with vpcNotUsablef().
   101  func isVPCNotUsableError(err error) bool {
   102  	err = errors.Cause(err)
   103  	_, ok := err.(*vpcNotUsableError)
   104  	return ok
   105  }
   106  
   107  // vpcNotRecommendedError indicates a user-specified VPC is unlikely to be
   108  // suitable for hosting a Juju controller instance and/or exposed workloads, due
   109  // to not satisfying the mininum requirements described in validateVPC()'s doc
   110  // comment. Users can still force Juju to use such a VPC by passing
   111  // 'vpc-id-force=true' setting.
   112  type vpcNotRecommendedError struct {
   113  	errors.Err
   114  }
   115  
   116  // vpcNotRecommendedf returns an error which satisfies isVPCNotRecommendedError().
   117  func vpcNotRecommendedf(format string, args ...interface{}) error {
   118  	outerErr := errors.Errorf(format, args...)
   119  	innerErr, _ := outerErr.(*errors.Err) // cannot fail.
   120  	return &vpcNotRecommendedError{*innerErr}
   121  }
   122  
   123  // isVPCNotRecommendedError reports whether err was created with vpcNotRecommendedf().
   124  func isVPCNotRecommendedError(err error) bool {
   125  	err = errors.Cause(err)
   126  	_, ok := err.(*vpcNotRecommendedError)
   127  	return ok
   128  }
   129  
   130  // vpcAPIClient defines a subset of the goamz API calls needed to validate a VPC.
   131  type vpcAPIClient interface {
   132  	// AccountAttributes, called with the "default-vpc" attribute. is used to
   133  	// find the ID of the region's default VPC (if any).
   134  	AccountAttributes(attributeNames ...string) (*ec2.AccountAttributesResp, error)
   135  
   136  	// VPCs is used to get details for the VPC being validated, including
   137  	// whether it exists, is available, and its CIDRBlock and IsDefault fields.
   138  	VPCs(ids []string, filter *ec2.Filter) (*ec2.VPCsResp, error)
   139  
   140  	// Subnets is used to get a list of all subnets of the validated VPC (if
   141  	// any),including their Id, AvailZone, and MapPublicIPOnLaunch fields.
   142  	Subnets(ids []string, filter *ec2.Filter) (*ec2.SubnetsResp, error)
   143  
   144  	// InternetGateways is used to get details of the Internet Gateway (IGW)
   145  	// attached to the validated VPC (if any), its Id to check against routes,
   146  	// and whether it's available.
   147  	InternetGateways(ids []string, filter *ec2.Filter) (*ec2.InternetGatewaysResp, error)
   148  
   149  	// RouteTables is used to find the main route table of the VPC (if any),
   150  	// whether it includes a default route to the attached IGW, a local route to
   151  	// the VPC CIDRBlock, and any per-subnet route tables.
   152  	RouteTables(ids []string, filter *ec2.Filter) (*ec2.RouteTablesResp, error)
   153  }
   154  
   155  // validateVPC requires both arguments to be set and validates that vpcID refers
   156  // to an existing AWS VPC (default or non-default) for the current region.
   157  // Returns an error satifying isVPCNotUsableError() when the VPC with the given
   158  // vpcID cannot be found, or when the VPC exists but contains no subnets.
   159  // Returns an error satisfying isVPCNotRecommendedError() in the following
   160  // cases:
   161  //
   162  // 1. The VPC's state is not "available".
   163  // 2. The VPC does not have an Internet Gateway (IGW) attached.
   164  // 3. A main route table is not associated with the VPC.
   165  // 4. The main route table lacks both a default route via the IGW and a local
   166  //    route matching the VPC's CIDR block.
   167  // 5. One or more of the VPC's subnets are not associated with the main route
   168  //    table of the VPC.
   169  // 6. None of the the VPC's subnets have the MapPublicIPOnLaunch attribute set.
   170  //
   171  // With the vpc-id-force config setting set to true, the provider can ignore a
   172  // vpcNotRecommendedError. A vpcNotUsableError cannot be ignored, while
   173  // unexpected API responses and errors could be retried.
   174  //
   175  // The above minimal requirements allow Juju to work out-of-the-box with most
   176  // common (and officially documented by AWS) VPC setups, easy try out with AWS
   177  // Console / VPC Wizard / CLI. Detecting VPC setups indicating intentional
   178  // customization by experienced users, protecting beginners from bad Juju-UX due
   179  // to broken VPC setup, while still allowing power users to override that and
   180  // continue (but knowing what that implies).
   181  func validateVPC(apiClient vpcAPIClient, ctx context.ProviderCallContext, vpcID string) error {
   182  	if vpcID == "" || apiClient == nil {
   183  		return errors.Errorf("invalid arguments: empty VPC ID or nil client")
   184  	}
   185  
   186  	vpc, err := getVPCByID(apiClient, ctx, vpcID)
   187  	if err != nil {
   188  		return errors.Trace(err)
   189  	}
   190  
   191  	if err := checkVPCIsAvailable(vpc); err != nil {
   192  		return errors.Trace(err)
   193  	}
   194  
   195  	subnets, err := getVPCSubnets(apiClient, ctx, vpc)
   196  	if err != nil {
   197  		return errors.Trace(err)
   198  	}
   199  
   200  	publicSubnet, err := findFirstPublicSubnet(subnets)
   201  	if err != nil {
   202  		return errors.Trace(err)
   203  	}
   204  
   205  	// TODO(dimitern): Rather than just logging that, use publicSubnet.Id or
   206  	// even publicSubnet.AvailZone as default bootstrap placement directive, so
   207  	// the controller would be reachable.
   208  	logger.Infof(
   209  		"found subnet %q (%s) in AZ %q, suitable for a Juju controller instance",
   210  		publicSubnet.Id, publicSubnet.CIDRBlock, publicSubnet.AvailZone,
   211  	)
   212  
   213  	gateway, err := getVPCInternetGateway(apiClient, ctx, vpc)
   214  	if err != nil {
   215  		return errors.Trace(err)
   216  	}
   217  
   218  	if err := checkInternetGatewayIsAvailable(gateway); err != nil {
   219  		return errors.Trace(err)
   220  	}
   221  
   222  	routeTables, err := getVPCRouteTables(apiClient, ctx, vpc)
   223  	if err != nil {
   224  		return errors.Trace(err)
   225  	}
   226  
   227  	mainRouteTable, err := findVPCMainRouteTable(routeTables)
   228  	if err != nil {
   229  		return errors.Trace(err)
   230  	}
   231  
   232  	if err := checkVPCRouteTableRoutes(vpc, mainRouteTable, gateway); err != nil {
   233  		return errors.Annotatef(err, "VPC %q main route table %q", vpcID, mainRouteTable.Id)
   234  	}
   235  
   236  	logger.Infof("VPC %q is suitable for Juju controllers and expose-able workloads", vpc.Id)
   237  	return nil
   238  }
   239  
   240  func getVPCByID(apiClient vpcAPIClient, ctx context.ProviderCallContext, vpcID string) (*ec2.VPC, error) {
   241  	response, err := apiClient.VPCs([]string{vpcID}, nil)
   242  	if isVPCNotFoundError(err) {
   243  		return nil, vpcNotUsablef(err, "")
   244  	} else if err != nil {
   245  		return nil, errors.Annotatef(maybeConvertCredentialError(err, ctx), "unexpected AWS response getting VPC %q", vpcID)
   246  	}
   247  
   248  	if numResults := len(response.VPCs); numResults == 0 {
   249  		return nil, vpcNotUsablef(nil, "VPC %q not found", vpcID)
   250  	} else if numResults > 1 {
   251  		logger.Debugf("VPCs() returned %#v", response)
   252  		return nil, errors.Errorf("expected 1 result from AWS, got %d", numResults)
   253  	}
   254  
   255  	vpc := response.VPCs[0]
   256  	return &vpc, nil
   257  }
   258  
   259  func isVPCNotFoundError(err error) bool {
   260  	return err != nil && ec2ErrCode(err) == "InvalidVpcID.NotFound"
   261  }
   262  
   263  func checkVPCIsAvailable(vpc *ec2.VPC) error {
   264  	if vpc.State != availableState {
   265  		return vpcNotRecommendedf("VPC has unexpected state %q", vpc.State)
   266  	}
   267  
   268  	if vpc.IsDefault {
   269  		logger.Infof("VPC %q is the default VPC for the region", vpc.Id)
   270  	}
   271  
   272  	return nil
   273  }
   274  
   275  func getVPCSubnets(apiClient vpcAPIClient, ctx context.ProviderCallContext, vpc *ec2.VPC) ([]ec2.Subnet, error) {
   276  	filter := ec2.NewFilter()
   277  	filter.Add("vpc-id", vpc.Id)
   278  	response, err := apiClient.Subnets(nil, filter)
   279  	if err != nil {
   280  		return nil, errors.Annotatef(maybeConvertCredentialError(err, ctx), "unexpected AWS response getting subnets of VPC %q", vpc.Id)
   281  	}
   282  
   283  	if len(response.Subnets) == 0 {
   284  		return nil, vpcNotUsablef(nil, "no subnets found for VPC %q", vpc.Id)
   285  	}
   286  
   287  	return response.Subnets, nil
   288  }
   289  
   290  func findFirstPublicSubnet(subnets []ec2.Subnet) (*ec2.Subnet, error) {
   291  	for _, subnet := range subnets {
   292  		// TODO(dimitern): goamz's AddDefaultVPCAndSubnets() does not set
   293  		// MapPublicIPOnLaunch only DefaultForAZ, but in reality the former is
   294  		// always set when the latter is. Until this is fixed in goamz, we check
   295  		// for both below to allow testing the behavior.
   296  		if subnet.MapPublicIPOnLaunch || subnet.DefaultForAZ {
   297  			logger.Debugf(
   298  				"VPC %q subnet %q has MapPublicIPOnLaunch=%v, DefaultForAZ=%v",
   299  				subnet.VPCId, subnet.Id, subnet.MapPublicIPOnLaunch, subnet.DefaultForAZ,
   300  			)
   301  			return &subnet, nil
   302  		}
   303  
   304  	}
   305  	return nil, vpcNotRecommendedf("VPC contains no public subnets")
   306  }
   307  
   308  func getVPCInternetGateway(apiClient vpcAPIClient, ctx context.ProviderCallContext, vpc *ec2.VPC) (*ec2.InternetGateway, error) {
   309  	filter := ec2.NewFilter()
   310  	filter.Add("attachment.vpc-id", vpc.Id)
   311  	response, err := apiClient.InternetGateways(nil, filter)
   312  	if err != nil {
   313  		return nil, errors.Annotatef(maybeConvertCredentialError(err, ctx), "unexpected AWS response getting Internet Gateway of VPC %q", vpc.Id)
   314  	}
   315  
   316  	if numResults := len(response.InternetGateways); numResults == 0 {
   317  		return nil, vpcNotRecommendedf("VPC has no Internet Gateway attached")
   318  	} else if numResults > 1 {
   319  		logger.Debugf("InternetGateways() returned %#v", response)
   320  		return nil, errors.Errorf("expected 1 result from AWS, got %d", numResults)
   321  	}
   322  
   323  	gateway := response.InternetGateways[0]
   324  	return &gateway, nil
   325  }
   326  
   327  func checkInternetGatewayIsAvailable(gateway *ec2.InternetGateway) error {
   328  	if state := gateway.AttachmentState; state != availableState {
   329  		return vpcNotRecommendedf("VPC has Internet Gateway %q in unexpected state %q", gateway.Id, state)
   330  	}
   331  
   332  	return nil
   333  }
   334  
   335  func getVPCRouteTables(apiClient vpcAPIClient, ctx context.ProviderCallContext, vpc *ec2.VPC) ([]ec2.RouteTable, error) {
   336  	filter := ec2.NewFilter()
   337  	filter.Add("vpc-id", vpc.Id)
   338  	response, err := apiClient.RouteTables(nil, filter)
   339  	if err != nil {
   340  		return nil, errors.Annotatef(maybeConvertCredentialError(err, ctx), "unexpected AWS response getting route tables of VPC %q", vpc.Id)
   341  	}
   342  
   343  	if len(response.Tables) == 0 {
   344  		return nil, vpcNotRecommendedf("VPC has no route tables")
   345  	}
   346  	logger.Tracef("RouteTables() returned %#v", response)
   347  
   348  	return response.Tables, nil
   349  }
   350  
   351  func getVPCCIDR(apiClient vpcAPIClient, ctx context.ProviderCallContext, vpcID string) (string, error) {
   352  	vpc, err := getVPCByID(apiClient, ctx, vpcID)
   353  	if err != nil {
   354  		return "", err
   355  	}
   356  	return vpc.CIDRBlock, nil
   357  }
   358  
   359  func findVPCMainRouteTable(routeTables []ec2.RouteTable) (*ec2.RouteTable, error) {
   360  	var mainTable *ec2.RouteTable
   361  	for i, table := range routeTables {
   362  		if len(table.Associations) < 1 {
   363  			logger.Tracef("ignoring VPC %q route table %q with no associations", table.VPCId, table.Id)
   364  			continue
   365  		}
   366  
   367  		for _, association := range table.Associations {
   368  			// TODO(dimitern): Of all the requirements, this seems like the most
   369  			// strict and likely to push users to use vpc-id-force=true. On the
   370  			// other hand, having to deal with more than the main route table's
   371  			// routes will likely overcomplicate the routes checks that follow.
   372  			if subnetID := association.SubnetId; subnetID != "" {
   373  				return nil, vpcNotRecommendedf("subnet %q not associated with VPC %q main route table", subnetID, table.VPCId)
   374  			}
   375  
   376  			if association.IsMain && mainTable == nil {
   377  				logger.Tracef("main route table of VPC %q has ID %q", table.VPCId, table.Id)
   378  				mainTable = &routeTables[i]
   379  			}
   380  		}
   381  	}
   382  
   383  	if mainTable == nil {
   384  		return nil, vpcNotRecommendedf("VPC has no associated main route table")
   385  	}
   386  
   387  	return mainTable, nil
   388  }
   389  
   390  func checkVPCRouteTableRoutes(vpc *ec2.VPC, routeTable *ec2.RouteTable, gateway *ec2.InternetGateway) error {
   391  	hasDefaultRoute := false
   392  	hasLocalRoute := false
   393  
   394  	logger.Tracef("checking route table %+v routes", routeTable)
   395  	for _, route := range routeTable.Routes {
   396  		if route.State != activeState {
   397  			logger.Tracef("skipping inactive route %+v", route)
   398  			continue
   399  		}
   400  
   401  		switch route.DestinationCIDRBlock {
   402  		case defaultRouteCIDRBlock:
   403  			if route.GatewayId == gateway.Id {
   404  				logger.Tracef("default route uses expected gateway %q", gateway.Id)
   405  				hasDefaultRoute = true
   406  			}
   407  		case vpc.CIDRBlock:
   408  			if route.GatewayId == localRouteGatewayID {
   409  				logger.Tracef("local route uses expected CIDR %q", vpc.CIDRBlock)
   410  				hasLocalRoute = true
   411  			}
   412  		default:
   413  			logger.Tracef("route %+v is neither local nor default (skipping)", route)
   414  		}
   415  	}
   416  
   417  	if hasDefaultRoute && hasLocalRoute {
   418  		return nil
   419  	}
   420  
   421  	if !hasDefaultRoute {
   422  		return vpcNotRecommendedf("missing default route via gateway %q", gateway.Id)
   423  	}
   424  	return vpcNotRecommendedf("missing local route with destination %q", vpc.CIDRBlock)
   425  }
   426  
   427  func findDefaultVPCID(apiClient vpcAPIClient, ctx context.ProviderCallContext) (string, error) {
   428  	response, err := apiClient.AccountAttributes("default-vpc")
   429  	if err != nil {
   430  		return "", errors.Annotate(maybeConvertCredentialError(err, ctx), "unexpected AWS response getting default-vpc account attribute")
   431  	}
   432  
   433  	if len(response.Attributes) == 0 ||
   434  		len(response.Attributes[0].Values) == 0 ||
   435  		response.Attributes[0].Name != "default-vpc" {
   436  		// No value for the requested "default-vpc" attribute, all bets are off.
   437  		return "", errors.NotFoundf("default-vpc account attribute")
   438  	}
   439  
   440  	firstAttributeValue := response.Attributes[0].Values[0]
   441  	if firstAttributeValue == vpcIDNone {
   442  		return "", errors.NotFoundf("default VPC")
   443  	}
   444  
   445  	return firstAttributeValue, nil
   446  }
   447  
   448  // getVPCSubnetIDsForAvailabilityZone returns a sorted list of subnet IDs, which
   449  // are both in the given vpcID and the given zoneName. If allowedSubnetIDs is
   450  // not empty, the returned list will only contain IDs present there. Returns an
   451  // error satisfying errors.IsNotFound() when no results match.
   452  func getVPCSubnetIDsForAvailabilityZone(
   453  	apiClient vpcAPIClient,
   454  	ctx context.ProviderCallContext,
   455  	vpcID, zoneName string,
   456  	allowedSubnetIDs []string,
   457  ) ([]string, error) {
   458  	allowedSubnets := set.NewStrings(allowedSubnetIDs...)
   459  	vpc := &ec2.VPC{Id: vpcID}
   460  	subnets, err := getVPCSubnets(apiClient, ctx, vpc)
   461  	if err != nil && !isVPCNotUsableError(err) {
   462  		return nil, errors.Annotatef(maybeConvertCredentialError(err, ctx), "cannot get VPC %q subnets", vpcID)
   463  	} else if isVPCNotUsableError(err) {
   464  		// We're reusing getVPCSubnets(), but not while validating a VPC
   465  		// pre-bootstrap, so we should change vpcNotUsableError to a simple
   466  		// NotFoundError.
   467  		message := fmt.Sprintf("VPC %q has no subnets in AZ %q", vpcID, zoneName)
   468  		return nil, errors.NewNotFound(err, message)
   469  	}
   470  
   471  	matchingSubnetIDs := set.NewStrings()
   472  	for _, subnet := range subnets {
   473  		if subnet.AvailZone != zoneName {
   474  			logger.Debugf("skipping subnet %q (in VPC %q): not in the chosen AZ %q", subnet.Id, vpcID, zoneName)
   475  			continue
   476  		}
   477  		if !allowedSubnets.IsEmpty() && !allowedSubnets.Contains(subnet.Id) {
   478  			logger.Debugf("skipping subnet %q (in VPC %q, AZ %q): not matching spaces constraints", subnet.Id, vpcID, zoneName)
   479  			continue
   480  		}
   481  		matchingSubnetIDs.Add(subnet.Id)
   482  	}
   483  
   484  	if matchingSubnetIDs.IsEmpty() {
   485  		message := fmt.Sprintf("VPC %q has no subnets in AZ %q", vpcID, zoneName)
   486  		return nil, errors.NewNotFound(nil, message)
   487  	}
   488  
   489  	sortedIDs := matchingSubnetIDs.SortedValues()
   490  	logger.Infof("found %d subnets in VPC %q matching AZ %q and constraints: %v", len(sortedIDs), vpcID, zoneName, sortedIDs)
   491  	return sortedIDs, nil
   492  }
   493  
   494  func findSubnetIDsForAvailabilityZone(zoneName string, subnetsToZones map[network.Id][]string) ([]string, error) {
   495  	matchingSubnetIDs := set.NewStrings()
   496  	for subnetID, zones := range subnetsToZones {
   497  		zonesSet := set.NewStrings(zones...)
   498  		if zonesSet.Contains(zoneName) {
   499  			matchingSubnetIDs.Add(string(subnetID))
   500  		}
   501  	}
   502  
   503  	if matchingSubnetIDs.IsEmpty() {
   504  		return nil, errors.NotFoundf("subnets in AZ %q", zoneName)
   505  	}
   506  
   507  	return matchingSubnetIDs.SortedValues(), nil
   508  }
   509  
   510  func isVPCIDSetButInvalid(vpcID string) bool {
   511  	return isVPCIDSet(vpcID) && !strings.HasPrefix(vpcID, "vpc-")
   512  }
   513  
   514  func isVPCIDSet(vpcID string) bool {
   515  	return vpcID != "" && vpcID != vpcIDNone
   516  }
   517  
   518  func validateBootstrapVPC(apiClient vpcAPIClient, cloudCtx context.ProviderCallContext, region, vpcID string, forceVPCID bool, ctx environs.BootstrapContext) error {
   519  	if vpcID == vpcIDNone {
   520  		ctx.Infof("Using EC2-classic features or default VPC in region %q", region)
   521  	}
   522  	if !isVPCIDSet(vpcID) {
   523  		return nil
   524  	}
   525  
   526  	err := validateVPC(apiClient, cloudCtx, vpcID)
   527  	switch {
   528  	case isVPCNotUsableError(err):
   529  		// VPC missing or has no subnets at all.
   530  		return errors.Annotate(err, vpcNotUsableForBootstrapErrorPrefix)
   531  	case isVPCNotRecommendedError(err):
   532  		// VPC does not meet minimum validation criteria.
   533  		if !forceVPCID {
   534  			return errors.Annotatef(err, vpcNotRecommendedErrorPrefix, vpcID)
   535  		}
   536  		ctx.Infof(vpcNotRecommendedButForcedWarning)
   537  	case err != nil:
   538  		// Anything else unexpected while validating the VPC.
   539  		return errors.Annotate(maybeConvertCredentialError(err, cloudCtx), cannotValidateVPCErrorPrefix)
   540  	}
   541  
   542  	ctx.Infof("Using VPC %q in region %q", vpcID, region)
   543  
   544  	return nil
   545  }
   546  
   547  func validateModelVPC(apiClient vpcAPIClient, ctx context.ProviderCallContext, modelName, vpcID string) error {
   548  	if !isVPCIDSet(vpcID) {
   549  		return nil
   550  	}
   551  
   552  	err := validateVPC(apiClient, ctx, vpcID)
   553  	switch {
   554  	case isVPCNotUsableError(err):
   555  		// VPC missing or has no subnets at all.
   556  		return errors.Annotate(err, vpcNotUsableForModelErrorPrefix)
   557  	case isVPCNotRecommendedError(err):
   558  		// VPC does not meet minimum validation criteria, but that's less
   559  		// important for hosted models, as the controller is already accessible.
   560  		logger.Infof(
   561  			"Juju will use, but does not recommend using VPC %q: %v",
   562  			vpcID, err.Error(),
   563  		)
   564  	case err != nil:
   565  		// Anything else unexpected while validating the VPC.
   566  		return errors.Annotate(maybeConvertCredentialError(err, ctx), cannotValidateVPCErrorPrefix)
   567  	}
   568  	logger.Infof("Using VPC %q for model %q", vpcID, modelName)
   569  
   570  	return nil
   571  }