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

kubernetes-sigs / kubebuilder / 26982023840

04 Jun 2026 09:55PM UTC coverage: 82.478% (+0.02%) from 82.459%
26982023840

Pull #5726

github

camilamacedo86
fix(helm/v2-alpha): Respect --force for default ServiceMonitor file
Pull Request #5726: 🐛 fix(helm/v2-alpha): Respect --force for default ServiceMonitor file

7 of 7 new or added lines in 2 files covered. (100.0%)

52 existing lines in 1 file now uncovered.

7809 of 9468 relevant lines covered (82.48%)

80.93 hits per line

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

43.32
/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/internal/common"
37
        "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v2alpha/scaffolds"
38
)
39

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

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

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

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

1✔
60
Parses 'make build-installer' output (dist/install.yaml) and generates chart to allow easy
1✔
61
distribution of your project. It also scaffolds default ServiceMonitor and NetworkPolicy files
1✔
62
when the kustomize output does not provide them. When enabled, adds Helm helpers targets to Makefile`
1✔
63

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

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

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

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

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

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

1✔
83
**NOTE**: Chart.yaml is never overwritten (contains user-managed version info).
1✔
84
Without --force, the plugin also preserves values.yaml, NOTES.txt, _helpers.tpl, .helmignore,
1✔
85
.github/workflows/test-chart.yml, network-policy/allow-metrics-traffic.yaml,
1✔
86
network-policy/allow-webhook-traffic.yaml, and
1✔
87
prometheus/controller-manager-metrics-monitor.yaml.
1✔
88
All other files in templates/ are always regenerated to match your current
1✔
89
kustomize output. Use --force to regenerate all files except Chart.yaml.
1✔
90

1✔
91
The generated chart structure mirrors your config/ directory:
1✔
92
<output>/chart/
1✔
93
├── Chart.yaml
1✔
94
├── values.yaml
1✔
95
├── .helmignore
1✔
96
└── templates/
1✔
97
    ├── NOTES.txt
1✔
98
    ├── _helpers.tpl
1✔
99
    ├── rbac/
1✔
100
    ├── manager/
1✔
101
    ├── webhook/
1✔
102
    ├── network-policy/
1✔
103
    ├── prometheus/
1✔
104
    └── ...
1✔
105
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
1✔
106
}
1✔
107

108
func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
1✔
109
        fs.BoolVar(&p.force, "force", false,
1✔
110
                "If set, regenerate preserved files except Chart.yaml (values.yaml, NOTES.txt, _helpers.tpl, "+
1✔
111
                        ".helmignore, test-chart.yml, network-policy/allow-metrics-traffic.yaml, "+
1✔
112
                        "network-policy/allow-webhook-traffic.yaml, "+
1✔
113
                        "prometheus/controller-manager-metrics-monitor.yaml)")
1✔
114
        fs.StringVar(&p.manifestsFile, "manifests", DefaultManifestsFile,
1✔
115
                "Path to the YAML file containing Kubernetes manifests from kustomize output "+
1✔
116
                        "(e.g., dist/install.yaml). Defaults to dist/install.yaml if unset")
1✔
117
        fs.StringVar(&p.outputDir, "output-dir", common.DefaultOutputDir,
1✔
118
                "Output directory for the generated Helm chart (e.g., charts). Defaults to dist if unset")
1✔
119
}
1✔
120

121
func (p *editSubcommand) InjectConfig(c config.Config) error {
1✔
122
        p.config = c
1✔
123
        return nil
1✔
124
}
1✔
125

UNCOV
126
func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
×
127
        // If using default manifests file, ensure it exists by running make build-installer
×
128
        if p.manifestsFile == DefaultManifestsFile {
×
129
                if err := p.ensureManifestsExist(); err != nil {
×
130
                        slog.Warn("Failed to generate default manifests file", "error", err, "file", p.manifestsFile)
×
131
                }
×
132
        }
133

UNCOV
134
        scaffolder := scaffolds.NewChartScaffolder(p.config, p.force, p.manifestsFile, p.outputDir)
×
UNCOV
135
        scaffolder.InjectFS(fs)
×
136
        err := scaffolder.Scaffold()
×
137
        if err != nil {
×
138
                return fmt.Errorf("error scaffolding Helm chart: %w", err)
×
139
        }
×
140

141
        // Remove deprecated v1-alpha plugin entry from PROJECT file
142
        // This must happen in Scaffold (before config is saved) to be persisted
143
        p.removeV1AlphaPluginEntry()
×
144

×
145
        // Save plugin config to PROJECT file
×
146
        key := plugin.GetPluginKeyForConfig(p.config.GetPluginChain(), Plugin{})
×
147
        canonicalKey := plugin.KeyFor(Plugin{})
×
148
        cfg := pluginConfig{}
×
149
        isFirstRun := false
×
150
        if err = p.config.DecodePluginConfig(key, &cfg); err != nil {
×
151
                switch {
×
152
                case errors.As(err, &config.UnsupportedFieldError{}):
×
153
                        // Config version doesn't support plugin metadata
×
154
                        return nil
×
155
                case errors.As(err, &config.PluginKeyNotFoundError{}):
×
156
                        // This is the first time the plugin is run
×
157
                        isFirstRun = true
×
158
                        if key != canonicalKey {
×
159
                                if err2 := p.config.DecodePluginConfig(canonicalKey, &cfg); err2 != nil {
×
160
                                        if errors.As(err2, &config.UnsupportedFieldError{}) {
×
161
                                                return nil
×
162
                                        }
×
UNCOV
163
                                        if !errors.As(err2, &config.PluginKeyNotFoundError{}) {
×
164
                                                return fmt.Errorf("error decoding plugin configuration: %w", err2)
×
165
                                        }
×
UNCOV
166
                                } else {
×
UNCOV
167
                                        // Found config under canonical key, not first run
×
UNCOV
168
                                        isFirstRun = false
×
UNCOV
169
                                }
×
170
                        }
171
                default:
×
172
                        return fmt.Errorf("error decoding plugin configuration: %w", err)
×
173
                }
174
        }
175

176
        // Update configuration with current parameters
UNCOV
177
        cfg.ManifestsFile = p.manifestsFile
×
178
        cfg.OutputDir = p.outputDir
×
179

×
180
        if err = p.config.EncodePluginConfig(key, cfg); err != nil {
×
181
                return fmt.Errorf("error encoding plugin configuration: %w", err)
×
182
        }
×
183

184
        // Add Helm deployment targets to Makefile only on first run
UNCOV
185
        if isFirstRun {
×
UNCOV
186
                slog.Info("adding Helm deployment targets to Makefile...")
×
187
                // Extract namespace from manifests for accurate Makefile generation
×
UNCOV
188
                namespace := p.extractNamespaceFromManifests()
×
UNCOV
189
                if err := p.addHelmMakefileTargets(namespace); err != nil {
×
190
                        slog.Warn("failed to add Helm targets to Makefile", "error", err)
×
191
                }
×
192
        }
193

194
        return nil
×
195
}
196

197
func (p *editSubcommand) ensureManifestsExist() error {
×
198
        slog.Info("Generating default manifests file", "file", p.manifestsFile)
×
UNCOV
199

×
UNCOV
200
        // Run the required make targets to generate the manifests file
×
UNCOV
201
        targets := []string{"manifests", "generate", "build-installer"}
×
202
        for _, target := range targets {
×
203
                if err := util.RunCmd(fmt.Sprintf("Running make %s", target), "make", target); err != nil {
×
204
                        return fmt.Errorf("make %s failed: %w", target, err)
×
UNCOV
205
                }
×
206
        }
207

208
        // Verify the file was created
UNCOV
209
        if _, err := os.Stat(p.manifestsFile); err != nil {
×
UNCOV
210
                return fmt.Errorf("manifests file %s was not created: %w", p.manifestsFile, err)
×
UNCOV
211
        }
×
212

UNCOV
213
        slog.Info("Successfully generated manifests file", "file", p.manifestsFile)
×
214
        return nil
×
215
}
216

217
func (p *editSubcommand) PostScaffold() error {
2✔
218
        hasWebhooks := hasWebhooksWith(p.config)
2✔
219

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

254
func (p *editSubcommand) addHelmMakefileTargets(namespace string) error {
3✔
255
        makefilePath := "Makefile"
3✔
256
        if _, err := os.Stat(makefilePath); os.IsNotExist(err) {
4✔
257
                return fmt.Errorf("makefile not found")
1✔
258
        }
1✔
259

260
        // Get the Helm Makefile targets
261
        helmTargets := getHelmMakefileTargets(p.config.GetProjectName(), namespace, p.outputDir)
2✔
262

2✔
263
        // Append the targets if they don't already exist
2✔
264
        if err := util.AppendCodeIfNotExist(makefilePath, helmTargets); err != nil {
2✔
UNCOV
265
                return fmt.Errorf("failed to append Helm targets to Makefile: %w", err)
×
UNCOV
266
        }
×
267

268
        slog.Info("added Helm deployment targets to Makefile",
2✔
269
                "targets", "helm-deploy, helm-uninstall, helm-status, helm-history, helm-rollback")
2✔
270
        return nil
2✔
271
}
272

273
// extractNamespaceFromManifests parses the manifests file to extract the manager namespace.
274
// Returns projectName-system if manifests don't exist or namespace not found.
275
func (p *editSubcommand) extractNamespaceFromManifests() string {
×
UNCOV
276
        // Default to project-name-system pattern
×
UNCOV
277
        defaultNamespace := p.config.GetProjectName() + "-system"
×
278

×
279
        // If manifests file doesn't exist, use default
×
280
        if _, err := os.Stat(p.manifestsFile); os.IsNotExist(err) {
×
281
                return defaultNamespace
×
282
        }
×
283

284
        // Parse the manifests to get the namespace
UNCOV
285
        file, err := os.Open(p.manifestsFile)
×
UNCOV
286
        if err != nil {
×
287
                return defaultNamespace
×
288
        }
×
289
        defer func() {
×
290
                _ = file.Close()
×
291
        }()
×
292

293
        // Parse YAML documents looking for the manager Deployment
294
        decoder := yaml.NewDecoder(file)
×
UNCOV
295
        for {
×
UNCOV
296
                var doc map[string]any
×
UNCOV
297
                if err := decoder.Decode(&doc); err != nil {
×
298
                        if err == io.EOF {
×
299
                                break
×
300
                        }
301
                        continue
×
302
                }
303

304
                // Check if this is a Deployment (manager)
305
                if kind, ok := doc["kind"].(string); ok && kind == "Deployment" {
×
UNCOV
306
                        if metadata, ok := doc["metadata"].(map[string]any); ok {
×
UNCOV
307
                                // Check if it's the manager deployment
×
UNCOV
308
                                if name, ok := metadata["name"].(string); ok && strings.Contains(name, "controller-manager") {
×
UNCOV
309
                                        // Extract namespace from the manager Deployment
×
UNCOV
310
                                        if namespace, ok := metadata["namespace"].(string); ok && namespace != "" {
×
UNCOV
311
                                                return namespace
×
312
                                        }
×
313
                                }
314
                        }
315
                }
316
        }
317

318
        // Fallback to default if manager Deployment not found
319
        return defaultNamespace
×
320
}
321

322
// getHelmMakefileTargets returns the Helm Makefile targets as a string
323
// following the same patterns as the existing Makefile deployment section
324
func getHelmMakefileTargets(projectName, namespace, outputDir string) string {
5✔
325
        if outputDir == "" {
5✔
UNCOV
326
                outputDir = "dist"
×
UNCOV
327
        }
×
328

329
        // Use the project name as default for release name
330
        release := projectName
5✔
331

5✔
332
        return helmMakefileTemplate(namespace, release, outputDir)
5✔
333
}
334

335
// helmMakefileTemplate returns the Helm deployment section template
336
// This follows the same pattern as the Kustomize deployment section in the Go plugin
337
const helmMakefileTemplateFormat = `
338
##@ Helm Deployment
339

