Skip to content
Closed
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
114 changes: 114 additions & 0 deletions internal/helm/engine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
Copyright 2026 Flant JSC

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 helm

import (
"fmt"
"io"
stdlog "log"

"github.com/werf/nelm/pkg/helm/pkg/chart"
"github.com/werf/nelm/pkg/helm/pkg/chart/loader"
"github.com/werf/nelm/pkg/helm/pkg/chartutil"
"github.com/werf/nelm/pkg/helm/pkg/engine"
"github.com/werf/nelm/pkg/helm/pkg/werf/helmopts"
)

// EngineOption is a functional option for configuring NelmEngine.
type EngineOption func(*NelmEngine)

// NelmEngine is a reusable wrapper around werf/nelm for rendering Helm charts.
// Unlike the old Renderer, it does not own chart name / namespace / lint mode —
// those are passed per-render call. Engine-level concerns (log suppression,
// default options, DepDownloader) are injected via functional options.
type NelmEngine struct {
chartLoadOpts helmopts.ChartLoadOptions
}

// NewEngine creates a NelmEngine with optional overrides.
func NewEngine(opts ...EngineOption) *NelmEngine {
e := &NelmEngine{
chartLoadOpts: helmopts.ChartLoadOptions{
DefaultChartAPIVersion: "v2",
DefaultChartVersion: "0.2.0",
DepDownloader: &lintDepDownloader{},
},
}
for _, o := range opts {
o(e)
}
return e

Check failure on line 54 in internal/helm/engine.go

View workflow job for this annotation

GitHub Actions / golangci-lint

missing whitespace above this line (too many lines above return) (wsl_v5)
}

// WithDepDownloader overrides the default lintDepDownloader.
func WithDepDownloader(d helmopts.DepDownloader) EngineOption {
return func(e *NelmEngine) {
e.chartLoadOpts.DepDownloader = d
}
}

// WithChartLoadOption sets a ChartLoadOptions field directly.
func WithChartLoadOption(fn func(*helmopts.ChartLoadOptions)) EngineOption {
return func(e *NelmEngine) {
fn(&e.chartLoadOpts)
}
}

// LoadChart loads the chart at chartDir using nelm's loader with the engine's
// default options. name is used as DefaultChartName when the chart lacks a
// Chart.yaml. Returns the loaded chart or an error.
func (e *NelmEngine) LoadChart(chartDir, name string) (*chart.Chart, error) {
if name == "" {
return nil, fmt.Errorf("helm chart must have a name")
}

opts := helmopts.HelmOptions{
ChartLoadOpts: e.chartLoadOpts,
}
opts.ChartLoadOpts.DefaultChartName = name

// Suppress nelm's indiscriminate symlink/Chart.lock logging during load.
stdlogWriter := stdlog.Writer()
stdlog.SetOutput(io.Discard)

Check failure on line 86 in internal/helm/engine.go

View workflow job for this annotation

GitHub Actions / golangci-lint

missing whitespace above this line (no shared variables above expr) (wsl_v5)

chrt, err := loader.LoadDir(chartDir, opts)

stdlog.SetOutput(stdlogWriter)

if err != nil {
return nil, fmt.Errorf("load chart: %w", err)
}
return chrt, nil

Check failure on line 95 in internal/helm/engine.go

View workflow job for this annotation

GitHub Actions / golangci-lint

missing whitespace above this line (too many lines above return) (wsl_v5)
}

// RenderChart loads the chart at chartDir and renders it with the given values.
// name is used as DefaultChartName; lintMode controls strictness of rendering.
func (e *NelmEngine) RenderChart(chartDir, name string, values map[string]any, lintMode bool) (map[string]string, error) {
chrt, err := e.LoadChart(chartDir, name)
if err != nil {
return nil, err
}

eng := engine.Engine{LintMode: lintMode}
out, err := eng.Render(chrt, chartutil.Values(values), helmopts.HelmOptions{

Check failure on line 107 in internal/helm/engine.go

View workflow job for this annotation

GitHub Actions / golangci-lint

missing whitespace above this line (too many statements above if) (wsl_v5)
ChartLoadOpts: e.chartLoadOpts,
})
if err != nil {
return nil, err
}
return out, nil

Check failure on line 113 in internal/helm/engine.go

View workflow job for this annotation

GitHub Actions / golangci-lint

missing whitespace above this line (too many lines above return) (wsl_v5)
}
43 changes: 13 additions & 30 deletions internal/helm/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@

import (
"fmt"
"io"
stdlog "log"
"path"

"github.com/werf/nelm/pkg/helm/pkg/chart"
Expand All @@ -38,6 +36,12 @@
loader.NoChartLockWarning = ""
}

