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

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package openstack
     5  
     6  import (
     7  	"net/http"
     8  
     9  	"github.com/go-goose/goose/v5/client"
    10  	goosehttp "github.com/go-goose/goose/v5/http"
    11  	"github.com/go-goose/goose/v5/identity"
    12  	gooselogging "github.com/go-goose/goose/v5/logging"
    13  	"github.com/go-goose/goose/v5/neutron"
    14  	"github.com/go-goose/goose/v5/nova"
    15  	"github.com/juju/errors"
    16  	jujuhttp "github.com/juju/http/v2"
    17  	"github.com/juju/loggo"
    18  
    19  	corelogger "github.com/juju/juju/core/logger"
    20  	environscloudspec "github.com/juju/juju/environs/cloudspec"
    21  )
    22  
    23  // ClientOption to be passed into the transport construction to customize the
    24  // default transport.
    25  type ClientOption func(*clientOptions)
    26  
    27  type clientOptions struct {
    28  	caCertificates           []string
    29  	skipHostnameVerification bool
    30  	httpHeadersFunc          goosehttp.HeadersFunc
    31  	httpClient               *http.Client
    32  }
    33  
    34  // WithCACertificates contains Authority certificates to be used to validate
    35  // certificates of cloud infrastructure components.
    36  // The contents are Base64 encoded x.509 certs.
    37  func WithCACertificates(value ...string) ClientOption {
    38  	return func(opt *clientOptions) {
    39  		opt.caCertificates = value
    40  	}
    41  }
    42  
    43  // WithSkipHostnameVerification will skip hostname verification on the TLS/SSL
    44  // certificates.
    45  func WithSkipHostnameVerification(value bool) ClientOption {
    46  	return func(opt *clientOptions) {
    47  		opt.skipHostnameVerification = value
    48  	}
    49  }
    50  
    51  // WithHTTPHeadersFunc allows passing in a new HTTP headers func for the client
    52  // to execute for each request.
    53  func WithHTTPHeadersFunc(httpHeadersFunc goosehttp.HeadersFunc) ClientOption {
    54  	return func(clientOptions *clientOptions) {
    55  		clientOptions.httpHeadersFunc = httpHeadersFunc
    56  	}
    57  }
    58  
    59  // WithHTTPClient allows to define the http.Client to use.
    60  func WithHTTPClient(value *http.Client) ClientOption {
    61  	return func(opt *clientOptions) {
    62  		opt.httpClient = value
    63  	}
    64  }
    65  
    66  // Create a clientOptions instance with default values.
    67  func newOptions() *clientOptions {
    68  	// In this case, use a default http.Client.
    69  	// Ideally we should always use the NewHTTPTLSTransport,
    70  	// however test suites such as JujuConnSuite and some facade
    71  	// tests rely on settings to the http.DefaultTransport for
    72  	// tests to run with different protocol scheme such as "test"
    73  	// and some replace the RoundTripper to answer test scenarios.
    74  	//
    75  	// https://bugs.launchpad.net/juju/+bug/1888888
    76  	defaultCopy := *http.DefaultClient
    77  
    78  	return &clientOptions{
    79  		httpHeadersFunc: goosehttp.DefaultHeaders,
    80  		httpClient:      &defaultCopy,
    81  	}
    82  }
    83  
    84  // SSLHostnameConfig defines the options for host name verification
    85  type SSLHostnameConfig interface {
    86  	SSLHostnameVerification() bool
    87  }
    88  
    89  // ClientFunc is used to create a goose client.
    90  type ClientFunc = func(cred identity.Credentials,
    91  	authMode identity.AuthMode,
    92  	options ...ClientOption) (client.AuthenticatingClient, error)
    93  
    94  // ClientFactory creates various goose (openstack) clients.
    95  // TODO (stickupkid): This should be moved into goose and the factory should
    96  // accept a configuration returning back goose clients.
    97  type ClientFactory struct {
    98  	spec              environscloudspec.CloudSpec
    99  	sslHostnameConfig SSLHostnameConfig
   100  
   101  	// We store the auth client, so nova can reuse it.
   102  	authClient client.AuthenticatingClient
   103  
   104  	// clientFunc is used to create a client from a set of arguments.
   105  	clientFunc ClientFunc
   106  }
   107  
   108  // NewClientFactory creates a new ClientFactory from the CloudSpec and environ
   109  // config arguments.
   110  func NewClientFactory(spec environscloudspec.CloudSpec, sslHostnameConfig SSLHostnameConfig) *ClientFactory {
   111  	return &ClientFactory{
   112  		spec:              spec,
   113  		sslHostnameConfig: sslHostnameConfig,
   114  		clientFunc:        newClient,
   115  	}
   116  }
   117  
   118  // Init the client factory, returns an error if the initialization fails.
   119  func (c *ClientFactory) Init() error {
   120  	// This is an unwanted side effect of the previous implementation only
   121  	// calling AuthClient once.
   122  	// To prevent the regression of calling it three times, one for checking,
   123  	// which auth client to use, then for nova and then for neutron.
   124  	// We get the auth client for the factory and reuse it for nova.
   125  	authClient, err := c.getClientState()
   126  	if err != nil {
   127  		return errors.Trace(err)
   128  	}
   129  	c.authClient = authClient
   130  	return nil
   131  }
   132  
   133  // AuthClient returns a goose AuthenticatingClient.
   134  func (c *ClientFactory) AuthClient() client.AuthenticatingClient {
   135  	return c.authClient
   136  }
   137  
   138  // Nova creates a new Nova client from the auth mode (v3 or falls back to v2)
   139  // and the updated credentials.
   140  func (c *ClientFactory) Nova() (*nova.Client, error) {
   141  	return nova.New(c.authClient), nil
   142  }
   143  
   144  // Neutron creates a new Neutron client from the auth mode (v3 or falls back to v2)
   145  // and the updated credentials.
   146  // Note: we override the http.Client headers with specific neutron client
   147  // headers.
   148  func (c *ClientFactory) Neutron() (*neutron.Client, error) {
   149  	client, err := c.getClientState(WithHTTPHeadersFunc(neutron.NeutronHeaders))
   150  	if err != nil {
   151  		return nil, errors.Trace(err)
   152  	}
   153  	return neutron.New(client), nil
   154  }
   155  
   156  func (c *ClientFactory) getClientState(options ...ClientOption) (client.AuthenticatingClient, error) {
   157  	identityClientVersion, err := identityClientVersion(c.spec.Endpoint)
   158  	if err != nil {
   159  		return nil, errors.Annotate(err, "cannot create a client")
   160  	}
   161  	cred, authMode, err := newCredentials(c.spec)
   162  	if err != nil {
   163  		return nil, errors.Annotate(err, "cannot create credential")
   164  	}
   165  
   166  	// Create a new fallback client using the existing authMode.
   167  	newClient, _ := c.getClientByAuthMode(authMode, cred, options...)
   168  
   169  	// Before returning, lets make sure that we want to have AuthMode
   170  	// AuthUserPass instead of its V3 counterpart.
   171  	if authMode == identity.AuthUserPass && (identityClientVersion == -1 || identityClientVersion == 3) {
   172  		authOptions, err := newClient.IdentityAuthOptions()
   173  		if err != nil {
   174  			logger.Errorf("cannot determine available auth versions %v", err)
   175  		}
   176  
   177  		// Walk over the options to verify if the AuthUserPassV3 exists, if it
   178  		// does exist use that to attempt authentication.
   179  		var authOption *identity.AuthOption
   180  		for _, v := range authOptions {
   181  			option := v
   182  			if option.Mode == identity.AuthUserPassV3 {
   183  				authOption = &option
   184  				break
   185  			}
   186  		}
   187  
   188  		// No AuthUserPassV3 found, exit early as no additional work is
   189  		// required.
   190  		if authOption == nil {
   191  			return newClient, nil
   192  		}
   193  
   194  		// Update the credential with the new identity.AuthOption and
   195  		// attempt to Authenticate.
   196  		newCreds := &cred
   197  		newCreds.URL = authOption.Endpoint
   198  
   199  		newClientV3, err := c.getClientByAuthMode(identity.AuthUserPassV3, *newCreds, options...)
   200  		if err != nil {
   201  			return nil, errors.Trace(err)
   202  		}
   203  
   204  		// If the AuthUserPassV3 client can authenticate, use it.
   205  		if err = newClientV3.Authenticate(); err == nil {
   206  			return newClientV3, nil
   207  		}
   208  		if identityClientVersion == 3 {
   209  			// We know it's a v3 server, so we can't fall back to v2.
   210  			return nil, errors.Trace(err)
   211  		}
   212  		// Otherwise, fall back to the v2 client.
   213  	}
   214  	return newClient, nil
   215  }
   216  
   217  // getClientByAuthMode creates a new client for the given AuthMode.
   218  func (c *ClientFactory) getClientByAuthMode(authMode identity.AuthMode, cred identity.Credentials, options ...ClientOption) (client.AuthenticatingClient, error) {
   219  	newClient, err := c.clientFunc(cred, authMode,
   220  		append(options,
   221  			WithSkipHostnameVerification(!c.sslHostnameConfig.SSLHostnameVerification()),
   222  			WithCACertificates(c.spec.CACertificates...),
   223  		)...,
   224  	)
   225  	if err != nil {
   226  		return nil, errors.NewNotValid(err, "cannot create a new client")
   227  	}
   228  
   229  	// Juju requires "compute" at a minimum. We'll use "network" if it's
   230  	// available in preference to the Neutron network APIs; and "volume" or
   231  	// "volume2" for storage if either one is available.
   232  	newClient.SetRequiredServiceTypes([]string{"compute"})
   233  	return newClient, nil
   234  }
   235  
   236  // newClient returns an authenticating client to talk to the
   237  // OpenStack cloud.  CACertificate and SSLHostnameVerification == false
   238  // config options are mutually exclusive here.
   239  func newClient(
   240  	cred identity.Credentials,
   241  	authMode identity.AuthMode,
   242  	clientOptions ...ClientOption,
   243  ) (client.AuthenticatingClient, error) {
   244  	opts := newOptions()
   245  	for _, option := range clientOptions {
   246  		option(opts)
   247  	}
   248  
   249  	logger := loggo.GetLogger("goose")
   250  	gooseLogger := gooselogging.DebugLoggerAdapater{
   251  		Logger: logger,
   252  	}
   253  
   254  	httpClient := jujuhttp.NewClient(
   255  		jujuhttp.WithSkipHostnameVerification(opts.skipHostnameVerification),
   256  		jujuhttp.WithCACertificates(opts.caCertificates...),
   257  		jujuhttp.WithLogger(logger.ChildWithLabels("http", corelogger.HTTP)),
   258  	)
   259  	return client.NewClient(&cred, authMode, gooseLogger,
   260  		client.WithHTTPClient(httpClient.Client()),
   261  		client.WithHTTPHeadersFunc(opts.httpHeadersFunc),
   262  	), nil
   263  }