Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/helmify/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func ReadFlags() (config.Config, error) {
flag.BoolVar(&result.PreserveNs, "preserve-ns", false, "Use the object's original namespace instead of adding all the resources to a common namespace.")
flag.BoolVar(&result.AddWebhookOption, "add-webhook-option", false, "Allows the user to add webhook option in values.yaml.")
flag.BoolVar(&result.OptionalCRDs, "optional-crds", false, "Enable optional CRD installation through values. (cannot be used with 'crd-dir')")
flag.BoolVar(&result.AddChecksumAnnotations, "add-checksum-annotations", false, "Add checksum annotations to pod templates for referenced ConfigMaps and Secrets. Triggers rolling restarts on config changes.")

flag.Parse()
if h || help {
Expand Down
1 change: 1 addition & 0 deletions cmd/helmify/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ func TestReadFlags_DefaultValuesMatchFlagDefaults(t *testing.T) {
{"original-name", func(cfg config.Config) bool { return cfg.OriginalName }},
{"preserve-ns", func(cfg config.Config) bool { return cfg.PreserveNs }},
{"add-webhook-option", func(cfg config.Config) bool { return cfg.AddWebhookOption }},
{"add-checksum-annotations", func(cfg config.Config) bool { return cfg.AddChecksumAnnotations }},
}

for _, tt := range stringTests {
Expand Down
29 changes: 29 additions & 0 deletions pkg/app/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ func (c *appContext) CreateHelm(stop <-chan struct{}) error {
"ChartName": c.appMeta.ChartName(),
"Namespace": c.appMeta.Namespace(),
}).Info("creating a chart")

if c.config.AddChecksumAnnotations {
c.precomputeConfigFileNames()
}

var templates []helmify.Template
var filenames []string
for i, obj := range c.objects {
Expand Down Expand Up @@ -103,3 +108,27 @@ func (c *appContext) process(obj *unstructured.Unstructured) (helmify.Template,
_, t, err := c.defaultProcessor.Process(c.appMeta, obj)
return t, err
}

// precomputeConfigFileNames builds the ConfigMap/Secret name → filename maps
// and stores them on appMeta so processors can generate checksum annotations.
// For objects from file input, the input filename is used. For stdin input,
// the filename follows the processor convention: trimmedName + ".yaml".
func (c *appContext) precomputeConfigFileNames() {
configMapFiles := map[string]string{}
secretFiles := map[string]string{}
for i, obj := range c.objects {
name := obj.GetName()
filename := c.fileNames[i]
if filename == "" {
filename = c.appMeta.TrimName(name) + ".yaml"
}
switch obj.GroupVersionKind() {
case metadata.ConfigMapGVK:
configMapFiles[name] = filename
case metadata.SecretGVK:
secretFiles[name] = filename
}
}
c.appMeta.SetConfigMapFiles(configMapFiles)
c.appMeta.SetSecretFiles(secretFiles)
}
61 changes: 61 additions & 0 deletions pkg/app/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package app

import (
"testing"

"github.com/arttor/helmify/internal"
"github.com/arttor/helmify/pkg/config"
"github.com/stretchr/testify/assert"
)

func Test_precomputeConfigFileNames(t *testing.T) {
t.Run("maps configmaps and secrets with input filenames", func(t *testing.T) {
conf := config.Config{ChartName: "my-app", AddChecksumAnnotations: true}
ctx := New(conf, nil)

cmObj := internal.GenerateObj(`apiVersion: v1
kind: ConfigMap
metadata:
name: my-app-config`)
secObj := internal.GenerateObj(`apiVersion: v1
kind: Secret
metadata:
name: my-app-secret`)
deplObj := internal.GenerateObj(`apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app-web`)

ctx.Add(cmObj, "configmap.yaml")
ctx.Add(secObj, "secret.yaml")
ctx.Add(deplObj, "deployment.yaml")

ctx.precomputeConfigFileNames()

assert.Equal(t, "configmap.yaml", ctx.appMeta.ConfigMapFiles()["my-app-config"])
assert.Equal(t, "secret.yaml", ctx.appMeta.SecretFiles()["my-app-secret"])
assert.Empty(t, ctx.appMeta.ConfigMapFiles()["my-app-web"])
})

t.Run("uses trimmed name for stdin input", func(t *testing.T) {
conf := config.Config{ChartName: "my-app", AddChecksumAnnotations: true}
ctx := New(conf, nil)

cmObj := internal.GenerateObj(`apiVersion: v1
kind: ConfigMap
metadata:
name: my-app-config`)
secObj := internal.GenerateObj(`apiVersion: v1
kind: Secret
metadata:
name: my-app-secret`)

ctx.Add(cmObj, "") // empty filename = stdin
ctx.Add(secObj, "")

ctx.precomputeConfigFileNames()

assert.Equal(t, "config.yaml", ctx.appMeta.ConfigMapFiles()["my-app-config"])
assert.Equal(t, "secret.yaml", ctx.appMeta.SecretFiles()["my-app-secret"])
})
}
3 changes: 3 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ type Config struct {
AddWebhookOption bool
// OptionalCRDs - Enable optional CRD installation through values.
OptionalCRDs bool
// AddChecksumAnnotations - Add checksum annotations for ConfigMaps and Secrets referenced by workloads.
// This triggers rolling restarts when referenced ConfigMap/Secret content changes.
AddChecksumAnnotations bool
}

func (c *Config) Validate() error {
Expand Down
12 changes: 12 additions & 0 deletions pkg/helmify/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,16 @@ type AppMetadata interface {
TrimName(objName string) string

Config() config.Config

// HasConfigMap returns true if a ConfigMap with the given name is part of the chart.
HasConfigMap(name string) bool
// HasSecret returns true if a Secret with the given name is part of the chart.
HasSecret(name string) bool

// ConfigMapFiles returns a map of ConfigMap object names to their template filenames.
// Only populated when AddChecksumAnnotations is enabled.
ConfigMapFiles() map[string]string
// SecretFiles returns a map of Secret object names to their template filenames.
// Only populated when AddChecksumAnnotations is enabled.
SecretFiles() map[string]string
}
77 changes: 72 additions & 5 deletions pkg/metadata/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,38 @@ var crdGVK = schema.GroupVersionKind{
Kind: "CustomResourceDefinition",
}

// ConfigMapGVK is the GroupVersionKind for core/v1 ConfigMap.
var ConfigMapGVK = schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "ConfigMap",
}

// SecretGVK is the GroupVersionKind for core/v1 Secret.
var SecretGVK = schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "Secret",
}

func New(conf config.Config) *Service {
return &Service{names: make(map[string]struct{}), conf: conf}
return &Service{
names: make(map[string]struct{}),
configMapNames: make(map[string]struct{}),
secretNames: make(map[string]struct{}),
conf: conf,
}
}

type Service struct {
commonPrefix string
namespace string
names map[string]struct{}
conf config.Config
commonPrefix string
namespace string
names map[string]struct{}
configMapNames map[string]struct{}
secretNames map[string]struct{}
configMapFiles map[string]string
secretFiles map[string]string
conf config.Config
}

func (a *Service) Config() config.Config {
Expand All @@ -58,6 +81,12 @@ var _ helmify.AppMetadata = &Service{}
// other app meta information.
func (a *Service) Load(obj *unstructured.Unstructured) {
a.names[obj.GetName()] = struct{}{}
switch obj.GroupVersionKind() {
case ConfigMapGVK:
a.configMapNames[obj.GetName()] = struct{}{}
case SecretGVK:
a.secretNames[obj.GetName()] = struct{}{}
}
a.commonPrefix = detectCommonPrefix(obj, a.commonPrefix)
objNs := extractAppNamespace(obj)
if objNs == "" {
Expand Down Expand Up @@ -94,6 +123,44 @@ func (a *Service) TemplatedName(name string) string {
return fmt.Sprintf(nameTeml, a.conf.ChartName, name)
}

// HasConfigMap returns true if a ConfigMap with the given name is part of the chart.
func (a *Service) HasConfigMap(name string) bool {
if a.configMapNames == nil {
return false
}
_, ok := a.configMapNames[name]
return ok
}

// HasSecret returns true if a Secret with the given name is part of the chart.
func (a *Service) HasSecret(name string) bool {
if a.secretNames == nil {
return false
}
_, ok := a.secretNames[name]
return ok
}

// SetConfigMapFiles sets the map of ConfigMap names to template filenames.
func (a *Service) SetConfigMapFiles(files map[string]string) {
a.configMapFiles = files
}

// SetSecretFiles sets the map of Secret names to template filenames.
func (a *Service) SetSecretFiles(files map[string]string) {
a.secretFiles = files
}

// ConfigMapFiles returns the map of ConfigMap names to template filenames.
func (a *Service) ConfigMapFiles() map[string]string {
return a.configMapFiles
}

// SecretFiles returns the map of Secret names to template filenames.
func (a *Service) SecretFiles() map[string]string {
return a.secretFiles
}

func (a *Service) TemplatedString(str string) string {
name := a.TrimName(str)
return fmt.Sprintf(nameTeml, a.conf.ChartName, name)
Expand Down
26 changes: 26 additions & 0 deletions pkg/metadata/metadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,32 @@ func Test_Service(t *testing.T) {
})
}

func Test_HasConfigMapAndSecret(t *testing.T) {
t.Run("tracks configmaps", func(t *testing.T) {
svc := New(config.Config{})
svc.Load(internal.GenerateObj(`apiVersion: v1
kind: ConfigMap
metadata:
name: my-config
namespace: ns`))
assert.True(t, svc.HasConfigMap("my-config"))
assert.False(t, svc.HasConfigMap("other-config"))
assert.False(t, svc.HasSecret("my-config"))
})
t.Run("tracks secrets", func(t *testing.T) {
svc := New(config.Config{})
svc.Load(createRes("my-secret", "ns"))
assert.True(t, svc.HasSecret("my-secret"))
assert.False(t, svc.HasSecret("other-secret"))
assert.False(t, svc.HasConfigMap("my-secret"))
})
t.Run("nil maps safe", func(t *testing.T) {
svc := &Service{}
assert.False(t, svc.HasConfigMap("anything"))
assert.False(t, svc.HasSecret("anything"))
})
}

func createRes(name, ns string) *unstructured.Unstructured {
objYaml := fmt.Sprintf(res, name, ns)
return internal.GenerateObj(objYaml)
Expand Down
24 changes: 19 additions & 5 deletions pkg/processor/daemonset/daemonset.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,27 @@ func (d daemonset) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstru
podLabels += fmt.Sprintf("\n {{- include \"%s.selectorLabels\" . | nindent 8 }}", appMeta.ChartName())

podAnnotations := ""
if appMeta.Config().AddChecksumAnnotations {
checksumAnns := pod.ChecksumAnnotations(appMeta, dae.Spec.Template.Spec, appMeta.ConfigMapFiles(), appMeta.SecretFiles(), 6)
if checksumAnns != "" {
podAnnotations = "\n" + checksumAnns
}
}
if len(dae.Spec.Template.ObjectMeta.Annotations) != 0 {
podAnnotations, err = yamlformat.Marshal(map[string]interface{}{"annotations": dae.Spec.Template.ObjectMeta.Annotations}, 6)
if err != nil {
return true, nil, err
existingAnns, err2 := yamlformat.Marshal(map[string]interface{}{"annotations": dae.Spec.Template.ObjectMeta.Annotations}, 6)
if err2 != nil {
return true, nil, err2
}
if podAnnotations == "" {
podAnnotations = "\n" + existingAnns
} else {
for _, line := range strings.Split(existingAnns, "\n") {
if strings.TrimSpace(line) == "annotations:" {
continue
}
podAnnotations += "\n" + line
}
}

podAnnotations = "\n" + podAnnotations
}

nameCamel := strcase.ToLowerCamel(name)
Expand Down
1 change: 1 addition & 0 deletions pkg/processor/daemonset/daemonset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,4 @@ func Test_daemonset_Process(t *testing.T) {
assert.Equal(t, false, processed)
})
}

25 changes: 20 additions & 5 deletions pkg/processor/deployment/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,28 @@ func (d deployment) Process(appMeta helmify.AppMetadata, obj *unstructured.Unstr
podLabels += fmt.Sprintf("\n {{- include \"%s.selectorLabels\" . | nindent 8 }}", appMeta.ChartName())

podAnnotations := ""
if appMeta.Config().AddChecksumAnnotations {
checksumAnns := pod.ChecksumAnnotations(appMeta, depl.Spec.Template.Spec, appMeta.ConfigMapFiles(), appMeta.SecretFiles(), 6)
if checksumAnns != "" {
podAnnotations = "\n" + checksumAnns
}
}
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
existingAnns, err2 := yamlformat.Marshal(map[string]interface{}{"annotations": depl.Spec.Template.ObjectMeta.Annotations}, 6)
if err2 != nil {
return true, nil, err2
}
if podAnnotations == "" {
podAnnotations = "\n" + existingAnns
} else {
// Append existing annotation values under the annotations: key already created by checksums.
for _, line := range strings.Split(existingAnns, "\n") {
if strings.TrimSpace(line) == "annotations:" {
continue
}
podAnnotations += "\n" + line
}
}

podAnnotations = "\n" + podAnnotations
}

nameCamel := strcase.ToLowerCamel(name)
Expand Down
Loading