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

kubernetes-sigs / kubebuilder / 20774603420

07 Jan 2026 07:55AM UTC coverage: 71.772% (-0.01%) from 71.782%
20774603420

Pull #5362

github

camilamacedo86
(helm/v2-alpha): Auto-detect and organize sample CRs in templates/samples/

Sample Custom Resources from config/samples/ are now automatically detected
and placed in templates/samples/ with conditional rendering via
.Values.samples.create (disabled by default). Detection uses CRD-based
matching to ensure only CR instances of defined CRDs are categorized as samples.

Assisted-by: Cursor
Pull Request #5362: WIP ✨ (helm/v2-alpha): Auto-detect and organize sample CRs in templates/samples/

81 of 91 new or added lines in 5 files covered. (89.01%)

41 existing lines in 2 files now uncovered.

6448 of 8984 relevant lines covered (71.77%)

32.11 hits per line

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

88.81
/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/parser.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 kustomize
18

19
import (
20
        "fmt"
21
        "io"
22
        "os"
23
        "strings"
24

25
        "go.yaml.in/yaml/v3"
26
        "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
27
)
28

29
// ParsedResources holds Kubernetes resources organized by type for Helm chart generation
30
type ParsedResources struct {
31
        // Core Kubernetes resources
32
        Namespace  *unstructured.Unstructured
33
        Deployment *unstructured.Unstructured
34
        Services   []*unstructured.Unstructured
35

36
        // RBAC resources
37
        ServiceAccount      *unstructured.Unstructured
38
        Roles               []*unstructured.Unstructured
39
        ClusterRoles        []*unstructured.Unstructured
40
        RoleBindings        []*unstructured.Unstructured
41
        ClusterRoleBindings []*unstructured.Unstructured
42

43
        // CRD and API resources
44
        CustomResourceDefinitions []*unstructured.Unstructured
45
        WebhookConfigurations     []*unstructured.Unstructured
46

47
        // Cert-manager resources
48
        Certificates []*unstructured.Unstructured
49
        Issuer       *unstructured.Unstructured
50

51
        // Monitoring resources
52
        ServiceMonitors []*unstructured.Unstructured
53

54
        // Sample Custom Resources (CR instances from config/samples)
55
        SampleResources []*unstructured.Unstructured
56

57
        // Other resources not fitting above categories
58
        Other []*unstructured.Unstructured
59

60
        // definedCRDTypes tracks GVK (Group/Version/Kind) of CRDs defined in this kustomize output
61
        // Used to identify which resources are instances of these CRDs (samples)
62
        definedCRDTypes map[string]bool
63
}
64

65
// Parser parses kustomize output and extracts resources by type
66
type Parser struct {
67
        filePath string
68
}
69

70
// NewParser creates a new parser for the given kustomize output file
71
func NewParser(filePath string) *Parser {
15✔
72
        return &Parser{filePath: filePath}
15✔
73
}
15✔
74

75
// Parse reads and parses the kustomize output file into organized resource groups
76
func (p *Parser) Parse() (*ParsedResources, error) {
15✔
77
        file, err := os.Open(p.filePath)
15✔
78
        if err != nil {
16✔
79
                return nil, fmt.Errorf("failed to open file %s: %w", p.filePath, err)
1✔
80
        }
1✔
81
        defer func() {
28✔
82
                _ = file.Close()
14✔
83
        }()
14✔
84

85
        return p.ParseFromReader(file)
14✔
86
}
87

