Skip to content

Commit

Permalink
Support K8S labelSelector filters for API requests (#2048)
Browse files Browse the repository at this point in the history
* Support filtering in k8s API with labelSelector (#2045)

Signed-off-by: Alexey Zarakovskiy <[email protected]>

* Drop K8S acronyms

Signed-off-by: Alexey Zarakovskiy <[email protected]>

* Drop useless comments

Signed-off-by: Alexey Zarakovskiy <[email protected]>

* Fix eskip formatting

Signed-off-by: Alexey Zarakovskiy <[email protected]>

* Do not default all labels to ingress label

Signed-off-by: Alexey Zarakovskiy <[email protected]>

* Test multiple selectors

Signed-off-by: Alexey Zarakovskiy <[email protected]>

* Fix lint

Signed-off-by: Alexey Zarakovskiy <[email protected]>

Signed-off-by: Alexey Zarakovskiy <[email protected]>
  • Loading branch information
azarakovskiy authored Sep 8, 2022
1 parent 6a0a590 commit 8b7cd03
Show file tree
Hide file tree
Showing 20 changed files with 911 additions and 323 deletions.
74 changes: 57 additions & 17 deletions dataclients/kubernetes/clusterclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import (
"io"
"net"
"net/http"
"net/url"
"os"
"regexp"
"sort"
"strings"
"time"

log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -43,6 +45,8 @@ const (
serviceAccountDir = "/var/run/secrets/kubernetes.io/serviceaccount/"
serviceAccountTokenKey = "token"
serviceAccountRootCAKey = "ca.crt"
labelSelectorFmt = "%s=%s"
labelSelectorQueryFmt = "?labelSelector=%s"
)

const RouteGroupsNotInstalledMessage = `RouteGroups CRD is not installed in the cluster.
Expand All @@ -63,6 +67,12 @@ type clusterClient struct {
httpClient *http.Client
ingressV1 bool

ingressLabelSelectors string
servicesLabelSelectors string
endpointsLabelSelectors string
secretsLabelSelectors string
routeGroupsLabelSelectors string

loggedMissingRouteGroups bool
}

Expand Down Expand Up @@ -142,17 +152,22 @@ func newClusterClient(o Options, apiURL, ingCls, rgCls string, quit <-chan struc
ingressURI = IngressesV1ClusterURI
}
c := &clusterClient{
ingressV1: o.KubernetesIngressV1,
ingressesURI: ingressURI,
routeGroupsURI: routeGroupsClusterURI,
servicesURI: ServicesClusterURI,
endpointsURI: EndpointsClusterURI,
secretsURI: SecretsClusterURI,
ingressClass: ingClsRx,
routeGroupClass: rgClsRx,
httpClient: httpClient,
apiURL: apiURL,
certificateRegistry: o.CertificateRegistry,
ingressV1: o.KubernetesIngressV1,
ingressesURI: ingressURI,
routeGroupsURI: routeGroupsClusterURI,
servicesURI: ServicesClusterURI,
endpointsURI: EndpointsClusterURI,
secretsURI: SecretsClusterURI,
ingressClass: ingClsRx,
ingressLabelSelectors: toLabelSelectorQuery(o.IngressLabelSelectors),
servicesLabelSelectors: toLabelSelectorQuery(o.ServicesLabelSelectors),
endpointsLabelSelectors: toLabelSelectorQuery(o.EndpointsLabelSelectors),
secretsLabelSelectors: toLabelSelectorQuery(o.SecretsLabelSelectors),
routeGroupsLabelSelectors: toLabelSelectorQuery(o.RouteGroupsLabelSelectors),
routeGroupClass: rgClsRx,
httpClient: httpClient,
apiURL: apiURL,
certificateRegistry: o.CertificateRegistry,
}

if o.KubernetesInCluster {
Expand All @@ -177,6 +192,29 @@ func newClusterClient(o Options, apiURL, ingCls, rgCls string, quit <-chan struc
return c, nil
}

// serializes a given map of label selectors to a string that can be appended to a request URI to kubernetes
// Examples (note that the resulting value in the query is URL escaped, for readability this is not done in examples):
// [] becomes ``
// ["label": ""] becomes `?labelSelector=label`
// ["label": "value"] becomes `?labelSelector=label=value`
// ["label": "value", "label2": "value2"] becomes `?labelSelector=label=value&label2=value2`
func toLabelSelectorQuery(selectors map[string]string) string {
if len(selectors) == 0 {
return ""
}

var strs []string
for k, v := range selectors {
if v == "" {
strs = append(strs, k)
} else {
strs = append(strs, fmt.Sprintf(labelSelectorFmt, k, v))
}
}

return fmt.Sprintf(labelSelectorQueryFmt, url.QueryEscape(strings.Join(strs, ",")))
}

func (c *clusterClient) setNamespace(namespace string) {
if c.ingressV1 {
c.ingressesURI = fmt.Sprintf(IngressesV1NamespaceFmt, namespace)
Expand Down Expand Up @@ -326,7 +364,7 @@ func sortByMetadata(slice interface{}, getMetadata func(int) *definitions.Metada

func (c *clusterClient) loadIngresses() ([]*definitions.IngressItem, error) {
var il definitions.IngressList
if err := c.getJSON(c.ingressesURI, &il); err != nil {
if err := c.getJSON(c.ingressesURI+c.ingressLabelSelectors, &il); err != nil {
log.Debugf("requesting all ingresses failed: %v", err)
return nil, err
}
Expand All @@ -340,7 +378,7 @@ func (c *clusterClient) loadIngresses() ([]*definitions.IngressItem, error) {

func (c *clusterClient) loadIngressesV1() ([]*definitions.IngressV1Item, error) {
var il definitions.IngressV1List
if err := c.getJSON(c.ingressesURI, &il); err != nil {
if err := c.getJSON(c.ingressesURI+c.ingressLabelSelectors, &il); err != nil {
log.Debugf("requesting all ingresses failed: %v", err)
return nil, err
}
Expand All @@ -354,7 +392,7 @@ func (c *clusterClient) loadIngressesV1() ([]*definitions.IngressV1Item, error)

func (c *clusterClient) LoadRouteGroups() ([]*definitions.RouteGroupItem, error) {
var rgl definitions.RouteGroupList
if err := c.getJSON(c.routeGroupsURI, &rgl); err != nil {
if err := c.getJSON(c.routeGroupsURI+c.routeGroupsLabelSelectors, &rgl); err != nil {
return nil, err
}

Expand Down Expand Up @@ -384,7 +422,8 @@ func (c *clusterClient) LoadRouteGroups() ([]*definitions.RouteGroupItem, error)

func (c *clusterClient) loadServices() (map[definitions.ResourceID]*service, error) {
var services serviceList
if err := c.getJSON(c.servicesURI, &services); err != nil {

if err := c.getJSON(c.servicesURI+c.servicesLabelSelectors, &services); err != nil {
log.Debugf("requesting all services failed: %v", err)
return nil, err
}
Expand All @@ -410,7 +449,8 @@ func (c *clusterClient) loadServices() (map[definitions.ResourceID]*service, err

func (c *clusterClient) loadSecrets() (map[definitions.ResourceID]*secret, error) {
var secrets secretList
if err := c.getJSON(c.secretsURI, &secrets); err != nil {

if err := c.getJSON(c.secretsURI+c.secretsLabelSelectors, &secrets); err != nil {
log.Debugf("requesting all secrets failed: %v", err)
return nil, err
}
Expand All @@ -430,7 +470,7 @@ func (c *clusterClient) loadSecrets() (map[definitions.ResourceID]*secret, error

func (c *clusterClient) loadEndpoints() (map[definitions.ResourceID]*endpoint, error) {
var endpoints endpointList
if err := c.getJSON(c.endpointsURI, &endpoints); err != nil {
if err := c.getJSON(c.endpointsURI+c.endpointsLabelSelectors, &endpoints); err != nil {
log.Debugf("requesting all endpoints failed: %v", err)
return nil, err
}
Expand Down
1 change: 1 addition & 0 deletions dataclients/kubernetes/definitions/definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Metadata struct {
Created time.Time `json:"creationTimestamp"`
Uid string `json:"uid"`
Annotations map[string]string `json:"annotations"`
Labels map[string]string `json:"labels"`
}

func (meta *Metadata) ToResourceID() ResourceID {
Expand Down
31 changes: 31 additions & 0 deletions dataclients/kubernetes/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,37 @@ type Options struct {
// skipper will load it. The default value for the RouteGroup class is 'skipper'.
RouteGroupClass string

// IngressLabelSelectors is a map of kubernetes labels to their values that must be present on a resource to be loaded
// by the client. A label and its value on an Ingress must be match exactly to be loaded by Skipper.
// If the value is irrelevant for a given configuration, it can be left empty. The default
// value is no labels required.
// Examples:
// Config [] will load all Ingresses.
// Config ["skipper-enabled": ""] will load only Ingresses with a label "skipper-enabled", no matter the value.
// Config ["skipper-enabled": "true"] will load only Ingresses with a label "skipper-enabled: true"
// Config ["skipper-enabled": "", "foo": "bar"] will load only Ingresses with both labels while label "foo" must have a value "bar".
IngressLabelSelectors map[string]string

// ServicesLabelSelectors is a map of kubernetes labels to their values that must be present on a resource to be loaded
// by the client. Read documentation for IngressLabelSelectors for examples and more details.
// The default value is no labels required.
ServicesLabelSelectors map[string]string

// EndpointsLabelSelectors is a map of kubernetes labels to their values that must be present on a resource to be loaded
// by the client. Read documentation for IngressLabelSelectors for examples and more details.
// The default value is no labels required.
EndpointsLabelSelectors map[string]string

// SecretsLabelSelectors is a map of kubernetes labels to their values that must be present on a resource to be loaded
// by the client. Read documentation for IngressLabelSelectors for examples and more details.
// The default value is no labels required.
SecretsLabelSelectors map[string]string

// RouteGroupsLabelSelectors is a map of kubernetes labels to their values that must be present on a resource to be loaded
// by the client. Read documentation for IngressLabelSelectors for examples and more details.
// The default value is no labels required.
RouteGroupsLabelSelectors map[string]string

// ReverseSourcePredicate set to true will do the Source IP
// whitelisting for the heartbeat endpoint correctly in AWS.
// Amazon's ALB writes the client IP to the last item of the
Expand Down
Loading

0 comments on commit 8b7cd03

Please sign in to comment.