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

kubernetes-sigs / kubebuilder / 30018202966

23 Jul 2026 02:56PM UTC coverage: 84.415% (+0.05%) from 84.369%
30018202966

Pull #5905

github

camilamacedo86
WIP-NP gaps
Pull Request #5905: (go/v4, helm/v2-alpha): ensure NetworkPolicies are correct and consistent

67 of 75 new or added lines in 8 files covered. (89.33%)

53 existing lines in 5 files now uncovered.

8244 of 9766 relevant lines covered (84.42%)

161.95 hits per line

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

53.53
/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 templates
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, and the user-owned templates under network-policy/ and
1✔
86
prometheus/ (the NetworkPolicies and the Prometheus ServiceMonitor).
1✔
87
All other template files in templates/ are always regenerated to match your current
1✔
88
kustomize output. Use --force to regenerate all files except Chart.yaml.
1✔
89

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

106
func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
1✔
107
        fs.BoolVar(&p.force, "force", false, "If set, regenerate all files except Chart.yaml")
1✔
108
        fs.StringVar(&p.manifestsFile, "manifests", DefaultManifestsFile,
1✔
109
                "Path to the YAML file containing Kubernetes manifests from kustomize output "+
1✔
110
                        "(e.g., dist/install.yaml). Defaults to dist/install.yaml if unset")
1✔
111
        fs.StringVar(&p.outputDir, "output-dir", common.DefaultOutputDir,
1✔
112
                "Output directory for the generated Helm chart (e.g., charts). Defaults to dist if unset")
1✔
113
}
1✔
114

115
func (p *editSubcommand) InjectConfig(c config.Config) error {
1✔
116
        p.config = c
1✔
117
        return nil
1✔
118
}
1✔
119

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

UNCOV
128
        scaffolder := scaffolds.NewChartScaffolder(p.config, p.force, p.manifestsFile, p.outputDir)
×
129
        scaffolder.InjectFS(fs)
×
130
        err := scaffolder.Scaffold()
×
131
        if err != nil {
×
132
                return fmt.Errorf("error scaffolding Helm chart: %w", err)
×
133
        }
×
134

135
        // Remove deprecated v1-alpha plugin entry from PROJECT file
136
        // This must happen in Scaffold (before config is saved) to be persisted
UNCOV
137
        p.removeV1AlphaPluginEntry()
×
138

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

170
        // Update configuration with current parameters
UNCOV
171
        cfg.ManifestsFile = p.manifestsFile
×
172
        cfg.OutputDir = p.outputDir
×
173

×
174
        if err = p.config.EncodePluginConfig(key, cfg); err != nil {
×
175
                return fmt.Errorf("error encoding plugin configuration: %w", err)
×
176
        }
×
177

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

UNCOV
188
        return nil
×
189
}
190

UNCOV
191
func (p *editSubcommand) ensureManifestsExist() error {
×
192
        slog.Info("Generating default manifests file", "file", p.manifestsFile)
×
193

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

202
        // Verify the file was created
UNCOV
203
        if _, err := os.Stat(p.manifestsFile); err != nil {
×
204
                return fmt.Errorf("manifests file %s was not created: %w", p.manifestsFile, err)
×
205
        }
×
206

UNCOV
207
        slog.Info("Successfully generated manifests file", "file", p.manifestsFile)
×
208
        return nil
×
209
}
210

211
func (p *editSubcommand) PostScaffold() error {
2✔
212
        hasWebhooks := hasWebhooksWith(p.config)
2✔
213

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

248
func (p *editSubcommand) addHelmMakefileTargets(namespace string) error {
3✔
249
        makefilePath := "Makefile"
3✔
250
        if _, err := os.Stat(makefilePath); os.IsNotExist(err) {
4✔
251
                return fmt.Errorf("makefile not found")
1✔
252
        }
1✔
253

254
        // Get the Helm Makefile targets
255
        helmTargets := getHelmMakefileTargets(p.config.GetProjectName(), namespace, p.outputDir)
2✔
256

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

262
        slog.Info("added Helm deployment targets to Makefile",
2✔
263
                "targets", "helm-deploy, helm-uninstall, helm-status, helm-history, helm-rollback")
2✔
264
        return nil
2✔
265
}
266

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

3✔
273
        // If manifests file doesn't exist, use default
3✔
274
        if _, err := os.Stat(p.manifestsFile); os.IsNotExist(err) {
4✔
275
                return defaultNamespace
1✔
276
        }
1✔
277

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

287
        // Parse YAML documents looking for the manager Deployment
288
        decoder := yaml.NewDecoder(file)
2✔
289
        for {
4✔
290
                var doc map[string]any
2✔
291
                if err := decoder.Decode(&doc); err != nil {
3✔
292
                        if err == io.EOF {
1✔
UNCOV
293
                                break
×
294
                        }
295
                        break
1✔
296
                }
297

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

312
        // Fallback to default if manager Deployment not found
313
        return defaultNamespace
1✔
314
}
315

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

323
        // Use the project name as default for release name
324
        release := projectName
5✔
325

5✔
326
        return helmMakefileTemplate(namespace, release, outputDir)
5✔
327
}
328

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

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

345
.PHONY: install-helm
346
install-helm: ## Install the latest version of Helm.
347
        @command -v $(HELM) >/dev/null 2>&1 || { \
348
                echo "Installing Helm..." && \
349
                curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash; \
350
        }
351

352
.PHONY: helm-deploy
353
helm-deploy: install-helm ## Deploy manager to the K8s cluster via Helm. Specify an image with IMG.
354
        IMG="$(IMG)"; $(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART_DIR) \
355
                --namespace $(HELM_NAMESPACE) \
356
                --create-namespace \
357
                --set manager.image.repository=$${IMG%%:*} \
358
                --set manager.image.tag=$${IMG##*:} \
359
                --wait \
360
                --timeout 5m \
361
                $(HELM_EXTRA_ARGS)
362

363
.PHONY: helm-uninstall
364
helm-uninstall: ## Uninstall the Helm release from the K8s cluster.
365
        $(HELM) uninstall $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
366

367
.PHONY: helm-status
368
helm-status: ## Show Helm release status.
369
        $(HELM) status $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
370

371
.PHONY: helm-history
372
helm-history: ## Show Helm release history.
373
        $(HELM) history $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
374

375
.PHONY: helm-rollback
376
helm-rollback: ## Rollback to previous Helm release.
377
        $(HELM) rollback $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
378
`
379

380
func helmMakefileTemplate(namespace, release, outputDir string) string {
5✔
381
        return fmt.Sprintf(helmMakefileTemplateFormat, namespace, release, outputDir)
5✔
382
}
5✔
383

384
func hasWebhooksWith(c config.Config) bool {
3✔
385
        resources, err := c.GetResources()
3✔
386
        if err != nil {
3✔
UNCOV
387
                return false
×
388
        }
×
389

390
        for _, res := range resources {
3✔
UNCOV
391
                if res.HasDefaultingWebhook() || res.HasValidationWebhook() || res.HasConversionWebhook() {
×
392
                        return true
×
393
                }
×
394
        }
395

396
        return false
3✔
397
}
398

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

408
        // Check if v1-alpha plugin entry exists
409
        if cfg.Plugins == nil {
6✔
410
                return
2✔
411
        }
2✔
412

413
        if _, exists := cfg.Plugins[v1AlphaPluginKey]; exists {
4✔
414
                delete(cfg.Plugins, v1AlphaPluginKey)
2✔
415
                slog.Info("removed deprecated v1-alpha plugin entry")
2✔
416
        }
2✔
417
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc