github.com/vnpaycloud-console/gophercloud/v2@v2.0.5/openstack/config/clouds/clouds.go (about) 1 // package clouds provides a parser for OpenStack credentials stored in a clouds.yaml file. 2 // 3 // Example use: 4 // 5 // ctx := context.Background() 6 // ao, eo, tlsConfig, err := clouds.Parse() 7 // if err != nil { 8 // panic(err) 9 // } 10 // 11 // providerClient, err := config.NewProviderClient(ctx, ao, config.WithTLSConfig(tlsConfig)) 12 // if err != nil { 13 // panic(err) 14 // } 15 // 16 // networkClient, err := openstack.NewNetworkV2(providerClient, eo) 17 // if err != nil { 18 // panic(err) 19 // } 20 package clouds 21 22 import ( 23 "crypto/tls" 24 "encoding/json" 25 "fmt" 26 "os" 27 "path" 28 "reflect" 29 30 "github.com/vnpaycloud-console/gophercloud/v2" 31 "gopkg.in/yaml.v2" 32 ) 33 34 // Parse fetches a clouds.yaml file from disk and returns the parsed 35 // credentials. 36 // 37 // By default this function mimics the behaviour of python-openstackclient, which is: 38 // 39 // - if the environment variable `OS_CLIENT_CONFIG_FILE` is set and points to a 40 // clouds.yaml, use that location as the only search location for `clouds.yaml` and `secure.yaml`; 41 // - otherwise, the search locations for `clouds.yaml` and `secure.yaml` are: 42 // 1. the current working directory (on Linux: `./`) 43 // 2. the directory `openstack` under the standatd user config location for 44 // the operating system (on Linux: `${XDG_CONFIG_HOME:-$HOME/.config}/openstack/`) 45 // 3. on Linux, `/etc/openstack/` 46 // 47 // Once `clouds.yaml` is found in a search location, the same location is used to search for `secure.yaml`. 48 // 49 // Like in python-openstackclient, relative paths in the `clouds.yaml` section 50 // `cacert` are interpreted as relative the the current directory, and not to 51 // the `clouds.yaml` location. 52 // 53 // Search locations, as well as individual `clouds.yaml` properties, can be 54 // overwritten with functional options. 55 func Parse(opts ...ParseOption) (gophercloud.AuthOptions, gophercloud.EndpointOpts, *tls.Config, error) { 56 options := cloudOpts{ 57 cloudName: os.Getenv("OS_CLOUD"), 58 region: os.Getenv("OS_REGION_NAME"), 59 endpointType: os.Getenv("OS_INTERFACE"), 60 locations: func() []string { 61 if path := os.Getenv("OS_CLIENT_CONFIG_FILE"); path != "" { 62 return []string{path} 63 } 64 return nil 65 }(), 66 } 67 68 for _, apply := range opts { 69 apply(&options) 70 } 71 72 if options.cloudName == "" { 73 return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("the empty string \"\" is not a valid cloud name") 74 } 75 76 // Set the defaults and open the files for reading. This code only runs 77 // if no override has been set, because it is fallible. 78 if options.cloudsyamlReader == nil { 79 if len(options.locations) < 1 { 80 cwd, err := os.Getwd() 81 if err != nil { 82 return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("failed to get the current working directory: %w", err) 83 } 84 userConfig, err := os.UserConfigDir() 85 if err != nil { 86 return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("failed to get the user config directory: %w", err) 87 } 88 options.locations = []string{path.Join(cwd, "clouds.yaml"), path.Join(userConfig, "openstack", "clouds.yaml"), path.Join("/etc", "openstack", "clouds.yaml")} 89 } 90 91 for _, cloudsPath := range options.locations { 92 f, err := os.Open(cloudsPath) 93 if err != nil { 94 continue 95 } 96 defer f.Close() 97 options.cloudsyamlReader = f 98 99 if options.secureyamlReader == nil { 100 securePath := path.Join(path.Dir(cloudsPath), "secure.yaml") 101 secureF, err := os.Open(securePath) 102 if err == nil { 103 defer secureF.Close() 104 options.secureyamlReader = secureF 105 } 106 } 107 break 108 } 109 if options.cloudsyamlReader == nil { 110 return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("clouds file not found. Search locations were: %v", options.locations) 111 } 112 } 113 114 // Parse the YAML payloads. 115 var clouds Clouds 116 if err := yaml.NewDecoder(options.cloudsyamlReader).Decode(&clouds); err != nil { 117 return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, err 118 } 119 120 cloud, ok := clouds.Clouds[options.cloudName] 121 if !ok { 122 return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("cloud %q not found in clouds.yaml", options.cloudName) 123 } 124 125 if options.secureyamlReader != nil { 126 var secureClouds Clouds 127 if err := yaml.NewDecoder(options.secureyamlReader).Decode(&secureClouds); err != nil { 128 return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("failed to parse secure.yaml: %w", err) 129 } 130 131 if secureCloud, ok := secureClouds.Clouds[options.cloudName]; ok { 132 // If secureCloud has content and it differs from the cloud entry, 133 // merge the two together. 134 if !reflect.DeepEqual((gophercloud.AuthOptions{}), secureClouds) && !reflect.DeepEqual(clouds, secureClouds) { 135 var err error 136 cloud, err = mergeClouds(secureCloud, cloud) 137 if err != nil { 138 return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("unable to merge information from clouds.yaml and secure.yaml") 139 } 140 } 141 } 142 } 143 144 tlsConfig, err := computeTLSConfig(cloud, options) 145 if err != nil { 146 return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("unable to compute TLS configuration: %w", err) 147 } 148 149 endpointType := coalesce(options.endpointType, cloud.EndpointType, cloud.Interface) 150 151 var scope *gophercloud.AuthScope 152 if trustID := cloud.AuthInfo.TrustID; trustID != "" { 153 scope = &gophercloud.AuthScope{ 154 TrustID: trustID, 155 } 156 } 157 158 return gophercloud.AuthOptions{ 159 IdentityEndpoint: coalesce(options.authURL, cloud.AuthInfo.AuthURL), 160 Username: coalesce(options.username, cloud.AuthInfo.Username), 161 UserID: coalesce(options.userID, cloud.AuthInfo.UserID), 162 Password: coalesce(options.password, cloud.AuthInfo.Password), 163 DomainID: coalesce(options.domainID, cloud.AuthInfo.UserDomainID, cloud.AuthInfo.ProjectDomainID, cloud.AuthInfo.DomainID), 164 DomainName: coalesce(options.domainName, cloud.AuthInfo.UserDomainName, cloud.AuthInfo.ProjectDomainName, cloud.AuthInfo.DomainName), 165 TenantID: coalesce(options.projectID, cloud.AuthInfo.ProjectID), 166 TenantName: coalesce(options.projectName, cloud.AuthInfo.ProjectName), 167 TokenID: coalesce(options.token, cloud.AuthInfo.Token), 168 Scope: coalesce(options.scope, scope), 169 ApplicationCredentialID: coalesce(options.applicationCredentialID, cloud.AuthInfo.ApplicationCredentialID), 170 ApplicationCredentialName: coalesce(options.applicationCredentialName, cloud.AuthInfo.ApplicationCredentialName), 171 ApplicationCredentialSecret: coalesce(options.applicationCredentialSecret, cloud.AuthInfo.ApplicationCredentialSecret), 172 }, gophercloud.EndpointOpts{ 173 Region: coalesce(options.region, cloud.RegionName), 174 Availability: computeAvailability(endpointType), 175 }, 176 tlsConfig, 177 nil 178 } 179 180 // computeAvailability is a helper method to determine the endpoint type 181 // requested by the user. 182 func computeAvailability(endpointType string) gophercloud.Availability { 183 if endpointType == "internal" || endpointType == "internalURL" { 184 return gophercloud.AvailabilityInternal 185 } 186 if endpointType == "admin" || endpointType == "adminURL" { 187 return gophercloud.AvailabilityAdmin 188 } 189 return gophercloud.AvailabilityPublic 190 } 191 192 // coalesce returns the first argument that is not the zero value for its type, 193 // or the zero value for its type. 194 func coalesce[T comparable](items ...T) T { 195 var t T 196 for _, item := range items { 197 if item != t { 198 return item 199 } 200 } 201 return t 202 } 203 204 // mergeClouds merges two Clouds recursively (the AuthInfo also gets merged). 205 // In case both Clouds define a value, the value in the 'override' cloud takes precedence 206 func mergeClouds(override, cloud Cloud) (Cloud, error) { 207 overrideJson, err := json.Marshal(override) 208 if err != nil { 209 return Cloud{}, err 210 } 211 cloudJson, err := json.Marshal(cloud) 212 if err != nil { 213 return Cloud{}, err 214 } 215 var overrideInterface any 216 err = json.Unmarshal(overrideJson, &overrideInterface) 217 if err != nil { 218 return Cloud{}, err 219 } 220 var cloudInterface any 221 err = json.Unmarshal(cloudJson, &cloudInterface) 222 if err != nil { 223 return Cloud{}, err 224 } 225 var mergedCloud Cloud 226 mergedInterface := mergeInterfaces(overrideInterface, cloudInterface) 227 mergedJson, err := json.Marshal(mergedInterface) 228 if err != nil { 229 return Cloud{}, err 230 } 231 err = json.Unmarshal(mergedJson, &mergedCloud) 232 if err != nil { 233 return Cloud{}, err 234 } 235 return mergedCloud, nil 236 } 237 238 // merges two interfaces. In cases where a value is defined for both 'overridingInterface' and 239 // 'inferiorInterface' the value in 'overridingInterface' will take precedence. 240 func mergeInterfaces(overridingInterface, inferiorInterface any) any { 241 switch overriding := overridingInterface.(type) { 242 case map[string]any: 243 interfaceMap, ok := inferiorInterface.(map[string]any) 244 if !ok { 245 return overriding 246 } 247 for k, v := range interfaceMap { 248 if overridingValue, ok := overriding[k]; ok { 249 overriding[k] = mergeInterfaces(overridingValue, v) 250 } else { 251 overriding[k] = v 252 } 253 } 254 case []any: 255 list, ok := inferiorInterface.([]any) 256 if !ok { 257 return overriding 258 } 259 260 return append(overriding, list...) 261 case nil: 262 // mergeClouds(nil, map[string]interface{...}) -> map[string]interface{...} 263 v, ok := inferiorInterface.(map[string]any) 264 if ok { 265 return v 266 } 267 } 268 // We don't want to override with empty values 269 if reflect.DeepEqual(overridingInterface, nil) || reflect.DeepEqual(reflect.Zero(reflect.TypeOf(overridingInterface)).Interface(), overridingInterface) { 270 return inferiorInterface 271 } else { 272 return overridingInterface 273 } 274 }