github.com/IBM-Cloud/bluemix-go@v0.0.0-20240423071914-9e96525baef4/api/container/containerv2/openshift.go (about) 1 package containerv2 2 3 /******************************************************************************* 4 * IBM Confidential 5 * OCO Source Materials 6 * IBM Cloud Schematics 7 * (C) Copyright IBM Corp. 2017 All Rights Reserved. 8 * The source code for this program is not published or otherwise divested of 9 * its trade secrets, irrespective of what has been deposited with 10 * the U.S. Copyright Office. 11 ******************************************************************************/ 12 13 /******************************************************************************* 14 * A file for openshift related utility functions, like getting kube 15 * config 16 ******************************************************************************/ 17 18 import ( 19 "encoding/base64" 20 "errors" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "net/http" 25 "net/url" 26 "regexp" 27 "runtime/debug" 28 "strings" 29 "time" 30 31 yaml "github.com/ghodss/yaml" 32 33 "github.com/IBM-Cloud/bluemix-go/client" 34 bxhttp "github.com/IBM-Cloud/bluemix-go/http" 35 "github.com/IBM-Cloud/bluemix-go/rest" 36 "github.com/IBM-Cloud/bluemix-go/trace" 37 ) 38 39 const ( 40 // IAMHTTPtimeout - 41 IAMHTTPtimeout = 10 * time.Second 42 VirtualPrivateEndpoint = "vpe" 43 PrivateServiceEndpoint = "private" 44 VirtualPrivateEndpointDNS = ".vpe.private" 45 PrivateEndpointDNS = ".private" 46 ) 47 48 // Frame - 49 type Frame uintptr 50 51 // StackTrace - 52 type StackTrace []Frame 53 type stackTracer interface { 54 StackTrace() StackTrace 55 } 56 57 type openShiftUser struct { 58 Kind string `json:"kind"` 59 APIVersion string `json:"apiVersion"` 60 Metadata struct { 61 Name string `json:"name"` 62 SelfLink string `json:"selfLink"` 63 UID string `json:"uid"` 64 ResourceVersion string `json:"resourceVersion"` 65 CreationTimestamp time.Time `json:"creationTimestamp"` 66 } `json:"metadata"` 67 Identities []string `json:"identities"` 68 Groups []string `json:"groups"` 69 } 70 71 type authEndpoints struct { 72 Issuer string `json:"issuer"` 73 AuthorizationEndpoint string `json:"authorization_endpoint"` 74 TokenEndpoint string `json:"token_endpoint"` 75 ServerURL string `json:"server_endpoint,omitempty"` 76 } 77 78 // PanicCatch - Catch panic and give error 79 func PanicCatch(r interface{}) error { 80 if r != nil { 81 var e error 82 switch x := r.(type) { 83 case string: 84 e = errors.New(x) 85 case error: 86 e = x 87 default: 88 e = errors.New("Unknown panic") 89 } 90 fmt.Printf("Panic error %v", e) 91 if err, ok := e.(stackTracer); ok { 92 fmt.Printf("Panic stack trace %v", err.StackTrace()) 93 } else { 94 debug.PrintStack() 95 } 96 return e 97 } 98 return nil 99 } 100 101 // NormalizeName - 102 func NormalizeName(name string) (string, error) { 103 name = strings.ToLower(name) 104 reg, err := regexp.Compile("[^A-Za-z0-9:]+") 105 if err != nil { 106 return "", err 107 } 108 return reg.ReplaceAllString(name, "-"), nil 109 } 110 111 // logInAndFillOCToken will update kubeConfig with an Openshift token, if one is not there 112 func (r *clusters) FetchOCTokenForKubeConfig(kubecfg []byte, cMeta *ClusterInfo, skipSSLVerification bool, endpointType string) (kubecfgEdited []byte, host string, rerr error) { 113 // TODO: this is not a a standard manner to login ... using propriatary OC cli reverse engineering 114 defer func() { 115 err := PanicCatch(recover()) 116 if err != nil { 117 rerr = fmt.Errorf("could not login to openshift account %s", err) 118 } 119 }() 120 121 var cfg map[string]interface{} 122 err := yaml.Unmarshal(kubecfg, &cfg) 123 if err != nil { 124 return kubecfg, "", err 125 } 126 var token, passcode string 127 if r.client.Config.BluemixAPIKey == "" { 128 trace.Logger.Println("Creating user passcode to login for getting oc token") 129 130 // Retry to cover rate limiting on passcode endpoint in particular 131 for try := 1; try <= 3; try++ { 132 passcode, err = r.client.TokenRefresher.GetPasscode() 133 134 if err == nil { 135 break 136 } 137 138 if err != nil && try == 3 { 139 return kubecfg, "", err 140 } 141 142 time.Sleep(1 * time.Second) 143 } 144 } 145 146 // honor the endpointType parameter if the current parameter is different 147 switch endpointType { 148 case PrivateServiceEndpoint: 149 if !strings.Contains(cMeta.ServerURL, PrivateEndpointDNS) || strings.Contains(cMeta.ServerURL, VirtualPrivateEndpointDNS) { 150 // Could be changed to private only if the cluster's private service endpoint is enabled and public is enabled 151 if cMeta.ServiceEndpoints.PrivateServiceEndpointEnabled && cMeta.ServiceEndpoints.PrivateServiceEndpointURL != "" && !cMeta.ServiceEndpoints.PublicServiceEndpointEnabled { 152 // As this is Openshift, we need to use the URL with the signed certificate (-e) (the right URL is not available in getCluster response) 153 urlParts := strings.Split(cMeta.ServiceEndpoints.PrivateServiceEndpointURL, ".") 154 cMeta.ServerURL = urlParts[0] + "-e." + strings.Join(urlParts[1:], ".") 155 } else { 156 trace.Logger.Println("Ignore endpoint parameter and use default ServerURL - currently unsupported scenario") 157 } 158 } 159 case VirtualPrivateEndpoint: 160 if !strings.Contains(cMeta.ServerURL, VirtualPrivateEndpointDNS) && !cMeta.ServiceEndpoints.PublicServiceEndpointEnabled { 161 if cMeta.VirtualPrivateEndpointURL != "" { 162 cMeta.ServerURL = cMeta.VirtualPrivateEndpointURL 163 } else { 164 return kubecfg, "", fmt.Errorf("virtual private endpoint is not supported by the cluster") 165 } 166 } 167 } 168 169 authEP, err := func(meta *ClusterInfo) (*authEndpoints, error) { 170 request := rest.GetRequest(meta.ServerURL + "/.well-known/oauth-authorization-server") 171 var auth authEndpoints 172 173 // Create new REST client - reusing modified existing client instances could lead to race conditions 174 restClient := &rest.Client{} 175 resp, err := restClient.Do(request, &auth, nil) 176 177 if err != nil { 178 return &auth, err 179 } 180 defer resp.Body.Close() 181 if resp.StatusCode > 299 { 182 msg, _ := ioutil.ReadAll(resp.Body) 183 return nil, fmt.Errorf("bad status code [%d] returned when fetching Cluster authentication endpoints: %s", resp.StatusCode, msg) 184 } 185 if endpointType != "" { 186 auth.AuthorizationEndpoint, err = reconfigureAuthorizationEndpoint(auth.AuthorizationEndpoint, endpointType, meta) 187 if err != nil { 188 return &auth, err 189 } 190 } 191 auth.ServerURL = meta.ServerURL 192 return &auth, nil 193 }(cMeta) 194 195 if err != nil { 196 return kubecfg, "", err 197 } 198 199 trace.Logger.Println("Got authentication endpoints for getting oc token") 200 token, uname, err := r.openShiftAuthorizePasscode(authEP, passcode, cMeta.IsStagingSatelliteCluster()) 201 202 if err != nil { 203 return kubecfg, "", err 204 } 205 206 trace.Logger.Println("Got the token and user ", uname) 207 clusterName, _ := NormalizeName(authEP.ServerURL[len("https://"):len(authEP.ServerURL)]) //TODO deal with http 208 ccontext := "default/" + clusterName + "/" + uname 209 uname = uname + "/" + clusterName 210 clusters := cfg["clusters"].([]interface{}) 211 newCluster := map[string]interface{}{"name": clusterName, "cluster": map[string]interface{}{"server": authEP.ServerURL}} 212 if skipSSLVerification { 213 newCluster["cluster"].(map[string]interface{})["insecure-skip-tls-verify"] = true 214 } 215 clusters = append(clusters, newCluster) 216 cfg["clusters"] = clusters 217 218 contexts := cfg["contexts"].([]interface{}) 219 newContext := map[string]interface{}{"name": ccontext, "context": map[string]interface{}{"cluster": clusterName, "namespace": "default", "user": uname}} 220 contexts = append(contexts, newContext) 221 cfg["contexts"] = contexts 222 223 users := cfg["users"].([]interface{}) 224 newUser := map[string]interface{}{"name": uname, "user": map[string]interface{}{"token": token}} 225 users = append(users, newUser) 226 cfg["users"] = users 227 228 cfg["current-context"] = ccontext 229 230 bytes, err := yaml.Marshal(cfg) 231 if err != nil { 232 return kubecfg, "", err 233 } 234 kubecfg = bytes 235 return kubecfg, cMeta.ServerURL, nil 236 } 237 238 // Never redirect. Let caller handle. This is an http.Client callback method (CheckRedirect) 239 func neverRedirect(req *http.Request, via []*http.Request) error { 240 return http.ErrUseLastResponse 241 } 242 243 func (r *clusters) openShiftAuthorizePasscode(authEP *authEndpoints, passcode string, skipSSLVerification bool) (string, string, error) { 244 var request *rest.Request 245 authString := "passcode:" + passcode 246 if r.client.Config.BluemixAPIKey != "" { 247 apikey := r.client.Config.BluemixAPIKey 248 authString = "apikey:" + apikey 249 } 250 request = rest.GetRequest(authEP.AuthorizationEndpoint+"?response_type=token&client_id=openshift-challenging-client"). 251 Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(authString))) 252 // Creating a new client instance (instead of tempering with existing one) to avoid race conditions 253 copyConfig := r.client.Config.Copy() 254 copyConfig.SSLDisable = skipSSLVerification 255 copyConfig.HTTPClient = bxhttp.NewHTTPClient(copyConfig) 256 copyConfig.HTTPClient.CheckRedirect = neverRedirect 257 258 client := client.New(copyConfig, r.client.ServiceName, r.client.TokenRefresher) 259 260 var respInterface interface{} 261 var resp *http.Response 262 var err error 263 for try := 1; try <= 3; try++ { 264 // bmxerror.NewRequestFailure("ServerErrorResponse", string(raw), resp.StatusCode) 265 resp, err = client.SendRequest(request, respInterface) 266 if err != nil { 267 if resp.StatusCode != 302 { 268 return "", "", err 269 } 270 } 271 defer resp.Body.Close() 272 if resp.StatusCode > 399 { 273 if try >= 3 { 274 msg, _ := io.ReadAll(resp.Body) 275 return "", "", fmt.Errorf("bad status code [%d] returned when openshift login: %s", resp.StatusCode, string(msg)) 276 } 277 time.Sleep(200 * time.Millisecond) 278 } else { 279 break 280 } 281 } 282 283 loc, err := resp.Location() 284 if err != nil { 285 return "", "", err 286 } 287 val, err := url.ParseQuery(loc.Fragment) 288 if err != nil { 289 return "", "", err 290 } 291 token := val.Get("access_token") 292 trace.Logger.Println("Getting username after getting the token") 293 name, err := r.getOpenShiftUser(authEP, token) 294 if err != nil { 295 return "", "", err 296 } 297 return token, name, nil 298 } 299 300 func (r *clusters) getOpenShiftUser(authEP *authEndpoints, token string) (string, error) { 301 request := rest.GetRequest(authEP.ServerURL+"/apis/user.openshift.io/v1/users/~"). 302 Set("Authorization", "Bearer "+token) 303 304 var user openShiftUser 305 resp, err := r.client.SendRequest(request, &user) 306 if err != nil { 307 return "", err 308 } 309 defer resp.Body.Close() 310 if resp.StatusCode > 299 { 311 msg, _ := io.ReadAll(resp.Body) 312 return "", fmt.Errorf("bad status code [%d] returned when fetching OpenShift user Details: %s", resp.StatusCode, string(msg)) 313 } 314 315 return user.Metadata.Name, nil 316 } 317 318 // honor endpointType for OauthServer if the current parameter is different 319 func reconfigureAuthorizationEndpoint(originalAuthEndpoint string, endpointType string, clusterInfo *ClusterInfo) (string, error) { 320 urlDefault, err := url.ParseRequestURI(originalAuthEndpoint) 321 if err != nil || urlDefault.Host == "" { 322 return "", fmt.Errorf("could not parse original auth endpoint raw url: %s, error: %v", originalAuthEndpoint, err) 323 } 324 switch endpointType { 325 case PrivateServiceEndpoint: 326 if (!strings.Contains(originalAuthEndpoint, PrivateEndpointDNS) || strings.Contains(originalAuthEndpoint, VirtualPrivateEndpointDNS)) && 327 !clusterInfo.ServiceEndpoints.PublicServiceEndpointEnabled && 328 clusterInfo.ServiceEndpoints.PrivateServiceEndpointEnabled { 329 urlPrivate, err := url.ParseRequestURI(clusterInfo.ServiceEndpoints.PrivateServiceEndpointURL) 330 if err != nil || urlPrivate.Host == "" { 331 return "", fmt.Errorf("could not parse private service endpoint raw url, cluster may not support it: %s, error: %v", clusterInfo.ServiceEndpoints.PrivateServiceEndpointURL, err) 332 } 333 // As this is Openshift, we need to use the URL with the signed certificate (the right URL is not available in getCluster response) 334 hostNameParts := strings.Split(urlPrivate.Hostname(), ".") 335 hostName := hostNameParts[0] + "-e." + strings.Join(hostNameParts[1:], ".") 336 337 u := url.URL{ 338 Scheme: urlDefault.Scheme, 339 Host: hostName + ":" + urlDefault.Port(), 340 Path: urlDefault.Path, 341 } 342 return u.String(), nil 343 } else { 344 trace.Logger.Println("Ignore endpoint parameter and use default OauthServerURL - currently unsupported scenario") 345 } 346 case VirtualPrivateEndpoint: 347 if !strings.Contains(originalAuthEndpoint, VirtualPrivateEndpointDNS) && !clusterInfo.ServiceEndpoints.PublicServiceEndpointEnabled { 348 urlVPE, err := url.ParseRequestURI(clusterInfo.VirtualPrivateEndpointURL) 349 if err != nil || urlVPE.Host == "" { 350 return "", fmt.Errorf("could not parse virtual private endpoint raw url, cluster may not support it: %s, error: %v", clusterInfo.VirtualPrivateEndpointURL, err) 351 } 352 u := url.URL{ 353 Scheme: urlDefault.Scheme, 354 Host: urlVPE.Hostname() + ":" + urlDefault.Port(), 355 Path: urlDefault.Path, 356 } 357 return u.String(), nil 358 } 359 } 360 return originalAuthEndpoint, nil 361 }