-
Notifications
You must be signed in to change notification settings - Fork 804
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Adding taint during image pre-pulling to prevent user pod being scheduled #3011
Open
xcompass
wants to merge
10
commits into
jupyterhub:main
Choose a base branch
from
ubc:main-taintmanager
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+720
−3
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
98fe35a
Add taintmanager
xcompass 08c344f
Add taintmanager toleration to core pods
xcompass 674f5a7
Add taintmanager to schema
xcompass fcb060a
Remove GODEBUG from taintmanager env var
xcompass 3f65fac
Add taint manager
xcompass 50bafb9
Update taint manager image name
xcompass ad399b4
Reformat to make prettier happy
xcompass 7da1b85
Add cli parameter for kubeconfig and node name
xcompass c32c90d
Fix a typo
xcompass c98495a
Change taintmanager image location to make chartpress happy
xcompass File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.idea | ||
taintmanager |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# syntax=docker/dockerfile:1 | ||
|
||
## Build | ||
FROM golang:1.18-bullseye AS build | ||
|
||
WORKDIR /app | ||
|
||
COPY go.mod ./ | ||
COPY go.sum ./ | ||
RUN go mod download | ||
|
||
COPY *.go ./ | ||
|
||
RUN go build -o /taintmanager | ||
|
||
## Deploy | ||
FROM gcr.io/distroless/base-debian11 | ||
|
||
WORKDIR / | ||
|
||
COPY --from=build /taintmanager /taintmanager | ||
|
||
USER nonroot:nonroot | ||
|
||
CMD ["/taintmanager"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# In Cluster Taint Manager | ||
|
||
To add or remove taint within a pod. The default mode is to run as a pod in a Kubernetes cluster. It will try to | ||
authenticate to K8S API server with in-cluster config (the service account token mounted inside pod). It also uses | ||
downward API to retrieve the node name where the pod is running on in order to change the taint. | ||
|
||
## Compile | ||
|
||
``` | ||
GOOS=linux GOARCH=amd64 go build -o taintmanager taintmanager.go | ||
``` | ||
|
||
## Development and Debug | ||
|
||
To test outside a cluster, commandline parameter `-kubeconfig` and `-node` can be specified. If `-kubeconfig` is not | ||
specified, taintmanager will try to load the kubeconfig from default path `HOME/.kube/config` | ||
|
||
Using default kubeconfig: | ||
|
||
``` | ||
taintmanager -node f6.workers.ctlt.ubc.ca -remove hub.jupyter.org/imagepulling:NoExecute | ||
``` | ||
|
||
## Test in Cluster | ||
|
||
The `test` directory contains YAML files for deploy a pod with required permissions to run taintmanager. | ||
Please change `namespace` field in `clusterrolebinding.yaml` before deploying to a cluster. | ||
|
||
After deploying yaml files, run `kubectl cp` to copy compiled binary to the target pod and run the binary inside pod. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
module taintmanager | ||
|
||
go 1.18 | ||
|
||
require ( | ||
k8s.io/api v0.25.4 | ||
k8s.io/apimachinery v0.25.4 | ||
k8s.io/client-go v0.25.4 | ||
) | ||
|
||
require ( | ||
github.com/davecgh/go-spew v1.1.1 // indirect | ||
github.com/emicklei/go-restful/v3 v3.10.1 // indirect | ||
github.com/go-logr/logr v1.2.3 // indirect | ||
github.com/go-openapi/jsonpointer v0.19.5 // indirect | ||
github.com/go-openapi/jsonreference v0.20.0 // indirect | ||
github.com/go-openapi/swag v0.22.3 // indirect | ||
github.com/gogo/protobuf v1.3.2 // indirect | ||
github.com/golang/protobuf v1.5.2 // indirect | ||
github.com/google/gnostic v0.6.9 // indirect | ||
github.com/google/go-cmp v0.5.9 // indirect | ||
github.com/google/gofuzz v1.2.0 // indirect | ||
github.com/imdario/mergo v0.3.6 // indirect | ||
github.com/josharian/intern v1.0.0 // indirect | ||
github.com/json-iterator/go v1.1.12 // indirect | ||
github.com/mailru/easyjson v0.7.7 // indirect | ||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect | ||
github.com/modern-go/reflect2 v1.0.2 // indirect | ||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect | ||
github.com/spf13/pflag v1.0.5 // indirect | ||
golang.org/x/net v0.2.0 // indirect | ||
golang.org/x/oauth2 v0.2.0 // indirect | ||
golang.org/x/sys v0.2.0 // indirect | ||
golang.org/x/term v0.2.0 // indirect | ||
golang.org/x/text v0.4.0 // indirect | ||
golang.org/x/time v0.2.0 // indirect | ||
google.golang.org/appengine v1.6.7 // indirect | ||
google.golang.org/protobuf v1.28.1 // indirect | ||
gopkg.in/inf.v0 v0.9.1 // indirect | ||
gopkg.in/yaml.v2 v2.4.0 // indirect | ||
gopkg.in/yaml.v3 v3.0.1 // indirect | ||
k8s.io/klog/v2 v2.80.1 // indirect | ||
k8s.io/kube-openapi v0.0.0-20221123214604-86e75ddd809a // indirect | ||
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect | ||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect | ||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect | ||
sigs.k8s.io/yaml v1.3.0 // indirect | ||
) |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"flag" | ||
"fmt" | ||
v1 "k8s.io/api/core/v1" | ||
"k8s.io/apimachinery/pkg/api/errors" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/util/validation" | ||
"k8s.io/client-go/kubernetes" | ||
"k8s.io/client-go/rest" | ||
"k8s.io/client-go/tools/clientcmd" | ||
"k8s.io/client-go/util/homedir" | ||
"log" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
) | ||
|
||
func buildConfig(kubeconfig string) (*rest.Config, error) { | ||
// load kubeconfig from command line | ||
if kubeconfig != "" { | ||
cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfig) | ||
if err != nil { | ||
return nil, err | ||
} | ||
log.Printf("Using kubeconfig %v.", kubeconfig) | ||
return cfg, nil | ||
} | ||
|
||
// try the default location HOME/.kube/config | ||
if home := homedir.HomeDir(); home != "" { | ||
kubeconfig = filepath.Join(home, ".kube", "config") | ||
cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfig) | ||
if err == nil { | ||
log.Printf("Using kubeconfig %v.", kubeconfig) | ||
return cfg, nil | ||
} | ||
} | ||
|
||
// try in-cluster auth | ||
cfg, err := rest.InClusterConfig() | ||
if err != nil { | ||
return nil, err | ||
} | ||
log.Printf("Using in-cluster config.") | ||
return cfg, nil | ||
} | ||
|
||
func usage() { | ||
log.Println("Usage: taintmanager [-add TAINT] [-remove TAINT] [-node NODE_NAME] [-kubeconfig /PATH/TO/KUBECONFIG]") | ||
flag.PrintDefaults() | ||
os.Exit(1) | ||
} | ||
|
||
func main() { | ||
kubeconfig := flag.String("kubeconfig", "", "(optional) absolute path to the kubeconfig file") | ||
nodeName := flag.String("node", "", "(optional) The name of the node to add/remove taints. Can be passed through environment variable MY_NODE_NAME") | ||
taintAdd := flag.String("add", "", "The taint to add") | ||
taintRemove := flag.String("remove", "", "The taint to remove") | ||
flag.Parse() | ||
|
||
// load kubeconfig | ||
config, err := buildConfig(*kubeconfig) | ||
if err != nil { | ||
log.Printf(err.Error()) | ||
log.Printf("Failed to load kubeconfig") | ||
usage() | ||
} | ||
|
||
// parse taints | ||
tAdd, errAdd := parseTaint(*taintAdd) | ||
tRemove, errRemove := parseTaint(*taintRemove) | ||
if errAdd != nil && errRemove != nil { | ||
log.Println("Please specify at least one option -add or -remove") | ||
usage() | ||
} | ||
|
||
// check node name | ||
if *nodeName == "" { | ||
// try from env var | ||
*nodeName = os.Getenv("MY_NODE_NAME") | ||
} | ||
if *nodeName == "" { | ||
log.Println("Please specify the node name") | ||
usage() | ||
} | ||
log.Printf("Node name %v\n", *nodeName) | ||
|
||
// creates the clientset | ||
clientset, err := kubernetes.NewForConfig(config) | ||
if err != nil { | ||
panic(err.Error()) | ||
} | ||
node, err := clientset.CoreV1().Nodes().Get(context.TODO(), *nodeName, metav1.GetOptions{}) | ||
if errors.IsNotFound(err) { | ||
log.Printf("Node %v not found in default namespace\n", *nodeName) | ||
} else if statusError, isStatus := err.(*errors.StatusError); isStatus { | ||
log.Printf("Error getting node %v\n", statusError.ErrStatus.Message) | ||
} else if err != nil { | ||
panic(err.Error()) | ||
} else { | ||
log.Printf("Found node %v\n", node.GetName()) | ||
for k, v := range node.Spec.Taints { | ||
log.Printf("%v: %v\n", k, v) | ||
} | ||
if errAdd == nil { | ||
node.Spec.Taints = append(node.Spec.Taints, tAdd) | ||
clientset.CoreV1().Nodes().Update(context.TODO(), node, metav1.UpdateOptions{}) | ||
log.Printf("Taint %v is added to node %v.", tAdd.ToString(), *nodeName) | ||
} | ||
if errRemove == nil { | ||
for i, taint := range node.Spec.Taints { | ||
if taint.Key == tRemove.Key { | ||
node.Spec.Taints = append(node.Spec.Taints[:i], node.Spec.Taints[i+1:]...) | ||
log.Printf("Taint %v is removed from node %v.", tRemove.ToString(), *nodeName) | ||
clientset.CoreV1().Nodes().Update(context.TODO(), node, metav1.UpdateOptions{}) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
// copied from https://github.com/kubernetes/kubernetes/blob/v1.25.4/pkg/util/taints/taints.go | ||
// parseTaint parses a taint from a string, whose form must be either | ||
// '<key>=<value>:<effect>', '<key>:<effect>', or '<key>'. | ||
func parseTaint(st string) (v1.Taint, error) { | ||
var taint v1.Taint | ||
|
||
var key string | ||
var value string | ||
var effect v1.TaintEffect | ||
|
||
parts := strings.Split(st, ":") | ||
switch len(parts) { | ||
case 1: | ||
key = parts[0] | ||
case 2: | ||
effect = v1.TaintEffect(parts[1]) | ||
if err := validateTaintEffect(effect); err != nil { | ||
return taint, err | ||
} | ||
|
||
partsKV := strings.Split(parts[0], "=") | ||
if len(partsKV) > 2 { | ||
return taint, fmt.Errorf("invalid taint spec: %v", st) | ||
} | ||
key = partsKV[0] | ||
if len(partsKV) == 2 { | ||
value = partsKV[1] | ||
if errs := validation.IsValidLabelValue(value); len(errs) > 0 { | ||
return taint, fmt.Errorf("invalid taint spec: %v, %s", st, strings.Join(errs, "; ")) | ||
} | ||
} | ||
default: | ||
return taint, fmt.Errorf("invalid taint spec: %v", st) | ||
} | ||
|
||
if errs := validation.IsQualifiedName(key); len(errs) > 0 { | ||
return taint, fmt.Errorf("invalid taint spec: %v, %s", st, strings.Join(errs, "; ")) | ||
} | ||
|
||
taint.Key = key | ||
taint.Value = value | ||
taint.Effect = effect | ||
|
||
return taint, nil | ||
} | ||
|
||
func validateTaintEffect(effect v1.TaintEffect) error { | ||
if effect != v1.TaintEffectNoSchedule && effect != v1.TaintEffectPreferNoSchedule && effect != v1.TaintEffectNoExecute { | ||
return fmt.Errorf("invalid taint effect: %v, unsupported taint effect", effect) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
apiVersion: rbac.authorization.k8s.io/v1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given we can test this by running the taintmanager locally, are these test files still needed? |
||
kind: ClusterRole | ||
metadata: | ||
name: taintmanager | ||
rules: | ||
- apiGroups: [""] # "" indicates the core API group | ||
resources: ["nodes"] | ||
verbs: ["get", "update"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
apiVersion: rbac.authorization.k8s.io/v1 | ||
# This role binding allows "jane" to read pods in the "default" namespace. | ||
# You need to already have a Role named "pod-reader" in that namespace. | ||
kind: ClusterRoleBinding | ||
metadata: | ||
name: taintmanager | ||
subjects: | ||
# You can specify more than one "subject" | ||
- kind: ServiceAccount | ||
name: taintmanager | ||
namespace: default | ||
roleRef: | ||
# "roleRef" specifies the binding to a Role / ClusterRole | ||
kind: ClusterRole #this must be Role or ClusterRole | ||
name: taintmanager # this must match the name of the Role or ClusterRole you wish to bind to | ||
apiGroup: rbac.authorization.k8s.io |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
apiVersion: apps/v1 | ||
kind: Deployment | ||
metadata: | ||
name: taintmanager | ||
spec: | ||
replicas: 1 | ||
selector: | ||
matchLabels: | ||
app: taintmanager | ||
template: | ||
metadata: | ||
labels: | ||
app: taintmanager | ||
spec: | ||
serviceAccountName: taintmanager | ||
containers: | ||
- image: busybox | ||
imagePullPolicy: IfNotPresent | ||
name: taintmanager | ||
command: ["sleep", "100000"] | ||
env: | ||
- name: MY_POD_NAME | ||
valueFrom: | ||
fieldRef: | ||
fieldPath: metadata.name | ||
- name: MY_NODE_NAME | ||
valueFrom: | ||
fieldRef: | ||
fieldPath: spec.nodeName | ||
ports: | ||
- containerPort: 8080 | ||
tolerations: | ||
- key: "hub.jupyter.org/dedicated" | ||
operator: "Exists" | ||
effect: "NoSchedule" | ||
restartPolicy: Always |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
apiVersion: v1 | ||
kind: ServiceAccount | ||
metadata: | ||
name: taintmanager | ||
namespace: default |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Atleast to start with and to keep the amount of required go knowledge to a minimum, what do you think about doing this build the same way as https://github.com/jupyterhub/zero-to-jupyterhub-k8s/blob/main/images/image-awaiter/Dockerfile? I think primarily that means using scratch as the final target rather than distroless.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the reason I'm using distroless is to run taintmanager as nonroot user to be more secure. If it is not a concern, I can change it to scratch.