diff --git a/README.md b/README.md index 3635934..8260fe8 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ Usage: ## Status Supported k8s resources: -- deployment +- deployment, statefulset - service, Ingress - PersistentVolumeClaim - RBAC (serviceaccount, (cluster-)role, (cluster-)rolebinding) diff --git a/pkg/app/app.go b/pkg/app/app.go index ac3705b..37dee15 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -16,6 +16,7 @@ import ( "github.com/arttor/helmify/pkg/processor/configmap" "github.com/arttor/helmify/pkg/processor/crd" "github.com/arttor/helmify/pkg/processor/deployment" + "github.com/arttor/helmify/pkg/processor/statefulset" "github.com/arttor/helmify/pkg/processor/rbac" "github.com/arttor/helmify/pkg/processor/secret" "github.com/arttor/helmify/pkg/processor/service" @@ -45,6 +46,7 @@ func Start(input io.Reader, config config.Config) error { configmap.New(), crd.New(), deployment.New(), + statefulset.New(), storage.New(), service.New(), service.NewIngress(), diff --git a/pkg/processor/deployment/deployment.go b/pkg/processor/deployment/deployment.go index 13f769a..80503d7 100644 --- a/pkg/processor/deployment/deployment.go +++ b/pkg/processor/deployment/deployment.go @@ -3,17 +3,15 @@ package deployment import ( "fmt" "io" - "strings" "text/template" + "github.com/arttor/helmify/pkg/helmify" "github.com/arttor/helmify/pkg/processor" - "github.com/arttor/helmify/pkg/helmify" - yamlformat "github.com/arttor/helmify/pkg/yaml" "github.com/iancoleman/strcase" "github.com/pkg/errors" + appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -41,10 +39,6 @@ spec: spec: {{ .Spec }}`) -const selectorTempl = `%[1]s -{{- include "%[2]s.selectorLabels" . | nindent 6 }} -%[3]s` - // New creates processor for k8s Deployment resource. func New() helmify.Processor { return &deployment{} @@ -57,107 +51,47 @@ func (d deployment) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstr if obj.GroupVersionKind() != deploymentGVC { return false, nil, nil } - depl := appsv1.Deployment{} - err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &depl) + typedObj := appsv1.Deployment{} + err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &typedObj) if err != nil { return true, nil, errors.Wrap(err, "unable to cast to deployment") } - meta, err := processor.ProcessObjMeta(appMeta, obj) - if err != nil { - return true, nil, err - } values := helmify.Values{} name := appMeta.TrimName(obj.GetName()) - replicas, err := processReplicas(name, &depl, &values) - if err != nil { - return true, nil, err - } - matchLabels, err := yamlformat.Marshal(map[string]interface{}{"matchLabels": depl.Spec.Selector.MatchLabels}, 0) + meta, err := processor.ProcessObjMeta(appMeta, obj) if err != nil { return true, nil, err } - matchExpr := "" - if depl.Spec.Selector.MatchExpressions != nil { - matchExpr, err = yamlformat.Marshal(map[string]interface{}{"matchExpressions": depl.Spec.Selector.MatchExpressions}, 0) - if err != nil { - return true, nil, err - } - } - selector := fmt.Sprintf(selectorTempl, matchLabels, appMeta.ChartName(), matchExpr) - selector = strings.Trim(selector, " \n") - selector = string(yamlformat.Indent([]byte(selector), 4)) - podLabels, err := yamlformat.Marshal(depl.Spec.Template.ObjectMeta.Labels, 8) + replicas, err := processor.ProcessReplicas(name, typedObj.Spec.Replicas, &values) if err != nil { return true, nil, err } - podLabels += fmt.Sprintf("\n {{- include \"%s.selectorLabels\" . | nindent 8 }}", appMeta.ChartName()) - - podAnnotations := "" - if len(depl.Spec.Template.ObjectMeta.Annotations) != 0 { - podAnnotations, err = yamlformat.Marshal(map[string]interface{}{"annotations": depl.Spec.Template.ObjectMeta.Annotations}, 6) - if err != nil { - return true, nil, err - } - - podAnnotations = "\n" + podAnnotations - } - nameCamel := strcase.ToLowerCamel(name) - podValues, err := processPodSpec(nameCamel, appMeta, &depl.Spec.Template.Spec) - if err != nil { - return true, nil, err - } - err = values.Merge(podValues) + selector, err := processor.ProcessSelector(appMeta, typedObj.Spec.Selector) if err != nil { return true, nil, err } - // replace PVC to templated name - for i := 0; i < len(depl.Spec.Template.Spec.Volumes); i++ { - vol := depl.Spec.Template.Spec.Volumes[i] - if vol.PersistentVolumeClaim == nil { - continue - } - tempPVCName := appMeta.TemplatedName(vol.PersistentVolumeClaim.ClaimName) - depl.Spec.Template.Spec.Volumes[i].PersistentVolumeClaim.ClaimName = tempPVCName + pod := processor.Pod{ + Name: strcase.ToLowerCamel(name), + AppMeta: appMeta, + Pod: &typedObj.Spec.Template, } - // replace container resources with template to values. - specMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&depl.Spec.Template.Spec) - if err != nil { - return true, nil, err - } - containers, _, err := unstructured.NestedSlice(specMap, "containers") + podLabels, podAnnotations, err := pod.ProcessObjectMeta() if err != nil { return true, nil, err } - for i := range containers { - containerName := strcase.ToLowerCamel((containers[i].(map[string]interface{})["name"]).(string)) - res, exists, err := unstructured.NestedMap(values, nameCamel, containerName, "resources") - if err != nil { - return true, nil, err - } - if !exists || len(res) == 0 { - continue - } - err = unstructured.SetNestedField(containers[i].(map[string]interface{}), fmt.Sprintf(`{{- toYaml .Values.%s.%s.resources | nindent 10 }}`, nameCamel, containerName), "resources") - if err != nil { - return true, nil, err - } - } - err = unstructured.SetNestedSlice(specMap, containers, "containers") - if err != nil { - return true, nil, err - } - spec, err := yamlformat.Marshal(specMap, 6) + podLabels += fmt.Sprintf("\n {{- include \"%s.selectorLabels\" . | nindent 8 }}", appMeta.ChartName()) + + spec, err := pod.ProcessSpec(&values) if err != nil { return true, nil, err } - spec = strings.ReplaceAll(spec, "'", "") return true, &result{ values: values, @@ -179,96 +113,6 @@ func (d deployment) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstr }, nil } -func processReplicas(name string, deployment *appsv1.Deployment, values *helmify.Values) (string, error) { - if deployment.Spec.Replicas == nil { - return "", nil - } - replicasTpl, err := values.Add(int64(*deployment.Spec.Replicas), name, "replicas") - if err != nil { - return "", err - } - replicas, err := yamlformat.Marshal(map[string]interface{}{"replicas": replicasTpl}, 2) - if err != nil { - return "", err - } - replicas = strings.ReplaceAll(replicas, "'", "") - return replicas, nil -} - -func processPodSpec(name string, appMeta helmify.AppMetadata, pod *corev1.PodSpec) (helmify.Values, error) { - values := helmify.Values{} - for i, c := range pod.Containers { - processed, err := processPodContainer(name, appMeta, c, &values) - if err != nil { - return nil, err - } - pod.Containers[i] = processed - } - for _, v := range pod.Volumes { - if v.ConfigMap != nil { - v.ConfigMap.Name = appMeta.TemplatedName(v.ConfigMap.Name) - } - if v.Secret != nil { - v.Secret.SecretName = appMeta.TemplatedName(v.Secret.SecretName) - } - } - pod.ServiceAccountName = appMeta.TemplatedName(pod.ServiceAccountName) - - for i, s := range pod.ImagePullSecrets { - pod.ImagePullSecrets[i].Name = appMeta.TemplatedName(s.Name) - } - - return values, nil -} - -func processPodContainer(name string, appMeta helmify.AppMetadata, c corev1.Container, values *helmify.Values) (corev1.Container, error) { - index := strings.LastIndex(c.Image, ":") - if index < 0 { - return c, errors.New("wrong image format: " + c.Image) - } - repo, tag := c.Image[:index], c.Image[index+1:] - containerName := strcase.ToLowerCamel(c.Name) - c.Image = fmt.Sprintf("{{ .Values.%[1]s.%[2]s.image.repository }}:{{ .Values.%[1]s.%[2]s.image.tag | default .Chart.AppVersion }}", name, containerName) - - err := unstructured.SetNestedField(*values, repo, name, containerName, "image", "repository") - if err != nil { - return c, errors.Wrap(err, "unable to set deployment value field") - } - err = unstructured.SetNestedField(*values, tag, name, containerName, "image", "tag") - if err != nil { - return c, errors.Wrap(err, "unable to set deployment value field") - } - for _, e := range c.Env { - if e.ValueFrom != nil && e.ValueFrom.SecretKeyRef != nil { - e.ValueFrom.SecretKeyRef.Name = appMeta.TemplatedName(e.ValueFrom.SecretKeyRef.Name) - } - if e.ValueFrom != nil && e.ValueFrom.ConfigMapKeyRef != nil { - e.ValueFrom.ConfigMapKeyRef.Name = appMeta.TemplatedName(e.ValueFrom.ConfigMapKeyRef.Name) - } - } - for _, e := range c.EnvFrom { - if e.SecretRef != nil { - e.SecretRef.Name = appMeta.TemplatedName(e.SecretRef.Name) - } - if e.ConfigMapRef != nil { - e.ConfigMapRef.Name = appMeta.TemplatedName(e.ConfigMapRef.Name) - } - } - for k, v := range c.Resources.Requests { - err = unstructured.SetNestedField(*values, v.ToUnstructured(), name, containerName, "resources", "requests", k.String()) - if err != nil { - return c, errors.Wrap(err, "unable to set container resources value") - } - } - for k, v := range c.Resources.Limits { - err = unstructured.SetNestedField(*values, v.ToUnstructured(), name, containerName, "resources", "limits", k.String()) - if err != nil { - return c, errors.Wrap(err, "unable to set container resources value") - } - } - return c, nil -} - type result struct { data struct { Meta string diff --git a/pkg/processor/pod.go b/pkg/processor/pod.go new file mode 100644 index 0000000..b26f4eb --- /dev/null +++ b/pkg/processor/pod.go @@ -0,0 +1,201 @@ +package processor + +import ( + "fmt" + "strings" + + "github.com/arttor/helmify/pkg/helmify" + yamlformat "github.com/arttor/helmify/pkg/yaml" + + "github.com/iancoleman/strcase" + "github.com/pkg/errors" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +type Pod struct { + Name string + AppMeta helmify.AppMetadata + Pod *corev1.PodTemplateSpec +} + +func (p *Pod) ProcessSpec(values *helmify.Values) (string, error) { + podValues, err := p.getValues() + if err != nil { + return "", err + } + err = values.Merge(podValues) + if err != nil { + return "", err + } + + template, err := p.template(values) + if err != nil { + return "", err + } + + return template, nil +} + +func (p *Pod) ProcessObjectMeta() (string, string, error) { + podLabels, err := yamlformat.Marshal(p.Pod.ObjectMeta.Labels, 8) + if err != nil { + return "", "", err + } + + podAnnotations := "" + if len(p.Pod.ObjectMeta.Annotations) != 0 { + podAnnotations, err = yamlformat.Marshal(map[string]interface{}{"annotations": p.Pod.ObjectMeta.Annotations}, 6) + if err != nil { + return "", "", err + } + + podAnnotations = "\n" + podAnnotations + } + + return podLabels, podAnnotations, nil +} + +func (p *Pod) getValues() (helmify.Values, error) { + values := helmify.Values{} + for i, c := range p.Pod.Spec.Containers { + processed, err := p.processPodContainer(c, &values) + if err != nil { + return nil, err + } + p.Pod.Spec.Containers[i] = processed + } + for _, v := range p.Pod.Spec.Volumes { + if v.ConfigMap != nil { + v.ConfigMap.Name = p.AppMeta.TemplatedName(v.ConfigMap.Name) + } + if v.Secret != nil { + v.Secret.SecretName = p.AppMeta.TemplatedName(v.Secret.SecretName) + } + } + p.Pod.Spec.ServiceAccountName = p.AppMeta.TemplatedName(p.Pod.Spec.ServiceAccountName) + + for i, s := range p.Pod.Spec.ImagePullSecrets { + p.Pod.Spec.ImagePullSecrets[i].Name = p.AppMeta.TemplatedName(s.Name) + } + + return values, nil +} + +func (p *Pod) processPodContainer(c corev1.Container, values *helmify.Values) (corev1.Container, error) { + index := strings.LastIndex(c.Image, ":") + if index < 0 { + return c, errors.New("wrong image format: " + c.Image) + } + repo, tag := c.Image[:index], c.Image[index+1:] + containerName := strcase.ToLowerCamel(c.Name) + c.Image = fmt.Sprintf("{{ .Values.%[1]s.%[2]s.image.repository }}:{{ .Values.%[1]s.%[2]s.image.tag | default .Chart.AppVersion }}", p.Name, containerName) + + err := unstructured.SetNestedField(*values, repo, p.Name, containerName, "image", "repository") + if err != nil { + return c, errors.Wrap(err, "unable to set deployment value field") + } + err = unstructured.SetNestedField(*values, tag, p.Name, containerName, "image", "tag") + if err != nil { + return c, errors.Wrap(err, "unable to set deployment value field") + } + for _, e := range c.Env { + if e.ValueFrom != nil && e.ValueFrom.SecretKeyRef != nil { + e.ValueFrom.SecretKeyRef.Name = p.AppMeta.TemplatedName(e.ValueFrom.SecretKeyRef.Name) + } + if e.ValueFrom != nil && e.ValueFrom.ConfigMapKeyRef != nil { + e.ValueFrom.ConfigMapKeyRef.Name = p.AppMeta.TemplatedName(e.ValueFrom.ConfigMapKeyRef.Name) + } + } + for _, e := range c.EnvFrom { + if e.SecretRef != nil { + e.SecretRef.Name = p.AppMeta.TemplatedName(e.SecretRef.Name) + } + if e.ConfigMapRef != nil { + e.ConfigMapRef.Name = p.AppMeta.TemplatedName(e.ConfigMapRef.Name) + } + } + for k, v := range c.Resources.Requests { + err = unstructured.SetNestedField(*values, v.ToUnstructured(), p.Name, containerName, "resources", "requests", k.String()) + if err != nil { + return c, errors.Wrap(err, "unable to set container resources value") + } + } + for k, v := range c.Resources.Limits { + err = unstructured.SetNestedField(*values, v.ToUnstructured(), p.Name, containerName, "resources", "limits", k.String()) + if err != nil { + return c, errors.Wrap(err, "unable to set container resources value") + } + } + if len(c.Args) != 0 { + err = unstructured.SetNestedStringSlice(*values, c.Args, p.Name, containerName, "args") + if err != nil { + return c, errors.Wrap(err, "unable to set container resources value") + } + } + return c, nil +} + +func (p *Pod) template(values *helmify.Values) (string, error) { + // replace PVC to templated name + for i := 0; i < len(p.Pod.Spec.Volumes); i++ { + vol := p.Pod.Spec.Volumes[i] + if vol.PersistentVolumeClaim == nil { + continue + } + tempPVCName := p.AppMeta.TemplatedName(vol.PersistentVolumeClaim.ClaimName) + p.Pod.Spec.Volumes[i].PersistentVolumeClaim.ClaimName = tempPVCName + } + + // replace container resources with template to values. + specMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&p.Pod.Spec) + if err != nil { + return "", err + } + containers, _, err := unstructured.NestedSlice(specMap, "containers") + if err != nil { + return "", err + } + for i := range containers { + containerName := strcase.ToLowerCamel((containers[i].(map[string]interface{})["name"]).(string)) + res, exists, err := unstructured.NestedMap(*values, p.Name, containerName, "resources") + if err != nil { + return "", err + } + if !exists || len(res) == 0 { + continue + } + err = unstructured.SetNestedField(containers[i].(map[string]interface{}), fmt.Sprintf(`{{- toYaml .Values.%s.%s.resources | nindent 10 }}`, p.Name, containerName), "resources") + if err != nil { + return "", err + } + } + for i := range containers { + containerName := strcase.ToLowerCamel((containers[i].(map[string]interface{})["name"]).(string)) + res, exists, err := unstructured.NestedSlice(*values, p.Name, containerName, "args") + if err != nil { + return "", err + } + if !exists || len(res) == 0 { + continue + } + err = unstructured.SetNestedField(containers[i].(map[string]interface{}), fmt.Sprintf(`{{- toYaml .Values.%s.%s.args | nindent 10 }}`, p.Name, containerName), "args") + if err != nil { + return "", err + } + } + err = unstructured.SetNestedSlice(specMap, containers, "containers") + if err != nil { + return "", err + } + + spec, err := yamlformat.Marshal(specMap, 6) + if err != nil { + return "", err + } + spec = strings.ReplaceAll(spec, "'", "") + + return spec, nil +} diff --git a/pkg/processor/statefulset/statefulset.go b/pkg/processor/statefulset/statefulset.go new file mode 100644 index 0000000..883388b --- /dev/null +++ b/pkg/processor/statefulset/statefulset.go @@ -0,0 +1,138 @@ +package statefulset + +import ( + "fmt" + "io" + "text/template" + + "github.com/arttor/helmify/pkg/helmify" + "github.com/arttor/helmify/pkg/processor" + + "github.com/iancoleman/strcase" + "github.com/pkg/errors" + + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var statefulsetGVC = schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "StatefulSet", +} + +var statefulsetTempl, _ = template.New("statefulset").Parse( + `{{- .Meta }} +spec: +{{- if .Replicas }} +{{ .Replicas }} +{{- end }} + selector: +{{ .Selector }} + template: + metadata: + labels: +{{ .PodLabels }} +{{- .PodAnnotations }} + spec: +{{ .Spec }}`) + +// New creates processor for k8s StatefulSet resource. +func New() helmify.Processor { + return &statefulset{} +} + +type statefulset struct{} + +// Process k8s StatefulSet object into template. Returns false if not capable of processing given resource type. +func (d statefulset) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstructured) (bool, helmify.Template, error) { + if obj.GroupVersionKind() != statefulsetGVC { + return false, nil, nil + } + typedObj := appsv1.StatefulSet{} + err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &typedObj) + if err != nil { + return true, nil, errors.Wrap(err, "unable to cast to statefulset") + } + + values := helmify.Values{} + + name := appMeta.TrimName(obj.GetName()) + + meta, err := processor.ProcessObjMeta(appMeta, obj) + if err != nil { + return true, nil, err + } + + replicas, err := processor.ProcessReplicas(name, typedObj.Spec.Replicas, &values) + if err != nil { + return true, nil, err + } + + selector, err := processor.ProcessSelector(appMeta, typedObj.Spec.Selector) + if err != nil { + return true, nil, err + } + + pod := processor.Pod{ + Name: strcase.ToLowerCamel(name), + AppMeta: appMeta, + Pod: &typedObj.Spec.Template, + } + + podLabels, podAnnotations, err := pod.ProcessObjectMeta() + if err != nil { + return true, nil, err + } + podLabels += fmt.Sprintf("\n {{- include \"%s.selectorLabels\" . | nindent 8 }}", appMeta.ChartName()) + + spec, err := pod.ProcessSpec(&values) + if err != nil { + return true, nil, err + } + + return true, &result{ + values: values, + data: struct { + Meta string + Replicas string + Selector string + PodLabels string + PodAnnotations string + Spec string + }{ + Meta: meta, + Replicas: replicas, + Selector: selector, + PodLabels: podLabels, + PodAnnotations: podAnnotations, + Spec: spec, + }, + }, nil +} + +type result struct { + data struct { + Meta string + Replicas string + Selector string + PodLabels string + PodAnnotations string + Spec string + } + values helmify.Values +} + +func (r *result) Filename() string { + return "statefulset.yaml" +} + +func (r *result) Values() helmify.Values { + return r.values +} + +func (r *result) Write(writer io.Writer) error { + return statefulsetTempl.Execute(writer, r.data) +} diff --git a/pkg/processor/util.go b/pkg/processor/util.go new file mode 100644 index 0000000..b764040 --- /dev/null +++ b/pkg/processor/util.go @@ -0,0 +1,76 @@ +package processor + +import ( + "fmt" + "strings" + + "github.com/arttor/helmify/pkg/helmify" + yamlformat "github.com/arttor/helmify/pkg/yaml" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ReplicaTyped struct { + Spec ReplicaTypedSpec +} + +type ReplicaTypedSpec struct { + Replicas *int32 +} + +func ProcessReplicas(name string, r *int32, values *helmify.Values) (string, error) { + obj := ReplicaTyped{ + Spec: ReplicaTypedSpec{ + Replicas: r, + }, + } + if obj.Spec.Replicas == nil { + return "", nil + } + replicasTpl, err := values.Add(int64(*obj.Spec.Replicas), name, "replicas") + if err != nil { + return "", err + } + replicas, err := yamlformat.Marshal(map[string]interface{}{"replicas": replicasTpl}, 2) + if err != nil { + return "", err + } + replicas = strings.ReplaceAll(replicas, "'", "") + return replicas, nil +} + +type SelectorTyped struct { + Spec SelectorTypedSpec +} + +type SelectorTypedSpec struct { + Selector *metav1.LabelSelector +} + +const selectorTempl = `%[1]s +{{- include "%[2]s.selectorLabels" . | nindent 6 }} +%[3]s` + +func ProcessSelector(appMeta helmify.AppMetadata, s *metav1.LabelSelector) (string, error) { + obj := SelectorTyped{ + Spec: SelectorTypedSpec{ + Selector: s, + }, + } + matchLabels, err := yamlformat.Marshal(map[string]interface{}{"matchLabels": obj.Spec.Selector.MatchLabels}, 0) + if err != nil { + return "", err + } + matchExpr := "" + if obj.Spec.Selector.MatchExpressions != nil { + matchExpr, err = yamlformat.Marshal(map[string]interface{}{"matchExpressions": obj.Spec.Selector.MatchExpressions}, 0) + if err != nil { + return "", err + } + } + selector := fmt.Sprintf(selectorTempl, matchLabels, appMeta.ChartName(), matchExpr) + selector = strings.Trim(selector, " \n") + selector = string(yamlformat.Indent([]byte(selector), 4)) + + return selector, nil +}