437 lines
12 KiB
Go
437 lines
12 KiB
Go
/*
|
|
Copyright 2019 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package controllers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/client-go/tools/record"
|
|
"k8s.io/utils/pointer"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/handler"
|
|
|
|
operatorv1 "k8s.io/kubeadm/operator/api/v1alpha1"
|
|
"k8s.io/kubeadm/operator/operations"
|
|
)
|
|
|
|
func getImage(c client.Client, namespace, name string) (string, error) {
|
|
pod := &corev1.Pod{}
|
|
key := client.ObjectKey{
|
|
Namespace: namespace,
|
|
Name: name,
|
|
}
|
|
err := c.Get(
|
|
context.Background(), key, pod,
|
|
)
|
|
if err != nil {
|
|
return "", errors.Errorf("error reading pod %s/%s", namespace, name)
|
|
}
|
|
|
|
var managerImage string
|
|
for _, c := range pod.Spec.Containers {
|
|
if c.Name == "manager" {
|
|
managerImage = c.Image
|
|
}
|
|
}
|
|
|
|
if managerImage == "" {
|
|
return "", errors.Errorf("unable to get Image for manager container in %s/%s", namespace, name)
|
|
}
|
|
|
|
return managerImage, nil
|
|
}
|
|
|
|
func getDaemonSet(c client.Client, operation *operatorv1.Operation, namespace string) (*appsv1.DaemonSet, error) {
|
|
daemonSet := &appsv1.DaemonSet{}
|
|
key := client.ObjectKey{
|
|
Namespace: namespace,
|
|
Name: daemonSetName(operation.Name),
|
|
}
|
|
err := c.Get(
|
|
context.Background(), key, daemonSet,
|
|
)
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return daemonSet, nil
|
|
}
|
|
|
|
func daemonSetName(operationName string) string {
|
|
return fmt.Sprintf("controller-agent-%s", operationName)
|
|
}
|
|
|
|
func hostPathTypePtr(t corev1.HostPathType) *corev1.HostPathType {
|
|
return &t
|
|
}
|
|
|
|
func createDaemonSet(c client.Client, operation *operatorv1.Operation, namespace, image string, metricsRBAC bool) error {
|
|
labels := map[string]string{}
|
|
for k, v := range operation.Labels {
|
|
labels[k] = v
|
|
}
|
|
|
|
daemonSet := &appsv1.DaemonSet{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: namespace,
|
|
Name: daemonSetName(operation.Name),
|
|
Labels: labels,
|
|
OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(operation, operation.GroupVersionKind())},
|
|
},
|
|
Spec: appsv1.DaemonSetSpec{
|
|
Selector: &metav1.LabelSelector{
|
|
MatchLabels: labels,
|
|
},
|
|
Template: corev1.PodTemplateSpec{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Labels: labels,
|
|
CreationTimestamp: metav1.Now(),
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
Tolerations: []corev1.Toleration{
|
|
{
|
|
Key: "node-role.kubernetes.io/master",
|
|
Effect: corev1.TaintEffectNoSchedule,
|
|
},
|
|
},
|
|
Containers: []corev1.Container{
|
|
{
|
|
Name: "agent",
|
|
Image: image,
|
|
Command: []string{
|
|
"/manager",
|
|
},
|
|
Args: []string{
|
|
"--mode=agent",
|
|
"--agent-node-name=$(MY_NODE_NAME)",
|
|
fmt.Sprintf("--agent-operation=%s", operation.Name),
|
|
},
|
|
Env: []corev1.EnvVar{
|
|
{
|
|
Name: "MY_NODE_NAME",
|
|
ValueFrom: &corev1.EnvVarSource{
|
|
FieldRef: &corev1.ObjectFieldSelector{
|
|
FieldPath: "spec.nodeName",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{
|
|
corev1.ResourceCPU: resource.MustParse("100m"),
|
|
corev1.ResourceMemory: resource.MustParse("30Mi"),
|
|
},
|
|
Requests: corev1.ResourceList{
|
|
corev1.ResourceCPU: resource.MustParse("100m"),
|
|
corev1.ResourceMemory: resource.MustParse("20Mi"),
|
|
},
|
|
},
|
|
SecurityContext: &corev1.SecurityContext{
|
|
Privileged: pointer.BoolPtr(true),
|
|
},
|
|
VolumeMounts: []corev1.VolumeMount{
|
|
{
|
|
Name: "kubeadm-binary",
|
|
MountPath: "/usr/bin/kubeadm",
|
|
},
|
|
{
|
|
Name: "etc-kubernetes",
|
|
MountPath: "/etc/kubernetes",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
TerminationGracePeriodSeconds: pointer.Int64Ptr(10),
|
|
HostNetwork: true,
|
|
Volumes: []corev1.Volume{
|
|
{
|
|
Name: "kubeadm-binary",
|
|
VolumeSource: corev1.VolumeSource{
|
|
HostPath: &corev1.HostPathVolumeSource{
|
|
Path: "/usr/bin/kubeadm",
|
|
Type: hostPathTypePtr(corev1.HostPathFile),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "etc-kubernetes",
|
|
VolumeSource: corev1.VolumeSource{
|
|
HostPath: &corev1.HostPathVolumeSource{
|
|
Path: "/etc/kubernetes",
|
|
Type: hostPathTypePtr(corev1.HostPathDirectory),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
extraLabels, err := operations.DaemonSetNodeSelectorLabels(operation)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get NodeSelector for the operation DaemonSet %s/%s", daemonSet.Namespace, daemonSet.Name)
|
|
}
|
|
if len(extraLabels) > 0 {
|
|
daemonSet.Spec.Template.Spec.NodeSelector = extraLabels
|
|
}
|
|
|
|
if metricsRBAC {
|
|
// Force /metrics to be accessible only locally
|
|
daemonSet.Spec.Template.Spec.Containers[0].Args = append(daemonSet.Spec.Template.Spec.Containers[0].Args,
|
|
"--metrics-addr=127.0.0.1:8080",
|
|
)
|
|
|
|
// Expose /metrics via rbac-proxy sidecar
|
|
daemonSet.Spec.Template.Spec.Containers = append(daemonSet.Spec.Template.Spec.Containers,
|
|
corev1.Container{
|
|
Name: "kube-rbac-proxy",
|
|
Image: "gcr.io/kubebuilder/kube-rbac-proxy:v0.4.0",
|
|
Args: []string{
|
|
"--secure-listen-address=0.0.0.0:8443",
|
|
"--upstream=http://127.0.0.1:8080/",
|
|
"--logtostderr=true",
|
|
"--v=10",
|
|
},
|
|
Ports: []corev1.ContainerPort{
|
|
{
|
|
ContainerPort: 8443,
|
|
Name: "https",
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
} else {
|
|
// Expose /metrics on default (insecure) port
|
|
if daemonSet.Annotations == nil {
|
|
daemonSet.Annotations = map[string]string{}
|
|
}
|
|
daemonSet.Annotations["prometheus.io/scrape"] = "true"
|
|
daemonSet.Spec.Template.Spec.Containers[0].Ports = []corev1.ContainerPort{
|
|
{
|
|
ContainerPort: 8080,
|
|
Name: "metrics",
|
|
Protocol: corev1.ProtocolTCP,
|
|
},
|
|
}
|
|
}
|
|
|
|
if err := c.Create(
|
|
context.Background(), daemonSet,
|
|
); err != nil {
|
|
return errors.Wrapf(err, "failed to create DaemonSet %s/%s", daemonSet.Namespace, daemonSet.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func deleteDaemonSet(c client.Client, daemonSet *appsv1.DaemonSet) error {
|
|
if err := c.Delete(
|
|
context.Background(), daemonSet,
|
|
); err != nil {
|
|
return errors.Wrapf(err, "failed to delete DaemonSet %s/%s", daemonSet.Namespace, daemonSet.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func listTaskGroupsByLabels(c client.Client, labels map[string]string) (*operatorv1.RuntimeTaskGroupList, error) {
|
|
taskdeployments := &operatorv1.RuntimeTaskGroupList{}
|
|
if err := c.List(
|
|
context.Background(), taskdeployments,
|
|
client.MatchingLabels(labels),
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return taskdeployments, nil
|
|
}
|
|
|
|
func recordPausedChange(recorder record.EventRecorder, obj runtime.Object, current, new bool, args ...string) {
|
|
if current != new {
|
|
reasonVerb := "Paused"
|
|
messageAction := "set to pause"
|
|
if !new {
|
|
reasonVerb = "Restarted"
|
|
messageAction = "set for restart"
|
|
}
|
|
|
|
reason := fmt.Sprintf("%s%s", obj.GetObjectKind().GroupVersionKind().Kind, reasonVerb)
|
|
message := fmt.Sprintf("%s %s", obj.GetObjectKind().GroupVersionKind().Kind, messageAction)
|
|
if len(args) > 0 {
|
|
message = fmt.Sprintf("%s %s", message, strings.Join(args, " "))
|
|
}
|
|
recorder.Event(obj, corev1.EventTypeNormal, reason, message)
|
|
}
|
|
}
|
|
|
|
func operationToTaskGroupRequests(c client.Client, o handler.MapObject) []ctrl.Request {
|
|
var result []ctrl.Request
|
|
|
|
operation, ok := o.Object.(*operatorv1.Operation)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
actual, err := listTaskGroupsByLabels(c, operation.Labels)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
for _, ms := range actual.Items {
|
|
name := client.ObjectKey{Namespace: ms.Namespace, Name: ms.Name}
|
|
result = append(result, ctrl.Request{NamespacedName: name})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func getOwnerOperation(ctx context.Context, c client.Client, obj metav1.ObjectMeta) (*operatorv1.Operation, error) {
|
|
for _, ref := range obj.OwnerReferences {
|
|
if ref.Kind == "Operation" && ref.APIVersion == operatorv1.GroupVersion.String() {
|
|
operation := &operatorv1.Operation{}
|
|
key := client.ObjectKey{
|
|
Namespace: obj.Namespace,
|
|
Name: ref.Name,
|
|
}
|
|
if err := c.Get(ctx, key, operation); err != nil {
|
|
return nil, errors.Wrapf(err, "error reading controller ref for %s/%s", obj.Namespace, obj.Name)
|
|
}
|
|
return operation, nil
|
|
}
|
|
}
|
|
return nil, errors.Errorf("missing controller ref for %s/%s", obj.Namespace, obj.Name)
|
|
}
|
|
|
|
type matchingSelector struct {
|
|
selector labels.Selector
|
|
}
|
|
|
|
func (m matchingSelector) ApplyToList(opts *client.ListOptions) {
|
|
opts.LabelSelector = m.selector
|
|
}
|
|
|
|
func listNodesBySelector(c client.Client, selector *metav1.LabelSelector) (*corev1.NodeList, error) {
|
|
s, err := metav1.LabelSelectorAsSelector(selector)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to convert TaskGroup.Spec.NodeSelector to a selector")
|
|
}
|
|
|
|
o := matchingSelector{selector: s}
|
|
|
|
nodes := &corev1.NodeList{}
|
|
if err := c.List(
|
|
context.Background(), nodes,
|
|
o,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nodes, nil
|
|
}
|
|
|
|
func filterNodes(nodes *corev1.NodeList, filter operatorv1.RuntimeTaskGroupNodeFilter) []corev1.Node {
|
|
if len(nodes.Items) == 0 {
|
|
return nodes.Items
|
|
}
|
|
|
|
if filter == operatorv1.RuntimeTaskGroupNodeFilterAll || filter == operatorv1.RuntimeTaskGroupNodeUnknownFilter {
|
|
return nodes.Items
|
|
}
|
|
|
|
// in order to ensure a predictable result, nodes are sorted by name before applying the filter
|
|
sort.Slice(nodes.Items, func(i, j int) bool { return nodes.Items[i].Name < nodes.Items[j].Name })
|
|
|
|
if filter == operatorv1.RuntimeTaskGroupNodeFilterHead {
|
|
return nodes.Items[:1]
|
|
}
|
|
|
|
// filter == operatorv1alpha1.TaskGroupNodeFilterTail
|
|
return nodes.Items[1:]
|
|
}
|
|
|
|
func listTasksBySelector(c client.Client, selector *metav1.LabelSelector) (*operatorv1.RuntimeTaskList, error) {
|
|
selectorMap, err := metav1.LabelSelectorAsMap(selector)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to convert TaskGroup.Spec.Selector to a selector")
|
|
}
|
|
|
|
tasks := &operatorv1.RuntimeTaskList{}
|
|
if err := c.List(
|
|
context.Background(), tasks,
|
|
client.MatchingLabels(selectorMap),
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return tasks, nil
|
|
}
|
|
|
|
func taskGroupToTaskRequests(c client.Client, o handler.MapObject) []ctrl.Request {
|
|
var result []ctrl.Request
|
|
|
|
taskgroup, ok := o.Object.(*operatorv1.RuntimeTaskGroup)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
actual, err := listTasksBySelector(c, &taskgroup.Spec.Selector)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
for _, ms := range actual.Items {
|
|
name := client.ObjectKey{Namespace: ms.Namespace, Name: ms.Name}
|
|
result = append(result, ctrl.Request{NamespacedName: name})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func getOwnerTaskGroup(ctx context.Context, c client.Client, obj metav1.ObjectMeta) (*operatorv1.RuntimeTaskGroup, error) {
|
|
for _, ref := range obj.OwnerReferences {
|
|
if ref.Kind == "RuntimeTaskGroup" && ref.APIVersion == operatorv1.GroupVersion.String() {
|
|
taskgroup := &operatorv1.RuntimeTaskGroup{}
|
|
key := client.ObjectKey{
|
|
Namespace: obj.Namespace,
|
|
Name: ref.Name,
|
|
}
|
|
if err := c.Get(ctx, key, taskgroup); err != nil {
|
|
return nil, errors.Wrapf(err, "error reading controller ref for %s/%s", obj.Namespace, obj.Name)
|
|
}
|
|
return taskgroup, nil
|
|
}
|
|
}
|
|
return nil, errors.Errorf("missing controller ref for %s/%s", obj.Namespace, obj.Name)
|
|
}
|