// Renderer is a convenience wrapper around NelmEngine that carries DMT-specific
// rendering concerns: chart name, namespace, lint mode, and helm_lib template
// overrides. It delegates chart loading and rendering to NelmEngine.
//
// Renderer is the stable public API for module rendering. For new consumers that
// do not need helm_lib overrides, use NelmEngine directly.
type Renderer struct {
Name string
Namespace string
Expand All @@ -61,42 +65,21 @@
return nil, fmt.Errorf("helm chart must have a name")
}

opts := helmopts.HelmOptions{
ChartLoadOpts: helmopts.ChartLoadOptions{
// deckhouse modules may omit Chart.yaml; provide sane defaults so the
// directory still loads as a chart.
DefaultChartAPIVersion: "v2",
DefaultChartName: r.Name,
DefaultChartVersion: "0.2.0",
// Nelm's chart loader calls DepDownloader.SetChartPath / Build when
// the chart has a Chart.lock with external (non-file://) dependencies.
// Leave it nil and the loader panics with a nil pointer dereference.
DepDownloader: &lintDepDownloader{},
},
}

// nelm's chart loader calls sympath.Walk which indiscriminately logs
// "found symbolic link in path: %s resolves to %s" via the standard
// log package. Deckhouse modules use symlinks for helm_lib and other
// shared resources; those messages are expected noise. Mute std log
// during the load call and restore it afterwards.
stdlogWriter := stdlog.Writer()

stdlog.SetOutput(io.Discard)

chrt, err := loader.LoadDir(chartDir, opts)

stdlog.SetOutput(stdlogWriter)
eng := NewEngine()

chrt, err := eng.LoadChart(chartDir, r.Name)
if err != nil {
return nil, fmt.Errorf("load chart: %w", err)
return nil, err
}

r.applyTemplateOverrides(chrt)

e := engine.Engine{LintMode: r.LintMode}
opts := helmopts.HelmOptions{
ChartLoadOpts: eng.chartLoadOpts,
}

e := engine.Engine{LintMode: r.LintMode}
out, err := e.Render(chrt, chartutil.Values(values), opts)

Check failure on line 82 in internal/helm/render.go

View workflow job for this annotation

GitHub Actions / golangci-lint

missing whitespace above this line (too many statements above if) (wsl_v5)
if err != nil {
return nil, err
}
Expand Down
27 changes: 27 additions & 0 deletions internal/module/helm_overrides.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
Copyright 2026 Flant JSC

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 module

// HelmLibOverrides returns the deterministic helm_lib helper stubs dmt injects
// during rendering so that image and module-name references resolve to stable
// values regardless of the helm_lib version a module ships with.
func HelmLibOverrides() map[string][]byte {
return map[string][]byte{
"_module_name.tpl": moduleNameTemplate,
"_module_image.tpl": moduleImageTemplate,
}
}
10 changes: 10 additions & 0 deletions internal/module/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type Module struct {
chart *chart.Chart
objectStore *storage.UnstructuredObjectStore
werfFile string
values map[string]any

linterConfig *pkg.LintersSettings
}
Expand Down Expand Up @@ -145,6 +146,13 @@ func (m *Module) GetModuleConfig() *pkg.LintersSettings {
return m.linterConfig
}

func (m *Module) GetValues() map[string]any {
if m == nil {
return nil
}
return m.values
}

// remapLinterSettings converts configuration settings from the config package format
// to the pkg package format, mapping both rule-level configurations and exclusion rules
// across all linter domains (Container, Image, NoCyrillic, OpenAPI, Templates, RBAC, Hooks, Module).
Expand Down Expand Up @@ -348,6 +356,7 @@ func mapTemplatesRules(linterSettings *pkg.LintersSettings, configSettings *conf
rules.EnabledModulesRule.SetLevel(globalRules.EnabledModulesRule.Impact, fallbackImpact)
rules.MountPointsRule.SetLevel(globalRules.MountPointsRule.Impact, fallbackImpact)
rules.WebhookConfigurationRule.SetLevel(globalRules.WebhookConfigurationRule.Impact, fallbackImpact)
rules.HelmRenderRule.SetLevel(globalRules.HelmRenderRule.Impact, fallbackImpact)
}

// mapOpenAPIRules configures OpenAPI linter rules
Expand Down Expand Up @@ -527,6 +536,7 @@ func NewModule(path string, vals *chartutil.Values, globalSchema *spec.Schema, r
}

module.objectStore = objectStore
module.values = schemas

