• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubernetes-sigs / kubebuilder / 21797329851

08 Feb 2026 11:23AM UTC coverage: 71.872% (-1.4%) from 73.272%
21797329851

Pull #5442

github

camilamacedo86
feat(helm/v2-alpha): Add Makefile targets for Helm Development
Pull Request #5442: ✨ (helm/v2-alpha): Add Makefile targets for Helm Development

0 of 70 new or added lines in 1 file covered. (0.0%)

80 existing lines in 2 files now uncovered.

6743 of 9382 relevant lines covered (71.87%)

42.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/pkg/plugins/optional/helm/v2alpha/edit.go
1
/*
2
Copyright 2025 The Kubernetes Authors.
3

4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    http://www.apache.org/licenses/LICENSE-2.0
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package v2alpha
18

19
import (
20
        "errors"
21
        "fmt"
22
        "io"
23
        "log/slog"
24
        "os"
25
        "path/filepath"
26
        "strings"
27

28
        "github.com/spf13/pflag"
29
        "go.yaml.in/yaml/v3"
30

31
        "sigs.k8s.io/kubebuilder/v4/pkg/config"
32
        cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
33
        "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
34
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
35
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
36
        "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v2alpha/scaffolds"
37
)
38

39
const (
40
        // DefaultManifestsFile is the default path for kustomize output manifests
41
        DefaultManifestsFile = "dist/install.yaml"
42
        // DefaultOutputDir is the default output directory for Helm charts
43
        DefaultOutputDir = "dist"
44
        // v1AlphaPluginKey is the deprecated v1-alpha plugin key
45
        v1AlphaPluginKey = "helm.kubebuilder.io/v1-alpha"
46
)
47

48
var _ plugin.EditSubcommand = &editSubcommand{}
49

50
type editSubcommand struct {
51
        config        config.Config
52
        force         bool
53
        manifestsFile string
54
        outputDir     string
55
}
56

57
//nolint:lll
UNCOV
58
func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
×
UNCOV
59
        subcmdMeta.Description = `Generate a Helm chart from your project's kustomize output.
×
UNCOV
60

×
NEW
61
Parses 'make build-installer' output (dist/install.yaml) and generates chart to allow easy
×
NEW
62
distribution of your project. When enabled, adds Helm helpers targets to Makefile`
×
UNCOV
63

×
UNCOV
64
        subcmdMeta.Examples = fmt.Sprintf(`# Generate Helm chart from default manifests (dist/install.yaml) to default output (dist/)
×
UNCOV
65
  %[1]s edit --plugins=%[2]s
×
UNCOV
66

×
UNCOV
67
# Generate Helm chart and overwrite existing files (useful for updates)
×
UNCOV
68
  %[1]s edit --plugins=%[2]s --force
×
UNCOV
69

×
UNCOV
70
# Generate Helm chart from a custom manifests file
×
UNCOV
71
  %[1]s edit --plugins=%[2]s --manifests=path/to/custom-install.yaml
×
UNCOV
72

×
UNCOV
73
# Generate Helm chart to a custom output directory
×
UNCOV
74
  %[1]s edit --plugins=%[2]s --output-dir=charts
×
UNCOV
75

×
UNCOV
76
# Generate from custom manifests to custom output directory
×
UNCOV
77
  %[1]s edit --plugins=%[2]s --manifests=manifests/install.yaml --output-dir=helm-charts
×
UNCOV
78

×
UNCOV
79
# Typical workflow:
×
UNCOV
80
  make build-installer  # Generate dist/install.yaml with latest changes
×
UNCOV
81
  %[1]s edit --plugins=%[2]s  # Generate/update Helm chart in dist/chart/
×
UNCOV
82

×
UNCOV
83
**NOTE**: Chart.yaml is never overwritten (contains user-managed version info).
×
UNCOV
84
Without --force, the plugin also preserves values.yaml, NOTES.txt, _helpers.tpl, .helmignore, and
×
UNCOV
85
.github/workflows/test-chart.yml. All template files in templates/ are always regenerated
×
UNCOV
86
to match your current kustomize output. Use --force to regenerate all files except Chart.yaml.
×
UNCOV
87

×
UNCOV
88
The generated chart structure mirrors your config/ directory:
×
UNCOV
89
<output>/chart/
×
UNCOV
90
├── Chart.yaml
×
UNCOV
91
├── values.yaml
×
UNCOV
92
├── .helmignore
×
UNCOV
93
└── templates/
×
UNCOV
94
    ├── NOTES.txt
×
UNCOV
95
    ├── _helpers.tpl
×
UNCOV
96
    ├── rbac/
×
UNCOV
97
    ├── manager/
×
UNCOV
98
    ├── webhook/
×
UNCOV
99
    └── ...
×
UNCOV
100
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
×
UNCOV
101
}
×
102

UNCOV
103
func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
×
UNCOV
104
        fs.BoolVar(&p.force, "force", false, "if true, regenerates all the files")
×
UNCOV
105
        fs.StringVar(&p.manifestsFile, "manifests", DefaultManifestsFile,
×
UNCOV
106
                "path to the YAML file containing Kubernetes manifests from kustomize output")
×
UNCOV
107
        fs.StringVar(&p.outputDir, "output-dir", DefaultOutputDir, "output directory for the generated Helm chart")
×
UNCOV
108
}
×
109

UNCOV
110
func (p *editSubcommand) InjectConfig(c config.Config) error {
×
UNCOV
111
        p.config = c
×
UNCOV
112
        return nil
×
UNCOV
113
}
×
114

115
func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
×
116
        // If using default manifests file, ensure it exists by running make build-installer
×
117
        if p.manifestsFile == DefaultManifestsFile {
×
118
                if err := p.ensureManifestsExist(); err != nil {
×
119
                        slog.Warn("Failed to generate default manifests file", "error", err, "file", p.manifestsFile)
×
120
                }
×
121
        }
122

123
        scaffolder := scaffolds.NewKustomizeHelmScaffolder(p.config, p.force, p.manifestsFile, p.outputDir)
×
124
        scaffolder.InjectFS(fs)
×
125
        err := scaffolder.Scaffold()
×
126
        if err != nil {
×
127
                return fmt.Errorf("error scaffolding Helm chart: %w", err)
×
128
        }
×
129

130
        // Remove deprecated v1-alpha plugin entry from PROJECT file
131
        // This must happen in Scaffold (before config is saved) to be persisted
132
        p.removeV1AlphaPluginEntry()
×
133

×
134
        // Save plugin config to PROJECT file
×
135
        key := plugin.GetPluginKeyForConfig(p.config.GetPluginChain(), Plugin{})
×
136
        canonicalKey := plugin.KeyFor(Plugin{})
×
137
        cfg := pluginConfig{}
×
NEW
138
        isFirstRun := false
×
139
        if err = p.config.DecodePluginConfig(key, &cfg); err != nil {
×
140
                switch {
×
141
                case errors.As(err, &config.UnsupportedFieldError{}):
×
142
                        // Config version doesn't support plugin metadata
×
143
                        return nil
×
144
                case errors.As(err, &config.PluginKeyNotFoundError{}):
×
NEW
145
                        // This is the first time the plugin is run
×
NEW
146
                        isFirstRun = true
×
147
                        if key != canonicalKey {
×
148
                                if err2 := p.config.DecodePluginConfig(canonicalKey, &cfg); err2 != nil {
×
149
                                        if errors.As(err2, &config.UnsupportedFieldError{}) {
×
150
                                                return nil
×
151
                                        }
×
152
                                        if !errors.As(err2, &config.PluginKeyNotFoundError{}) {
×
153
                                                return fmt.Errorf("error decoding plugin configuration: %w", err2)
×
154
                                        }
×
NEW
155
                                } else {
×
NEW
156
                                        // Found config under canonical key, not first run
×
NEW
157
                                        isFirstRun = false
×
UNCOV
158
                                }
×
159
                        }
160
                default:
×
161
                        return fmt.Errorf("error decoding plugin configuration: %w", err)
×
162
                }
163
        }
164

165
        // Update configuration with current parameters
166
        cfg.ManifestsFile = p.manifestsFile
×
167
        cfg.OutputDir = p.outputDir
×
168

×
169
        if err = p.config.EncodePluginConfig(key, cfg); err != nil {
×
170
                return fmt.Errorf("error encoding plugin configuration: %w", err)
×
171
        }
×
172

173
        // Add Helm deployment targets to Makefile only on first run
NEW
174
        if isFirstRun {
×
NEW
175
                slog.Info("adding Helm deployment targets to Makefile...")
×
NEW
176
                // Extract namespace from manifests for accurate Makefile generation
×
NEW
177
                namespace := p.extractNamespaceFromManifests()
×
NEW
178
                if err := p.addHelmMakefileTargets(namespace); err != nil {
×
NEW
179
                        slog.Warn("failed to add Helm targets to Makefile", "error", err)
×
NEW
180
                }
×
181
        }
182

UNCOV
183
        return nil
×
184
}
185

186
// ensureManifestsExist runs make build-installer to generate the default manifests file
187
func (p *editSubcommand) ensureManifestsExist() error {
×
188
        slog.Info("Generating default manifests file", "file", p.manifestsFile)
×
189

×
190
        // Run the required make targets to generate the manifests file
×
191
        targets := []string{"manifests", "generate", "build-installer"}
×
192
        for _, target := range targets {
×
193
                if err := util.RunCmd(fmt.Sprintf("Running make %s", target), "make", target); err != nil {
×
194
                        return fmt.Errorf("make %s failed: %w", target, err)
×
195
                }
×
196
        }
197

198
        // Verify the file was created
199
        if _, err := os.Stat(p.manifestsFile); err != nil {
×
200
                return fmt.Errorf("manifests file %s was not created: %w", p.manifestsFile, err)
×
201
        }
×
202

203
        slog.Info("Successfully generated manifests file", "file", p.manifestsFile)
×
204
        return nil
×
205
}
206

207
// PostScaffold automatically uncomments cert-manager installation when webhooks are present
UNCOV
208
func (p *editSubcommand) PostScaffold() error {
×
UNCOV
209
        hasWebhooks := hasWebhooksWith(p.config)
×
UNCOV
210

×
UNCOV
211
        if hasWebhooks {
×
212
                workflowFile := filepath.Join(".github", "workflows", "test-chart.yml")
×
213
                if _, err := os.Stat(workflowFile); err != nil {
×
214
                        slog.Info(
×
215
                                "Workflow file not found, unable to uncomment cert-manager installation",
×
216
                                "error", err,
×
217
                                "file", workflowFile,
×
218
                        )
×
219
                        return nil
×
220
                }
×
221
                target := `
×
222
#      - name: Install cert-manager via Helm (wait for readiness)
×
223
#        run: |
×
224
#          helm repo add jetstack https://charts.jetstack.io
×
225
#          helm repo update
×
226
#          helm install cert-manager jetstack/cert-manager \
×
227
#            --namespace cert-manager \
×
228
#            --create-namespace \
×
229
#            --set crds.enabled=true \
×
230
#            --wait \
×
231
#            --timeout 300s`
×
232
                if err := util.UncommentCode(workflowFile, target, "#"); err != nil {
×
233
                        hasUncommented, errCheck := util.HasFileContentWith(workflowFile, "- name: Install cert-manager via Helm")
×
234
                        if !hasUncommented || errCheck != nil {
×
235
                                slog.Warn("Failed to uncomment cert-manager installation in workflow file", "error", err, "file", workflowFile)
×
236
                        }
×
237
                } else {
×
238
                        target = `# TODO: Uncomment if cert-manager is enabled`
×
239
                        _ = util.ReplaceInFile(workflowFile, target, "")
×
240
                }
×
241
        }
UNCOV
242
        return nil
×
243
}
244

245
// addHelmMakefileTargets appends Helm deployment targets to the Makefile if they don't already exist
NEW
246
func (p *editSubcommand) addHelmMakefileTargets(namespace string) error {
×
NEW
247
        makefilePath := "Makefile"
×
NEW
248
        if _, err := os.Stat(makefilePath); os.IsNotExist(err) {
×
NEW
249
                return fmt.Errorf("makefile not found")
×
NEW
250
        }
×
251

252
        // Get the Helm Makefile targets
NEW
253
        helmTargets := getHelmMakefileTargets(p.config.GetProjectName(), namespace, p.outputDir)
×
NEW
254

×
NEW
255
        // Append the targets if they don't already exist
×
NEW
256
        if err := util.AppendCodeIfNotExist(makefilePath, helmTargets); err != nil {
×
NEW
257
                return fmt.Errorf("failed to append Helm targets to Makefile: %w", err)
×
NEW
258
        }
×
259

NEW
260
        slog.Info("added Helm deployment targets to Makefile",
×
NEW
261
                "targets", "helm-deploy, helm-uninstall, helm-status, helm-history, helm-rollback")
×
NEW
262
        return nil
×
263
}
264

265
// extractNamespaceFromManifests parses the manifests file to extract the manager namespace.
266
// Returns projectName-system if manifests don't exist or namespace not found.
NEW
267
func (p *editSubcommand) extractNamespaceFromManifests() string {
×
NEW
268
        // Default to project-name-system pattern
×
NEW
269
        defaultNamespace := p.config.GetProjectName() + "-system"
×
NEW
270

×
NEW
271
        // If manifests file doesn't exist, use default
×
NEW
272
        if _, err := os.Stat(p.manifestsFile); os.IsNotExist(err) {
×
NEW
273
                return defaultNamespace
×
NEW
274
        }
×
275

276
        // Parse the manifests to get the namespace
NEW
277
        file, err := os.Open(p.manifestsFile)
×
NEW
278
        if err != nil {
×
NEW
279
                return defaultNamespace
×
NEW
280
        }
×
NEW
281
        defer func() {
×
NEW
282
                _ = file.Close()
×
NEW
283
        }()
×
284

285
        // Parse YAML documents looking for the manager Deployment
NEW
286
        decoder := yaml.NewDecoder(file)
×
NEW
287
        for {
×
NEW
288
                var doc map[string]any
×
NEW
289
                if err := decoder.Decode(&doc); err != nil {
×
NEW
290
                        if err == io.EOF {
×
NEW
291
                                break
×
292
                        }
NEW
293
                        continue
×
294
                }
295

296
                // Check if this is a Deployment (manager)
NEW
297
                if kind, ok := doc["kind"].(string); ok && kind == "Deployment" {
×
NEW
298
                        if metadata, ok := doc["metadata"].(map[string]any); ok {
×
NEW
299
                                // Check if it's the manager deployment
×
NEW
300
                                if name, ok := metadata["name"].(string); ok && strings.Contains(name, "controller-manager") {
×
NEW
301
                                        // Extract namespace from the manager Deployment
×
NEW
302
                                        if namespace, ok := metadata["namespace"].(string); ok && namespace != "" {
×
NEW
303
                                                return namespace
×
NEW
304
                                        }
×
305
                                }
306
                        }
307
                }
308
        }
309

310
        // Fallback to default if manager Deployment not found
NEW
311
        return defaultNamespace
×
312
}
313

314
// getHelmMakefileTargets returns the Helm Makefile targets as a string
315
// following the same patterns as the existing Makefile deployment section
NEW
316
func getHelmMakefileTargets(projectName, namespace, outputDir string) string {
×
NEW
317
        if outputDir == "" {
×
NEW
318
                outputDir = "dist"
×
NEW
319
        }
×
320

321
        // Use the project name as default for release name
NEW
322
        release := projectName
×
NEW
323

×
NEW
324
        return helmMakefileTemplate(namespace, release, outputDir)
×
325
}
326

327
// helmMakefileTemplate returns the Helm deployment section template
328
// This follows the same pattern as the Kustomize deployment section in the Go plugin
329
const helmMakefileTemplateFormat = `
330
##@ Helm Deployment
331

332
## Helm binary to use for deploying the chart
333
HELM ?= helm
334
## Namespace to deploy the Helm release
335
HELM_NAMESPACE ?= %s
336
## Name of the Helm release
337
HELM_RELEASE ?= %s
338
## Path to the Helm chart directory
339
HELM_CHART_DIR ?= %s/chart
340
## Additional arguments to pass to helm commands
341
HELM_EXTRA_ARGS ?=
342

343
.PHONY: helm-deploy
344
helm-deploy: ## Deploy manager to the K8s cluster via Helm. Specify an image with IMG.
345
        $(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART_DIR) \
346
                --namespace $(HELM_NAMESPACE) \
347
                --create-namespace \
348
                --set manager.image.repository=$${IMG%%:*} \
349
                --set manager.image.tag=$${IMG##*:} \
350
                --wait \
351
                --timeout 5m \
352
                $(HELM_EXTRA_ARGS)
353

354
.PHONY: helm-uninstall
355
helm-uninstall: ## Uninstall the Helm release from the K8s cluster.
356
        $(HELM) uninstall $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
357

358
.PHONY: helm-status
359
helm-status: ## Show Helm release status.
360
        $(HELM) status $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
361

362
.PHONY: helm-history
363
helm-history: ## Show Helm release history.
364
        $(HELM) history $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
365

366
.PHONY: helm-rollback
367
helm-rollback: ## Rollback to previous Helm release.
368
        $(HELM) rollback $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
369
`
370

NEW
371
func helmMakefileTemplate(namespace, release, outputDir string) string {
×
NEW
372
        return fmt.Sprintf(helmMakefileTemplateFormat, namespace, release, outputDir)
×
NEW
373
}
×
374

UNCOV
375
func hasWebhooksWith(c config.Config) bool {
×
UNCOV
376
        resources, err := c.GetResources()
×
UNCOV
377
        if err != nil {
×
378
                return false
×
379
        }
×
380

UNCOV
381
        for _, res := range resources {
×
382
                if res.HasDefaultingWebhook() || res.HasValidationWebhook() || res.HasConversionWebhook() {
×
383
                        return true
×
384
                }
×
385
        }
386

UNCOV
387
        return false
×
388
}
389

390
// removeV1AlphaPluginEntry removes the deprecated helm.kubebuilder.io/v1-alpha plugin entry.
391
// This must be called from Scaffold (before config is saved) for changes to be persisted.
UNCOV
392
func (p *editSubcommand) removeV1AlphaPluginEntry() {
×
UNCOV
393
        // Only attempt to remove if using v3 config (which supports plugin configs)
×
UNCOV
394
        cfg, ok := p.config.(*cfgv3.Cfg)
×
UNCOV
395
        if !ok {
×
396
                return
×
397
        }
×
398

399
        // Check if v1-alpha plugin entry exists
UNCOV
400
        if cfg.Plugins == nil {
×
UNCOV
401
                return
×
UNCOV
402
        }
×
403

UNCOV
404
        if _, exists := cfg.Plugins[v1AlphaPluginKey]; exists {
×
UNCOV
405
                delete(cfg.Plugins, v1AlphaPluginKey)
×
UNCOV
406
                slog.Info("removed deprecated v1-alpha plugin entry")
×
UNCOV
407
        }
×
408
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc