Using K8s Label Selectors in Go without doing it wrong
Labels are how we group objects together in Kubernetes. Label selectors are how we use those labels to retrieve our grouped objects. There are a bazillion articles out there on the subject, so if you’re not familiar, start there.
Instead, our focus here is: how do I use Kubernetes libraries to work with labels via label selectors?
If you’re just looking for quick help, the Cheatsheet section will give you top-level summary info. Otherwise, read on.
Important: all Go Playground examples here might need to be run a few times for the dependencies to load in. Go Playground has an unforgiving timeout.
Cheatsheet
There are two Go types for working with label selectors: the labels.Selector interface, and the LabelSelector struct. They are different.
labels.Selector
is used for filtering client reads. It’s like theclient-go
equivalent ofkubectl label ... -l service=backend
. Uselabels.NewSelector
+labels.NewRequirement
to construct something theclient-go
can query with.- The
LabelSelector
struct is used as part of individual Resources as the API type. You’d write this struct in your object’s YAML before applying. So you might get this from a read (as part of a returned object, e.g.Deployment
), but don’t use this struct for client reads. - You can convert from a
LabelSelector
(what you get from a read) to alabel.Selector
(what you use to read) withLabelSelectorAsSelector
. But if you’re doing this, you’re either doing something Controller-esque (if so, fine) OR you should just be usinglabels.Selector
directly.
Using labels.Selector
The labels.Selector
interface is located in the apimachinery repo under pkg/labels/selector.go
. It’s used to read and query Kubernetes Objects via their labels.
You should use it because:
- It provides standard validation for requirements, keys, and values.
- It’s what Kubernetes libraries uses, so it’s battle-tested to the extreme.
Initialize a new selector with labels.NewSelector()
, and then add labels.Requirements
as desired. Think of each Requirement
as akin to the normal kubectl label
operations you might use for equality-based and set-based label selection (Go Playground):
apimachinery
gives you a few additional perks out of the box:
- Requirement validation (e.g.
Equals
shouldn’t have two values). - Key validation (e.g. can’t lead with characters like
-
or_
) - Value validation (e.g. label values can’t exceed 63 characters)
(Note: these key / value validations are widely used and worth perusing in full.)
apimachinery
also provides helpful additional functionality (Go Playground for both):
labels.Parse
allows you to use the samekubectl label
syntax you might already be familiar with.
labels.ValidatedSelectorFromSet
is a validated convenience for the simplest label selection use case: give me all objects that match this label set exactly, AND’d together. Pass aSet
object (under the hood, justmap[string]string
like labels themselves), it validates client-side (early errors, no extra load on the API Server), and gives you a Selector you can use to query:
What about LabelSelector?
LabelSelector
is an API type. That means it’s used as part of various Kubernetes Resource Specs. It’s not for client usage or library code, it’s part of the API, just like Pods
and ConfigMaps
.
This fact is reflected by where it lives: in apimachinery
, but under pkg/apis/v1/meta/types.go.
Indeed, comments in that file indicate:
The package contains two categories of types:
— external (serialized) types that lack their own version (e.g TypeMeta)
— internal (never-serialized) types that are needed by several different api groups, and so live here, to avoid duplication and/or import loops (e.g. LabelSelector).
So when you create an Object, you’re probably going to write out a LabelSelector
struct in YAML, and when you retrieve Objects from the API Server using labels.Selector
as above, your returned Objects might contain the LabelSelector
struct as a field. Modified example from the docs:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
<snip>
That spec.selector
is a LabelSelector
struct object behind the scenes. The Deployment
controller will use it (converting it to labels.Selector
, more below) to query the Deployment’s Pods
.
If this all sounds very obvious to you, good. The key point to understand here is the difference between normal library code and API types. labels.Selector
is library code used to read and filter Kubernetes Objects. LabelSelector
is part of the API type you get back from those reads.
Converting between the two
Say you use a labels.Selector
to query Deployments
. You get back matching Deployment
Objects, all of which have their own LabelSelector
data at spec.selector
. Maybe you want to now use those LabelSelector
objects to query the Pods
programmatically. So we need to convert from LabelSelector
to labels.Selector
(API type -> library type for querying).
LabelsSelectorAsSelector
is your ticket here (r.i.p. Go Playground, not a chance on these dependencies):
Why do we need to do this? Well, controllers do it all the time! In order for the Deployment
controller to get all Pods
it owns, it will:
- Get the
LabelSelector
API type from aDeployment
atspec.selector
. - Convert it to a
labels.Selector
usingLabelsSelectorAsSelector
. - Query
Pods
using the newly-convertedlabels.Selector
.
See for yourself in the Deployment Controller, reproduced and shortened here:
This also serves as a great example of everything we’ve discussed here, including actually passing a labels.Selector
to a Pod
client and querying away!
Conclusions
labels.Selector
is library code for filtering reads. LabelSelector
is an API type used in other Objects like Deployments
and StatefulSets
. If you’re writing Go code, you probably want to work with labels.Selector
, unless you’re writing controller code that needs to work with some Resource’s selector field!