340
## Helm binary to use for deploying the chart
341
HELM ?= helm
342
## Namespace to deploy the Helm release
343
HELM_NAMESPACE ?= %s
344
## Name of the Helm release
345
HELM_RELEASE ?= %s
346
## Path to the Helm chart directory
347
HELM_CHART_DIR ?= %s/chart
348
## Additional arguments to pass to helm commands
349
HELM_EXTRA_ARGS ?=
350

351
.PHONY: install-helm
352
install-helm: ## Install the latest version of Helm.
353
        @command -v $(HELM) >/dev/null 2>&1 || { \
354
                echo "Installing Helm..." && \
355
                curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash; \
356
        }
357

358
.PHONY: helm-deploy
359
helm-deploy: install-helm ## Deploy manager to the K8s cluster via Helm. Specify an image with IMG.
360
        $(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART_DIR) \
361
                --namespace $(HELM_NAMESPACE) \
362
                --create-namespace \
363
                --set manager.image.repository=$${IMG%%:*} \
364
                --set manager.image.tag=$${IMG##*:} \
365
                --wait \
366
                --timeout 5m \
367
                $(HELM_EXTRA_ARGS)
368

369
.PHONY: helm-uninstall
370
helm-uninstall: ## Uninstall the Helm release from the K8s cluster.
371
        $(HELM) uninstall $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
372

373
.PHONY: helm-status
374
helm-status: ## Show Helm release status.
375
        $(HELM) status $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
376

377
.PHONY: helm-history
378
helm-history: ## Show Helm release history.
379
        $(HELM) history $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
380

381
.PHONY: helm-rollback
382
helm-rollback: ## Rollback to previous Helm release.
383
        $(HELM) rollback $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
384
`
385

386
func helmMakefileTemplate(namespace, release, outputDir string) string {
5✔
387
        return fmt.Sprintf(helmMakefileTemplateFormat, namespace, release, outputDir)
5✔
388
}
5✔
389

390
func hasWebhooksWith(c config.Config) bool {
3✔
391
        resources, err := c.GetResources()
3✔
392
        if err != nil {
3✔
UNCOV
393
                return false
×
UNCOV
394
        }
×
395

396
        for _, res := range resources {
3✔
UNCOV
397
                if res.HasDefaultingWebhook() || res.HasValidationWebhook() || res.HasConversionWebhook() {
×
UNCOV
398
                        return true
×
UNCOV
399
                }
×
400
        }
401

402
        return false
3✔
403
}
404

405
// removeV1AlphaPluginEntry removes the deprecated helm.kubebuilder.io/v1-alpha plugin entry.
406
// This must be called from Scaffold (before config is saved) for changes to be persisted.
407
func (p *editSubcommand) removeV1AlphaPluginEntry() {
4✔
408
        // Only attempt to remove if using v3 config (which supports plugin configs)
4✔
409
        cfg, ok := p.config.(*cfgv3.Cfg)
4✔
410
        if !ok {
4✔
UNCOV
411
                return
×
UNCOV
412
        }
×
413

414
        // Check if v1-alpha plugin entry exists
415
        if cfg.Plugins == nil {
6✔
416
                return
2✔
417
        }
2✔
418

419
        if _, exists := cfg.Plugins[v1AlphaPluginKey]; exists {
4✔
420
                delete(cfg.Plugins, v1AlphaPluginKey)
2✔
421
                slog.Info("removed deprecated v1-alpha plugin entry")
2✔
422
        }
2✔
423
}
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