github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/oci/provider.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package oci 5 6 import ( 7 stdcontext "context" 8 "fmt" 9 "net" 10 "os" 11 "strings" 12 13 "github.com/juju/clock" 14 "github.com/juju/errors" 15 "github.com/juju/jsonschema" 16 "github.com/juju/loggo" 17 "github.com/juju/schema" 18 ociIdentity "github.com/oracle/oci-go-sdk/v65/identity" 19 "gopkg.in/ini.v1" 20 "gopkg.in/juju/environschema.v1" 21 22 "github.com/juju/juju/cloud" 23 "github.com/juju/juju/core/instance" 24 "github.com/juju/juju/environs" 25 environscloudspec "github.com/juju/juju/environs/cloudspec" 26 "github.com/juju/juju/environs/config" 27 "github.com/juju/juju/environs/context" 28 "github.com/juju/juju/provider/oci/common" 29 ) 30 31 var logger = loggo.GetLogger("juju.provider.oci") 32 33 // EnvironProvider type implements environs.EnvironProvider interface 34 type EnvironProvider struct{} 35 36 type environConfig struct { 37 *config.Config 38 attrs map[string]interface{} 39 } 40 41 var _ config.ConfigSchemaSource = (*EnvironProvider)(nil) 42 var _ environs.ProviderSchema = (*EnvironProvider)(nil) 43 44 var configSchema = environschema.Fields{ 45 "compartment-id": { 46 Description: "The OCID of the compartment in which juju has access to create resources.", 47 Type: environschema.Tstring, 48 }, 49 "address-space": { 50 Description: "The CIDR block to use when creating default subnets. The subnet must have at least a /16 size.", 51 Type: environschema.Tstring, 52 }, 53 } 54 55 var configDefaults = schema.Defaults{ 56 "compartment-id": "", 57 "address-space": DefaultAddressSpace, 58 } 59 60 var configFields = func() schema.Fields { 61 fs, _, err := configSchema.ValidationSchema() 62 if err != nil { 63 panic(err) 64 } 65 return fs 66 }() 67 68 // credentialSection holds the keys present in one section of the OCI 69 // config file, as created by the OCI command line. This is only used 70 // during credential detection 71 type credentialSection struct { 72 User string 73 Tenancy string 74 KeyFile string 75 PassPhrase string 76 Fingerprint string 77 } 78 79 var credentialSchema = map[cloud.AuthType]cloud.CredentialSchema{ 80 cloud.HTTPSigAuthType: { 81 { 82 "user", cloud.CredentialAttr{ 83 Description: "Username OCID", 84 }, 85 }, 86 { 87 "tenancy", cloud.CredentialAttr{ 88 Description: "Tenancy OCID", 89 }, 90 }, 91 { 92 "key", cloud.CredentialAttr{ 93 Description: "PEM encoded private key", 94 }, 95 }, 96 { 97 "pass-phrase", cloud.CredentialAttr{ 98 Description: "Passphrase used to unlock the key", 99 Hidden: true, 100 }, 101 }, 102 { 103 "fingerprint", cloud.CredentialAttr{ 104 Description: "Private key fingerprint", 105 }, 106 }, 107 // Deprecated, but still supported for backward compatibility 108 { 109 "region", cloud.CredentialAttr{ 110 Description: "DEPRECATED: Region to log into", 111 }, 112 }, 113 }, 114 } 115 116 func (p EnvironProvider) newConfig(cfg *config.Config) (*environConfig, error) { 117 if cfg == nil { 118 return nil, errors.New("cannot set config on uninitialized env") 119 } 120 121 valid, err := p.Validate(cfg, nil) 122 if err != nil { 123 return nil, err 124 } 125 return &environConfig{valid, valid.UnknownAttrs()}, nil 126 } 127 128 func (c *environConfig) compartmentID() *string { 129 compartmentID := c.attrs["compartment-id"].(string) 130 if compartmentID == "" { 131 return nil 132 } 133 return &compartmentID 134 } 135 136 func (c *environConfig) addressSpace() *string { 137 addressSpace := c.attrs["address-space"].(string) 138 if addressSpace == "" { 139 addressSpace = DefaultAddressSpace 140 } 141 return &addressSpace 142 } 143 144 // Schema implements environs.ProviderSchema 145 func (o *EnvironProvider) Schema() environschema.Fields { 146 fields, err := config.Schema(configSchema) 147 if err != nil { 148 panic(err) 149 } 150 return fields 151 } 152 153 // ConfigSchema implements config.ConfigSchemaSource 154 func (o *EnvironProvider) ConfigSchema() schema.Fields { 155 return configFields 156 } 157 158 // ConfigDefaults implements config.ConfigSchemaSource 159 func (o *EnvironProvider) ConfigDefaults() schema.Defaults { 160 return configDefaults 161 } 162 163 // Version implements environs.EnvironProvider. 164 func (e EnvironProvider) Version() int { 165 return 1 166 } 167 168 // CloudSchema implements environs.EnvironProvider. 169 func (e EnvironProvider) CloudSchema() *jsonschema.Schema { 170 return nil 171 } 172 173 // Ping implements environs.EnvironProvider. 174 func (e *EnvironProvider) Ping(ctx context.ProviderCallContext, endpoint string) error { 175 return errors.NotImplementedf("Ping") 176 } 177 178 func validateCloudSpec(c environscloudspec.CloudSpec) error { 179 if err := c.Validate(); err != nil { 180 return errors.Trace(err) 181 } 182 if c.Credential == nil { 183 return errors.NotValidf("missing credential") 184 } 185 if authType := c.Credential.AuthType(); authType != cloud.HTTPSigAuthType { 186 return errors.NotSupportedf("%q auth-type", authType) 187 } 188 return nil 189 } 190 191 // PrepareConfig implements environs.EnvironProvider. 192 func (e EnvironProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { 193 if err := validateCloudSpec(args.Cloud); err != nil { 194 return nil, errors.Annotate(err, "validating cloud spec") 195 } 196 // TODO(gsamfira): Set default block storage backend 197 return args.Config, nil 198 } 199 200 // Open implements environs.EnvironProvider. 201 func (e *EnvironProvider) Open(_ stdcontext.Context, params environs.OpenParams) (environs.Environ, error) { 202 logger.Infof("opening model %q", params.Config.Name()) 203 204 if err := validateCloudSpec(params.Cloud); err != nil { 205 return nil, errors.Trace(err) 206 } 207 208 creds := params.Cloud.Credential.Attributes() 209 providerConfig := common.JujuConfigProvider{ 210 Key: []byte(creds["key"]), 211 Fingerprint: creds["fingerprint"], 212 Passphrase: creds["pass-phrase"], 213 Tenancy: creds["tenancy"], 214 User: creds["user"], 215 OCIRegion: params.Cloud.Region, 216 } 217 // We don't support setting a default region in the credentials anymore. Because, such approach conflicts with the 218 // way we handle regions in Juju. 219 if creds["region"] != "" { 220 logger.Warningf("Setting a default region in Oracle Cloud credentials is not supported.") 221 } 222 err := providerConfig.Validate() 223 if err != nil { 224 return nil, errors.Trace(err) 225 } 226 compute, err := common.NewComputeClient(providerConfig) 227 if err != nil { 228 return nil, errors.Trace(err) 229 } 230 231 networking, err := common.NewNetworkClient(providerConfig) 232 if err != nil { 233 return nil, errors.Trace(err) 234 } 235 236 storage, err := common.NewStorageClient(providerConfig) 237 if err != nil { 238 return nil, errors.Trace(err) 239 } 240 241 identity, err := ociIdentity.NewIdentityClientWithConfigurationProvider(providerConfig) 242 if err != nil { 243 return nil, errors.Trace(err) 244 } 245 246 env := &Environ{ 247 Compute: compute, 248 Networking: networking, 249 Storage: storage, 250 Firewall: networking, 251 Identity: identity, 252 ociConfig: providerConfig, 253 clock: clock.WallClock, 254 p: e, 255 } 256 257 if err := env.SetConfig(params.Config); err != nil { 258 return nil, err 259 } 260 261 env.namespace, err = instance.NewNamespace(env.Config().UUID()) 262 if err != nil { 263 return nil, errors.Trace(err) 264 } 265 266 cfg := env.ecfg() 267 if cfg.compartmentID() == nil { 268 return nil, errors.New("compartment-id may not be empty") 269 } 270 271 addressSpace := cfg.addressSpace() 272 if _, ipNET, err := net.ParseCIDR(*addressSpace); err == nil { 273 size, _ := ipNET.Mask.Size() 274 if size > 16 { 275 return nil, errors.Errorf("configured subnet (%q) is not large enough. Please use a prefix length in the range /8 to /16. Current prefix length is /%d", *addressSpace, size) 276 } 277 } else { 278 return nil, errors.Trace(err) 279 } 280 281 return env, nil 282 } 283 284 // CredentialSchemas implements environs.ProviderCredentials. 285 func (e EnvironProvider) CredentialSchemas() map[cloud.AuthType]cloud.CredentialSchema { 286 return credentialSchema 287 } 288 289 // DetectCredentials implements environs.ProviderCredentials. 290 // Configuration options for the OCI SDK are detailed here: 291 // https://docs.us-phoenix-1.oraclecloud.com/Content/API/Concepts/sdkconfig.htm 292 func (e EnvironProvider) DetectCredentials(cloudName string) (*cloud.CloudCredential, error) { 293 result := cloud.CloudCredential{ 294 AuthCredentials: make(map[string]cloud.Credential), 295 } 296 cfg_file, err := ociConfigFile() 297 if err != nil { 298 if os.IsNotExist(errors.Cause(err)) { 299 return &result, nil 300 } 301 return nil, errors.Trace(err) 302 } 303 304 cfg, err := ini.LooseLoad(cfg_file) 305 if err != nil { 306 return nil, errors.Trace(err) 307 } 308 cfg.NameMapper = ini.TitleUnderscore 309 310 for _, val := range cfg.SectionStrings() { 311 values := new(credentialSection) 312 if err := cfg.Section(val).MapTo(values); err != nil { 313 logger.Warningf("invalid value in section %s: %s", val, err) 314 continue 315 } 316 missingFields := []string{} 317 if values.User == "" { 318 missingFields = append(missingFields, "user") 319 } 320 321 if values.Tenancy == "" { 322 missingFields = append(missingFields, "tenancy") 323 } 324 325 if values.KeyFile == "" { 326 missingFields = append(missingFields, "key_file") 327 } 328 329 if values.Fingerprint == "" { 330 missingFields = append(missingFields, "fingerprint") 331 } 332 333 if len(missingFields) > 0 { 334 logger.Warningf("missing required field(s) in section %s: %s", val, strings.Join(missingFields, ", ")) 335 continue 336 } 337 338 pemFileContent, err := os.ReadFile(values.KeyFile) 339 if err != nil { 340 return nil, errors.Trace(err) 341 } 342 if err := common.ValidateKey(pemFileContent, values.PassPhrase); err != nil { 343 logger.Warningf("failed to decrypt PEM %s using the configured pass phrase", values.KeyFile) 344 continue 345 } 346 347 httpSigCreds := cloud.NewCredential( 348 cloud.HTTPSigAuthType, 349 map[string]string{ 350 "user": values.User, 351 "tenancy": values.Tenancy, 352 "key": string(pemFileContent), 353 "pass-phrase": values.PassPhrase, 354 "fingerprint": values.Fingerprint, 355 }, 356 ) 357 httpSigCreds.Label = fmt.Sprintf("OCI credential %q", val) 358 result.AuthCredentials[val] = httpSigCreds 359 } 360 if len(result.AuthCredentials) == 0 { 361 return nil, errors.NotFoundf("OCI credentials") 362 } 363 return &result, nil 364 } 365 366 // FinalizeCredential implements environs.ProviderCredentials. 367 func (e EnvironProvider) FinalizeCredential( 368 ctx environs.FinalizeCredentialContext, 369 params environs.FinalizeCredentialParams) (*cloud.Credential, error) { 370 371 return ¶ms.Credential, nil 372 } 373 374 // Validate implements config.Validator. 375 func (e EnvironProvider) Validate(cfg, old *config.Config) (valid *config.Config, err error) { 376 if err := config.Validate(cfg, old); err != nil { 377 return nil, err 378 } 379 newAttrs, err := cfg.ValidateUnknownAttrs( 380 configFields, configDefaults, 381 ) 382 if err != nil { 383 return nil, err 384 } 385 386 return cfg.Apply(newAttrs) 387 }