88
// ParseFromReader parses multi-document YAML from a reader and categorizes resources by type
89
func (p *Parser) ParseFromReader(reader io.Reader) (*ParsedResources, error) {
14✔
90
        decoder := yaml.NewDecoder(reader)
14✔
91
        resources := &ParsedResources{
14✔
92
                CustomResourceDefinitions: make([]*unstructured.Unstructured, 0),
14✔
93
                Roles:                     make([]*unstructured.Unstructured, 0),
14✔
94
                ClusterRoles:              make([]*unstructured.Unstructured, 0),
14✔
95
                RoleBindings:              make([]*unstructured.Unstructured, 0),
14✔
96
                ClusterRoleBindings:       make([]*unstructured.Unstructured, 0),
14✔
97
                Services:                  make([]*unstructured.Unstructured, 0),
14✔
98
                Certificates:              make([]*unstructured.Unstructured, 0),
14✔
99
                WebhookConfigurations:     make([]*unstructured.Unstructured, 0),
14✔
100
                ServiceMonitors:           make([]*unstructured.Unstructured, 0),
14✔
101
                SampleResources:           make([]*unstructured.Unstructured, 0),
14✔
102
                Other:                     make([]*unstructured.Unstructured, 0),
14✔
103
                definedCRDTypes:           make(map[string]bool),
14✔
104
        }
14✔
105

14✔
106
        // First pass: collect all resources
14✔
107
        var allResources []*unstructured.Unstructured
14✔
108

14✔
109
        for {
73✔
110
                var doc map[string]any
59✔
111
                err := decoder.Decode(&doc)
59✔
112
                if err == io.EOF {
72✔
113
                        break
13✔
114
                }
115
                if err != nil {
47✔
116
                        return nil, fmt.Errorf("failed to decode YAML document: %w", err)
1✔
117
                }
1✔
118

119
                // Skip empty documents
120
                if doc == nil {
45✔
121
                        continue
×
122
                }
123

124
                obj := &unstructured.Unstructured{Object: doc}
45✔
125
                allResources = append(allResources, obj)
45✔
126
        }
127

128
        // Second pass: build CRD type map (extract GVK from all CRDs)
129
        const kindCRD = "CustomResourceDefinition"
13✔
130
        for _, obj := range allResources {
58✔
131
                if obj.GetKind() == kindCRD {
53✔
132
                        p.extractCRDType(obj, resources)
8✔
133
                }
8✔
134
        }
135

136
        // Third pass: categorize all resources (now we know which CRDs are defined)
137
        for _, obj := range allResources {
58✔
138
                p.categorizeResource(obj, resources)
45✔
139
        }
45✔
140

141
        return resources, nil
13✔
142
}
143

144
// extractCRDType extracts the group/version/kind from a CRD and stores it in the definedCRDTypes map
145
func (p *Parser) extractCRDType(crd *unstructured.Unstructured, resources *ParsedResources) {
8✔
146
        // Extract group from spec.group
8✔
147
        group, found, err := unstructured.NestedString(crd.Object, "spec", "group")
8✔
148
        if !found || err != nil {
8✔
NEW
149
                return
×
NEW
150
        }
×
151

152
        // Extract versions from spec.versions (use NestedFieldNoCopy to avoid deep copy issues)
153
        versionsField, found, err := unstructured.NestedFieldNoCopy(crd.Object, "spec", "versions")
8✔
154
        if !found || err != nil {
8✔
NEW
155
                return
×
NEW
156
        }
×
157

158
        versions, ok := versionsField.([]any)
8✔
159
        if !ok {
8✔
NEW
160
                return
×
NEW
161
        }
×
162

163
        // Extract kind from spec.names.kind
164
        kind, found, err := unstructured.NestedString(crd.Object, "spec", "names", "kind")
8✔
165
        if !found || err != nil {
9✔
166
                return
1✔
167
        }
1✔
168

169
        // Register all versions of this CRD
170
        for _, v := range versions {
14✔
171
                versionMap, ok := v.(map[string]any)
7✔
172
                if !ok {
7✔
NEW
173
                        continue
×
174
                }
175
                versionName, ok := versionMap["name"].(string)
7✔
176
                if !ok {
7✔
NEW
177
                        continue
×
178
                }
179

180
                // Create GVK key: group/version/Kind
181
                gvk := fmt.Sprintf("%s/%s/%s", group, versionName, kind)
7✔
182
                resources.definedCRDTypes[gvk] = true
7✔
183
        }
184
}
185

