github.com/oam-dev/cluster-gateway@v1.9.0/pkg/apis/cluster/v1alpha1/clustergateway_proxy.go (about) 1 /* 2 Licensed under the Apache License, Version 2.0 (the "License"); 3 you may not use this file except in compliance with the License. 4 You may obtain a copy of the License at 5 6 http://www.apache.org/licenses/LICENSE-2.0 7 8 Unless required by applicable law or agreed to in writing, software 9 distributed under the License is distributed on an "AS IS" BASIS, 10 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 See the License for the specific language governing permissions and 12 limitations under the License. 13 */ 14 15 package v1alpha1 16 17 import ( 18 "context" 19 "fmt" 20 "net" 21 "net/http" 22 "net/url" 23 "os" 24 gopath "path" 25 "regexp" 26 "strings" 27 "time" 28 29 "k8s.io/apiserver/pkg/server" 30 utilfeature "k8s.io/apiserver/pkg/util/feature" 31 "k8s.io/klog/v2" 32 "k8s.io/utils/strings/slices" 33 34 "github.com/oam-dev/cluster-gateway/pkg/config" 35 "github.com/oam-dev/cluster-gateway/pkg/featuregates" 36 "github.com/oam-dev/cluster-gateway/pkg/metrics" 37 38 "github.com/pkg/errors" 39 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 40 "k8s.io/apimachinery/pkg/runtime" 41 utilnet "k8s.io/apimachinery/pkg/util/net" 42 apiproxy "k8s.io/apimachinery/pkg/util/proxy" 43 "k8s.io/apimachinery/pkg/util/sets" 44 "k8s.io/apiserver/pkg/authorization/authorizer" 45 "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" 46 "k8s.io/apiserver/pkg/endpoints/request" 47 registryrest "k8s.io/apiserver/pkg/registry/rest" 48 restclient "k8s.io/client-go/rest" 49 "k8s.io/client-go/transport" 50 "sigs.k8s.io/apiserver-runtime/pkg/builder/resource" 51 "sigs.k8s.io/apiserver-runtime/pkg/builder/resource/resourcerest" 52 contextutil "sigs.k8s.io/apiserver-runtime/pkg/util/context" 53 "sigs.k8s.io/apiserver-runtime/pkg/util/loopback" 54 ) 55 56 var _ resource.SubResource = &ClusterGatewayProxy{} 57 var _ registryrest.Storage = &ClusterGatewayProxy{} 58 var _ resourcerest.Connecter = &ClusterGatewayProxy{} 59 60 var proxyMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"} 61 62 // ClusterGatewayProxy is a subresource for ClusterGateway which allows user to proxy 63 // kubernetes resource requests to the managed cluster. 64 type ClusterGatewayProxy struct { 65 } 66 67 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 68 type ClusterGatewayProxyOptions struct { 69 metav1.TypeMeta 70 71 // Path is the target api path of the proxy request. 72 // e.g. "/healthz", "/api/v1" 73 Path string `json:"path"` 74 75 // Impersonate indicates whether to impersonate as the original 76 // user identity from the request context after proxying to the 77 // target cluster. 78 // Note that this will requires additional RBAC settings inside 79 // the target cluster for the impersonated users (i.e. the end- 80 // user using the proxy subresource.). 81 Impersonate bool `json:"impersonate"` 82 } 83 84 func (c *ClusterGatewayProxy) SubResourceName() string { 85 return "proxy" 86 } 87 88 func (c *ClusterGatewayProxy) New() runtime.Object { 89 return &ClusterGatewayProxyOptions{} 90 } 91 92 func (in *ClusterGatewayProxy) Destroy() {} 93 94 func (c *ClusterGatewayProxy) Connect(ctx context.Context, id string, options runtime.Object, r registryrest.Responder) (http.Handler, error) { 95 ts := time.Now() 96 97 proxyOpts, ok := options.(*ClusterGatewayProxyOptions) 98 if !ok { 99 return nil, fmt.Errorf("invalid options object: %#v", options) 100 } 101 102 parentStorage, ok := contextutil.GetParentStorageGetter(ctx) 103 if !ok { 104 return nil, fmt.Errorf("no parent storage found") 105 } 106 parentObj, err := parentStorage.Get(ctx, id, &metav1.GetOptions{}) 107 if err != nil { 108 return nil, fmt.Errorf("no such cluster %v", id) 109 } 110 clusterGateway := parentObj.(*ClusterGateway) 111 112 reqInfo, _ := request.RequestInfoFrom(ctx) 113 factory := request.RequestInfoFactory{ 114 APIPrefixes: sets.NewString("api", "apis"), 115 GrouplessAPIPrefixes: sets.NewString("api"), 116 } 117 proxyReqInfo, _ := factory.NewRequestInfo(&http.Request{ 118 URL: &url.URL{ 119 Path: proxyOpts.Path, 120 }, 121 Method: strings.ToUpper(reqInfo.Verb), 122 }) 123 proxyReqInfo.Verb = reqInfo.Verb 124 125 if config.AuthorizateProxySubpath { 126 user, _ := request.UserFrom(ctx) 127 var attr authorizer.Attributes 128 if proxyReqInfo.IsResourceRequest { 129 attr = authorizer.AttributesRecord{ 130 User: user, 131 APIGroup: proxyReqInfo.APIGroup, 132 APIVersion: proxyReqInfo.APIVersion, 133 Resource: proxyReqInfo.Resource, 134 Subresource: proxyReqInfo.Subresource, 135 Namespace: proxyReqInfo.Namespace, 136 Name: proxyReqInfo.Name, 137 Verb: proxyReqInfo.Verb, 138 } 139 } else { 140 path, _ := url.ParseRequestURI(proxyReqInfo.Path) 141 attr = authorizer.AttributesRecord{ 142 User: user, 143 Path: path.Path, 144 Verb: proxyReqInfo.Verb, 145 } 146 } 147 148 decision, reason, err := loopback.GetAuthorizer().Authorize(ctx, attr) 149 if err != nil { 150 return nil, errors.Wrapf(err, "authorization failed due to %s", reason) 151 } 152 if decision != authorizer.DecisionAllow { 153 return nil, fmt.Errorf("proxying by user %v is forbidden authorization failed", user.GetName()) 154 } 155 } 156 157 return &proxyHandler{ 158 parentName: id, 159 path: proxyOpts.Path, 160 impersonate: proxyOpts.Impersonate, 161 clusterGateway: clusterGateway, 162 responder: r, 163 finishFunc: func(code int) { 164 metrics.RecordProxiedRequestsByResource(proxyReqInfo.Resource, proxyReqInfo.Verb, code) 165 metrics.RecordProxiedRequestsByCluster(id, code) 166 metrics.RecordProxiedRequestsDuration(proxyReqInfo.Resource, proxyReqInfo.Verb, id, code, time.Since(ts)) 167 }, 168 }, nil 169 } 170 171 func (c *ClusterGatewayProxy) NewConnectOptions() (runtime.Object, bool, string) { 172 return &ClusterGatewayProxyOptions{}, true, "path" 173 } 174 175 func (c *ClusterGatewayProxy) ConnectMethods() []string { 176 return proxyMethods 177 } 178 179 var _ resource.QueryParameterObject = &ClusterGatewayProxyOptions{} 180 181 func (in *ClusterGatewayProxyOptions) ConvertFromUrlValues(values *url.Values) error { 182 in.Path = values.Get("path") 183 in.Impersonate = values.Get("impersonate") == "true" 184 return nil 185 } 186 187 var _ http.Handler = &proxyHandler{} 188 189 type proxyHandler struct { 190 parentName string 191 path string 192 impersonate bool 193 clusterGateway *ClusterGateway 194 responder registryrest.Responder 195 finishFunc func(code int) 196 } 197 198 var ( 199 apiPrefix = "/apis/" + config.MetaApiGroupName + "/" + config.MetaApiVersionName + "/clustergateways/" 200 apiSuffix = "/proxy" 201 ) 202 203 type proxyResponseWriter struct { 204 http.ResponseWriter 205 http.Hijacker 206 http.Flusher 207 statusCode int 208 } 209 210 func (in *proxyResponseWriter) WriteHeader(statusCode int) { 211 in.statusCode = statusCode 212 in.ResponseWriter.WriteHeader(statusCode) 213 } 214 215 func newProxyResponseWriter(_writer http.ResponseWriter) *proxyResponseWriter { 216 writer := &proxyResponseWriter{ResponseWriter: _writer, statusCode: http.StatusOK} 217 writer.Hijacker, _ = _writer.(http.Hijacker) 218 writer.Flusher, _ = _writer.(http.Flusher) 219 return writer 220 } 221 222 func (p *proxyHandler) ServeHTTP(_writer http.ResponseWriter, request *http.Request) { 223 writer := newProxyResponseWriter(_writer) 224 defer func() { 225 p.finishFunc(writer.statusCode) 226 }() 227 cluster := p.clusterGateway 228 if cluster.Spec.Access.Credential == nil { 229 responsewriters.InternalError(writer, request, fmt.Errorf("proxying cluster %s not support due to lacking credentials", cluster.Name)) 230 return 231 } 232 233 // Go 1.19 removes the URL clone in WithContext method and therefore change 234 // to deep copy here 235 newReq := request.Clone(request.Context()) 236 newReq.Header = utilnet.CloneHeader(request.Header) 237 newReq.URL.Path = p.path 238 239 urlAddr, err := GetEndpointURL(cluster) 240 if err != nil { 241 responsewriters.InternalError(writer, request, errors.Wrapf(err, "failed parsing endpoint for cluster %s", cluster.Name)) 242 return 243 } 244 host, _, _ := net.SplitHostPort(urlAddr.Host) 245 path := strings.TrimPrefix(request.URL.Path, apiPrefix+p.parentName+apiSuffix) 246 newReq.Host = host 247 newReq.URL.Path = gopath.Join(urlAddr.Path, path) 248 newReq.URL.RawQuery = unescapeQueryValues(request.URL.Query()).Encode() 249 newReq.RequestURI = newReq.URL.RequestURI() 250 251 cfg, err := NewConfigFromCluster(request.Context(), cluster) 252 if err != nil { 253 responsewriters.InternalError(writer, request, errors.Wrapf(err, "failed creating cluster proxy client config %s", cluster.Name)) 254 return 255 } 256 if p.impersonate || utilfeature.DefaultFeatureGate.Enabled(featuregates.ClientIdentityPenetration) { 257 cfg.Impersonate = p.getImpersonationConfig(request) 258 } 259 rt, err := restclient.TransportFor(cfg) 260 if err != nil { 261 responsewriters.InternalError(writer, request, errors.Wrapf(err, "failed creating cluster proxy client %s", cluster.Name)) 262 return 263 } 264 proxy := apiproxy.NewUpgradeAwareHandler( 265 &url.URL{ 266 Scheme: urlAddr.Scheme, 267 Path: newReq.URL.Path, 268 Host: urlAddr.Host, 269 RawQuery: request.URL.RawQuery, 270 }, 271 rt, 272 false, 273 false, 274 nil) 275 276 const defaultFlushInterval = 200 * time.Millisecond 277 transportCfg, err := cfg.TransportConfig() 278 if err != nil { 279 responsewriters.InternalError(writer, request, errors.Wrapf(err, "failed creating transport config %s", cluster.Name)) 280 return 281 } 282 tlsConfig, err := transport.TLSConfigFor(transportCfg) 283 if err != nil { 284 responsewriters.InternalError(writer, request, errors.Wrapf(err, "failed creating tls config %s", cluster.Name)) 285 return 286 } 287 upgrader, err := transport.HTTPWrappersForConfig(transportCfg, apiproxy.MirrorRequest) 288 if err != nil { 289 responsewriters.InternalError(writer, request, errors.Wrapf(err, "failed creating upgrader client %s", cluster.Name)) 290 return 291 } 292 upgrading := utilnet.SetOldTransportDefaults(&http.Transport{ 293 TLSClientConfig: tlsConfig, 294 DialContext: cfg.Dial, 295 }) 296 proxy.UpgradeTransport = apiproxy.NewUpgradeRequestRoundTripper( 297 upgrading, 298 RoundTripperFunc(func(req *http.Request) (*http.Response, error) { 299 newReq := utilnet.CloneRequest(req) 300 return upgrader.RoundTrip(newReq) 301 })) 302 proxy.Transport = rt 303 proxy.FlushInterval = defaultFlushInterval 304 proxy.Responder = ErrorResponderFunc(func(w http.ResponseWriter, req *http.Request, err error) { 305 p.responder.Error(err) 306 }) 307 proxy.ServeHTTP(writer, newReq) 308 } 309 310 type noSuppressPanicError struct{} 311 312 func (noSuppressPanicError) Write(p []byte) (n int, err error) { 313 // skip "suppressing panic for copyResponse error in test; copy error" error message 314 // that ends up in CI tests on each kube-apiserver termination as noise and 315 // everybody thinks this is fatal. 316 if strings.Contains(string(p), "suppressing panic") { 317 return len(p), nil 318 } 319 return os.Stderr.Write(p) 320 } 321 322 // +k8s:deepcopy-gen=false 323 type RoundTripperFunc func(req *http.Request) (*http.Response, error) 324 325 func (fn RoundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { 326 return fn(req) 327 } 328 329 var _ apiproxy.ErrorResponder = ErrorResponderFunc(nil) 330 331 // +k8s:deepcopy-gen=false 332 type ErrorResponderFunc func(w http.ResponseWriter, req *http.Request, err error) 333 334 func (e ErrorResponderFunc) Error(w http.ResponseWriter, req *http.Request, err error) { 335 e(w, req, err) 336 } 337 338 func (p *proxyHandler) getImpersonationConfig(req *http.Request) restclient.ImpersonationConfig { 339 user, _ := request.UserFrom(req.Context()) 340 if p.clusterGateway.Spec.ProxyConfig != nil { 341 matched, ruleName, projected, err := ExchangeIdentity(&p.clusterGateway.Spec.ProxyConfig.Spec.ClientIdentityExchanger, user, p.parentName) 342 if err != nil { 343 klog.Errorf("exchange identity with cluster config error: %w", err) 344 } 345 if matched { 346 klog.Infof("identity exchanged with rule `%s` in the proxy config from cluster `%s`", ruleName, p.clusterGateway.Name) 347 return *projected 348 } 349 } 350 matched, ruleName, projected, err := ExchangeIdentity(&GlobalClusterGatewayProxyConfiguration.Spec.ClientIdentityExchanger, user, p.parentName) 351 if err != nil { 352 klog.Errorf("exchange identity with global config error: %w", err) 353 } 354 if matched { 355 klog.Infof("identity exchanged with rule `%s` in the proxy config from global config", ruleName) 356 return *projected 357 } 358 return restclient.ImpersonationConfig{ 359 UserName: user.GetName(), 360 Groups: user.GetGroups(), 361 Extra: user.GetExtra(), 362 } 363 } 364 365 // NewClusterGatewayProxyRequestEscaper wrap the base http.Handler and escape 366 // the dryRun parameter. Otherwise, the dryRun request will be blocked by 367 // apiserver middlewares 368 func NewClusterGatewayProxyRequestEscaper(delegate http.Handler) http.Handler { 369 return &clusterGatewayProxyRequestEscaper{delegate: delegate} 370 } 371 372 type clusterGatewayProxyRequestEscaper struct { 373 delegate http.Handler 374 } 375 376 var ( 377 clusterGatewayProxyPathPattern = regexp.MustCompile(strings.Join([]string{ 378 server.APIGroupPrefix, 379 config.MetaApiGroupName, 380 config.MetaApiVersionName, 381 "clustergateways", 382 "[a-z0-9]([-a-z0-9]*[a-z0-9])?", 383 "proxy"}, "/")) 384 clusterGatewayProxyQueryKeysToEscape = []string{"dryRun"} 385 clusterGatewayProxyEscaperPrefix = "__" 386 ) 387 388 func (in *clusterGatewayProxyRequestEscaper) ServeHTTP(w http.ResponseWriter, req *http.Request) { 389 if clusterGatewayProxyPathPattern.MatchString(req.URL.Path) { 390 newReq := req.Clone(req.Context()) 391 q := req.URL.Query() 392 for _, k := range clusterGatewayProxyQueryKeysToEscape { 393 if q.Has(k) { 394 q.Set(clusterGatewayProxyEscaperPrefix+k, q.Get(k)) 395 q.Del(k) 396 } 397 } 398 newReq.URL.RawQuery = q.Encode() 399 req = newReq 400 } 401 in.delegate.ServeHTTP(w, req) 402 } 403 404 func unescapeQueryValues(values url.Values) url.Values { 405 unescaped := url.Values{} 406 for k, vs := range values { 407 if strings.HasPrefix(k, clusterGatewayProxyEscaperPrefix) && 408 slices.Contains(clusterGatewayProxyQueryKeysToEscape, 409 strings.TrimPrefix(k, clusterGatewayProxyEscaperPrefix)) { 410 k = strings.TrimPrefix(k, clusterGatewayProxyEscaperPrefix) 411 } 412 unescaped[k] = vs 413 } 414 return unescaped 415 }