github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/gce/environ.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package gce
     5  
     6  import (
     7  	stdcontext "context"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/juju/errors"
    13  	jujuhttp "github.com/juju/http/v2"
    14  	"google.golang.org/api/compute/v1"
    15  
    16  	jujucloud "github.com/juju/juju/cloud"
    17  	"github.com/juju/juju/core/instance"
    18  	corelogger "github.com/juju/juju/core/logger"
    19  	"github.com/juju/juju/core/network"
    20  	"github.com/juju/juju/core/network/firewall"
    21  	"github.com/juju/juju/environs"
    22  	environscloudspec "github.com/juju/juju/environs/cloudspec"
    23  	"github.com/juju/juju/environs/config"
    24  	"github.com/juju/juju/environs/context"
    25  	"github.com/juju/juju/environs/instances"
    26  	"github.com/juju/juju/environs/simplestreams"
    27  	"github.com/juju/juju/provider/common"
    28  	"github.com/juju/juju/provider/gce/google"
    29  )
    30  
    31  type gceConnection interface {
    32  	VerifyCredentials() error
    33  
    34  	// Instance gets the up-to-date info about the given instance
    35  	// and returns it.
    36  	Instance(id, zone string) (google.Instance, error)
    37  	Instances(prefix string, statuses ...string) ([]google.Instance, error)
    38  	AddInstance(spec google.InstanceSpec) (*google.Instance, error)
    39  	RemoveInstances(prefix string, ids ...string) error
    40  	UpdateMetadata(key, value string, ids ...string) error
    41  
    42  	IngressRules(fwname string) (firewall.IngressRules, error)
    43  	OpenPorts(fwname string, rules firewall.IngressRules) error
    44  	ClosePorts(fwname string, rules firewall.IngressRules) error
    45  
    46  	AvailabilityZones(region string) ([]google.AvailabilityZone, error)
    47  	// Subnetworks returns the subnetworks that machines can be
    48  	// assigned to in the given region.
    49  	Subnetworks(region string) ([]*compute.Subnetwork, error)
    50  	// Networks returns the available networks that exist across
    51  	// regions.
    52  	Networks() ([]*compute.Network, error)
    53  
    54  	// Storage related methods.
    55  
    56  	// CreateDisks will attempt to create the disks described by <disks> spec and
    57  	// return a slice of Disk representing the created disks or error if one of them failed.
    58  	CreateDisks(zone string, disks []google.DiskSpec) ([]*google.Disk, error)
    59  	// Disks will return a list of all Disks found in the project.
    60  	Disks() ([]*google.Disk, error)
    61  	// Disk will return a Disk representing the disk identified by the
    62  	// passed <name> or error.
    63  	Disk(zone, id string) (*google.Disk, error)
    64  	// RemoveDisk will destroy the disk identified by <name> in <zone>.
    65  	RemoveDisk(zone, id string) error
    66  	// SetDiskLabels sets the labels on a disk, ensuring that the disk's
    67  	// label fingerprint matches the one supplied.
    68  	SetDiskLabels(zone, id, labelFingerprint string, labels map[string]string) error
    69  	// AttachDisk will attach the volume identified by <volumeName> into the instance
    70  	// <instanceId> and return an AttachedDisk representing it or error.
    71  	AttachDisk(zone, volumeName, instanceId string, mode google.DiskMode) (*google.AttachedDisk, error)
    72  	// DetachDisk will detach <volumeName> disk from <instanceId> if possible
    73  	// and return error.
    74  	DetachDisk(zone, instanceId, volumeName string) error
    75  	// InstanceDisks returns a list of the disks attached to the passed instance.
    76  	InstanceDisks(zone, instanceId string) ([]*google.AttachedDisk, error)
    77  	// ListMachineTypes returns a list of machines available in the project and zone provided.
    78  	ListMachineTypes(zone string) ([]google.MachineType, error)
    79  }
    80  
    81  type environ struct {
    82  	environs.NoSpaceDiscoveryEnviron
    83  
    84  	name  string
    85  	uuid  string
    86  	cloud environscloudspec.CloudSpec
    87  	gce   gceConnection
    88  
    89  	lock sync.Mutex // lock protects access to ecfg
    90  	ecfg *environConfig
    91  
    92  	instTypeListLock    sync.Mutex
    93  	instCacheExpireAt   time.Time
    94  	cachedInstanceTypes []instances.InstanceType
    95  
    96  	// namespace is used to create the machine and device hostnames.
    97  	namespace instance.Namespace
    98  }
    99  
   100  var _ environs.Environ = (*environ)(nil)
   101  var _ environs.NetworkingEnviron = (*environ)(nil)
   102  
   103  // Function entry points defined as variables so they can be overridden
   104  // for testing purposes.
   105  var (
   106  	newConnection = func(ctx stdcontext.Context, conn google.ConnectionConfig, creds *google.Credentials) (gceConnection, error) {
   107  		return google.Connect(ctx, conn, creds)
   108  	}
   109  	destroyEnv = common.Destroy
   110  	bootstrap  = common.Bootstrap
   111  )
   112  
   113  func newEnviron(cloud environscloudspec.CloudSpec, cfg *config.Config) (*environ, error) {
   114  	ecfg, err := newConfig(cfg, nil)
   115  	if err != nil {
   116  		return nil, errors.Annotate(err, "invalid config")
   117  	}
   118  
   119  	namespace, err := instance.NewNamespace(cfg.UUID())
   120  	if err != nil {
   121  		return nil, errors.Trace(err)
   122  	}
   123  
   124  	e := &environ{
   125  		name:      ecfg.config.Name(),
   126  		uuid:      ecfg.config.UUID(),
   127  		ecfg:      ecfg,
   128  		namespace: namespace,
   129  	}
   130  	if err = e.SetCloudSpec(stdcontext.TODO(), cloud); err != nil {
   131  		return nil, err
   132  	}
   133  	return e, nil
   134  }
   135  
   136  // SetCloudSpec is specified in the environs.Environ interface.
   137  func (e *environ) SetCloudSpec(_ stdcontext.Context, spec environscloudspec.CloudSpec) error {
   138  	e.lock.Lock()
   139  	defer e.lock.Unlock()
   140  
   141  	e.cloud = spec
   142  	credAttrs := spec.Credential.Attributes()
   143  	if spec.Credential.AuthType() == jujucloud.JSONFileAuthType {
   144  		contents := credAttrs[credAttrFile]
   145  		credential, err := parseJSONAuthFile(strings.NewReader(contents))
   146  		if err != nil {
   147  			return errors.Trace(err)
   148  		}
   149  		credAttrs = credential.Attributes()
   150  	}
   151  
   152  	credential := &google.Credentials{
   153  		ClientID:    credAttrs[credAttrClientID],
   154  		ProjectID:   credAttrs[credAttrProjectID],
   155  		ClientEmail: credAttrs[credAttrClientEmail],
   156  		PrivateKey:  []byte(credAttrs[credAttrPrivateKey]),
   157  	}
   158  
   159  	connectionConfig := google.ConnectionConfig{
   160  		Region:    spec.Region,
   161  		ProjectID: credential.ProjectID,
   162  
   163  		// TODO (Stickupkid): Pass the http.Client through on the construction
   164  		// of the environ.
   165  		HTTPClient: jujuhttp.NewClient(
   166  			jujuhttp.WithSkipHostnameVerification(spec.SkipTLSVerify),
   167  			jujuhttp.WithLogger(logger.ChildWithLabels("http", corelogger.HTTP)),
   168  		),
   169  	}
   170  
   171  	// TODO (stickupkid): Pass the context through the method call.
   172  	ctx := stdcontext.Background()
   173  
   174  	// Connect and authenticate.
   175  	var err error
   176  	e.gce, err = newConnection(ctx, connectionConfig, credential)
   177  	if err != nil {
   178  		return errors.Trace(err)
   179  	}
   180  	return nil
   181  }
   182  
   183  // Name returns the name of the environment.
   184  func (env *environ) Name() string {
   185  	return env.name
   186  }
   187  
   188  // Provider returns the environment provider that created this env.
   189  func (*environ) Provider() environs.EnvironProvider {
   190  	return providerInstance
   191  }
   192  
   193  // Region returns the CloudSpec to use for the provider, as configured.
   194  func (env *environ) Region() (simplestreams.CloudSpec, error) {
   195  	return simplestreams.CloudSpec{
   196  		Region:   env.cloud.Region,
   197  		Endpoint: env.cloud.Endpoint,
   198  	}, nil
   199  }
   200  
   201  // SetConfig updates the env's configuration.
   202  func (env *environ) SetConfig(cfg *config.Config) error {
   203  	env.lock.Lock()
   204  	defer env.lock.Unlock()
   205  
   206  	ecfg, err := newConfig(cfg, env.ecfg.config)
   207  	if err != nil {
   208  		return errors.Annotate(err, "invalid config change")
   209  	}
   210  	env.ecfg = ecfg
   211  	return nil
   212  }
   213  
   214  // Config returns the configuration data with which the env was created.
   215  func (env *environ) Config() *config.Config {
   216  	env.lock.Lock()
   217  	defer env.lock.Unlock()
   218  	return env.ecfg.config
   219  }
   220  
   221  // PrepareForBootstrap implements environs.Environ.
   222  func (env *environ) PrepareForBootstrap(ctx environs.BootstrapContext, controllerName string) error {
   223  	if ctx.ShouldVerifyCredentials() {
   224  		if err := env.gce.VerifyCredentials(); err != nil {
   225  			return errors.Trace(err)
   226  		}
   227  	}
   228  	return nil
   229  }
   230  
   231  // Create implements environs.Environ.
   232  func (env *environ) Create(ctx context.ProviderCallContext, p environs.CreateParams) error {
   233  	if err := env.gce.VerifyCredentials(); err != nil {
   234  		return google.HandleCredentialError(errors.Trace(err), ctx)
   235  	}
   236  	return nil
   237  }
   238  
   239  // Bootstrap creates a new instance, choosing the series and arch out of
   240  // available tools. The series and arch are returned along with a func
   241  // that must be called to finalize the bootstrap process by transferring
   242  // the tools and installing the initial juju controller.
   243  func (env *environ) Bootstrap(ctx environs.BootstrapContext, callCtx context.ProviderCallContext, params environs.BootstrapParams) (*environs.BootstrapResult, error) {
   244  	// Ensure the API server port is open (globally for all instances
   245  	// on the network, not just for the specific node of the state
   246  	// server). See LP bug #1436191 for details.
   247  	rules := firewall.IngressRules{
   248  		firewall.NewIngressRule(
   249  			network.PortRange{
   250  				FromPort: params.ControllerConfig.APIPort(),
   251  				ToPort:   params.ControllerConfig.APIPort(),
   252  				Protocol: "tcp",
   253  			},
   254  		),
   255  	}
   256  	if params.ControllerConfig.AutocertDNSName() != "" {
   257  		// Open port 80 as well as it handles Let's Encrypt HTTP challenge.
   258  		rules = append(rules, firewall.NewIngressRule(network.MustParsePortRange("80/tcp")))
   259  	}
   260  
   261  	if err := env.gce.OpenPorts(env.globalFirewallName(), rules); err != nil {
   262  		return nil, google.HandleCredentialError(errors.Trace(err), callCtx)
   263  	}
   264  	return bootstrap(ctx, env, callCtx, params)
   265  }
   266  
   267  // Destroy shuts down all known machines and destroys the rest of the
   268  // known environment.
   269  func (env *environ) Destroy(ctx context.ProviderCallContext) error {
   270  	ports, err := env.IngressRules(ctx)
   271  	if err != nil {
   272  		return errors.Trace(err)
   273  	}
   274  
   275  	if len(ports) > 0 {
   276  		if err := env.ClosePorts(ctx, ports); err != nil {
   277  			return errors.Trace(err)
   278  		}
   279  	}
   280  
   281  	return destroyEnv(env, ctx)
   282  }
   283  
   284  // DestroyController implements the Environ interface.
   285  func (env *environ) DestroyController(ctx context.ProviderCallContext, controllerUUID string) error {
   286  	// TODO(wallyworld): destroy hosted model resources
   287  	return env.Destroy(ctx)
   288  }