mirror of
https://github.com/k3s-io/k3s.git
synced 2024-06-07 19:41:36 +00:00
380 lines
8.7 KiB
Go
380 lines
8.7 KiB
Go
package helm
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
|
|
helmv1 "github.com/rancher/helm-controller/pkg/apis/helm.cattle.io/v1"
|
|
helmcontroller "github.com/rancher/helm-controller/pkg/generated/controllers/helm.cattle.io/v1"
|
|
batchcontroller "github.com/rancher/wrangler-api/pkg/generated/controllers/batch/v1"
|
|
corecontroller "github.com/rancher/wrangler-api/pkg/generated/controllers/core/v1"
|
|
rbaccontroller "github.com/rancher/wrangler-api/pkg/generated/controllers/rbac/v1"
|
|
"github.com/rancher/wrangler/pkg/apply"
|
|
"github.com/rancher/wrangler/pkg/objectset"
|
|
"github.com/rancher/wrangler/pkg/relatedresource"
|
|
batch "k8s.io/api/batch/v1"
|
|
core "k8s.io/api/core/v1"
|
|
rbac "k8s.io/api/rbac/v1"
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/intstr"
|
|
)
|
|
|
|
var (
|
|
trueVal = true
|
|
)
|
|
|
|
type Controller struct {
|
|
namespace string
|
|
helmController helmcontroller.HelmChartController
|
|
jobsCache batchcontroller.JobCache
|
|
apply apply.Apply
|
|
}
|
|
|
|
const (
|
|
image = "rancher/klipper-helm:v0.2.2"
|
|
label = "helmcharts.helm.cattle.io/chart"
|
|
name = "helm-controller"
|
|
)
|
|
|
|
func Register(ctx context.Context, apply apply.Apply,
|
|
helms helmcontroller.HelmChartController,
|
|
jobs batchcontroller.JobController,
|
|
crbs rbaccontroller.ClusterRoleBindingController,
|
|
sas corecontroller.ServiceAccountController,
|
|
cm corecontroller.ConfigMapController) {
|
|
apply = apply.WithSetID(name).
|
|
WithCacheTypes(helms, jobs, crbs, sas, cm).
|
|
WithStrictCaching().WithPatcher(batch.SchemeGroupVersion.WithKind("Job"), func(namespace, name string, pt types.PatchType, data []byte) (runtime.Object, error) {
|
|
err := jobs.Delete(namespace, name, &metav1.DeleteOptions{})
|
|
if err == nil {
|
|
return nil, fmt.Errorf("replace job")
|
|
}
|
|
return nil, err
|
|
})
|
|
|
|
relatedresource.Watch(ctx, "helm-pod-watch",
|
|
func(namespace, name string, obj runtime.Object) ([]relatedresource.Key, error) {
|
|
if job, ok := obj.(*batch.Job); ok {
|
|
name := job.Labels[label]
|
|
if name != "" {
|
|
return []relatedresource.Key{
|
|
{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
},
|
|
}, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
},
|
|
helms,
|
|
jobs)
|
|
|
|
controller := &Controller{
|
|
helmController: helms,
|
|
jobsCache: jobs.Cache(),
|
|
apply: apply,
|
|
}
|
|
|
|
helms.OnChange(ctx, name, controller.OnHelmChanged)
|
|
helms.OnRemove(ctx, name, controller.OnHelmRemove)
|
|
}
|
|
|
|
func (c *Controller) OnHelmChanged(key string, chart *helmv1.HelmChart) (*helmv1.HelmChart, error) {
|
|
if chart == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
if chart.Spec.Chart == "" {
|
|
return chart, nil
|
|
}
|
|
|
|
objs := objectset.NewObjectSet()
|
|
job, configMap := job(chart)
|
|
objs.Add(serviceAccount(chart))
|
|
objs.Add(roleBinding(chart))
|
|
objs.Add(job)
|
|
if configMap != nil {
|
|
objs.Add(configMap)
|
|
}
|
|
|
|
if err := c.apply.WithOwner(chart).Apply(objs); err != nil {
|
|
return chart, err
|
|
}
|
|
|
|
chartCopy := chart.DeepCopy()
|
|
chartCopy.Status.JobName = job.Name
|
|
return c.helmController.Update(chartCopy)
|
|
}
|
|
|
|
func (c *Controller) OnHelmRemove(key string, chart *helmv1.HelmChart) (*helmv1.HelmChart, error) {
|
|
if chart.Spec.Chart == "" {
|
|
return chart, nil
|
|
}
|
|
job, _ := job(chart)
|
|
job, err := c.jobsCache.Get(chart.Namespace, job.Name)
|
|
|
|
if errors.IsNotFound(err) {
|
|
_, err := c.OnHelmChanged(key, chart)
|
|
if err != nil {
|
|
return chart, err
|
|
}
|
|
} else if err != nil {
|
|
return chart, err
|
|
}
|
|
|
|
if job.Status.Succeeded <= 0 {
|
|
return chart, fmt.Errorf("waiting for delete of helm chart %s", chart.Name)
|
|
}
|
|
|
|
chartCopy := chart.DeepCopy()
|
|
chartCopy.Status.JobName = job.Name
|
|
newChart, err := c.helmController.Update(chartCopy)
|
|
|
|
if err != nil {
|
|
return newChart, err
|
|
}
|
|
|
|
return newChart, c.apply.WithOwner(newChart).Apply(objectset.NewObjectSet())
|
|
}
|
|
|
|
func job(chart *helmv1.HelmChart) (*batch.Job, *core.ConfigMap) {
|
|
oneThousand := int32(1000)
|
|
valuesHash := sha256.Sum256([]byte(chart.Spec.ValuesContent))
|
|
|
|
action := "install"
|
|
if chart.DeletionTimestamp != nil {
|
|
action = "delete"
|
|
}
|
|
job := &batch.Job{
|
|
TypeMeta: meta.TypeMeta{
|
|
APIVersion: "batch/v1",
|
|
Kind: "Job",
|
|
},
|
|
ObjectMeta: meta.ObjectMeta{
|
|
Name: fmt.Sprintf("helm-%s-%s", action, chart.Name),
|
|
Namespace: chart.Namespace,
|
|
Labels: map[string]string{
|
|
label: chart.Name,
|
|
},
|
|
},
|
|
Spec: batch.JobSpec{
|
|
BackoffLimit: &oneThousand,
|
|
Template: core.PodTemplateSpec{
|
|
ObjectMeta: meta.ObjectMeta{
|
|
Labels: map[string]string{
|
|
label: chart.Name,
|
|
},
|
|
},
|
|
Spec: core.PodSpec{
|
|
RestartPolicy: core.RestartPolicyOnFailure,
|
|
Containers: []core.Container{
|
|
{
|
|
Name: "helm",
|
|
Image: image,
|
|
ImagePullPolicy: core.PullIfNotPresent,
|
|
Args: args(chart),
|
|
Env: []core.EnvVar{
|
|
{
|
|
Name: "NAME",
|
|
Value: chart.Name,
|
|
},
|
|
{
|
|
Name: "VERSION",
|
|
Value: chart.Spec.Version,
|
|
},
|
|
{
|
|
Name: "REPO",
|
|
Value: chart.Spec.Repo,
|
|
},
|
|
{
|
|
Name: "VALUES_HASH",
|
|
Value: hex.EncodeToString(valuesHash[:]),
|
|
},
|
|
{
|
|
Name: "HELM_DRIVER",
|
|
Value: "secret",
|
|
},
|
|
{
|
|
Name: "CHART_NAMESPACE",
|
|
Value: chart.Namespace,
|
|
},
|
|
{
|
|
Name: "CHART",
|
|
Value: chart.Spec.Chart,
|
|
},
|
|
{
|
|
Name: "HELM_VERSION",
|
|
Value: chart.Spec.HelmVersion,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ServiceAccountName: fmt.Sprintf("helm-%s", chart.Name),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
setProxyEnv(job)
|
|
configMap := configMap(chart)
|
|
if configMap == nil {
|
|
return job, nil
|
|
}
|
|
|
|
job.Spec.Template.Spec.Volumes = []core.Volume{
|
|
{
|
|
Name: "values",
|
|
VolumeSource: core.VolumeSource{
|
|
ConfigMap: &core.ConfigMapVolumeSource{
|
|
LocalObjectReference: core.LocalObjectReference{
|
|
Name: configMap.Name,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
job.Spec.Template.Spec.Containers[0].VolumeMounts = []core.VolumeMount{
|
|
{
|
|
MountPath: "/config",
|
|
Name: "values",
|
|
},
|
|
}
|
|
|
|
return job, configMap
|
|
}
|
|
|
|
func configMap(chart *helmv1.HelmChart) *core.ConfigMap {
|
|
if chart.Spec.ValuesContent == "" {
|
|
return nil
|
|
}
|
|
|
|
return &core.ConfigMap{
|
|
TypeMeta: meta.TypeMeta{
|
|
APIVersion: "v1",
|
|
Kind: "ConfigMap",
|
|
},
|
|
ObjectMeta: meta.ObjectMeta{
|
|
Name: fmt.Sprintf("chart-values-%s", chart.Name),
|
|
Namespace: chart.Namespace,
|
|
},
|
|
Data: map[string]string{
|
|
"values.yaml": chart.Spec.ValuesContent,
|
|
},
|
|
}
|
|
}
|
|
|
|
func roleBinding(chart *helmv1.HelmChart) *rbac.ClusterRoleBinding {
|
|
return &rbac.ClusterRoleBinding{
|
|
TypeMeta: meta.TypeMeta{
|
|
APIVersion: "rbac.authorization.k8s.io/v1",
|
|
Kind: "ClusterRoleBinding",
|
|
},
|
|
ObjectMeta: meta.ObjectMeta{
|
|
Name: fmt.Sprintf("helm-%s-%s", chart.Namespace, chart.Name),
|
|
},
|
|
RoleRef: rbac.RoleRef{
|
|
Kind: "ClusterRole",
|
|
APIGroup: "rbac.authorization.k8s.io",
|
|
Name: "cluster-admin",
|
|
},
|
|
Subjects: []rbac.Subject{
|
|
{
|
|
Name: fmt.Sprintf("helm-%s", chart.Name),
|
|
Kind: "ServiceAccount",
|
|
Namespace: chart.Namespace,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func serviceAccount(chart *helmv1.HelmChart) *core.ServiceAccount {
|
|
return &core.ServiceAccount{
|
|
TypeMeta: meta.TypeMeta{
|
|
APIVersion: "v1",
|
|
Kind: "ServiceAccount",
|
|
},
|
|
ObjectMeta: meta.ObjectMeta{
|
|
Name: fmt.Sprintf("helm-%s", chart.Name),
|
|
Namespace: chart.Namespace,
|
|
},
|
|
AutomountServiceAccountToken: &trueVal,
|
|
}
|
|
}
|
|
|
|
func args(chart *helmv1.HelmChart) []string {
|
|
if chart.DeletionTimestamp != nil {
|
|
return []string{
|
|
"delete",
|
|
}
|
|
}
|
|
|
|
spec := chart.Spec
|
|
args := []string{
|
|
"install",
|
|
}
|
|
if spec.TargetNamespace != "" {
|
|
args = append(args, "--namespace", spec.TargetNamespace)
|
|
}
|
|
if spec.Repo != "" {
|
|
args = append(args, "--repo", spec.Repo)
|
|
}
|
|
if spec.Version != "" {
|
|
args = append(args, "--version", spec.Version)
|
|
}
|
|
|
|
for _, k := range keys(spec.Set) {
|
|
val := spec.Set[k]
|
|
if val.StrVal != "" {
|
|
args = append(args, "--set-string", fmt.Sprintf("%s=%s", k, val.StrVal))
|
|
} else {
|
|
args = append(args, "--set", fmt.Sprintf("%s=%d", k, val.IntVal))
|
|
}
|
|
}
|
|
|
|
return args
|
|
}
|
|
|
|
func keys(val map[string]intstr.IntOrString) []string {
|
|
var keys []string
|
|
for k := range val {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
func setProxyEnv(job *batch.Job) {
|
|
proxySysEnv := []string{
|
|
"all_proxy",
|
|
"ALL_PROXY",
|
|
"http_proxy",
|
|
"HTTP_PROXY",
|
|
"https_proxy",
|
|
"HTTPS_PROXY",
|
|
"no_proxy",
|
|
"NO_PROXY",
|
|
}
|
|
for _, proxyEnv := range proxySysEnv {
|
|
proxyEnvValue := os.Getenv(proxyEnv)
|
|
if len(proxyEnvValue) == 0 {
|
|
continue
|
|
}
|
|
envar := core.EnvVar{
|
|
Name: proxyEnv,
|
|
Value: proxyEnvValue,
|
|
}
|
|
job.Spec.Template.Spec.Containers[0].Env = append(
|
|
job.Spec.Template.Spec.Containers[0].Env,
|
|
envar)
|
|
}
|
|
}
|