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 }