github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/powervs/session.go (about) 1 package powervs 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "os" 9 "path/filepath" 10 "strings" 11 "time" 12 13 survey "github.com/AlecAivazis/survey/v2" 14 "github.com/IBM-Cloud/power-go-client/ibmpisession" 15 "github.com/IBM/go-sdk-core/v5/core" 16 "github.com/form3tech-oss/jwt-go" 17 "github.com/sirupsen/logrus" 18 "k8s.io/apimachinery/pkg/util/sets" 19 20 "github.com/openshift/installer/pkg/types/powervs" 21 ) 22 23 var ( 24 defSessionTimeout time.Duration = 9000000000000000000.0 25 defRegion = "us_south" 26 defaultAuthFilePath = filepath.Join(os.Getenv("HOME"), ".powervs", "config.json") 27 ) 28 29 // BxClient is struct which provides bluemix session details 30 type BxClient struct { 31 APIKey string 32 Region string 33 Zone string 34 PISession *ibmpisession.IBMPISession 35 User *User 36 PowerVSResourceGroup string 37 } 38 39 // User is struct with user details 40 type User struct { 41 ID string 42 Email string 43 Account string 44 } 45 46 // SessionStore is an object and store that holds credentials and variables required to create a SessionVars object. 47 type SessionStore struct { 48 ID string `json:"id,omitempty"` 49 APIKey string `json:"apikey,omitempty"` 50 DefaultRegion string `json:"region,omitempty"` 51 DefaultZone string `json:"zone,omitempty"` 52 PowerVSResourceGroup string `json:"resourcegroup,omitempty"` 53 } 54 55 // SessionVars is an object that holds the variables required to create an ibmpisession object. 56 type SessionVars struct { 57 ID string 58 APIKey string 59 Region string 60 Zone string 61 PowerVSResourceGroup string 62 } 63 64 func authenticateAPIKey(apikey string) (string, error) { 65 a, err := core.NewIamAuthenticatorBuilder().SetApiKey(apikey).Build() 66 if err != nil { 67 return "", err 68 } 69 token, err := a.GetToken() 70 if err != nil { 71 return "", err 72 } 73 return token, nil 74 } 75 76 // FetchUserDetails returns User details from the given API key. 77 func FetchUserDetails(apikey string) (*User, error) { 78 user := User{} 79 var bluemixToken string 80 81 iamToken, err := authenticateAPIKey(apikey) 82 if err != nil { 83 return &user, err 84 } 85 86 if strings.HasPrefix(iamToken, "Bearer ") { 87 bluemixToken = iamToken[len("Bearer "):] 88 } else { 89 bluemixToken = iamToken 90 } 91 92 token, err := jwt.Parse(bluemixToken, func(token *jwt.Token) (interface{}, error) { 93 return "", nil 94 }) 95 if err != nil && !strings.Contains(err.Error(), "key is of invalid type") { 96 return &user, err 97 } 98 99 claims := token.Claims.(jwt.MapClaims) 100 if email, ok := claims["email"]; ok { 101 user.Email = email.(string) 102 } 103 user.ID = claims["id"].(string) 104 user.Account = claims["account"].(map[string]interface{})["bss"].(string) 105 106 return &user, nil 107 } 108 109 // NewBxClient func returns bluemix client 110 func NewBxClient(survey bool) (*BxClient, error) { 111 c := &BxClient{} 112 sv, err := getSessionVars(survey) 113 if err != nil { 114 return nil, err 115 } 116 117 c.APIKey = sv.APIKey 118 c.Region = sv.Region 119 c.Zone = sv.Zone 120 c.PowerVSResourceGroup = sv.PowerVSResourceGroup 121 122 c.User, err = FetchUserDetails(c.APIKey) 123 if err != nil { 124 return nil, err 125 } 126 127 return c, nil 128 } 129 130 func getSessionVars(survey bool) (SessionVars, error) { 131 var sv SessionVars 132 var ss SessionStore 133 134 // Grab the session store from the installer written authFilePath 135 logrus.Debug("Gathering credentials from AuthFile") 136 err := getSessionStoreFromAuthFile(&ss) 137 if err != nil { 138 return sv, err 139 } 140 141 // Transfer the store to vars if they were found in the AuthFile 142 sv.ID = ss.ID 143 sv.APIKey = ss.APIKey 144 sv.Region = ss.DefaultRegion 145 sv.Zone = ss.DefaultZone 146 sv.PowerVSResourceGroup = ss.PowerVSResourceGroup 147 148 // Grab variables from the users environment 149 logrus.Debug("Gathering variables from user environment") 150 err = getSessionVarsFromEnv(&sv) 151 if err != nil { 152 return sv, err 153 } 154 155 // Grab variable from the user themselves 156 if survey { 157 // Prompt the user for the first set of remaining variables. 158 err = getFirstSessionVarsFromUser(&sv, &ss) 159 if err != nil { 160 return sv, err 161 } 162 163 // Transfer vars to the store to write out to the AuthFile 164 ss.ID = sv.ID 165 ss.APIKey = sv.APIKey 166 ss.DefaultRegion = sv.Region 167 ss.DefaultZone = sv.Zone 168 ss.PowerVSResourceGroup = sv.PowerVSResourceGroup 169 170 // Save the session store to the disk. 171 err = saveSessionStoreToAuthFile(&ss) 172 if err != nil { 173 return sv, err 174 } 175 176 // Since there is a minimal store at this point, it is safe 177 // to call the function. 178 // Prompt the user for the second set of remaining variables. 179 err = getSecondSessionVarsFromUser(&sv, &ss) 180 if err != nil { 181 return sv, err 182 } 183 } 184 185 // Transfer vars to the store to write out to the AuthFile 186 ss.ID = sv.ID 187 ss.APIKey = sv.APIKey 188 ss.DefaultRegion = sv.Region 189 ss.DefaultZone = sv.Zone 190 ss.PowerVSResourceGroup = sv.PowerVSResourceGroup 191 192 // Save the session store to the disk. 193 err = saveSessionStoreToAuthFile(&ss) 194 if err != nil { 195 return sv, err 196 } 197 198 return sv, nil 199 } 200 201 // NewPISession updates pisession details, return error on fail. 202 func (c *BxClient) NewPISession() error { 203 var authenticator core.Authenticator = &core.IamAuthenticator{ 204 ApiKey: c.APIKey, 205 } 206 207 // Create the session 208 options := &ibmpisession.IBMPIOptions{ 209 Authenticator: authenticator, 210 UserAccount: c.User.Account, 211 Region: c.Region, 212 Zone: c.Zone, 213 Debug: false, 214 } 215 216 // Avoid by defining err as a variable: non-name c.PISession on left side of := 217 var err error 218 c.PISession, err = ibmpisession.NewIBMPISession(options) 219 if err != nil { 220 return err 221 } 222 223 return nil 224 } 225 226 // GetBxClientAPIKey returns the API key used by the Blue Mix Client. 227 func (c *BxClient) GetBxClientAPIKey() string { 228 return c.APIKey 229 } 230 231 // getSessionStoreFromAuthFile gets the session creds from the auth file. 232 func getSessionStoreFromAuthFile(pss *SessionStore) error { 233 if pss == nil { 234 return errors.New("nil var: SessionStore") 235 } 236 237 authFilePath := defaultAuthFilePath 238 if f := os.Getenv("POWERVS_AUTH_FILEPATH"); len(f) > 0 { 239 authFilePath = f 240 } 241 242 if _, err := os.Stat(authFilePath); os.IsNotExist(err) { 243 return nil 244 } 245 246 content, err := os.ReadFile(authFilePath) 247 if err != nil { 248 return err 249 } 250 251 err = json.Unmarshal(content, pss) 252 if err != nil { 253 return err 254 } 255 256 return nil 257 } 258 259 func getSessionVarsFromEnv(psv *SessionVars) error { 260 if psv == nil { 261 return errors.New("nil var: PiSessionVars") 262 } 263 264 if len(psv.ID) == 0 { 265 psv.ID = os.Getenv("IBMID") 266 } 267 268 if len(psv.APIKey) == 0 { 269 // APIKeyEnvVars is a list of environment variable names containing an IBM Cloud API key. 270 var APIKeyEnvVars = []string{"IC_API_KEY", "IBMCLOUD_API_KEY", "BM_API_KEY", "BLUEMIX_API_KEY"} 271 psv.APIKey = getEnv(APIKeyEnvVars) 272 } 273 274 if len(psv.Region) == 0 { 275 var regionEnvVars = []string{"IBMCLOUD_REGION", "IC_REGION"} 276 psv.Region = getEnv(regionEnvVars) 277 } 278 279 if len(psv.Zone) == 0 { 280 var zoneEnvVars = []string{"IBMCLOUD_ZONE"} 281 psv.Zone = getEnv(zoneEnvVars) 282 } 283 284 if len(psv.PowerVSResourceGroup) == 0 { 285 var resourceEnvVars = []string{"IBMCLOUD_RESOURCE_GROUP"} 286 psv.PowerVSResourceGroup = getEnv(resourceEnvVars) 287 } 288 289 return nil 290 } 291 292 // Prompt the user for the first set of remaining variables. 293 // This is a chicken and egg problem. We cannot call NewBxClient() or NewClient() 294 // yet for complicated questions to the user since those calls load the session 295 // variables from the store. There is the possibility that the are empty at the 296 // moment. 297 func getFirstSessionVarsFromUser(psv *SessionVars, pss *SessionStore) error { 298 var err error 299 300 if psv == nil { 301 return errors.New("nil var: PiSessionVars") 302 } 303 304 if len(psv.ID) == 0 { 305 err = survey.Ask([]*survey.Question{ 306 { 307 Prompt: &survey.Input{ 308 Message: "IBM Cloud User ID", 309 Help: "The login for \nhttps://cloud.ibm.com/", 310 }, 311 }, 312 }, &psv.ID) 313 if err != nil { 314 return errors.New("error saving the IBM Cloud User ID") 315 } 316 } 317 318 if len(psv.APIKey) == 0 { 319 err = survey.Ask([]*survey.Question{ 320 { 321 Prompt: &survey.Password{ 322 Message: "IBM Cloud API Key", 323 Help: "The API key installation.\nhttps://cloud.ibm.com/iam/apikeys", 324 }, 325 }, 326 }, &psv.APIKey) 327 if err != nil { 328 return errors.New("error saving the API Key") 329 } 330 } 331 332 return nil 333 } 334 335 // Prompt the user for the second set of remaining variables. 336 // This is a chicken and egg problem. Now we can call NewBxClient() or NewClient() 337 // because the session store should at least have some minimal settings like the 338 // APIKey. 339 func getSecondSessionVarsFromUser(psv *SessionVars, pss *SessionStore) error { 340 var ( 341 client *Client 342 err error 343 ) 344 345 if psv == nil { 346 return errors.New("nil var: PiSessionVars") 347 } 348 349 if len(psv.Region) == 0 { 350 psv.Region, err = GetRegion(pss.DefaultRegion) 351 if err != nil { 352 return err 353 } 354 } 355 356 if len(psv.Zone) == 0 { 357 psv.Zone, err = GetZone(psv.Region, pss.DefaultZone) 358 if err != nil { 359 return err 360 } 361 } 362 363 if len(psv.PowerVSResourceGroup) == 0 { 364 if client == nil { 365 client, err = NewClient() 366 if err != nil { 367 return fmt.Errorf("failed to powervs.NewClient: %w", err) 368 } 369 } 370 371 ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Minute) 372 defer cancel() 373 374 resourceGroups, err := client.ListResourceGroups(ctx) 375 if err != nil { 376 return fmt.Errorf("failed to list resourceGroups: %w", err) 377 } 378 379 resourceGroupsSurvey := make([]string, len(resourceGroups.Resources)) 380 for i, resourceGroup := range resourceGroups.Resources { 381 resourceGroupsSurvey[i] = *resourceGroup.Name 382 } 383 384 err = survey.Ask([]*survey.Question{ 385 { 386 Prompt: &survey.Select{ 387 Message: "Resource Group", 388 Help: "The Power VS resource group to be used for installation.", 389 Default: "", 390 Options: resourceGroupsSurvey, 391 }, 392 }, 393 }, &psv.PowerVSResourceGroup) 394 if err != nil { 395 return fmt.Errorf("survey.ask failed with: %w", err) 396 } 397 } 398 399 return nil 400 } 401 402 func saveSessionStoreToAuthFile(pss *SessionStore) error { 403 authFilePath := defaultAuthFilePath 404 if f := os.Getenv("POWERVS_AUTH_FILEPATH"); len(f) > 0 { 405 authFilePath = f 406 } 407 408 jsonVars, err := json.Marshal(*pss) 409 if err != nil { 410 return err 411 } 412 413 err = os.MkdirAll(filepath.Dir(authFilePath), 0700) 414 if err != nil { 415 return err 416 } 417 418 return os.WriteFile(authFilePath, jsonVars, 0o600) 419 } 420 421 func getEnv(envs []string) string { 422 for _, k := range envs { 423 if v := os.Getenv(k); v != "" { 424 return v 425 } 426 } 427 return "" 428 } 429 430 // FilterServiceEndpoints drops service endpoint overrides that are not supported by PowerVS CAPI provider. 431 func (c *BxClient) FilterServiceEndpoints(cfg *powervs.Metadata) []string { 432 capiSupported := sets.New("cos", "powervs", "rc", "rm", "vpc") // see serviceIDs array definition in https://github.com/kubernetes-sigs/cluster-api-provider-ibmcloud/blob/main/pkg/endpoints/endpoints.go 433 overrides := make([]string, 0, len(cfg.ServiceEndpoints)) 434 // CAPI expects name=url pairs of service endpoints 435 for _, endpoint := range cfg.ServiceEndpoints { 436 if capiSupported.Has(endpoint.Name) { 437 overrides = append(overrides, fmt.Sprintf("%s=%s", endpoint.Name, endpoint.URL)) 438 } else { 439 logrus.Infof("Unsupported service endpoint skipped: %s", endpoint.Name) 440 } 441 } 442 return overrides 443 }