|
| 1 | +package webhook |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "net" |
| 6 | + "net/url" |
| 7 | + |
| 8 | + v1 "k8s.io/api/core/v1" |
| 9 | + "k8s.io/apimachinery/pkg/api/errors" |
| 10 | + apierrors "k8s.io/apimachinery/pkg/api/errors" |
| 11 | + webhookutil "k8s.io/apiserver/pkg/util/webhook" |
| 12 | + corev1 "k8s.io/client-go/listers/core/v1" |
| 13 | +) |
| 14 | + |
| 15 | +// ServiceResolver knows how to convert a service reference into an actual location. |
| 16 | +type ServiceResolver interface { |
| 17 | + ResolveEndpoint(namespace, name string, port int32) (*url.URL, error) |
| 18 | +} |
| 19 | + |
| 20 | +// NewServiceResolver returns a ServiceResolver that parses service first, |
| 21 | +// if service not exist, constructs a service URL from a given namespace and name. |
| 22 | +func NewServiceResolver(services corev1.ServiceLister) ServiceResolver { |
| 23 | + return &serviceResolver{ |
| 24 | + services: services, |
| 25 | + defaultResolver: webhookutil.NewDefaultServiceResolver(), |
| 26 | + } |
| 27 | +} |
| 28 | + |
| 29 | +type serviceResolver struct { |
| 30 | + services corev1.ServiceLister |
| 31 | + defaultResolver ServiceResolver |
| 32 | +} |
| 33 | + |
| 34 | +func (r *serviceResolver) ResolveEndpoint(namespace, name string, port int32) (*url.URL, error) { |
| 35 | + res, err := resolveCluster(r.services, namespace, name, port) |
| 36 | + if err != nil && apierrors.IsNotFound(err) { |
| 37 | + return r.defaultResolver.ResolveEndpoint(namespace, name, port) |
| 38 | + } |
| 39 | + return res, err |
| 40 | +} |
| 41 | + |
| 42 | +// resolveCluster parses Service resource to url. |
| 43 | +// It is lifted from https://github.com/kubernetes/apiserver/blob/release-1.31/pkg/util/proxy/proxy.go#L105. |
| 44 | +func resolveCluster(services corev1.ServiceLister, namespace, id string, port int32) (*url.URL, error) { |
| 45 | + svc, err := services.Services(namespace).Get(id) |
| 46 | + if err != nil { |
| 47 | + return nil, err |
| 48 | + } |
| 49 | + |
| 50 | + switch { |
| 51 | + case svc.Spec.Type == v1.ServiceTypeClusterIP && svc.Spec.ClusterIP == v1.ClusterIPNone: |
| 52 | + return nil, fmt.Errorf(`cannot route to service with ClusterIP "None"`) |
| 53 | + // use IP from a clusterIP for these service types |
| 54 | + case svc.Spec.Type == v1.ServiceTypeClusterIP, svc.Spec.Type == v1.ServiceTypeLoadBalancer, svc.Spec.Type == v1.ServiceTypeNodePort: |
| 55 | + svcPort, err := findServicePort(svc, port) |
| 56 | + if err != nil { |
| 57 | + return nil, err |
| 58 | + } |
| 59 | + return &url.URL{ |
| 60 | + Scheme: "https", |
| 61 | + Host: net.JoinHostPort(svc.Spec.ClusterIP, fmt.Sprintf("%d", svcPort.Port)), |
| 62 | + }, nil |
| 63 | + case svc.Spec.Type == v1.ServiceTypeExternalName: |
| 64 | + return &url.URL{ |
| 65 | + Scheme: "https", |
| 66 | + Host: net.JoinHostPort(svc.Spec.ExternalName, fmt.Sprintf("%d", port)), |
| 67 | + }, nil |
| 68 | + default: |
| 69 | + return nil, fmt.Errorf("unsupported service type %q", svc.Spec.Type) |
| 70 | + } |
| 71 | + |
| 72 | +} |
| 73 | + |
| 74 | +// findServicePort finds the service port by name or numerically. |
| 75 | +func findServicePort(svc *v1.Service, port int32) (*v1.ServicePort, error) { |
| 76 | + for _, svcPort := range svc.Spec.Ports { |
| 77 | + if svcPort.Port == port { |
| 78 | + return &svcPort, nil |
| 79 | + } |
| 80 | + } |
| 81 | + return nil, errors.NewServiceUnavailable(fmt.Sprintf("no service port %d found for service %q", port, svc.Name)) |
| 82 | +} |
0 commit comments