karmada/operator/pkg/util/patcher/pather.go

283 lines
7.9 KiB
Go

package patcher
import (
"errors"
"fmt"
"sort"
"strconv"
"strings"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/klog/v2"
operatorv1alpha1 "github.com/karmada-io/karmada/operator/pkg/apis/operator/v1alpha1"
"github.com/karmada-io/karmada/operator/pkg/constants"
)
// Patcher defines multiple variables that need to be patched.
type Patcher struct {
labels map[string]string
annotations map[string]string
extraArgs map[string]string
featureGates map[string]bool
volume *operatorv1alpha1.VolumeData
}
// NewPatcher returns a patcher.
func NewPatcher() *Patcher {
return &Patcher{}
}
// WithLabels sets labels to the patcher.
func (p *Patcher) WithLabels(labels labels.Set) *Patcher {
p.labels = labels
return p
}
// WithAnnotations sets annotations to the patcher.
func (p *Patcher) WithAnnotations(annotations labels.Set) *Patcher {
p.annotations = annotations
return p
}
// WithExtraArgs sets extraArgs to the patcher.
func (p *Patcher) WithExtraArgs(extraArgs map[string]string) *Patcher {
p.extraArgs = extraArgs
return p
}
// WithFeatureGates sets featureGates to the patcher.
func (p *Patcher) WithFeatureGates(featureGates map[string]bool) *Patcher {
p.featureGates = featureGates
return p
}
// WithVolumeData sets VolumeData to the patcher.
func (p *Patcher) WithVolumeData(volume *operatorv1alpha1.VolumeData) *Patcher {
p.volume = volume
return p
}
// ForDeployment patches the deployment manifest.
func (p *Patcher) ForDeployment(deployment *appsv1.Deployment) {
deployment.Labels = labels.Merge(deployment.Labels, p.labels)
deployment.Spec.Template.Labels = labels.Merge(deployment.Spec.Template.Labels, p.labels)
deployment.Annotations = labels.Merge(deployment.Annotations, p.annotations)
deployment.Spec.Template.Annotations = labels.Merge(deployment.Spec.Template.Annotations, p.annotations)
if len(p.extraArgs) != 0 || len(p.featureGates) != 0 {
// It's considered the first container is the karmada component by default.
baseArguments := deployment.Spec.Template.Spec.Containers[0].Command
argsMap := parseArgumentListToMap(baseArguments)
overrideArgs := map[string]string{}
// merge featureGates and build to an argurment.
if len(p.featureGates) != 0 {
baseFeatureGates := map[string]bool{}
if argument, ok := argsMap["feature-gates"]; ok {
baseFeatureGates = parseFeatrueGatesArgumentToMap(argument)
}
overrideArgs["feature-gates"] = buildFeatureGatesArgumentFromMap(baseFeatureGates, p.featureGates)
}
for key, val := range p.extraArgs {
overrideArgs[key] = val
}
// the first argument is most often the binary name
command := []string{baseArguments[0]}
command = append(command, buildArgumentListFromMap(argsMap, overrideArgs)...)
deployment.Spec.Template.Spec.Containers[0].Command = command
}
}
// ForStatefulSet patches the statefulset manifest.
func (p *Patcher) ForStatefulSet(sts *appsv1.StatefulSet) {
sts.Labels = labels.Merge(sts.Labels, p.labels)
sts.Spec.Template.Labels = labels.Merge(sts.Spec.Template.Labels, p.labels)
sts.Annotations = labels.Merge(sts.Annotations, p.annotations)
sts.Spec.Template.Annotations = labels.Merge(sts.Spec.Template.Annotations, p.annotations)
if p.volume != nil {
patchVolumeForStatefulSet(sts, p.volume)
}
if len(p.extraArgs) != 0 {
// It's considered the first container is the karmada component by default.
baseArguments := sts.Spec.Template.Spec.Containers[0].Command
argsMap := parseArgumentListToMap(baseArguments)
sts.Spec.Template.Spec.Containers[0].Command = buildArgumentListFromMap(argsMap, p.extraArgs)
}
}
func buildArgumentListFromMap(baseArguments, overrideArguments map[string]string) []string {
var command []string
var keys []string
argsMap := make(map[string]string)
for k, v := range baseArguments {
argsMap[k] = v
}
for k, v := range overrideArguments {
argsMap[k] = v
}
for k := range argsMap {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
command = append(command, fmt.Sprintf("--%s=%s", k, argsMap[k]))
}
return command
}
func parseFeatrueGatesArgumentToMap(featureGates string) map[string]bool {
featureGateSlice := strings.Split(featureGates, ",")
featureGatesMap := map[string]bool{}
for _, featureGate := range featureGateSlice {
key, val, err := parseFeatrueGate(featureGate)
if err != nil {
continue
}
featureGatesMap[key] = val
}
return featureGatesMap
}
func buildFeatureGatesArgumentFromMap(baseFeatureGates, overrideFeatureGates map[string]bool) string {
var featureGates []string
var keys []string
featureGateMap := make(map[string]bool)
for k, v := range baseFeatureGates {
featureGateMap[k] = v
}
for k, v := range overrideFeatureGates {
featureGateMap[k] = v
}
for k := range featureGateMap {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
featureGates = append(featureGates, fmt.Sprintf("%s=%s", k, strconv.FormatBool(featureGateMap[k])))
}
return strings.Join(featureGates, ",")
}
func parseArgumentListToMap(arguments []string) map[string]string {
resultingMap := map[string]string{}
for i, arg := range arguments {
key, val, err := parseArgument(arg)
// Ignore if the first argument doesn't satisfy the criteria, it's most often the binary name
// Warn in all other cases, but don't error out. This can happen only if the user has edited the argument list by hand, so they might know what they are doing
if err != nil {
if i != 0 {
klog.Warningf("WARNING: The component argument %q could not be parsed correctly. The argument must be of the form %q. Skipping...\n", arg, "--")
}
continue
}
resultingMap[key] = val
}
return resultingMap
}
func parseArgument(arg string) (string, string, error) {
if !strings.HasPrefix(arg, "--") {
return "", "", errors.New("the argument should start with '--'")
}
if !strings.Contains(arg, "=") {
return "", "", errors.New("the argument should have a '=' between the flag and the value")
}
arg = strings.TrimPrefix(arg, "--")
keyvalSlice := strings.SplitN(arg, "=", 2)
if len(keyvalSlice) != 2 {
return "", "", errors.New("the argument must have both a key and a value")
}
if len(keyvalSlice[0]) == 0 {
return "", "", errors.New("the argument must have a key")
}
return keyvalSlice[0], keyvalSlice[1], nil
}
func parseFeatrueGate(featureGate string) (string, bool, error) {
if !strings.Contains(featureGate, "=") {
return "", false, errors.New("the featureGate should have a '=' between the flag and the value")
}
keyvalSlice := strings.SplitN(featureGate, "=", 2)
if len(keyvalSlice) != 2 {
return "", false, errors.New("the featureGate must have both a key and a value")
}
if len(keyvalSlice[0]) == 0 {
return "", false, errors.New("the featureGate must have a key")
}
val, err := strconv.ParseBool(keyvalSlice[1])
if err != nil {
return "", false, errors.New("the featureGate value must have a value of type bool")
}
return keyvalSlice[0], val, nil
}
func patchVolumeForStatefulSet(sts *appsv1.StatefulSet, volume *operatorv1alpha1.VolumeData) {
if volume.EmptyDir != nil {
volumes := sts.Spec.Template.Spec.Volumes
volumes = append(volumes, corev1.Volume{
Name: constants.EtcdDataVolumeName,
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
})
sts.Spec.Template.Spec.Volumes = volumes
}
if volume.HostPath != nil {
volumes := sts.Spec.Template.Spec.Volumes
volumes = append(volumes, corev1.Volume{
Name: constants.EtcdDataVolumeName,
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{
Path: volume.HostPath.Path,
Type: volume.HostPath.Type,
},
},
})
sts.Spec.Template.Spec.Volumes = volumes
}
if volume.VolumeClaim != nil {
sts.Spec.VolumeClaimTemplates = []corev1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Name: constants.EtcdDataVolumeName,
},
Spec: volume.VolumeClaim.Spec,
},
}
}
}