diff --git a/internal/chartverifier/checks/chart-0.1.0-v3.with-crd-in-charts.tgz b/internal/chartverifier/checks/chart-0.1.0-v3.with-crd-in-charts.tgz new file mode 100644 index 00000000..856c72eb Binary files /dev/null and b/internal/chartverifier/checks/chart-0.1.0-v3.with-crd-in-charts.tgz differ diff --git a/internal/chartverifier/checks/chart-0.1.0-v3.with-crd-in-root.tgz b/internal/chartverifier/checks/chart-0.1.0-v3.with-crd-in-root.tgz new file mode 100644 index 00000000..f599136f Binary files /dev/null and b/internal/chartverifier/checks/chart-0.1.0-v3.with-crd-in-root.tgz differ diff --git a/internal/chartverifier/checks/chart-0.1.0-v3.with-crd-in-subchart-crds.tgz b/internal/chartverifier/checks/chart-0.1.0-v3.with-crd-in-subchart-crds.tgz new file mode 100644 index 00000000..ef55826b Binary files /dev/null and b/internal/chartverifier/checks/chart-0.1.0-v3.with-crd-in-subchart-crds.tgz differ diff --git a/internal/chartverifier/checks/chart-0.1.0-v3.with-crd-in-templates.tgz b/internal/chartverifier/checks/chart-0.1.0-v3.with-crd-in-templates.tgz new file mode 100644 index 00000000..b3af07bc Binary files /dev/null and b/internal/chartverifier/checks/chart-0.1.0-v3.with-crd-in-templates.tgz differ diff --git a/internal/chartverifier/checks/chart-0.1.0-v3.with-crd-quoted-kind.tgz b/internal/chartverifier/checks/chart-0.1.0-v3.with-crd-quoted-kind.tgz new file mode 100644 index 00000000..c2720169 Binary files /dev/null and b/internal/chartverifier/checks/chart-0.1.0-v3.with-crd-quoted-kind.tgz differ diff --git a/internal/chartverifier/checks/checks.go b/internal/chartverifier/checks/checks.go index 60f4df85..208a09c4 100644 --- a/internal/chartverifier/checks/checks.go +++ b/internal/chartverifier/checks/checks.go @@ -28,6 +28,7 @@ import ( "github.com/opdev/getocprange" "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/lint" "helm.sh/helm/v3/pkg/lint/support" @@ -234,14 +235,107 @@ func NotContainCRDs(opts *CheckOptions) (Result, error) { r := NewResult(true, ChartDoesNotContainCRDs) - if len(c.CRDObjects()) > 0 { + // Check standard CRD directory in main chart and dependencies + if hasCRDObjects(c) { + r.Ok = false + r.SetResult(false, ChartContainCRDs) + return r, nil + } + + // Check for CRDs in templates (main chart and dependencies) + if hasCRDInTemplates(c) { + r.Ok = false + r.SetResult(false, ChartContainCRDs) + return r, nil + } + + // Check for CRDs in files (root directory of main chart and dependencies) + if hasCRDInFiles(c) { r.Ok = false r.SetResult(false, ChartContainCRDs) + return r, nil } return r, nil } +func hasCRDObjects(c *chart.Chart) bool { + // Check main chart CRDs directory + if len(c.CRDObjects()) > 0 { + return true + } + + // Recursively check dependencies' CRDs directories + for _, dep := range c.Dependencies() { + if hasCRDObjects(dep) { + return true + } + } + + return false +} + +func hasCRDInTemplates(c *chart.Chart) bool { + // Check main chart templates + for _, f := range c.Templates { + if !strings.HasSuffix(f.Name, ".yaml") && !strings.HasSuffix(f.Name, ".yml") { + continue + } + if isCRDFile(f.Data) { + return true + } + } + + // Check dependency/subchart templates + for _, dep := range c.Dependencies() { + if hasCRDInTemplates(dep) { + return true + } + } + + return false +} + +func isCRDFile(data []byte) bool { + // Split on YAML document separator for multi-doc files + docs := strings.Split(string(data), "\n---") + for _, doc := range docs { + for _, line := range strings.Split(doc, "\n") { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "kind:") { + kind := strings.TrimSpace(strings.TrimPrefix(trimmed, "kind:")) + // Remove surrounding quotes if present (both single and double) + kind = strings.Trim(kind, "\"'") + if kind == "CustomResourceDefinition" { + return true + } + } + } + } + return false +} + +func hasCRDInFiles(c *chart.Chart) bool { + // Check this chart's files (root directory) + for _, f := range c.Files { + if !strings.HasSuffix(f.Name, ".yaml") && !strings.HasSuffix(f.Name, ".yml") { + continue + } + if isCRDFile(f.Data) { + return true + } + } + + // Recursively check dependencies + for _, dep := range c.Dependencies() { + if hasCRDInFiles(dep) { + return true + } + } + + return false +} + func HelmLint(opts *CheckOptions) (Result, error) { _, p, err := LoadChartFromURI(opts) if err != nil { diff --git a/internal/chartverifier/checks/checks_test.go b/internal/chartverifier/checks/checks_test.go index fbab3910..8170101a 100644 --- a/internal/chartverifier/checks/checks_test.go +++ b/internal/chartverifier/checks/checks_test.go @@ -313,6 +313,11 @@ func TestNotContainCRDs(t *testing.T) { negativeTestCases := []testCase{ {description: "Contain CRDs", uri: "chart-0.1.0-v3.with-crd.tgz"}, + {description: "Contain CRDs in /templates", uri: "chart-0.1.0-v3.with-crd-in-templates.tgz"}, + {description: "Contain CRDs in root", uri: "chart-0.1.0-v3.with-crd-in-root.tgz"}, + {description: "Contain CRDs in /charts", uri: "chart-0.1.0-v3.with-crd-in-charts.tgz"}, + {description: "Contain CRDs in subchart /crds", uri: "chart-0.1.0-v3.with-crd-in-subchart-crds.tgz"}, + {description: "Contain CRDs with quoted kind values", uri: "chart-0.1.0-v3.with-crd-quoted-kind.tgz"}, } for _, tc := range negativeTestCases {