werfFile, err := werf.GetWerfConfig(path)
if err != nil {
Expand Down
5 changes: 1 addition & 4 deletions internal/module/render_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ import (
// during rendering so that image and module-name references resolve to stable
// values regardless of the helm_lib version a module ships with.
func helmLibOverrides() map[string][]byte {
return map[string][]byte{
"_module_name.tpl": moduleNameTemplate,
"_module_image.tpl": moduleImageTemplate,
}
return HelmLibOverrides()
}

// RenderModuleWithValues renders the module at modulePath with the supplied
Expand Down
1 change: 1 addition & 0 deletions pkg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ type TemplatesLinterRules struct {
EnabledModulesRule RuleConfig
WebhookConfigurationRule RuleConfig
MountPointsRule RuleConfig
HelmRenderRule RuleConfig
}

type PrometheusRuleSettings struct {
Expand Down
1 change: 1 addition & 0 deletions pkg/config/global/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ type TemplatesLinterRules struct {
EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"`
WebhookConfigurationRule RuleConfig `mapstructure:"webhook-configuration-annotations"`
MountPointsRule RuleConfig `mapstructure:"mount-points"`
HelmRenderRule RuleConfig `mapstructure:"helm-render"`
}

func (c LinterConfig) IsWarn() bool {
Expand Down
1 change: 1 addition & 0 deletions pkg/config/linters_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ type TemplatesLinterRules struct {
EnabledModulesRule RuleConfig `mapstructure:"enabled-modules"`
WebhookConfigurationRule RuleConfig `mapstructure:"webhook-configuration-annotations"`
MountPointsRule RuleConfig `mapstructure:"mount-points"`
HelmRenderRule RuleConfig `mapstructure:"helm-render"`
}

type TemplatesExcludeRules struct {
Expand Down
18 changes: 5 additions & 13 deletions pkg/linters/module/rules/helmignore.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,16 @@ const (
)

// moduleTemplateExclude is the set of files and directories that belong to
// the standard Deckhouse module (Helm chart) template and therefore should
// NOT be listed in .helmignore.
// moduleTemplateExclude is the set of files and directories that belong to
// the Deckhouse module template and therefore should NOT be listed in
// .helmignore. These are either required by Helm or read by Deckhouse
// directly from the module filesystem (not from the rendered chart).
// the Deckhouse module and therefore should NOT be listed in .helmignore.
// These are either required by Helm for rendering or read by Deckhouse
// directly from the module filesystem.
var moduleTemplateExclude = map[string]bool{
// Required by Helm
// Required by Helm for chart rendering
"templates": true,
"charts": true,
"monitoring": true,
"Chart.yaml": true,
"values.yaml": true,

// Read by Deckhouse directly, not part of the Helm chart
"module.yaml": true,
".namespace": true,
"oss.yaml": true,
"rbac.yaml": true,
}

func NewHelmignoreRule(disable bool) *HelmignoreRule {
Expand Down
29 changes: 24 additions & 5 deletions pkg/linters/module/rules/helmignore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,10 @@ func TestHelmignoreRule_CheckHelmignore(t *testing.T) {
name: "multiple missing directories",
createFile: true,
fileContent: "hooks/",
directories: []string{"hooks", "images", "docs"},
directories: []string{"hooks", "images", "scripts"},
expectedErrors: []string{
"Directory 'images/' is not listed in .helmignore",
"Directory 'docs/' is not listed in .helmignore",
"Directory 'scripts/' is not listed in .helmignore",
},
},
{
Expand Down Expand Up @@ -211,10 +211,10 @@ func TestHelmignoreRule_CheckHelmignore(t *testing.T) {
{
name: "negated pattern does not count as covered",
createFile: true,
fileContent: "!hooks/",
directories: []string{"hooks"},
fileContent: "!images/",
directories: []string{"images"},
expectedErrors: []string{
"Directory 'hooks/' is not listed in .helmignore",
"Directory 'images/' is not listed in .helmignore",
},
},
{
Expand All @@ -231,6 +231,25 @@ func TestHelmignoreRule_CheckHelmignore(t *testing.T) {
directories: []string{"hooks", "charts"},
expectedErrors: []string{},
},
{
name: "monitoring directory is skipped (needed in chart)",
createFile: true,
fileContent: ".git/",
directories: []string{"monitoring"},
expectedErrors: []string{},
},
{
name: "only helm rendering dirs are skipped",
createFile: true,
fileContent: ".git/",
directories: []string{"docs", "crds", "hooks", "monitoring", "openapi", "templates", "charts"},
expectedErrors: []string{
"Directory 'docs/' is not listed in .helmignore",
"Directory 'crds/' is not listed in .helmignore",
"Directory 'hooks/' is not listed in .helmignore",
"Directory 'openapi/' is not listed in .helmignore",
},
},
{
name: "empty module root only templates",
createFile: true,
Expand Down
Loading
Loading