186
// categorizeResource sorts a Kubernetes resource into the appropriate category based on kind and API version
187
func (p *Parser) categorizeResource(obj *unstructured.Unstructured, resources *ParsedResources) {
45✔
188
        kind := obj.GetKind()
45✔
189
        apiVersion := obj.GetAPIVersion()
45✔
190

45✔
191
        switch {
45✔
192
        case kind == "Namespace":
5✔
193
                resources.Namespace = obj
5✔
194
        case kind == "CustomResourceDefinition":
8✔
195
                resources.CustomResourceDefinitions = append(resources.CustomResourceDefinitions, obj)
8✔
196
        case kind == "ServiceAccount":
3✔
197
                resources.ServiceAccount = obj
3✔
198
        case kind == "Role":
×
199
                resources.Roles = append(resources.Roles, obj)
×
200
        case kind == "ClusterRole":
2✔
201
                resources.ClusterRoles = append(resources.ClusterRoles, obj)
2✔
202
        case kind == "RoleBinding":
×
203
                resources.RoleBindings = append(resources.RoleBindings, obj)
×
204
        case kind == "ClusterRoleBinding":
2✔
205
                resources.ClusterRoleBindings = append(resources.ClusterRoleBindings, obj)
2✔
206
        case kind == "Service":
7✔
207
                resources.Services = append(resources.Services, obj)
7✔
208
        case kind == "Deployment":
5✔
209
                resources.Deployment = obj
5✔
210
        case kind == "Certificate" && apiVersion == "cert-manager.io/v1":
1✔
211
                resources.Certificates = append(resources.Certificates, obj)
1✔
212
        case kind == "Issuer" && apiVersion == "cert-manager.io/v1":
1✔
213
                resources.Issuer = obj
1✔
214
        case kind == "ValidatingWebhookConfiguration" || kind == "MutatingWebhookConfiguration":
1✔
215
                resources.WebhookConfigurations = append(resources.WebhookConfigurations, obj)
1✔
216
        case kind == "ServiceMonitor" && apiVersion == "monitoring.coreos.com/v1":
1✔
217
                resources.ServiceMonitors = append(resources.ServiceMonitors, obj)
1✔
218
        case p.isSampleCustomResource(obj, resources):
7✔
219
                // Custom Resource instances (from config/samples) should go to samples group
7✔
220
                // These are resources that match CRDs defined in this kustomize output
7✔
221
                resources.SampleResources = append(resources.SampleResources, obj)
7✔
222
        default:
2✔
223
                resources.Other = append(resources.Other, obj)
2✔
224
        }
225
}
226

227
// isSampleCustomResource determines if a resource is a Custom Resource instance (sample)
228
// It checks if the resource is an instance of a CRD defined in the kustomize output
229
func (p *Parser) isSampleCustomResource(obj *unstructured.Unstructured, resources *ParsedResources) bool {
9✔
230
        kind := obj.GetKind()
9✔
231
        apiVersion := obj.GetAPIVersion()
9✔
232

9✔
233
        // Build GVK key: group/version/Kind
9✔
234
        // apiVersion format is either "group/version" or just "version" for core types
9✔
235
        gvk := fmt.Sprintf("%s/%s", apiVersion, kind)
9✔
236

9✔
237
        // Check if this resource type matches any defined CRD
9✔
238
        return resources.definedCRDTypes[gvk]
9✔
239
}
9✔
240

241
func (pr *ParsedResources) EstimatePrefix(projectName string) string {
1✔
242
        prefix := projectName
1✔
243
        if pr.Deployment != nil {
2✔
244
                if name := pr.Deployment.GetName(); name != "" {
2✔
245
                        deploymentPrefix, found := strings.CutSuffix(name, "-controller-manager")
1✔
246
                        if found {
2✔
247
                                prefix = deploymentPrefix
1✔
248
                        }
1✔
249
                }
250
        }
251
        // Double check that the prefix is also the prefix for the service names
252
        for _, svc := range pr.Services {
2✔
253
                if name := svc.GetName(); name != "" {
2✔
254
                        if !strings.HasPrefix(name, prefix) {
1✔
255
                                // If not, fallback to just project name
×
256
                                prefix = projectName
×
257
                                break
×
258
                        }
259
                }
260
        }
261
        return prefix
1✔
262
}
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