[0-9]+)|(?P(?:alpha|beta|rc)[0-9]+))?$`
)
-var (
- go113 = goVersion{
- major: 1,
- minor: 13,
- }
- go116alpha1 = goVersion{
- major: 1,
- minor: 16,
- prerelease: "alpha1",
- }
+var goVerRegexp = regexp.MustCompile(goVerPattern)
- goVerRegexp = regexp.MustCompile(goVerPattern)
-)
-
-type goVersion struct {
+// GoVersion describes a Go version.
+type GoVersion struct {
major, minor, patch int
prerelease string
}
-func (v *goVersion) parse(verStr string) error {
+func (v GoVersion) String() string {
+ switch {
+ case v.patch != 0:
+ return fmt.Sprintf("go%d.%d.%d", v.major, v.minor, v.patch)
+ case v.prerelease != "":
+ return fmt.Sprintf("go%d.%d%s", v.major, v.minor, v.prerelease)
+ }
+ return fmt.Sprintf("go%d.%d", v.major, v.minor)
+}
+
+// MustParse will panic if verStr does not match the expected Go version string spec.
+func MustParse(verStr string) (v GoVersion) {
+ if err := v.parse(verStr); err != nil {
+ panic(err)
+ }
+ return v
+}
+
+func (v *GoVersion) parse(verStr string) error {
m := goVerRegexp.FindStringSubmatch(verStr)
if m == nil {
return fmt.Errorf("invalid version string")
@@ -57,18 +64,18 @@ func (v *goVersion) parse(verStr string) error {
v.major, err = strconv.Atoi(m[1])
if err != nil {
- return fmt.Errorf("error parsing major version '%s': %s", m[1], err)
+ return fmt.Errorf("error parsing major version %q: %w", m[1], err)
}
v.minor, err = strconv.Atoi(m[2])
if err != nil {
- return fmt.Errorf("error parsing minor version '%s': %s", m[2], err)
+ return fmt.Errorf("error parsing minor version %q: %w", m[2], err)
}
if m[3] != "" {
v.patch, err = strconv.Atoi(m[3])
if err != nil {
- return fmt.Errorf("error parsing patch version '%s': %s", m[2], err)
+ return fmt.Errorf("error parsing patch version %q: %w", m[2], err)
}
}
@@ -77,7 +84,8 @@ func (v *goVersion) parse(verStr string) error {
return nil
}
-func (v goVersion) compare(other goVersion) int {
+// Compare returns -1, 0, or 1 if v < other, v == other, or v > other, respectively.
+func (v GoVersion) Compare(other GoVersion) int {
if v.major > other.major {
return 1
}
@@ -114,16 +122,16 @@ func (v goVersion) compare(other goVersion) int {
return -1
}
-// ValidateGoVersion verifies that Go is installed and the current go version is supported by kubebuilder
-func ValidateGoVersion() error {
- err := fetchAndCheckGoVersion()
+// ValidateGoVersion verifies that Go is installed and the current go version is supported by a plugin.
+func ValidateGoVersion(minVersion, maxVersion GoVersion) error {
+ err := fetchAndCheckGoVersion(minVersion, maxVersion)
if err != nil {
- return fmt.Errorf("%s. You can skip this check using the --skip-go-version-check flag", err)
+ return fmt.Errorf("you can skip this check using the --skip-go-version-check flag: %w", err)
}
return nil
}
-func fetchAndCheckGoVersion() error {
+func fetchAndCheckGoVersion(minVersion, maxVersion GoVersion) error {
cmd := exec.Command("go", "version")
out, err := cmd.Output()
if err != nil {
@@ -135,22 +143,20 @@ func fetchAndCheckGoVersion() error {
return fmt.Errorf("found invalid Go version: %q", string(out))
}
goVer := split[2]
- if err := checkGoVersion(goVer); err != nil {
- return fmt.Errorf("go version '%s' is incompatible because '%s'", goVer, err)
+ if err := checkGoVersion(goVer, minVersion, maxVersion); err != nil {
+ return fmt.Errorf("go version %q is incompatible: %w", goVer, err)
}
return nil
}
-// checkGoVersion should only ever check if the Go version >= 1.13, since the kubebuilder binary only cares
-// that the go binary supports go modules which were stabilized in that version (i.e. in go 1.13) by default
-func checkGoVersion(verStr string) error {
- var version goVersion
+func checkGoVersion(verStr string, minVersion, maxVersion GoVersion) error {
+ var version GoVersion
if err := version.parse(verStr); err != nil {
return err
}
- if version.compare(go113) < 0 || version.compare(go116alpha1) >= 0 {
- return fmt.Errorf("requires 1.13 <= version < 1.16")
+ if version.Compare(minVersion) < 0 || version.Compare(maxVersion) >= 0 {
+ return fmt.Errorf("plugin requires %q <= version < %q", minVersion, maxVersion)
}
return nil
diff --git a/pkg/plugins/golang/go_version_test.go b/pkg/plugins/golang/go_version_test.go
index ccba6bd9b34..4153dded316 100644
--- a/pkg/plugins/golang/go_version_test.go
+++ b/pkg/plugins/golang/go_version_test.go
@@ -17,49 +17,77 @@ limitations under the License.
package golang
import (
+ "errors"
"sort"
- . "github.com/onsi/ginkgo"
- . "github.com/onsi/ginkgo/extensions/table"
+ . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
-var _ = Describe("goVersion", func() {
+var _ = Describe("GoVersion", func() {
+ Context("String", func() {
+ It("patch is not empty", func() {
+ v := GoVersion{major: 1, minor: 1, patch: 1}
+ Expect(v.String()).To(Equal("go1.1.1"))
+ })
+ It("preRelease is not empty", func() {
+ v := GoVersion{major: 1, minor: 1, prerelease: "-alpha"}
+ Expect(v.String()).To(Equal("go1.1-alpha"))
+ })
+ It("default", func() {
+ v := GoVersion{major: 1, minor: 1}
+ Expect(v.String()).To(Equal("go1.1"))
+ })
+ })
+
+ Context("MustParse", func() {
+ It("succeeds", func() {
+ v := GoVersion{major: 1, minor: 1, patch: 1}
+ Expect(MustParse("go1.1.1")).To(Equal(v))
+ })
+ It("panics", func() {
+ triggerPanic := func() {
+ MustParse("go1.a")
+ }
+ Expect(triggerPanic).To(PanicWith(errors.New("invalid version string")))
+ })
+ })
+
Context("parse", func() {
- var v goVersion
+ var v GoVersion
BeforeEach(func() {
- v = goVersion{}
+ v = GoVersion{}
})
DescribeTable("should succeed for valid versions",
- func(version string, expected goVersion) {
+ func(version string, expected GoVersion) {
Expect(v.parse(version)).NotTo(HaveOccurred())
Expect(v.major).To(Equal(expected.major))
Expect(v.minor).To(Equal(expected.minor))
Expect(v.patch).To(Equal(expected.patch))
Expect(v.prerelease).To(Equal(expected.prerelease))
},
- Entry("for minor release", "go1.15", goVersion{
+ Entry("for minor release", "go1.15", GoVersion{
major: 1,
minor: 15,
}),
- Entry("for patch release", "go1.15.1", goVersion{
+ Entry("for patch release", "go1.15.1", GoVersion{
major: 1,
minor: 15,
patch: 1,
}),
- Entry("for alpha release", "go1.15alpha1", goVersion{
+ Entry("for alpha release", "go1.15alpha1", GoVersion{
major: 1,
minor: 15,
prerelease: "alpha1",
}),
- Entry("for beta release", "go1.15beta1", goVersion{
+ Entry("for beta release", "go1.15beta1", GoVersion{
major: 1,
minor: 15,
prerelease: "beta1",
}),
- Entry("for release candidate", "go1.15rc1", goVersion{
+ Entry("for release candidate", "go1.15rc1", GoVersion{
major: 1,
minor: 15,
prerelease: "rc1",
@@ -78,10 +106,15 @@ var _ = Describe("goVersion", func() {
)
})
- Context("compare", func() {
- // Test compare() by sorting a list.
+ Context("Compare", func() {
+ // Test Compare() by sorting a list.
var (
- versions = []goVersion{
+ versions []GoVersion
+ sortedVersions []GoVersion
+ )
+
+ BeforeEach(func() {
+ versions = []GoVersion{
{major: 1, minor: 15, prerelease: "rc2"},
{major: 1, minor: 15, patch: 1},
{major: 1, minor: 16},
@@ -98,7 +131,7 @@ var _ = Describe("goVersion", func() {
{major: 0, minor: 123},
}
- sortedVersions = []goVersion{
+ sortedVersions = []GoVersion{
{major: 0, minor: 123},
{major: 1, minor: 13},
{major: 1, minor: 14},
@@ -114,20 +147,48 @@ var _ = Describe("goVersion", func() {
{major: 1, minor: 16},
{major: 2, minor: 0},
}
- )
+ })
It("sorts a valid list of versions correctly", func() {
sort.Slice(versions, func(i int, j int) bool {
- return versions[i].compare(versions[j]) == -1
+ return versions[i].Compare(versions[j]) == -1
})
Expect(versions).To(Equal(sortedVersions))
})
})
})
+var _ = Describe("ValidateGoVersion", func() {
+ DescribeTable("should return no error for valid/supported go versions", func(minVersion, maxVersion GoVersion) {
+ Expect(ValidateGoVersion(minVersion, maxVersion)).To(Succeed())
+ },
+ Entry("for minVersion: 1.1.1 and maxVersion: 2000.1.1", GoVersion{major: 1, minor: 1, patch: 1},
+ GoVersion{major: 2000, minor: 1, patch: 1}),
+ Entry("for minVersion: 1.1.1 and maxVersion: 1.2000.2000", GoVersion{major: 1, minor: 1, patch: 1},
+ GoVersion{major: 1, minor: 2000, patch: 1}),
+ )
+
+ DescribeTable("should return error for invalid/unsupported go versions", func(minVersion, maxVersion GoVersion) {
+ Expect(ValidateGoVersion(minVersion, maxVersion)).NotTo(Succeed())
+ },
+ Entry("for invalid min and maxVersions", GoVersion{major: 2, minor: 2, patch: 2},
+ GoVersion{major: 1, minor: 1, patch: 1}),
+ )
+})
+
var _ = Describe("checkGoVersion", func() {
- DescribeTable("should return true for supported go versions",
- func(version string) { Expect(checkGoVersion(version)).NotTo(HaveOccurred()) },
+ var (
+ goVerMin GoVersion
+ goVerMax GoVersion
+ )
+
+ BeforeEach(func() {
+ goVerMin = MustParse("go1.13")
+ goVerMax = MustParse("go2.0alpha1")
+ })
+
+ DescribeTable("should return no error for supported go versions",
+ func(version string) { Expect(checkGoVersion(version, goVerMin, goVerMax)).To(Succeed()) },
Entry("for go 1.13", "go1.13"),
Entry("for go 1.13.1", "go1.13.1"),
Entry("for go 1.13.2", "go1.13.2"),
@@ -174,16 +235,41 @@ var _ = Describe("checkGoVersion", func() {
Entry("for go 1.15.6", "go1.15.6"),
Entry("for go 1.15.7", "go1.15.7"),
Entry("for go 1.15.8", "go1.15.8"),
+ Entry("for go 1.16", "go1.16"),
+ Entry("for go 1.16.1", "go1.16.1"),
+ Entry("for go 1.16.2", "go1.16.2"),
+ Entry("for go 1.16.3", "go1.16.3"),
+ Entry("for go 1.16.4", "go1.16.4"),
+ Entry("for go 1.16.5", "go1.16.5"),
+ Entry("for go 1.16.6", "go1.16.6"),
+ Entry("for go 1.16.7", "go1.16.7"),
+ Entry("for go 1.16.8", "go1.16.8"),
+ Entry("for go 1.16.9", "go1.16.9"),
+ Entry("for go 1.16.10", "go1.16.10"),
+ Entry("for go 1.16.11", "go1.16.11"),
+ Entry("for go 1.16.12", "go1.16.12"),
+ Entry("for go 1.17.1", "go1.17.1"),
+ Entry("for go 1.17.2", "go1.17.2"),
+ Entry("for go 1.17.3", "go1.17.3"),
+ Entry("for go 1.17.4", "go1.17.4"),
+ Entry("for go 1.17.5", "go1.17.5"),
+ Entry("for go 1.18.1", "go1.18.1"),
+ Entry("for go.1.19", "go1.19"),
+ Entry("for go.1.19.1", "go1.19.1"),
+ Entry("for go.1.20", "go1.20"),
+ Entry("for go.1.21", "go1.21"),
+ Entry("for go.1.22", "go1.22"),
+ Entry("for go.1.23", "go1.23"),
+ Entry("for go.1.24", "go1.24"),
)
- DescribeTable("should return false for non-supported go versions",
- func(version string) { Expect(checkGoVersion(version)).To(HaveOccurred()) },
+ DescribeTable("should return an error for non-supported go versions",
+ func(version string) { Expect(checkGoVersion(version, goVerMin, goVerMax)).NotTo(Succeed()) },
Entry("for invalid go versions", "go"),
Entry("for go 1.13beta1", "go1.13beta1"),
Entry("for go 1.13rc1", "go1.13rc1"),
Entry("for go 1.13rc2", "go1.13rc2"),
- Entry("for go 1.16beta1", "go1.16beta1"),
- Entry("for go 1.16rc1", "go1.16rc1"),
- Entry("for go 1.16", "go1.16"),
+ Entry("for go 2.0alpha1", "go2.0alpha1"),
+ Entry("for go 2.0.0", "go2.0.0"),
)
})
diff --git a/pkg/plugins/golang/options.go b/pkg/plugins/golang/options.go
index 230dc1c3383..6c8b0c8b914 100644
--- a/pkg/plugins/golang/options.go
+++ b/pkg/plugins/golang/options.go
@@ -1,5 +1,5 @@
/*
-Copyright 2021 The Kubernetes Authors.
+Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -19,48 +19,47 @@ package golang
import (
"path"
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2"
- "sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
)
-var (
- coreGroups = map[string]string{
- "admission": "k8s.io",
- "admissionregistration": "k8s.io",
- "apps": "",
- "auditregistration": "k8s.io",
- "apiextensions": "k8s.io",
- "authentication": "k8s.io",
- "authorization": "k8s.io",
- "autoscaling": "",
- "batch": "",
- "certificates": "k8s.io",
- "coordination": "k8s.io",
- "core": "",
- "events": "k8s.io",
- "extensions": "",
- "imagepolicy": "k8s.io",
- "networking": "k8s.io",
- "node": "k8s.io",
- "metrics": "k8s.io",
- "policy": "",
- "rbac.authorization": "k8s.io",
- "scheduling": "k8s.io",
- "setting": "k8s.io",
- "storage": "k8s.io",
- }
-)
+var coreGroups = map[string]string{
+ "admission": "k8s.io",
+ "admissionregistration": "k8s.io",
+ "apps": "",
+ "auditregistration": "k8s.io",
+ "apiextensions": "k8s.io",
+ "authentication": "k8s.io",
+ "authorization": "k8s.io",
+ "autoscaling": "",
+ "batch": "",
+ "certificates": "k8s.io",
+ "coordination": "k8s.io",
+ "core": "",
+ "events": "k8s.io",
+ "extensions": "",
+ "imagepolicy": "k8s.io",
+ "networking": "k8s.io",
+ "node": "k8s.io",
+ "metrics": "k8s.io",
+ "policy": "",
+ "rbac.authorization": "k8s.io",
+ "scheduling": "k8s.io",
+ "setting": "k8s.io",
+ "storage": "k8s.io",
+}
// Options contains the information required to build a new resource.Resource.
type Options struct {
// Plural is the resource's kind plural form.
Plural string
- // CRDVersion is the CustomResourceDefinition API version that will be used for the resource.
- CRDVersion string
- // WebhookVersion is the {Validating,Mutating}WebhookConfiguration API version that will be used for the resource.
- WebhookVersion string
+ // ExternalAPIPath allows to inform a path for APIs not defined in the project
+ ExternalAPIPath string
+
+ // ExternalAPIPath allows to inform the resource domain to build the Qualified Group
+ // to generate the RBAC markers
+ ExternalAPIDomain string
// Namespaced is true if the resource should be namespaced.
Namespaced bool
@@ -71,6 +70,9 @@ type Options struct {
DoDefaulting bool
DoValidation bool
DoConversion bool
+
+ // Spoke versions for conversion webhook
+ Spoke []string
}
// UpdateResource updates the provided resource with the options
@@ -81,8 +83,9 @@ func (opts Options) UpdateResource(res *resource.Resource, c config.Config) {
if opts.DoAPI {
res.Path = resource.APIPackagePath(c.GetRepository(), res.Group, res.Version, c.IsMultiGroup())
+
res.API = &resource.API{
- CRDVersion: opts.CRDVersion,
+ CRDVersion: "v1",
Namespaced: opts.Namespaced,
}
}
@@ -93,7 +96,8 @@ func (opts Options) UpdateResource(res *resource.Resource, c config.Config) {
if opts.DoDefaulting || opts.DoValidation || opts.DoConversion {
res.Path = resource.APIPackagePath(c.GetRepository(), res.Group, res.Version, c.IsMultiGroup())
- res.Webhooks.WebhookVersion = opts.WebhookVersion
+
+ res.Webhooks.WebhookVersion = "v1"
if opts.DoDefaulting {
res.Webhooks.Defaulting = true
}
@@ -102,27 +106,34 @@ func (opts Options) UpdateResource(res *resource.Resource, c config.Config) {
}
if opts.DoConversion {
res.Webhooks.Conversion = true
+ res.Webhooks.Spoke = opts.Spoke
}
}
+ if len(opts.ExternalAPIPath) > 0 {
+ res.External = true
+ }
+
// domain and path may need to be changed in case we are referring to a builtin core resource:
// - Check if we are scaffolding the resource now => project resource
// - Check if we already scaffolded the resource => project resource
// - Check if the resource group is a well-known core group => builtin core resource
// - In any other case, default to => project resource
- // TODO: need to support '--resource-pkg-path' flag for specifying resourcePath
if !opts.DoAPI {
var alreadyHasAPI bool
- if c.GetVersion().Compare(cfgv2.Version) == 0 {
- alreadyHasAPI = c.HasResource(res.GVK)
- } else {
- loadedRes, err := c.GetResource(res.GVK)
- alreadyHasAPI = err == nil && loadedRes.HasAPI()
- }
+ loadedRes, err := c.GetResource(res.GVK)
+ alreadyHasAPI = err == nil && loadedRes.HasAPI()
if !alreadyHasAPI {
- if domain, found := coreGroups[res.Group]; found {
- res.Domain = domain
- res.Path = path.Join("k8s.io", "api", res.Group, res.Version)
+ if res.External {
+ res.Path = opts.ExternalAPIPath
+ res.Domain = opts.ExternalAPIDomain
+ } else {
+ // Handle core types
+ if domain, found := coreGroups[res.Group]; found {
+ res.Core = true
+ res.Domain = domain
+ res.Path = path.Join("k8s.io", "api", res.Group, res.Version)
+ }
}
}
}
diff --git a/pkg/plugins/golang/options_test.go b/pkg/plugins/golang/options_test.go
index 7df8d828907..49f6fc43daa 100644
--- a/pkg/plugins/golang/options_test.go
+++ b/pkg/plugins/golang/options_test.go
@@ -1,5 +1,5 @@
/*
-Copyright 2021 The Kubernetes Authors.
+Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -19,14 +19,12 @@ package golang
import (
"path"
- . "github.com/onsi/ginkgo"
- . "github.com/onsi/ginkgo/extensions/table"
+ . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2"
- cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3"
- "sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
+ "sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
)
var _ = Describe("Options", func() {
@@ -37,7 +35,13 @@ var _ = Describe("Options", func() {
version = "v1"
kind = "FirstMate"
)
+
var (
+ gvk resource.GVK
+ cfg config.Config
+ )
+
+ BeforeEach(func() {
gvk = resource.GVK{
Group: group,
Domain: domain,
@@ -45,10 +49,6 @@ var _ = Describe("Options", func() {
Kind: kind,
}
- cfg config.Config
- )
-
- BeforeEach(func() {
cfg = cfgv3.New()
_ = cfg.SetRepository("test")
})
@@ -78,17 +78,18 @@ var _ = Describe("Options", func() {
if options.DoAPI || options.DoDefaulting || options.DoValidation || options.DoConversion {
if multiGroup {
Expect(res.Path).To(Equal(
- path.Join(cfg.GetRepository(), "apis", gvk.Group, gvk.Version)))
+ path.Join(cfg.GetRepository(), "api", gvk.Group, gvk.Version)))
} else {
Expect(res.Path).To(Equal(path.Join(cfg.GetRepository(), "api", gvk.Version)))
}
+ } else if len(options.ExternalAPIPath) > 0 {
+ Expect(res.Path).To(Equal("testPath"))
} else {
// Core-resources have a path despite not having an API/Webhook but they are not tested here
Expect(res.Path).To(Equal(""))
}
Expect(res.API).NotTo(BeNil())
if options.DoAPI {
- Expect(res.API.CRDVersion).To(Equal(options.CRDVersion))
Expect(res.API.Namespaced).To(Equal(options.Namespaced))
Expect(res.API.IsEmpty()).To(BeFalse())
} else {
@@ -97,14 +98,20 @@ var _ = Describe("Options", func() {
Expect(res.Controller).To(Equal(options.DoController))
Expect(res.Webhooks).NotTo(BeNil())
if options.DoDefaulting || options.DoValidation || options.DoConversion {
- Expect(res.Webhooks.WebhookVersion).To(Equal(options.WebhookVersion))
Expect(res.Webhooks.Defaulting).To(Equal(options.DoDefaulting))
Expect(res.Webhooks.Validation).To(Equal(options.DoValidation))
Expect(res.Webhooks.Conversion).To(Equal(options.DoConversion))
+ Expect(res.Webhooks.Spoke).To(Equal(options.Spoke))
Expect(res.Webhooks.IsEmpty()).To(BeFalse())
} else {
Expect(res.Webhooks.IsEmpty()).To(BeTrue())
}
+
+ if len(options.ExternalAPIPath) > 0 {
+ Expect(res.External).To(BeTrue())
+ Expect(res.Domain).To(Equal("test.io"))
+ }
+
Expect(res.QualifiedGroup()).To(Equal(gvk.Group + "." + gvk.Domain))
Expect(res.PackageName()).To(Equal(gvk.Group))
Expect(res.ImportAlias()).To(Equal(gvk.Group + gvk.Version))
@@ -112,10 +119,10 @@ var _ = Describe("Options", func() {
},
Entry("when updating nothing", Options{}),
Entry("when updating the plural", Options{Plural: "mates"}),
- Entry("when updating the API", Options{DoAPI: true, CRDVersion: "v1", Namespaced: true}),
Entry("when updating the Controller", Options{DoController: true}),
- Entry("when updating Webhooks",
- Options{WebhookVersion: "v1", DoDefaulting: true, DoValidation: true, DoConversion: true}),
+ Entry("when updating with External API Path", Options{ExternalAPIPath: "testPath", ExternalAPIDomain: "test.io"}),
+ Entry("when updating the API with setting webhooks params",
+ Options{DoAPI: true, DoDefaulting: true, DoValidation: true, DoConversion: true}),
)
DescribeTable("should use core apis",
@@ -157,7 +164,7 @@ var _ = Describe("Options", func() {
// the `HasAPI` method of the resource obtained with `GetResource` will always return false.
// Instead, the existence of a resource in the list means the API was scaffolded.
func(group, qualified string) {
- cfg = cfgv2.New()
+ cfg = cfgv3.New()
_ = cfg.SetRepository("test")
options := Options{}
diff --git a/pkg/plugins/golang/repository.go b/pkg/plugins/golang/repository.go
index 47debda41c3..e815e5dce4c 100644
--- a/pkg/plugins/golang/repository.go
+++ b/pkg/plugins/golang/repository.go
@@ -18,6 +18,7 @@ package golang
import (
"encoding/json"
+ "errors"
"fmt"
"os"
"os/exec"
@@ -39,14 +40,15 @@ func findGoModulePath() (string, error) {
cmd.Env = append(cmd.Env, os.Environ()...)
out, err := cmd.Output()
if err != nil {
- if exitErr, isExitErr := err.(*exec.ExitError); isExitErr {
+ var exitErr *exec.ExitError
+ if errors.As(err, &exitErr) {
err = fmt.Errorf("%s", string(exitErr.Stderr))
}
return "", err
}
mod := goMod{}
- if err := json.Unmarshal(out, &mod); err != nil {
- return "", err
+ if err = json.Unmarshal(out, &mod); err != nil {
+ return "", fmt.Errorf("failed to unmarshal go.mod: %w", err)
}
return mod.Module.Path, nil
}
@@ -77,12 +79,13 @@ func FindCurrentRepo() (string, error) {
cmd := exec.Command("go", "mod", "init")
cmd.Env = append(cmd.Env, os.Environ()...)
if _, err := cmd.Output(); err != nil {
- if exitErr, isExitErr := err.(*exec.ExitError); isExitErr {
+ var exitErr *exec.ExitError
+ if errors.As(err, &exitErr) {
err = fmt.Errorf("%s", string(exitErr.Stderr))
}
// give up, let the user figure it out
return "", fmt.Errorf("could not determine repository path from module data, "+
- "package data, or by initializing a module: %v", err)
+ "package data, or by initializing a module: %w", err)
}
//nolint:errcheck
defer os.Remove("go.mod") // clean up after ourselves
diff --git a/pkg/plugins/golang/repository_test.go b/pkg/plugins/golang/repository_test.go
new file mode 100644
index 00000000000..ee86a29a12d
--- /dev/null
+++ b/pkg/plugins/golang/repository_test.go
@@ -0,0 +1,111 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package golang
+
+import (
+ "os"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("golang:repository", func() {
+ var (
+ tmpDir string
+ oldDir string
+ )
+
+ BeforeEach(func() {
+ var err error
+ tmpDir, err = os.MkdirTemp("", "repo-test")
+ Expect(err).NotTo(HaveOccurred())
+ oldDir, err = os.Getwd()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(os.Chdir(tmpDir)).To(Succeed())
+ })
+
+ AfterEach(func() {
+ Expect(os.Chdir(oldDir)).To(Succeed())
+ Expect(os.RemoveAll(tmpDir)).To(Succeed())
+ })
+
+ When("go.mod exists", func() {
+ BeforeEach(func() {
+ // Simulate `go mod edit -json` output by writing a go.mod file and using go commands
+ Expect(os.WriteFile("go.mod", []byte("module github.com/example/repo\n"), 0o644)).To(Succeed())
+ })
+
+ It("findGoModulePath returns the module path", func() {
+ path, err := findGoModulePath()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(path).To(Equal("github.com/example/repo"))
+ })
+
+ It("FindCurrentRepo returns the module path", func() {
+ path, err := FindCurrentRepo()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(path).To(Equal("github.com/example/repo"))
+ })
+ })
+
+ When("go.mod does not exist", func() {
+ It("findGoModulePath returns error", func() {
+ got, err := findGoModulePath()
+ Expect(err).To(HaveOccurred())
+ Expect(got).To(Equal(""))
+ })
+
+ It("FindCurrentRepo tries to init a module and returns the path or a helpful error", func() {
+ path, err := FindCurrentRepo()
+ if err != nil {
+ Expect(path).To(Equal(""))
+ Expect(err.Error()).To(ContainSubstring("could not determine repository path"))
+ } else {
+ Expect(path).NotTo(BeEmpty())
+ }
+ })
+ })
+
+ When("go mod command fails with exec.ExitError", func() {
+ var origPath string
+
+ BeforeEach(func() {
+ // Move go binary out of PATH to force exec error
+ origPath = os.Getenv("PATH")
+ // Set PATH to empty so "go" cannot be found
+ Expect(os.Setenv("PATH", "")).To(Succeed())
+ })
+
+ AfterEach(func() {
+ Expect(os.Setenv("PATH", origPath)).To(Succeed())
+ })
+
+ It("findGoModulePath returns error with stderr message", func() {
+ got, err := findGoModulePath()
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).NotTo(BeEmpty())
+ Expect(got).To(Equal(""))
+ })
+
+ It("FindCurrentRepo returns error with stderr message", func() {
+ got, err := FindCurrentRepo()
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("could not determine repository path"))
+ Expect(got).To(Equal(""))
+ })
+ })
+})
diff --git a/pkg/plugins/golang/suite_test.go b/pkg/plugins/golang/suite_test.go
index 2e805c0e98d..c38994dd3ce 100644
--- a/pkg/plugins/golang/suite_test.go
+++ b/pkg/plugins/golang/suite_test.go
@@ -1,5 +1,5 @@
/*
-Copyright 2021 The Kubernetes Authors.
+Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -19,7 +19,7 @@ package golang
import (
"testing"
- . "github.com/onsi/ginkgo"
+ . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
diff --git a/pkg/plugins/golang/v2/api.go b/pkg/plugins/golang/v2/api.go
deleted file mode 100644
index 99704870d37..00000000000
--- a/pkg/plugins/golang/v2/api.go
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v2
-
-import (
- "bufio"
- "errors"
- "fmt"
- "os"
-
- "github.com/spf13/pflag"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugin"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util"
- goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds"
-)
-
-var _ plugin.CreateAPISubcommand = &createAPISubcommand{}
-
-type createAPISubcommand struct {
- config config.Config
-
- options *goPlugin.Options
-
- resource *resource.Resource
-
- // Check if we have to scaffold resource and/or controller
- resourceFlag *pflag.Flag
- controllerFlag *pflag.Flag
-
- // force indicates that the resource should be created even if it already exists
- force bool
-
- // runMake indicates whether to run make or not after scaffolding APIs
- runMake bool
-}
-
-func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
- subcmdMeta.Description = `Scaffold a Kubernetes API by writing a Resource definition and/or a Controller.
-
-If information about whether the resource and controller should be scaffolded
-was not explicitly provided, it will prompt the user if they should be.
-
-After the scaffold is written, the dependencies will be updated and
-make generate will be run.
-`
- subcmdMeta.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate
- %[1]s create api --group ship --version v1beta1 --kind Frigate
-
- # Edit the API Scheme
- nano api/v1beta1/frigate_types.go
-
- # Edit the Controller
- nano controllers/frigate/frigate_controller.go
-
- # Edit the Controller Test
- nano controllers/frigate/frigate_controller_test.go
-
- # Install CRDs into the Kubernetes cluster using kubectl apply
- make install
-
- # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config
- make run
-`, cliMeta.CommandName)
-}
-
-func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) {
- fs.BoolVar(&p.runMake, "make", true, "if true, run `make generate` after generating files")
-
- fs.BoolVar(&p.force, "force", false,
- "attempt to create resource even if it already exists")
-
- p.options = &goPlugin.Options{CRDVersion: "v1beta1"}
- // p.options.Plural can be set to specify an irregular plural form
-
- fs.BoolVar(&p.options.DoAPI, "resource", true,
- "if set, generate the resource without prompting the user")
- p.resourceFlag = fs.Lookup("resource")
- fs.BoolVar(&p.options.Namespaced, "namespaced", true, "resource is namespaced")
-
- fs.BoolVar(&p.options.DoController, "controller", true,
- "if set, generate the controller without prompting the user")
- p.controllerFlag = fs.Lookup("controller")
-}
-
-func (p *createAPISubcommand) InjectConfig(c config.Config) error {
- p.config = c
-
- return nil
-}
-
-func (p *createAPISubcommand) InjectResource(res *resource.Resource) error {
- p.resource = res
-
- if p.resource.Group == "" {
- return fmt.Errorf("group cannot be empty")
- }
-
- // Ask for API and Controller if not specified
- reader := bufio.NewReader(os.Stdin)
- if !p.resourceFlag.Changed {
- fmt.Println("Create Resource [y/n]")
- p.options.DoAPI = util.YesNo(reader)
- }
- if !p.controllerFlag.Changed {
- fmt.Println("Create Controller [y/n]")
- p.options.DoController = util.YesNo(reader)
- }
-
- p.options.UpdateResource(p.resource, p.config)
-
- if err := p.resource.Validate(); err != nil {
- return err
- }
-
- // In case we want to scaffold a resource API we need to do some checks
- if p.options.DoAPI {
- // Check that resource doesn't have the API scaffolded or flag force was set
- if res, err := p.config.GetResource(p.resource.GVK); err == nil && res.HasAPI() && !p.force {
- return errors.New("API resource already exists")
- }
-
- // Check that the provided group can be added to the project
- if !p.config.IsMultiGroup() && p.config.ResourcesLength() != 0 && !p.config.HasGroup(p.resource.Group) {
- return fmt.Errorf("multiple groups are not allowed by default, to enable multi-group visit %s",
- "https://kubebuilder.io/migration/multi-group.html")
- }
- }
-
- return nil
-}
-
-func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error {
- scaffolder := scaffolds.NewAPIScaffolder(p.config, *p.resource, p.force)
- scaffolder.InjectFS(fs)
- return scaffolder.Scaffold()
-}
-
-func (p *createAPISubcommand) PostScaffold() error {
- err := util.RunCmd("Update dependencies", "go", "mod", "tidy")
- if err != nil {
- return err
- }
-
- if p.runMake && p.resource.HasAPI() {
- err = util.RunCmd("Running make", "make", "generate")
- if err != nil {
- return err
- }
- }
-
- return nil
-}
diff --git a/pkg/plugins/golang/v2/edit.go b/pkg/plugins/golang/v2/edit.go
deleted file mode 100644
index bdab9054400..00000000000
--- a/pkg/plugins/golang/v2/edit.go
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v2
-
-import (
- "fmt"
-
- "github.com/spf13/pflag"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugin"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds"
-)
-
-var _ plugin.EditSubcommand = &editSubcommand{}
-
-type editSubcommand struct {
- config config.Config
-
- multigroup bool
-}
-
-func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
- subcmdMeta.Description = `This command will edit the project configuration.
-Features supported:
- - Toggle between single or multi group projects.
-`
- subcmdMeta.Examples = fmt.Sprintf(` # Enable the multigroup layout
- %[1]s edit --multigroup
-
- # Disable the multigroup layout
- %[1]s edit --multigroup=false
-`, cliMeta.CommandName)
-}
-
-func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
- fs.BoolVar(&p.multigroup, "multigroup", false, "enable or disable multigroup layout")
-}
-
-func (p *editSubcommand) InjectConfig(c config.Config) error {
- p.config = c
-
- return nil
-}
-
-func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
- scaffolder := scaffolds.NewEditScaffolder(p.config, p.multigroup)
- scaffolder.InjectFS(fs)
- return scaffolder.Scaffold()
-}
diff --git a/pkg/plugins/golang/v2/init.go b/pkg/plugins/golang/v2/init.go
deleted file mode 100644
index 6c9c7bbdb3d..00000000000
--- a/pkg/plugins/golang/v2/init.go
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v2
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
-
- "github.com/spf13/pflag"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2"
- "sigs.k8s.io/kubebuilder/v3/pkg/internal/validation"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugin"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds"
-)
-
-var _ plugin.InitSubcommand = &initSubcommand{}
-
-type initSubcommand struct {
- config config.Config
-
- // For help text.
- commandName string
-
- // boilerplate options
- license string
- owner string
-
- // config options
- domain string
- repo string
- name string
-
- // flags
- fetchDeps bool
- skipGoVersionCheck bool
-}
-
-func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
- p.commandName = cliMeta.CommandName
-
- subcmdMeta.Description = `Initialize a new project including the following files:
- - a "go.mod" with project dependencies
- - a "PROJECT" file that stores project configuration
- - a "Makefile" with several useful make targets for the project
- - several YAML files for project deployment under the "config" directory
- - a "main.go" file that creates the manager that will run the project controllers
-`
- subcmdMeta.Examples = fmt.Sprintf(` # Initialize a new project with your domain and name in copyright
- %[1]s init --plugins go/v2 --domain example.org --owner "Your name"
-
- # Initialize a new project defining an specific project version
- %[1]s init --plugins go/v2 --project-version 2
-`, cliMeta.CommandName)
-}
-
-func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) {
- fs.BoolVar(&p.skipGoVersionCheck, "skip-go-version-check",
- false, "if specified, skip checking the Go version")
-
- // dependency args
- fs.BoolVar(&p.fetchDeps, "fetch-deps", true, "ensure dependencies are downloaded")
-
- // boilerplate args
- fs.StringVar(&p.license, "license", "apache2",
- "license to use to boilerplate, may be one of 'apache2', 'none'")
- fs.StringVar(&p.owner, "owner", "", "owner to add to the copyright")
-
- // project args
- fs.StringVar(&p.domain, "domain", "my.domain", "domain for groups")
- fs.StringVar(&p.repo, "repo", "", "name to use for go module (e.g., github.com/user/repo), "+
- "defaults to the go package of the current working directory.")
- fs.StringVar(&p.name, "project-name", "", "name of this project")
-}
-
-func (p *initSubcommand) InjectConfig(c config.Config) error {
- p.config = c
-
- if err := p.config.SetDomain(p.domain); err != nil {
- return err
- }
-
- // Try to guess repository if flag is not set.
- if p.repo == "" {
- repoPath, err := golang.FindCurrentRepo()
- if err != nil {
- return fmt.Errorf("error finding current repository: %v", err)
- }
- p.repo = repoPath
- }
- if err := p.config.SetRepository(p.repo); err != nil {
- return err
- }
-
- if p.config.GetVersion().Compare(cfgv2.Version) > 0 {
- // Assign a default project name
- if p.name == "" {
- dir, err := os.Getwd()
- if err != nil {
- return fmt.Errorf("error getting current directory: %v", err)
- }
- p.name = strings.ToLower(filepath.Base(dir))
- }
- // Check if the project name is a valid k8s namespace (DNS 1123 label).
- if err := validation.IsDNS1123Label(p.name); err != nil {
- return fmt.Errorf("project name (%s) is invalid: %v", p.name, err)
- }
- if err := p.config.SetProjectName(p.name); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func (p *initSubcommand) PreScaffold(machinery.Filesystem) error {
- // Validate the supported go versions
- if !p.skipGoVersionCheck {
- if err := golang.ValidateGoVersion(); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error {
- scaffolder := scaffolds.NewInitScaffolder(p.config, p.license, p.owner)
- scaffolder.InjectFS(fs)
- err := scaffolder.Scaffold()
- if err != nil {
- return err
- }
-
- if !p.fetchDeps {
- fmt.Println("Skipping fetching dependencies.")
- return nil
- }
-
- // Ensure that we are pinning controller-runtime version
- // xref: https://github.com/kubernetes-sigs/kubebuilder/issues/997
- err = util.RunCmd("Get controller runtime", "go", "get",
- "sigs.k8s.io/controller-runtime@"+scaffolds.ControllerRuntimeVersion)
- if err != nil {
- return err
- }
-
- return nil
-}
-
-func (p *initSubcommand) PostScaffold() error {
- err := util.RunCmd("Update dependencies", "go", "mod", "tidy")
- if err != nil {
- return err
- }
-
- fmt.Printf("Next: define a resource with:\n$ %s create api\n", p.commandName)
- return nil
-}
diff --git a/pkg/plugins/golang/v2/plugin.go b/pkg/plugins/golang/v2/plugin.go
deleted file mode 100644
index fd1d7779416..00000000000
--- a/pkg/plugins/golang/v2/plugin.go
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v2
-
-import (
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2"
- cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugin"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins"
-)
-
-const pluginName = "go." + plugins.DefaultNameQualifier
-
-var (
- pluginVersion = plugin.Version{Number: 2}
- supportedProjectVersions = []config.Version{cfgv2.Version, cfgv3.Version}
-)
-
-var _ plugin.Full = Plugin{}
-
-// Plugin implements the plugin.Full interface
-type Plugin struct {
- initSubcommand
- createAPISubcommand
- createWebhookSubcommand
- editSubcommand
-}
-
-// Name returns the name of the plugin
-func (Plugin) Name() string { return pluginName }
-
-// Version returns the version of the plugin
-func (Plugin) Version() plugin.Version { return pluginVersion }
-
-// SupportedProjectVersions returns an array with all project versions supported by the plugin
-func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions }
-
-// GetInitSubcommand will return the subcommand which is responsible for initializing and common scaffolding
-func (p Plugin) GetInitSubcommand() plugin.InitSubcommand { return &p.initSubcommand }
-
-// GetCreateAPISubcommand will return the subcommand which is responsible for scaffolding apis
-func (p Plugin) GetCreateAPISubcommand() plugin.CreateAPISubcommand { return &p.createAPISubcommand }
-
-// GetCreateWebhookSubcommand will return the subcommand which is responsible for scaffolding webhooks
-func (p Plugin) GetCreateWebhookSubcommand() plugin.CreateWebhookSubcommand {
- return &p.createWebhookSubcommand
-}
-
-// GetEditSubcommand will return the subcommand which is responsible for editing the scaffold of the project
-func (p Plugin) GetEditSubcommand() plugin.EditSubcommand { return &p.editSubcommand }
diff --git a/pkg/plugins/golang/v2/scaffolds/api.go b/pkg/plugins/golang/v2/scaffolds/api.go
deleted file mode 100644
index 46d7f20fc42..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/api.go
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package scaffolds
-
-import (
- "fmt"
-
- "github.com/spf13/afero"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/api"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/samples"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/hack"
-)
-
-var _ plugins.Scaffolder = &apiScaffolder{}
-
-// apiScaffolder contains configuration for generating scaffolding for Go type
-// representing the API and controller that implements the behavior for the API.
-type apiScaffolder struct {
- config config.Config
- resource resource.Resource
-
- // fs is the filesystem that will be used by the scaffolder
- fs machinery.Filesystem
-
- // force indicates whether to scaffold controller files even if it exists or not
- force bool
-}
-
-// NewAPIScaffolder returns a new Scaffolder for API/controller creation operations
-func NewAPIScaffolder(config config.Config, res resource.Resource, force bool) plugins.Scaffolder {
- return &apiScaffolder{
- config: config,
- resource: res,
- force: force,
- }
-}
-
-// InjectFS implements cmdutil.Scaffolder
-func (s *apiScaffolder) InjectFS(fs machinery.Filesystem) {
- s.fs = fs
-}
-
-// Scaffold implements cmdutil.Scaffolder
-func (s *apiScaffolder) Scaffold() error {
- fmt.Println("Writing scaffold for you to edit...")
-
- // Load the boilerplate
- boilerplate, err := afero.ReadFile(s.fs.FS, hack.DefaultBoilerplatePath)
- if err != nil {
- return fmt.Errorf("error scaffolding API/controller: unable to load boilerplate: %w", err)
- }
-
- // Initialize the machinery.Scaffold that will write the files to disk
- scaffold := machinery.NewScaffold(s.fs,
- machinery.WithConfig(s.config),
- machinery.WithBoilerplate(string(boilerplate)),
- machinery.WithResource(&s.resource),
- )
-
- // Keep track of these values before the update
- doAPI := s.resource.HasAPI()
- doController := s.resource.HasController()
-
- // Project version v2 only tracked GVK triplets of each resource.
- // As they were only tracked when the API was scaffolded, the presence of a
- // resource in the config file was used in webhook creation to verify that
- // the API had been scaffolded previously. From project version v3 onwards
- // this information is stored in the API field of the resource, so we can
- // update the resources except for project version 2 when no API was scaffolded.
- if doAPI || s.config.GetVersion().Compare(cfgv2.Version) == 1 {
- if err := s.config.UpdateResource(s.resource); err != nil {
- return fmt.Errorf("error updating resource: %w", err)
- }
- }
-
- if doAPI {
- if err := scaffold.Execute(
- &api.Types{Force: s.force},
- &api.Group{},
- &samples.CRDSample{Force: s.force},
- &rbac.CRDEditorRole{},
- &rbac.CRDViewerRole{},
- &patches.EnableWebhookPatch{},
- &patches.EnableCAInjectionPatch{},
- ); err != nil {
- return fmt.Errorf("error scaffolding APIs: %w", err)
- }
-
- if err := scaffold.Execute(
- &crd.Kustomization{},
- &crd.KustomizeConfig{},
- ); err != nil {
- return fmt.Errorf("error scaffolding kustomization: %v", err)
- }
- }
-
- if doController {
- if err := scaffold.Execute(
- &controllers.SuiteTest{Force: s.force},
- &controllers.Controller{ControllerRuntimeVersion: ControllerRuntimeVersion, Force: s.force},
- ); err != nil {
- return fmt.Errorf("error scaffolding controller: %v", err)
- }
- }
-
- if err := scaffold.Execute(
- &templates.MainUpdater{WireResource: doAPI, WireController: doController},
- ); err != nil {
- return fmt.Errorf("error updating main.go: %v", err)
- }
-
- return nil
-}
diff --git a/pkg/plugins/golang/v2/scaffolds/doc.go b/pkg/plugins/golang/v2/scaffolds/doc.go
deleted file mode 100644
index d969260e58b..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/doc.go
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-// Package scaffolds contains libraries for scaffolding code to use with controller-runtime
-package scaffolds
diff --git a/pkg/plugins/golang/v2/scaffolds/edit.go b/pkg/plugins/golang/v2/scaffolds/edit.go
deleted file mode 100644
index 0e4f39751c2..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/edit.go
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package scaffolds
-
-import (
- "fmt"
- "strings"
-
- "github.com/spf13/afero"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins"
-)
-
-var _ plugins.Scaffolder = &editScaffolder{}
-
-type editScaffolder struct {
- config config.Config
- multigroup bool
-
- // fs is the filesystem that will be used by the scaffolder
- fs machinery.Filesystem
-}
-
-// NewEditScaffolder returns a new Scaffolder for configuration edit operations
-func NewEditScaffolder(config config.Config, multigroup bool) plugins.Scaffolder {
- return &editScaffolder{
- config: config,
- multigroup: multigroup,
- }
-}
-
-// InjectFS implements cmdutil.Scaffolder
-func (s *editScaffolder) InjectFS(fs machinery.Filesystem) {
- s.fs = fs
-}
-
-// Scaffold implements cmdutil.Scaffolder
-func (s *editScaffolder) Scaffold() error {
- filename := "Dockerfile"
- bs, err := afero.ReadFile(s.fs.FS, filename)
- if err != nil {
- return err
- }
- str := string(bs)
-
- // update dockerfile
- if s.multigroup {
- str, err = ensureExistAndReplace(
- str,
- "COPY api/ api/",
- `COPY apis/ apis/`)
- } else {
- str, err = ensureExistAndReplace(
- str,
- "COPY apis/ apis/",
- `COPY api/ api/`)
- }
-
- // Ignore the error encountered, if the file is already in desired format.
- if err != nil && s.multigroup != s.config.IsMultiGroup() {
- return err
- }
-
- if s.multigroup {
- _ = s.config.SetMultiGroup()
- } else {
- _ = s.config.ClearMultiGroup()
- }
-
- // Check if the str is not empty, because when the file is already in desired format it will return empty string
- // because there is nothing to replace.
- if str != "" {
- // TODO: instead of writing it directly, we should use the scaffolding machinery for consistency
- return afero.WriteFile(s.fs.FS, filename, []byte(str), 0644)
- }
-
- return nil
-}
-
-func ensureExistAndReplace(input, match, replace string) (string, error) {
- if !strings.Contains(input, match) {
- return "", fmt.Errorf("can't find %q", match)
- }
- return strings.Replace(input, match, replace, -1), nil
-}
diff --git a/pkg/plugins/golang/v2/scaffolds/init.go b/pkg/plugins/golang/v2/scaffolds/init.go
deleted file mode 100644
index 4651a109914..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/init.go
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package scaffolds
-
-import (
- "fmt"
-
- "github.com/spf13/afero"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/hack"
-)
-
-const (
- // ControllerRuntimeVersion is the kubernetes-sigs/controller-runtime version to be used in the project
- ControllerRuntimeVersion = "v0.6.4"
- // ControllerToolsVersion is the kubernetes-sigs/controller-tools version to be used in the project
- ControllerToolsVersion = "v0.3.0"
- // KustomizeVersion is the kubernetes-sigs/kustomize version to be used in the project
- KustomizeVersion = "v3.5.4"
-
- imageName = "controller:latest"
-)
-
-var _ plugins.Scaffolder = &initScaffolder{}
-
-type initScaffolder struct {
- config config.Config
- boilerplatePath string
- license string
- owner string
-
- // fs is the filesystem that will be used by the scaffolder
- fs machinery.Filesystem
-}
-
-// NewInitScaffolder returns a new Scaffolder for project initialization operations
-func NewInitScaffolder(config config.Config, license, owner string) plugins.Scaffolder {
- return &initScaffolder{
- config: config,
- boilerplatePath: hack.DefaultBoilerplatePath,
- license: license,
- owner: owner,
- }
-}
-
-// InjectFS implements cmdutil.Scaffolder
-func (s *initScaffolder) InjectFS(fs machinery.Filesystem) {
- s.fs = fs
-}
-
-// Scaffold implements cmdutil.Scaffolder
-func (s *initScaffolder) Scaffold() error {
- fmt.Println("Writing scaffold for you to edit...")
-
- // Initialize the machinery.Scaffold that will write the boilerplate file to disk
- // The boilerplate file needs to be scaffolded as a separate step as it is going to
- // be used by the rest of the files, even those scaffolded in this command call.
- scaffold := machinery.NewScaffold(s.fs,
- machinery.WithConfig(s.config),
- )
-
- bpFile := &hack.Boilerplate{
- License: s.license,
- Owner: s.owner,
- }
- bpFile.Path = s.boilerplatePath
- if err := scaffold.Execute(bpFile); err != nil {
- return err
- }
-
- boilerplate, err := afero.ReadFile(s.fs.FS, s.boilerplatePath)
- if err != nil {
- return err
- }
-
- // Initialize the machinery.Scaffold that will write the files to disk
- scaffold = machinery.NewScaffold(s.fs,
- machinery.WithConfig(s.config),
- machinery.WithBoilerplate(string(boilerplate)),
- )
-
- return scaffold.Execute(
- &rbac.Kustomization{},
- &rbac.AuthProxyRole{},
- &rbac.AuthProxyRoleBinding{},
- &rbac.AuthProxyService{},
- &rbac.AuthProxyClientRole{},
- &rbac.RoleBinding{},
- &rbac.LeaderElectionRole{},
- &rbac.LeaderElectionRoleBinding{},
- &manager.Kustomization{},
- &manager.Config{Image: imageName},
- &templates.Main{},
- &templates.GoMod{ControllerRuntimeVersion: ControllerRuntimeVersion},
- &templates.GitIgnore{},
- &templates.Makefile{
- Image: imageName,
- BoilerplatePath: s.boilerplatePath,
- ControllerToolsVersion: ControllerToolsVersion,
- KustomizeVersion: KustomizeVersion,
- },
- &templates.Dockerfile{},
- &kdefault.Kustomization{},
- &kdefault.ManagerAuthProxyPatch{},
- &kdefault.ManagerWebhookPatch{},
- &kdefault.WebhookCAInjectionPatch{},
- &webhook.Kustomization{},
- &webhook.KustomizeConfig{},
- &webhook.Service{},
- &prometheus.Kustomization{},
- &prometheus.Monitor{},
- &certmanager.Certificate{},
- &certmanager.Kustomization{},
- &certmanager.KustomizeConfig{},
- )
-}
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/group.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/group.go
deleted file mode 100644
index ba9f21f6acc..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/group.go
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package api
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Group{}
-
-// Group scaffolds the file that defines the registration methods for a certain group and version
-type Group struct {
- machinery.TemplateMixin
- machinery.MultiGroupMixin
- machinery.BoilerplateMixin
- machinery.ResourceMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Group) SetTemplateDefaults() error {
- if f.Path == "" {
- if f.MultiGroup {
- f.Path = filepath.Join("apis", "%[group]", "%[version]", "groupversion_info.go")
- } else {
- f.Path = filepath.Join("api", "%[version]", "groupversion_info.go")
- }
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
-
- f.TemplateBody = groupTemplate
-
- return nil
-}
-
-//nolint:lll
-const groupTemplate = `{{ .Boilerplate }}
-
-// Package {{ .Resource.Version }} contains API Schema definitions for the {{ .Resource.Group }} {{ .Resource.Version }} API group
-//+kubebuilder:object:generate=true
-//+groupName={{ .Resource.QualifiedGroup }}
-package {{ .Resource.Version }}
-
-import (
- "k8s.io/apimachinery/pkg/runtime/schema"
- "sigs.k8s.io/controller-runtime/pkg/scheme"
-)
-
-var (
- // GroupVersion is group version used to register these objects
- GroupVersion = schema.GroupVersion{Group: "{{ .Resource.QualifiedGroup }}", Version: "{{ .Resource.Version }}"}
-
- // SchemeBuilder is used to add go types to the GroupVersionKind scheme
- SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
-
- // AddToScheme adds the types in this group-version to the given scheme.
- AddToScheme = SchemeBuilder.AddToScheme
-)
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/types.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/types.go
deleted file mode 100644
index d80e5f33b93..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/types.go
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package api
-
-import (
- "fmt"
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Types{}
-
-// Types scaffolds the file that defines the schema for a CRD
-// nolint:maligned
-type Types struct {
- machinery.TemplateMixin
- machinery.MultiGroupMixin
- machinery.BoilerplateMixin
- machinery.ResourceMixin
-
- Force bool
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Types) SetTemplateDefaults() error {
- if f.Path == "" {
- if f.MultiGroup {
- f.Path = filepath.Join("apis", "%[group]", "%[version]", "%[kind]_types.go")
- } else {
- f.Path = filepath.Join("api", "%[version]", "%[kind]_types.go")
- }
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
- fmt.Println(f.Path)
-
- f.TemplateBody = typesTemplate
-
- if f.Force {
- f.IfExistsAction = machinery.OverwriteFile
- } else {
- f.IfExistsAction = machinery.Error
- }
-
- return nil
-}
-
-const typesTemplate = `{{ .Boilerplate }}
-
-package {{ .Resource.Version }}
-
-import (
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-)
-
-// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
-// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
-
-// {{ .Resource.Kind }}Spec defines the desired state of {{ .Resource.Kind }}
-type {{ .Resource.Kind }}Spec struct {
- // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
- // Important: Run "make" to regenerate code after modifying this file
-
- // Foo is an example field of {{ .Resource.Kind }}. Edit {{ lower .Resource.Kind }}_types.go to remove/update
- Foo string ` + "`" + `json:"foo,omitempty"` + "`" + `
-}
-
-// {{ .Resource.Kind }}Status defines the observed state of {{ .Resource.Kind }}
-type {{ .Resource.Kind }}Status struct {
- // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
- // Important: Run "make" to regenerate code after modifying this file
-}
-
-//+kubebuilder:object:root=true
-//+kubebuilder:subresource:status
-{{- if not .Resource.API.Namespaced }}
-//+kubebuilder:resource:scope=Cluster
-{{- end }}
-
-// {{ .Resource.Kind }} is the Schema for the {{ .Resource.Plural }} API
-type {{ .Resource.Kind }} struct {
- metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + `
- metav1.ObjectMeta ` + "`" + `json:"metadata,omitempty"` + "`" + `
-
- Spec {{ .Resource.Kind }}Spec ` + "`" + `json:"spec,omitempty"` + "`" + `
- Status {{ .Resource.Kind }}Status ` + "`" + `json:"status,omitempty"` + "`" + `
-}
-
-//+kubebuilder:object:root=true
-
-// {{ .Resource.Kind }}List contains a list of {{ .Resource.Kind }}
-type {{ .Resource.Kind }}List struct {
- metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + `
- metav1.ListMeta ` + "`" + `json:"metadata,omitempty"` + "`" + `
- Items []{{ .Resource.Kind }} ` + "`" + `json:"items"` + "`" + `
-}
-
-func init() {
- SchemeBuilder.Register(&{{ .Resource.Kind }}{}, &{{ .Resource.Kind }}List{})
-}
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/webhook.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/webhook.go
deleted file mode 100644
index 5def3986d32..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/webhook.go
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package api
-
-import (
- "fmt"
- "path/filepath"
- "strings"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Webhook{}
-
-// Webhook scaffolds the file that defines a webhook for a CRD or a builtin resource
-type Webhook struct { // nolint:maligned
- machinery.TemplateMixin
- machinery.MultiGroupMixin
- machinery.BoilerplateMixin
- machinery.ResourceMixin
-
- // Is the Group domain for the Resource replacing '.' with '-'
- QualifiedGroupWithDash string
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Webhook) SetTemplateDefaults() error {
- if f.Path == "" {
- if f.MultiGroup {
- f.Path = filepath.Join("apis", "%[group]", "%[version]", "%[kind]_webhook.go")
- } else {
- f.Path = filepath.Join("api", "%[version]", "%[kind]_webhook.go")
- }
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
- fmt.Println(f.Path)
-
- webhookTemplate := webhookTemplate
- if f.Resource.HasDefaultingWebhook() {
- webhookTemplate = webhookTemplate + defaultingWebhookTemplate
- }
- if f.Resource.HasValidationWebhook() {
- webhookTemplate = webhookTemplate + validatingWebhookTemplate
- }
- f.TemplateBody = webhookTemplate
-
- f.IfExistsAction = machinery.Error
-
- f.QualifiedGroupWithDash = strings.Replace(f.Resource.QualifiedGroup(), ".", "-", -1)
-
- return nil
-}
-
-const (
- webhookTemplate = `{{ .Boilerplate }}
-
-package {{ .Resource.Version }}
-
-import (
- ctrl "sigs.k8s.io/controller-runtime"
- logf "sigs.k8s.io/controller-runtime/pkg/log"
- {{- if .Resource.HasValidationWebhook }}
- "k8s.io/apimachinery/pkg/runtime"
- {{- end }}
- {{- if or .Resource.HasValidationWebhook .Resource.HasDefaultingWebhook }}
- "sigs.k8s.io/controller-runtime/pkg/webhook"
- {{- end }}
-)
-
-// log is for logging in this package.
-var {{ lower .Resource.Kind }}log = logf.Log.WithName("{{ lower .Resource.Kind }}-resource")
-
-func (r *{{ .Resource.Kind }}) SetupWebhookWithManager(mgr ctrl.Manager) error {
- return ctrl.NewWebhookManagedBy(mgr).
- For(r).
- Complete()
-}
-
-// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
-`
-
- //nolint:lll
- defaultingWebhookTemplate = `
-//+kubebuilder:webhook:path=/mutate-{{ .QualifiedGroupWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=true,failurePolicy=fail,groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=m{{ lower .Resource.Kind }}.kb.io
-
-var _ webhook.Defaulter = &{{ .Resource.Kind }}{}
-
-// Default implements webhook.Defaulter so a webhook will be registered for the type
-func (r *{{ .Resource.Kind }}) Default() {
- {{ lower .Resource.Kind }}log.Info("default", "name", r.Name)
-
- // TODO(user): fill in your defaulting logic.
-}
-`
- //nolint:lll
- validatingWebhookTemplate = `
-// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
-//+kubebuilder:webhook:verbs=create;update,path=/validate-{{ .QualifiedGroupWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=false,failurePolicy=fail,groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},versions={{ .Resource.Version }},name=v{{ lower .Resource.Kind }}.kb.io
-
-var _ webhook.Validator = &{{ .Resource.Kind }}{}
-
-// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
-func (r *{{ .Resource.Kind }}) ValidateCreate() error {
- {{ lower .Resource.Kind }}log.Info("validate create", "name", r.Name)
-
- // TODO(user): fill in your validation logic upon object creation.
- return nil
-}
-
-// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
-func (r *{{ .Resource.Kind }}) ValidateUpdate(old runtime.Object) error {
- {{ lower .Resource.Kind }}log.Info("validate update", "name", r.Name)
-
- // TODO(user): fill in your validation logic upon object update.
- return nil
-}
-
-// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
-func (r *{{ .Resource.Kind }}) ValidateDelete() error {
- {{ lower .Resource.Kind }}log.Info("validate delete", "name", r.Name)
-
- // TODO(user): fill in your validation logic upon object deletion.
- return nil
-}
-`
-)
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/certificate.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/certificate.go
deleted file mode 100644
index 4d17b8dff60..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/certificate.go
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package certmanager
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Certificate{}
-
-// Certificate scaffolds a file that defines the issuer CR and the certificate CR
-type Certificate struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Certificate) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "certmanager", "certificate.yaml")
- }
-
- f.TemplateBody = certManagerTemplate
-
- return nil
-}
-
-const certManagerTemplate = `# The following manifests contain a self-signed issuer CR and a certificate CR.
-# More document can be found at https://docs.cert-manager.io
-# WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for
-# breaking changes
-apiVersion: cert-manager.io/v1alpha2
-kind: Issuer
-metadata:
- name: selfsigned-issuer
- namespace: system
-spec:
- selfSigned: {}
----
-apiVersion: cert-manager.io/v1alpha2
-kind: Certificate
-metadata:
- name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml
- namespace: system
-spec:
- # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize
- dnsNames:
- - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc
- - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local
- issuerRef:
- kind: Issuer
- name: selfsigned-issuer
- secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomization.go
deleted file mode 100644
index 78b2c5e9484..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomization.go
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package certmanager
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Kustomization{}
-
-// Kustomization scaffolds a file that defines the kustomization scheme for the certmanager folder
-type Kustomization struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Kustomization) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "certmanager", "kustomization.yaml")
- }
-
- f.TemplateBody = kustomizationTemplate
-
- return nil
-}
-
-const kustomizationTemplate = `resources:
-- certificate.yaml
-
-configurations:
-- kustomizeconfig.yaml
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go
deleted file mode 100644
index e7dbcf8986d..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package certmanager
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &KustomizeConfig{}
-
-// KustomizeConfig scaffolds a file that configures the kustomization for the certmanager folder
-type KustomizeConfig struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *KustomizeConfig) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "certmanager", "kustomizeconfig.yaml")
- }
-
- f.TemplateBody = kustomizeConfigTemplate
-
- return nil
-}
-
-//nolint:lll
-const kustomizeConfigTemplate = `# This configuration is for teaching kustomize how to update name ref and var substitution
-nameReference:
-- kind: Issuer
- group: cert-manager.io
- fieldSpecs:
- - kind: Certificate
- group: cert-manager.io
- path: spec/issuerRef/name
-
-varReference:
-- kind: Certificate
- group: cert-manager.io
- path: spec/commonName
-- kind: Certificate
- group: cert-manager.io
- path: spec/dnsNames
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomization.go
deleted file mode 100644
index 37ec7556cb1..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomization.go
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package crd
-
-import (
- "fmt"
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Kustomization{}
-var _ machinery.Inserter = &Kustomization{}
-
-// Kustomization scaffolds a file that defines the kustomization scheme for the crd folder
-type Kustomization struct {
- machinery.TemplateMixin
- machinery.ResourceMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Kustomization) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "crd", "kustomization.yaml")
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
-
- f.TemplateBody = fmt.Sprintf(kustomizationTemplate,
- machinery.NewMarkerFor(f.Path, resourceMarker),
- machinery.NewMarkerFor(f.Path, webhookPatchMarker),
- machinery.NewMarkerFor(f.Path, caInjectionPatchMarker),
- )
-
- return nil
-}
-
-const (
- resourceMarker = "crdkustomizeresource"
- webhookPatchMarker = "crdkustomizewebhookpatch"
- caInjectionPatchMarker = "crdkustomizecainjectionpatch"
-)
-
-// GetMarkers implements file.Inserter
-func (f *Kustomization) GetMarkers() []machinery.Marker {
- return []machinery.Marker{
- machinery.NewMarkerFor(f.Path, resourceMarker),
- machinery.NewMarkerFor(f.Path, webhookPatchMarker),
- machinery.NewMarkerFor(f.Path, caInjectionPatchMarker),
- }
-}
-
-const (
- resourceCodeFragment = `- bases/%s_%s.yaml
-`
- webhookPatchCodeFragment = `#- patches/webhook_in_%s.yaml
-`
- caInjectionPatchCodeFragment = `#- patches/cainjection_in_%s.yaml
-`
-)
-
-// GetCodeFragments implements file.Inserter
-func (f *Kustomization) GetCodeFragments() machinery.CodeFragmentsMap {
- fragments := make(machinery.CodeFragmentsMap, 3)
-
- // Generate resource code fragments
- res := make([]string, 0)
- res = append(res, fmt.Sprintf(resourceCodeFragment, f.Resource.QualifiedGroup(), f.Resource.Plural))
-
- // Generate resource code fragments
- webhookPatch := make([]string, 0)
- webhookPatch = append(webhookPatch, fmt.Sprintf(webhookPatchCodeFragment, f.Resource.Plural))
-
- // Generate resource code fragments
- caInjectionPatch := make([]string, 0)
- caInjectionPatch = append(caInjectionPatch, fmt.Sprintf(caInjectionPatchCodeFragment, f.Resource.Plural))
-
- // Only store code fragments in the map if the slices are non-empty
- if len(res) != 0 {
- fragments[machinery.NewMarkerFor(f.Path, resourceMarker)] = res
- }
- if len(webhookPatch) != 0 {
- fragments[machinery.NewMarkerFor(f.Path, webhookPatchMarker)] = webhookPatch
- }
- if len(caInjectionPatch) != 0 {
- fragments[machinery.NewMarkerFor(f.Path, caInjectionPatchMarker)] = caInjectionPatch
- }
-
- return fragments
-}
-
-var kustomizationTemplate = `# This kustomization.yaml is not intended to be run by itself,
-# since it depends on service name and namespace that are out of this kustomize package.
-# It should be run by config/default
-resources:
-%s
-
-patchesStrategicMerge:
-# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
-# patches here are for enabling the conversion webhook for each CRD
-%s
-
-# [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix.
-# patches here are for enabling the CA injection for each CRD
-%s
-
-# the following config is for teaching kustomize how to do kustomization for CRDs.
-configurations:
-- kustomizeconfig.yaml
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomizeconfig.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomizeconfig.go
deleted file mode 100644
index 0013026f81c..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomizeconfig.go
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package crd
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &KustomizeConfig{}
-
-// KustomizeConfig scaffolds a file that configures the kustomization for the crd folder
-type KustomizeConfig struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *KustomizeConfig) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "crd", "kustomizeconfig.yaml")
- }
-
- f.TemplateBody = kustomizeConfigTemplate
-
- return nil
-}
-
-//nolint:lll
-const kustomizeConfigTemplate = `# This file is for teaching kustomize how to substitute name and namespace reference in CRD
-nameReference:
-- kind: Service
- version: v1
- fieldSpecs:
- - kind: CustomResourceDefinition
- group: apiextensions.k8s.io
- path: spec/conversion/webhookClientConfig/service/name
-
-namespace:
-- kind: CustomResourceDefinition
- group: apiextensions.k8s.io
- path: spec/conversion/webhookClientConfig/service/namespace
- create: false
-
-varReference:
-- path: metadata/annotations
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go
deleted file mode 100644
index 376a058a01c..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package patches
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &EnableCAInjectionPatch{}
-
-// EnableCAInjectionPatch scaffolds a file that defines the patch that injects CA into the CRD
-type EnableCAInjectionPatch struct {
- machinery.TemplateMixin
- machinery.ResourceMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *EnableCAInjectionPatch) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "crd", "patches", "cainjection_in_%[plural].yaml")
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
-
- f.TemplateBody = enableCAInjectionPatchTemplate
-
- return nil
-}
-
-const enableCAInjectionPatchTemplate = `# The following patch adds a directive for certmanager to inject CA into the CRD
-# CRD conversion requires k8s 1.13 or later.
-apiVersion: apiextensions.k8s.io/v1beta1
-kind: CustomResourceDefinition
-metadata:
- annotations:
- cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
- name: {{ .Resource.Plural }}.{{ .Resource.QualifiedGroup }}
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go
deleted file mode 100644
index 1472028b627..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package patches
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &EnableWebhookPatch{}
-
-// EnableWebhookPatch scaffolds a file that defines the patch that enables conversion webhook for the CRD
-type EnableWebhookPatch struct {
- machinery.TemplateMixin
- machinery.ResourceMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *EnableWebhookPatch) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "crd", "patches", "webhook_in_%[plural].yaml")
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
-
- f.TemplateBody = enableWebhookPatchTemplate
-
- return nil
-}
-
-const enableWebhookPatchTemplate = `# The following patch enables conversion webhook for CRD
-# CRD conversion requires k8s 1.13 or later.
-apiVersion: apiextensions.k8s.io/v1beta1
-kind: CustomResourceDefinition
-metadata:
- name: {{ .Resource.Plural }}.{{ .Resource.QualifiedGroup }}
-spec:
- conversion:
- strategy: Webhook
- webhookClientConfig:
- # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank,
- # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager)
- caBundle: Cg==
- service:
- namespace: system
- name: webhook-service
- path: /convert
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go
deleted file mode 100644
index 3cee6b8595d..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package kdefault
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &WebhookCAInjectionPatch{}
-
-// WebhookCAInjectionPatch scaffolds a file that defines the patch that adds annotation to webhooks
-type WebhookCAInjectionPatch struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *WebhookCAInjectionPatch) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "default", "webhookcainjection_patch.yaml")
- }
-
- f.TemplateBody = injectCAPatchTemplate
-
- f.IfExistsAction = machinery.Error
-
- return nil
-}
-
-const injectCAPatchTemplate = `# This patch add annotation to admission webhook config and
-# the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize.
-apiVersion: admissionregistration.k8s.io/v1beta1
-kind: MutatingWebhookConfiguration
-metadata:
- name: mutating-webhook-configuration
- annotations:
- cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
----
-apiVersion: admissionregistration.k8s.io/v1beta1
-kind: ValidatingWebhookConfiguration
-metadata:
- name: validating-webhook-configuration
- annotations:
- cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/kustomization.go
deleted file mode 100644
index 68ecc351cd7..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/kustomization.go
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package kdefault
-
-import (
- "os"
- "path/filepath"
- "strings"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Kustomization{}
-
-// Kustomization scaffolds a file that defines the kustomization scheme for the default overlay folder
-type Kustomization struct {
- machinery.TemplateMixin
- machinery.ProjectNameMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Kustomization) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "default", "kustomization.yaml")
- }
-
- f.TemplateBody = kustomizeTemplate
-
- f.IfExistsAction = machinery.Error
-
- if f.ProjectName == "" {
- // Use directory name as project name, which will be empty if the project version is < v3.
- dir, err := os.Getwd()
- if err != nil {
- return err
- }
- f.ProjectName = strings.ToLower(filepath.Base(dir))
- }
-
- return nil
-}
-
-const kustomizeTemplate = `# Adds namespace to all resources.
-namespace: {{ .ProjectName }}-system
-
-# Value of this field is prepended to the
-# names of all resources, e.g. a deployment named
-# "wordpress" becomes "alices-wordpress".
-# Note that it should also match with the prefix (text before '-') of the namespace
-# field above.
-namePrefix: {{ .ProjectName }}-
-
-# Labels to add to all resources and selectors.
-#commonLabels:
-# someName: someValue
-
-bases:
-- ../crd
-- ../rbac
-- ../manager
-# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
-# crd/kustomization.yaml
-#- ../webhook
-# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
-#- ../certmanager
-# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
-#- ../prometheus
-
-patchesStrategicMerge:
- # Protect the /metrics endpoint by putting it behind auth.
- # If you want your controller-manager to expose the /metrics
- # endpoint w/o any authn/z, please comment the following line.
-- manager_auth_proxy_patch.yaml
-
-# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
-# crd/kustomization.yaml
-#- manager_webhook_patch.yaml
-
-# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
-# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
-# 'CERTMANAGER' needs to be enabled to use ca injection
-#- webhookcainjection_patch.yaml
-
-# the following config is for teaching kustomize how to do var substitution
-vars:
-# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
-#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
-# objref:
-# kind: Certificate
-# group: cert-manager.io
-# version: v1alpha2
-# name: serving-cert # this name should match the one in certificate.yaml
-# fieldref:
-# fieldpath: metadata.namespace
-#- name: CERTIFICATE_NAME
-# objref:
-# kind: Certificate
-# group: cert-manager.io
-# version: v1alpha2
-# name: serving-cert # this name should match the one in certificate.yaml
-#- name: SERVICE_NAMESPACE # namespace of the service
-# objref:
-# kind: Service
-# version: v1
-# name: webhook-service
-# fieldref:
-# fieldpath: metadata.namespace
-#- name: SERVICE_NAME
-# objref:
-# kind: Service
-# version: v1
-# name: webhook-service
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go
deleted file mode 100644
index 7d49171c16f..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package kdefault
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &ManagerAuthProxyPatch{}
-
-// ManagerAuthProxyPatch scaffolds a file that defines the patch that enables prometheus metrics for the manager
-type ManagerAuthProxyPatch struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *ManagerAuthProxyPatch) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "default", "manager_auth_proxy_patch.yaml")
- }
-
- f.TemplateBody = kustomizeAuthProxyPatchTemplate
-
- f.IfExistsAction = machinery.Error
-
- return nil
-}
-
-const kustomizeAuthProxyPatchTemplate = `# This patch inject a sidecar container which is a HTTP proxy for the
-# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews.
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: controller-manager
- namespace: system
-spec:
- template:
- spec:
- containers:
- - name: kube-rbac-proxy
- image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0
- args:
- - "--secure-listen-address=0.0.0.0:8443"
- - "--upstream=http://127.0.0.1:8080/"
- - "--logtostderr=true"
- - "--v=10"
- ports:
- - containerPort: 8443
- name: https
- - name: manager
- args:
- - "--metrics-addr=127.0.0.1:8080"
- - "--enable-leader-election"
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go
deleted file mode 100644
index a9e0844bf18..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package kdefault
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &ManagerWebhookPatch{}
-
-// ManagerWebhookPatch scaffolds a file that defines the patch that enables webhooks on the manager
-type ManagerWebhookPatch struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *ManagerWebhookPatch) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "default", "manager_webhook_patch.yaml")
- }
-
- f.TemplateBody = managerWebhookPatchTemplate
-
- return nil
-}
-
-const managerWebhookPatchTemplate = `apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: controller-manager
- namespace: system
-spec:
- template:
- spec:
- containers:
- - name: manager
- ports:
- - containerPort: 9443
- name: webhook-server
- protocol: TCP
- volumeMounts:
- - mountPath: /tmp/k8s-webhook-server/serving-certs
- name: cert
- readOnly: true
- volumes:
- - name: cert
- secret:
- defaultMode: 420
- secretName: webhook-server-cert
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/config.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/config.go
deleted file mode 100644
index 1e08e134923..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/config.go
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package manager
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Config{}
-
-// Config scaffolds a file that defines the namespace and the manager deployment
-type Config struct {
- machinery.TemplateMixin
-
- // Image is controller manager image name
- Image string
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Config) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "manager", "manager.yaml")
- }
-
- f.TemplateBody = configTemplate
-
- return nil
-}
-
-const configTemplate = `apiVersion: v1
-kind: Namespace
-metadata:
- labels:
- control-plane: controller-manager
- name: system
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: controller-manager
- namespace: system
- labels:
- control-plane: controller-manager
-spec:
- selector:
- matchLabels:
- control-plane: controller-manager
- replicas: 1
- template:
- metadata:
- labels:
- control-plane: controller-manager
- spec:
- containers:
- - command:
- - /manager
- args:
- - --enable-leader-election
- image: {{ .Image }}
- name: manager
- resources:
- limits:
- cpu: 100m
- memory: 30Mi
- requests:
- cpu: 100m
- memory: 20Mi
- terminationGracePeriodSeconds: 10
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/kustomization.go
deleted file mode 100644
index f8d5ecd7ec9..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/kustomization.go
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package manager
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Kustomization{}
-
-// Kustomization scaffolds a file that defines the kustomization scheme for the manager folder
-type Kustomization struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Kustomization) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "manager", "kustomization.yaml")
- }
-
- f.TemplateBody = kustomizeManagerTemplate
-
- f.IfExistsAction = machinery.Error
-
- return nil
-}
-
-const kustomizeManagerTemplate = `resources:
-- manager.yaml
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/kustomization.go
deleted file mode 100644
index c271a6a3dbb..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/kustomization.go
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package prometheus
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Kustomization{}
-
-// Kustomization scaffolds a file that defines the kustomization scheme for the prometheus folder
-type Kustomization struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Kustomization) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "prometheus", "kustomization.yaml")
- }
-
- f.TemplateBody = kustomizationTemplate
-
- return nil
-}
-
-const kustomizationTemplate = `resources:
-- monitor.yaml
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/monitor.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/monitor.go
deleted file mode 100644
index 261282075ec..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/monitor.go
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package prometheus
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Monitor{}
-
-// Monitor scaffolds a file that defines the prometheus service monitor
-type Monitor struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Monitor) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "prometheus", "monitor.yaml")
- }
-
- f.TemplateBody = serviceMonitorTemplate
-
- return nil
-}
-
-const serviceMonitorTemplate = `
-# Prometheus Monitor Service (Metrics)
-apiVersion: monitoring.coreos.com/v1
-kind: ServiceMonitor
-metadata:
- labels:
- control-plane: controller-manager
- name: controller-manager-metrics-monitor
- namespace: system
-spec:
- endpoints:
- - path: /metrics
- port: https
- scheme: https
- bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
- tlsConfig:
- insecureSkipVerify: true
- selector:
- matchLabels:
- control-plane: controller-manager
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go
deleted file mode 100644
index 1eee0af2031..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package rbac
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &AuthProxyClientRole{}
-
-// AuthProxyClientRole scaffolds a file that defines the role for the metrics reader
-type AuthProxyClientRole struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *AuthProxyClientRole) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "rbac", "auth_proxy_client_clusterrole.yaml")
- }
-
- f.TemplateBody = clientClusterRoleTemplate
-
- return nil
-}
-
-const clientClusterRoleTemplate = `apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
- name: metrics-reader
-rules:
-- nonResourceURLs: ["/metrics"]
- verbs: ["get"]
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role.go
deleted file mode 100644
index df22ef8dc39..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role.go
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package rbac
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &AuthProxyRole{}
-
-// AuthProxyRole scaffolds a file that defines the role for the auth proxy
-type AuthProxyRole struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *AuthProxyRole) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "rbac", "auth_proxy_role.yaml")
- }
-
- f.TemplateBody = proxyRoleTemplate
-
- return nil
-}
-
-const proxyRoleTemplate = `apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
- name: proxy-role
-rules:
-- apiGroups: ["authentication.k8s.io"]
- resources:
- - tokenreviews
- verbs: ["create"]
-- apiGroups: ["authorization.k8s.io"]
- resources:
- - subjectaccessreviews
- verbs: ["create"]
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go
deleted file mode 100644
index eafc45f6ee9..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package rbac
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &AuthProxyRoleBinding{}
-
-// AuthProxyRoleBinding scaffolds a file that defines the role binding for the auth proxy
-type AuthProxyRoleBinding struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *AuthProxyRoleBinding) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "rbac", "auth_proxy_role_binding.yaml")
- }
-
- f.TemplateBody = proxyRoleBindinggTemplate
-
- return nil
-}
-
-const proxyRoleBindinggTemplate = `apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
- name: proxy-rolebinding
-roleRef:
- apiGroup: rbac.authorization.k8s.io
- kind: ClusterRole
- name: proxy-role
-subjects:
-- kind: ServiceAccount
- name: default
- namespace: system
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_service.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_service.go
deleted file mode 100644
index 6287d360ebb..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_service.go
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package rbac
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &AuthProxyService{}
-
-// AuthProxyService scaffolds a file that defines the service for the auth proxy
-type AuthProxyService struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *AuthProxyService) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "rbac", "auth_proxy_service.yaml")
- }
-
- f.TemplateBody = authProxyServiceTemplate
-
- return nil
-}
-
-const authProxyServiceTemplate = `apiVersion: v1
-kind: Service
-metadata:
- labels:
- control-plane: controller-manager
- name: controller-manager-metrics-service
- namespace: system
-spec:
- ports:
- - name: https
- port: 8443
- targetPort: https
- selector:
- control-plane: controller-manager
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_editor_role.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_editor_role.go
deleted file mode 100644
index 7024549629d..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_editor_role.go
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package rbac
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &CRDEditorRole{}
-
-// CRDEditorRole scaffolds a file that defines the role that allows to edit plurals
-type CRDEditorRole struct {
- machinery.TemplateMixin
- machinery.ResourceMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *CRDEditorRole) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "rbac", "%[kind]_editor_role.yaml")
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
-
- f.TemplateBody = crdRoleEditorTemplate
-
- return nil
-}
-
-const crdRoleEditorTemplate = `# permissions for end users to edit {{ .Resource.Plural }}.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
- name: {{ lower .Resource.Kind }}-editor-role
-rules:
-- apiGroups:
- - {{ .Resource.QualifiedGroup }}
- resources:
- - {{ .Resource.Plural }}
- verbs:
- - create
- - delete
- - get
- - list
- - patch
- - update
- - watch
-- apiGroups:
- - {{ .Resource.QualifiedGroup }}
- resources:
- - {{ .Resource.Plural }}/status
- verbs:
- - get
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_viewer_role.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_viewer_role.go
deleted file mode 100644
index 74177476661..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_viewer_role.go
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package rbac
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &CRDViewerRole{}
-
-// CRDViewerRole scaffolds a file that defines the role that allows to view plurals
-type CRDViewerRole struct {
- machinery.TemplateMixin
- machinery.ResourceMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *CRDViewerRole) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "rbac", "%[kind]_viewer_role.yaml")
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
-
- f.TemplateBody = crdRoleViewerTemplate
-
- return nil
-}
-
-const crdRoleViewerTemplate = `# permissions for end users to view {{ .Resource.Plural }}.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
- name: {{ lower .Resource.Kind }}-viewer-role
-rules:
-- apiGroups:
- - {{ .Resource.QualifiedGroup }}
- resources:
- - {{ .Resource.Plural }}
- verbs:
- - get
- - list
- - watch
-- apiGroups:
- - {{ .Resource.QualifiedGroup }}
- resources:
- - {{ .Resource.Plural }}/status
- verbs:
- - get
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/kustomization.go
deleted file mode 100644
index f5b164e5b79..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/kustomization.go
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package rbac
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Kustomization{}
-
-// Kustomization scaffolds a file that defines the kustomization scheme for the rbac folder
-type Kustomization struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Kustomization) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "rbac", "kustomization.yaml")
- }
-
- f.TemplateBody = kustomizeRBACTemplate
-
- f.IfExistsAction = machinery.Error
-
- return nil
-}
-
-const kustomizeRBACTemplate = `resources:
-- role.yaml
-- role_binding.yaml
-- leader_election_role.yaml
-- leader_election_role_binding.yaml
-# Comment the following 4 lines if you want to disable
-# the auth proxy (https://github.com/brancz/kube-rbac-proxy)
-# which protects your /metrics endpoint.
-- auth_proxy_service.yaml
-- auth_proxy_role.yaml
-- auth_proxy_role_binding.yaml
-- auth_proxy_client_clusterrole.yaml
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role.go
deleted file mode 100644
index 6de4d48b784..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role.go
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package rbac
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &LeaderElectionRole{}
-
-// LeaderElectionRole scaffolds a file that defines the role that allows leader election
-type LeaderElectionRole struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *LeaderElectionRole) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "rbac", "leader_election_role.yaml")
- }
-
- f.TemplateBody = leaderElectionRoleTemplate
-
- return nil
-}
-
-const leaderElectionRoleTemplate = `# permissions to do leader election.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
- name: leader-election-role
-rules:
-- apiGroups:
- - ""
- resources:
- - configmaps
- verbs:
- - get
- - list
- - watch
- - create
- - update
- - patch
- - delete
-- apiGroups:
- - ""
- resources:
- - configmaps/status
- verbs:
- - get
- - update
- - patch
-- apiGroups:
- - ""
- resources:
- - events
- verbs:
- - create
- - patch
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go
deleted file mode 100644
index 9dd75b7ff6b..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package rbac
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &LeaderElectionRoleBinding{}
-
-// LeaderElectionRoleBinding scaffolds a file that defines the role binding that allows leader election
-type LeaderElectionRoleBinding struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *LeaderElectionRoleBinding) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "rbac", "leader_election_role_binding.yaml")
- }
-
- f.TemplateBody = leaderElectionRoleBindingTemplate
-
- return nil
-}
-
-const leaderElectionRoleBindingTemplate = `apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
- name: leader-election-rolebinding
-roleRef:
- apiGroup: rbac.authorization.k8s.io
- kind: Role
- name: leader-election-role
-subjects:
-- kind: ServiceAccount
- name: default
- namespace: system
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/role_binding.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/role_binding.go
deleted file mode 100644
index 0cc6687e8c3..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/role_binding.go
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package rbac
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &RoleBinding{}
-
-// RoleBinding scaffolds a file that defines the role binding for the manager
-type RoleBinding struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *RoleBinding) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "rbac", "role_binding.yaml")
- }
-
- f.TemplateBody = managerBindingTemplate
-
- return nil
-}
-
-const managerBindingTemplate = `apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
- name: manager-rolebinding
-roleRef:
- apiGroup: rbac.authorization.k8s.io
- kind: ClusterRole
- name: manager-role
-subjects:
-- kind: ServiceAccount
- name: default
- namespace: system
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/samples/crd_sample.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/samples/crd_sample.go
deleted file mode 100644
index af9e29f6f6c..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/samples/crd_sample.go
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package samples
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &CRDSample{}
-
-// CRDSample scaffolds a file that defines a sample manifest for the CRD
-type CRDSample struct {
- machinery.TemplateMixin
- machinery.ResourceMixin
-
- Force bool
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *CRDSample) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "samples", "%[group]_%[version]_%[kind].yaml")
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
-
- if f.Force {
- f.IfExistsAction = machinery.OverwriteFile
- } else {
- f.IfExistsAction = machinery.Error
- }
-
- f.TemplateBody = crdSampleTemplate
-
- return nil
-}
-
-const crdSampleTemplate = `apiVersion: {{ .Resource.QualifiedGroup }}/{{ .Resource.Version }}
-kind: {{ .Resource.Kind }}
-metadata:
- name: {{ lower .Resource.Kind }}-sample
-spec:
- # Add fields here
- foo: bar
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomization.go
deleted file mode 100644
index 7157dd8380c..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomization.go
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package webhook
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Kustomization{}
-
-// Kustomization scaffolds a file that defines the kustomization scheme for the webhook folder
-type Kustomization struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Kustomization) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "webhook", "kustomization.yaml")
- }
-
- f.TemplateBody = kustomizeWebhookTemplate
-
- f.IfExistsAction = machinery.Error
-
- return nil
-}
-
-const kustomizeWebhookTemplate = `resources:
-- manifests.yaml
-- service.yaml
-
-configurations:
-- kustomizeconfig.yaml
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomizeconfig.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomizeconfig.go
deleted file mode 100644
index ac2c92cc89e..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomizeconfig.go
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package webhook
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &KustomizeConfig{}
-
-// KustomizeConfig scaffolds a file that configures the kustomization for the webhook folder
-type KustomizeConfig struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *KustomizeConfig) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "webhook", "kustomizeconfig.yaml")
- }
-
- f.TemplateBody = kustomizeConfigWebhookTemplate
-
- f.IfExistsAction = machinery.Error
-
- return nil
-}
-
-//nolint:lll
-const kustomizeConfigWebhookTemplate = `# the following config is for teaching kustomize where to look at when substituting vars.
-# It requires kustomize v2.1.0 or newer to work properly.
-nameReference:
-- kind: Service
- version: v1
- fieldSpecs:
- - kind: MutatingWebhookConfiguration
- group: admissionregistration.k8s.io
- path: webhooks/clientConfig/service/name
- - kind: ValidatingWebhookConfiguration
- group: admissionregistration.k8s.io
- path: webhooks/clientConfig/service/name
-
-namespace:
-- kind: MutatingWebhookConfiguration
- group: admissionregistration.k8s.io
- path: webhooks/clientConfig/service/namespace
- create: true
-- kind: ValidatingWebhookConfiguration
- group: admissionregistration.k8s.io
- path: webhooks/clientConfig/service/namespace
- create: true
-
-varReference:
-- path: metadata/annotations
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/service.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/service.go
deleted file mode 100644
index 7783aa136c4..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/service.go
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package webhook
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Service{}
-
-// Service scaffolds a file that defines the webhook service
-type Service struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Service) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join("config", "webhook", "service.yaml")
- }
-
- f.TemplateBody = serviceTemplate
-
- f.IfExistsAction = machinery.Error
-
- return nil
-}
-
-const serviceTemplate = `
-apiVersion: v1
-kind: Service
-metadata:
- name: webhook-service
- namespace: system
-spec:
- ports:
- - port: 443
- targetPort: 9443
- selector:
- control-plane: controller-manager
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller.go
deleted file mode 100644
index 3d400d399e1..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller.go
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package controllers
-
-import (
- "fmt"
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Controller{}
-
-// Controller scaffolds the file that defines the controller for a CRD or a builtin resource
-// nolint:maligned
-type Controller struct {
- machinery.TemplateMixin
- machinery.MultiGroupMixin
- machinery.BoilerplateMixin
- machinery.ResourceMixin
-
- ControllerRuntimeVersion string
-
- Force bool
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Controller) SetTemplateDefaults() error {
- if f.Path == "" {
- if f.MultiGroup {
- f.Path = filepath.Join("controllers", "%[group]", "%[kind]_controller.go")
- } else {
- f.Path = filepath.Join("controllers", "%[kind]_controller.go")
- }
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
- fmt.Println(f.Path)
-
- f.TemplateBody = controllerTemplate
-
- if f.Force {
- f.IfExistsAction = machinery.OverwriteFile
- } else {
- f.IfExistsAction = machinery.Error
- }
-
- return nil
-}
-
-//nolint:lll
-const controllerTemplate = `{{ .Boilerplate }}
-
-package controllers
-
-import (
- "context"
- "github.com/go-logr/logr"
- "k8s.io/apimachinery/pkg/runtime"
- ctrl "sigs.k8s.io/controller-runtime"
- "sigs.k8s.io/controller-runtime/pkg/client"
- {{ if not (isEmptyStr .Resource.Path) -}}
- {{ .Resource.ImportAlias }} "{{ .Resource.Path }}"
- {{- end }}
-)
-
-// {{ .Resource.Kind }}Reconciler reconciles a {{ .Resource.Kind }} object
-type {{ .Resource.Kind }}Reconciler struct {
- client.Client
- Log logr.Logger
- Scheme *runtime.Scheme
-}
-
-//+kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=get;list;watch;create;update;patch;delete
-//+kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }}/status,verbs=get;update;patch
-
-// Reconcile is part of the main kubernetes reconciliation loop which aims to
-// move the current state of the cluster closer to the desired state.
-// TODO(user): Modify the Reconcile function to compare the state specified by
-// the {{ .Resource.Kind }} object against the actual cluster state, and then
-// perform operations to make the cluster state reflect the state specified by
-// the user.
-//
-// For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@{{ .ControllerRuntimeVersion }}/pkg/reconcile
-func (r *{{ .Resource.Kind }}Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
- _ = context.Background()
- _ = r.Log.WithValues("{{ .Resource.Kind | lower }}", req.NamespacedName)
-
- // your logic here
-
- return ctrl.Result{}, nil
-}
-
-// SetupWithManager sets up the controller with the Manager.
-func (r *{{ .Resource.Kind }}Reconciler) SetupWithManager(mgr ctrl.Manager) error {
- return ctrl.NewControllerManagedBy(mgr).
- {{ if not (isEmptyStr .Resource.Path) -}}
- For(&{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}).
- {{- else -}}
- // Uncomment the following line adding a pointer to an instance of the controlled resource as an argument
- // For().
- {{- end }}
- Complete(r)
-}
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller_suitetest.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller_suitetest.go
deleted file mode 100644
index 31cef7a8650..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller_suitetest.go
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package controllers
-
-import (
- "fmt"
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &SuiteTest{}
-var _ machinery.Inserter = &SuiteTest{}
-
-// SuiteTest scaffolds the file that sets up the controller tests
-// nolint:maligned
-type SuiteTest struct {
- machinery.TemplateMixin
- machinery.MultiGroupMixin
- machinery.BoilerplateMixin
- machinery.ResourceMixin
-
- // CRDDirectoryRelativePath define the Path for the CRD
- CRDDirectoryRelativePath string
-
- Force bool
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *SuiteTest) SetTemplateDefaults() error {
- if f.Path == "" {
- if f.MultiGroup {
- f.Path = filepath.Join("controllers", "%[group]", "suite_test.go")
- } else {
- f.Path = filepath.Join("controllers", "suite_test.go")
- }
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
-
- f.TemplateBody = fmt.Sprintf(controllerSuiteTestTemplate,
- machinery.NewMarkerFor(f.Path, importMarker),
- machinery.NewMarkerFor(f.Path, addSchemeMarker),
- )
-
- // If is multigroup the path needs to be ../../ since it has
- // the group dir.
- f.CRDDirectoryRelativePath = `".."`
- if f.MultiGroup {
- f.CRDDirectoryRelativePath = `"..", ".."`
- }
-
- if f.Force {
- f.IfExistsAction = machinery.OverwriteFile
- }
-
- return nil
-}
-
-const (
- importMarker = "imports"
- addSchemeMarker = "scheme"
-)
-
-// GetMarkers implements file.Inserter
-func (f *SuiteTest) GetMarkers() []machinery.Marker {
- return []machinery.Marker{
- machinery.NewMarkerFor(f.Path, importMarker),
- machinery.NewMarkerFor(f.Path, addSchemeMarker),
- }
-}
-
-const (
- apiImportCodeFragment = `%s "%s"
-`
- addschemeCodeFragment = `err = %s.AddToScheme(scheme.Scheme)
-Expect(err).NotTo(HaveOccurred())
-
-`
-)
-
-// GetCodeFragments implements file.Inserter
-func (f *SuiteTest) GetCodeFragments() machinery.CodeFragmentsMap {
- fragments := make(machinery.CodeFragmentsMap, 2)
-
- // Generate import code fragments
- imports := make([]string, 0)
- if f.Resource.Path != "" {
- imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path))
- }
-
- // Generate add scheme code fragments
- addScheme := make([]string, 0)
- if f.Resource.Path != "" {
- addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias()))
- }
-
- // Only store code fragments in the map if the slices are non-empty
- if len(imports) != 0 {
- fragments[machinery.NewMarkerFor(f.Path, importMarker)] = imports
- }
- if len(addScheme) != 0 {
- fragments[machinery.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme
- }
-
- return fragments
-}
-
-const controllerSuiteTestTemplate = `{{ .Boilerplate }}
-
-package controllers
-
-import (
- "path/filepath"
- "testing"
- . "github.com/onsi/ginkgo"
- . "github.com/onsi/gomega"
- "k8s.io/client-go/kubernetes/scheme"
- "k8s.io/client-go/rest"
- "sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/controller-runtime/pkg/envtest"
- "sigs.k8s.io/controller-runtime/pkg/envtest/printer"
- logf "sigs.k8s.io/controller-runtime/pkg/log"
- "sigs.k8s.io/controller-runtime/pkg/log/zap"
- %s
-)
-
-// These tests use Ginkgo (BDD-style Go testing framework). Refer to
-// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
-
-var cfg *rest.Config
-var k8sClient client.Client
-var testEnv *envtest.Environment
-
-func TestAPIs(t *testing.T) {
- RegisterFailHandler(Fail)
-
- RunSpecsWithDefaultAndCustomReporters(t,
- "Controller Suite",
- []Reporter{printer.NewlineReporter{}})
-}
-
-var _ = BeforeSuite(func(done Done) {
- logf.SetLogger(zap.LoggerTo(GinkgoWriter, true))
-
- By("bootstrapping test environment")
- testEnv = &envtest.Environment{
- CRDDirectoryPaths: []string{filepath.Join({{ .CRDDirectoryRelativePath }}, "config", "crd", "bases")},
- }
-
- var err error
- cfg, err = testEnv.Start()
- Expect(err).ToNot(HaveOccurred())
- Expect(cfg).ToNot(BeNil())
-
- %s
-
- k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
- Expect(err).ToNot(HaveOccurred())
- Expect(k8sClient).ToNot(BeNil())
-
- close(done)
-}, 60)
-
-var _ = AfterSuite(func() {
- By("tearing down the test environment")
- err := testEnv.Stop()
- Expect(err).ToNot(HaveOccurred())
-})
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/dockerfile.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/dockerfile.go
deleted file mode 100644
index 38e4c47167c..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/dockerfile.go
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package templates
-
-import (
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Dockerfile{}
-
-// Dockerfile scaffolds a file that defines the containerized build process
-type Dockerfile struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Dockerfile) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = "Dockerfile"
- }
-
- f.TemplateBody = dockerfileTemplate
-
- return nil
-}
-
-const dockerfileTemplate = `# Build the manager binary
-FROM golang:1.13 as builder
-
-WORKDIR /workspace
-# Copy the Go Modules manifests
-COPY go.mod go.mod
-COPY go.sum go.sum
-# cache deps before building and copying source so that we don't need to re-download as much
-# and so that source changes don't invalidate our downloaded layer
-RUN go mod download
-
-# Copy the go source
-COPY main.go main.go
-COPY api/ api/
-COPY controllers/ controllers/
-
-# Build
-RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go
-
-# Use distroless as minimal base image to package the manager binary
-# Refer to https://github.com/GoogleContainerTools/distroless for more details
-FROM gcr.io/distroless/static:nonroot
-WORKDIR /
-COPY --from=builder /workspace/manager .
-USER nonroot:nonroot
-
-ENTRYPOINT ["/manager"]
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/gitignore.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/gitignore.go
deleted file mode 100644
index a60f46d6760..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/gitignore.go
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package templates
-
-import (
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &GitIgnore{}
-
-// GitIgnore scaffolds a file that defines which files should be ignored by git
-type GitIgnore struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *GitIgnore) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = ".gitignore"
- }
-
- f.TemplateBody = gitignoreTemplate
-
- return nil
-}
-
-const gitignoreTemplate = `
-# Binaries for programs and plugins
-*.exe
-*.exe~
-*.dll
-*.so
-*.dylib
-bin
-
-# Test binary, build with ` + "`go test -c`" + `
-*.test
-
-# Output of the go coverage tool, specifically when used with LiteIDE
-*.out
-
-# Kubernetes Generated files - skip generated files, except for vendored files
-
-!vendor/**/zz_generated.*
-
-# editor and IDE paraphernalia
-.idea
-*.swp
-*.swo
-*~
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/gomod.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/gomod.go
deleted file mode 100644
index e369bc98567..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/gomod.go
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package templates
-
-import (
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &GoMod{}
-
-// GoMod scaffolds a file that defines the project dependencies
-type GoMod struct {
- machinery.TemplateMixin
- machinery.RepositoryMixin
-
- ControllerRuntimeVersion string
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *GoMod) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = "go.mod"
- }
-
- f.TemplateBody = goModTemplate
-
- f.IfExistsAction = machinery.OverwriteFile
-
- return nil
-}
-
-const goModTemplate = `
-module {{ .Repo }}
-
-go 1.13
-
-require (
- sigs.k8s.io/controller-runtime {{ .ControllerRuntimeVersion }}
-)
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/hack/boilerplate.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/hack/boilerplate.go
deleted file mode 100644
index 1d07c79b45a..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/hack/boilerplate.go
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package hack
-
-import (
- "fmt"
- "path/filepath"
- "time"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-// DefaultBoilerplatePath is the default path to the boilerplate file
-var DefaultBoilerplatePath = filepath.Join("hack", "boilerplate.go.txt")
-
-var _ machinery.Template = &Boilerplate{}
-
-// Boilerplate scaffolds a file that defines the common header for the rest of the files
-type Boilerplate struct {
- machinery.TemplateMixin
- machinery.BoilerplateMixin
-
- // License is the License type to write
- License string
-
- // Licenses maps License types to their actual string
- Licenses map[string]string
-
- // Owner is the copyright owner - e.g. "The Kubernetes Authors"
- Owner string
-
- // Year is the copyright year
- Year string
-}
-
-// Validate implements file.RequiresValidation
-func (f Boilerplate) Validate() error {
- if f.License == "" {
- // A default license will be set later
- } else if _, found := knownLicenses[f.License]; found {
- // One of the know licenses
- } else if _, found := f.Licenses[f.License]; found {
- // A map containing the requested license was also provided
- } else {
- return fmt.Errorf("unknown specified license %s", f.License)
- }
-
- return nil
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Boilerplate) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = DefaultBoilerplatePath
- }
-
- if f.License == "" {
- f.License = "apache2"
- }
-
- if f.Licenses == nil {
- f.Licenses = make(map[string]string, len(knownLicenses))
- }
-
- for key, value := range knownLicenses {
- if _, hasLicense := f.Licenses[key]; !hasLicense {
- f.Licenses[key] = value
- }
- }
-
- if f.Year == "" {
- f.Year = fmt.Sprintf("%v", time.Now().Year())
- }
-
- // Boilerplate given
- if len(f.Boilerplate) > 0 {
- f.TemplateBody = f.Boilerplate
- return nil
- }
-
- f.TemplateBody = boilerplateTemplate
-
- return nil
-}
-
-const boilerplateTemplate = `/*
-{{ if .Owner -}}
-Copyright {{ .Year }} {{ .Owner }}.
-{{- else -}}
-Copyright {{ .Year }}.
-{{- end }}
-{{ index .Licenses .License }}*/`
-
-var knownLicenses = map[string]string{
- "apache2": apache2,
- "none": "",
-}
-
-const apache2 = `
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go
deleted file mode 100644
index c2bd6928d85..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go
+++ /dev/null
@@ -1,242 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package templates
-
-import (
- "fmt"
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-const defaultMainPath = "main.go"
-
-var _ machinery.Template = &Main{}
-
-// Main scaffolds a file that defines the controller manager entry point
-type Main struct {
- machinery.TemplateMixin
- machinery.BoilerplateMixin
- machinery.DomainMixin
- machinery.RepositoryMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Main) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join(defaultMainPath)
- }
-
- f.TemplateBody = fmt.Sprintf(mainTemplate,
- machinery.NewMarkerFor(f.Path, importMarker),
- machinery.NewMarkerFor(f.Path, addSchemeMarker),
- machinery.NewMarkerFor(f.Path, setupMarker),
- )
-
- return nil
-}
-
-var _ machinery.Inserter = &MainUpdater{}
-
-// MainUpdater updates main.go to run Controllers
-type MainUpdater struct { //nolint:maligned
- machinery.RepositoryMixin
- machinery.MultiGroupMixin
- machinery.ResourceMixin
-
- // Flags to indicate which parts need to be included when updating the file
- WireResource, WireController, WireWebhook bool
-}
-
-// GetPath implements file.Builder
-func (*MainUpdater) GetPath() string {
- return defaultMainPath
-}
-
-// GetIfExistsAction implements file.Builder
-func (*MainUpdater) GetIfExistsAction() machinery.IfExistsAction {
- return machinery.OverwriteFile
-}
-
-const (
- importMarker = "imports"
- addSchemeMarker = "scheme"
- setupMarker = "builder"
-)
-
-// GetMarkers implements file.Inserter
-func (f *MainUpdater) GetMarkers() []machinery.Marker {
- return []machinery.Marker{
- machinery.NewMarkerFor(defaultMainPath, importMarker),
- machinery.NewMarkerFor(defaultMainPath, addSchemeMarker),
- machinery.NewMarkerFor(defaultMainPath, setupMarker),
- }
-}
-
-const (
- apiImportCodeFragment = `%s "%s"
-`
- controllerImportCodeFragment = `"%s/controllers"
-`
- multiGroupControllerImportCodeFragment = `%scontroller "%s/controllers/%s"
-`
- addschemeCodeFragment = `utilruntime.Must(%s.AddToScheme(scheme))
-`
- reconcilerSetupCodeFragment = `if err = (&controllers.%sReconciler{
- Client: mgr.GetClient(),
- Log: ctrl.Log.WithName("controllers").WithName("%s"),
- Scheme: mgr.GetScheme(),
- }).SetupWithManager(mgr); err != nil {
- setupLog.Error(err, "unable to create controller", "controller", "%s")
- os.Exit(1)
- }
-`
- multiGroupReconcilerSetupCodeFragment = `if err = (&%scontroller.%sReconciler{
- Client: mgr.GetClient(),
- Log: ctrl.Log.WithName("controllers").WithName("%s"),
- Scheme: mgr.GetScheme(),
- }).SetupWithManager(mgr); err != nil {
- setupLog.Error(err, "unable to create controller", "controller", "%s")
- os.Exit(1)
- }
-`
- webhookSetupCodeFragment = `if err = (&%s.%s{}).SetupWebhookWithManager(mgr); err != nil {
- setupLog.Error(err, "unable to create webhook", "webhook", "%s")
- os.Exit(1)
- }
-`
-)
-
-// GetCodeFragments implements file.Inserter
-func (f *MainUpdater) GetCodeFragments() machinery.CodeFragmentsMap {
- fragments := make(machinery.CodeFragmentsMap, 3)
-
- // If resource is not being provided we are creating the file, not updating it
- if f.Resource == nil {
- return fragments
- }
-
- // Generate import code fragments
- imports := make([]string, 0)
- if f.WireResource {
- imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path))
- }
-
- if f.WireController {
- if !f.MultiGroup {
- imports = append(imports, fmt.Sprintf(controllerImportCodeFragment, f.Repo))
- } else {
- imports = append(imports, fmt.Sprintf(multiGroupControllerImportCodeFragment,
- f.Resource.PackageName(), f.Repo, f.Resource.Group))
- }
- }
-
- // Generate add scheme code fragments
- addScheme := make([]string, 0)
- if f.WireResource {
- addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias()))
- }
-
- // Generate setup code fragments
- setup := make([]string, 0)
- if f.WireController {
- if !f.MultiGroup {
- setup = append(setup, fmt.Sprintf(reconcilerSetupCodeFragment,
- f.Resource.Kind, f.Resource.Kind, f.Resource.Kind))
- } else {
- setup = append(setup, fmt.Sprintf(multiGroupReconcilerSetupCodeFragment,
- f.Resource.PackageName(), f.Resource.Kind, f.Resource.Kind, f.Resource.Kind))
- }
- }
- if f.WireWebhook {
- setup = append(setup, fmt.Sprintf(webhookSetupCodeFragment,
- f.Resource.ImportAlias(), f.Resource.Kind, f.Resource.Kind))
- }
-
- // Only store code fragments in the map if the slices are non-empty
- if len(imports) != 0 {
- fragments[machinery.NewMarkerFor(defaultMainPath, importMarker)] = imports
- }
- if len(addScheme) != 0 {
- fragments[machinery.NewMarkerFor(defaultMainPath, addSchemeMarker)] = addScheme
- }
- if len(setup) != 0 {
- fragments[machinery.NewMarkerFor(defaultMainPath, setupMarker)] = setup
- }
-
- return fragments
-}
-
-var mainTemplate = `{{ .Boilerplate }}
-
-package main
-
-import (
- "flag"
- "os"
- "k8s.io/apimachinery/pkg/runtime"
- utilruntime "k8s.io/apimachinery/pkg/util/runtime"
- clientgoscheme "k8s.io/client-go/kubernetes/scheme"
- _ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
- ctrl "sigs.k8s.io/controller-runtime"
- "sigs.k8s.io/controller-runtime/pkg/log/zap"
- %s
-)
-
-var (
- scheme = runtime.NewScheme()
- setupLog = ctrl.Log.WithName("setup")
-)
-
-func init() {
- utilruntime.Must(clientgoscheme.AddToScheme(scheme))
-
- %s
-}
-
-func main() {
- var metricsAddr string
- var enableLeaderElection bool
- flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
- flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
- "Enable leader election for controller manager. " +
- "Enabling this will ensure there is only one active controller manager.")
- flag.Parse()
-
- ctrl.SetLogger(zap.New(zap.UseDevMode(true)))
-
- mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
- Scheme: scheme,
- MetricsBindAddress: metricsAddr,
- Port: 9443,
- LeaderElection: enableLeaderElection,
- LeaderElectionID: "{{ hashFNV .Repo }}.{{ .Domain }}",
- })
- if err != nil {
- setupLog.Error(err, "unable to start manager")
- os.Exit(1)
- }
-
- %s
-
- setupLog.Info("starting manager")
- if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
- setupLog.Error(err, "problem running manager")
- os.Exit(1)
- }
-}
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/makefile.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/makefile.go
deleted file mode 100644
index e799bd05cc9..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/internal/templates/makefile.go
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
-Copyright 2019 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package templates
-
-import (
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Makefile{}
-
-// Makefile scaffolds a file that defines project management CLI commands
-type Makefile struct {
- machinery.TemplateMixin
-
- // Image is controller manager image name
- Image string
- // BoilerplatePath is the path to the boilerplate file
- BoilerplatePath string
- // Controller tools version to use in the project
- ControllerToolsVersion string
- // Kustomize version to use in the project
- KustomizeVersion string
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Makefile) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = "Makefile"
- }
-
- f.TemplateBody = makefileTemplate
-
- f.IfExistsAction = machinery.Error
-
- if f.Image == "" {
- f.Image = "controller:latest"
- }
-
- return nil
-}
-
-//nolint:lll
-const makefileTemplate = `
-# Image URL to use all building/pushing image targets
-IMG ?= {{ .Image }}
-# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
-CRD_OPTIONS ?= "crd:trivialVersions=true"
-
-# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
-ifeq (,$(shell go env GOBIN))
-GOBIN=$(shell go env GOPATH)/bin
-else
-GOBIN=$(shell go env GOBIN)
-endif
-
-all: build
-
-##@ General
-
-# The help target prints out all targets with their descriptions organized
-# beneath their categories. The categories are represented by '##@' and the
-# target descriptions by '##'. The awk commands is responsible for reading the
-# entire set of makefiles included in this invocation, looking for lines of the
-# file as xyz: ## something, and then pretty-format the target and help. Then,
-# if there's a line with ##@ something, that gets pretty-printed as a category.
-# More info on the usage of ANSI control characters for terminal formatting:
-# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
-# More info on the awk command:
-# http://linuxcommand.org/lc3_adv_awk.php
-
-help: ## Display this help.
- @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
-
-##@ Development
-
-manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
- $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
-
-generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
- $(CONTROLLER_GEN) object:headerFile={{printf "%q" .BoilerplatePath}} paths="./..."
-
-fmt: ## Run go fmt against code.
- go fmt ./...
-
-vet: ## Run go vet against code.
- go vet ./...
-
-test: manifests generate fmt vet ## Run tests.
- go test ./... -coverprofile cover.out
-
-##@ Build
-
-build: generate fmt vet ## Build manager binary.
- go build -o bin/manager main.go
-
-# Backwards compatibility
-manager: build ## Build manager binary (alias for build target).
-
-run: manifests generate fmt vet ## Run a controller from your host.
- go run ./main.go
-
-docker-build: test ## Build docker image with the manager.
- docker build -t ${IMG} .
-
-docker-push: ## Push docker image with the manager.
- docker push ${IMG}
-
-##@ Deployment
-
-install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
- $(KUSTOMIZE) build config/crd | kubectl apply -f -
-
-uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config.
- $(KUSTOMIZE) build config/crd | kubectl delete -f -
-
-deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
- cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
- $(KUSTOMIZE) build config/default | kubectl apply -f -
-
-undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config.
- $(KUSTOMIZE) build config/default | kubectl delete -f -
-
-
-controller-gen: ## Download controller-gen locally if necessary.
-ifeq (, $(shell which controller-gen))
- @{ \
- set -e ;\
- CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\
- cd $$CONTROLLER_GEN_TMP_DIR ;\
- go mod init tmp ;\
- go get sigs.k8s.io/controller-tools/cmd/controller-gen@{{ .ControllerToolsVersion }} ;\
- rm -rf $$CONTROLLER_GEN_TMP_DIR ;\
- }
-CONTROLLER_GEN=$(GOBIN)/controller-gen
-else
-CONTROLLER_GEN=$(shell which controller-gen)
-endif
-
-kustomize: ## Download kustomize locally if necessary.
-ifeq (, $(shell which kustomize))
- @{ \
- set -e ;\
- KUSTOMIZE_GEN_TMP_DIR=$$(mktemp -d) ;\
- cd $$KUSTOMIZE_GEN_TMP_DIR ;\
- go mod init tmp ;\
- go get sigs.k8s.io/kustomize/kustomize/v3@{{ .KustomizeVersion }} ;\
- rm -rf $$KUSTOMIZE_GEN_TMP_DIR ;\
- }
-KUSTOMIZE=$(GOBIN)/kustomize
-else
-KUSTOMIZE=$(shell which kustomize)
-endif
-`
diff --git a/pkg/plugins/golang/v2/scaffolds/webhook.go b/pkg/plugins/golang/v2/scaffolds/webhook.go
deleted file mode 100644
index d3cef711cad..00000000000
--- a/pkg/plugins/golang/v2/scaffolds/webhook.go
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package scaffolds
-
-import (
- "fmt"
-
- "github.com/spf13/afero"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/api"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/hack"
-)
-
-var _ plugins.Scaffolder = &webhookScaffolder{}
-
-type webhookScaffolder struct {
- config config.Config
- resource resource.Resource
-
- // fs is the filesystem that will be used by the scaffolder
- fs machinery.Filesystem
-}
-
-// NewWebhookScaffolder returns a new Scaffolder for v2 webhook creation operations
-func NewWebhookScaffolder(config config.Config, resource resource.Resource) plugins.Scaffolder {
- return &webhookScaffolder{
- config: config,
- resource: resource,
- }
-}
-
-// InjectFS implements cmdutil.Scaffolder
-func (s *webhookScaffolder) InjectFS(fs machinery.Filesystem) {
- s.fs = fs
-}
-
-// Scaffold implements cmdutil.Scaffolder
-func (s *webhookScaffolder) Scaffold() error {
- fmt.Println("Writing scaffold for you to edit...")
-
- // Load the boilerplate
- boilerplate, err := afero.ReadFile(s.fs.FS, hack.DefaultBoilerplatePath)
- if err != nil {
- return fmt.Errorf("error scaffolding webhook: unable to load boilerplate: %w", err)
- }
-
- // Initialize the machinery.Scaffold that will write the files to disk
- scaffold := machinery.NewScaffold(s.fs,
- machinery.WithConfig(s.config),
- machinery.WithBoilerplate(string(boilerplate)),
- machinery.WithResource(&s.resource),
- )
-
- if err := s.config.UpdateResource(s.resource); err != nil {
- return fmt.Errorf("error updating resource: %w", err)
- }
-
- if err := scaffold.Execute(
- &api.Webhook{},
- &templates.MainUpdater{WireWebhook: true},
- ); err != nil {
- return err
- }
-
- if s.resource.HasConversionWebhook() {
- fmt.Println(`Webhook server has been set up for you.
-You need to implement the conversion.Hub and conversion.Convertible interfaces for your CRD types.`)
- }
-
- return nil
-}
diff --git a/pkg/plugins/golang/v2/webhook.go b/pkg/plugins/golang/v2/webhook.go
deleted file mode 100644
index d142298850b..00000000000
--- a/pkg/plugins/golang/v2/webhook.go
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v2
-
-import (
- "fmt"
-
- "github.com/spf13/pflag"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugin"
- goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds"
-)
-
-var _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{}
-
-type createWebhookSubcommand struct {
- config config.Config
- // For help text.
- commandName string
-
- options *goPlugin.Options
-
- resource *resource.Resource
-}
-
-func (p *createWebhookSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
- p.commandName = cliMeta.CommandName
-
- subcmdMeta.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting,
-validating and/or conversion webhooks.
-`
- subcmdMeta.Examples = fmt.Sprintf(` # Create defaulting and validating webhooks for Group: ship, Version: v1beta1
- # and Kind: Frigate
- %[1]s create webhook --group ship --version v1beta1 --kind Frigate --defaulting --programmatic-validation
-
- # Create conversion webhook for Group: ship, Version: v1beta1
- # and Kind: Frigate
- %[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion
-`, cliMeta.CommandName)
-}
-
-func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) {
- p.options = &goPlugin.Options{WebhookVersion: "v1beta1"}
-
- fs.StringVar(&p.options.Plural, "resource", "", "resource irregular plural form")
-
- fs.BoolVar(&p.options.DoDefaulting, "defaulting", false,
- "if set, scaffold the defaulting webhook")
- fs.BoolVar(&p.options.DoValidation, "programmatic-validation", false,
- "if set, scaffold the validating webhook")
- fs.BoolVar(&p.options.DoConversion, "conversion", false,
- "if set, scaffold the conversion webhook")
-}
-
-func (p *createWebhookSubcommand) InjectConfig(c config.Config) error {
- p.config = c
-
- return nil
-}
-
-func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error {
- p.resource = res
-
- if p.resource.Group == "" {
- return fmt.Errorf("group cannot be empty")
- }
-
- p.options.UpdateResource(p.resource, p.config)
-
- if err := p.resource.Validate(); err != nil {
- return err
- }
-
- if !p.resource.HasDefaultingWebhook() && !p.resource.HasValidationWebhook() && !p.resource.HasConversionWebhook() {
- return fmt.Errorf("%s create webhook requires at least one of --defaulting,"+
- " --programmatic-validation and --conversion to be true", p.commandName)
- }
-
- // check if resource exist to create webhook
- if p.config.GetVersion().Compare(cfgv2.Version) == 0 {
- if !p.config.HasResource(p.resource.GVK) {
- return fmt.Errorf("%s create webhook requires a previously created API ", p.commandName)
- }
- } else {
- if r, err := p.config.GetResource(p.resource.GVK); err != nil {
- return fmt.Errorf("%s create webhook requires a previously created API ", p.commandName)
- } else if r.Webhooks != nil && !r.Webhooks.IsEmpty() {
- return fmt.Errorf("webhook resource already exists")
- }
- }
-
- return nil
-}
-
-func (p *createWebhookSubcommand) Scaffold(fs machinery.Filesystem) error {
- scaffolder := scaffolds.NewWebhookScaffolder(p.config, *p.resource)
- scaffolder.InjectFS(fs)
- return scaffolder.Scaffold()
-}
diff --git a/pkg/plugins/golang/v3/api.go b/pkg/plugins/golang/v3/api.go
deleted file mode 100644
index 71d4443e93d..00000000000
--- a/pkg/plugins/golang/v3/api.go
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v3
-
-import (
- "bufio"
- "errors"
- "fmt"
- "os"
-
- "github.com/spf13/pflag"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugin"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util"
- goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds"
-)
-
-const (
- // defaultCRDVersion is the default CRD API version to scaffold.
- defaultCRDVersion = "v1"
-)
-
-// DefaultMainPath is default file path of main.go
-const DefaultMainPath = "main.go"
-
-var _ plugin.CreateAPISubcommand = &createAPISubcommand{}
-
-type createAPISubcommand struct {
- config config.Config
-
- options *goPlugin.Options
-
- resource *resource.Resource
-
- // Check if we have to scaffold resource and/or controller
- resourceFlag *pflag.Flag
- controllerFlag *pflag.Flag
-
- // force indicates that the resource should be created even if it already exists
- force bool
-
- // runMake indicates whether to run make or not after scaffolding APIs
- runMake bool
-}
-
-func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
- subcmdMeta.Description = `Scaffold a Kubernetes API by writing a Resource definition and/or a Controller.
-
-If information about whether the resource and controller should be scaffolded
-was not explicitly provided, it will prompt the user if they should be.
-
-After the scaffold is written, the dependencies will be updated and
-make generate will be run.
-`
- subcmdMeta.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate
- %[1]s create api --group ship --version v1beta1 --kind Frigate
-
- # Edit the API Scheme
- nano api/v1beta1/frigate_types.go
-
- # Edit the Controller
- nano controllers/frigate/frigate_controller.go
-
- # Edit the Controller Test
- nano controllers/frigate/frigate_controller_test.go
-
- # Install CRDs into the Kubernetes cluster using kubectl apply
- make install
-
- # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config
- make run
-`, cliMeta.CommandName)
-}
-
-func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) {
- fs.BoolVar(&p.runMake, "make", true, "if true, run `make generate` after generating files")
-
- fs.BoolVar(&p.force, "force", false,
- "attempt to create resource even if it already exists")
-
- p.options = &goPlugin.Options{}
-
- fs.StringVar(&p.options.Plural, "plural", "", "resource irregular plural form")
-
- fs.BoolVar(&p.options.DoAPI, "resource", true,
- "if set, generate the resource without prompting the user")
- p.resourceFlag = fs.Lookup("resource")
- fs.StringVar(&p.options.CRDVersion, "crd-version", defaultCRDVersion,
- "version of CustomResourceDefinition to scaffold. Options: [v1, v1beta1]")
- fs.BoolVar(&p.options.Namespaced, "namespaced", true, "resource is namespaced")
-
- fs.BoolVar(&p.options.DoController, "controller", true,
- "if set, generate the controller without prompting the user")
- p.controllerFlag = fs.Lookup("controller")
-}
-
-func (p *createAPISubcommand) InjectConfig(c config.Config) error {
- p.config = c
-
- return nil
-}
-
-func (p *createAPISubcommand) InjectResource(res *resource.Resource) error {
- p.resource = res
-
- // TODO: re-evaluate whether y/n input still makes sense. We should probably always
- // scaffold the resource and controller.
- // Ask for API and Controller if not specified
- reader := bufio.NewReader(os.Stdin)
- if !p.resourceFlag.Changed {
- fmt.Println("Create Resource [y/n]")
- p.options.DoAPI = util.YesNo(reader)
- }
- if !p.controllerFlag.Changed {
- fmt.Println("Create Controller [y/n]")
- p.options.DoController = util.YesNo(reader)
- }
-
- p.options.UpdateResource(p.resource, p.config)
-
- if err := p.resource.Validate(); err != nil {
- return err
- }
-
- // In case we want to scaffold a resource API we need to do some checks
- if p.options.DoAPI {
- // Check that resource doesn't have the API scaffolded or flag force was set
- if r, err := p.config.GetResource(p.resource.GVK); err == nil && r.HasAPI() && !p.force {
- return errors.New("API resource already exists")
- }
-
- // Check that the provided group can be added to the project
- if !p.config.IsMultiGroup() && p.config.ResourcesLength() != 0 && !p.config.HasGroup(p.resource.Group) {
- return fmt.Errorf("multiple groups are not allowed by default, " +
- "to enable multi-group visit https://kubebuilder.io/migration/multi-group.html")
- }
-
- // Check CRDVersion against all other CRDVersions in p.config for compatibility.
- if util.HasDifferentCRDVersion(p.config, p.resource.API.CRDVersion) {
- return fmt.Errorf("only one CRD version can be used for all resources, cannot add %q",
- p.resource.API.CRDVersion)
- }
- }
-
- return nil
-}
-
-func (p *createAPISubcommand) PreScaffold(machinery.Filesystem) error {
- // check if main.go is present in the root directory
- if _, err := os.Stat(DefaultMainPath); os.IsNotExist(err) {
- return fmt.Errorf("%s file should present in the root directory", DefaultMainPath)
- }
-
- return nil
-}
-
-func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error {
- scaffolder := scaffolds.NewAPIScaffolder(p.config, *p.resource, p.force)
- scaffolder.InjectFS(fs)
- return scaffolder.Scaffold()
-}
-
-func (p *createAPISubcommand) PostScaffold() error {
- err := util.RunCmd("Update dependencies", "go", "mod", "tidy")
- if err != nil {
- return err
- }
- if p.runMake && p.resource.HasAPI() {
- err = util.RunCmd("Running make", "make", "generate")
- if err != nil {
- return err
- }
- }
-
- return nil
-}
diff --git a/pkg/plugins/golang/v3/edit.go b/pkg/plugins/golang/v3/edit.go
deleted file mode 100644
index b75e7339bd6..00000000000
--- a/pkg/plugins/golang/v3/edit.go
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v3
-
-import (
- "fmt"
-
- "github.com/spf13/pflag"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugin"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds"
-)
-
-var _ plugin.EditSubcommand = &editSubcommand{}
-
-type editSubcommand struct {
- config config.Config
-
- multigroup bool
-}
-
-func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
- subcmdMeta.Description = `This command will edit the project configuration.
-Features supported:
- - Toggle between single or multi group projects.
-`
- subcmdMeta.Examples = fmt.Sprintf(` # Enable the multigroup layout
- %[1]s edit --multigroup
-
- # Disable the multigroup layout
- %[1]s edit --multigroup=false
-`, cliMeta.CommandName)
-}
-
-func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
- fs.BoolVar(&p.multigroup, "multigroup", false, "enable or disable multigroup layout")
-}
-
-func (p *editSubcommand) InjectConfig(c config.Config) error {
- p.config = c
-
- return nil
-}
-
-func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
- scaffolder := scaffolds.NewEditScaffolder(p.config, p.multigroup)
- scaffolder.InjectFS(fs)
- return scaffolder.Scaffold()
-}
diff --git a/pkg/plugins/golang/v3/init.go b/pkg/plugins/golang/v3/init.go
deleted file mode 100644
index 296f11872f5..00000000000
--- a/pkg/plugins/golang/v3/init.go
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v3
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
-
- "github.com/spf13/pflag"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugin"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds"
-)
-
-var _ plugin.InitSubcommand = &initSubcommand{}
-
-type initSubcommand struct {
- config config.Config
- // For help text.
- commandName string
-
- // boilerplate options
- license string
- owner string
-
- // go config options
- repo string
-
- // flags
- fetchDeps bool
- skipGoVersionCheck bool
-}
-
-func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
- p.commandName = cliMeta.CommandName
-
- subcmdMeta.Description = `Initialize a new project including the following files:
- - a "go.mod" with project dependencies
- - a "PROJECT" file that stores project configuration
- - a "Makefile" with several useful make targets for the project
- - several YAML files for project deployment under the "config" directory
- - a "main.go" file that creates the manager that will run the project controllers
-`
- subcmdMeta.Examples = fmt.Sprintf(` # Initialize a new project with your domain and name in copyright
- %[1]s init --plugins go/v3 --domain example.org --owner "Your name"
-
- # Initialize a new project defining an specific project version
- %[1]s init --plugins go/v3 --project-version 3
-`, cliMeta.CommandName)
-}
-
-func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) {
- fs.BoolVar(&p.skipGoVersionCheck, "skip-go-version-check",
- false, "if specified, skip checking the Go version")
-
- // dependency args
- fs.BoolVar(&p.fetchDeps, "fetch-deps", true, "ensure dependencies are downloaded")
-
- // boilerplate args
- fs.StringVar(&p.license, "license", "apache2",
- "license to use to boilerplate, may be one of 'apache2', 'none'")
- fs.StringVar(&p.owner, "owner", "", "owner to add to the copyright")
-
- // project args
- fs.StringVar(&p.repo, "repo", "", "name to use for go module (e.g., github.com/user/repo), "+
- "defaults to the go package of the current working directory.")
-}
-
-func (p *initSubcommand) InjectConfig(c config.Config) error {
- p.config = c
-
- // Try to guess repository if flag is not set.
- if p.repo == "" {
- repoPath, err := golang.FindCurrentRepo()
- if err != nil {
- return fmt.Errorf("error finding current repository: %v", err)
- }
- p.repo = repoPath
- }
- if err := p.config.SetRepository(p.repo); err != nil {
- return err
- }
-
- return nil
-}
-
-func (p *initSubcommand) PreScaffold(machinery.Filesystem) error {
- // Requires go1.11+
- if !p.skipGoVersionCheck {
- if err := golang.ValidateGoVersion(); err != nil {
- return err
- }
- }
-
- // Check if the current directory has not files or directories which does not allow to init the project
- if err := checkDir(); err != nil {
- return err
- }
-
- return nil
-}
-
-func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error {
- scaffolder := scaffolds.NewInitScaffolder(p.config, p.license, p.owner)
- scaffolder.InjectFS(fs)
- err := scaffolder.Scaffold()
- if err != nil {
- return err
- }
-
- if !p.fetchDeps {
- fmt.Println("Skipping fetching dependencies.")
- return nil
- }
-
- // Ensure that we are pinning controller-runtime version
- // xref: https://github.com/kubernetes-sigs/kubebuilder/issues/997
- err = util.RunCmd("Get controller runtime", "go", "get",
- "sigs.k8s.io/controller-runtime@"+scaffolds.ControllerRuntimeVersion)
- if err != nil {
- return err
- }
-
- return nil
-}
-
-func (p *initSubcommand) PostScaffold() error {
- err := util.RunCmd("Update dependencies", "go", "mod", "tidy")
- if err != nil {
- return err
- }
-
- fmt.Printf("Next: define a resource with:\n$ %s create api\n", p.commandName)
- return nil
-}
-
-// checkDir will return error if the current directory has files which are not allowed.
-// Note that, it is expected that the directory to scaffold the project is cleaned.
-// Otherwise, it might face issues to do the scaffold.
-func checkDir() error {
- err := filepath.Walk(".",
- func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
- // Allow directory trees starting with '.'
- if info.IsDir() && strings.HasPrefix(info.Name(), ".") && info.Name() != "." {
- return filepath.SkipDir
- }
- // Allow files starting with '.'
- if strings.HasPrefix(info.Name(), ".") {
- return nil
- }
- // Allow files in the following list
- allowedFiles := []string{
- "go.mod", // user might run `go mod init` instead of providing the `--flag` at init
- "go.sum", // auto-generated file related to go.mod
- "LICENSE", // can be generated when initializing a GitHub project
- "README.md", // can be generated when initializing a GitHub project
- }
- for _, allowedFile := range allowedFiles {
- if info.Name() == allowedFile {
- return nil
- }
- }
- // Do not allow any other file
- return fmt.Errorf(
- "target directory is not empty (only %s, and files and directories with the prefix \".\" are "+
- "allowed); found existing file %q", strings.Join(allowedFiles, ", "), path)
- })
- if err != nil {
- return err
- }
- return nil
-}
diff --git a/pkg/plugins/golang/v3/plugin.go b/pkg/plugins/golang/v3/plugin.go
deleted file mode 100644
index 3ad89267e4b..00000000000
--- a/pkg/plugins/golang/v3/plugin.go
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v3
-
-import (
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugin"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang"
-)
-
-const pluginName = "base." + golang.DefaultNameQualifier
-
-var (
- pluginVersion = plugin.Version{Number: 3}
- supportedProjectVersions = []config.Version{cfgv3.Version}
-)
-
-var _ plugin.Full = Plugin{}
-
-// Plugin implements the plugin.Full interface
-type Plugin struct {
- initSubcommand
- createAPISubcommand
- createWebhookSubcommand
- editSubcommand
-}
-
-// Name returns the name of the plugin
-func (Plugin) Name() string { return pluginName }
-
-// Version returns the version of the plugin
-func (Plugin) Version() plugin.Version { return pluginVersion }
-
-// SupportedProjectVersions returns an array with all project versions supported by the plugin
-func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions }
-
-// GetInitSubcommand will return the subcommand which is responsible for initializing and common scaffolding
-func (p Plugin) GetInitSubcommand() plugin.InitSubcommand { return &p.initSubcommand }
-
-// GetCreateAPISubcommand will return the subcommand which is responsible for scaffolding apis
-func (p Plugin) GetCreateAPISubcommand() plugin.CreateAPISubcommand { return &p.createAPISubcommand }
-
-// GetCreateWebhookSubcommand will return the subcommand which is responsible for scaffolding webhooks
-func (p Plugin) GetCreateWebhookSubcommand() plugin.CreateWebhookSubcommand {
- return &p.createWebhookSubcommand
-}
-
-// GetEditSubcommand will return the subcommand which is responsible for editing the scaffold of the project
-func (p Plugin) GetEditSubcommand() plugin.EditSubcommand { return &p.editSubcommand }
diff --git a/pkg/plugins/golang/v3/scaffolds/api.go b/pkg/plugins/golang/v3/scaffolds/api.go
deleted file mode 100644
index fe9ad41da08..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/api.go
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package scaffolds
-
-import (
- "fmt"
-
- "github.com/spf13/afero"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/api"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/hack"
-)
-
-var _ plugins.Scaffolder = &apiScaffolder{}
-
-// apiScaffolder contains configuration for generating scaffolding for Go type
-// representing the API and controller that implements the behavior for the API.
-type apiScaffolder struct {
- config config.Config
- resource resource.Resource
-
- // fs is the filesystem that will be used by the scaffolder
- fs machinery.Filesystem
-
- // force indicates whether to scaffold controller files even if it exists or not
- force bool
-}
-
-// NewAPIScaffolder returns a new Scaffolder for API/controller creation operations
-func NewAPIScaffolder(config config.Config, res resource.Resource, force bool) plugins.Scaffolder {
- return &apiScaffolder{
- config: config,
- resource: res,
- force: force,
- }
-}
-
-// InjectFS implements cmdutil.Scaffolder
-func (s *apiScaffolder) InjectFS(fs machinery.Filesystem) {
- s.fs = fs
-}
-
-// Scaffold implements cmdutil.Scaffolder
-func (s *apiScaffolder) Scaffold() error {
- fmt.Println("Writing scaffold for you to edit...")
-
- // Load the boilerplate
- boilerplate, err := afero.ReadFile(s.fs.FS, hack.DefaultBoilerplatePath)
- if err != nil {
- return fmt.Errorf("error scaffolding API/controller: unable to load boilerplate: %w", err)
- }
-
- // Initialize the machinery.Scaffold that will write the files to disk
- scaffold := machinery.NewScaffold(s.fs,
- machinery.WithConfig(s.config),
- machinery.WithBoilerplate(string(boilerplate)),
- machinery.WithResource(&s.resource),
- )
-
- // Keep track of these values before the update
- doAPI := s.resource.HasAPI()
- doController := s.resource.HasController()
-
- if err := s.config.UpdateResource(s.resource); err != nil {
- return fmt.Errorf("error updating resource: %w", err)
- }
-
- if doAPI {
- if err := scaffold.Execute(
- &api.Types{Force: s.force},
- &api.Group{},
- ); err != nil {
- return fmt.Errorf("error scaffolding APIs: %v", err)
- }
- }
-
- if doController {
- if err := scaffold.Execute(
- &controllers.SuiteTest{Force: s.force},
- &controllers.Controller{ControllerRuntimeVersion: ControllerRuntimeVersion, Force: s.force},
- ); err != nil {
- return fmt.Errorf("error scaffolding controller: %v", err)
- }
- }
-
- if err := scaffold.Execute(
- &templates.MainUpdater{WireResource: doAPI, WireController: doController},
- ); err != nil {
- return fmt.Errorf("error updating main.go: %v", err)
- }
-
- return nil
-}
diff --git a/pkg/plugins/golang/v3/scaffolds/doc.go b/pkg/plugins/golang/v3/scaffolds/doc.go
deleted file mode 100644
index 71ae0484cf4..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/doc.go
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-// Package scaffolds contains libraries for scaffolding code to use with controller-runtime
-package scaffolds
diff --git a/pkg/plugins/golang/v3/scaffolds/edit.go b/pkg/plugins/golang/v3/scaffolds/edit.go
deleted file mode 100644
index e099a8a7b16..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/edit.go
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package scaffolds
-
-import (
- "fmt"
- "strings"
-
- "github.com/spf13/afero"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins"
-)
-
-var _ plugins.Scaffolder = &editScaffolder{}
-
-type editScaffolder struct {
- config config.Config
- multigroup bool
-
- // fs is the filesystem that will be used by the scaffolder
- fs machinery.Filesystem
-}
-
-// NewEditScaffolder returns a new Scaffolder for configuration edit operations
-func NewEditScaffolder(config config.Config, multigroup bool) plugins.Scaffolder {
- return &editScaffolder{
- config: config,
- multigroup: multigroup,
- }
-}
-
-// InjectFS implements cmdutil.Scaffolder
-func (s *editScaffolder) InjectFS(fs machinery.Filesystem) {
- s.fs = fs
-}
-
-// Scaffold implements cmdutil.Scaffolder
-func (s *editScaffolder) Scaffold() error {
- filename := "Dockerfile"
- bs, err := afero.ReadFile(s.fs.FS, filename)
- if err != nil {
- return err
- }
- str := string(bs)
-
- // update dockerfile
- if s.multigroup {
- str, err = ensureExistAndReplace(
- str,
- "COPY api/ api/",
- `COPY apis/ apis/`)
-
- } else {
- str, err = ensureExistAndReplace(
- str,
- "COPY apis/ apis/",
- `COPY api/ api/`)
- }
-
- // Ignore the error encountered, if the file is already in desired format.
- if err != nil && s.multigroup != s.config.IsMultiGroup() {
- return err
- }
-
- if s.multigroup {
- _ = s.config.SetMultiGroup()
- } else {
- _ = s.config.ClearMultiGroup()
- }
-
- // Check if the str is not empty, because when the file is already in desired format it will return empty string
- // because there is nothing to replace.
- if str != "" {
- // TODO: instead of writing it directly, we should use the scaffolding machinery for consistency
- return afero.WriteFile(s.fs.FS, filename, []byte(str), 0644)
- }
-
- return nil
-}
-
-func ensureExistAndReplace(input, match, replace string) (string, error) {
- if !strings.Contains(input, match) {
- return "", fmt.Errorf("can't find %q", match)
- }
- return strings.Replace(input, match, replace, -1), nil
-}
diff --git a/pkg/plugins/golang/v3/scaffolds/init.go b/pkg/plugins/golang/v3/scaffolds/init.go
deleted file mode 100644
index 14d3c84317c..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/init.go
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package scaffolds
-
-import (
- "fmt"
-
- "github.com/spf13/afero"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/hack"
-)
-
-const (
- // ControllerRuntimeVersion is the kubernetes-sigs/controller-runtime version to be used in the project
- ControllerRuntimeVersion = "v0.7.2"
- // ControllerToolsVersion is the kubernetes-sigs/controller-tools version to be used in the project
- ControllerToolsVersion = "v0.4.1"
- // KustomizeVersion is the kubernetes-sigs/kustomize version to be used in the project
- KustomizeVersion = "v3.8.7"
-
- imageName = "controller:latest"
-)
-
-var _ plugins.Scaffolder = &initScaffolder{}
-
-type initScaffolder struct {
- config config.Config
- boilerplatePath string
- license string
- owner string
-
- // fs is the filesystem that will be used by the scaffolder
- fs machinery.Filesystem
-}
-
-// NewInitScaffolder returns a new Scaffolder for project initialization operations
-func NewInitScaffolder(config config.Config, license, owner string) plugins.Scaffolder {
- return &initScaffolder{
- config: config,
- boilerplatePath: hack.DefaultBoilerplatePath,
- license: license,
- owner: owner,
- }
-}
-
-// InjectFS implements cmdutil.Scaffolder
-func (s *initScaffolder) InjectFS(fs machinery.Filesystem) {
- s.fs = fs
-}
-
-// Scaffold implements cmdutil.Scaffolder
-func (s *initScaffolder) Scaffold() error {
- fmt.Println("Writing scaffold for you to edit...")
-
- // Initialize the machinery.Scaffold that will write the boilerplate file to disk
- // The boilerplate file needs to be scaffolded as a separate step as it is going to
- // be used by the rest of the files, even those scaffolded in this command call.
- scaffold := machinery.NewScaffold(s.fs,
- machinery.WithConfig(s.config),
- )
-
- bpFile := &hack.Boilerplate{
- License: s.license,
- Owner: s.owner,
- }
- bpFile.Path = s.boilerplatePath
- if err := scaffold.Execute(bpFile); err != nil {
- return err
- }
-
- boilerplate, err := afero.ReadFile(s.fs.FS, s.boilerplatePath)
- if err != nil {
- return err
- }
-
- // Initialize the machinery.Scaffold that will write the files to disk
- scaffold = machinery.NewScaffold(s.fs,
- machinery.WithConfig(s.config),
- machinery.WithBoilerplate(string(boilerplate)),
- )
-
- return scaffold.Execute(
- &templates.Main{},
- &templates.GoMod{ControllerRuntimeVersion: ControllerRuntimeVersion},
- &templates.GitIgnore{},
- &templates.Makefile{
- Image: imageName,
- BoilerplatePath: s.boilerplatePath,
- ControllerToolsVersion: ControllerToolsVersion,
- KustomizeVersion: KustomizeVersion,
- ControllerRuntimeVersion: ControllerRuntimeVersion,
- },
- &templates.Dockerfile{},
- &templates.DockerIgnore{},
- )
-}
diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/group.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/group.go
deleted file mode 100644
index 8c93af689a0..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/group.go
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package api
-
-import (
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Group{}
-
-// Group scaffolds the file that defines the registration methods for a certain group and version
-type Group struct {
- machinery.TemplateMixin
- machinery.MultiGroupMixin
- machinery.BoilerplateMixin
- machinery.ResourceMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Group) SetTemplateDefaults() error {
- if f.Path == "" {
- if f.MultiGroup {
- if f.Resource.Group != "" {
- f.Path = filepath.Join("apis", "%[group]", "%[version]", "groupversion_info.go")
- } else {
- f.Path = filepath.Join("apis", "%[version]", "groupversion_info.go")
- }
- } else {
- f.Path = filepath.Join("api", "%[version]", "groupversion_info.go")
- }
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
-
- f.TemplateBody = groupTemplate
-
- return nil
-}
-
-//nolint:lll
-const groupTemplate = `{{ .Boilerplate }}
-
-// Package {{ .Resource.Version }} contains API Schema definitions for the {{ .Resource.Group }} {{ .Resource.Version }} API group
-//+kubebuilder:object:generate=true
-//+groupName={{ .Resource.QualifiedGroup }}
-package {{ .Resource.Version }}
-
-import (
- "k8s.io/apimachinery/pkg/runtime/schema"
- "sigs.k8s.io/controller-runtime/pkg/scheme"
-)
-
-var (
- // GroupVersion is group version used to register these objects
- GroupVersion = schema.GroupVersion{Group: "{{ .Resource.QualifiedGroup }}", Version: "{{ .Resource.Version }}"}
-
- // SchemeBuilder is used to add go types to the GroupVersionKind scheme
- SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
-
- // AddToScheme adds the types in this group-version to the given scheme.
- AddToScheme = SchemeBuilder.AddToScheme
-)
-`
diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/types.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/types.go
deleted file mode 100644
index a76e3cb1391..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/types.go
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package api
-
-import (
- "fmt"
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Types{}
-
-// Types scaffolds the file that defines the schema for a CRD
-// nolint:maligned
-type Types struct {
- machinery.TemplateMixin
- machinery.MultiGroupMixin
- machinery.BoilerplateMixin
- machinery.ResourceMixin
-
- Force bool
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Types) SetTemplateDefaults() error {
- if f.Path == "" {
- if f.MultiGroup {
- if f.Resource.Group != "" {
- f.Path = filepath.Join("apis", "%[group]", "%[version]", "%[kind]_types.go")
- } else {
- f.Path = filepath.Join("apis", "%[version]", "%[kind]_types.go")
- }
- } else {
- f.Path = filepath.Join("api", "%[version]", "%[kind]_types.go")
- }
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
- fmt.Println(f.Path)
-
- f.TemplateBody = typesTemplate
-
- if f.Force {
- f.IfExistsAction = machinery.OverwriteFile
- } else {
- f.IfExistsAction = machinery.Error
- }
-
- return nil
-}
-
-const typesTemplate = `{{ .Boilerplate }}
-
-package {{ .Resource.Version }}
-
-import (
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-)
-
-// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
-// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
-
-// {{ .Resource.Kind }}Spec defines the desired state of {{ .Resource.Kind }}
-type {{ .Resource.Kind }}Spec struct {
- // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
- // Important: Run "make" to regenerate code after modifying this file
-
- // Foo is an example field of {{ .Resource.Kind }}. Edit {{ lower .Resource.Kind }}_types.go to remove/update
- Foo string ` + "`" + `json:"foo,omitempty"` + "`" + `
-}
-
-// {{ .Resource.Kind }}Status defines the observed state of {{ .Resource.Kind }}
-type {{ .Resource.Kind }}Status struct {
- // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
- // Important: Run "make" to regenerate code after modifying this file
-}
-
-//+kubebuilder:object:root=true
-//+kubebuilder:subresource:status
-{{- if and (not .Resource.API.Namespaced) (not .Resource.IsRegularPlural) }}
-//+kubebuilder:resource:path={{ .Resource.Plural }},scope=Cluster
-{{- else if not .Resource.API.Namespaced }}
-//+kubebuilder:resource:scope=Cluster
-{{- else if not .Resource.IsRegularPlural }}
-//+kubebuilder:resource:path={{ .Resource.Plural }}
-{{- end }}
-
-// {{ .Resource.Kind }} is the Schema for the {{ .Resource.Plural }} API
-type {{ .Resource.Kind }} struct {
- metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + `
- metav1.ObjectMeta ` + "`" + `json:"metadata,omitempty"` + "`" + `
-
- Spec {{ .Resource.Kind }}Spec ` + "`" + `json:"spec,omitempty"` + "`" + `
- Status {{ .Resource.Kind }}Status ` + "`" + `json:"status,omitempty"` + "`" + `
-}
-
-//+kubebuilder:object:root=true
-
-// {{ .Resource.Kind }}List contains a list of {{ .Resource.Kind }}
-type {{ .Resource.Kind }}List struct {
- metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + `
- metav1.ListMeta ` + "`" + `json:"metadata,omitempty"` + "`" + `
- Items []{{ .Resource.Kind }} ` + "`" + `json:"items"` + "`" + `
-}
-
-func init() {
- SchemeBuilder.Register(&{{ .Resource.Kind }}{}, &{{ .Resource.Kind }}List{})
-}
-`
diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook.go
deleted file mode 100644
index f51ef49bda0..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook.go
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package api
-
-import (
- "fmt"
- "path/filepath"
- "strings"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Webhook{}
-
-// Webhook scaffolds the file that defines a webhook for a CRD or a builtin resource
-type Webhook struct { // nolint:maligned
- machinery.TemplateMixin
- machinery.MultiGroupMixin
- machinery.BoilerplateMixin
- machinery.ResourceMixin
-
- // Is the Group domain for the Resource replacing '.' with '-'
- QualifiedGroupWithDash string
-
- Force bool
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Webhook) SetTemplateDefaults() error {
- if f.Path == "" {
- if f.MultiGroup {
- if f.Resource.Group != "" {
- f.Path = filepath.Join("apis", "%[group]", "%[version]", "%[kind]_webhook.go")
- } else {
- f.Path = filepath.Join("apis", "%[version]", "%[kind]_webhook.go")
- }
- } else {
- f.Path = filepath.Join("api", "%[version]", "%[kind]_webhook.go")
- }
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
- fmt.Println(f.Path)
-
- webhookTemplate := webhookTemplate
- if f.Resource.HasDefaultingWebhook() {
- webhookTemplate = webhookTemplate + defaultingWebhookTemplate
- }
- if f.Resource.HasValidationWebhook() {
- webhookTemplate = webhookTemplate + validatingWebhookTemplate
- }
- f.TemplateBody = webhookTemplate
-
- if f.Force {
- f.IfExistsAction = machinery.OverwriteFile
- } else {
- f.IfExistsAction = machinery.Error
- }
-
- f.QualifiedGroupWithDash = strings.Replace(f.Resource.QualifiedGroup(), ".", "-", -1)
-
- return nil
-}
-
-const (
- webhookTemplate = `{{ .Boilerplate }}
-
-package {{ .Resource.Version }}
-
-import (
- ctrl "sigs.k8s.io/controller-runtime"
- logf "sigs.k8s.io/controller-runtime/pkg/log"
- {{- if .Resource.HasValidationWebhook }}
- "k8s.io/apimachinery/pkg/runtime"
- {{- end }}
- {{- if or .Resource.HasValidationWebhook .Resource.HasDefaultingWebhook }}
- "sigs.k8s.io/controller-runtime/pkg/webhook"
- {{- end }}
-)
-
-// log is for logging in this package.
-var {{ lower .Resource.Kind }}log = logf.Log.WithName("{{ lower .Resource.Kind }}-resource")
-
-func (r *{{ .Resource.Kind }}) SetupWebhookWithManager(mgr ctrl.Manager) error {
- return ctrl.NewWebhookManagedBy(mgr).
- For(r).
- Complete()
-}
-
-// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
-`
-
- // TODO(estroz): update admissionReviewVersions to include v1 when controller-runtime supports that version.
- //nolint:lll
- defaultingWebhookTemplate = `
-//+kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}path=/mutate-{{ .QualifiedGroupWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=true,failurePolicy=fail,sideEffects=None,groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=m{{ lower .Resource.Kind }}.kb.io,admissionReviewVersions={v1,v1beta1}
-
-var _ webhook.Defaulter = &{{ .Resource.Kind }}{}
-
-// Default implements webhook.Defaulter so a webhook will be registered for the type
-func (r *{{ .Resource.Kind }}) Default() {
- {{ lower .Resource.Kind }}log.Info("default", "name", r.Name)
-
- // TODO(user): fill in your defaulting logic.
-}
-`
-
- // TODO(estroz): update admissionReviewVersions to include v1 when controller-runtime supports that version.
- //nolint:lll
- validatingWebhookTemplate = `
-// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
-//+kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}path=/validate-{{ .QualifiedGroupWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=false,failurePolicy=fail,sideEffects=None,groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=v{{ lower .Resource.Kind }}.kb.io,admissionReviewVersions={v1,v1beta1}
-
-var _ webhook.Validator = &{{ .Resource.Kind }}{}
-
-// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
-func (r *{{ .Resource.Kind }}) ValidateCreate() error {
- {{ lower .Resource.Kind }}log.Info("validate create", "name", r.Name)
-
- // TODO(user): fill in your validation logic upon object creation.
- return nil
-}
-
-// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
-func (r *{{ .Resource.Kind }}) ValidateUpdate(old runtime.Object) error {
- {{ lower .Resource.Kind }}log.Info("validate update", "name", r.Name)
-
- // TODO(user): fill in your validation logic upon object update.
- return nil
-}
-
-// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
-func (r *{{ .Resource.Kind }}) ValidateDelete() error {
- {{ lower .Resource.Kind }}log.Info("validate delete", "name", r.Name)
-
- // TODO(user): fill in your validation logic upon object deletion.
- return nil
-}
-`
-)
diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook_suitetest.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook_suitetest.go
deleted file mode 100644
index 7f30dd89f37..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook_suitetest.go
+++ /dev/null
@@ -1,230 +0,0 @@
-package api
-
-import (
- "fmt"
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &WebhookSuite{}
-var _ machinery.Inserter = &WebhookSuite{}
-
-// WebhookSuite scaffolds the file that sets up the webhook tests
-type WebhookSuite struct { //nolint:maligned
- machinery.TemplateMixin
- machinery.MultiGroupMixin
- machinery.BoilerplateMixin
- machinery.ResourceMixin
-
- // todo: currently is not possible to know if an API was or not scaffolded. We can fix it when #1826 be addressed
- WireResource bool
-
- // BaseDirectoryRelativePath define the Path for the base directory when it is multigroup
- BaseDirectoryRelativePath string
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *WebhookSuite) SetTemplateDefaults() error {
- if f.Path == "" {
- if f.MultiGroup {
- if f.Resource.Group != "" {
- f.Path = filepath.Join("apis", "%[group]", "%[version]", "webhook_suite_test.go")
- } else {
- f.Path = filepath.Join("apis", "%[version]", "webhook_suite_test.go")
- }
- } else {
- f.Path = filepath.Join("api", "%[version]", "webhook_suite_test.go")
- }
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
-
- f.TemplateBody = fmt.Sprintf(webhookTestSuiteTemplate,
- machinery.NewMarkerFor(f.Path, importMarker),
- machinery.NewMarkerFor(f.Path, addSchemeMarker),
- machinery.NewMarkerFor(f.Path, addWebhookManagerMarker),
- "%s",
- "%d",
- )
-
- // If is multigroup the path needs to be ../../.. since it has the group dir.
- f.BaseDirectoryRelativePath = `"..", ".."`
- if f.MultiGroup && f.Resource.Group != "" {
- f.BaseDirectoryRelativePath = `"..", "..",".."`
- }
-
- return nil
-}
-
-const (
- // TODO: admission webhook versions should be based on the input of the user. For More Info #1664
- admissionImportAlias = "admissionv1beta1"
- admissionPath = "k8s.io/api/admission/v1beta1"
- importMarker = "imports"
- addWebhookManagerMarker = "webhook"
- addSchemeMarker = "scheme"
-)
-
-// GetMarkers implements file.Inserter
-func (f *WebhookSuite) GetMarkers() []machinery.Marker {
- return []machinery.Marker{
- machinery.NewMarkerFor(f.Path, importMarker),
- machinery.NewMarkerFor(f.Path, addSchemeMarker),
- machinery.NewMarkerFor(f.Path, addWebhookManagerMarker),
- }
-}
-
-const (
- apiImportCodeFragment = `%s "%s"
-`
- addschemeCodeFragment = `err = %s.AddToScheme(scheme )
-Expect(err).NotTo(HaveOccurred())
-
-`
- addWebhookManagerCodeFragment = `err = (&%s{}).SetupWebhookWithManager(mgr)
-Expect(err).NotTo(HaveOccurred())
-
-`
-)
-
-// GetCodeFragments implements file.Inserter
-func (f *WebhookSuite) GetCodeFragments() machinery.CodeFragmentsMap {
- fragments := make(machinery.CodeFragmentsMap, 3)
-
- // Generate import code fragments
- imports := make([]string, 0)
- imports = append(imports, fmt.Sprintf(apiImportCodeFragment, admissionImportAlias, admissionPath))
-
- // Generate add scheme code fragments
- addScheme := make([]string, 0)
- addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, admissionImportAlias))
-
- // Generate add webhookManager code fragments
- addWebhookManager := make([]string, 0)
- addWebhookManager = append(addWebhookManager, fmt.Sprintf(addWebhookManagerCodeFragment, f.Resource.Kind))
-
- // Only store code fragments in the map if the slices are non-empty
- if len(addWebhookManager) != 0 {
- fragments[machinery.NewMarkerFor(f.Path, addWebhookManagerMarker)] = addWebhookManager
- }
- if len(imports) != 0 {
- fragments[machinery.NewMarkerFor(f.Path, importMarker)] = imports
- }
- if len(addScheme) != 0 {
- fragments[machinery.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme
- }
-
- return fragments
-}
-
-const webhookTestSuiteTemplate = `{{ .Boilerplate }}
-
-package {{ .Resource.Version }}
-
-import (
- "context"
- "path/filepath"
- "testing"
- "fmt"
-
- . "github.com/onsi/ginkgo"
- . "github.com/onsi/gomega"
- %s
- "k8s.io/client-go/kubernetes/scheme"
- "k8s.io/client-go/rest"
- "k8s.io/apimachinery/pkg/runtime"
- ctrl "sigs.k8s.io/controller-runtime"
- "sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/controller-runtime/pkg/envtest"
- "sigs.k8s.io/controller-runtime/pkg/envtest/printer"
- logf "sigs.k8s.io/controller-runtime/pkg/log"
- "sigs.k8s.io/controller-runtime/pkg/log/zap"
-)
-
-// These tests use Ginkgo (BDD-style Go testing framework). Refer to
-// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
-
-var cfg *rest.Config
-var k8sClient client.Client
-var testEnv *envtest.Environment
-var ctx context.Context
-var cancel context.CancelFunc
-
-func TestAPIs(t *testing.T) {
- RegisterFailHandler(Fail)
-
- RunSpecsWithDefaultAndCustomReporters(t,
- "Webhook Suite",
- []Reporter{printer.NewlineReporter{}})
-}
-
-var _ = BeforeSuite(func() {
- logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
-
- ctx, cancel = context.WithCancel(context.TODO())
-
- By("bootstrapping test environment")
- testEnv = &envtest.Environment{
- CRDDirectoryPaths: []string{filepath.Join({{ .BaseDirectoryRelativePath }}, "config", "crd", "bases")},
- ErrorIfCRDPathMissing: {{ .WireResource }},
- WebhookInstallOptions: envtest.WebhookInstallOptions{
- Paths: []string{filepath.Join({{ .BaseDirectoryRelativePath }}, "config", "webhook")},
- },
- }
-
- cfg, err := testEnv.Start()
- Expect(err).NotTo(HaveOccurred())
- Expect(cfg).NotTo(BeNil())
-
- scheme := runtime.NewScheme()
- err = AddToScheme(scheme)
- Expect(err).NotTo(HaveOccurred())
-
- %s
-
- k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
- Expect(err).NotTo(HaveOccurred())
- Expect(k8sClient).NotTo(BeNil())
-
- // start webhook server using Manager
- webhookInstallOptions := &testEnv.WebhookInstallOptions
- mgr, err := ctrl.NewManager(cfg, ctrl.Options{
- Scheme: scheme,
- Host: webhookInstallOptions.LocalServingHost,
- Port: webhookInstallOptions.LocalServingPort,
- CertDir: webhookInstallOptions.LocalServingCertDir,
- LeaderElection: false,
- MetricsBindAddress: "0",
- })
- Expect(err).NotTo(HaveOccurred())
-
- %s
-
- go func() {
- err = mgr.Start(ctx)
- if err != nil {
- Expect(err).NotTo(HaveOccurred())
- }
- }()
-
- // wait for the webhook server to get ready
- dialer := &net.Dialer{Timeout: time.Second}
- addrPort := fmt.Sprintf("%s:%s", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
- Eventually(func() error {
- conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
- if err != nil {
- return err
- }
- conn.Close()
- return nil
- }).Should(Succeed())
-
-}, 60)
-
-var _ = AfterSuite(func() {
- cancel()
- By("tearing down the test environment")
- err := testEnv.Stop()
- Expect(err).NotTo(HaveOccurred())
-})
-`
diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller.go
deleted file mode 100644
index 32d64bcf006..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller.go
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package controllers
-
-import (
- "fmt"
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Controller{}
-
-// Controller scaffolds the file that defines the controller for a CRD or a builtin resource
-// nolint:maligned
-type Controller struct {
- machinery.TemplateMixin
- machinery.MultiGroupMixin
- machinery.BoilerplateMixin
- machinery.ResourceMixin
-
- ControllerRuntimeVersion string
-
- Force bool
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Controller) SetTemplateDefaults() error {
- if f.Path == "" {
- if f.MultiGroup && f.Resource.Group != "" {
- f.Path = filepath.Join("controllers", "%[group]", "%[kind]_controller.go")
- } else {
- f.Path = filepath.Join("controllers", "%[kind]_controller.go")
- }
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
- fmt.Println(f.Path)
-
- f.TemplateBody = controllerTemplate
-
- if f.Force {
- f.IfExistsAction = machinery.OverwriteFile
- } else {
- f.IfExistsAction = machinery.Error
- }
-
- return nil
-}
-
-//nolint:lll
-const controllerTemplate = `{{ .Boilerplate }}
-
-package {{ if and .MultiGroup .Resource.Group }}{{ .Resource.PackageName }}{{ else }}controllers{{ end }}
-
-import (
- "context"
- "github.com/go-logr/logr"
- "k8s.io/apimachinery/pkg/runtime"
- ctrl "sigs.k8s.io/controller-runtime"
- "sigs.k8s.io/controller-runtime/pkg/client"
- {{ if not (isEmptyStr .Resource.Path) -}}
- {{ .Resource.ImportAlias }} "{{ .Resource.Path }}"
- {{- end }}
-)
-
-// {{ .Resource.Kind }}Reconciler reconciles a {{ .Resource.Kind }} object
-type {{ .Resource.Kind }}Reconciler struct {
- client.Client
- Log logr.Logger
- Scheme *runtime.Scheme
-}
-
-//+kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=get;list;watch;create;update;patch;delete
-//+kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }}/status,verbs=get;update;patch
-//+kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }}/finalizers,verbs=update
-
-// Reconcile is part of the main kubernetes reconciliation loop which aims to
-// move the current state of the cluster closer to the desired state.
-// TODO(user): Modify the Reconcile function to compare the state specified by
-// the {{ .Resource.Kind }} object against the actual cluster state, and then
-// perform operations to make the cluster state reflect the state specified by
-// the user.
-//
-// For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@{{ .ControllerRuntimeVersion }}/pkg/reconcile
-func (r *{{ .Resource.Kind }}Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
- _ = r.Log.WithValues("{{ .Resource.Kind | lower }}", req.NamespacedName)
-
- // your logic here
-
- return ctrl.Result{}, nil
-}
-
-// SetupWithManager sets up the controller with the Manager.
-func (r *{{ .Resource.Kind }}Reconciler) SetupWithManager(mgr ctrl.Manager) error {
- return ctrl.NewControllerManagedBy(mgr).
- {{ if not (isEmptyStr .Resource.Path) -}}
- For(&{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}).
- {{- else -}}
- // Uncomment the following line adding a pointer to an instance of the controlled resource as an argument
- // For().
- {{- end }}
- Complete(r)
-}
-`
diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller_suitetest.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller_suitetest.go
deleted file mode 100644
index 1dbb649a40c..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller_suitetest.go
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package controllers
-
-import (
- "fmt"
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &SuiteTest{}
-var _ machinery.Inserter = &SuiteTest{}
-
-// SuiteTest scaffolds the file that sets up the controller tests
-// nolint:maligned
-type SuiteTest struct {
- machinery.TemplateMixin
- machinery.MultiGroupMixin
- machinery.BoilerplateMixin
- machinery.ResourceMixin
-
- // CRDDirectoryRelativePath define the Path for the CRD
- CRDDirectoryRelativePath string
-
- Force bool
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *SuiteTest) SetTemplateDefaults() error {
- if f.Path == "" {
- if f.MultiGroup && f.Resource.Group != "" {
- f.Path = filepath.Join("controllers", "%[group]", "suite_test.go")
- } else {
- f.Path = filepath.Join("controllers", "suite_test.go")
- }
- }
- f.Path = f.Resource.Replacer().Replace(f.Path)
-
- f.TemplateBody = fmt.Sprintf(controllerSuiteTestTemplate,
- machinery.NewMarkerFor(f.Path, importMarker),
- machinery.NewMarkerFor(f.Path, addSchemeMarker),
- )
-
- // If is multigroup the path needs to be ../../ since it has
- // the group dir.
- f.CRDDirectoryRelativePath = `".."`
- if f.MultiGroup && f.Resource.Group != "" {
- f.CRDDirectoryRelativePath = `"..", ".."`
- }
-
- if f.Force {
- f.IfExistsAction = machinery.OverwriteFile
- }
-
- return nil
-}
-
-const (
- importMarker = "imports"
- addSchemeMarker = "scheme"
-)
-
-// GetMarkers implements file.Inserter
-func (f *SuiteTest) GetMarkers() []machinery.Marker {
- return []machinery.Marker{
- machinery.NewMarkerFor(f.Path, importMarker),
- machinery.NewMarkerFor(f.Path, addSchemeMarker),
- }
-}
-
-const (
- apiImportCodeFragment = `%s "%s"
-`
- addschemeCodeFragment = `err = %s.AddToScheme(scheme.Scheme)
-Expect(err).NotTo(HaveOccurred())
-
-`
-)
-
-// GetCodeFragments implements file.Inserter
-func (f *SuiteTest) GetCodeFragments() machinery.CodeFragmentsMap {
- fragments := make(machinery.CodeFragmentsMap, 2)
-
- // Generate import code fragments
- imports := make([]string, 0)
- if f.Resource.Path != "" {
- imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path))
- }
-
- // Generate add scheme code fragments
- addScheme := make([]string, 0)
- if f.Resource.Path != "" {
- addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias()))
- }
-
- // Only store code fragments in the map if the slices are non-empty
- if len(imports) != 0 {
- fragments[machinery.NewMarkerFor(f.Path, importMarker)] = imports
- }
- if len(addScheme) != 0 {
- fragments[machinery.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme
- }
-
- return fragments
-}
-
-const controllerSuiteTestTemplate = `{{ .Boilerplate }}
-
-{{if and .MultiGroup .Resource.Group }}
-package {{ .Resource.PackageName }}
-{{else}}
-package controllers
-{{end}}
-
-import (
- "path/filepath"
- "testing"
- . "github.com/onsi/ginkgo"
- . "github.com/onsi/gomega"
- "k8s.io/client-go/kubernetes/scheme"
- "k8s.io/client-go/rest"
- "sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/controller-runtime/pkg/envtest"
- "sigs.k8s.io/controller-runtime/pkg/envtest/printer"
- logf "sigs.k8s.io/controller-runtime/pkg/log"
- "sigs.k8s.io/controller-runtime/pkg/log/zap"
- %s
-)
-
-// These tests use Ginkgo (BDD-style Go testing framework). Refer to
-// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
-
-var cfg *rest.Config
-var k8sClient client.Client
-var testEnv *envtest.Environment
-
-func TestAPIs(t *testing.T) {
- RegisterFailHandler(Fail)
-
- RunSpecsWithDefaultAndCustomReporters(t,
- "Controller Suite",
- []Reporter{printer.NewlineReporter{}})
-}
-
-var _ = BeforeSuite(func() {
- logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
-
- By("bootstrapping test environment")
- testEnv = &envtest.Environment{
- CRDDirectoryPaths: []string{filepath.Join({{ .CRDDirectoryRelativePath }}, "config", "crd", "bases")},
- ErrorIfCRDPathMissing: {{ .Resource.HasAPI }},
- }
-
- cfg, err := testEnv.Start()
- Expect(err).NotTo(HaveOccurred())
- Expect(cfg).NotTo(BeNil())
-
- %s
-
- k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
- Expect(err).NotTo(HaveOccurred())
- Expect(k8sClient).NotTo(BeNil())
-
-}, 60)
-
-var _ = AfterSuite(func() {
- By("tearing down the test environment")
- err := testEnv.Stop()
- Expect(err).NotTo(HaveOccurred())
-})
-`
diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerfile.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerfile.go
deleted file mode 100644
index 7794ab4e08c..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerfile.go
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package templates
-
-import (
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Dockerfile{}
-
-// Dockerfile scaffolds a file that defines the containerized build process
-type Dockerfile struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Dockerfile) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = "Dockerfile"
- }
-
- f.TemplateBody = dockerfileTemplate
-
- return nil
-}
-
-const dockerfileTemplate = `# Build the manager binary
-FROM golang:1.15 as builder
-
-WORKDIR /workspace
-# Copy the Go Modules manifests
-COPY go.mod go.mod
-COPY go.sum go.sum
-# cache deps before building and copying source so that we don't need to re-download as much
-# and so that source changes don't invalidate our downloaded layer
-RUN go mod download
-
-# Copy the go source
-COPY main.go main.go
-COPY api/ api/
-COPY controllers/ controllers/
-
-# Build
-RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager main.go
-
-# Use distroless as minimal base image to package the manager binary
-# Refer to https://github.com/GoogleContainerTools/distroless for more details
-FROM gcr.io/distroless/static:nonroot
-WORKDIR /
-COPY --from=builder /workspace/manager .
-USER 65532:65532
-
-ENTRYPOINT ["/manager"]
-`
diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/gitignore.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/gitignore.go
deleted file mode 100644
index 2b5ef060f98..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/internal/templates/gitignore.go
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package templates
-
-import (
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &GitIgnore{}
-
-// GitIgnore scaffolds a file that defines which files should be ignored by git
-type GitIgnore struct {
- machinery.TemplateMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *GitIgnore) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = ".gitignore"
- }
-
- f.TemplateBody = gitignoreTemplate
-
- return nil
-}
-
-const gitignoreTemplate = `
-# Binaries for programs and plugins
-*.exe
-*.exe~
-*.dll
-*.so
-*.dylib
-bin
-testbin/*
-
-# Test binary, build with ` + "`go test -c`" + `
-*.test
-
-# Output of the go coverage tool, specifically when used with LiteIDE
-*.out
-
-# Kubernetes Generated files - skip generated files, except for vendored files
-
-!vendor/**/zz_generated.*
-
-# editor and IDE paraphernalia
-.idea
-*.swp
-*.swo
-*~
-`
diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/gomod.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/gomod.go
deleted file mode 100644
index e79e17bf47a..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/internal/templates/gomod.go
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package templates
-
-import (
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &GoMod{}
-
-// GoMod scaffolds a file that defines the project dependencies
-type GoMod struct {
- machinery.TemplateMixin
- machinery.RepositoryMixin
-
- ControllerRuntimeVersion string
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *GoMod) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = "go.mod"
- }
-
- f.TemplateBody = goModTemplate
-
- f.IfExistsAction = machinery.OverwriteFile
-
- return nil
-}
-
-const goModTemplate = `
-module {{ .Repo }}
-
-go 1.15
-
-require (
- sigs.k8s.io/controller-runtime {{ .ControllerRuntimeVersion }}
-)
-`
diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/hack/boilerplate.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/hack/boilerplate.go
deleted file mode 100644
index 3efbe592a54..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/internal/templates/hack/boilerplate.go
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package hack
-
-import (
- "fmt"
- "path/filepath"
- "time"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-// DefaultBoilerplatePath is the default path to the boilerplate file
-var DefaultBoilerplatePath = filepath.Join("hack", "boilerplate.go.txt")
-
-var _ machinery.Template = &Boilerplate{}
-
-// Boilerplate scaffolds a file that defines the common header for the rest of the files
-type Boilerplate struct {
- machinery.TemplateMixin
- machinery.BoilerplateMixin
-
- // License is the License type to write
- License string
-
- // Licenses maps License types to their actual string
- Licenses map[string]string
-
- // Owner is the copyright owner - e.g. "The Kubernetes Authors"
- Owner string
-
- // Year is the copyright year
- Year string
-}
-
-// Validate implements file.RequiresValidation
-func (f Boilerplate) Validate() error {
- if f.License == "" {
- // A default license will be set later
- } else if _, found := knownLicenses[f.License]; found {
- // One of the know licenses
- } else if _, found := f.Licenses[f.License]; found {
- // A map containing the requested license was also provided
- } else {
- return fmt.Errorf("unknown specified license %s", f.License)
- }
-
- return nil
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Boilerplate) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = DefaultBoilerplatePath
- }
-
- if f.License == "" {
- f.License = "apache2"
- }
-
- if f.Licenses == nil {
- f.Licenses = make(map[string]string, len(knownLicenses))
- }
-
- for key, value := range knownLicenses {
- if _, hasLicense := f.Licenses[key]; !hasLicense {
- f.Licenses[key] = value
- }
- }
-
- if f.Year == "" {
- f.Year = fmt.Sprintf("%v", time.Now().Year())
- }
-
- // Boilerplate given
- if len(f.Boilerplate) > 0 {
- f.TemplateBody = f.Boilerplate
- return nil
- }
-
- f.TemplateBody = boilerplateTemplate
-
- return nil
-}
-
-const boilerplateTemplate = `/*
-{{ if .Owner -}}
-Copyright {{ .Year }} {{ .Owner }}.
-{{- else -}}
-Copyright {{ .Year }}.
-{{- end }}
-{{ index .Licenses .License }}*/`
-
-var knownLicenses = map[string]string{
- "apache2": apache2,
- "none": "",
-}
-
-const apache2 = `
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-`
diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/main.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/main.go
deleted file mode 100644
index df7abb4751f..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/internal/templates/main.go
+++ /dev/null
@@ -1,286 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package templates
-
-import (
- "fmt"
- "path/filepath"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-const defaultMainPath = "main.go"
-
-var _ machinery.Template = &Main{}
-
-// Main scaffolds a file that defines the controller manager entry point
-type Main struct {
- machinery.TemplateMixin
- machinery.BoilerplateMixin
- machinery.DomainMixin
- machinery.RepositoryMixin
- machinery.ComponentConfigMixin
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Main) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = filepath.Join(defaultMainPath)
- }
-
- f.TemplateBody = fmt.Sprintf(mainTemplate,
- machinery.NewMarkerFor(f.Path, importMarker),
- machinery.NewMarkerFor(f.Path, addSchemeMarker),
- machinery.NewMarkerFor(f.Path, setupMarker),
- )
-
- return nil
-}
-
-var _ machinery.Inserter = &MainUpdater{}
-
-// MainUpdater updates main.go to run Controllers
-type MainUpdater struct { //nolint:maligned
- machinery.RepositoryMixin
- machinery.MultiGroupMixin
- machinery.ResourceMixin
-
- // Flags to indicate which parts need to be included when updating the file
- WireResource, WireController, WireWebhook bool
-}
-
-// GetPath implements file.Builder
-func (*MainUpdater) GetPath() string {
- return defaultMainPath
-}
-
-// GetIfExistsAction implements file.Builder
-func (*MainUpdater) GetIfExistsAction() machinery.IfExistsAction {
- return machinery.OverwriteFile
-}
-
-const (
- importMarker = "imports"
- addSchemeMarker = "scheme"
- setupMarker = "builder"
-)
-
-// GetMarkers implements file.Inserter
-func (f *MainUpdater) GetMarkers() []machinery.Marker {
- return []machinery.Marker{
- machinery.NewMarkerFor(defaultMainPath, importMarker),
- machinery.NewMarkerFor(defaultMainPath, addSchemeMarker),
- machinery.NewMarkerFor(defaultMainPath, setupMarker),
- }
-}
-
-const (
- apiImportCodeFragment = `%s "%s"
-`
- controllerImportCodeFragment = `"%s/controllers"
-`
- multiGroupControllerImportCodeFragment = `%scontrollers "%s/controllers/%s"
-`
- addschemeCodeFragment = `utilruntime.Must(%s.AddToScheme(scheme))
-`
- reconcilerSetupCodeFragment = `if err = (&controllers.%sReconciler{
- Client: mgr.GetClient(),
- Log: ctrl.Log.WithName("controllers").WithName("%s"),
- Scheme: mgr.GetScheme(),
- }).SetupWithManager(mgr); err != nil {
- setupLog.Error(err, "unable to create controller", "controller", "%s")
- os.Exit(1)
- }
-`
- multiGroupReconcilerSetupCodeFragment = `if err = (&%scontrollers.%sReconciler{
- Client: mgr.GetClient(),
- Log: ctrl.Log.WithName("controllers").WithName("%s").WithName("%s"),
- Scheme: mgr.GetScheme(),
- }).SetupWithManager(mgr); err != nil {
- setupLog.Error(err, "unable to create controller", "controller", "%s")
- os.Exit(1)
- }
-`
- webhookSetupCodeFragment = `if err = (&%s.%s{}).SetupWebhookWithManager(mgr); err != nil {
- setupLog.Error(err, "unable to create webhook", "webhook", "%s")
- os.Exit(1)
- }
-`
-)
-
-// GetCodeFragments implements file.Inserter
-func (f *MainUpdater) GetCodeFragments() machinery.CodeFragmentsMap {
- fragments := make(machinery.CodeFragmentsMap, 3)
-
- // If resource is not being provided we are creating the file, not updating it
- if f.Resource == nil {
- return fragments
- }
-
- // Generate import code fragments
- imports := make([]string, 0)
- if f.WireResource {
- imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path))
- }
-
- if f.WireController {
- if !f.MultiGroup || f.Resource.Group == "" {
- imports = append(imports, fmt.Sprintf(controllerImportCodeFragment, f.Repo))
- } else {
- imports = append(imports, fmt.Sprintf(multiGroupControllerImportCodeFragment,
- f.Resource.PackageName(), f.Repo, f.Resource.Group))
- }
- }
-
- // Generate add scheme code fragments
- addScheme := make([]string, 0)
- if f.WireResource {
- addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias()))
- }
-
- // Generate setup code fragments
- setup := make([]string, 0)
- if f.WireController {
- if !f.MultiGroup || f.Resource.Group == "" {
- setup = append(setup, fmt.Sprintf(reconcilerSetupCodeFragment,
- f.Resource.Kind, f.Resource.Kind, f.Resource.Kind))
- } else {
- setup = append(setup, fmt.Sprintf(multiGroupReconcilerSetupCodeFragment,
- f.Resource.PackageName(), f.Resource.Kind, f.Resource.Group, f.Resource.Kind, f.Resource.Kind))
- }
- }
- if f.WireWebhook {
- setup = append(setup, fmt.Sprintf(webhookSetupCodeFragment,
- f.Resource.ImportAlias(), f.Resource.Kind, f.Resource.Kind))
- }
-
- // Only store code fragments in the map if the slices are non-empty
- if len(imports) != 0 {
- fragments[machinery.NewMarkerFor(defaultMainPath, importMarker)] = imports
- }
- if len(addScheme) != 0 {
- fragments[machinery.NewMarkerFor(defaultMainPath, addSchemeMarker)] = addScheme
- }
- if len(setup) != 0 {
- fragments[machinery.NewMarkerFor(defaultMainPath, setupMarker)] = setup
- }
-
- return fragments
-}
-
-var mainTemplate = `{{ .Boilerplate }}
-
-package main
-
-import (
- "flag"
- "os"
-
- // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
- // to ensure that exec-entrypoint and run can make use of them.
- _ "k8s.io/client-go/plugin/pkg/client/auth"
-
- "k8s.io/apimachinery/pkg/runtime"
- utilruntime "k8s.io/apimachinery/pkg/util/runtime"
- clientgoscheme "k8s.io/client-go/kubernetes/scheme"
- ctrl "sigs.k8s.io/controller-runtime"
- "sigs.k8s.io/controller-runtime/pkg/log/zap"
- "sigs.k8s.io/controller-runtime/pkg/healthz"
- %s
-)
-
-var (
- scheme = runtime.NewScheme()
- setupLog = ctrl.Log.WithName("setup")
-)
-
-func init() {
- utilruntime.Must(clientgoscheme.AddToScheme(scheme))
-
- %s
-}
-
-func main() {
-{{- if not .ComponentConfig }}
- var metricsAddr string
- var enableLeaderElection bool
- var probeAddr string
- flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
- flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
- flag.BoolVar(&enableLeaderElection, "leader-elect", false,
- "Enable leader election for controller manager. " +
- "Enabling this will ensure there is only one active controller manager.")
-{{- else }}
- var configFile string
- flag.StringVar(&configFile, "config", "",
- "The controller will load its initial configuration from this file. " +
- "Omit this flag to use the default configuration values. " +
- "Command-line flags override configuration from this file.")
-{{- end }}
- opts := zap.Options{
- Development: true,
- }
- opts.BindFlags(flag.CommandLine)
- flag.Parse()
-
- ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
-
-{{ if not .ComponentConfig }}
- mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
- Scheme: scheme,
- MetricsBindAddress: metricsAddr,
- Port: 9443,
- HealthProbeBindAddress: probeAddr,
- LeaderElection: enableLeaderElection,
- LeaderElectionID: "{{ hashFNV .Repo }}.{{ .Domain }}",
- })
-{{- else }}
- var err error
- options := ctrl.Options{Scheme: scheme}
- if configFile != "" {
- options, err = options.AndFrom(ctrl.ConfigFile().AtPath(configFile))
- if err != nil {
- setupLog.Error(err, "unable to load the config file")
- os.Exit(1)
- }
- }
-
- mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options)
-{{- end }}
- if err != nil {
- setupLog.Error(err, "unable to start manager")
- os.Exit(1)
- }
-
- %s
-
- if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
- setupLog.Error(err, "unable to set up health check")
- os.Exit(1)
- }
- if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
- setupLog.Error(err, "unable to set up ready check")
- os.Exit(1)
- }
-
- setupLog.Info("starting manager")
- if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
- setupLog.Error(err, "problem running manager")
- os.Exit(1)
- }
-}
-`
diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/makefile.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/makefile.go
deleted file mode 100644
index 787c1ef34f2..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/internal/templates/makefile.go
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package templates
-
-import (
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
-)
-
-var _ machinery.Template = &Makefile{}
-
-// Makefile scaffolds a file that defines project management CLI commands
-type Makefile struct {
- machinery.TemplateMixin
- machinery.ComponentConfigMixin
-
- // Image is controller manager image name
- Image string
- // BoilerplatePath is the path to the boilerplate file
- BoilerplatePath string
- // Controller tools version to use in the project
- ControllerToolsVersion string
- // Kustomize version to use in the project
- KustomizeVersion string
- // ControllerRuntimeVersion version to be used to download the envtest setup script
- ControllerRuntimeVersion string
-}
-
-// SetTemplateDefaults implements file.Template
-func (f *Makefile) SetTemplateDefaults() error {
- if f.Path == "" {
- f.Path = "Makefile"
- }
-
- f.TemplateBody = makefileTemplate
-
- f.IfExistsAction = machinery.Error
-
- if f.Image == "" {
- f.Image = "controller:latest"
- }
-
- return nil
-}
-
-//nolint:lll
-const makefileTemplate = `
-# Image URL to use all building/pushing image targets
-IMG ?= {{ .Image }}
-# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
-CRD_OPTIONS ?= "crd:trivialVersions=true,preserveUnknownFields=false"
-
-# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
-ifeq (,$(shell go env GOBIN))
-GOBIN=$(shell go env GOPATH)/bin
-else
-GOBIN=$(shell go env GOBIN)
-endif
-
-all: build
-
-##@ General
-
-# The help target prints out all targets with their descriptions organized
-# beneath their categories. The categories are represented by '##@' and the
-# target descriptions by '##'. The awk commands is responsible for reading the
-# entire set of makefiles included in this invocation, looking for lines of the
-# file as xyz: ## something, and then pretty-format the target and help. Then,
-# if there's a line with ##@ something, that gets pretty-printed as a category.
-# More info on the usage of ANSI control characters for terminal formatting:
-# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
-# More info on the awk command:
-# http://linuxcommand.org/lc3_adv_awk.php
-
-help: ## Display this help.
- @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
-
-##@ Development
-
-manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
- $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
-
-generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
- $(CONTROLLER_GEN) object:headerFile={{printf "%q" .BoilerplatePath}} paths="./..."
-
-fmt: ## Run go fmt against code.
- go fmt ./...
-
-vet: ## Run go vet against code.
- go vet ./...
-
-ENVTEST_ASSETS_DIR=$(shell pwd)/testbin
-test: manifests generate fmt vet ## Run tests.
- mkdir -p ${ENVTEST_ASSETS_DIR}
- test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/{{ .ControllerRuntimeVersion }}/hack/setup-envtest.sh
- source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test ./... -coverprofile cover.out
-
-##@ Build
-
-build: generate fmt vet ## Build manager binary.
- go build -o bin/manager main.go
-
-run: manifests generate fmt vet ## Run a controller from your host.
- go run ./main.go
-
-docker-build: test ## Build docker image with the manager.
- docker build -t ${IMG} .
-
-docker-push: ## Push docker image with the manager.
- docker push ${IMG}
-
-##@ Deployment
-
-install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
- $(KUSTOMIZE) build config/crd | kubectl apply -f -
-
-uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config.
- $(KUSTOMIZE) build config/crd | kubectl delete -f -
-
-deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
- cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
- $(KUSTOMIZE) build config/default | kubectl apply -f -
-
-undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config.
- $(KUSTOMIZE) build config/default | kubectl delete -f -
-
-
-CONTROLLER_GEN = $(shell pwd)/bin/controller-gen
-controller-gen: ## Download controller-gen locally if necessary.
- $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@{{ .ControllerToolsVersion }})
-
-KUSTOMIZE = $(shell pwd)/bin/kustomize
-kustomize: ## Download kustomize locally if necessary.
- $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@{{ .KustomizeVersion }})
-
-# go-get-tool will 'go get' any package $2 and install it to $1.
-PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
-define go-get-tool
-@[ -f $(1) ] || { \
-set -e ;\
-TMP_DIR=$$(mktemp -d) ;\
-cd $$TMP_DIR ;\
-go mod init tmp ;\
-echo "Downloading $(2)" ;\
-GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\
-rm -rf $$TMP_DIR ;\
-}
-endef
-`
diff --git a/pkg/plugins/golang/v3/scaffolds/webhook.go b/pkg/plugins/golang/v3/scaffolds/webhook.go
deleted file mode 100644
index 5ca51c39844..00000000000
--- a/pkg/plugins/golang/v3/scaffolds/webhook.go
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package scaffolds
-
-import (
- "fmt"
-
- "github.com/spf13/afero"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/api"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/hack"
-)
-
-var _ plugins.Scaffolder = &webhookScaffolder{}
-
-type webhookScaffolder struct {
- config config.Config
- resource resource.Resource
-
- // fs is the filesystem that will be used by the scaffolder
- fs machinery.Filesystem
-
- // force indicates whether to scaffold controller files even if it exists or not
- force bool
-}
-
-// NewWebhookScaffolder returns a new Scaffolder for v2 webhook creation operations
-func NewWebhookScaffolder(config config.Config, resource resource.Resource, force bool) plugins.Scaffolder {
- return &webhookScaffolder{
- config: config,
- resource: resource,
- force: force,
- }
-}
-
-// InjectFS implements cmdutil.Scaffolder
-func (s *webhookScaffolder) InjectFS(fs machinery.Filesystem) {
- s.fs = fs
-}
-
-// Scaffold implements cmdutil.Scaffolder
-func (s *webhookScaffolder) Scaffold() error {
- fmt.Println("Writing scaffold for you to edit...")
-
- // Load the boilerplate
- boilerplate, err := afero.ReadFile(s.fs.FS, hack.DefaultBoilerplatePath)
- if err != nil {
- return fmt.Errorf("error scaffolding webhook: unable to load boilerplate: %w", err)
- }
-
- // Initialize the machinery.Scaffold that will write the files to disk
- scaffold := machinery.NewScaffold(s.fs,
- machinery.WithConfig(s.config),
- machinery.WithBoilerplate(string(boilerplate)),
- machinery.WithResource(&s.resource),
- )
-
- // Keep track of these values before the update
- doDefaulting := s.resource.HasDefaultingWebhook()
- doValidation := s.resource.HasValidationWebhook()
- doConversion := s.resource.HasConversionWebhook()
-
- if err := s.config.UpdateResource(s.resource); err != nil {
- return fmt.Errorf("error updating resource: %w", err)
- }
-
- if err := scaffold.Execute(
- &api.Webhook{Force: s.force},
- &templates.MainUpdater{WireWebhook: true},
- ); err != nil {
- return err
- }
-
- if doConversion {
- fmt.Println(`Webhook server has been set up for you.
-You need to implement the conversion.Hub and conversion.Convertible interfaces for your CRD types.`)
- }
-
- // TODO: Add test suite for conversion webhook after #1664 has been merged & conversion tests supported in envtest.
- if doDefaulting || doValidation {
- if err := scaffold.Execute(
- &api.WebhookSuite{},
- ); err != nil {
- return err
- }
- }
-
- return nil
-}
diff --git a/pkg/plugins/golang/v3/webhook.go b/pkg/plugins/golang/v3/webhook.go
deleted file mode 100644
index 5151b11d65b..00000000000
--- a/pkg/plugins/golang/v3/webhook.go
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
-Copyright 2020 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v3
-
-import (
- "fmt"
-
- "github.com/spf13/pflag"
-
- "sigs.k8s.io/kubebuilder/v3/pkg/config"
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
- "sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugin"
- pluginutil "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util"
- goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang"
- "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds"
-)
-
-// defaultWebhookVersion is the default mutating/validating webhook config API version to scaffold.
-const defaultWebhookVersion = "v1"
-
-var _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{}
-
-type createWebhookSubcommand struct {
- config config.Config
- // For help text.
- commandName string
-
- options *goPlugin.Options
-
- resource *resource.Resource
-
- // force indicates that the resource should be created even if it already exists
- force bool
-}
-
-func (p *createWebhookSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
- p.commandName = cliMeta.CommandName
-
- subcmdMeta.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting,
-validating and/or conversion webhooks.
-`
- subcmdMeta.Examples = fmt.Sprintf(` # Create defaulting and validating webhooks for Group: ship, Version: v1beta1
- # and Kind: Frigate
- %[1]s create webhook --group ship --version v1beta1 --kind Frigate --defaulting --programmatic-validation
-
- # Create conversion webhook for Group: ship, Version: v1beta1
- # and Kind: Frigate
- %[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion
-`, cliMeta.CommandName)
-}
-
-func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) {
- p.options = &goPlugin.Options{}
-
- fs.StringVar(&p.options.Plural, "plural", "", "resource irregular plural form")
-
- fs.StringVar(&p.options.WebhookVersion, "webhook-version", defaultWebhookVersion,
- "version of {Mutating,Validating}WebhookConfigurations to scaffold. Options: [v1, v1beta1]")
- fs.BoolVar(&p.options.DoDefaulting, "defaulting", false,
- "if set, scaffold the defaulting webhook")
- fs.BoolVar(&p.options.DoValidation, "programmatic-validation", false,
- "if set, scaffold the validating webhook")
- fs.BoolVar(&p.options.DoConversion, "conversion", false,
- "if set, scaffold the conversion webhook")
-
- fs.BoolVar(&p.force, "force", false,
- "attempt to create resource even if it already exists")
-}
-
-func (p *createWebhookSubcommand) InjectConfig(c config.Config) error {
- p.config = c
-
- return nil
-}
-
-func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error {
- p.resource = res
-
- p.options.UpdateResource(p.resource, p.config)
-
- if err := p.resource.Validate(); err != nil {
- return err
- }
-
- if !p.resource.HasDefaultingWebhook() && !p.resource.HasValidationWebhook() && !p.resource.HasConversionWebhook() {
- return fmt.Errorf("%s create webhook requires at least one of --defaulting,"+
- " --programmatic-validation and --conversion to be true", p.commandName)
- }
-
- // check if resource exist to create webhook
- if r, err := p.config.GetResource(p.resource.GVK); err != nil {
- return fmt.Errorf("%s create webhook requires a previously created API ", p.commandName)
- } else if r.Webhooks != nil && !r.Webhooks.IsEmpty() && !p.force {
- return fmt.Errorf("webhook resource already exists")
- }
-
- if pluginutil.HasDifferentWebhookVersion(p.config, p.resource.Webhooks.WebhookVersion) {
- return fmt.Errorf("only one webhook version can be used for all resources, cannot add %q",
- p.resource.Webhooks.WebhookVersion)
- }
-
- return nil
-}
-
-func (p *createWebhookSubcommand) Scaffold(fs machinery.Filesystem) error {
- scaffolder := scaffolds.NewWebhookScaffolder(p.config, *p.resource, p.force)
- scaffolder.InjectFS(fs)
- return scaffolder.Scaffold()
-}
diff --git a/pkg/plugins/golang/v4/api.go b/pkg/plugins/golang/v4/api.go
new file mode 100644
index 00000000000..3747dc9de7d
--- /dev/null
+++ b/pkg/plugins/golang/v4/api.go
@@ -0,0 +1,204 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v4
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ log "log/slog"
+ "os"
+
+ "github.com/spf13/pflag"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
+ goPlugin "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds"
+)
+
+// DefaultMainPath is default file path of main.go
+const DefaultMainPath = "cmd/main.go"
+
+var _ plugin.CreateAPISubcommand = &createAPISubcommand{}
+
+type createAPISubcommand struct {
+ config config.Config
+
+ options *goPlugin.Options
+
+ resource *resource.Resource
+
+ // Check if we have to scaffold resource and/or controller
+ resourceFlag *pflag.Flag
+ controllerFlag *pflag.Flag
+
+ // force indicates that the resource should be created even if it already exists
+ force bool
+
+ // runMake indicates whether to run make or not after scaffolding APIs
+ runMake bool
+}
+
+func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
+ subcmdMeta.Description = `Scaffold a Kubernetes API by writing a Resource definition and/or a Controller.
+
+If information about whether the resource and controller should be scaffolded
+was not explicitly provided, it will prompt the user if they should be.
+
+After the scaffold is written, the dependencies will be updated and
+make generate will be run.
+`
+ subcmdMeta.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate
+ %[1]s create api --group ship --version v1beta1 --kind Frigate
+
+ # Edit the API Scheme
+
+ nano api/v1beta1/frigate_types.go
+
+ # Edit the Controller
+ nano internal/controller/frigate/frigate_controller.go
+
+ # Edit the Controller Test
+ nano internal/controller/frigate/frigate_controller_test.go
+
+ # Generate the manifests
+ make manifests
+
+ # Install CRDs into the Kubernetes cluster using kubectl apply
+ make install
+
+ # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config
+ make run
+`, cliMeta.CommandName)
+}
+
+func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) {
+ fs.BoolVar(&p.runMake, "make", true, "if true, run `make generate` after generating files")
+
+ fs.BoolVar(&p.force, "force", false,
+ "attempt to create resource even if it already exists")
+
+ p.options = &goPlugin.Options{}
+
+ fs.StringVar(&p.options.Plural, "plural", "", "resource irregular plural form")
+
+ fs.BoolVar(&p.options.DoAPI, "resource", true,
+ "if set, generate the resource without prompting the user")
+ p.resourceFlag = fs.Lookup("resource")
+ fs.BoolVar(&p.options.Namespaced, "namespaced", true, "resource is namespaced")
+
+ fs.BoolVar(&p.options.DoController, "controller", true,
+ "if set, generate the controller without prompting the user")
+ p.controllerFlag = fs.Lookup("controller")
+
+ fs.StringVar(&p.options.ExternalAPIPath, "external-api-path", "",
+ "Specify the Go package import path for the external API. This is used to scaffold controllers for resources "+
+ "defined outside this project (e.g., github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1).")
+
+ fs.StringVar(&p.options.ExternalAPIDomain, "external-api-domain", "",
+ "Specify the domain name for the external API. This domain is used to generate accurate RBAC "+
+ "markers and permissions for the external resources (e.g., cert-manager.io).")
+}
+
+func (p *createAPISubcommand) InjectConfig(c config.Config) error {
+ p.config = c
+ return nil
+}
+
+func (p *createAPISubcommand) InjectResource(res *resource.Resource) error {
+ p.resource = res
+
+ reader := bufio.NewReader(os.Stdin)
+ if !p.resourceFlag.Changed {
+ log.Info("Create Resource [y/n]")
+ p.options.DoAPI = util.YesNo(reader)
+ }
+ if !p.controllerFlag.Changed {
+ log.Info("Create Controller [y/n]")
+ p.options.DoController = util.YesNo(reader)
+ }
+
+ // Ensure that external API options cannot be used when creating an API in the project.
+ if p.options.DoAPI {
+ if len(p.options.ExternalAPIPath) != 0 || len(p.options.ExternalAPIDomain) != 0 {
+ return errors.New("cannot use '--external-api-path' or '--external-api-domain' " +
+ "when creating an API in the project with '--resource=true'. " +
+ "Use '--resource=false' when referencing an external API")
+ }
+ }
+
+ p.options.UpdateResource(p.resource, p.config)
+
+ if err := p.resource.Validate(); err != nil {
+ return fmt.Errorf("error validating resource: %w", err)
+ }
+
+ // In case we want to scaffold a resource API we need to do some checks
+ if p.options.DoAPI {
+ // Check that resource doesn't have the API scaffolded or flag force was set
+ if r, err := p.config.GetResource(p.resource.GVK); err == nil && r.HasAPI() && !p.force {
+ return errors.New("API resource already exists")
+ }
+
+ // Check that the provided group can be added to the project
+ if !p.config.IsMultiGroup() && p.config.ResourcesLength() != 0 && !p.config.HasGroup(p.resource.Group) {
+ return fmt.Errorf("multiple groups are not allowed by default, " +
+ "to enable multi-group visit https://kubebuilder.io/migration/multi-group.html")
+ }
+ }
+
+ return nil
+}
+
+func (p *createAPISubcommand) PreScaffold(machinery.Filesystem) error {
+ // check if main.go is present in the root directory
+ if _, err := os.Stat(DefaultMainPath); os.IsNotExist(err) {
+ return fmt.Errorf("%s file should present in the root directory", DefaultMainPath)
+ }
+
+ return nil
+}
+
+func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error {
+ scaffolder := scaffolds.NewAPIScaffolder(p.config, *p.resource, p.force)
+ scaffolder.InjectFS(fs)
+ if err := scaffolder.Scaffold(); err != nil {
+ return fmt.Errorf("error scaffolding API: %w", err)
+ }
+
+ return nil
+}
+
+func (p *createAPISubcommand) PostScaffold() error {
+ err := util.RunCmd("Update dependencies", "go", "mod", "tidy")
+ if err != nil {
+ return fmt.Errorf("error updating go dependencies: %w", err)
+ }
+ if p.runMake && p.resource.HasAPI() {
+ err = util.RunCmd("Running make", "make", "generate")
+ if err != nil {
+ return fmt.Errorf("error running make generate: %w", err)
+ }
+ fmt.Print("Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:\n$ make manifests\n")
+ }
+
+ return nil
+}
diff --git a/pkg/plugins/golang/v4/edit.go b/pkg/plugins/golang/v4/edit.go
new file mode 100644
index 00000000000..d60567bf150
--- /dev/null
+++ b/pkg/plugins/golang/v4/edit.go
@@ -0,0 +1,69 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v4
+
+import (
+ "fmt"
+
+ "github.com/spf13/pflag"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds"
+)
+
+var _ plugin.EditSubcommand = &editSubcommand{}
+
+type editSubcommand struct {
+ config config.Config
+
+ multigroup bool
+}
+
+func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
+ subcmdMeta.Description = `This command will edit the project configuration.
+Features supported:
+ - Toggle between single or multi group projects.
+`
+ subcmdMeta.Examples = fmt.Sprintf(` # Enable the multigroup layout
+ %[1]s edit --multigroup
+
+ # Disable the multigroup layout
+ %[1]s edit --multigroup=false
+`, cliMeta.CommandName)
+}
+
+func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
+ fs.BoolVar(&p.multigroup, "multigroup", false, "enable or disable multigroup layout")
+}
+
+func (p *editSubcommand) InjectConfig(c config.Config) error {
+ p.config = c
+
+ return nil
+}
+
+func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
+ scaffolder := scaffolds.NewEditScaffolder(p.config, p.multigroup)
+ scaffolder.InjectFS(fs)
+ if err := scaffolder.Scaffold(); err != nil {
+ return fmt.Errorf("failed to edit scaffold: %w", err)
+ }
+
+ return nil
+}
diff --git a/pkg/plugins/golang/v4/init.go b/pkg/plugins/golang/v4/init.go
new file mode 100644
index 00000000000..278f5f3bdf3
--- /dev/null
+++ b/pkg/plugins/golang/v4/init.go
@@ -0,0 +1,214 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v4
+
+import (
+ "fmt"
+ log "log/slog"
+ "os"
+ "path/filepath"
+ "strings"
+ "unicode"
+
+ "github.com/spf13/pflag"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds"
+)
+
+// Variables and function to check Go version requirements.
+var (
+ goVerMin = golang.MustParse("go1.23.0")
+ goVerMax = golang.MustParse("go2.0alpha1")
+)
+
+var _ plugin.InitSubcommand = &initSubcommand{}
+
+type initSubcommand struct {
+ config config.Config
+ // For help text.
+ commandName string
+
+ // boilerplate options
+ license string
+ owner string
+
+ // go config options
+ repo string
+
+ // flags
+ fetchDeps bool
+ skipGoVersionCheck bool
+}
+
+func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
+ p.commandName = cliMeta.CommandName
+
+ subcmdMeta.Description = `Initialize a new project including the following files:
+ - a "go.mod" with project dependencies
+ - a "PROJECT" file that stores project configuration
+ - a "Makefile" with several useful make targets for the project
+ - several YAML files for project deployment under the "config" directory
+ - a "cmd/main.go" file that creates the manager that will run the project controllers
+`
+ subcmdMeta.Examples = fmt.Sprintf(` # Initialize a new project with your domain and name in copyright
+ %[1]s init --plugins go/v4 --domain example.org --owner "Your name"
+
+ # Initialize a new project defining a specific project version
+ %[1]s init --plugins go/v4 --project-version 3
+`, cliMeta.CommandName)
+}
+
+func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) {
+ fs.BoolVar(&p.skipGoVersionCheck, "skip-go-version-check",
+ false, "if specified, skip checking the Go version")
+
+ // dependency args
+ fs.BoolVar(&p.fetchDeps, "fetch-deps", true, "ensure dependencies are downloaded")
+
+ // boilerplate args
+ fs.StringVar(&p.license, "license", "apache2",
+ "license to use to boilerplate, may be one of 'apache2', 'none'")
+ fs.StringVar(&p.owner, "owner", "", "owner to add to the copyright")
+
+ // project args
+ fs.StringVar(&p.repo, "repo", "", "name to use for go module (e.g., github.com/user/repo), "+
+ "defaults to the go package of the current working directory.")
+}
+
+func (p *initSubcommand) InjectConfig(c config.Config) error {
+ p.config = c
+
+ // Try to guess repository if flag is not set.
+ if p.repo == "" {
+ repoPath, err := golang.FindCurrentRepo()
+ if err != nil {
+ return fmt.Errorf("error finding current repository: %w", err)
+ }
+ p.repo = repoPath
+ }
+
+ if err := p.config.SetRepository(p.repo); err != nil {
+ return fmt.Errorf("error setting repository: %w", err)
+ }
+
+ return nil
+}
+
+func (p *initSubcommand) PreScaffold(machinery.Filesystem) error {
+ // Ensure Go version is in the allowed range if check not turned off.
+ if !p.skipGoVersionCheck {
+ if err := golang.ValidateGoVersion(goVerMin, goVerMax); err != nil {
+ return fmt.Errorf("error validating go version: %w", err)
+ }
+ }
+
+ // Check if the current directory has no files or directories which does not allow to init the project
+ return checkDir()
+}
+
+func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error {
+ scaffolder := scaffolds.NewInitScaffolder(p.config, p.license, p.owner, p.commandName)
+ scaffolder.InjectFS(fs)
+ if err := scaffolder.Scaffold(); err != nil {
+ return fmt.Errorf("error scaffolding init plugin: %w", err)
+ }
+
+ if !p.fetchDeps {
+ log.Info("Skipping fetching dependencies.")
+ return nil
+ }
+
+ // Ensure that we are pinning controller-runtime version
+ // xref: https://github.com/kubernetes-sigs/kubebuilder/issues/997
+ err := util.RunCmd("Get controller runtime", "go", "get",
+ "sigs.k8s.io/controller-runtime@"+scaffolds.ControllerRuntimeVersion)
+ if err != nil {
+ return fmt.Errorf("error getting controller-runtime version: %w", err)
+ }
+
+ return nil
+}
+
+func (p *initSubcommand) PostScaffold() error {
+ err := util.RunCmd("Update dependencies", "go", "mod", "tidy")
+ if err != nil {
+ return fmt.Errorf("error updating go dependencies: %w", err)
+ }
+
+ fmt.Printf("Next: define a resource with:\n$ %s create api\n", p.commandName)
+ return nil
+}
+
+// checkDir will return error if the current directory has files which are not allowed.
+// Note that, it is expected that the directory to scaffold the project is cleaned.
+// Otherwise, it might face issues to do the scaffold.
+func checkDir() error {
+ err := filepath.Walk(".",
+ func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return fmt.Errorf("error walking path %q: %w", path, err)
+ }
+ // Allow directory trees starting with '.'
+ if info.IsDir() && strings.HasPrefix(info.Name(), ".") && info.Name() != "." {
+ return filepath.SkipDir
+ }
+ // Allow files starting with '.'
+ if strings.HasPrefix(info.Name(), ".") {
+ return nil
+ }
+ // Allow files ending with '.md' extension
+ if strings.HasSuffix(info.Name(), ".md") && !info.IsDir() {
+ return nil
+ }
+ // Allow capitalized files except PROJECT
+ isCapitalized := true
+ for _, l := range info.Name() {
+ if !unicode.IsUpper(l) {
+ isCapitalized = false
+ break
+ }
+ }
+ if isCapitalized && info.Name() != "PROJECT" {
+ return nil
+ }
+ disallowedExtensions := []string{
+ ".go",
+ ".yaml",
+ ".mod",
+ ".sum",
+ }
+ // Deny files with .go or .yaml or .mod or .sum extensions
+ for _, ext := range disallowedExtensions {
+ if strings.HasSuffix(info.Name(), ext) {
+ return nil
+ }
+ }
+ // Do not allow any other file
+ return fmt.Errorf("target directory is not empty and contains a disallowed file %q. "+
+ "files with the following extensions [%s] are not allowed to avoid conflicts with the tooling",
+ path, strings.Join(disallowedExtensions, ", "))
+ })
+ if err != nil {
+ return fmt.Errorf("error walking directory: %w", err)
+ }
+ return nil
+}
diff --git a/pkg/plugins/golang/v4/plugin.go b/pkg/plugins/golang/v4/plugin.go
new file mode 100644
index 00000000000..19baeef40f0
--- /dev/null
+++ b/pkg/plugins/golang/v4/plugin.go
@@ -0,0 +1,70 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v4
+
+import (
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
+ "sigs.k8s.io/kubebuilder/v4/pkg/model/stage"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang"
+)
+
+const pluginName = "base." + golang.DefaultNameQualifier
+
+var (
+ pluginVersion = plugin.Version{Number: 4, Stage: stage.Stable}
+ supportedProjectVersions = []config.Version{cfgv3.Version}
+)
+
+var _ plugin.Full = Plugin{}
+
+// Plugin implements the plugin.Full interface
+type Plugin struct {
+ initSubcommand
+ createAPISubcommand
+ createWebhookSubcommand
+ editSubcommand
+}
+
+// Name returns the name of the plugin
+func (Plugin) Name() string { return pluginName }
+
+// Version returns the version of the plugin
+func (Plugin) Version() plugin.Version { return pluginVersion }
+
+// SupportedProjectVersions returns an array with all project versions supported by the plugin
+func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions }
+
+// GetInitSubcommand will return the subcommand which is responsible for initializing and common scaffolding
+func (p Plugin) GetInitSubcommand() plugin.InitSubcommand { return &p.initSubcommand }
+
+// GetCreateAPISubcommand will return the subcommand which is responsible for scaffolding apis
+func (p Plugin) GetCreateAPISubcommand() plugin.CreateAPISubcommand { return &p.createAPISubcommand }
+
+// GetCreateWebhookSubcommand will return the subcommand which is responsible for scaffolding webhooks
+func (p Plugin) GetCreateWebhookSubcommand() plugin.CreateWebhookSubcommand {
+ return &p.createWebhookSubcommand
+}
+
+// GetEditSubcommand will return the subcommand which is responsible for editing the scaffold of the project
+func (p Plugin) GetEditSubcommand() plugin.EditSubcommand { return &p.editSubcommand }
+
+// DeprecationWarning define the deprecation message or return empty when plugin is not deprecated
+func (p Plugin) DeprecationWarning() string {
+ return ""
+}
diff --git a/pkg/plugins/golang/v4/scaffolds/api.go b/pkg/plugins/golang/v4/scaffolds/api.go
new file mode 100644
index 00000000000..aa8c708417a
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/api.go
@@ -0,0 +1,125 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package scaffolds
+
+import (
+ "errors"
+ "fmt"
+ log "log/slog"
+
+ "github.com/spf13/afero"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/api"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/cmd"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/controllers"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/hack"
+)
+
+var _ plugins.Scaffolder = &apiScaffolder{}
+
+// apiScaffolder contains configuration for generating scaffolding for Go type
+// representing the API and controller that implements the behavior for the API.
+type apiScaffolder struct {
+ config config.Config
+ resource resource.Resource
+
+ // fs is the filesystem that will be used by the scaffolder
+ fs machinery.Filesystem
+
+ // force indicates whether to scaffold controller files even if it exists or not
+ force bool
+}
+
+// NewAPIScaffolder returns a new Scaffolder for API/controller creation operations
+func NewAPIScaffolder(cfg config.Config, res resource.Resource, force bool) plugins.Scaffolder {
+ return &apiScaffolder{
+ config: cfg,
+ resource: res,
+ force: force,
+ }
+}
+
+// InjectFS implements cmdutil.Scaffolder
+func (s *apiScaffolder) InjectFS(fs machinery.Filesystem) {
+ s.fs = fs
+}
+
+// Scaffold implements cmdutil.Scaffolder
+func (s *apiScaffolder) Scaffold() error {
+ log.Info("Writing scaffold for you to edit...")
+
+ // Load the boilerplate
+ boilerplate, err := afero.ReadFile(s.fs.FS, hack.DefaultBoilerplatePath)
+ if err != nil {
+ if errors.Is(err, afero.ErrFileNotFound) {
+ log.Warn("Unable to find boilerplate file."+
+ "This file is used to generate the license header in the project.\n"+
+ "Note that controller-gen will also use this. Therefore, ensure that you "+
+ "add the license file or configure your project accordingly.",
+ "file_path", hack.DefaultBoilerplatePath, "error", err)
+ boilerplate = []byte("")
+ } else {
+ return fmt.Errorf("error scaffolding API/controller: unable to load boilerplate: %w", err)
+ }
+ }
+
+ // Initialize the machinery.Scaffold that will write the files to disk
+ scaffold := machinery.NewScaffold(s.fs,
+ machinery.WithConfig(s.config),
+ machinery.WithBoilerplate(string(boilerplate)),
+ machinery.WithResource(&s.resource),
+ )
+
+ // Keep track of these values before the update
+ doAPI := s.resource.HasAPI()
+ doController := s.resource.HasController()
+
+ if err := s.config.UpdateResource(s.resource); err != nil {
+ return fmt.Errorf("error updating resource: %w", err)
+ }
+
+ if doAPI {
+ if err := scaffold.Execute(
+ &api.Types{Force: s.force},
+ &api.Group{},
+ ); err != nil {
+ return fmt.Errorf("error scaffolding APIs: %w", err)
+ }
+ }
+
+ if doController {
+ if err := scaffold.Execute(
+ &controllers.SuiteTest{Force: s.force},
+ &controllers.Controller{ControllerRuntimeVersion: ControllerRuntimeVersion, Force: s.force},
+ &controllers.ControllerTest{Force: s.force, DoAPI: doAPI},
+ ); err != nil {
+ return fmt.Errorf("error scaffolding controller: %w", err)
+ }
+ }
+
+ if err := scaffold.Execute(
+ &cmd.MainUpdater{WireResource: doAPI, WireController: doController},
+ ); err != nil {
+ return fmt.Errorf("error updating cmd/main.go: %w", err)
+ }
+
+ return nil
+}
diff --git a/pkg/plugins/golang/v4/scaffolds/doc.go b/pkg/plugins/golang/v4/scaffolds/doc.go
new file mode 100644
index 00000000000..81757e9692b
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/doc.go
@@ -0,0 +1,18 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package scaffolds contains libraries for scaffolding code to use with controller-runtime
+package scaffolds
diff --git a/pkg/plugins/golang/v4/scaffolds/edit.go b/pkg/plugins/golang/v4/scaffolds/edit.go
new file mode 100644
index 00000000000..fcc4b791194
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/edit.go
@@ -0,0 +1,77 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package scaffolds
+
+import (
+ "fmt"
+
+ "github.com/spf13/afero"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins"
+)
+
+var _ plugins.Scaffolder = &editScaffolder{}
+
+type editScaffolder struct {
+ config config.Config
+ multigroup bool
+
+ // fs is the filesystem that will be used by the scaffolder
+ fs machinery.Filesystem
+}
+
+// NewEditScaffolder returns a new Scaffolder for configuration edit operations
+func NewEditScaffolder(cfg config.Config, multigroup bool) plugins.Scaffolder {
+ return &editScaffolder{
+ config: cfg,
+ multigroup: multigroup,
+ }
+}
+
+// InjectFS implements cmdutil.Scaffolder
+func (s *editScaffolder) InjectFS(fs machinery.Filesystem) {
+ s.fs = fs
+}
+
+// Scaffold implements cmdutil.Scaffolder
+func (s *editScaffolder) Scaffold() error {
+ filename := "Dockerfile"
+ bs, err := afero.ReadFile(s.fs.FS, filename)
+ if err != nil {
+ return fmt.Errorf("error reading %q: %w", filename, err)
+ }
+ str := string(bs)
+
+ if s.multigroup {
+ _ = s.config.SetMultiGroup()
+ } else {
+ _ = s.config.ClearMultiGroup()
+ }
+
+ // Check if the str is not empty, because when the file is already in desired format it will return empty string
+ // because there is nothing to replace.
+ if str != "" {
+ // TODO: instead of writing it directly, we should use the scaffolding machinery for consistency
+ if err = afero.WriteFile(s.fs.FS, filename, []byte(str), 0o644); err != nil {
+ return fmt.Errorf("error writing %q: %w", filename, err)
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/plugins/golang/v4/scaffolds/init.go b/pkg/plugins/golang/v4/scaffolds/init.go
new file mode 100644
index 00000000000..e8bcd053eae
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/init.go
@@ -0,0 +1,196 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package scaffolds
+
+import (
+ "errors"
+ "fmt"
+ log "log/slog"
+ "strings"
+
+ "github.com/spf13/afero"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins"
+ kustomizecommonv2 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/cmd"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/github"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/hack"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/test/e2e"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/test/utils"
+)
+
+const (
+ // GolangciLintVersion is the golangci-lint version to be used in the project
+ GolangciLintVersion = "v2.5.0"
+ // ControllerRuntimeVersion is the kubernetes-sigs/controller-runtime version to be used in the project
+ ControllerRuntimeVersion = "v0.22.3"
+ // ControllerToolsVersion is the kubernetes-sigs/controller-tools version to be used in the project
+ ControllerToolsVersion = "v0.19.0"
+
+ imageName = "controller:latest"
+)
+
+var _ plugins.Scaffolder = &initScaffolder{}
+
+var kustomizeVersion string
+
+type initScaffolder struct {
+ config config.Config
+ boilerplatePath string
+ license string
+ owner string
+ commandName string
+
+ // fs is the filesystem that will be used by the scaffolder
+ fs machinery.Filesystem
+}
+
+// NewInitScaffolder returns a new Scaffolder for project initialization operations
+func NewInitScaffolder(cfg config.Config, license, owner, commandName string) plugins.Scaffolder {
+ return &initScaffolder{
+ config: cfg,
+ boilerplatePath: hack.DefaultBoilerplatePath,
+ license: license,
+ owner: owner,
+ commandName: commandName,
+ }
+}
+
+// InjectFS implements cmdutil.Scaffolder
+func (s *initScaffolder) InjectFS(fs machinery.Filesystem) {
+ s.fs = fs
+}
+
+// getControllerRuntimeReleaseBranch converts the ControllerRuntime semantic versioning string to a
+// release branch string. Example input: "v0.17.0" -> Output: "release-0.17"
+func getControllerRuntimeReleaseBranch() string {
+ v := strings.TrimPrefix(ControllerRuntimeVersion, "v")
+ tmp := strings.Split(v, ".")
+
+ if len(tmp) < 2 {
+ fmt.Println("Invalid version format. Expected at least major and minor version numbers.")
+ return ""
+ }
+ releaseBranch := fmt.Sprintf("release-%s.%s", tmp[0], tmp[1])
+ return releaseBranch
+}
+
+// Scaffold implements cmdutil.Scaffolder
+func (s *initScaffolder) Scaffold() error {
+ log.Info("Writing scaffold for you to edit...")
+
+ // Initialize the machinery.Scaffold that will write the boilerplate file to disk
+ // The boilerplate file needs to be scaffolded as a separate step as it is going to
+ // be used by the rest of the files, even those scaffolded in this command call.
+ scaffold := machinery.NewScaffold(s.fs,
+ machinery.WithConfig(s.config),
+ )
+
+ if s.license != "none" {
+ bpFile := &hack.Boilerplate{
+ License: s.license,
+ Owner: s.owner,
+ }
+ bpFile.Path = s.boilerplatePath
+ if err := scaffold.Execute(bpFile); err != nil {
+ return fmt.Errorf("failed to execute boilerplate: %w", err)
+ }
+
+ boilerplate, err := afero.ReadFile(s.fs.FS, s.boilerplatePath)
+ if err != nil {
+ if errors.Is(err, afero.ErrFileNotFound) {
+ log.Warn("Unable to find boilerplate file. "+
+ "This file is used to generate the license header in the project. "+
+ "Note that controller-gen will also use this. Therefore, ensure that you "+
+ "add the license file or configure your project accordingly.",
+ "file_path", s.boilerplatePath,
+ "error", err)
+ boilerplate = []byte("")
+ } else {
+ return fmt.Errorf("unable to load boilerplate: %w", err)
+ }
+ }
+ // Initialize the machinery.Scaffold that will write the files to disk
+ scaffold = machinery.NewScaffold(s.fs,
+ machinery.WithConfig(s.config),
+ machinery.WithBoilerplate(string(boilerplate)),
+ )
+ } else {
+ s.boilerplatePath = ""
+ // Initialize the machinery.Scaffold without boilerplate
+ scaffold = machinery.NewScaffold(s.fs,
+ machinery.WithConfig(s.config),
+ )
+ }
+
+ // If the KustomizeV2 was used to do the scaffold then
+ // we need to ensure that we use its supported Kustomize Version
+ // in order to support it
+ kustomizev2 := kustomizecommonv2.Plugin{}
+ gov4 := "go.kubebuilder.io/v4"
+ pluginKeyForKustomizeV2 := plugin.KeyFor(kustomizev2)
+
+ for _, pluginKey := range s.config.GetPluginChain() {
+ if pluginKey == pluginKeyForKustomizeV2 || pluginKey == gov4 {
+ kustomizeVersion = kustomizecommonv2.KustomizeVersion
+ break
+ }
+ }
+
+ err := scaffold.Execute(
+ &cmd.Main{
+ ControllerRuntimeVersion: ControllerRuntimeVersion,
+ },
+ &templates.GoMod{
+ ControllerRuntimeVersion: ControllerRuntimeVersion,
+ },
+ &templates.GitIgnore{},
+ &templates.Makefile{
+ Image: imageName,
+ BoilerplatePath: s.boilerplatePath,
+ ControllerToolsVersion: ControllerToolsVersion,
+ KustomizeVersion: kustomizeVersion,
+ GolangciLintVersion: GolangciLintVersion,
+ ControllerRuntimeVersion: ControllerRuntimeVersion,
+ EnvtestVersion: getControllerRuntimeReleaseBranch(),
+ },
+ &templates.Dockerfile{},
+ &templates.DockerIgnore{},
+ &templates.Readme{CommandName: s.commandName},
+ &templates.Golangci{},
+ &e2e.Test{},
+ &e2e.WebhookTestUpdater{WireWebhook: false},
+ &e2e.SuiteTest{},
+ &github.E2eTestCi{},
+ &github.TestCi{},
+ &github.LintCi{
+ GolangciLintVersion: GolangciLintVersion,
+ },
+ &utils.Utils{},
+ &templates.DevContainer{},
+ &templates.DevContainerPostInstallScript{},
+ )
+ if err != nil {
+ return fmt.Errorf("failed to execute init scaffold: %w", err)
+ }
+
+ return nil
+}
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/group.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/group.go
new file mode 100644
index 00000000000..817722b461b
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/group.go
@@ -0,0 +1,76 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+ log "log/slog"
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Group{}
+
+// Group scaffolds the file that defines the registration methods for a certain group and version
+type Group struct {
+ machinery.TemplateMixin
+ machinery.MultiGroupMixin
+ machinery.BoilerplateMixin
+ machinery.ResourceMixin
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *Group) SetTemplateDefaults() error {
+ if f.Path == "" {
+ if f.MultiGroup && f.Resource.Group != "" {
+ f.Path = filepath.Join("api", "%[group]", "%[version]", "groupversion_info.go")
+ } else {
+ f.Path = filepath.Join("api", "%[version]", "groupversion_info.go")
+ }
+ }
+
+ f.Path = f.Resource.Replacer().Replace(f.Path)
+ log.Info(f.Path)
+ f.TemplateBody = groupTemplate
+
+ return nil
+}
+
+//nolint:lll
+const groupTemplate = `{{ .Boilerplate }}
+
+// Package {{ .Resource.Version }} contains API Schema definitions for the {{ .Resource.Group }} {{ .Resource.Version }} API group.
+// +kubebuilder:object:generate=true
+// +groupName={{ .Resource.QualifiedGroup }}
+package {{ .Resource.Version }}
+
+import (
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "sigs.k8s.io/controller-runtime/pkg/scheme"
+)
+
+var (
+ // GroupVersion is group version used to register these objects.
+ GroupVersion = schema.GroupVersion{Group: "{{ .Resource.QualifiedGroup }}", Version: "{{ .Resource.Version }}"}
+
+ // SchemeBuilder is used to add go types to the GroupVersionKind scheme.
+ SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
+
+ // AddToScheme adds the types in this group-version to the given scheme.
+ AddToScheme = SchemeBuilder.AddToScheme
+)
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/hub.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/hub.go
new file mode 100644
index 00000000000..bb46111b2f3
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/hub.go
@@ -0,0 +1,72 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+ log "log/slog"
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Hub{}
+
+// Hub scaffolds the file that defines hub
+//
+
+type Hub struct {
+ machinery.TemplateMixin
+ machinery.MultiGroupMixin
+ machinery.BoilerplateMixin
+ machinery.ResourceMixin
+
+ Force bool
+}
+
+// SetTemplateDefaults implements file.Template
+func (f *Hub) SetTemplateDefaults() error {
+ if f.Path == "" {
+ if f.MultiGroup && f.Resource.Group != "" {
+ f.Path = filepath.Join("api", "%[group]", "%[version]", "%[kind]_conversion.go")
+ } else {
+ f.Path = filepath.Join("api", "%[version]", "%[kind]_conversion.go")
+ }
+ }
+
+ f.Path = f.Resource.Replacer().Replace(f.Path)
+ log.Info(f.Path)
+
+ f.TemplateBody = hubTemplate
+
+ if f.Force {
+ f.IfExistsAction = machinery.OverwriteFile
+ } else {
+ f.IfExistsAction = machinery.SkipFile
+ }
+
+ return nil
+}
+
+const hubTemplate = `{{ .Boilerplate }}
+
+package {{ .Resource.Version }}
+
+// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
+
+// Hub marks this type as a conversion hub.
+func (*{{ .Resource.Kind }}) Hub() {}
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/spoke.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/spoke.go
new file mode 100644
index 00000000000..74c67df8b1d
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/spoke.go
@@ -0,0 +1,108 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+ log "log/slog"
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Spoke{}
+
+// Spoke scaffolds the file that defines spoke version conversion
+type Spoke struct {
+ machinery.TemplateMixin
+ machinery.MultiGroupMixin
+ machinery.BoilerplateMixin
+ machinery.ResourceMixin
+
+ Force bool
+ SpokeVersion string
+}
+
+// SetTemplateDefaults implements file.Template
+func (f *Spoke) SetTemplateDefaults() error {
+ if f.Path == "" {
+ if f.MultiGroup && f.Resource.Group != "" {
+ // Use SpokeVersion for dynamic file path generation
+ f.Path = filepath.Join("api", f.Resource.Group, f.SpokeVersion, "%[kind]_conversion.go")
+ } else {
+ f.Path = filepath.Join("api", f.SpokeVersion, "%[kind]_conversion.go")
+ }
+ }
+
+ // Replace placeholders in the path
+ f.Path = f.Resource.Replacer().Replace(f.Path)
+ log.Info("Creating spoke conversion file", "path", f.Path)
+
+ f.TemplateBody = spokeTemplate
+
+ if f.Force {
+ f.IfExistsAction = machinery.OverwriteFile
+ } else {
+ f.IfExistsAction = machinery.SkipFile
+ }
+
+ return nil
+}
+
+//nolint:lll
+const spokeTemplate = `{{ .Boilerplate }}
+
+package {{ .SpokeVersion }}
+
+import (
+ "log"
+
+ "sigs.k8s.io/controller-runtime/pkg/conversion"
+ {{ .Resource.ImportAlias }} "{{ .Resource.Path }}"
+)
+
+// ConvertTo converts this {{ .Resource.Kind }} ({{ .SpokeVersion }}) to the Hub version ({{ .Resource.Version }}).
+func (src *{{ .Resource.Kind }}) ConvertTo(dstRaw conversion.Hub) error {
+ dst := dstRaw.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }})
+ log.Printf("ConvertTo: Converting {{ .Resource.Kind }} from Spoke version {{ .SpokeVersion }} to Hub version {{ .Resource.Version }};" +
+ "source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name)
+
+ // TODO(user): Implement conversion logic from {{ .SpokeVersion }} to {{ .Resource.Version }}
+ // Example: Copying Spec fields
+ // dst.Spec.Size = src.Spec.Replicas
+
+ // Copy ObjectMeta to preserve name, namespace, labels, etc.
+ dst.ObjectMeta = src.ObjectMeta
+
+ return nil
+}
+
+// ConvertFrom converts the Hub version ({{ .Resource.Version }}) to this {{ .Resource.Kind }} ({{ .SpokeVersion }}).
+func (dst *{{ .Resource.Kind }}) ConvertFrom(srcRaw conversion.Hub) error {
+ src := srcRaw.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }})
+ log.Printf("ConvertFrom: Converting {{ .Resource.Kind }} from Hub version {{ .Resource.Version }} to Spoke version {{ .SpokeVersion }};" +
+ "source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name)
+
+ // TODO(user): Implement conversion logic from {{ .Resource.Version }} to {{ .SpokeVersion }}
+ // Example: Copying Spec fields
+ // dst.Spec.Replicas = src.Spec.Size
+
+ // Copy ObjectMeta to preserve name, namespace, labels, etc.
+ dst.ObjectMeta = src.ObjectMeta
+
+ return nil
+}
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/types.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/types.go
new file mode 100644
index 00000000000..429b68192d6
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/types.go
@@ -0,0 +1,150 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+ log "log/slog"
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Types{}
+
+// Types scaffolds the file that defines the schema for a CRD
+//
+
+type Types struct {
+ machinery.TemplateMixin
+ machinery.MultiGroupMixin
+ machinery.BoilerplateMixin
+ machinery.ResourceMixin
+
+ Force bool
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *Types) SetTemplateDefaults() error {
+ if f.Path == "" {
+ if f.MultiGroup && f.Resource.Group != "" {
+ f.Path = filepath.Join("api", "%[group]", "%[version]", "%[kind]_types.go")
+ } else {
+ f.Path = filepath.Join("api", "%[version]", "%[kind]_types.go")
+ }
+ }
+
+ f.Path = f.Resource.Replacer().Replace(f.Path)
+ log.Info(f.Path)
+
+ f.TemplateBody = typesTemplate
+
+ if f.Force {
+ f.IfExistsAction = machinery.OverwriteFile
+ } else {
+ f.IfExistsAction = machinery.Error
+ }
+
+ return nil
+}
+
+//nolint:lll
+const typesTemplate = `{{ .Boilerplate }}
+
+package {{ .Resource.Version }}
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
+// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
+
+// {{ .Resource.Kind }}Spec defines the desired state of {{ .Resource.Kind }}
+type {{ .Resource.Kind }}Spec struct {
+ // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
+ // Important: Run "make" to regenerate code after modifying this file
+ // The following markers will use OpenAPI v3 schema to validate the value
+ // More info: https://book.kubebuilder.io/reference/markers/crd-validation.html
+
+ // foo is an example field of {{ .Resource.Kind }}. Edit {{ lower .Resource.Kind }}_types.go to remove/update
+ // +optional
+ Foo *string ` + "`" + `json:"foo,omitempty"` + "`" + `
+}
+
+// {{ .Resource.Kind }}Status defines the observed state of {{ .Resource.Kind }}.
+type {{ .Resource.Kind }}Status struct {
+ // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
+ // Important: Run "make" to regenerate code after modifying this file
+
+ // For Kubernetes API conventions, see:
+ // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
+
+ // conditions represent the current state of the {{ .Resource.Kind }} resource.
+ // Each condition has a unique type and reflects the status of a specific aspect of the resource.
+ //
+ // Standard condition types include:
+ // - "Available": the resource is fully functional
+ // - "Progressing": the resource is being created or updated
+ // - "Degraded": the resource failed to reach or maintain its desired state
+ //
+ // The status of each condition is one of True, False, or Unknown.
+ // +listType=map
+ // +listMapKey=type
+ // +optional
+ Conditions []metav1.Condition ` + "`" + `json:"conditions,omitempty"` + "`" + `
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+{{- if and (not .Resource.API.Namespaced) (not .Resource.IsRegularPlural) }}
+// +kubebuilder:resource:path={{ .Resource.Plural }},scope=Cluster
+{{- else if not .Resource.API.Namespaced }}
+// +kubebuilder:resource:scope=Cluster
+{{- else if not .Resource.IsRegularPlural }}
+// +kubebuilder:resource:path={{ .Resource.Plural }}
+{{- end }}
+
+// {{ .Resource.Kind }} is the Schema for the {{ .Resource.Plural }} API
+type {{ .Resource.Kind }} struct {
+ metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + `
+
+ // metadata is a standard object metadata
+ // +optional
+ metav1.ObjectMeta ` + "`" + `json:"metadata,omitempty,omitzero"` + "`" + `
+
+ // spec defines the desired state of {{ .Resource.Kind }}
+ // +required
+ Spec {{ .Resource.Kind }}Spec ` + "`" + `json:"spec"` + "`" + `
+
+ // status defines the observed state of {{ .Resource.Kind }}
+ // +optional
+ Status {{ .Resource.Kind }}Status ` + "`" + `json:"status,omitempty,omitzero"` + "`" + `
+}
+
+// +kubebuilder:object:root=true
+
+// {{ .Resource.Kind }}List contains a list of {{ .Resource.Kind }}
+type {{ .Resource.Kind }}List struct {
+ metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + `
+ metav1.ListMeta ` + "`" + `json:"metadata,omitempty"` + "`" + `
+ Items []{{ .Resource.Kind }} ` + "`" + `json:"items"` + "`" + `
+}
+
+func init() {
+ SchemeBuilder.Register(&{{ .Resource.Kind }}{}, &{{ .Resource.Kind }}List{})
+}
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/cmd/main.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/cmd/main.go
new file mode 100644
index 00000000000..dedb30b6ae0
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/cmd/main.go
@@ -0,0 +1,410 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package cmd
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+const defaultMainPath = "cmd/main.go"
+
+var _ machinery.Template = &Main{}
+
+// Main scaffolds a file that defines the controller manager entry point
+type Main struct {
+ machinery.TemplateMixin
+ machinery.BoilerplateMixin
+ machinery.DomainMixin
+ machinery.RepositoryMixin
+
+ ControllerRuntimeVersion string
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *Main) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join(defaultMainPath)
+ }
+
+ f.TemplateBody = fmt.Sprintf(mainTemplate,
+ machinery.NewMarkerFor(f.Path, importMarker),
+ machinery.NewMarkerFor(f.Path, addSchemeMarker),
+ machinery.NewMarkerFor(f.Path, setupMarker),
+ )
+
+ return nil
+}
+
+var _ machinery.Inserter = &MainUpdater{}
+
+// MainUpdater updates cmd/main.go to run Controllers
+type MainUpdater struct {
+ machinery.RepositoryMixin
+ machinery.MultiGroupMixin
+ machinery.ResourceMixin
+
+ // Flags to indicate which parts need to be included when updating the file
+ WireResource, WireController, WireWebhook bool
+
+ // Deprecated - The flag should be removed from go/v5
+ // IsLegacyPath indicates if webhooks should be scaffolded under the API.
+ // Webhooks are now decoupled from APIs based on controller-runtime updates and community feedback.
+ // This flag ensures backward compatibility by allowing scaffolding in the legacy/deprecated path.
+ IsLegacyPath bool
+}
+
+// GetPath implements file.Builder
+func (*MainUpdater) GetPath() string {
+ return defaultMainPath
+}
+
+// GetIfExistsAction implements file.Builder
+func (*MainUpdater) GetIfExistsAction() machinery.IfExistsAction {
+ return machinery.OverwriteFile
+}
+
+const (
+ importMarker = "imports"
+ addSchemeMarker = "scheme"
+ setupMarker = "builder"
+)
+
+// GetMarkers implements file.Inserter
+func (f *MainUpdater) GetMarkers() []machinery.Marker {
+ return []machinery.Marker{
+ machinery.NewMarkerFor(defaultMainPath, importMarker),
+ machinery.NewMarkerFor(defaultMainPath, addSchemeMarker),
+ machinery.NewMarkerFor(defaultMainPath, setupMarker),
+ }
+}
+
+const (
+ apiImportCodeFragment = `%s "%s"
+`
+ controllerImportCodeFragment = `"%s/internal/controller"
+`
+ webhookImportCodeFragment = `%s "%s/internal/webhook/%s"
+`
+ multiGroupWebhookImportCodeFragment = `%s "%s/internal/webhook/%s/%s"
+`
+ multiGroupControllerImportCodeFragment = `%scontroller "%s/internal/controller/%s"
+`
+ addschemeCodeFragment = `utilruntime.Must(%s.AddToScheme(scheme))
+`
+ reconcilerSetupCodeFragment = `if err := (&controller.%sReconciler{
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ }).SetupWithManager(mgr); err != nil {
+ setupLog.Error(err, "unable to create controller", "controller", "%s")
+ os.Exit(1)
+ }
+`
+ multiGroupReconcilerSetupCodeFragment = `if err := (&%scontroller.%sReconciler{
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ }).SetupWithManager(mgr); err != nil {
+ setupLog.Error(err, "unable to create controller", "controller", "%s")
+ os.Exit(1)
+ }
+`
+ webhookSetupCodeFragmentLegacy = `// nolint:goconst
+ if os.Getenv("ENABLE_WEBHOOKS") != "false" {
+ if err := (&%s.%s{}).SetupWebhookWithManager(mgr); err != nil {
+ setupLog.Error(err, "unable to create webhook", "webhook", "%s")
+ os.Exit(1)
+ }
+ }
+`
+
+ webhookSetupCodeFragment = `// nolint:goconst
+ if os.Getenv("ENABLE_WEBHOOKS") != "false" {
+ if err := %s.Setup%sWebhookWithManager(mgr); err != nil {
+ setupLog.Error(err, "unable to create webhook", "webhook", "%s")
+ os.Exit(1)
+ }
+ }
+`
+)
+
+// GetCodeFragments implements file.Inserter
+func (f *MainUpdater) GetCodeFragments() machinery.CodeFragmentsMap {
+ fragments := make(machinery.CodeFragmentsMap, 3)
+
+ // If resource is not being provided we are creating the file, not updating it
+ if f.Resource == nil {
+ return fragments
+ }
+
+ // Generate import code fragments
+ imports := make([]string, 0)
+ if f.WireResource || f.Resource.IsExternal() {
+ imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path))
+ }
+ if f.WireWebhook && !f.IsLegacyPath {
+ if !f.MultiGroup || f.Resource.Group == "" {
+ importPath := fmt.Sprintf("webhook%s", f.Resource.Version)
+ imports = append(imports, fmt.Sprintf(webhookImportCodeFragment, importPath, f.Repo, f.Resource.Version))
+ } else {
+ importPath := fmt.Sprintf("webhook%s", f.Resource.ImportAlias())
+ imports = append(imports, fmt.Sprintf(multiGroupWebhookImportCodeFragment, importPath,
+ f.Repo, f.Resource.Group, f.Resource.Version))
+ }
+ }
+
+ if f.WireController {
+ if !f.MultiGroup || f.Resource.Group == "" {
+ imports = append(imports, fmt.Sprintf(controllerImportCodeFragment, f.Repo))
+ } else {
+ imports = append(imports, fmt.Sprintf(multiGroupControllerImportCodeFragment,
+ f.Resource.PackageName(), f.Repo, f.Resource.Group))
+ }
+ }
+
+ // Generate add scheme code fragments
+ addScheme := make([]string, 0)
+ if f.WireResource || f.Resource.IsExternal() {
+ addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias()))
+ }
+
+ // Generate setup code fragments
+ setup := make([]string, 0)
+ if f.WireController {
+ if !f.MultiGroup || f.Resource.Group == "" {
+ setup = append(setup, fmt.Sprintf(reconcilerSetupCodeFragment,
+ f.Resource.Kind, f.Resource.Kind))
+ } else {
+ setup = append(setup, fmt.Sprintf(multiGroupReconcilerSetupCodeFragment,
+ f.Resource.PackageName(), f.Resource.Kind, f.Resource.Kind))
+ }
+ }
+ if f.WireWebhook {
+ if f.IsLegacyPath {
+ setup = append(setup, fmt.Sprintf(webhookSetupCodeFragmentLegacy,
+ f.Resource.ImportAlias(), f.Resource.Kind, f.Resource.Kind))
+ } else {
+ if !f.MultiGroup || f.Resource.Group == "" {
+ setup = append(setup, fmt.Sprintf(webhookSetupCodeFragment,
+ "webhook"+f.Resource.Version, f.Resource.Kind, f.Resource.Kind))
+ } else {
+ setup = append(setup, fmt.Sprintf(webhookSetupCodeFragment,
+ "webhook"+f.Resource.ImportAlias(), f.Resource.Kind, f.Resource.Kind))
+ }
+ }
+ }
+
+ // Only store code fragments in the map if the slices are non-empty
+ if len(imports) != 0 {
+ fragments[machinery.NewMarkerFor(defaultMainPath, importMarker)] = imports
+ }
+ if len(addScheme) != 0 {
+ fragments[machinery.NewMarkerFor(defaultMainPath, addSchemeMarker)] = addScheme
+ }
+ if len(setup) != 0 {
+ fragments[machinery.NewMarkerFor(defaultMainPath, setupMarker)] = setup
+ }
+
+ return fragments
+}
+
+//nolint:lll
+var mainTemplate = `{{ .Boilerplate }}
+
+package main
+
+import (
+ "crypto/tls"
+ "flag"
+ "os"
+
+ // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
+ // to ensure that exec-entrypoint and run can make use of them.
+ _ "k8s.io/client-go/plugin/pkg/client/auth"
+
+ "k8s.io/apimachinery/pkg/runtime"
+ utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+ clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/log/zap"
+ "sigs.k8s.io/controller-runtime/pkg/healthz"
+ "sigs.k8s.io/controller-runtime/pkg/metrics/filters"
+ "sigs.k8s.io/controller-runtime/pkg/webhook"
+ metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+ %s
+)
+
+var (
+ scheme = runtime.NewScheme()
+ setupLog = ctrl.Log.WithName("setup")
+)
+
+func init() {
+ utilruntime.Must(clientgoscheme.AddToScheme(scheme))
+
+ %s
+}
+
+// nolint:gocyclo
+func main() {
+ var metricsAddr string
+ var metricsCertPath, metricsCertName, metricsCertKey string
+ var webhookCertPath, webhookCertName, webhookCertKey string
+ var enableLeaderElection bool
+ var probeAddr string
+ var secureMetrics bool
+ var enableHTTP2 bool
+ var tlsOpts []func(*tls.Config)
+ flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. " +
+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
+ flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
+ flag.BoolVar(&enableLeaderElection, "leader-elect", false,
+ "Enable leader election for controller manager. " +
+ "Enabling this will ensure there is only one active controller manager.")
+ flag.BoolVar(&secureMetrics, "metrics-secure", true,
+ "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
+ flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
+ flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
+ flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
+ flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
+ "The directory that contains the metrics server certificate.")
+ flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
+ flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
+ flag.BoolVar(&enableHTTP2, "enable-http2", false,
+ "If set, HTTP/2 will be enabled for the metrics and webhook servers")
+ opts := zap.Options{
+ Development: true,
+ }
+ opts.BindFlags(flag.CommandLine)
+ flag.Parse()
+
+ ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
+
+ // if the enable-http2 flag is false (the default), http/2 should be disabled
+ // due to its vulnerabilities. More specifically, disabling http/2 will
+ // prevent from being vulnerable to the HTTP/2 Stream Cancellation and
+ // Rapid Reset CVEs. For more information see:
+ // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
+ // - https://github.com/advisories/GHSA-4374-p667-p6c8
+ disableHTTP2 := func(c *tls.Config) {
+ setupLog.Info("disabling http/2")
+ c.NextProtos = []string{"http/1.1"}
+ }
+
+ if !enableHTTP2 {
+ tlsOpts = append(tlsOpts, disableHTTP2)
+ }
+
+ // Initial webhook TLS options
+ webhookTLSOpts := tlsOpts
+ webhookServerOptions := webhook.Options{
+ TLSOpts: webhookTLSOpts,
+ }
+
+ if len(webhookCertPath) > 0 {
+ setupLog.Info("Initializing webhook certificate watcher using provided certificates",
+ "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)
+
+ webhookServerOptions.CertDir = webhookCertPath
+ webhookServerOptions.CertName = webhookCertName
+ webhookServerOptions.KeyName = webhookCertKey
+ }
+
+ webhookServer := webhook.NewServer(webhookServerOptions)
+
+ // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
+ // More info:
+ // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@{{ .ControllerRuntimeVersion }}/pkg/metrics/server
+ // - https://book.kubebuilder.io/reference/metrics.html
+ metricsServerOptions := metricsserver.Options{
+ BindAddress: metricsAddr,
+ SecureServing: secureMetrics,
+ TLSOpts: tlsOpts,
+ }
+
+ if secureMetrics {
+ // FilterProvider is used to protect the metrics endpoint with authn/authz.
+ // These configurations ensure that only authorized users and service accounts
+ // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
+ // https://pkg.go.dev/sigs.k8s.io/controller-runtime@{{ .ControllerRuntimeVersion }}/pkg/metrics/filters#WithAuthenticationAndAuthorization
+ metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
+ }
+
+ // If the certificate is not specified, controller-runtime will automatically
+ // generate self-signed certificates for the metrics server. While convenient for development and testing,
+ // this setup is not recommended for production.
+ //
+ // TODO(user): If you enable certManager, uncomment the following lines:
+ // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates
+ // managed by cert-manager for the metrics server.
+ // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification.
+ if len(metricsCertPath) > 0 {
+ setupLog.Info("Initializing metrics certificate watcher using provided certificates",
+ "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)
+
+ metricsServerOptions.CertDir = metricsCertPath
+ metricsServerOptions.CertName = metricsCertName
+ metricsServerOptions.KeyName = metricsCertKey
+ }
+
+ mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
+ Scheme: scheme,
+ Metrics: metricsServerOptions,
+ WebhookServer: webhookServer,
+ HealthProbeBindAddress: probeAddr,
+ LeaderElection: enableLeaderElection,
+ {{- if not .Domain }}
+ LeaderElectionID: "{{ hashFNV .Repo }}",
+ {{- else }}
+ LeaderElectionID: "{{ hashFNV .Repo }}.{{ .Domain }}",
+ {{- end }}
+ // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
+ // when the Manager ends. This requires the binary to immediately end when the
+ // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
+ // speeds up voluntary leader transitions as the new leader don't have to wait
+ // LeaseDuration time first.
+ //
+ // In the default scaffold provided, the program ends immediately after
+ // the manager stops, so would be fine to enable this option. However,
+ // if you are doing or is intended to do any operation such as perform cleanups
+ // after the manager stops then its usage might be unsafe.
+ // LeaderElectionReleaseOnCancel: true,
+ })
+ if err != nil {
+ setupLog.Error(err, "unable to start manager")
+ os.Exit(1)
+ }
+
+ %s
+
+ if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
+ setupLog.Error(err, "unable to set up health check")
+ os.Exit(1)
+ }
+ if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
+ setupLog.Error(err, "unable to set up ready check")
+ os.Exit(1)
+ }
+
+ setupLog.Info("starting manager")
+ if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
+ setupLog.Error(err, "problem running manager")
+ os.Exit(1)
+ }
+}
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller.go
new file mode 100644
index 00000000000..640f64efb4c
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller.go
@@ -0,0 +1,125 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controllers
+
+import (
+ log "log/slog"
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Controller{}
+
+// Controller scaffolds the file that defines the controller for a CRD or a builtin resource
+//
+
+type Controller struct {
+ machinery.TemplateMixin
+ machinery.MultiGroupMixin
+ machinery.BoilerplateMixin
+ machinery.ResourceMixin
+
+ ControllerRuntimeVersion string
+
+ Force bool
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *Controller) SetTemplateDefaults() error {
+ if f.Path == "" {
+ if f.MultiGroup && f.Resource.Group != "" {
+ f.Path = filepath.Join("internal", "controller", "%[group]", "%[kind]_controller.go")
+ } else {
+ f.Path = filepath.Join("internal", "controller", "%[kind]_controller.go")
+ }
+ }
+
+ f.Path = f.Resource.Replacer().Replace(f.Path)
+ log.Info(f.Path)
+
+ f.TemplateBody = controllerTemplate
+
+ if f.Force {
+ f.IfExistsAction = machinery.OverwriteFile
+ } else {
+ f.IfExistsAction = machinery.Error
+ }
+
+ return nil
+}
+
+//nolint:lll
+const controllerTemplate = `{{ .Boilerplate }}
+
+package {{ if and .MultiGroup .Resource.Group }}{{ .Resource.PackageName }}{{ else }}controller{{ end }}
+
+import (
+ "context"
+ "k8s.io/apimachinery/pkg/runtime"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ {{ if not (isEmptyStr .Resource.Path) -}}
+ {{ .Resource.ImportAlias }} "{{ .Resource.Path }}"
+ {{- end }}
+)
+
+// {{ .Resource.Kind }}Reconciler reconciles a {{ .Resource.Kind }} object
+type {{ .Resource.Kind }}Reconciler struct {
+ client.Client
+ Scheme *runtime.Scheme
+}
+
+// +kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }}/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }}/finalizers,verbs=update
+
+// Reconcile is part of the main kubernetes reconciliation loop which aims to
+// move the current state of the cluster closer to the desired state.
+// TODO(user): Modify the Reconcile function to compare the state specified by
+// the {{ .Resource.Kind }} object against the actual cluster state, and then
+// perform operations to make the cluster state reflect the state specified by
+// the user.
+//
+// For more details, check Reconcile and its Result here:
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@{{ .ControllerRuntimeVersion }}/pkg/reconcile
+func (r *{{ .Resource.Kind }}Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ _ = logf.FromContext(ctx)
+
+ // TODO(user): your logic here
+
+ return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *{{ .Resource.Kind }}Reconciler) SetupWithManager(mgr ctrl.Manager) error {
+ return ctrl.NewControllerManagedBy(mgr).
+ {{ if not (isEmptyStr .Resource.Path) -}}
+ For(&{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}).
+ {{- else -}}
+ // Uncomment the following line adding a pointer to an instance of the controlled resource as an argument
+ // For().
+ {{- end }}
+ {{- if and (.MultiGroup) (not (isEmptyStr .Resource.Group)) }}
+ Named("{{ lower .Resource.Group }}-{{ lower .Resource.Kind }}").
+ {{- else }}
+ Named("{{ lower .Resource.Kind }}").
+ {{- end }}
+ Complete(r)
+}
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller_suitetest.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller_suitetest.go
new file mode 100644
index 00000000000..8ef84af7ff9
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller_suitetest.go
@@ -0,0 +1,229 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controllers
+
+import (
+ "fmt"
+ log "log/slog"
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var (
+ _ machinery.Template = &SuiteTest{}
+ _ machinery.Inserter = &SuiteTest{}
+)
+
+// SuiteTest scaffolds the file that sets up the controller tests
+//
+
+type SuiteTest struct {
+ machinery.TemplateMixin
+ machinery.MultiGroupMixin
+ machinery.BoilerplateMixin
+ machinery.ResourceMixin
+
+ // CRDDirectoryRelativePath define the Path for the CRD
+ CRDDirectoryRelativePath string
+
+ Force bool
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *SuiteTest) SetTemplateDefaults() error {
+ if f.Path == "" {
+ if f.MultiGroup && f.Resource.Group != "" {
+ f.Path = filepath.Join("internal", "controller", "%[group]", "suite_test.go")
+ } else {
+ f.Path = filepath.Join("internal", "controller", "suite_test.go")
+ }
+ }
+
+ f.Path = f.Resource.Replacer().Replace(f.Path)
+ log.Info(f.Path)
+
+ f.TemplateBody = fmt.Sprintf(controllerSuiteTestTemplate,
+ machinery.NewMarkerFor(f.Path, importMarker),
+ machinery.NewMarkerFor(f.Path, addSchemeMarker),
+ )
+
+ // If is multigroup the path needs to be ../../ since it has
+ // the group dir.
+ f.CRDDirectoryRelativePath = `"..",".."`
+ if f.MultiGroup && f.Resource.Group != "" {
+ f.CRDDirectoryRelativePath = `"..", "..",".."`
+ }
+
+ if f.Force {
+ f.IfExistsAction = machinery.OverwriteFile
+ }
+
+ return nil
+}
+
+const (
+ importMarker = "imports"
+ addSchemeMarker = "scheme"
+)
+
+// GetMarkers implements file.Inserter
+func (f *SuiteTest) GetMarkers() []machinery.Marker {
+ return []machinery.Marker{
+ machinery.NewMarkerFor(f.Path, importMarker),
+ machinery.NewMarkerFor(f.Path, addSchemeMarker),
+ }
+}
+
+const (
+ apiImportCodeFragment = `%s "%s"
+`
+ addschemeCodeFragment = `err = %s.AddToScheme(scheme.Scheme)
+Expect(err).NotTo(HaveOccurred())
+
+`
+)
+
+// GetCodeFragments implements file.Inserter
+func (f *SuiteTest) GetCodeFragments() machinery.CodeFragmentsMap {
+ fragments := make(machinery.CodeFragmentsMap, 2)
+
+ // Generate import code fragments
+ imports := make([]string, 0)
+ if f.Resource.Path != "" {
+ imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path))
+ }
+
+ // Generate add scheme code fragments
+ addScheme := make([]string, 0)
+ if f.Resource.Path != "" {
+ addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias()))
+ }
+
+ // Only store code fragments in the map if the slices are non-empty
+ if len(imports) != 0 {
+ fragments[machinery.NewMarkerFor(f.Path, importMarker)] = imports
+ }
+ if len(addScheme) != 0 {
+ fragments[machinery.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme
+ }
+
+ return fragments
+}
+
+const controllerSuiteTestTemplate = `{{ .Boilerplate }}
+
+{{if and .MultiGroup .Resource.Group }}
+package {{ .Resource.PackageName }}
+{{else}}
+package controller
+{{end}}
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "testing"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "k8s.io/client-go/kubernetes/scheme"
+ "k8s.io/client-go/rest"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/envtest"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/log/zap"
+ %s
+)
+
+// These tests use Ginkgo (BDD-style Go testing framework). Refer to
+// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
+
+var (
+ ctx context.Context
+ cancel context.CancelFunc
+ testEnv *envtest.Environment
+ cfg *rest.Config
+ k8sClient client.Client
+)
+
+func TestControllers(t *testing.T) {
+ RegisterFailHandler(Fail)
+
+ RunSpecs(t, "Controller Suite")
+}
+
+var _ = BeforeSuite(func() {
+ logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
+
+ ctx, cancel = context.WithCancel(context.TODO())
+
+ var err error
+ %s
+
+ By("bootstrapping test environment")
+ testEnv = &envtest.Environment{
+ CRDDirectoryPaths: []string{filepath.Join({{ .CRDDirectoryRelativePath }}, "config", "crd", "bases")},
+ ErrorIfCRDPathMissing: {{ .Resource.HasAPI }},
+ }
+
+ // Retrieve the first found binary directory to allow running tests from IDEs
+ if getFirstFoundEnvTestBinaryDir() != "" {
+ testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
+ }
+
+ // cfg is defined in this file globally.
+ cfg, err = testEnv.Start()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cfg).NotTo(BeNil())
+
+ k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
+ Expect(err).NotTo(HaveOccurred())
+ Expect(k8sClient).NotTo(BeNil())
+})
+
+var _ = AfterSuite(func() {
+ By("tearing down the test environment")
+ cancel()
+ err := testEnv.Stop()
+ Expect(err).NotTo(HaveOccurred())
+})
+
+// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
+// ENVTEST-based tests depend on specific binaries, usually located in paths set by
+// controller-runtime. When running tests directly (e.g., via an IDE) without using
+// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
+//
+// This function streamlines the process by finding the required binaries, similar to
+// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
+// properly set up, run 'make setup-envtest' beforehand.
+func getFirstFoundEnvTestBinaryDir() string {
+ basePath := filepath.Join({{ .CRDDirectoryRelativePath }}, "bin", "k8s")
+ entries, err := os.ReadDir(basePath)
+ if err != nil {
+ logf.Log.Error(err, "Failed to read directory", "path", basePath)
+ return ""
+ }
+ for _, entry := range entries {
+ if entry.IsDir() {
+ return filepath.Join(basePath, entry.Name())
+ }
+ }
+ return ""
+}
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller_test_template.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller_test_template.go
new file mode 100644
index 00000000000..1df4f15dc6b
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller_test_template.go
@@ -0,0 +1,147 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controllers
+
+import (
+ log "log/slog"
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &ControllerTest{}
+
+// ControllerTest scaffolds the file that sets up the controller unit tests
+//
+
+type ControllerTest struct {
+ machinery.TemplateMixin
+ machinery.MultiGroupMixin
+ machinery.BoilerplateMixin
+ machinery.ResourceMixin
+
+ Force bool
+
+ DoAPI bool
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *ControllerTest) SetTemplateDefaults() error {
+ if f.Path == "" {
+ if f.MultiGroup && f.Resource.Group != "" {
+ f.Path = filepath.Join("internal", "controller", "%[group]", "%[kind]_controller_test.go")
+ } else {
+ f.Path = filepath.Join("internal", "controller", "%[kind]_controller_test.go")
+ }
+ }
+
+ f.Path = f.Resource.Replacer().Replace(f.Path)
+ log.Info(f.Path)
+
+ f.TemplateBody = controllerTestTemplate
+
+ if f.Force {
+ f.IfExistsAction = machinery.OverwriteFile
+ }
+
+ return nil
+}
+
+const controllerTestTemplate = `{{ .Boilerplate }}
+
+{{if and .MultiGroup .Resource.Group }}
+package {{ .Resource.PackageName }}
+{{else}}
+package controller
+{{end}}
+
+import (
+ {{ if .DoAPI -}}
+ "context"
+ {{- end }}
+ . "github.com/onsi/ginkgo/v2"
+ {{ if .DoAPI -}}
+
+ . "github.com/onsi/gomega"
+ "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ {{ if not (isEmptyStr .Resource.Path) -}}
+ {{ .Resource.ImportAlias }} "{{ .Resource.Path }}"
+ {{- end }}
+ {{- end }}
+)
+
+var _ = Describe("{{ .Resource.Kind }} Controller", func() {
+ Context("When reconciling a resource", func() {
+ {{ if .DoAPI -}}
+ const resourceName = "test-resource"
+
+ ctx := context.Background()
+
+ typeNamespacedName := types.NamespacedName{
+ Name: resourceName,
+ Namespace: "default", // TODO(user):Modify as needed
+ }
+ {{ lower .Resource.Kind }} := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}
+
+ BeforeEach(func() {
+ By("creating the custom resource for the Kind {{ .Resource.Kind }}")
+ err := k8sClient.Get(ctx, typeNamespacedName, {{ lower .Resource.Kind }})
+ if err != nil && errors.IsNotFound(err) {
+ resource := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: resourceName,
+ Namespace: "default",
+ },
+ // TODO(user): Specify other spec details if needed.
+ }
+ Expect(k8sClient.Create(ctx, resource)).To(Succeed())
+ }
+ })
+
+ AfterEach(func() {
+ // TODO(user): Cleanup logic after each test, like removing the resource instance.
+ resource := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}
+ err := k8sClient.Get(ctx, typeNamespacedName, resource)
+ Expect(err).NotTo(HaveOccurred())
+
+ By("Cleanup the specific resource instance {{ .Resource.Kind }}")
+ Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
+ })
+ {{- end }}
+ It("should successfully reconcile the resource", func() {
+ {{ if .DoAPI -}}
+ By("Reconciling the created resource")
+ controllerReconciler := &{{ .Resource.Kind }}Reconciler{
+ Client: k8sClient,
+ Scheme: k8sClient.Scheme(),
+ }
+
+ _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
+ NamespacedName: typeNamespacedName,
+ })
+ Expect(err).NotTo(HaveOccurred())
+ {{- end }}
+ // TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
+ // Example: If you expect a certain status condition after reconciliation, verify it here.
+ })
+ })
+})
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/devcontainer.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/devcontainer.go
new file mode 100644
index 00000000000..2a22df963a0
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/devcontainer.go
@@ -0,0 +1,110 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package templates
+
+import (
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+const devContainerTemplate = `{
+ "name": "Kubebuilder DevContainer",
+ "image": "golang:1.25",
+ "features": {
+ "ghcr.io/devcontainers/features/docker-in-docker:2": {},
+ "ghcr.io/devcontainers/features/git:1": {}
+ },
+
+ "runArgs": ["--network=host"],
+
+ "customizations": {
+ "vscode": {
+ "settings": {
+ "terminal.integrated.shell.linux": "/bin/bash"
+ },
+ "extensions": [
+ "ms-kubernetes-tools.vscode-kubernetes-tools",
+ "ms-azuretools.vscode-docker"
+ ]
+ }
+ },
+
+ "onCreateCommand": "bash .devcontainer/post-install.sh"
+}
+
+`
+
+const postInstallScript = `#!/bin/bash
+set -x
+
+curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH)
+chmod +x ./kind
+mv ./kind /usr/local/bin/kind
+
+curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/linux/$(go env GOARCH)
+chmod +x kubebuilder
+mv kubebuilder /usr/local/bin/
+
+KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt)
+curl -LO "https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/$(go env GOARCH)/kubectl"
+chmod +x kubectl
+mv kubectl /usr/local/bin/kubectl
+
+docker network create -d=bridge --subnet=172.19.0.0/24 kind
+
+kind version
+kubebuilder version
+docker --version
+go version
+kubectl version --client
+`
+
+var (
+ _ machinery.Template = &DevContainer{}
+ _ machinery.Template = &DevContainerPostInstallScript{}
+)
+
+// DevContainer scaffoldds a `devcontainer.json` configurations file for creating Kubebuilder & Kind based DevContainer.
+type DevContainer struct {
+ machinery.TemplateMixin
+}
+
+// DevContainerPostInstallScript defines the scaffold that will be done with the post install script
+type DevContainerPostInstallScript struct {
+ machinery.TemplateMixin
+}
+
+// SetTemplateDefaults set defaults for this template
+func (f *DevContainer) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = ".devcontainer/devcontainer.json"
+ }
+
+ f.TemplateBody = devContainerTemplate
+
+ return nil
+}
+
+// SetTemplateDefaults set the defaults of this template
+func (f *DevContainerPostInstallScript) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = ".devcontainer/post-install.sh"
+ }
+
+ f.TemplateBody = postInstallScript
+
+ return nil
+}
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/dockerfile.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/dockerfile.go
new file mode 100644
index 00000000000..0c8f64c0978
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/dockerfile.go
@@ -0,0 +1,72 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package templates
+
+import (
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Dockerfile{}
+
+// Dockerfile scaffolds a file that defines the containerized build process
+type Dockerfile struct {
+ machinery.TemplateMixin
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *Dockerfile) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = "Dockerfile"
+ }
+
+ f.TemplateBody = dockerfileTemplate
+
+ return nil
+}
+
+const dockerfileTemplate = `# Build the manager binary
+FROM golang:1.25 AS builder
+ARG TARGETOS
+ARG TARGETARCH
+
+WORKDIR /workspace
+# Copy the Go Modules manifests
+COPY go.mod go.mod
+COPY go.sum go.sum
+# cache deps before building and copying source so that we don't need to re-download as much
+# and so that source changes don't invalidate our downloaded layer
+RUN go mod download
+
+# Copy the Go source (relies on .dockerignore to filter)
+COPY . .
+
+# Build
+# the GOARCH has no default value to allow the binary to be built according to the host where the command
+# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
+# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
+# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
+RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go
+
+# Use distroless as minimal base image to package the manager binary
+# Refer to https://github.com/GoogleContainerTools/distroless for more details
+FROM gcr.io/distroless/static:nonroot
+WORKDIR /
+COPY --from=builder /workspace/manager .
+USER 65532:65532
+
+ENTRYPOINT ["/manager"]
+`
diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerignore.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/dockerignore.go
similarity index 77%
rename from pkg/plugins/golang/v3/scaffolds/internal/templates/dockerignore.go
rename to pkg/plugins/golang/v4/scaffolds/internal/templates/dockerignore.go
index 2051adc956c..d95cc54e046 100644
--- a/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerignore.go
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/dockerignore.go
@@ -1,5 +1,5 @@
/*
-Copyright 2020 The Kubernetes Authors.
+Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,7 +17,7 @@ limitations under the License.
package templates
import (
- "sigs.k8s.io/kubebuilder/v3/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)
var _ machinery.Template = &DockerIgnore{}
@@ -27,7 +27,7 @@ type DockerIgnore struct {
machinery.TemplateMixin
}
-// SetTemplateDefaults implements file.Template
+// SetTemplateDefaults implements machinery.Template
func (f *DockerIgnore) SetTemplateDefaults() error {
if f.Path == "" {
f.Path = ".dockerignore"
@@ -39,8 +39,14 @@ func (f *DockerIgnore) SetTemplateDefaults() error {
}
const dockerignorefileTemplate = `# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
-# Ignore all files which are not go type
+# Ignore everything by default and re-include only needed files
+**
+
+# Re-include Go source files (but not *_test.go)
!**/*.go
-!**/*.mod
-!**/*.sum
+**/*_test.go
+
+# Re-include Go module files
+!go.mod
+!go.sum
`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/github/lint.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/github/lint.go
new file mode 100644
index 00000000000..730eba48ab4
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/github/lint.go
@@ -0,0 +1,72 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package github
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &TestCi{}
+
+// LintCi scaffolds the GitHub Action to lint the project
+type LintCi struct {
+ machinery.TemplateMixin
+ machinery.BoilerplateMixin
+
+ // golangci-lint version to use in the project
+ GolangciLintVersion string
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *LintCi) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join(".github", "workflows", "lint.yml")
+ }
+
+ f.TemplateBody = lintCiTemplate
+
+ f.IfExistsAction = machinery.SkipFile
+
+ return nil
+}
+
+const lintCiTemplate = `name: Lint
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ lint:
+ name: Run on Ubuntu
+ runs-on: ubuntu-latest
+ steps:
+ - name: Clone the code
+ uses: actions/checkout@v4
+
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+
+ - name: Run linter
+ uses: golangci/golangci-lint-action@v8
+ with:
+ version: {{ .GolangciLintVersion }}
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/github/test-e2e.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/github/test-e2e.go
new file mode 100644
index 00000000000..6404c0b6a04
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/github/test-e2e.go
@@ -0,0 +1,79 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package github
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &E2eTestCi{}
+
+// E2eTestCi scaffolds the GitHub Action to call make test-e2e
+type E2eTestCi struct {
+ machinery.TemplateMixin
+ machinery.BoilerplateMixin
+ machinery.ProjectNameMixin
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *E2eTestCi) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join(".github", "workflows", "test-e2e.yml")
+ }
+
+ f.TemplateBody = e2eTestCiTemplate
+
+ f.IfExistsAction = machinery.SkipFile
+
+ return nil
+}
+
+const e2eTestCiTemplate = `name: E2E Tests
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ test-e2e:
+ name: Run on Ubuntu
+ runs-on: ubuntu-latest
+ steps:
+ - name: Clone the code
+ uses: actions/checkout@v4
+
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+
+ - name: Install the latest version of kind
+ run: |
+ curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH)
+ chmod +x ./kind
+ sudo mv ./kind /usr/local/bin/kind
+
+ - name: Verify kind installation
+ run: kind version
+
+ - name: Running Test e2e
+ run: |
+ go mod tidy
+ make test-e2e
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/github/test.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/github/test.go
new file mode 100644
index 00000000000..8701bd8ffd9
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/github/test.go
@@ -0,0 +1,69 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package github
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &TestCi{}
+
+// TestCi scaffolds the GitHub Action to call make test
+type TestCi struct {
+ machinery.TemplateMixin
+ machinery.BoilerplateMixin
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *TestCi) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join(".github", "workflows", "test.yml")
+ }
+
+ f.TemplateBody = testCiTemplate
+
+ f.IfExistsAction = machinery.SkipFile
+
+ return nil
+}
+
+const testCiTemplate = `name: Tests
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ test:
+ name: Run on Ubuntu
+ runs-on: ubuntu-latest
+ steps:
+ - name: Clone the code
+ uses: actions/checkout@v4
+
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+
+ - name: Running Tests
+ run: |
+ go mod tidy
+ make test
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/gitignore.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/gitignore.go
new file mode 100644
index 00000000000..029f8affa9f
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/gitignore.go
@@ -0,0 +1,71 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package templates
+
+import (
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &GitIgnore{}
+
+// GitIgnore scaffolds a file that defines which files should be ignored by git
+type GitIgnore struct {
+ machinery.TemplateMixin
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *GitIgnore) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = ".gitignore"
+ }
+
+ f.TemplateBody = gitignoreTemplate
+
+ return nil
+}
+
+const gitignoreTemplate = `# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+bin/*
+Dockerfile.cross
+
+# Test binary, built with ` + "`go test -c`" + `
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Go workspace file
+go.work
+
+# Kubernetes Generated files - skip generated files, except for vendored files
+!vendor/**/zz_generated.*
+
+# editor and IDE paraphernalia
+.idea
+.vscode
+*.swp
+*.swo
+*~
+
+# Kubeconfig might contain secrets
+*.kubeconfig
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/golangci.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/golangci.go
new file mode 100644
index 00000000000..d2a1d1fc012
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/golangci.go
@@ -0,0 +1,96 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package templates
+
+import (
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Golangci{}
+
+// Golangci scaffolds a file which define Golangci rules
+type Golangci struct {
+ machinery.TemplateMixin
+ machinery.ProjectNameMixin
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *Golangci) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = ".golangci.yml"
+ }
+
+ f.TemplateBody = golangciTemplate
+
+ f.IfExistsAction = machinery.SkipFile
+
+ return nil
+}
+
+const golangciTemplate = `version: "2"
+run:
+ allow-parallel-runners: true
+linters:
+ default: none
+ enable:
+ - copyloopvar
+ - dupl
+ - errcheck
+ - ginkgolinter
+ - goconst
+ - gocyclo
+ - govet
+ - ineffassign
+ - lll
+ - misspell
+ - nakedret
+ - prealloc
+ - revive
+ - staticcheck
+ - unconvert
+ - unparam
+ - unused
+ settings:
+ revive:
+ rules:
+ - name: comment-spacings
+ - name: import-shadowing
+ exclusions:
+ generated: lax
+ rules:
+ - linters:
+ - lll
+ path: api/*
+ - linters:
+ - dupl
+ - lll
+ path: internal/*
+ paths:
+ - third_party$
+ - builtin$
+ - examples$
+formatters:
+ enable:
+ - gofmt
+ - goimports
+ exclusions:
+ generated: lax
+ paths:
+ - third_party$
+ - builtin$
+ - examples$
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/gomod.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/gomod.go
new file mode 100644
index 00000000000..2485580f3b3
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/gomod.go
@@ -0,0 +1,53 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package templates
+
+import (
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &GoMod{}
+
+// GoMod scaffolds a file that defines the project dependencies
+type GoMod struct {
+ machinery.TemplateMixin
+ machinery.RepositoryMixin
+
+ ControllerRuntimeVersion string
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *GoMod) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = "go.mod"
+ }
+
+ f.TemplateBody = goModTemplate
+
+ f.IfExistsAction = machinery.OverwriteFile
+
+ return nil
+}
+
+const goModTemplate = `module {{ .Repo }}
+
+go 1.24.5
+
+require (
+ sigs.k8s.io/controller-runtime {{ .ControllerRuntimeVersion }}
+)
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/hack/boilerplate.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/hack/boilerplate.go
new file mode 100644
index 00000000000..b200fca0f90
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/hack/boilerplate.go
@@ -0,0 +1,122 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package hack
+
+import (
+ "fmt"
+ "path/filepath"
+ "time"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+// DefaultBoilerplatePath is the default path to the boilerplate file
+var DefaultBoilerplatePath = filepath.Join("hack", "boilerplate.go.txt")
+
+var _ machinery.Template = &Boilerplate{}
+
+// Boilerplate scaffolds a file that defines the common header for the rest of the files
+type Boilerplate struct {
+ machinery.TemplateMixin
+ machinery.BoilerplateMixin
+
+ // License is the License type to write
+ License string
+
+ // Licenses maps License types to their actual string
+ Licenses map[string]string
+
+ // Owner is the copyright owner - e.g. "The Kubernetes Authors"
+ Owner string
+
+ // Year is the copyright year
+ Year string
+}
+
+// Validate implements file.RequiresValidation
+func (f *Boilerplate) Validate() error {
+ if f.License != "" {
+ if _, foundKnown := knownLicenses[f.License]; !foundKnown {
+ if _, found := f.Licenses[f.License]; !found {
+ return fmt.Errorf("unknown specified license %s", f.License)
+ }
+ }
+ }
+ return nil
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *Boilerplate) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = DefaultBoilerplatePath
+ }
+
+ if f.License == "" {
+ f.License = "apache2"
+ }
+
+ if f.Licenses == nil {
+ f.Licenses = make(map[string]string, len(knownLicenses))
+ }
+
+ for key, value := range knownLicenses {
+ if _, hasLicense := f.Licenses[key]; !hasLicense {
+ f.Licenses[key] = value
+ }
+ }
+
+ if f.Year == "" {
+ f.Year = fmt.Sprintf("%v", time.Now().Year())
+ }
+
+ // Boilerplate given
+ if len(f.Boilerplate) > 0 {
+ f.TemplateBody = f.Boilerplate
+ return nil
+ }
+
+ f.TemplateBody = boilerplateTemplate
+
+ return nil
+}
+
+const boilerplateTemplate = `/*
+{{ if .Owner -}}
+Copyright {{ .Year }} {{ .Owner }}.
+{{- else -}}
+Copyright {{ .Year }}.
+{{- end }}
+{{ index .Licenses .License }}*/`
+
+var knownLicenses = map[string]string{
+ "apache2": apache2,
+ "copyright": "",
+}
+
+const apache2 = `
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/makefile.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/makefile.go
new file mode 100644
index 00000000000..4ee5fbc7ff7
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/makefile.go
@@ -0,0 +1,330 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package templates
+
+import (
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Makefile{}
+
+// Makefile scaffolds a file that defines project management CLI commands
+type Makefile struct {
+ machinery.TemplateMixin
+ machinery.ProjectNameMixin
+
+ // Image is controller manager image name
+ Image string
+ // BoilerplatePath is the path to the boilerplate file
+ BoilerplatePath string
+ // Controller tools version to use in the project
+ ControllerToolsVersion string
+ // Kustomize version to use in the project
+ KustomizeVersion string
+ // golangci-lint version to use in the project
+ GolangciLintVersion string
+ // ControllerRuntimeVersion version to be used to download the envtest setup script
+ ControllerRuntimeVersion string
+ // EnvtestVersion store the name of the verions to be used to install setup-envtest
+ EnvtestVersion string
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *Makefile) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = "Makefile"
+ }
+
+ f.TemplateBody = makefileTemplate
+
+ f.IfExistsAction = machinery.Error
+
+ if f.Image == "" {
+ f.Image = "controller:latest"
+ }
+
+ // TODO: Current workaround for setup-envtest compatibility
+ // Due to past instances where controller-runtime maintainers released
+ // versions without corresponding branches, directly relying on branches
+ // poses a risk of breaking the Kubebuilder chain. Such practices may
+ // change over time, potentially leading to compatibility issues. This
+ // approach, although not ideal, remains the best solution for ensuring
+ // compatibility with controller-runtime releases as of now. For more
+ // details on the quest for a more robust solution, refer to the issue
+ // raised in the controller-runtime repository: https://github.com/kubernetes-sigs/controller-runtime/issues/2744
+ if f.EnvtestVersion == "" {
+ f.EnvtestVersion = "latest"
+ }
+ return nil
+}
+
+//nolint:lll
+const makefileTemplate = `# Image URL to use all building/pushing image targets
+IMG ?= {{ .Image }}
+
+# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
+ifeq (,$(shell go env GOBIN))
+GOBIN=$(shell go env GOPATH)/bin
+else
+GOBIN=$(shell go env GOBIN)
+endif
+
+# CONTAINER_TOOL defines the container tool to be used for building images.
+# Be aware that the target commands are only tested with Docker which is
+# scaffolded by default. However, you might want to replace it to use other
+# tools. (i.e. podman)
+CONTAINER_TOOL ?= docker
+
+# Setting SHELL to bash allows bash commands to be executed by recipes.
+# Options are set to exit when a recipe line exits non-zero or a piped command fails.
+SHELL = /usr/bin/env bash -o pipefail
+.SHELLFLAGS = -ec
+
+.PHONY: all
+all: build
+
+##@ General
+
+# The help target prints out all targets with their descriptions organized
+# beneath their categories. The categories are represented by '##@' and the
+# target descriptions by '##'. The awk command is responsible for reading the
+# entire set of makefiles included in this invocation, looking for lines of the
+# file as xyz: ## something, and then pretty-format the target and help. Then,
+# if there's a line with ##@ something, that gets pretty-printed as a category.
+# More info on the usage of ANSI control characters for terminal formatting:
+# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
+# More info on the awk command:
+# http://linuxcommand.org/lc3_adv_awk.php
+
+.PHONY: help
+help: ## Display this help.
+ @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
+
+##@ Development
+
+.PHONY: manifests
+manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
+ "$(CONTROLLER_GEN)" rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
+
+.PHONY: generate
+generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
+ {{ if .BoilerplatePath -}}
+ "$(CONTROLLER_GEN)" object:headerFile={{printf "%q" .BoilerplatePath}} paths="./..."
+ {{- else -}}
+ "$(CONTROLLER_GEN)" object paths="./..."
+ {{- end }}
+
+.PHONY: fmt
+fmt: ## Run go fmt against code.
+ go fmt ./...
+
+.PHONY: vet
+vet: ## Run go vet against code.
+ go vet ./...
+
+.PHONY: test
+test: manifests generate fmt vet setup-envtest ## Run tests.
+ KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out
+
+# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
+# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
+# CertManager is installed by default; skip with:
+# - CERT_MANAGER_INSTALL_SKIP=true
+KIND_CLUSTER ?= {{ .ProjectName }}-test-e2e
+
+.PHONY: setup-test-e2e
+setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist
+ @command -v $(KIND) >/dev/null 2>&1 || { \
+ echo "Kind is not installed. Please install Kind manually."; \
+ exit 1; \
+ }
+ @case "$$($(KIND) get clusters)" in \
+ *"$(KIND_CLUSTER)"*) \
+ echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \
+ *) \
+ echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \
+ $(KIND) create cluster --name $(KIND_CLUSTER) ;; \
+ esac
+
+.PHONY: test-e2e
+test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind.
+ KIND=$(KIND) KIND_CLUSTER=$(KIND_CLUSTER) go test -tags=e2e ./test/e2e/ -v -ginkgo.v
+ $(MAKE) cleanup-test-e2e
+
+.PHONY: cleanup-test-e2e
+cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests
+ @$(KIND) delete cluster --name $(KIND_CLUSTER)
+
+.PHONY: lint
+lint: golangci-lint ## Run golangci-lint linter
+ "$(GOLANGCI_LINT)" run
+
+.PHONY: lint-fix
+lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
+ "$(GOLANGCI_LINT)" run --fix
+
+.PHONY: lint-config
+lint-config: golangci-lint ## Verify golangci-lint linter configuration
+ "$(GOLANGCI_LINT)" config verify
+
+##@ Build
+
+.PHONY: build
+build: manifests generate fmt vet ## Build manager binary.
+ go build -o bin/manager cmd/main.go
+
+.PHONY: run
+run: manifests generate fmt vet ## Run a controller from your host.
+ go run ./cmd/main.go
+
+# If you wish to build the manager image targeting other platforms you can use the --platform flag.
+# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
+# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
+.PHONY: docker-build
+docker-build: ## Build docker image with the manager.
+ $(CONTAINER_TOOL) build -t ${IMG} .
+
+.PHONY: docker-push
+docker-push: ## Push docker image with the manager.
+ $(CONTAINER_TOOL) push ${IMG}
+
+# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple
+# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
+# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/
+# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/
+# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail)
+# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option.
+PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
+.PHONY: docker-buildx
+docker-buildx: ## Build and push docker image for the manager for cross-platform support
+ # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
+ sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
+ - $(CONTAINER_TOOL) buildx create --name {{ .ProjectName }}-builder
+ $(CONTAINER_TOOL) buildx use {{ .ProjectName }}-builder
+ - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
+ - $(CONTAINER_TOOL) buildx rm {{ .ProjectName }}-builder
+ rm Dockerfile.cross
+
+.PHONY: build-installer
+build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment.
+ mkdir -p dist
+ cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
+ "$(KUSTOMIZE)" build config/default > dist/install.yaml
+
+##@ Deployment
+
+ifndef ignore-not-found
+ ignore-not-found = false
+endif
+
+.PHONY: install
+install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
+ @out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
+ if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" apply -f -; else echo "No CRDs to install; skipping."; fi
+
+.PHONY: uninstall
+uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
+ @out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
+ if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi
+
+.PHONY: deploy
+deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
+ cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
+ "$(KUSTOMIZE)" build config/default | "$(KUBECTL)" apply -f -
+
+.PHONY: undeploy
+undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
+ "$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -
+
+##@ Dependencies
+
+## Location to install dependencies to
+LOCALBIN ?= $(shell pwd)/bin
+$(LOCALBIN):
+ mkdir -p "$(LOCALBIN)"
+
+## Tool Binaries
+KUBECTL ?= kubectl
+KIND ?= kind
+KUSTOMIZE ?= $(LOCALBIN)/kustomize
+CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
+ENVTEST ?= $(LOCALBIN)/setup-envtest
+GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
+
+## Tool Versions
+KUSTOMIZE_VERSION ?= {{ .KustomizeVersion }}
+CONTROLLER_TOOLS_VERSION ?= {{ .ControllerToolsVersion }}
+
+#ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20)
+ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \
+ [ -n "$$v" ] || { echo "Set ENVTEST_VERSION manually (controller-runtime replace has no tag)" >&2; exit 1; }; \
+ printf '%s\n' "$$v" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/release-\1.\2/')
+
+#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
+ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \
+ [ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \
+ printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/')
+
+GOLANGCI_LINT_VERSION ?= {{ .GolangciLintVersion }}
+.PHONY: kustomize
+kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
+$(KUSTOMIZE): $(LOCALBIN)
+ $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))
+
+.PHONY: controller-gen
+controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
+$(CONTROLLER_GEN): $(LOCALBIN)
+ $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))
+
+.PHONY: setup-envtest
+setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory.
+ @echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
+ @"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \
+ echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
+ exit 1; \
+ }
+
+.PHONY: envtest
+envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
+$(ENVTEST): $(LOCALBIN)
+ $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))
+
+.PHONY: golangci-lint
+golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
+$(GOLANGCI_LINT): $(LOCALBIN)
+ $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
+
+# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
+# $1 - target path with name of binary
+# $2 - package url which can be installed
+# $3 - specific version of package
+define go-install-tool
+@[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \
+set -e; \
+package=$(2)@$(3) ;\
+echo "Downloading $${package}" ;\
+rm -f "$(1)" ;\
+GOBIN="$(LOCALBIN)" go install $${package} ;\
+mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\
+} ;\
+ln -sf "$$(realpath "$(1)-$(3)")" "$(1)"
+endef
+
+define gomodver
+$(shell go list -m -f '{{"{{"}}if .Replace{{"}}"}}{{"{{"}}.Replace.Version{{"}}"}}{{"{{"}}else{{"}}"}}{{"{{"}}.Version{{"}}"}}{{"{{"}}end{{"}}"}}' $(1) 2>/dev/null)
+endef
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/readme.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/readme.go
new file mode 100644
index 00000000000..5511c9be69a
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/readme.go
@@ -0,0 +1,172 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package templates
+
+import (
+ "fmt"
+ "strings"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Readme{}
+
+// Readme scaffolds a README.md file
+type Readme struct {
+ machinery.TemplateMixin
+ machinery.BoilerplateMixin
+ machinery.ProjectNameMixin
+
+ License string
+
+ // CommandName stores the name of the bin used
+ CommandName string
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *Readme) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = "README.md"
+ }
+
+ f.License = strings.Replace(
+ strings.Replace(f.Boilerplate, "/*", "", 1),
+ "*/", "", 1)
+
+ f.TemplateBody = fmt.Sprintf(readmeFileTemplate,
+ codeFence("make docker-build docker-push IMG=/{{ .ProjectName }}:tag"),
+ codeFence("make install"),
+ codeFence("make deploy IMG=/{{ .ProjectName }}:tag"),
+ codeFence("kubectl apply -k config/samples/"),
+ codeFence("kubectl delete -k config/samples/"),
+ codeFence("make uninstall"),
+ codeFence("make undeploy"),
+ codeFence("make build-installer IMG=/{{ .ProjectName }}:tag"),
+ codeFence("kubectl apply -f https://raw.githubusercontent.com//{{ .ProjectName }}/"+
+ "/dist/install.yaml"),
+ codeFence(fmt.Sprintf("%s edit --plugins=helm/v2-alpha", f.CommandName)),
+ )
+
+ return nil
+}
+
+const readmeFileTemplate = `# {{ .ProjectName }}
+// TODO(user): Add simple overview of use/purpose
+
+## Description
+// TODO(user): An in-depth paragraph about your project and overview of use
+
+## Getting Started
+
+### Prerequisites
+- go version v1.24.0+
+- docker version 17.03+.
+- kubectl version v1.11.3+.
+- Access to a Kubernetes v1.11.3+ cluster.
+
+### To Deploy on the cluster
+**Build and push your image to the location specified by ` + "`IMG`" + `:**
+
+%s
+
+**NOTE:** This image ought to be published in the personal registry you specified.
+And it is required to have access to pull the image from the working environment.
+Make sure you have the proper permission to the registry if the above commands don’t work.
+
+**Install the CRDs into the cluster:**
+
+%s
+
+**Deploy the Manager to the cluster with the image specified by ` + "`IMG`" + `:**
+
+%s
+
+> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin
+privileges or be logged in as admin.
+
+**Create instances of your solution**
+You can apply the samples (examples) from the config/sample:
+
+%s
+
+>**NOTE**: Ensure that the samples has default values to test it out.
+
+### To Uninstall
+**Delete the instances (CRs) from the cluster:**
+
+%s
+
+**Delete the APIs(CRDs) from the cluster:**
+
+%s
+
+**UnDeploy the controller from the cluster:**
+
+%s
+
+## Project Distribution
+
+Following the options to release and provide this solution to the users.
+
+### By providing a bundle with all YAML files
+
+1. Build the installer for the image built and published in the registry:
+
+%s
+
+**NOTE:** The makefile target mentioned above generates an 'install.yaml'
+file in the dist directory. This file contains all the resources built
+with Kustomize, which are necessary to install this project without its
+dependencies.
+
+2. Using the installer
+
+Users can just run 'kubectl apply -f ' to install
+the project, i.e.:
+
+%s
+
+### By providing a Helm Chart
+
+1. Build the chart using the optional helm plugin
+
+%s
+
+2. See that a chart was generated under 'dist/chart', and users
+can obtain this solution from there.
+
+**NOTE:** If you change the project, you need to update the Helm Chart
+using the same command above to sync the latest changes. Furthermore,
+if you create webhooks, you need to use the above command with
+the '--force' flag and manually ensure that any custom configuration
+previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml'
+is manually re-applied afterwards.
+
+## Contributing
+// TODO(user): Add detailed information on how you would like others to contribute to this project
+
+**NOTE:** Run ` + "`make help`" + ` for more information on all potential ` + "`make`" + ` targets
+
+More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)
+
+## License
+{{ .License }}
+`
+
+func codeFence(code string) string {
+ return "```sh" + "\n" + code + "\n" + "```"
+}
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/test/e2e/suite.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/test/e2e/suite.go
new file mode 100644
index 00000000000..6923e7ad87f
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/test/e2e/suite.go
@@ -0,0 +1,121 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package e2e
+
+import (
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &SuiteTest{}
+
+// SuiteTest scaffolds the files for the e2e tests
+type SuiteTest struct {
+ machinery.TemplateMixin
+ machinery.BoilerplateMixin
+ machinery.RepositoryMixin
+ machinery.ProjectNameMixin
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *SuiteTest) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = "test/e2e/e2e_suite_test.go"
+ }
+
+ f.TemplateBody = suiteTestTemplate
+ return nil
+}
+
+var suiteTestTemplate = `//go:build e2e
+// +build e2e
+
+{{ .Boilerplate }}
+
+package e2e
+
+import (
+ "fmt"
+ "testing"
+ "os"
+ "os/exec"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "{{ .Repo }}/test/utils"
+)
+
+var (
+ // Optional Environment Variables:
+ // - CERT_MANAGER_INSTALL_SKIP=true: Skips CertManager installation during test setup.
+ // These variables are useful if CertManager is already installed, avoiding
+ // re-installation and conflicts.
+ skipCertManagerInstall = os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true"
+ // isCertManagerAlreadyInstalled will be set true when CertManager CRDs be found on the cluster
+ isCertManagerAlreadyInstalled = false
+
+ // projectImage is the name of the image which will be build and loaded
+ // with the code source changes to be tested.
+ projectImage = "example.com/{{ .ProjectName }}:v0.0.1"
+)
+
+// TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated,
+// temporary environment to validate project changes with the purpose of being used in CI jobs.
+// The default setup requires Kind, builds/loads the Manager Docker image locally, and installs
+// CertManager.
+func TestE2E(t *testing.T) {
+ RegisterFailHandler(Fail)
+ _, _ = fmt.Fprintf(GinkgoWriter, "Starting {{ .ProjectName }} integration test suite\n")
+ RunSpecs(t, "e2e suite")
+}
+
+var _ = BeforeSuite(func() {
+ By("building the manager(Operator) image")
+ cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectImage))
+ _, err := utils.Run(cmd)
+ ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager(Operator) image")
+
+ // TODO(user): If you want to change the e2e test vendor from Kind, ensure the image is
+ // built and available before running the tests. Also, remove the following block.
+ By("loading the manager(Operator) image on Kind")
+ err = utils.LoadImageToKindClusterWithName(projectImage)
+ ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager(Operator) image into Kind")
+
+ // The tests-e2e are intended to run on a temporary cluster that is created and destroyed for testing.
+ // To prevent errors when tests run in environments with CertManager already installed,
+ // we check for its presence before execution.
+ // Setup CertManager before the suite if not skipped and if not already installed
+ if !skipCertManagerInstall {
+ By("checking if cert manager is installed already")
+ isCertManagerAlreadyInstalled = utils.IsCertManagerCRDsInstalled()
+ if !isCertManagerAlreadyInstalled {
+ _, _ = fmt.Fprintf(GinkgoWriter, "Installing CertManager...\n")
+ Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager")
+ } else {
+ _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: CertManager is already installed. Skipping installation...\n")
+ }
+ }
+})
+
+var _ = AfterSuite(func() {
+ // Teardown CertManager after the suite if not skipped and if it was not already installed
+ if !skipCertManagerInstall && !isCertManagerAlreadyInstalled {
+ _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling CertManager...\n")
+ utils.UninstallCertManager()
+ }
+})
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/test/e2e/test.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/test/e2e/test.go
new file mode 100644
index 00000000000..a641e46e92e
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/test/e2e/test.go
@@ -0,0 +1,518 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package e2e
+
+import (
+ "bytes"
+ "fmt"
+ log "log/slog"
+ "os"
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var (
+ _ machinery.Template = &Test{}
+ _ machinery.Inserter = &WebhookTestUpdater{}
+)
+
+const webhookChecksMarker = "e2e-webhooks-checks"
+
+// Test defines the basic setup for the e2e test
+type Test struct {
+ machinery.TemplateMixin
+ machinery.BoilerplateMixin
+ machinery.RepositoryMixin
+ machinery.ProjectNameMixin
+}
+
+// SetTemplateDefaults set defaults for this template
+func (f *Test) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join("test", "e2e", "e2e_test.go")
+ }
+
+ // This is where the template body is defined with markers
+ f.TemplateBody = testCodeTemplate
+
+ return nil
+}
+
+// WebhookTestUpdater updates e2e_test.go to insert additional webhook validation tests
+type WebhookTestUpdater struct {
+ machinery.RepositoryMixin
+ machinery.ProjectNameMixin
+ machinery.ResourceMixin
+ WireWebhook bool
+}
+
+// GetPath implements file.Builder
+func (*WebhookTestUpdater) GetPath() string {
+ return filepath.Join("test", "e2e", "e2e_test.go")
+}
+
+// GetIfExistsAction implements file.Builder
+func (*WebhookTestUpdater) GetIfExistsAction() machinery.IfExistsAction {
+ return machinery.OverwriteFile // Ensures only the marker is replaced
+}
+
+// GetMarkers implements file.Inserter
+func (f *WebhookTestUpdater) GetMarkers() []machinery.Marker {
+ return []machinery.Marker{
+ machinery.NewMarkerFor(f.GetPath(), webhookChecksMarker),
+ }
+}
+
+// GetCodeFragments implements file.Inserter
+func (f *WebhookTestUpdater) GetCodeFragments() machinery.CodeFragmentsMap {
+ if !f.WireWebhook {
+ return nil
+ }
+
+ filePath := f.GetPath()
+
+ content, err := os.ReadFile(filePath)
+ if err != nil {
+ log.Warn("Unable to read file", "file", filePath, "error", err)
+ log.Warn("Webhook test code injection will be skipped for this file.")
+ log.Warn("This typically occurs when the file was removed and is missing.")
+ log.Warn("If you intend to scaffold webhook tests, ensure the file and its markers exist.")
+ return nil
+ }
+
+ codeFragments := machinery.CodeFragmentsMap{}
+ markers := f.GetMarkers()
+
+ for _, marker := range markers {
+ if !bytes.Contains(content, []byte(marker.String())) {
+ log.Warn("Marker not found in file, skipping webhook test code injection",
+ "marker", marker.String(),
+ "file_path", filePath)
+ continue // skip this marker
+ }
+
+ var fragments []string
+ fragments = append(fragments, webhookChecksFragment)
+
+ if f.Resource != nil && f.Resource.HasDefaultingWebhook() {
+ mutatingWebhookCode := fmt.Sprintf(mutatingWebhookChecksFragment, f.ProjectName)
+ fragments = append(fragments, mutatingWebhookCode)
+ }
+
+ if f.Resource != nil && f.Resource.HasValidationWebhook() {
+ validatingWebhookCode := fmt.Sprintf(validatingWebhookChecksFragment, f.ProjectName)
+ fragments = append(fragments, validatingWebhookCode)
+ }
+
+ if f.Resource != nil && f.Resource.HasConversionWebhook() {
+ conversionWebhookCode := fmt.Sprintf(
+ conversionWebhookChecksFragment,
+ f.Resource.Kind,
+ f.Resource.Plural+"."+f.Resource.Group+"."+f.Resource.Domain,
+ )
+ fragments = append(fragments, conversionWebhookCode)
+ }
+
+ codeFragments[marker] = fragments
+ }
+
+ if len(codeFragments) == 0 {
+ return nil
+ }
+
+ return codeFragments
+}
+
+const webhookChecksFragment = `It("should provisioned cert-manager", func() {
+ By("validating that cert-manager has the certificate Secret")
+ verifyCertManager := func(g Gomega) {
+ cmd := exec.Command("kubectl", "get", "secrets", "webhook-server-cert", "-n", namespace)
+ _, err := utils.Run(cmd)
+ g.Expect(err).NotTo(HaveOccurred())
+ }
+ Eventually(verifyCertManager).Should(Succeed())
+})
+
+`
+
+const mutatingWebhookChecksFragment = `It("should have CA injection for mutating webhooks", func() {
+ By("checking CA injection for mutating webhooks")
+ verifyCAInjection := func(g Gomega) {
+ cmd := exec.Command("kubectl", "get",
+ "mutatingwebhookconfigurations.admissionregistration.k8s.io",
+ "%s-mutating-webhook-configuration",
+ "-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}")
+ mwhOutput, err := utils.Run(cmd)
+ g.Expect(err).NotTo(HaveOccurred())
+ g.Expect(len(mwhOutput)).To(BeNumerically(">", 10))
+ }
+ Eventually(verifyCAInjection).Should(Succeed())
+})
+
+`
+
+const validatingWebhookChecksFragment = `It("should have CA injection for validating webhooks", func() {
+ By("checking CA injection for validating webhooks")
+ verifyCAInjection := func(g Gomega) {
+ cmd := exec.Command("kubectl", "get",
+ "validatingwebhookconfigurations.admissionregistration.k8s.io",
+ "%s-validating-webhook-configuration",
+ "-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}")
+ vwhOutput, err := utils.Run(cmd)
+ g.Expect(err).NotTo(HaveOccurred())
+ g.Expect(len(vwhOutput)).To(BeNumerically(">", 10))
+ }
+ Eventually(verifyCAInjection).Should(Succeed())
+})
+
+`
+
+const conversionWebhookChecksFragment = `It("should have CA injection for %[1]s conversion webhook", func() {
+ By("checking CA injection for %[1]s conversion webhook")
+ verifyCAInjection := func(g Gomega) {
+ cmd := exec.Command("kubectl", "get",
+ "customresourcedefinitions.apiextensions.k8s.io",
+ "%[2]s",
+ "-o", "go-template={{ .spec.conversion.webhook.clientConfig.caBundle }}")
+ vwhOutput, err := utils.Run(cmd)
+ g.Expect(err).NotTo(HaveOccurred())
+ g.Expect(len(vwhOutput)).To(BeNumerically(">", 10))
+ }
+ Eventually(verifyCAInjection).Should(Succeed())
+})
+
+`
+
+var testCodeTemplate = `//go:build e2e
+// +build e2e
+
+{{ .Boilerplate }}
+
+package e2e
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "time"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "{{ .Repo }}/test/utils"
+)
+
+// namespace where the project is deployed in
+const namespace = "{{ .ProjectName }}-system"
+// serviceAccountName created for the project
+const serviceAccountName = "{{ .ProjectName }}-controller-manager"
+// metricsServiceName is the name of the metrics service of the project
+const metricsServiceName = "{{ .ProjectName }}-controller-manager-metrics-service"
+// metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data
+const metricsRoleBindingName = "{{ .ProjectName }}-metrics-binding"
+
+var _ = Describe("Manager", Ordered, func() {
+ var controllerPodName string
+
+ // Before running the tests, set up the environment by creating the namespace,
+ // enforce the restricted security policy to the namespace, installing CRDs,
+ // and deploying the controller.
+ BeforeAll(func() {
+ By("creating manager namespace")
+ cmd := exec.Command("kubectl", "create", "ns", namespace)
+ _, err := utils.Run(cmd)
+ Expect(err).NotTo(HaveOccurred(), "Failed to create namespace")
+
+ By("labeling the namespace to enforce the restricted security policy")
+ cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace,
+ "pod-security.kubernetes.io/enforce=restricted")
+ _, err = utils.Run(cmd)
+ Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy")
+
+ By("installing CRDs")
+ cmd = exec.Command("make", "install")
+ _, err = utils.Run(cmd)
+ Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs")
+
+ By("deploying the controller-manager")
+ cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectImage))
+ _, err = utils.Run(cmd)
+ Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager")
+ })
+
+ // After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs,
+ // and deleting the namespace.
+ AfterAll(func() {
+ By("cleaning up the curl pod for metrics")
+ cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace)
+ _, _ = utils.Run(cmd)
+
+ By("undeploying the controller-manager")
+ cmd = exec.Command("make", "undeploy")
+ _, _ = utils.Run(cmd)
+
+ By("uninstalling CRDs")
+ cmd = exec.Command("make", "uninstall")
+ _, _ = utils.Run(cmd)
+
+ By("removing manager namespace")
+ cmd = exec.Command("kubectl", "delete", "ns", namespace)
+ _, _ = utils.Run(cmd)
+ })
+
+ // After each test, check for failures and collect logs, events,
+ // and pod descriptions for debugging.
+ AfterEach(func() {
+ specReport := CurrentSpecReport()
+ if specReport.Failed() {
+ By("Fetching controller manager pod logs")
+ cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
+ controllerLogs, err := utils.Run(cmd)
+ if err == nil {
+ _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs)
+ } else {
+ _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err)
+ }
+
+ By("Fetching Kubernetes events")
+ cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp")
+ eventsOutput, err := utils.Run(cmd)
+ if err == nil {
+ _, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput)
+ } else {
+ _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err)
+ }
+
+ By("Fetching curl-metrics logs")
+ cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
+ metricsOutput, err := utils.Run(cmd)
+ if err == nil {
+ _, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput)
+ } else {
+ _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err)
+ }
+
+ By("Fetching controller manager pod description")
+ cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace)
+ podDescription, err := utils.Run(cmd)
+ if err == nil {
+ fmt.Println("Pod description:\n", podDescription)
+ } else {
+ fmt.Println("Failed to describe controller pod")
+ }
+ }
+ })
+
+ SetDefaultEventuallyTimeout(2 * time.Minute)
+ SetDefaultEventuallyPollingInterval(time.Second)
+
+ Context("Manager", func() {
+ It("should run successfully", func() {
+ By("validating that the controller-manager pod is running as expected")
+ verifyControllerUp := func(g Gomega) {
+ // Get the name of the controller-manager pod
+ cmd := exec.Command("kubectl", "get",
+ "pods", "-l", "control-plane=controller-manager",
+ "-o", "go-template={{"{{"}} range .items {{"}}"}}" +
+ "{{"{{"}} if not .metadata.deletionTimestamp {{"}}"}}" +
+ "{{"{{"}} .metadata.name {{"}}"}}"+
+ "{{"{{"}} \"\\n\" {{"}}"}}{{"{{"}} end {{"}}"}}{{"{{"}} end {{"}}"}}",
+ "-n", namespace,
+ )
+
+ podOutput, err := utils.Run(cmd)
+ g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information")
+ podNames := utils.GetNonEmptyLines(podOutput)
+ g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running")
+ controllerPodName = podNames[0]
+ g.Expect(controllerPodName).To(ContainSubstring("controller-manager"))
+
+ // Validate the pod's status
+ cmd = exec.Command("kubectl", "get",
+ "pods", controllerPodName, "-o", "jsonpath={.status.phase}",
+ "-n", namespace,
+ )
+ output, err := utils.Run(cmd)
+ g.Expect(err).NotTo(HaveOccurred())
+ g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status")
+ }
+ Eventually(verifyControllerUp).Should(Succeed())
+ })
+
+ It("should ensure the metrics endpoint is serving metrics", func() {
+ By("creating a ClusterRoleBinding for the service account to allow access to metrics")
+ cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName,
+ "--clusterrole={{ .ProjectName}}-metrics-reader",
+ fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName),
+ )
+ _, err := utils.Run(cmd)
+ Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding")
+
+ By("validating that the metrics service is available")
+ cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace)
+ _, err = utils.Run(cmd)
+ Expect(err).NotTo(HaveOccurred(), "Metrics service should exist")
+
+ By("getting the service account token")
+ token, err := serviceAccountToken()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(token).NotTo(BeEmpty())
+
+ By("waiting for the metrics endpoint to be ready")
+ verifyMetricsEndpointReady := func(g Gomega) {
+ cmd := exec.Command("kubectl", "get", "endpoints", metricsServiceName, "-n", namespace)
+ output, err := utils.Run(cmd)
+ g.Expect(err).NotTo(HaveOccurred())
+ g.Expect(output).To(ContainSubstring("8443"), "Metrics endpoint is not ready")
+ }
+ Eventually(verifyMetricsEndpointReady).Should(Succeed())
+
+ By("verifying that the controller manager is serving the metrics server")
+ verifyMetricsServerStarted := func(g Gomega) {
+ cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
+ output, err := utils.Run(cmd)
+ g.Expect(err).NotTo(HaveOccurred())
+ g.Expect(output).To(ContainSubstring("controller-runtime.metrics\tServing metrics server"),
+ "Metrics server not yet started")
+ }
+ Eventually(verifyMetricsServerStarted).Should(Succeed())
+
+ By("creating the curl-metrics pod to access the metrics endpoint")
+ cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never",
+ "--namespace", namespace,
+ "--image=curlimages/curl:latest",
+ "--overrides",
+ fmt.Sprintf(` + "`" + `{
+ "spec": {
+ "containers": [{
+ "name": "curl",
+ "image": "curlimages/curl:latest",
+ "command": ["/bin/sh", "-c"],
+ "args": ["curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics"],
+ "securityContext": {
+ "readOnlyRootFilesystem": true,
+ "allowPrivilegeEscalation": false,
+ "capabilities": {
+ "drop": ["ALL"]
+ },
+ "runAsNonRoot": true,
+ "runAsUser": 1000,
+ "seccompProfile": {
+ "type": "RuntimeDefault"
+ }
+ }
+ }],
+ "serviceAccountName": "%s"
+ }
+ }` + "`" + `, token, metricsServiceName, namespace, serviceAccountName))
+ _, err = utils.Run(cmd)
+ Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod")
+
+ By("waiting for the curl-metrics pod to complete.")
+ verifyCurlUp := func(g Gomega) {
+ cmd := exec.Command("kubectl", "get", "pods", "curl-metrics",
+ "-o", "jsonpath={.status.phase}",
+ "-n", namespace)
+ output, err := utils.Run(cmd)
+ g.Expect(err).NotTo(HaveOccurred())
+ g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status")
+ }
+ Eventually(verifyCurlUp, 5 * time.Minute).Should(Succeed())
+
+ By("getting the metrics by checking curl-metrics logs")
+ verifyMetricsAvailable := func(g Gomega) {
+ metricsOutput, err := getMetricsOutput()
+ g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
+ g.Expect(metricsOutput).NotTo(BeEmpty())
+ g.Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK"))
+ }
+ Eventually(verifyMetricsAvailable, 2*time.Minute).Should(Succeed())
+ })
+
+ // +kubebuilder:scaffold:e2e-webhooks-checks
+
+ // TODO: Customize the e2e test suite with scenarios specific to your project.
+ // Consider applying sample/CR(s) and check their status and/or verifying
+ // the reconciliation by using the metrics, i.e.:
+ // metricsOutput, err := getMetricsOutput()
+ // Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
+ // Expect(metricsOutput).To(ContainSubstring(
+ // fmt.Sprintf(` + "`controller_runtime_reconcile_total{controller=\"%s\",result=\"success\"} 1`" + `,
+ // strings.ToLower(),
+ // ))
+ })
+})
+
+// serviceAccountToken returns a token for the specified service account in the given namespace.
+// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
+// and parsing the resulting token from the API response.
+func serviceAccountToken() (string, error) {
+ const tokenRequestRawString = ` + "`" + `{
+ "apiVersion": "authentication.k8s.io/v1",
+ "kind": "TokenRequest"
+ }` + "`" + `
+
+ // Temporary file to store the token request
+ secretName := fmt.Sprintf("%s-token-request", serviceAccountName)
+ tokenRequestFile := filepath.Join("/tmp", secretName)
+ err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644))
+ if err != nil {
+ return "", err
+ }
+
+ var out string
+ verifyTokenCreation := func(g Gomega) {
+ // Execute kubectl command to create the token
+ cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf(
+ "/api/v1/namespaces/%s/serviceaccounts/%s/token",
+ namespace,
+ serviceAccountName,
+ ), "-f", tokenRequestFile)
+
+ output, err := cmd.CombinedOutput()
+ g.Expect(err).NotTo(HaveOccurred())
+
+ // Parse the JSON output to extract the token
+ var token tokenRequest
+ err = json.Unmarshal(output, &token)
+ g.Expect(err).NotTo(HaveOccurred())
+
+ out = token.Status.Token
+ }
+ Eventually(verifyTokenCreation).Should(Succeed())
+
+ return out, err
+}
+
+// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint.
+func getMetricsOutput() (string, error) {
+ By("getting the curl-metrics logs")
+ cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
+ return utils.Run(cmd)
+}
+
+// tokenRequest is a simplified representation of the Kubernetes TokenRequest API response,
+// containing only the token field that we need to extract.
+type tokenRequest struct {
+ Status struct {
+ Token string ` + "`json:\"token\"`" + `
+ } ` + "`json:\"status\"`" + `
+}
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/test/utils/utils.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/test/utils/utils.go
new file mode 100644
index 00000000000..44866e5c3cb
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/test/utils/utils.go
@@ -0,0 +1,255 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package utils
+
+import (
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Utils{}
+
+// Utils define the template for the utils file
+type Utils struct {
+ machinery.TemplateMixin
+ machinery.BoilerplateMixin
+ machinery.ProjectNameMixin
+}
+
+// SetTemplateDefaults set the defaults for its template
+func (f *Utils) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = "test/utils/utils.go"
+ }
+
+ f.TemplateBody = utilsTemplate
+
+ return nil
+}
+
+var utilsTemplate = `{{ .Boilerplate }}
+
+package utils
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "os"
+ "os/exec"
+ "strings"
+
+ . "github.com/onsi/ginkgo/v2" // nolint:revive,staticcheck
+)
+
+const (
+ certmanagerVersion = "v1.19.1"
+ certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml"
+
+ defaultKindBinary = "kind"
+ defaultKindCluster = "kind"
+)
+
+func warnError(err error) {
+ _, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err)
+}
+
+// Run executes the provided command within this context
+func Run(cmd *exec.Cmd) (string, error) {
+ dir, _ := GetProjectDir()
+ cmd.Dir = dir
+
+ if err := os.Chdir(cmd.Dir); err != nil {
+ _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %q\n", err)
+ }
+
+ cmd.Env = append(os.Environ(), "GO111MODULE=on")
+ command := strings.Join(cmd.Args, " ")
+ _, _ = fmt.Fprintf(GinkgoWriter, "running: %q\n", command)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return string(output), fmt.Errorf("%q failed with error %q: %w", command, string(output), err)
+ }
+
+ return string(output), nil
+}
+
+// UninstallCertManager uninstalls the cert manager
+func UninstallCertManager() {
+ url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
+ cmd := exec.Command("kubectl", "delete", "-f", url)
+ if _, err := Run(cmd); err != nil {
+ warnError(err)
+ }
+
+ // Delete leftover leases in kube-system (not cleaned by default)
+ kubeSystemLeases := []string{
+ "cert-manager-cainjector-leader-election",
+ "cert-manager-controller",
+ }
+ for _, lease := range kubeSystemLeases {
+ cmd = exec.Command("kubectl", "delete", "lease", lease,
+ "-n", "kube-system", "--ignore-not-found", "--force", "--grace-period=0")
+ if _, err := Run(cmd); err != nil {
+ warnError(err)
+ }
+ }
+}
+
+// InstallCertManager installs the cert manager bundle.
+func InstallCertManager() error {
+ url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
+ cmd := exec.Command("kubectl", "apply", "-f", url)
+ if _, err := Run(cmd); err != nil {
+ return err
+ }
+ // Wait for cert-manager-webhook to be ready, which can take time if cert-manager
+ // was re-installed after uninstalling on a cluster.
+ cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook",
+ "--for", "condition=Available",
+ "--namespace", "cert-manager",
+ "--timeout", "5m",
+ )
+
+ _, err := Run(cmd)
+ return err
+}
+
+// IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed
+// by verifying the existence of key CRDs related to Cert Manager.
+func IsCertManagerCRDsInstalled() bool {
+ // List of common Cert Manager CRDs
+ certManagerCRDs := []string{
+ "certificates.cert-manager.io",
+ "issuers.cert-manager.io",
+ "clusterissuers.cert-manager.io",
+ "certificaterequests.cert-manager.io",
+ "orders.acme.cert-manager.io",
+ "challenges.acme.cert-manager.io",
+ }
+
+ // Execute the kubectl command to get all CRDs
+ cmd := exec.Command("kubectl", "get", "crds")
+ output, err := Run(cmd)
+ if err != nil {
+ return false
+ }
+
+ // Check if any of the Cert Manager CRDs are present
+ crdList := GetNonEmptyLines(output)
+ for _, crd := range certManagerCRDs {
+ for _, line := range crdList {
+ if strings.Contains(line, crd) {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+// LoadImageToKindClusterWithName loads a local docker image to the kind cluster
+func LoadImageToKindClusterWithName(name string) error {
+ cluster := defaultKindCluster
+ if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
+ cluster = v
+ }
+ kindOptions := []string{"load", "docker-image", name, "--name", cluster}
+ kindBinary := defaultKindBinary
+ if v, ok := os.LookupEnv("KIND"); ok {
+ kindBinary = v
+ }
+ cmd := exec.Command(kindBinary, kindOptions...)
+ _, err := Run(cmd)
+ return err
+}
+
+// GetNonEmptyLines converts given command output string into individual objects
+// according to line breakers, and ignores the empty elements in it.
+func GetNonEmptyLines(output string) []string {
+ var res []string
+ elements := strings.Split(output, "\n")
+ for _, element := range elements {
+ if element != "" {
+ res = append(res, element)
+ }
+ }
+
+ return res
+}
+
+// GetProjectDir will return the directory where the project is
+func GetProjectDir() (string, error) {
+ wd, err := os.Getwd()
+ if err != nil {
+ return wd, fmt.Errorf("failed to get current working directory: %w", err)
+ }
+ wd = strings.ReplaceAll(wd, "/test/e2e", "")
+ return wd, nil
+}
+
+// UncommentCode searches for target in the file and remove the comment prefix
+// of the target content. The target content may span multiple lines.
+func UncommentCode(filename, target, prefix string) error {
+ // false positive
+ // nolint:gosec
+ content, err := os.ReadFile(filename)
+ if err != nil {
+ return fmt.Errorf("failed to read file %q: %w", filename, err)
+ }
+ strContent := string(content)
+
+ idx := strings.Index(strContent, target)
+ if idx < 0 {
+ return fmt.Errorf("unable to find the code %q to be uncomment", target)
+ }
+
+ out := new(bytes.Buffer)
+ _, err = out.Write(content[:idx])
+ if err != nil {
+ return fmt.Errorf("failed to write to output: %w", err)
+ }
+
+ scanner := bufio.NewScanner(bytes.NewBufferString(target))
+ if !scanner.Scan() {
+ return nil
+ }
+ for {
+ if _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)); err != nil {
+ return fmt.Errorf("failed to write to output: %w", err)
+ }
+ // Avoid writing a newline in case the previous line was the last in target.
+ if !scanner.Scan() {
+ break
+ }
+ if _, err = out.WriteString("\n"); err != nil {
+ return fmt.Errorf("failed to write to output: %w", err)
+ }
+ }
+
+ if _, err = out.Write(content[idx+len(target):]); err != nil {
+ return fmt.Errorf("failed to write to output: %w", err)
+ }
+
+ // false positive
+ // nolint:gosec
+ if err = os.WriteFile(filename, out.Bytes(), 0644); err != nil {
+ return fmt.Errorf("failed to write file %q: %w", filename, err)
+ }
+
+ return nil
+}
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go
new file mode 100644
index 00000000000..26a0b0f0f61
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go
@@ -0,0 +1,266 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package webhooks
+
+import (
+ log "log/slog"
+ "path/filepath"
+ "strings"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Webhook{}
+
+// Webhook scaffolds the file that defines a webhook for a CRD or a builtin resource
+type Webhook struct {
+ machinery.TemplateMixin
+ machinery.MultiGroupMixin
+ machinery.BoilerplateMixin
+ machinery.ResourceMixin
+
+ // Is the Group domain for the Resource replacing '.' with '-'
+ QualifiedGroupWithDash string
+
+ // Define value for AdmissionReviewVersions marker
+ AdmissionReviewVersions string
+
+ Force bool
+
+ // Deprecated - The flag should be removed from go/v5
+ // IsLegacyPath indicates if webhooks should be scaffolded under the API.
+ // Webhooks are now decoupled from APIs based on controller-runtime updates and community feedback.
+ // This flag ensures backward compatibility by allowing scaffolding in the legacy/deprecated path.
+ IsLegacyPath bool
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *Webhook) SetTemplateDefaults() error {
+ if f.Path == "" {
+ // Deprecated: Remove me when remove go/v4
+ baseDir := "api"
+ if !f.IsLegacyPath {
+ baseDir = filepath.Join("internal", "webhook")
+ }
+
+ if f.MultiGroup && f.Resource.Group != "" {
+ f.Path = filepath.Join(baseDir, "%[group]", "%[version]", "%[kind]_webhook.go")
+ } else {
+ f.Path = filepath.Join(baseDir, "%[version]", "%[kind]_webhook.go")
+ }
+ }
+
+ f.Path = f.Resource.Replacer().Replace(f.Path)
+ log.Info(f.Path)
+
+ webhookTemplate := webhookTemplate
+ if f.Resource.HasDefaultingWebhook() {
+ webhookTemplate = webhookTemplate + defaultingWebhookTemplate
+ }
+ if f.Resource.HasValidationWebhook() {
+ webhookTemplate = webhookTemplate + validatingWebhookTemplate
+ }
+ f.TemplateBody = webhookTemplate
+
+ if f.Force {
+ f.IfExistsAction = machinery.OverwriteFile
+ } else {
+ f.IfExistsAction = machinery.Error
+ }
+
+ f.AdmissionReviewVersions = "v1"
+ f.QualifiedGroupWithDash = strings.ReplaceAll(f.Resource.QualifiedGroup(), ".", "-")
+
+ return nil
+}
+
+const (
+ webhookTemplate = `{{ .Boilerplate }}
+
+package {{ .Resource.Version }}
+
+import (
+ {{- if or .Resource.HasValidationWebhook .Resource.HasDefaultingWebhook }}
+ "context"
+ "fmt"
+ {{- end }}
+
+ ctrl "sigs.k8s.io/controller-runtime"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ {{- if or .Resource.HasValidationWebhook .Resource.HasDefaultingWebhook }}
+ "k8s.io/apimachinery/pkg/runtime"
+ "sigs.k8s.io/controller-runtime/pkg/webhook"
+ {{- end }}
+ {{- if .Resource.HasValidationWebhook }}
+ "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+ {{- end }}
+ {{ if not .IsLegacyPath -}}
+ {{ if not (isEmptyStr .Resource.Path) -}}
+ {{ .Resource.ImportAlias }} "{{ .Resource.Path }}"
+ {{- end }}
+ {{- end }}
+)
+
+// nolint:unused
+// log is for logging in this package.
+var {{ lower .Resource.Kind }}log = logf.Log.WithName("{{ lower .Resource.Kind }}-resource")
+
+{{- if .IsLegacyPath -}}
+// SetupWebhookWithManager will setup the manager to manage the webhooks.
+func (r *{{ .Resource.Kind }}) SetupWebhookWithManager(mgr ctrl.Manager) error {
+ return ctrl.NewWebhookManagedBy(mgr).
+ For(r).
+ {{- if .Resource.HasValidationWebhook }}
+ WithValidator(&{{ .Resource.Kind }}CustomValidator{}).
+ {{- end }}
+ {{- if .Resource.HasDefaultingWebhook }}
+ WithDefaulter(&{{ .Resource.Kind }}CustomDefaulter{}).
+ {{- end }}
+ Complete()
+}
+{{- else }}
+// Setup{{ .Resource.Kind }}WebhookWithManager registers the webhook for {{ .Resource.Kind }} in the manager.
+func Setup{{ .Resource.Kind }}WebhookWithManager(mgr ctrl.Manager) error {
+ return ctrl.NewWebhookManagedBy(mgr).
+ {{- if not (isEmptyStr .Resource.ImportAlias) -}}
+ For(&{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}).
+ {{- else -}}
+ For(&{{ .Resource.Kind }}{}).
+ {{- end }}
+ {{- if .Resource.HasValidationWebhook }}
+ WithValidator(&{{ .Resource.Kind }}CustomValidator{}).
+ {{- end }}
+ {{- if .Resource.HasDefaultingWebhook }}
+ WithDefaulter(&{{ .Resource.Kind }}CustomDefaulter{}).
+ {{- end }}
+ Complete()
+}
+{{- end }}
+
+// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
+`
+
+ //nolint:lll
+ defaultingWebhookTemplate = `
+// +kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}path=/mutate-{{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}-{{ else }}{{ .QualifiedGroupWithDash }}-{{ end }}{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=true,failurePolicy=fail,sideEffects=None,groups={{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}""{{ else }}{{ .Resource.QualifiedGroup }}{{ end }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=m{{ lower .Resource.Kind }}-{{ .Resource.Version }}.kb.io,admissionReviewVersions={{ .AdmissionReviewVersions }}
+
+{{ if .IsLegacyPath -}}
+// +kubebuilder:object:generate=false
+{{- end }}
+// {{ .Resource.Kind }}CustomDefaulter struct is responsible for setting default values on the custom resource of the
+// Kind {{ .Resource.Kind }} when those are created or updated.
+//
+// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
+// as it is used only for temporary operations and does not need to be deeply copied.
+type {{ .Resource.Kind }}CustomDefaulter struct {
+ // TODO(user): Add more fields as needed for defaulting
+}
+
+var _ webhook.CustomDefaulter = &{{ .Resource.Kind }}CustomDefaulter{}
+
+// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind {{ .Resource.Kind }}.
+func (d *{{ .Resource.Kind }}CustomDefaulter) Default(_ context.Context, obj runtime.Object) error {
+ {{- if .IsLegacyPath -}}
+ {{ lower .Resource.Kind }}, ok := obj.(*{{ .Resource.Kind }})
+ {{- else }}
+ {{ lower .Resource.Kind }}, ok := obj.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }})
+ {{- end }}
+
+ if !ok {
+ return fmt.Errorf("expected an {{ .Resource.Kind }} object but got %T", obj)
+ }
+ {{ lower .Resource.Kind }}log.Info("Defaulting for {{ .Resource.Kind }}", "name", {{ lower .Resource.Kind }}.GetName())
+
+ // TODO(user): fill in your defaulting logic.
+
+ return nil
+}
+`
+
+ //nolint:lll
+ validatingWebhookTemplate = `
+// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
+// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here.
+// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook.
+// +kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}path=/validate-{{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}-{{ else }}{{ .QualifiedGroupWithDash }}-{{ end }}{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=false,failurePolicy=fail,sideEffects=None,groups={{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}""{{ else }}{{ .Resource.QualifiedGroup }}{{ end }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=v{{ lower .Resource.Kind }}-{{ .Resource.Version }}.kb.io,admissionReviewVersions={{ .AdmissionReviewVersions }}
+
+{{ if .IsLegacyPath -}}
+// +kubebuilder:object:generate=false
+{{- end }}
+// {{ .Resource.Kind }}CustomValidator struct is responsible for validating the {{ .Resource.Kind }} resource
+// when it is created, updated, or deleted.
+//
+// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
+// as this struct is used only for temporary operations and does not need to be deeply copied.
+type {{ .Resource.Kind }}CustomValidator struct{
+ // TODO(user): Add more fields as needed for validation
+}
+
+var _ webhook.CustomValidator = &{{ .Resource.Kind }}CustomValidator{}
+
+// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type {{ .Resource.Kind }}.
+func (v *{{ .Resource.Kind }}CustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
+ {{- if .IsLegacyPath -}}
+ {{ lower .Resource.Kind }}, ok := obj.(*{{ .Resource.Kind }})
+ {{- else }}
+ {{ lower .Resource.Kind }}, ok := obj.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }})
+ {{- end }}
+ if !ok {
+ return nil, fmt.Errorf("expected a {{ .Resource.Kind }} object but got %T", obj)
+ }
+ {{ lower .Resource.Kind }}log.Info("Validation for {{ .Resource.Kind }} upon creation", "name", {{ lower .Resource.Kind }}.GetName())
+
+ // TODO(user): fill in your validation logic upon object creation.
+
+ return nil, nil
+}
+
+// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type {{ .Resource.Kind }}.
+func (v *{{ .Resource.Kind }}CustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
+ {{- if .IsLegacyPath -}}
+ {{ lower .Resource.Kind }}, ok := newObj.(*{{ .Resource.Kind }})
+ {{- else }}
+ {{ lower .Resource.Kind }}, ok := newObj.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }})
+ {{- end }}
+ if !ok {
+ return nil, fmt.Errorf("expected a {{ .Resource.Kind }} object for the newObj but got %T", newObj)
+ }
+ {{ lower .Resource.Kind }}log.Info("Validation for {{ .Resource.Kind }} upon update", "name", {{ lower .Resource.Kind }}.GetName())
+
+ // TODO(user): fill in your validation logic upon object update.
+
+ return nil, nil
+}
+
+// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type {{ .Resource.Kind }}.
+func (v *{{ .Resource.Kind }}CustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
+ {{- if .IsLegacyPath -}}
+ {{ lower .Resource.Kind }}, ok := obj.(*{{ .Resource.Kind }})
+ {{- else }}
+ {{ lower .Resource.Kind }}, ok := obj.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }})
+ {{- end }}
+ if !ok {
+ return nil, fmt.Errorf("expected a {{ .Resource.Kind }} object but got %T", obj)
+ }
+ {{ lower .Resource.Kind }}log.Info("Validation for {{ .Resource.Kind }} upon deletion", "name", {{ lower .Resource.Kind }}.GetName())
+
+ // TODO(user): fill in your validation logic upon object deletion.
+
+ return nil, nil
+}
+`
+)
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_suitetest.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_suitetest.go
new file mode 100644
index 00000000000..17d705c164d
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_suitetest.go
@@ -0,0 +1,456 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package webhooks
+
+import (
+ "fmt"
+ log "log/slog"
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var (
+ _ machinery.Template = &WebhookSuite{}
+ _ machinery.Inserter = &WebhookSuite{}
+)
+
+// WebhookSuite scaffolds the file that sets up the webhook tests
+type WebhookSuite struct {
+ machinery.TemplateMixin
+ machinery.MultiGroupMixin
+ machinery.BoilerplateMixin
+ machinery.ResourceMixin
+
+ // todo: currently is not possible to know if an API was or not scaffolded. We can fix it when #1826 be addressed
+ WireResource bool
+
+ // K8SVersion define the k8s version used to do the scaffold
+ // so that is possible retrieve the binaries
+ K8SVersion string
+
+ // BaseDirectoryRelativePath define the Path for the base directory when it is multigroup
+ BaseDirectoryRelativePath string
+
+ // Deprecated - The flag should be removed from go/v5
+ // IsLegacyPath indicates if webhooks should be scaffolded under the API.
+ // Webhooks are now decoupled from APIs based on controller-runtime updates and community feedback.
+ // This flag ensures backward compatibility by allowing scaffolding in the legacy/deprecated path.
+ IsLegacyPath bool
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *WebhookSuite) SetTemplateDefaults() error {
+ if f.Path == "" {
+ // Deprecated: Remove me when remove go/v4
+ baseDir := "api"
+ if !f.IsLegacyPath {
+ baseDir = filepath.Join("internal", "webhook")
+ }
+
+ if f.MultiGroup && f.Resource.Group != "" {
+ f.Path = filepath.Join(baseDir, "%[group]", "%[version]", "webhook_suite_test.go")
+ } else {
+ f.Path = filepath.Join(baseDir, "%[version]", "webhook_suite_test.go")
+ }
+ }
+
+ f.Path = f.Resource.Replacer().Replace(f.Path)
+ log.Info(f.Path)
+
+ if f.IsLegacyPath {
+ f.TemplateBody = fmt.Sprintf(webhookTestSuiteTemplateLegacy,
+ machinery.NewMarkerFor(f.Path, importMarker),
+ admissionImportAlias,
+ machinery.NewMarkerFor(f.Path, addSchemeMarker),
+ machinery.NewMarkerFor(f.Path, addWebhookManagerMarker),
+ "%s",
+ "%d",
+ )
+ } else {
+ f.TemplateBody = fmt.Sprintf(webhookTestSuiteTemplate,
+ machinery.NewMarkerFor(f.Path, importMarker),
+ f.Resource.ImportAlias(),
+ machinery.NewMarkerFor(f.Path, addSchemeMarker),
+ machinery.NewMarkerFor(f.Path, addWebhookManagerMarker),
+ "%s",
+ "%d",
+ )
+ }
+
+ if f.IsLegacyPath {
+ // If is multigroup the path needs to be ../../../ since it has the group dir.
+ f.BaseDirectoryRelativePath = `"..", ".."`
+ if f.MultiGroup && f.Resource.Group != "" {
+ f.BaseDirectoryRelativePath = `"..", "..", ".."`
+ }
+ } else {
+ // If is multigroup the path needs to be ../../../../ since it has the group dir.
+ f.BaseDirectoryRelativePath = `"..", "..", ".."`
+ if f.MultiGroup && f.Resource.Group != "" {
+ f.BaseDirectoryRelativePath = `"..", "..", "..", ".."`
+ }
+ }
+
+ return nil
+}
+
+const (
+ admissionImportAlias = "admissionv1"
+ admissionPath = "k8s.io/api/admission/v1"
+ importMarker = "imports"
+ addWebhookManagerMarker = "webhook"
+ addSchemeMarker = "scheme"
+)
+
+// GetMarkers implements file.Inserter
+func (f *WebhookSuite) GetMarkers() []machinery.Marker {
+ return []machinery.Marker{
+ machinery.NewMarkerFor(f.Path, importMarker),
+ machinery.NewMarkerFor(f.Path, addSchemeMarker),
+ machinery.NewMarkerFor(f.Path, addWebhookManagerMarker),
+ }
+}
+
+const (
+ apiImportCodeFragment = `%s "%s"
+`
+
+ // Deprecated - TODO: remove for go/v5
+ // addWebhookManagerCodeFragmentLegacy is for the path under API
+ addWebhookManagerCodeFragmentLegacy = `err = (&%s{}).SetupWebhookWithManager(mgr)
+Expect(err).NotTo(HaveOccurred())
+
+`
+
+ addWebhookManagerCodeFragment = `err = Setup%sWebhookWithManager(mgr)
+Expect(err).NotTo(HaveOccurred())
+
+`
+)
+
+// GetCodeFragments implements file.Inserter
+func (f *WebhookSuite) GetCodeFragments() machinery.CodeFragmentsMap {
+ fragments := make(machinery.CodeFragmentsMap, 3)
+
+ // Generate import code fragments
+ imports := make([]string, 0)
+
+ // Generate add scheme code fragments
+ addScheme := make([]string, 0)
+
+ // Generate add webhookManager code fragments
+ addWebhookManager := make([]string, 0)
+ if f.IsLegacyPath {
+ imports = append(imports, fmt.Sprintf(apiImportCodeFragment, admissionImportAlias, admissionPath))
+ addWebhookManager = append(addWebhookManager, fmt.Sprintf(addWebhookManagerCodeFragmentLegacy, f.Resource.Kind))
+ } else {
+ imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path))
+ addWebhookManager = append(addWebhookManager, fmt.Sprintf(addWebhookManagerCodeFragment, f.Resource.Kind))
+ }
+
+ // Only store code fragments in the map if the slices are non-empty
+ if len(addWebhookManager) != 0 {
+ fragments[machinery.NewMarkerFor(f.Path, addWebhookManagerMarker)] = addWebhookManager
+ }
+ if len(imports) != 0 {
+ fragments[machinery.NewMarkerFor(f.Path, importMarker)] = imports
+ }
+ if len(addScheme) != 0 {
+ fragments[machinery.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme
+ }
+
+ return fragments
+}
+
+const webhookTestSuiteTemplate = `{{ .Boilerplate }}
+
+package {{ .Resource.Version }}
+
+import (
+ "context"
+ "crypto/tls"
+ "fmt"
+ "net"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "k8s.io/client-go/kubernetes/scheme"
+ "k8s.io/client-go/rest"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/envtest"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+ "sigs.k8s.io/controller-runtime/pkg/log/zap"
+ %s
+)
+
+// These tests use Ginkgo (BDD-style Go testing framework). Refer to
+// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
+
+var (
+ ctx context.Context
+ cancel context.CancelFunc
+ k8sClient client.Client
+ cfg *rest.Config
+ testEnv *envtest.Environment
+)
+
+func TestAPIs(t *testing.T) {
+ RegisterFailHandler(Fail)
+
+ RunSpecs(t, "Webhook Suite")
+}
+
+var _ = BeforeSuite(func() {
+ logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
+
+ ctx, cancel = context.WithCancel(context.TODO())
+
+ var err error
+ err = %s.AddToScheme(scheme.Scheme)
+ Expect(err).NotTo(HaveOccurred())
+
+ %s
+
+ By("bootstrapping test environment")
+ testEnv = &envtest.Environment{
+ CRDDirectoryPaths: []string{filepath.Join({{ .BaseDirectoryRelativePath }}, "config", "crd", "bases")},
+ ErrorIfCRDPathMissing: {{ .WireResource }},
+
+ WebhookInstallOptions: envtest.WebhookInstallOptions{
+ Paths: []string{filepath.Join({{ .BaseDirectoryRelativePath }}, "config", "webhook")},
+ },
+ }
+
+ // Retrieve the first found binary directory to allow running tests from IDEs
+ if getFirstFoundEnvTestBinaryDir() != "" {
+ testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
+ }
+
+ // cfg is defined in this file globally.
+ cfg, err = testEnv.Start()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cfg).NotTo(BeNil())
+
+ k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
+ Expect(err).NotTo(HaveOccurred())
+ Expect(k8sClient).NotTo(BeNil())
+
+ // start webhook server using Manager.
+ webhookInstallOptions := &testEnv.WebhookInstallOptions
+ mgr, err := ctrl.NewManager(cfg, ctrl.Options{
+ Scheme: scheme.Scheme,
+ WebhookServer: webhook.NewServer(webhook.Options{
+ Host: webhookInstallOptions.LocalServingHost,
+ Port: webhookInstallOptions.LocalServingPort,
+ CertDir: webhookInstallOptions.LocalServingCertDir,
+ }),
+ LeaderElection: false,
+ Metrics: metricsserver.Options{BindAddress: "0"},
+
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ %s
+
+ go func() {
+ defer GinkgoRecover()
+ err = mgr.Start(ctx)
+ Expect(err).NotTo(HaveOccurred())
+ }()
+
+ // wait for the webhook server to get ready.
+ dialer := &net.Dialer{Timeout: time.Second}
+ addrPort := fmt.Sprintf("%s:%s", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
+ Eventually(func() error {
+ conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
+ if err != nil {
+ return err
+ }
+
+ return conn.Close();
+ }).Should(Succeed())
+})
+
+var _ = AfterSuite(func() {
+ By("tearing down the test environment")
+ cancel()
+ err := testEnv.Stop()
+ Expect(err).NotTo(HaveOccurred())
+})
+
+// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
+// ENVTEST-based tests depend on specific binaries, usually located in paths set by
+// controller-runtime. When running tests directly (e.g., via an IDE) without using
+// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
+//
+// This function streamlines the process by finding the required binaries, similar to
+// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
+// properly set up, run 'make setup-envtest' beforehand.
+func getFirstFoundEnvTestBinaryDir() string {
+ basePath := filepath.Join({{ .BaseDirectoryRelativePath }}, "bin", "k8s")
+ entries, err := os.ReadDir(basePath)
+ if err != nil {
+ logf.Log.Error(err, "Failed to read directory", "path", basePath)
+ return ""
+ }
+ for _, entry := range entries {
+ if entry.IsDir() {
+ return filepath.Join(basePath, entry.Name())
+ }
+ }
+ return ""
+}
+`
+
+const webhookTestSuiteTemplateLegacy = `{{ .Boilerplate }}
+
+package {{ .Resource.Version }}
+
+import (
+ "context"
+ "crypto/tls"
+ "fmt"
+ "net"
+ "path/filepath"
+ "testing"
+ "time"
+ "runtime"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ %s
+ "k8s.io/client-go/kubernetes/scheme"
+ "k8s.io/client-go/rest"
+ apimachineryruntime "k8s.io/apimachinery/pkg/runtime"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/envtest"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/log/zap"
+ metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+)
+
+// These tests use Ginkgo (BDD-style Go testing framework). Refer to
+// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
+
+var (
+ cancel context.CancelFunc
+ cfg *rest.Config
+ ctx context.Context
+ k8sClient client.Client
+ testEnv *envtest.Environment
+)
+
+func TestAPIs(t *testing.T) {
+ RegisterFailHandler(Fail)
+
+ RunSpecs(t, "Webhook Suite")
+}
+
+var _ = BeforeSuite(func() {
+ logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
+
+ ctx, cancel = context.WithCancel(context.TODO())
+
+ By("bootstrapping test environment")
+ testEnv = &envtest.Environment{
+ CRDDirectoryPaths: []string{filepath.Join({{ .BaseDirectoryRelativePath }}, "config", "crd", "bases")},
+ ErrorIfCRDPathMissing: {{ .WireResource }},
+
+ // The BinaryAssetsDirectory is only required if you want to run the tests directly
+ // without call the makefile target test. If not informed it will look for the
+ // default path defined in controller-runtime which is /usr/local/kubebuilder/.
+ // Note that you must have the required binaries setup under the bin directory to perform
+ // the tests directly. When we run make test it will be setup and used automatically.
+ BinaryAssetsDirectory: filepath.Join({{ .BaseDirectoryRelativePath }}, "bin", "k8s",
+ fmt.Sprintf("{{ .K8SVersion }}-%%s-%%s", runtime.GOOS, runtime.GOARCH)),
+
+ WebhookInstallOptions: envtest.WebhookInstallOptions{
+ Paths: []string{filepath.Join({{ .BaseDirectoryRelativePath }}, "config", "webhook")},
+ },
+ }
+
+ var err error
+ // cfg is defined in this file globally.
+ cfg, err = testEnv.Start()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(cfg).NotTo(BeNil())
+
+ scheme := apimachineryruntime.NewScheme()
+ err = AddToScheme(scheme)
+ Expect(err).NotTo(HaveOccurred())
+
+ err = %s.AddToScheme(scheme)
+ Expect(err).NotTo(HaveOccurred())
+
+ %s
+
+ k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
+ Expect(err).NotTo(HaveOccurred())
+ Expect(k8sClient).NotTo(BeNil())
+
+ // start webhook server using Manager.
+ webhookInstallOptions := &testEnv.WebhookInstallOptions
+ mgr, err := ctrl.NewManager(cfg, ctrl.Options{
+ Scheme: scheme,
+ WebhookServer: webhook.NewServer(webhook.Options{
+ Host: webhookInstallOptions.LocalServingHost,
+ Port: webhookInstallOptions.LocalServingPort,
+ CertDir: webhookInstallOptions.LocalServingCertDir,
+ }),
+ LeaderElection: false,
+ Metrics: metricsserver.Options{BindAddress: "0"},
+
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ %s
+
+ go func() {
+ defer GinkgoRecover()
+ err = mgr.Start(ctx)
+ Expect(err).NotTo(HaveOccurred())
+ }()
+
+ // wait for the webhook server to get ready.
+ dialer := &net.Dialer{Timeout: time.Second}
+ addrPort := fmt.Sprintf("%s:%s", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
+ Eventually(func() error {
+ conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
+ if err != nil {
+ return err
+ }
+
+ return conn.Close();
+ }).Should(Succeed())
+})
+
+var _ = AfterSuite(func() {
+ By("tearing down the test environment")
+ cancel()
+ err := testEnv.Stop()
+ Expect(err).NotTo(HaveOccurred())
+})
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_test_template.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_test_template.go
new file mode 100644
index 00000000000..db1c7fc79ab
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_test_template.go
@@ -0,0 +1,221 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package webhooks
+
+import (
+ "fmt"
+ log "log/slog"
+ "path/filepath"
+ "strings"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &WebhookTest{}
+
+// WebhookTest scaffolds the file that sets up the webhook unit tests
+type WebhookTest struct {
+ machinery.TemplateMixin
+ machinery.MultiGroupMixin
+ machinery.BoilerplateMixin
+ machinery.ResourceMixin
+ machinery.IfNotExistsActionMixin
+
+ Force bool
+
+ // Deprecated - The flag should be removed from go/v5
+ // IsLegacyPath indicates if webhooks should be scaffolded under the API.
+ // Webhooks are now decoupled from APIs based on controller-runtime updates and community feedback.
+ // This flag ensures backward compatibility by allowing scaffolding in the legacy/deprecated path.
+ IsLegacyPath bool
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *WebhookTest) SetTemplateDefaults() error {
+ if f.Path == "" {
+ // Deprecated: Remove me when remove go/v4
+ const baseDir = "api"
+ pathAPI := baseDir
+ if !f.IsLegacyPath {
+ pathAPI = filepath.Join("internal", "webhook")
+ }
+
+ if f.MultiGroup && f.Resource.Group != "" {
+ f.Path = filepath.Join(pathAPI, "%[group]", "%[version]", "%[kind]_webhook_test.go")
+ } else {
+ f.Path = filepath.Join(pathAPI, "%[version]", "%[kind]_webhook_test.go")
+ }
+ }
+ f.Path = f.Resource.Replacer().Replace(f.Path)
+ log.Info(f.Path)
+
+ webhookTestTemplate := webhookTestTemplate
+ templates := make([]string, 0)
+ if f.Resource.HasDefaultingWebhook() {
+ templates = append(templates, defaultWebhookTestTemplate)
+ }
+ if f.Resource.HasValidationWebhook() {
+ templates = append(templates, validateWebhookTestTemplate)
+ }
+ if f.Resource.HasConversionWebhook() {
+ templates = append(templates, conversionWebhookTestTemplate)
+ }
+ f.TemplateBody = fmt.Sprintf(webhookTestTemplate, strings.Join(templates, "\n"))
+
+ if f.Force {
+ f.IfExistsAction = machinery.OverwriteFile
+ }
+ f.IfNotExistsAction = machinery.IgnoreFile
+
+ return nil
+}
+
+const webhookTestTemplate = `{{ .Boilerplate }}
+
+package {{ .Resource.Version }}
+
+import (
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ {{ if not .IsLegacyPath -}}
+ {{ if not (isEmptyStr .Resource.Path) -}}
+ {{ .Resource.ImportAlias }} "{{ .Resource.Path }}"
+ {{- end }}
+ {{- end }}
+ // TODO (user): Add any additional imports if needed
+)
+
+var _ = Describe("{{ .Resource.Kind }} Webhook", func() {
+ var (
+ {{- if .IsLegacyPath -}}
+ obj *{{ .Resource.Kind }}
+ {{- else }}
+ obj *{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}
+ oldObj *{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}
+ {{- if .Resource.HasValidationWebhook }}
+ validator {{ .Resource.Kind }}CustomValidator
+ {{- end }}
+ {{- if .Resource.HasDefaultingWebhook }}
+ defaulter {{ .Resource.Kind }}CustomDefaulter
+ {{- end }}
+ {{- end }}
+ )
+
+ BeforeEach(func() {
+ {{- if .IsLegacyPath -}}
+ obj = &{{ .Resource.Kind }}{}
+ {{- else }}
+ obj = &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}
+ oldObj = &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}
+ {{- if .Resource.HasValidationWebhook }}
+ validator = {{ .Resource.Kind }}CustomValidator{}
+ Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
+ {{- end }}
+ {{- if .Resource.HasDefaultingWebhook }}
+ defaulter = {{ .Resource.Kind }}CustomDefaulter{}
+ Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
+ {{- end }}
+ Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
+ {{- end }}
+ Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
+ // TODO (user): Add any setup logic common to all tests
+ })
+
+ AfterEach(func() {
+ // TODO (user): Add any teardown logic common to all tests
+ })
+
+ %s
+})
+`
+
+const conversionWebhookTestTemplate = `
+Context("When creating {{ .Resource.Kind }} under Conversion Webhook", func() {
+ // TODO (user): Add logic to convert the object to the desired version and verify the conversion
+ // Example:
+ // It("Should convert the object correctly", func() {
+ {{- if .IsLegacyPath -}}
+ // convertedObj := &{{ .Resource.Kind }}{}
+ {{- else }}
+ // convertedObj := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}
+ {{- end }}
+ // Expect(obj.ConvertTo(convertedObj)).To(Succeed())
+ // Expect(convertedObj).ToNot(BeNil())
+ // })
+})
+`
+
+const validateWebhookTestTemplate = `
+Context("When creating or updating {{ .Resource.Kind }} under Validating Webhook", func() {
+ // TODO (user): Add logic for validating webhooks
+ // Example:
+ // It("Should deny creation if a required field is missing", func() {
+ // By("simulating an invalid creation scenario")
+ // obj.SomeRequiredField = ""
+ {{- if .IsLegacyPath -}}
+ // Expect(obj.ValidateCreate(ctx)).Error().To(HaveOccurred())
+ {{- else }}
+ // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
+ {{- end }}
+ // })
+ //
+ // It("Should admit creation if all required fields are present", func() {
+ // By("simulating an invalid creation scenario")
+ // obj.SomeRequiredField = "valid_value"
+ {{- if .IsLegacyPath -}}
+ // Expect(obj.ValidateCreate(ctx)).To(BeNil())
+ {{- else }}
+ // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
+ {{- end }}
+ // })
+ //
+ // It("Should validate updates correctly", func() {
+ // By("simulating a valid update scenario")
+ {{- if .IsLegacyPath -}}
+ // oldObj := &Captain{SomeRequiredField: "valid_value"}
+ // obj.SomeRequiredField = "updated_value"
+ // Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil())
+ {{- else }}
+ // oldObj.SomeRequiredField = "updated_value"
+ // obj.SomeRequiredField = "updated_value"
+ // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
+ {{- end }}
+ // })
+})
+`
+
+const defaultWebhookTestTemplate = `
+Context("When creating {{ .Resource.Kind }} under Defaulting Webhook", func() {
+ // TODO (user): Add logic for defaulting webhooks
+ // Example:
+ // It("Should apply defaults when a required field is empty", func() {
+ // By("simulating a scenario where defaults should be applied")
+ {{- if .IsLegacyPath -}}
+ // obj.SomeFieldWithDefault = ""
+ // Expect(obj.Default(ctx)).To(Succeed())
+ // Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
+ {{- else }}
+ // obj.SomeFieldWithDefault = ""
+ // By("calling the Default method to apply defaults")
+ // defaulter.Default(ctx, obj)
+ // By("checking that the default values are set")
+ // Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
+ {{- end }}
+ // })
+})
+`
diff --git a/pkg/plugins/golang/v4/scaffolds/webhook.go b/pkg/plugins/golang/v4/scaffolds/webhook.go
new file mode 100644
index 00000000000..efc83e7fcd2
--- /dev/null
+++ b/pkg/plugins/golang/v4/scaffolds/webhook.go
@@ -0,0 +1,170 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package scaffolds
+
+import (
+ "errors"
+ "fmt"
+ log "log/slog"
+ "strings"
+
+ "github.com/spf13/afero"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
+ pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/api"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/cmd"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/hack"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/test/e2e"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks"
+)
+
+var _ plugins.Scaffolder = &webhookScaffolder{}
+
+type webhookScaffolder struct {
+ config config.Config
+ resource resource.Resource
+
+ // fs is the filesystem that will be used by the scaffolder
+ fs machinery.Filesystem
+
+ // force indicates whether to scaffold controller files even if it exists or not
+ force bool
+
+ // Deprecated - TODO: remove it for go/v5
+ // isLegacy indicates that the resource should be created in the legacy path under the api
+ isLegacy bool
+}
+
+// NewWebhookScaffolder returns a new Scaffolder for v2 webhook creation operations
+func NewWebhookScaffolder(cfg config.Config, res resource.Resource, force bool, isLegacy bool) plugins.Scaffolder {
+ return &webhookScaffolder{
+ config: cfg,
+ resource: res,
+ force: force,
+ isLegacy: isLegacy,
+ }
+}
+
+// InjectFS implements cmdutil.Scaffolder
+func (s *webhookScaffolder) InjectFS(fs machinery.Filesystem) {
+ s.fs = fs
+}
+
+// Scaffold implements cmdutil.Scaffolder
+func (s *webhookScaffolder) Scaffold() error {
+ log.Info("Writing scaffold for you to edit...")
+
+ // Load the boilerplate
+ boilerplate, err := afero.ReadFile(s.fs.FS, hack.DefaultBoilerplatePath)
+ if err != nil {
+ if errors.Is(err, afero.ErrFileNotFound) {
+ log.Warn("Unable to find boilerplate file."+
+ "This file is used to generate the license header in the project.\n"+
+ "Note that controller-gen will also use this. Therefore, ensure that you "+
+ "add the license file or configure your project accordingly.",
+ "file_path", hack.DefaultBoilerplatePath, "error", err)
+ boilerplate = []byte("")
+ } else {
+ return fmt.Errorf("error scaffolding webhook: unable to load boilerplate: %w", err)
+ }
+ }
+
+ // Initialize the machinery.Scaffold that will write the files to disk
+ scaffold := machinery.NewScaffold(s.fs,
+ machinery.WithConfig(s.config),
+ machinery.WithBoilerplate(string(boilerplate)),
+ machinery.WithResource(&s.resource),
+ )
+
+ // Keep track of these values before the update
+ doDefaulting := s.resource.HasDefaultingWebhook()
+ doValidation := s.resource.HasValidationWebhook()
+ doConversion := s.resource.HasConversionWebhook()
+
+ if err = s.config.UpdateResource(s.resource); err != nil {
+ return fmt.Errorf("error updating resource: %w", err)
+ }
+
+ if err = scaffold.Execute(
+ &webhooks.Webhook{Force: s.force, IsLegacyPath: s.isLegacy},
+ &e2e.WebhookTestUpdater{WireWebhook: true},
+ &cmd.MainUpdater{WireWebhook: true, IsLegacyPath: s.isLegacy},
+ &webhooks.WebhookTest{Force: s.force, IsLegacyPath: s.isLegacy},
+ ); err != nil {
+ return fmt.Errorf("error updating webhook: %w", err)
+ }
+
+ if doConversion {
+ resourceFilePath := fmt.Sprintf("api/%s/%s_types.go",
+ s.resource.Version, strings.ToLower(s.resource.Kind))
+ if s.config.IsMultiGroup() {
+ resourceFilePath = fmt.Sprintf("api/%s/%s/%s_types.go",
+ s.resource.Group, s.resource.Version,
+ strings.ToLower(s.resource.Kind))
+ }
+
+ err = pluginutil.InsertCodeIfNotExist(resourceFilePath,
+ "// +kubebuilder:object:root=true",
+ "\n// +kubebuilder:storageversion")
+ if err != nil {
+ log.Error("Unable to insert storage version marker in file",
+ "marker", "// +kubebuilder:storageversion",
+ "file_path", resourceFilePath,
+ "error", err)
+ }
+
+ if err = scaffold.Execute(&api.Hub{Force: s.force}); err != nil {
+ return fmt.Errorf("error scaffold resource with hub: %w", err)
+ }
+
+ for _, spoke := range s.resource.Webhooks.Spoke {
+ log.Info("Scaffolding for spoke version", "version", spoke)
+ if err = scaffold.Execute(&api.Spoke{Force: s.force, SpokeVersion: spoke}); err != nil {
+ return fmt.Errorf("failed to scaffold spoke %s: %w", spoke, err)
+ }
+ }
+
+ log.Info(`Webhook server has been set up for you.
+You need to implement the conversion.Hub and conversion.Convertible interfaces for your CRD types.`)
+ }
+
+ // TODO: Add test suite for conversion webhook after #1664 has been merged & conversion tests supported in envtest.
+ if doDefaulting || doValidation {
+ if err = scaffold.Execute(&webhooks.WebhookSuite{IsLegacyPath: s.isLegacy}); err != nil {
+ return fmt.Errorf("error scaffold webhook suite: %w", err)
+ }
+ }
+
+ // TODO: remove for go/v5
+ if !s.isLegacy {
+ if hasInternalController, err := pluginutil.HasFileContentWith("Dockerfile", "internal/controller"); err != nil {
+ log.Error("Unable to read Dockerfile to check if webhook(s) will be properly copied", "error", err)
+ } else if hasInternalController {
+ log.Warn("Dockerfile is copying internal/controller. To allow copying webhooks, " +
+ "it will be edited, and `internal/controller` will be replaced by `internal/`.")
+
+ if err = pluginutil.ReplaceInFile("Dockerfile", "internal/controller", "internal/"); err != nil {
+ log.Error("Unable to replace \"internal/controller\" with \"internal/\" in the Dockerfile", "error", err)
+ }
+ }
+ }
+ return nil
+}
diff --git a/pkg/plugins/golang/v4/webhook.go b/pkg/plugins/golang/v4/webhook.go
new file mode 100644
index 00000000000..8e70df13242
--- /dev/null
+++ b/pkg/plugins/golang/v4/webhook.go
@@ -0,0 +1,201 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v4
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/spf13/pflag"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
+ goPlugin "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds"
+)
+
+var _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{}
+
+type createWebhookSubcommand struct {
+ config config.Config
+ // For help text.
+ commandName string
+
+ options *goPlugin.Options
+
+ resource *resource.Resource
+
+ // force indicates that the resource should be created even if it already exists
+ force bool
+
+ // Deprecated - TODO: remove it for go/v5
+ // isLegacyPath indicates that the resource should be created in the legacy path under the api
+ isLegacyPath bool
+
+ // runMake indicates whether to run make or not after scaffolding APIs
+ runMake bool
+}
+
+func (p *createWebhookSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
+ p.commandName = cliMeta.CommandName
+
+ subcmdMeta.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting,
+validating and/or conversion webhooks.
+`
+ subcmdMeta.Examples = fmt.Sprintf(` # Create defaulting and validating webhooks for Group: ship, Version: v1beta1
+ # and Kind: Frigate
+ %[1]s create webhook --group ship --version v1beta1 --kind Frigate --defaulting --programmatic-validation
+
+ # Create conversion webhook for Group: ship, Version: v1beta1
+ # and Kind: Frigate
+ %[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion --spoke v1
+`, cliMeta.CommandName)
+}
+
+func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) {
+ p.options = &goPlugin.Options{}
+
+ fs.BoolVar(&p.runMake, "make", true, "if true, run `make generate` after generating files")
+
+ fs.StringVar(&p.options.Plural, "plural", "", "resource irregular plural form")
+
+ fs.BoolVar(&p.options.DoDefaulting, "defaulting", false,
+ "if set, scaffold the defaulting webhook")
+ fs.BoolVar(&p.options.DoValidation, "programmatic-validation", false,
+ "if set, scaffold the validating webhook")
+ fs.BoolVar(&p.options.DoConversion, "conversion", false,
+ "if set, scaffold the conversion webhook")
+
+ fs.StringSliceVar(&p.options.Spoke, "spoke",
+ nil,
+ "Comma-separated list of spoke versions to be added to the conversion webhook (e.g., --spoke v1,v2)")
+
+ // TODO: remove for go/v5
+ fs.BoolVar(&p.isLegacyPath, "legacy", false,
+ "[DEPRECATED] Attempts to create resource under the API directory (legacy path). "+
+ "This option will be removed in future versions.")
+
+ fs.StringVar(&p.options.ExternalAPIPath, "external-api-path", "",
+ "Specify the Go package import path for the external API. This is used to scaffold controllers for resources "+
+ "defined outside this project (e.g., github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1).")
+
+ fs.StringVar(&p.options.ExternalAPIDomain, "external-api-domain", "",
+ "Specify the domain name for the external API. This domain is used to generate accurate RBAC "+
+ "markers and permissions for the external resources (e.g., cert-manager.io).")
+
+ fs.BoolVar(&p.force, "force", false,
+ "attempt to create resource even if it already exists")
+}
+
+func (p *createWebhookSubcommand) InjectConfig(c config.Config) error {
+ p.config = c
+ return nil
+}
+
+func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error {
+ p.resource = res
+
+ if len(p.options.ExternalAPIPath) != 0 && len(p.options.ExternalAPIDomain) != 0 && p.isLegacyPath {
+ return errors.New("you cannot scaffold webhooks for external types using the legacy path")
+ }
+
+ for _, spoke := range p.options.Spoke {
+ spoke = strings.TrimSpace(spoke)
+ if !isValidVersion(spoke, res, p.config) {
+ return fmt.Errorf("invalid spoke version %q", spoke)
+ }
+ res.Webhooks.Spoke = append(res.Webhooks.Spoke, spoke)
+ }
+
+ p.options.UpdateResource(p.resource, p.config)
+
+ if err := p.resource.Validate(); err != nil {
+ return fmt.Errorf("error validating resource: %w", err)
+ }
+
+ if !p.resource.HasDefaultingWebhook() && !p.resource.HasValidationWebhook() && !p.resource.HasConversionWebhook() {
+ return fmt.Errorf("%s create webhook requires at least one of --defaulting,"+
+ " --programmatic-validation and --conversion to be true", p.commandName)
+ }
+
+ // check if resource exist to create webhook
+ resValue, err := p.config.GetResource(p.resource.GVK)
+ res = &resValue
+ if err != nil {
+ if !p.resource.External && !p.resource.Core {
+ return fmt.Errorf("%s create webhook requires a previously created API ", p.commandName)
+ }
+ } else if res.Webhooks != nil && !res.Webhooks.IsEmpty() && !p.force {
+ // FIXME: This is a temporary fix to allow we move forward
+ // However, users should be able to call the command to create an webhook
+ // even if the resource already has one when the webhook is not of the same type.
+ return fmt.Errorf("webhook resource already exists")
+ }
+
+ return nil
+}
+
+func (p *createWebhookSubcommand) Scaffold(fs machinery.Filesystem) error {
+ scaffolder := scaffolds.NewWebhookScaffolder(p.config, *p.resource, p.force, p.isLegacyPath)
+ scaffolder.InjectFS(fs)
+ if err := scaffolder.Scaffold(); err != nil {
+ return fmt.Errorf("failed to scaffold webhook: %w", err)
+ }
+
+ return nil
+}
+
+func (p *createWebhookSubcommand) PostScaffold() error {
+ err := pluginutil.RunCmd("Update dependencies", "go", "mod", "tidy")
+ if err != nil {
+ return fmt.Errorf("error updating go dependencies: %w", err)
+ }
+
+ if p.runMake {
+ err = pluginutil.RunCmd("Running make", "make", "generate")
+ if err != nil {
+ return fmt.Errorf("error running make generate: %w", err)
+ }
+ }
+
+ fmt.Print("Next: implement your new Webhook and generate the manifests with:\n$ make manifests\n")
+
+ return nil
+}
+
+// Helper function to validate spoke versions
+func isValidVersion(version string, res *resource.Resource, cfg config.Config) bool {
+ // Fetch all resources in the config
+ resources, err := cfg.GetResources()
+ if err != nil {
+ return false
+ }
+
+ // Iterate through resources and validate if the given version exists for the same Group and Kind
+ for _, r := range resources {
+ if r.Group == res.Group && r.Kind == res.Kind && r.Version == version {
+ return true
+ }
+ }
+
+ // If no matching version is found, return false
+ return false
+}
diff --git a/pkg/plugins/optional/autoupdate/v1alpha/edit.go b/pkg/plugins/optional/autoupdate/v1alpha/edit.go
new file mode 100644
index 00000000000..0f82926d40c
--- /dev/null
+++ b/pkg/plugins/optional/autoupdate/v1alpha/edit.go
@@ -0,0 +1,70 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha
+
+import (
+ "fmt"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/autoupdate/v1alpha/scaffolds"
+)
+
+var _ plugin.EditSubcommand = &editSubcommand{}
+
+type editSubcommand struct {
+ config config.Config
+}
+
+func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
+ subcmdMeta.Description = metaDataDescription
+
+ subcmdMeta.Examples = fmt.Sprintf(` # Edit a common project with this plugin
+ %[1]s edit --plugins=%[2]s
+`, cliMeta.CommandName, pluginKey)
+}
+
+func (p *editSubcommand) InjectConfig(c config.Config) error {
+ p.config = c
+ return nil
+}
+
+func (p *editSubcommand) PreScaffold(machinery.Filesystem) error {
+ if len(p.config.GetCliVersion()) == 0 {
+ return fmt.Errorf(
+ "you must manually upgrade your project to a version that records the CLI version in PROJECT (`cliVersion`) " +
+ "to allow the `alpha update` command to work properly before using this plugin.\n" +
+ "More info: https://book.kubebuilder.io/migrations",
+ )
+ }
+ return nil
+}
+
+func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
+ if err := insertPluginMetaToConfig(p.config, pluginConfig{}); err != nil {
+ return fmt.Errorf("error inserting project plugin meta to configuration: %w", err)
+ }
+
+ scaffolder := scaffolds.NewInitScaffolder()
+ scaffolder.InjectFS(fs)
+ if err := scaffolder.Scaffold(); err != nil {
+ return fmt.Errorf("error scaffolding edit subcommand: %w", err)
+ }
+
+ return nil
+}
diff --git a/pkg/plugins/optional/autoupdate/v1alpha/init.go b/pkg/plugins/optional/autoupdate/v1alpha/init.go
new file mode 100644
index 00000000000..633054040e6
--- /dev/null
+++ b/pkg/plugins/optional/autoupdate/v1alpha/init.go
@@ -0,0 +1,59 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha
+
+import (
+ "fmt"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/autoupdate/v1alpha/scaffolds"
+)
+
+var _ plugin.InitSubcommand = &initSubcommand{}
+
+type initSubcommand struct {
+ config config.Config
+}
+
+func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
+ subcmdMeta.Description = metaDataDescription
+
+ subcmdMeta.Examples = fmt.Sprintf(` # Initialize a common project with this plugin
+ %[1]s init --plugins=%[2]s
+`, cliMeta.CommandName, pluginKey)
+}
+
+func (p *initSubcommand) InjectConfig(c config.Config) error {
+ p.config = c
+ return nil
+}
+
+func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error {
+ if err := insertPluginMetaToConfig(p.config, pluginConfig{}); err != nil {
+ return fmt.Errorf("error inserting project plugin meta to configuration: %w", err)
+ }
+
+ scaffolder := scaffolds.NewInitScaffolder()
+ scaffolder.InjectFS(fs)
+ if err := scaffolder.Scaffold(); err != nil {
+ return fmt.Errorf("error scaffolding init subcommand: %w", err)
+ }
+
+ return nil
+}
diff --git a/pkg/plugins/optional/autoupdate/v1alpha/plugin.go b/pkg/plugins/optional/autoupdate/v1alpha/plugin.go
new file mode 100644
index 00000000000..8857c025408
--- /dev/null
+++ b/pkg/plugins/optional/autoupdate/v1alpha/plugin.go
@@ -0,0 +1,96 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha
+
+import (
+ "errors"
+ "fmt"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
+ "sigs.k8s.io/kubebuilder/v4/pkg/model/stage"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins"
+)
+
+//nolint:lll
+const metaDataDescription = `This plugin scaffolds a GitHub Action that helps you keep your project aligned with the latest Kubebuilder improvements. With a tiny amount of setup, you’ll receive **automatic issue notifications** whenever a new Kubebuilder release is available. Each issue includes a **compare link** so you can open a Pull Request with one click and review the changes safely.
+
+Under the hood, the workflow runs 'kubebuilder alpha update' using a **3-way merge strategy** to refresh your scaffold while preserving your code. It creates and pushes an update branch, then opens a GitHub **Issue** containing the PR URL you can use to review and merge.
+
+### How to set it up
+
+1) **Add the plugin**: Use the Kubebuilder CLI to scaffold the automation into your repo.
+2) **Review the workflow**: The file '.github/workflows/auto_update.yml' runs on a schedule to check for updates.
+3) **Permissions required** (via the built-in 'GITHUB_TOKEN'):
+ - **contents: write** — needed to create and push the update branch.
+ - **issues: write** — needed to create the tracking Issue with the PR link.
+4) **Protect your branches**: Enable **branch protection rules** so automated changes **cannot** be pushed directly. All updates must go through a Pull Request for review.`
+
+const pluginName = "autoupdate." + plugins.DefaultNameQualifier
+
+var (
+ pluginVersion = plugin.Version{Number: 1, Stage: stage.Alpha}
+ supportedProjectVersions = []config.Version{cfgv3.Version}
+ pluginKey = plugin.KeyFor(Plugin{})
+)
+
+// Plugin implements the plugin.Full interface
+type Plugin struct {
+ editSubcommand
+ initSubcommand
+}
+
+var _ plugin.Init = Plugin{}
+
+type pluginConfig struct{}
+
+// Name returns the name of the plugin
+func (Plugin) Name() string { return pluginName }
+
+// Version returns the version of the Helm plugin
+func (Plugin) Version() plugin.Version { return pluginVersion }
+
+// SupportedProjectVersions returns an array with all project versions supported by the plugin
+func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions }
+
+// GetEditSubcommand will return the subcommand which is responsible for adding and/or edit a autoupdate
+func (p Plugin) GetEditSubcommand() plugin.EditSubcommand { return &p.editSubcommand }
+
+// GetInitSubcommand will return the subcommand which is responsible for init autoupdate plugin
+func (p Plugin) GetInitSubcommand() plugin.InitSubcommand { return &p.initSubcommand }
+
+// DeprecationWarning define the deprecation message or return empty when plugin is not deprecated
+func (p Plugin) DeprecationWarning() string {
+ return ""
+}
+
+// insertPluginMetaToConfig will insert the metadata to the plugin configuration
+func insertPluginMetaToConfig(target config.Config, cfg pluginConfig) error {
+ err := target.DecodePluginConfig(pluginKey, cfg)
+ if !errors.As(err, &config.UnsupportedFieldError{}) {
+ if err != nil && !errors.As(err, &config.PluginKeyNotFoundError{}) {
+ return fmt.Errorf("error decoding plugin configuration: %w", err)
+ }
+
+ if err = target.EncodePluginConfig(pluginKey, cfg); err != nil {
+ return fmt.Errorf("error encoding plugin configuration: %w", err)
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/plugins/optional/autoupdate/v1alpha/scaffolds/init.go b/pkg/plugins/optional/autoupdate/v1alpha/scaffolds/init.go
new file mode 100644
index 00000000000..c36af8f2a8a
--- /dev/null
+++ b/pkg/plugins/optional/autoupdate/v1alpha/scaffolds/init.go
@@ -0,0 +1,64 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package scaffolds
+
+import (
+ "fmt"
+ log "log/slog"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/autoupdate/v1alpha/scaffolds/internal/github"
+)
+
+var _ plugins.Scaffolder = &initScaffolder{}
+
+type initScaffolder struct {
+ config config.Config
+
+ // fs is the filesystem that will be used by the scaffolder
+ fs machinery.Filesystem
+}
+
+// NewInitScaffolder returns a new Scaffolder for project initialization operations
+func NewInitScaffolder() plugins.Scaffolder {
+ return &initScaffolder{}
+}
+
+// InjectFS implements cmdutil.Scaffolder
+func (s *initScaffolder) InjectFS(fs machinery.Filesystem) {
+ s.fs = fs
+}
+
+// Scaffold implements cmdutil.Scaffolder
+func (s *initScaffolder) Scaffold() error {
+ log.Info("Writing scaffold for you to edit...")
+
+ scaffold := machinery.NewScaffold(s.fs,
+ machinery.WithConfig(s.config),
+ )
+
+ err := scaffold.Execute(
+ &github.AutoUpdate{},
+ )
+ if err != nil {
+ return fmt.Errorf("failed to execute init scaffold: %w", err)
+ }
+
+ return nil
+}
diff --git a/pkg/plugins/optional/autoupdate/v1alpha/scaffolds/internal/github/auto_update.go b/pkg/plugins/optional/autoupdate/v1alpha/scaffolds/internal/github/auto_update.go
new file mode 100644
index 00000000000..80a50b768a2
--- /dev/null
+++ b/pkg/plugins/optional/autoupdate/v1alpha/scaffolds/internal/github/auto_update.go
@@ -0,0 +1,119 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package github
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &AutoUpdate{}
+
+// AutoUpdate scaffolds the GitHub Action to lint the project
+type AutoUpdate struct {
+ machinery.TemplateMixin
+ machinery.BoilerplateMixin
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *AutoUpdate) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join(".github", "workflows", "auto_update.yml")
+ }
+
+ f.TemplateBody = autoUpdateTemplate
+ f.IfExistsAction = machinery.SkipFile
+
+ return nil
+}
+
+const autoUpdateTemplate = `name: Auto Update
+
+# The 'kubebuilder alpha update 'command requires write access to the repository to create a branch
+# with the update files and allow you to open a pull request using the link provided in the issue.
+# The branch created will be named in the format kubebuilder-update-from--to- by default.
+# To protect your codebase, please ensure that you have branch protection rules configured for your
+# main branches. This will guarantee that no one can bypass a review and push directly to a branch like 'main'.
+permissions:
+ contents: write
+ issues: write
+ models: read
+
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: "0 0 * * 2" # Every Tuesday at 00:00 UTC
+
+jobs:
+ auto-update:
+ runs-on: ubuntu-latest
+ env:
+ GH_TOKEN: {{ "${{ secrets.GITHUB_TOKEN }}" }}
+
+ # Step 1: Checkout the repository.
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ token: {{ "${{ secrets.GITHUB_TOKEN }}" }}
+ fetch-depth: 0
+
+ # Step 2: Configure Git to create commits with the GitHub Actions bot.
+ - name: Configure Git
+ run: |
+ git config --global user.name "github-actions[bot]"
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+
+ # Step 3: Set up Go environment.
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: stable
+
+ # Step 4: Install Kubebuilder.
+ - name: Install Kubebuilder
+ run: |
+ curl -L -o kubebuilder "https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)"
+ chmod +x kubebuilder
+ sudo mv kubebuilder /usr/local/bin/
+ kubebuilder version
+
+ # Step 5: Install Models extension for GitHub CLI
+ - name: Install/upgrade gh-models extension
+ run: |
+ gh extension install github/gh-models --force
+ gh models --help >/dev/null
+
+ # Step 6: Run the Kubebuilder alpha update command.
+ # More info: https://kubebuilder.io/reference/commands/alpha_update
+ - name: Run kubebuilder alpha update
+ # Executes the update command with specified flags.
+ # --force: Completes the merge even if conflicts occur, leaving conflict markers.
+ # --push: Automatically pushes the resulting output branch to the 'origin' remote.
+ # --restore-path: Preserves specified paths (e.g., CI workflow files) when squashing.
+ # --open-gh-issue: Creates a GitHub Issue with a link for opening a PR for review.
+ # --use-gh-models: Adds an AI-generated comment to the created Issue with
+ # a short overview of the scaffold changes and conflict-resolution guidance (If Any).
+ run: |
+ kubebuilder alpha update \
+ --force \
+ --push \
+ --restore-path .github/workflows \
+ --open-gh-issue \
+ --use-gh-models
+`
diff --git a/pkg/plugins/optional/grafana/v1alpha/commons.go b/pkg/plugins/optional/grafana/v1alpha/commons.go
new file mode 100644
index 00000000000..3cd97b507cd
--- /dev/null
+++ b/pkg/plugins/optional/grafana/v1alpha/commons.go
@@ -0,0 +1,40 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha
+
+import (
+ "errors"
+ "fmt"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+)
+
+// InsertPluginMetaToConfig will insert the metadata to the plugin configuration
+func InsertPluginMetaToConfig(target config.Config, cfg pluginConfig) error {
+ err := target.DecodePluginConfig(pluginKey, cfg)
+ if !errors.As(err, &config.UnsupportedFieldError{}) {
+ if err != nil && !errors.As(err, &config.PluginKeyNotFoundError{}) {
+ return fmt.Errorf("error decoding plugin configuration: %w", err)
+ }
+
+ if err = target.EncodePluginConfig(pluginKey, cfg); err != nil {
+ return fmt.Errorf("error encoding plugin configuration: %w", err)
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/plugins/optional/grafana/v1alpha/constants.go b/pkg/plugins/optional/grafana/v1alpha/constants.go
new file mode 100644
index 00000000000..402cecbafed
--- /dev/null
+++ b/pkg/plugins/optional/grafana/v1alpha/constants.go
@@ -0,0 +1,29 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha
+
+//nolint:lll
+const metaDataDescription = `This command will add Grafana manifests to the project:
+ - A JSON file includes dashboard manifest that can be directly copied to Grafana Web UI.
+ ('grafana/controller-runtime-metrics.json')
+
+NOTE: This plugin requires:
+- Access to Prometheus
+- Your project must be using controller-runtime to expose the metrics via the controller metrics and they need to be collected by Prometheus.
+- Access to Grafana (https://grafana.com/docs/grafana/latest/setup-grafana/installation/)
+Check how to enable the metrics for your project by looking at the doc: https://book.kubebuilder.io/reference/metrics.html
+`
diff --git a/pkg/plugins/optional/grafana/v1alpha/edit.go b/pkg/plugins/optional/grafana/v1alpha/edit.go
new file mode 100644
index 00000000000..c688c188b83
--- /dev/null
+++ b/pkg/plugins/optional/grafana/v1alpha/edit.go
@@ -0,0 +1,60 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+//nolint:dupl
+package v1alpha
+
+import (
+ "fmt"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha/scaffolds"
+)
+
+var _ plugin.EditSubcommand = &editSubcommand{}
+
+type editSubcommand struct {
+ config config.Config
+}
+
+func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
+ subcmdMeta.Description = metaDataDescription
+
+ subcmdMeta.Examples = fmt.Sprintf(` # Edit a common project with this plugin
+ %[1]s edit --plugins=%[2]s
+`, cliMeta.CommandName, pluginKey)
+}
+
+func (p *editSubcommand) InjectConfig(c config.Config) error {
+ p.config = c
+ return nil
+}
+
+func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
+ if err := InsertPluginMetaToConfig(p.config, pluginConfig{}); err != nil {
+ return fmt.Errorf("error inserting project plugin meta to configuration: %w", err)
+ }
+
+ scaffolder := scaffolds.NewEditScaffolder()
+ scaffolder.InjectFS(fs)
+ if err := scaffolder.Scaffold(); err != nil {
+ return fmt.Errorf("error scaffolding edit subcommand: %w", err)
+ }
+
+ return nil
+}
diff --git a/pkg/plugins/optional/grafana/v1alpha/init.go b/pkg/plugins/optional/grafana/v1alpha/init.go
new file mode 100644
index 00000000000..a5abdc0197d
--- /dev/null
+++ b/pkg/plugins/optional/grafana/v1alpha/init.go
@@ -0,0 +1,60 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+//nolint:dupl
+package v1alpha
+
+import (
+ "fmt"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha/scaffolds"
+)
+
+var _ plugin.InitSubcommand = &initSubcommand{}
+
+type initSubcommand struct {
+ config config.Config
+}
+
+func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
+ subcmdMeta.Description = metaDataDescription
+
+ subcmdMeta.Examples = fmt.Sprintf(` # Initialize a common project with this plugin
+ %[1]s init --plugins=%[2]s
+`, cliMeta.CommandName, pluginKey)
+}
+
+func (p *initSubcommand) InjectConfig(c config.Config) error {
+ p.config = c
+ return nil
+}
+
+func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error {
+ if err := InsertPluginMetaToConfig(p.config, pluginConfig{}); err != nil {
+ return fmt.Errorf("error inserting project plugin meta to configuration: %w", err)
+ }
+
+ scaffolder := scaffolds.NewInitScaffolder()
+ scaffolder.InjectFS(fs)
+ if err := scaffolder.Scaffold(); err != nil {
+ return fmt.Errorf("error scaffolding init subcommand: %w", err)
+ }
+
+ return nil
+}
diff --git a/pkg/plugins/optional/grafana/v1alpha/plugin.go b/pkg/plugins/optional/grafana/v1alpha/plugin.go
new file mode 100644
index 00000000000..39e77079cfd
--- /dev/null
+++ b/pkg/plugins/optional/grafana/v1alpha/plugin.go
@@ -0,0 +1,63 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha
+
+import (
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
+ "sigs.k8s.io/kubebuilder/v4/pkg/model/stage"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins"
+)
+
+const pluginName = "grafana." + plugins.DefaultNameQualifier
+
+var (
+ pluginVersion = plugin.Version{Number: 1, Stage: stage.Alpha}
+ supportedProjectVersions = []config.Version{cfgv3.Version}
+ pluginKey = plugin.KeyFor(Plugin{})
+)
+
+// Plugin implements the plugin.Full interface
+type Plugin struct {
+ initSubcommand
+ editSubcommand
+}
+
+var _ plugin.Init = Plugin{}
+
+// Name returns the name of the plugin
+func (Plugin) Name() string { return pluginName }
+
+// Version returns the version of the grafana plugin
+func (Plugin) Version() plugin.Version { return pluginVersion }
+
+// SupportedProjectVersions returns an array with all project versions supported by the plugin
+func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions }
+
+// GetInitSubcommand will return the subcommand which is responsible for initializing and scaffolding grafana manifests
+func (p Plugin) GetInitSubcommand() plugin.InitSubcommand { return &p.initSubcommand }
+
+// GetEditSubcommand will return the subcommand which is responsible for adding grafana manifests
+func (p Plugin) GetEditSubcommand() plugin.EditSubcommand { return &p.editSubcommand }
+
+type pluginConfig struct{}
+
+// DeprecationWarning define the deprecation message or return empty when plugin is not deprecated
+func (p Plugin) DeprecationWarning() string {
+ return ""
+}
diff --git a/pkg/plugins/optional/grafana/v1alpha/scaffolds/edit.go b/pkg/plugins/optional/grafana/v1alpha/scaffolds/edit.go
new file mode 100644
index 00000000000..132c229c0f2
--- /dev/null
+++ b/pkg/plugins/optional/grafana/v1alpha/scaffolds/edit.go
@@ -0,0 +1,192 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package scaffolds
+
+import (
+ "fmt"
+ "io"
+ log "log/slog"
+ "os"
+ "strings"
+
+ "sigs.k8s.io/yaml"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates"
+)
+
+var _ plugins.Scaffolder = &editScaffolder{}
+
+const configFilePath = "grafana/custom-metrics/config.yaml"
+
+type editScaffolder struct {
+ // fs is the filesystem that will be used by the scaffolder
+ fs machinery.Filesystem
+}
+
+// NewEditScaffolder returns a new Scaffolder for project edition operations
+func NewEditScaffolder() plugins.Scaffolder {
+ return &editScaffolder{}
+}
+
+// InjectFS implements cmdutil.Scaffolder
+func (s *editScaffolder) InjectFS(fs machinery.Filesystem) {
+ s.fs = fs
+}
+
+func fileExist(configFilePath string) bool {
+ if _, err := os.Stat(configFilePath); os.IsNotExist(err) {
+ return false
+ }
+ return true
+}
+
+func loadConfig(configPath string) ([]templates.CustomMetricItem, error) {
+ if !fileExist(configPath) {
+ return nil, nil
+ }
+
+ //nolint:gosec
+ f, err := os.Open(configPath)
+ if err != nil {
+ return nil, fmt.Errorf("error loading plugin config: %w", err)
+ }
+
+ items, err := configReader(f)
+ if err != nil {
+ return nil, fmt.Errorf("error reading config.yaml: %w", err)
+ }
+
+ if err = f.Close(); err != nil {
+ return nil, fmt.Errorf("could not close config.yaml: %w", err)
+ }
+
+ return items, nil
+}
+
+func configReader(reader io.Reader) ([]templates.CustomMetricItem, error) {
+ yamlFile, err := io.ReadAll(reader)
+ if err != nil {
+ return nil, fmt.Errorf("error reading config.yaml: %w", err)
+ }
+
+ config := templates.CustomMetricsConfig{}
+
+ err = yaml.Unmarshal(yamlFile, &config)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing config.yaml: %w", err)
+ }
+
+ validatedMetricItems := validateCustomMetricItems(config.CustomMetrics)
+
+ return validatedMetricItems, nil
+}
+
+func validateCustomMetricItems(rawItems []templates.CustomMetricItem) []templates.CustomMetricItem {
+ // 1. Filter items of missing `Metric` or `Type`
+ var filterResult []templates.CustomMetricItem
+ for _, item := range rawItems {
+ if hasFields(item) {
+ filterResult = append(filterResult, item)
+ }
+ }
+
+ // 2. Fill Expr and Unit if missing
+ validatedItems := make([]templates.CustomMetricItem, len(filterResult))
+ for i, item := range filterResult {
+ item = fillMissingExpr(item)
+ validatedItems[i] = fillMissingUnit(item)
+ }
+
+ return validatedItems
+}
+
+func hasFields(item templates.CustomMetricItem) bool {
+ // If `Expr` exists, return true
+ if item.Expr != "" {
+ return true
+ }
+
+ // If `Metric` & valid `Type` exists, return true
+ metricType := strings.ToLower(item.Type)
+ if item.Metric != "" && (metricType == "counter" || metricType == "gauge" || metricType == "histogram") {
+ return true
+ }
+
+ return false
+}
+
+// TODO: Prom_ql exprs can improved to be more pratical and applicable
+func fillMissingExpr(item templates.CustomMetricItem) templates.CustomMetricItem {
+ if item.Expr == "" {
+ switch strings.ToLower(item.Type) {
+ case "counter":
+ item.Expr = "sum(rate(" + item.Metric + `{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)`
+ case "histogram":
+ //nolint:lll
+ item.Expr = "histogram_quantile(0.90, sum by(instance, le) (rate(" + item.Metric + `{job=\"$job\", namespace=\"$namespace\"}[5m])))`
+ default: // gauge
+ item.Expr = item.Metric
+ }
+ }
+ return item
+}
+
+func fillMissingUnit(item templates.CustomMetricItem) templates.CustomMetricItem {
+ if item.Unit == "" {
+ name := strings.ToLower(item.Metric)
+ item.Unit = "none"
+ if strings.Contains(name, "second") || strings.Contains(name, "duration") {
+ item.Unit = "s"
+ } else if strings.Contains(name, "byte") {
+ item.Unit = "bytes"
+ } else if strings.Contains(name, "ratio") {
+ item.Unit = "percent"
+ }
+ }
+ return item
+}
+
+// Scaffold implements cmdutil.Scaffolder
+func (s *editScaffolder) Scaffold() error {
+ log.Info("Generating Grafana manifests to visualize controller status...")
+
+ // Initialize the machinery.Scaffold that will write the files to disk
+ scaffold := machinery.NewScaffold(s.fs)
+
+ configPath := configFilePath
+
+ templatesBuilder := []machinery.Builder{
+ &templates.RuntimeManifest{},
+ &templates.ResourcesManifest{},
+ &templates.CustomMetricsConfigManifest{ConfigPath: configPath},
+ }
+
+ configItems, err := loadConfig(configPath)
+ if err == nil && len(configItems) > 0 {
+ templatesBuilder = append(templatesBuilder, &templates.CustomMetricsDashManifest{Items: configItems})
+ } else if err != nil {
+ _, _ = fmt.Fprintf(os.Stderr, "Error on scaffolding manifest for custom metris:\n%v", err)
+ }
+
+ if err = scaffold.Execute(templatesBuilder...); err != nil {
+ return fmt.Errorf("error scaffolding Grafana manifests: %w", err)
+ }
+
+ return nil
+}
diff --git a/pkg/plugins/optional/grafana/v1alpha/scaffolds/init.go b/pkg/plugins/optional/grafana/v1alpha/scaffolds/init.go
new file mode 100644
index 00000000000..f67ffb621e8
--- /dev/null
+++ b/pkg/plugins/optional/grafana/v1alpha/scaffolds/init.go
@@ -0,0 +1,62 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package scaffolds
+
+import (
+ "fmt"
+ log "log/slog"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates"
+)
+
+var _ plugins.Scaffolder = &initScaffolder{}
+
+type initScaffolder struct {
+ // fs is the filesystem that will be used by the scaffolder
+ fs machinery.Filesystem
+}
+
+// NewInitScaffolder returns a new Scaffolder for project initialization operations
+func NewInitScaffolder() plugins.Scaffolder {
+ return &initScaffolder{}
+}
+
+// InjectFS implements cmdutil.Scaffolder
+func (s *initScaffolder) InjectFS(fs machinery.Filesystem) {
+ s.fs = fs
+}
+
+// Scaffold implements cmdutil.Scaffolder
+func (s *initScaffolder) Scaffold() error {
+ log.Info("Generating Grafana manifests to visualize controller status...")
+
+ // Initialize the machinery.Scaffold that will write the files to disk
+ scaffold := machinery.NewScaffold(s.fs)
+
+ err := scaffold.Execute(
+ &templates.RuntimeManifest{},
+ &templates.ResourcesManifest{},
+ &templates.CustomMetricsConfigManifest{ConfigPath: configFilePath},
+ )
+ if err != nil {
+ return fmt.Errorf("error scaffolding Grafana memanifests: %w", err)
+ }
+
+ return nil
+}
diff --git a/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/custom.go b/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/custom.go
new file mode 100644
index 00000000000..faa8dcb73bc
--- /dev/null
+++ b/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/custom.go
@@ -0,0 +1,57 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package templates
+
+import (
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &CustomMetricsConfigManifest{}
+
+// CustomMetricsConfigManifest scaffolds a file that defines the kustomization scheme for the prometheus folder
+type CustomMetricsConfigManifest struct {
+ machinery.TemplateMixin
+ ConfigPath string
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *CustomMetricsConfigManifest) SetTemplateDefaults() error {
+ f.Path = f.ConfigPath
+
+ f.TemplateBody = customMetricsConfigTemplate
+
+ f.IfExistsAction = machinery.SkipFile
+
+ return nil
+}
+
+const customMetricsConfigTemplate = `---
+customMetrics:
+# - metric: # Raw custom metric (required)
+# type: # Metric type: counter/gauge/histogram (required)
+# expr: # Prom_ql for the metric (optional)
+# unit: # Unit of measurement, examples: s,none,bytes,percent,etc. (optional)
+#
+#
+# Example:
+# ---
+# customMetrics:
+# - metric: foo_bar
+# unit: none
+# type: histogram
+# expr: histogram_quantile(0.90, sum by(instance, le) (rate(foo_bar{job=\"$job\", namespace=\"$namespace\"}[5m])))
+`
diff --git a/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/custom_metrics.go b/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/custom_metrics.go
new file mode 100644
index 00000000000..9cdece75a2d
--- /dev/null
+++ b/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/custom_metrics.go
@@ -0,0 +1,312 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package templates
+
+import (
+ "bytes"
+ "fmt"
+ "path/filepath"
+ "strings"
+ "text/template"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+// CustomMetricsConfig represents the configuration for custom metrics
+type CustomMetricsConfig struct {
+ CustomMetrics []CustomMetricItem `json:"customMetrics"`
+}
+
+// CustomMetricItem defines the config items for the custom metrics
+type CustomMetricItem struct {
+ Metric string `json:"metric"`
+ Type string `json:"type"`
+ Expr string `json:"expr,omitempty"`
+ Unit string `json:"unit,omitempty"`
+}
+
+var _ machinery.Template = &CustomMetricsDashManifest{}
+
+// CustomMetricsDashManifest scaffolds a file that defines the kustomization scheme for the prometheus folder
+type CustomMetricsDashManifest struct {
+ machinery.TemplateMixin
+
+ Items []CustomMetricItem
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *CustomMetricsDashManifest) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join("grafana", "custom-metrics", "custom-metrics-dashboard.json")
+ }
+
+ defaultTemplate, err := f.createTemplate()
+ if err != nil {
+ return err
+ }
+
+ f.TemplateBody = defaultTemplate
+
+ f.IfExistsAction = machinery.OverwriteFile
+
+ return nil
+}
+
+var fns = template.FuncMap{
+ "plus1": func(x int) int {
+ return x + 1
+ },
+ "hasSuffix": strings.HasSuffix,
+}
+
+func (f *CustomMetricsDashManifest) createTemplate() (string, error) {
+ t := template.Must(template.New("customMetricsDashTemplate").Funcs(fns).Parse(customMetricsDashTemplate))
+
+ outputTmpl := &bytes.Buffer{}
+ if err := t.Execute(outputTmpl, f.Items); err != nil {
+ return "", fmt.Errorf("error when generating manifest from config: %w", err)
+ }
+
+ return outputTmpl.String(), nil
+}
+
+const customMetricsDashTemplate = `{
+ "__inputs": [
+ {
+ "name": "DS_PROMETHEUS",
+ "label": "Prometheus",
+ "description": "",
+ "type": "datasource",
+ "pluginId": "prometheus",
+ "pluginName": "Prometheus"
+ }
+ ],
+ "__requires": [
+ {
+ "type": "datasource",
+ "id": "prometheus",
+ "name": "Prometheus",
+ "version": "1.0.0"
+ }
+ ],
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "target": {
+ "limit": 100,
+ "matchAny": false,
+ "tags": [],
+ "type": "dashboard"
+ },
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "links": [],
+ "liveNow": false,
+ "panels": [{{ $n := len . }}{{ range $i, $e := . }}
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "continuous-GrYlRd"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 20,
+ "gradientMode": "scheme",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "smooth",
+ "lineWidth": 3,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "{{ .Unit }}"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 7,
+ "w": 24
+ },
+ "interval": "1m",
+ "links": [],
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "8.4.3",
+ "targets": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "exemplar": true,
+ "expr": "{{ .Expr }}",
+ "format": "time_series",
+ "interval": "",
+ "intervalFactor": 2,
+ "refId": "A",
+ "step": 10
+ }
+ ],
+ "title": "{{ .Metric }} ({{ .Type }})",
+{{- if hasSuffix .Metric "_info" }}
+ "transformations": [
+ {
+ "id": "labelsToFields",
+ "options": {
+ "mode": "rows"
+ }
+ }
+ ],
+ "type": "table"
+{{- else }}
+ "type": "timeseries"
+{{- end }}
+ }{{ if ne (plus1 $i) $n }},
+ {{end}}{{end}}
+ ],
+ "refresh": "",
+ "style": "dark",
+ "tags": [],
+ "templating": {
+ "list": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
+ "hide": 0,
+ "includeAll": false,
+ "multi": false,
+ "name": "job",
+ "options": [],
+ "query": {
+ "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
+ "refId": "StandardVariableQuery"
+ },
+ "refresh": 2,
+ "regex": "",
+ "skipUrlSync": false,
+ "sort": 0,
+ "type": "query"
+ },
+ {
+ "current": {
+ "selected": false,
+ "text": "observability",
+ "value": "observability"
+ },
+ "datasource": "${DS_PROMETHEUS}",
+ "definition": "label_values(controller_runtime_reconcile_total, namespace)",
+ "hide": 0,
+ "includeAll": false,
+ "multi": false,
+ "name": "namespace",
+ "options": [],
+ "query": {
+ "query": "label_values(controller_runtime_reconcile_total, namespace)",
+ "refId": "StandardVariableQuery"
+ },
+ "refresh": 1,
+ "regex": "",
+ "skipUrlSync": false,
+ "sort": 0,
+ "type": "query"
+ },
+ {
+ "current": {
+ "selected": false,
+ "text": "All",
+ "value": "$__all"
+ },
+ "datasource": "${DS_PROMETHEUS}",
+ "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
+ "hide": 2,
+ "includeAll": true,
+ "label": "pod",
+ "multi": true,
+ "name": "pod",
+ "options": [],
+ "query": {
+ "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
+ "refId": "StandardVariableQuery"
+ },
+ "refresh": 2,
+ "regex": "",
+ "skipUrlSync": false,
+ "sort": 0,
+ "type": "query"
+ }
+ ]
+ },
+ "time": {
+ "from": "now-15m",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "",
+ "title": "Custom-Metrics",
+ "weekStart": ""
+}
+`
diff --git a/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/resources.go b/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/resources.go
new file mode 100644
index 00000000000..928593a2609
--- /dev/null
+++ b/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/resources.go
@@ -0,0 +1,354 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package templates
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &ResourcesManifest{}
+
+// ResourcesManifest scaffolds a file that defines the kustomization scheme for the prometheus folder
+type ResourcesManifest struct {
+ machinery.TemplateMixin
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *ResourcesManifest) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join("grafana", "controller-resources-metrics.json")
+ }
+
+ // Grafana syntax use {{ }} quite often, which is collided with default delimiter for go template parsing.
+ // Provide an alternative delimiter here to avoid overlaps.
+ f.SetDelim("[[", "]]")
+ f.TemplateBody = controllerResourcesTemplate
+
+ f.IfExistsAction = machinery.OverwriteFile
+
+ return nil
+}
+
+const controllerResourcesTemplate = `{
+ "__inputs": [
+ {
+ "name": "DS_PROMETHEUS",
+ "label": "Prometheus",
+ "description": "",
+ "type": "datasource",
+ "pluginId": "prometheus",
+ "pluginName": "Prometheus"
+ }
+ ],
+ "__requires": [
+ {
+ "type": "datasource",
+ "id": "prometheus",
+ "name": "Prometheus",
+ "version": "1.0.0"
+ }
+ ],
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "target": {
+ "limit": 100,
+ "matchAny": false,
+ "tags": [],
+ "type": "dashboard"
+ },
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "links": [],
+ "liveNow": false,
+ "panels": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "continuous-GrYlRd"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 20,
+ "gradientMode": "scheme",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "smooth",
+ "lineWidth": 3,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "percent"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 0
+ },
+ "id": 2,
+ "interval": "1m",
+ "links": [],
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "8.4.3",
+ "targets": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "exemplar": true,
+ "expr": "rate(process_cpu_seconds_total{job=\"$job\", namespace=\"$namespace\", pod=\"$pod\"}[5m]) * 100",
+ "format": "time_series",
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "Pod: {{pod}} | Container: {{container}}",
+ "refId": "A",
+ "step": 10
+ }
+ ],
+ "title": "Controller CPU Usage",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "continuous-GrYlRd"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 20,
+ "gradientMode": "scheme",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "smooth",
+ "lineWidth": 3,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "bytes"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 0
+ },
+ "id": 4,
+ "interval": "1m",
+ "links": [],
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "8.4.3",
+ "targets": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "exemplar": true,
+ "expr": "process_resident_memory_bytes{job=\"$job\", namespace=\"$namespace\", pod=\"$pod\"}",
+ "format": "time_series",
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "Pod: {{pod}} | Container: {{container}}",
+ "refId": "A",
+ "step": 10
+ }
+ ],
+ "title": "Controller Memory Usage",
+ "type": "timeseries"
+ }
+ ],
+ "refresh": "",
+ "style": "dark",
+ "tags": [],
+ "templating": {
+ "list": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
+ "hide": 0,
+ "includeAll": false,
+ "multi": false,
+ "name": "job",
+ "options": [],
+ "query": {
+ "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
+ "refId": "StandardVariableQuery"
+ },
+ "refresh": 2,
+ "regex": "",
+ "skipUrlSync": false,
+ "sort": 0,
+ "type": "query"
+ },
+ {
+ "current": {
+ "selected": false,
+ "text": "observability",
+ "value": "observability"
+ },
+ "datasource": "${DS_PROMETHEUS}",
+ "definition": "label_values(controller_runtime_reconcile_total, namespace)",
+ "hide": 0,
+ "includeAll": false,
+ "multi": false,
+ "name": "namespace",
+ "options": [],
+ "query": {
+ "query": "label_values(controller_runtime_reconcile_total, namespace)",
+ "refId": "StandardVariableQuery"
+ },
+ "refresh": 1,
+ "regex": "",
+ "skipUrlSync": false,
+ "sort": 0,
+ "type": "query"
+ },
+ {
+ "current": {
+ "selected": false,
+ "text": "All",
+ "value": "$__all"
+ },
+ "datasource": "${DS_PROMETHEUS}",
+ "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
+ "hide": 2,
+ "includeAll": true,
+ "label": "pod",
+ "multi": true,
+ "name": "pod",
+ "options": [],
+ "query": {
+ "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
+ "refId": "StandardVariableQuery"
+ },
+ "refresh": 2,
+ "regex": "",
+ "skipUrlSync": false,
+ "sort": 0,
+ "type": "query"
+ }
+ ]
+ },
+ "time": {
+ "from": "now-15m",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "",
+ "title": "Controller-Resources-Metrics",
+ "weekStart": ""
+}
+`
diff --git a/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/runtime.go b/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/runtime.go
new file mode 100644
index 00000000000..8973ac8fb45
--- /dev/null
+++ b/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/runtime.go
@@ -0,0 +1,946 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package templates
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &RuntimeManifest{}
+
+// RuntimeManifest scaffolds a file that defines the kustomization scheme for the prometheus folder
+type RuntimeManifest struct {
+ machinery.TemplateMixin
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *RuntimeManifest) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join("grafana", "controller-runtime-metrics.json")
+ }
+
+ // Grafana syntax use {{ }} quite often, which is collided with default delimiter for go template parsing.
+ // Provide an alternative delimiter here to avoid overlaps.
+ f.SetDelim("[[", "]]")
+ f.TemplateBody = controllerRuntimeTemplate
+ f.IfExistsAction = machinery.OverwriteFile
+
+ return nil
+}
+
+//nolint:lll
+const controllerRuntimeTemplate = `{
+ "__inputs": [
+ {
+ "name": "DS_PROMETHEUS",
+ "label": "Prometheus",
+ "description": "",
+ "type": "datasource",
+ "pluginId": "prometheus",
+ "pluginName": "Prometheus"
+ }
+ ],
+ "__requires": [
+ {
+ "type": "datasource",
+ "id": "prometheus",
+ "name": "Prometheus",
+ "version": "1.0.0"
+ }
+ ],
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": {
+ "type": "datasource",
+ "uid": "grafana"
+ },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "target": {
+ "limit": 100,
+ "matchAny": false,
+ "tags": [],
+ "type": "dashboard"
+ },
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "links": [],
+ "liveNow": false,
+ "panels": [
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 0
+ },
+ "id": 9,
+ "panels": [],
+ "title": "Reconciliation Metrics",
+ "type": "row"
+ },
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "fieldConfig": {
+ "defaults": {
+ "mappings": [],
+ "thresholds": {
+ "mode": "percentage",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "orange",
+ "value": 70
+ },
+ {
+ "color": "red",
+ "value": 85
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 3,
+ "x": 0,
+ "y": 1
+ },
+ "id": 24,
+ "options": {
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": ["lastNotNull"],
+ "fields": "",
+ "values": false
+ },
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true
+ },
+ "pluginVersion": "9.5.3",
+ "targets": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "exemplar": true,
+ "expr": "controller_runtime_active_workers{job=\"$job\", namespace=\"$namespace\"}",
+ "interval": "",
+ "legendFormat": "{{controller}} {{instance}}",
+ "refId": "A"
+ }
+ ],
+ "title": "Number of workers in use",
+ "type": "gauge"
+ },
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "description": "Total number of reconciliations per controller",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "continuous-GrYlRd"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 20,
+ "gradientMode": "scheme",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "smooth",
+ "lineWidth": 3,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "cpm"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 11,
+ "x": 3,
+ "y": 1
+ },
+ "id": 7,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "editorMode": "code",
+ "exemplar": true,
+ "expr": "sum(rate(controller_runtime_reconcile_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)",
+ "interval": "",
+ "legendFormat": "{{instance}} {{pod}}",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Total Reconciliation Count Per Controller",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "description": "Total number of reconciliation errors per controller",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "continuous-GrYlRd"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 20,
+ "gradientMode": "scheme",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "smooth",
+ "lineWidth": 3,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "cpm"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 10,
+ "x": 14,
+ "y": 1
+ },
+ "id": 6,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "editorMode": "code",
+ "exemplar": true,
+ "expr": "sum(rate(controller_runtime_reconcile_errors_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)",
+ "interval": "",
+ "legendFormat": "{{instance}} {{pod}}",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Reconciliation Error Count Per Controller",
+ "type": "timeseries"
+ },
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 9
+ },
+ "id": 11,
+ "panels": [],
+ "title": "Work Queue Metrics",
+ "type": "row"
+ },
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "fieldConfig": {
+ "defaults": {
+ "mappings": [],
+ "thresholds": {
+ "mode": "percentage",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "orange",
+ "value": 70
+ },
+ {
+ "color": "red",
+ "value": 85
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 3,
+ "x": 0,
+ "y": 10
+ },
+ "id": 22,
+ "options": {
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": ["lastNotNull"],
+ "fields": "",
+ "values": false
+ },
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true
+ },
+ "pluginVersion": "9.5.3",
+ "targets": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "exemplar": true,
+ "expr": "workqueue_depth{job=\"$job\", namespace=\"$namespace\"}",
+ "interval": "",
+ "legendFormat": "",
+ "refId": "A"
+ }
+ ],
+ "title": "WorkQueue Depth",
+ "type": "gauge"
+ },
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "description": "How long in seconds an item stays in workqueue before being requested",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "normal"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "s"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 11,
+ "x": 3,
+ "y": 10
+ },
+ "id": 13,
+ "options": {
+ "legend": {
+ "calcs": [
+ "max",
+ "mean"
+ ],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "exemplar": true,
+ "expr": "histogram_quantile(0.50, sum(rate(workqueue_queue_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
+ "interval": "",
+ "legendFormat": "P50 {{name}} {{instance}} ",
+ "refId": "A"
+ },
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "exemplar": true,
+ "expr": "histogram_quantile(0.90, sum(rate(workqueue_queue_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
+ "hide": false,
+ "interval": "",
+ "legendFormat": "P90 {{name}} {{instance}} ",
+ "refId": "B"
+ },
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "exemplar": true,
+ "expr": "histogram_quantile(0.99, sum(rate(workqueue_queue_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
+ "hide": false,
+ "interval": "",
+ "legendFormat": "P99 {{name}} {{instance}} ",
+ "refId": "C"
+ }
+ ],
+ "title": "Seconds For Items Stay In Queue (before being requested) (P50, P90, P99)",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "continuous-GrYlRd"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 20,
+ "gradientMode": "scheme",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "smooth",
+ "lineWidth": 3,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "ops"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 10,
+ "x": 14,
+ "y": 10
+ },
+ "id": 15,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "pluginVersion": "8.4.3",
+ "targets": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "exemplar": true,
+ "expr": "sum(rate(workqueue_adds_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name)",
+ "interval": "",
+ "legendFormat": "{{name}} {{instance}}",
+ "refId": "A"
+ }
+ ],
+ "title": "Work Queue Add Rate",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "description": "How many seconds of work has done that is in progress and hasn't been observed by work_duration.\nLarge values indicate stuck threads.\nOne can deduce the number of stuck threads by observing the rate at which this increases.",
+ "fieldConfig": {
+ "defaults": {
+ "mappings": [],
+ "thresholds": {
+ "mode": "percentage",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "orange",
+ "value": 70
+ },
+ {
+ "color": "red",
+ "value": 85
+ }
+ ]
+ },
+ "unit": "s"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 9,
+ "w": 3,
+ "x": 0,
+ "y": 18
+ },
+ "id": 23,
+ "options": {
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": ["lastNotNull"],
+ "fields": "",
+ "values": false
+ },
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true
+ },
+ "pluginVersion": "9.5.3",
+ "targets": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "exemplar": true,
+ "expr": "rate(workqueue_unfinished_work_seconds{job=\"$job\", namespace=\"$namespace\"}[5m])",
+ "interval": "",
+ "legendFormat": "",
+ "refId": "A"
+ }
+ ],
+ "title": "Unfinished Seconds",
+ "type": "gauge"
+ },
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "description": "How long in seconds processing an item from workqueue takes.",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "s"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 9,
+ "w": 11,
+ "x": 3,
+ "y": 18
+ },
+ "id": 19,
+ "options": {
+ "legend": {
+ "calcs": [
+ "max",
+ "mean"
+ ],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "exemplar": true,
+ "expr": "histogram_quantile(0.50, sum(rate(workqueue_work_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
+ "interval": "",
+ "legendFormat": "P50 {{name}} {{instance}} ",
+ "refId": "A"
+ },
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "exemplar": true,
+ "expr": "histogram_quantile(0.90, sum(rate(workqueue_work_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
+ "hide": false,
+ "interval": "",
+ "legendFormat": "P90 {{name}} {{instance}} ",
+ "refId": "B"
+ },
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "exemplar": true,
+ "expr": "histogram_quantile(0.99, sum(rate(workqueue_work_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
+ "hide": false,
+ "interval": "",
+ "legendFormat": "P99 {{name}} {{instance}} ",
+ "refId": "C"
+ }
+ ],
+ "title": "Seconds Processing Items From WorkQueue (P50, P90, P99)",
+ "type": "timeseries"
+ },
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "description": "Total number of retries handled by workqueue",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "continuous-GrYlRd"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 20,
+ "gradientMode": "scheme",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "smooth",
+ "lineWidth": 3,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "ops"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 9,
+ "w": 10,
+ "x": 14,
+ "y": 18
+ },
+ "id": 17,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "exemplar": true,
+ "expr": "sum(rate(workqueue_retries_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name)",
+ "interval": "",
+ "legendFormat": "{{name}} {{instance}} ",
+ "refId": "A"
+ }
+ ],
+ "title": "Work Queue Retries Rate",
+ "type": "timeseries"
+ }
+ ],
+ "refresh": "",
+ "style": "dark",
+ "tags": [],
+ "templating": {
+ "list": [
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
+ "hide": 0,
+ "includeAll": false,
+ "multi": false,
+ "name": "job",
+ "options": [],
+ "query": {
+ "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
+ "refId": "StandardVariableQuery"
+ },
+ "refresh": 2,
+ "regex": "",
+ "skipUrlSync": false,
+ "sort": 0,
+ "type": "query"
+ },
+ {
+ "datasource": "${DS_PROMETHEUS}",
+ "definition": "label_values(controller_runtime_reconcile_total, namespace)",
+ "hide": 0,
+ "includeAll": false,
+ "multi": false,
+ "name": "namespace",
+ "options": [],
+ "query": {
+ "query": "label_values(controller_runtime_reconcile_total, namespace)",
+ "refId": "StandardVariableQuery"
+ },
+ "refresh": 1,
+ "regex": "",
+ "skipUrlSync": false,
+ "sort": 0,
+ "type": "query"
+ },
+ {
+ "current": {
+ "selected": true,
+ "text": [
+ "All"
+ ],
+ "value": [
+ "$__all"
+ ]
+ },
+ "datasource": "${DS_PROMETHEUS}",
+ "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
+ "hide": 2,
+ "includeAll": true,
+ "label": "pod",
+ "multi": true,
+ "name": "pod",
+ "options": [],
+ "query": {
+ "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
+ "refId": "StandardVariableQuery"
+ },
+ "refresh": 2,
+ "regex": "",
+ "skipUrlSync": false,
+ "sort": 0,
+ "type": "query"
+ }
+ ]
+ },
+ "time": {
+ "from": "now-15m",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "",
+ "title": "Controller-Runtime-Metrics",
+ "weekStart": ""
+}
+`
diff --git a/pkg/plugins/optional/helm/v1alpha/commons.go b/pkg/plugins/optional/helm/v1alpha/commons.go
new file mode 100644
index 00000000000..02370c48dae
--- /dev/null
+++ b/pkg/plugins/optional/helm/v1alpha/commons.go
@@ -0,0 +1,38 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha
+
+import (
+ "errors"
+ "fmt"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+)
+
+func insertPluginMetaToConfig(target config.Config, cfg pluginConfig) error {
+ err := target.DecodePluginConfig(pluginKey, cfg)
+ if !errors.As(err, &config.UnsupportedFieldError{}) {
+ if err != nil && !errors.As(err, &config.PluginKeyNotFoundError{}) {
+ return fmt.Errorf("error decoding plugin configuration: %w", err)
+ }
+ if err = target.EncodePluginConfig(pluginKey, cfg); err != nil {
+ return fmt.Errorf("error encoding plugin config: %w", err)
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/plugins/optional/helm/v1alpha/edit.go b/pkg/plugins/optional/helm/v1alpha/edit.go
new file mode 100644
index 00000000000..e462eb8e6dd
--- /dev/null
+++ b/pkg/plugins/optional/helm/v1alpha/edit.go
@@ -0,0 +1,143 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha
+
+import (
+ "fmt"
+ log "log/slog"
+ "os"
+ "path/filepath"
+
+ "github.com/spf13/pflag"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds"
+)
+
+var _ plugin.EditSubcommand = &editSubcommand{}
+
+type editSubcommand struct {
+ config config.Config
+ force bool
+}
+
+//nolint:lll
+func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
+ subcmdMeta.Description = `Initialize or update a Helm chart to distribute the project under the dist/ directory.
+
+**NOTE** Before running the edit command, ensure you first execute 'make manifests' to regenerate
+the latest Helm chart with your most recent changes.`
+
+ subcmdMeta.Examples = fmt.Sprintf(`# Initialize or update a Helm chart to distribute the project under the dist/ directory
+ %[1]s edit --plugins=%[2]s
+
+# Update the Helm chart under the dist/ directory and overwrite all files
+ %[1]s edit --plugins=%[2]s --force
+
+**IMPORTANT**: If the "--force" flag is not used, the following files will not be updated to preserve your customizations:
+dist/chart/
+├── values.yaml
+└── templates/
+ └── manager/
+ └── manager.yaml
+
+The following files are never updated after their initial creation:
+ - chart/Chart.yaml
+ - chart/templates/_helpers.tpl
+ - chart/.helmignore
+
+All other files are updated without the usage of the '--force=true' flag
+when the edit option is used to ensure that the
+manifests in the chart align with the latest changes.
+`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
+}
+
+func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
+ fs.BoolVar(&p.force, "force", false, "if true, regenerates all the files")
+}
+
+func (p *editSubcommand) InjectConfig(c config.Config) error {
+ p.config = c
+ return nil
+}
+
+func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
+ scaffolder := scaffolds.NewHelmScaffolder(p.config, p.force)
+ scaffolder.InjectFS(fs)
+ err := scaffolder.Scaffold()
+ if err != nil {
+ return fmt.Errorf("error scaffolding Helm chart: %w", err)
+ }
+
+ // Track the resources following a declarative approach
+ return insertPluginMetaToConfig(p.config, pluginConfig{})
+}
+
+// PostScaffold automatically uncomments cert-manager installation when webhooks are present
+func (p *editSubcommand) PostScaffold() error {
+ hasWebhooks := hasWebhooksWith(p.config)
+
+ if hasWebhooks {
+ workflowFile := filepath.Join(".github", "workflows", "test-chart.yml")
+ if _, err := os.Stat(workflowFile); err != nil {
+ log.Info(
+ "Workflow file not found, unable to uncomment cert-manager installation",
+ "error", err,
+ "file", workflowFile,
+ )
+ return nil
+ }
+ //nolint:lll
+ target := `
+# - name: Install cert-manager via Helm
+# run: |
+# helm repo add jetstack https://charts.jetstack.io
+# helm repo update
+# helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set crds.enabled=true
+#
+# - name: Wait for cert-manager to be ready
+# run: |
+# kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager
+# kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-cainjector
+# kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-webhook`
+ if err := util.UncommentCode(workflowFile, target, "#"); err != nil {
+ hasUncommented, errCheck := util.HasFileContentWith(workflowFile, "- name: Install cert-manager via Helm")
+ if !hasUncommented || errCheck != nil {
+ log.Warn("Failed to uncomment cert-manager installation in workflow file", "error", err, "file", workflowFile)
+ }
+ }
+ }
+ return nil
+}
+
+func hasWebhooksWith(c config.Config) bool {
+ resources, err := c.GetResources()
+ if err != nil {
+ return false
+ }
+
+ for _, res := range resources {
+ if res.HasDefaultingWebhook() || res.HasValidationWebhook() || res.HasConversionWebhook() {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/pkg/plugins/optional/helm/v1alpha/plugin.go b/pkg/plugins/optional/helm/v1alpha/plugin.go
new file mode 100644
index 00000000000..231a15860c1
--- /dev/null
+++ b/pkg/plugins/optional/helm/v1alpha/plugin.go
@@ -0,0 +1,60 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha
+
+import (
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
+ "sigs.k8s.io/kubebuilder/v4/pkg/model/stage"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins"
+)
+
+const pluginName = "helm." + plugins.DefaultNameQualifier
+
+var (
+ pluginVersion = plugin.Version{Number: 1, Stage: stage.Alpha}
+ supportedProjectVersions = []config.Version{cfgv3.Version}
+ pluginKey = plugin.KeyFor(Plugin{})
+)
+
+// Plugin implements the plugin.Full interface
+type Plugin struct {
+ editSubcommand
+}
+
+var _ plugin.Edit = Plugin{}
+
+type pluginConfig struct{}
+
+// Name returns the name of the plugin
+func (Plugin) Name() string { return pluginName }
+
+// Version returns the version of the Helm plugin
+func (Plugin) Version() plugin.Version { return pluginVersion }
+
+// SupportedProjectVersions returns an array with all project versions supported by the plugin
+func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions }
+
+// GetEditSubcommand will return the subcommand which is responsible for adding and/or edit a helm chart
+func (p Plugin) GetEditSubcommand() plugin.EditSubcommand { return &p.editSubcommand }
+
+// DeprecationWarning define the deprecation message or return empty when plugin is not deprecated
+func (p Plugin) DeprecationWarning() string {
+ return "helm/v1-alpha plugin is deprecated, use helm/v2-alpha instead which " +
+ "provides dynamic Helm chart generation from kustomize output"
+}
diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/edit.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/edit.go
new file mode 100644
index 00000000000..0c2b35d3d0e
--- /dev/null
+++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/edit.go
@@ -0,0 +1,564 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package scaffolds
+
+import (
+ "fmt"
+ log "log/slog"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "sigs.k8s.io/yaml"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins"
+ deployimagev1alpha1 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates"
+ charttemplates "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates"
+ templatescertmanager "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/cert-manager"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/manager"
+ templatesmetrics "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/metrics"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/prometheus"
+ templateswebhooks "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/github"
+)
+
+var _ plugins.Scaffolder = &editScaffolder{}
+
+type editScaffolder struct {
+ config config.Config
+
+ fs machinery.Filesystem
+
+ force bool
+}
+
+// NewHelmScaffolder returns a new Scaffolder for HelmPlugin
+func NewHelmScaffolder(cfg config.Config, force bool) plugins.Scaffolder {
+ return &editScaffolder{
+ config: cfg,
+ force: force,
+ }
+}
+
+// InjectFS implements cmdutil.Scaffolder
+func (s *editScaffolder) InjectFS(fs machinery.Filesystem) {
+ s.fs = fs
+}
+
+// Scaffold scaffolds the Helm chart with the necessary files.
+func (s *editScaffolder) Scaffold() error {
+ log.Info("Generating Helm Chart to distribute project")
+
+ imagesEnvVars := s.getDeployImagesEnvVars()
+
+ scaffold := machinery.NewScaffold(s.fs,
+ machinery.WithConfig(s.config),
+ )
+
+ // Found webhooks by looking at the config our scaffolds files
+ mutatingWebhooks, validatingWebhooks, err := s.extractWebhooksFromGeneratedFiles()
+ if err != nil {
+ return fmt.Errorf("failed to extract webhooks: %w", err)
+ }
+ hasWebhooks := hasWebhooksWith(s.config) || (len(mutatingWebhooks) > 0 && len(validatingWebhooks) > 0)
+
+ buildScaffold := []machinery.Builder{
+ &github.HelmChartCI{},
+ &templates.HelmChart{},
+ &templates.HelmValues{
+ HasWebhooks: hasWebhooks,
+ DeployImages: imagesEnvVars,
+ Force: s.force,
+ },
+ &templates.HelmIgnore{},
+ &charttemplates.HelmHelpers{},
+ &manager.Deployment{
+ Force: s.force,
+ DeployImages: len(imagesEnvVars) > 0,
+ HasWebhooks: hasWebhooks,
+ },
+ &templatescertmanager.Certificate{HasWebhooks: hasWebhooks},
+ &templatesmetrics.Service{},
+ &prometheus.Monitor{},
+ }
+
+ if len(mutatingWebhooks) > 0 || len(validatingWebhooks) > 0 {
+ buildScaffold = append(buildScaffold,
+ &templateswebhooks.Template{
+ MutatingWebhooks: mutatingWebhooks,
+ ValidatingWebhooks: validatingWebhooks,
+ },
+ )
+ }
+
+ if hasWebhooks {
+ buildScaffold = append(buildScaffold,
+ &templateswebhooks.Service{},
+ )
+ }
+
+ if err = scaffold.Execute(buildScaffold...); err != nil {
+ return fmt.Errorf("error scaffolding helm-chart manifests: %w", err)
+ }
+
+ // Copy relevant files from config/ to dist/chart/templates/
+ err = s.copyConfigFiles()
+ if err != nil {
+ return fmt.Errorf("failed to copy manifests from config to dist/chart/templates/: %w", err)
+ }
+
+ return nil
+}
+
+// getDeployImagesEnvVars will return the values to append the envvars for projects
+// which has the APIs scaffolded with DeployImage plugin
+func (s *editScaffolder) getDeployImagesEnvVars() map[string]string {
+ deployImages := make(map[string]string)
+
+ pluginConfig := struct {
+ Resources []struct {
+ Kind string `json:"kind"`
+ Options map[string]string `json:"options"`
+ } `json:"resources"`
+ }{}
+
+ err := s.config.DecodePluginConfig(plugin.KeyFor(deployimagev1alpha1.Plugin{}), &pluginConfig)
+ if err == nil {
+ for _, res := range pluginConfig.Resources {
+ image, ok := res.Options["image"]
+ if ok {
+ deployImages[strings.ToUpper(res.Kind)] = image
+ }
+ }
+ }
+ return deployImages
+}
+
+// extractWebhooksFromGeneratedFiles parses the files generated by controller-gen under
+// config/webhooks and created Mutating and Validating helper structures to
+// generate the webhook manifest for the helm-chart
+func (s *editScaffolder) extractWebhooksFromGeneratedFiles() (mutatingWebhooks []templateswebhooks.DataWebhook,
+ validatingWebhooks []templateswebhooks.DataWebhook, err error,
+) {
+ manifestFile := "config/webhook/manifests.yaml"
+
+ if _, err = os.Stat(manifestFile); os.IsNotExist(err) {
+ log.Info("webhook manifests were not found", "path", manifestFile)
+ return nil, nil, nil
+ }
+
+ content, err := os.ReadFile(manifestFile)
+ if err != nil {
+ return nil, nil,
+ fmt.Errorf("failed to read %q: %w", manifestFile, err)
+ }
+
+ docs := strings.Split(string(content), "---")
+ for _, doc := range docs {
+ var webhookConfig struct {
+ Kind string `yaml:"kind"`
+ Webhooks []struct {
+ Name string `yaml:"name"`
+ ClientConfig struct {
+ Service struct {
+ Name string `yaml:"name"`
+ Namespace string `yaml:"namespace"`
+ Path string `yaml:"path"`
+ } `yaml:"service"`
+ } `yaml:"clientConfig"`
+ Rules []templateswebhooks.DataWebhookRule `yaml:"rules"`
+ FailurePolicy string `yaml:"failurePolicy"`
+ SideEffects string `yaml:"sideEffects"`
+ AdmissionReviewVersions []string `yaml:"admissionReviewVersions"`
+ } `yaml:"webhooks"`
+ }
+
+ if err := yaml.Unmarshal([]byte(doc), &webhookConfig); err != nil {
+ log.Error("fail to unmarshalling webhook YAML", "error", err)
+ continue
+ }
+
+ for _, w := range webhookConfig.Webhooks {
+ for i := range w.Rules {
+ if len(w.Rules[i].APIGroups) == 0 {
+ w.Rules[i].APIGroups = []string{""}
+ }
+ }
+ webhook := templateswebhooks.DataWebhook{
+ Name: w.Name,
+ ServiceName: fmt.Sprintf("%s-webhook-service", s.config.GetProjectName()),
+ Path: w.ClientConfig.Service.Path,
+ FailurePolicy: w.FailurePolicy,
+ SideEffects: w.SideEffects,
+ AdmissionReviewVersions: w.AdmissionReviewVersions,
+ Rules: w.Rules,
+ }
+
+ switch webhookConfig.Kind {
+ case "MutatingWebhookConfiguration":
+ mutatingWebhooks = append(mutatingWebhooks, webhook)
+ case "ValidatingWebhookConfiguration":
+ validatingWebhooks = append(validatingWebhooks, webhook)
+ }
+ }
+ }
+
+ return mutatingWebhooks, validatingWebhooks, nil
+}
+
+// Helper function to copy files from config/ to dist/chart/templates/
+func (s *editScaffolder) copyConfigFiles() error {
+ configDirs := []struct {
+ SrcDir string
+ DestDir string
+ SubDir string
+ }{
+ {"config/rbac", "dist/chart/templates/rbac", "rbac"},
+ {"config/crd/bases", "dist/chart/templates/crd", "crd"},
+ {"config/network-policy", "dist/chart/templates/network-policy", "networkPolicy"},
+ }
+
+ for _, dir := range configDirs {
+ // Check if the source directory exists
+ if _, err := os.Stat(dir.SrcDir); os.IsNotExist(err) {
+ // Skip if the source directory does not exist
+ continue
+ }
+
+ files, err := filepath.Glob(filepath.Join(dir.SrcDir, "*.yaml"))
+ if err != nil {
+ return fmt.Errorf("failed finding files in %q: %w", dir.SrcDir, err)
+ }
+
+ // Skip processing if the directory is empty (no matching files)
+ if len(files) == 0 {
+ continue
+ }
+
+ // Ensure destination directory exists
+ if err := os.MkdirAll(dir.DestDir, 0o755); err != nil {
+ return fmt.Errorf("failed to create directory %q: %w", dir.DestDir, err)
+ }
+
+ for _, srcFile := range files {
+ destFile := filepath.Join(dir.DestDir, filepath.Base(srcFile))
+
+ hasConvertionalWebhook := false
+ if hasWebhooksWith(s.config) {
+ resources, err := s.config.GetResources()
+ if err != nil {
+ break
+ }
+ for _, res := range resources {
+ if res.HasConversionWebhook() {
+ hasConvertionalWebhook = true
+ break
+ }
+ }
+ }
+
+ err := copyFileWithHelmLogic(srcFile, destFile, dir.SubDir, s.config.GetProjectName(), hasConvertionalWebhook)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+// copyFileWithHelmLogic reads the source file, modifies the content for Helm, applies patches
+// to spec.conversion if applicable, and writes it to the destination
+func copyFileWithHelmLogic(srcFile, destFile, subDir, projectName string, hasConvertionalWebhook bool) error {
+ if _, err := os.Stat(srcFile); os.IsNotExist(err) {
+ log.Info("Source file does not exist", "source_file", srcFile)
+ return fmt.Errorf("source file does not exist %q: %w", srcFile, err)
+ }
+
+ content, err := os.ReadFile(srcFile)
+ if err != nil {
+ log.Info("Error reading source file", "source_file", srcFile)
+ return fmt.Errorf("failed to read file %q: %w", srcFile, err)
+ }
+
+ contentStr := string(content)
+
+ // Skip kustomization.yaml or kustomizeconfig.yaml files
+ if strings.HasSuffix(srcFile, "kustomization.yaml") ||
+ strings.HasSuffix(srcFile, "kustomizeconfig.yaml") {
+ return nil
+ }
+
+ // Apply RBAC-specific replacements
+ if subDir == "rbac" {
+ contentStr = strings.ReplaceAll(contentStr,
+ "name: controller-manager",
+ "name: {{ .Values.controllerManager.serviceAccountName }}")
+ contentStr = strings.Replace(contentStr,
+ "name: metrics-reader",
+ fmt.Sprintf("name: %s-metrics-reader", projectName), 1)
+
+ contentStr = strings.ReplaceAll(contentStr,
+ "name: metrics-auth-role",
+ fmt.Sprintf("name: %s-metrics-auth-role", projectName))
+ contentStr = strings.Replace(contentStr,
+ "name: metrics-auth-rolebinding",
+ fmt.Sprintf("name: %s-metrics-auth-rolebinding", projectName), 1)
+
+ if strings.Contains(contentStr, ".Values.controllerManager.serviceAccountName") &&
+ strings.Contains(contentStr, "kind: ServiceAccount") &&
+ !strings.Contains(contentStr, "RoleBinding") {
+ // The generated Service Account does not have the annotations field so we must add it.
+ contentStr = strings.Replace(contentStr,
+ "metadata:", `metadata:
+ {{- if and .Values.controllerManager.serviceAccount .Values.controllerManager.serviceAccount.annotations }}
+ annotations:
+ {{- range $key, $value := .Values.controllerManager.serviceAccount.annotations }}
+ {{ $key }}: {{ $value }}
+ {{- end }}
+ {{- end }}`, 1)
+ }
+ contentStr = strings.ReplaceAll(contentStr,
+ "name: leader-election-role",
+ fmt.Sprintf("name: %s-leader-election-role", projectName))
+ contentStr = strings.Replace(contentStr,
+ "name: leader-election-rolebinding",
+ fmt.Sprintf("name: %s-leader-election-rolebinding", projectName), 1)
+ contentStr = strings.ReplaceAll(contentStr,
+ "name: manager-role",
+ fmt.Sprintf("name: %s-manager-role", projectName))
+ contentStr = strings.Replace(contentStr,
+ "name: manager-rolebinding",
+ fmt.Sprintf("name: %s-manager-rolebinding", projectName), 1)
+
+ // The generated files do not include the namespace
+ if strings.Contains(contentStr, "leader-election-rolebinding") ||
+ strings.Contains(contentStr, "leader-election-role") {
+ namespace := `
+ namespace: {{ .Release.Namespace }}`
+ contentStr = strings.Replace(contentStr, "metadata:", "metadata:"+namespace, 1)
+ }
+ }
+
+ // Conditionally handle CRD patches and annotations for CRDs
+ if subDir == "crd" {
+ kind, group := extractKindAndGroupFromFileName(filepath.Base(srcFile))
+ hasWebhookPatch := false
+
+ // Retrieve patch content for the CRD's spec.conversion, if it exists
+ patchContent, patchExists, errPatch := getCRDPatchContent(kind, group)
+ if errPatch != nil {
+ return errPatch
+ }
+
+ // If patch content exists, inject it under spec.conversion with Helm conditional
+ if patchExists {
+ conversionSpec := extractConversionSpec(patchContent)
+ // Projects scaffolded with old Kubebuilder versions does not have the conversion
+ // webhook properly generated because before 4.4.0 this feature was not fully addressed.
+ // The patch was added by default when should not. See the related fixes:
+ //
+ // Issue fixed in release 4.3.1: (which will cause the injection of webhook conditionals for projects without
+ // conversion webhooks)
+ // (kustomize/v2, go/v4): Corrected the generation of manifests under config/crd/patches
+ // to ensure the /convert service patch is only created for webhooks configured with --conversion. (#4280)
+ //
+ // Conversion webhook fully fixed in release 4.4.0:
+ // (kustomize/v2, go/v4): Fixed CA injection for conversion webhooks. Previously, the CA injection
+ // was applied incorrectly to all CRDs instead of only conversion types. The issue dates back to release 3.5.0
+ // due to kustomize/v2-alpha changes. Now, conversion webhooks are properly generated. (#4254, #4282)
+ if len(conversionSpec) > 0 && !hasConvertionalWebhook {
+ log.Warn("\n" +
+ "============================================================\n" +
+ "| [WARNING] Webhook Patch Issue Detected |\n" +
+ "============================================================\n" +
+ "Webhook patch found, but no conversion webhook is configured for this project.\n\n" +
+ "Note: Older scaffolds have an issue where the conversion webhook patch was \n" +
+ " scaffolded by default, and conversion webhook injection was not properly limited \n" +
+ " to specific CRDs.\n\n" +
+ "Recommended Action:\n" +
+ " - Upgrade your project to the latest available version.\n" +
+ " - Consider using the 'alpha generate' command.\n\n" +
+ "The cert-manager injection and webhook conversion patch found for CRDs will\n" +
+ "be skipped and NOT added to the Helm chart.\n" +
+ "============================================================")
+
+ hasWebhookPatch = false
+ } else {
+ contentStr = injectConversionSpecWithCondition(contentStr, conversionSpec)
+ hasWebhookPatch = true
+ }
+ }
+
+ // Inject annotations after "annotations:" in a single block without extra spaces
+ contentStr = injectAnnotations(contentStr, hasWebhookPatch)
+ }
+
+ // Remove existing labels if necessary
+ contentStr = removeLabels(contentStr)
+
+ // Replace namespace with Helm template variable
+ contentStr = strings.ReplaceAll(contentStr, "namespace: system", "namespace: {{ .Release.Namespace }}")
+
+ contentStr = strings.Replace(contentStr, "metadata:", `metadata:
+ labels:
+ {{- include "chart.labels" . | nindent 4 }}`, 1)
+
+ // Append project name to webhook service name
+ contentStr = strings.ReplaceAll(contentStr, "name: webhook-service", "name: "+projectName+"-webhook-service")
+
+ var wrappedContent string
+ if isMetricRBACFile(subDir, srcFile) {
+ wrappedContent = fmt.Sprintf(
+ "{{- if and .Values.rbac.enable .Values.metrics.enable }}\n%s{{- end -}}\n", contentStr)
+ } else {
+ wrappedContent = fmt.Sprintf(
+ "{{- if .Values.%s.enable }}\n%s{{- end -}}\n", subDir, contentStr)
+ }
+
+ if err = os.MkdirAll(filepath.Dir(destFile), 0o755); err != nil {
+ return fmt.Errorf("error creating directory %q: %w", filepath.Dir(destFile), err)
+ }
+
+ err = os.WriteFile(destFile, []byte(wrappedContent), 0o644)
+ if err != nil {
+ log.Info("Error writing destination file", "destination_file", destFile)
+ return fmt.Errorf("error writing destination file %q: %w", destFile, err)
+ }
+
+ log.Info("Successfully copied file", "from", srcFile, "to", destFile)
+ return nil
+}
+
+// extractKindAndGroupFromFileName extracts the kind and group from a CRD filename
+func extractKindAndGroupFromFileName(fileName string) (kind, group string) {
+ parts := strings.Split(fileName, "_")
+ if len(parts) >= 2 {
+ group = strings.Split(parts[0], ".")[0] // Extract group up to the first dot
+ kind = strings.TrimSuffix(parts[1], ".yaml")
+ }
+ return kind, group
+}
+
+// getCRDPatchContent finds and reads the appropriate patch content for a given kind and group
+func getCRDPatchContent(kind, group string) (string, bool, error) {
+ // First, look for patches that contain both "webhook", the group, and kind in their filename
+ groupKindPattern := fmt.Sprintf("config/crd/patches/webhook_*%s*%s*.yaml", group, kind)
+ patchFiles, err := filepath.Glob(groupKindPattern)
+ if err != nil {
+ return "", false, fmt.Errorf("failed to list patches: %w", err)
+ }
+
+ // If no group-specific patch found, search for patches that contain only "webhook" and the kind
+ if len(patchFiles) == 0 {
+ kindOnlyPattern := fmt.Sprintf("config/crd/patches/webhook_*%s*.yaml", kind)
+ patchFiles, err = filepath.Glob(kindOnlyPattern)
+ if err != nil {
+ return "", false, fmt.Errorf("failed to list patches: %w", err)
+ }
+ }
+
+ // Read the first matching patch file (if any)
+ if len(patchFiles) > 0 {
+ patchContent, err := os.ReadFile(patchFiles[0])
+ if err != nil {
+ return "", false, fmt.Errorf("failed to read patch file %q: %w", patchFiles[0], err)
+ }
+ return string(patchContent), true, nil
+ }
+
+ return "", false, nil
+}
+
+// extractConversionSpec extracts only the conversion section from the patch content
+func extractConversionSpec(patchContent string) string {
+ specStart := strings.Index(patchContent, "conversion:")
+ if specStart == -1 {
+ return ""
+ }
+ return patchContent[specStart:]
+}
+
+// injectConversionSpecWithCondition inserts the conversion spec under the main spec field with Helm conditional
+func injectConversionSpecWithCondition(contentStr, conversionSpec string) string {
+ specPosition := strings.Index(contentStr, "spec:")
+ if specPosition == -1 {
+ return contentStr // No spec field found; return unchanged
+ }
+ conditionalSpec := fmt.Sprintf("\n {{- if .Values.webhook.enable }}\n %s\n {{- end }}",
+ strings.TrimRight(conversionSpec, "\n"))
+ return contentStr[:specPosition+5] + conditionalSpec + contentStr[specPosition+5:]
+}
+
+// injectAnnotations inserts the required annotations after the "annotations:" field in a single block without
+// extra spaces
+func injectAnnotations(contentStr string, hasWebhookPatch bool) string {
+ annotationsBlock := `
+ {{- if .Values.certmanager.enable }}
+ cert-manager.io/inject-ca-from: "{{ .Release.Namespace }}/serving-cert"
+ {{- end }}
+ {{- if .Values.crd.keep }}
+ "helm.sh/resource-policy": keep
+ {{- end }}`
+ if hasWebhookPatch {
+ return strings.Replace(contentStr, "annotations:", "annotations:"+annotationsBlock, 1)
+ }
+
+ // Apply only resource policy if no webhook patch
+ resourcePolicy := `
+ {{- if .Values.crd.keep }}
+ "helm.sh/resource-policy": keep
+ {{- end }}`
+ return strings.Replace(contentStr, "annotations:", "annotations:"+resourcePolicy, 1)
+}
+
+// isMetricRBACFile checks if the file is in the "rbac"
+// subdirectory and matches one of the metric-related RBAC filenames
+func isMetricRBACFile(subDir, srcFile string) bool {
+ return subDir == "rbac" && (strings.HasSuffix(srcFile, "metrics_auth_role.yaml") ||
+ strings.HasSuffix(srcFile, "metrics_auth_role_binding.yaml") ||
+ strings.HasSuffix(srcFile, "metrics_reader_role.yaml"))
+}
+
+// removeLabels removes any existing labels section from the content
+func removeLabels(content string) string {
+ labelRegex := `(?m)^ labels:\n(?: [^\n]+\n)*`
+ re := regexp.MustCompile(labelRegex)
+
+ return re.ReplaceAllString(content, "")
+}
+
+func hasWebhooksWith(c config.Config) bool {
+ // Get the list of resources
+ resources, err := c.GetResources()
+ if err != nil {
+ return false // If there's an error getting resources, assume no webhooks
+ }
+
+ for _, res := range resources {
+ if res.HasDefaultingWebhook() || res.HasValidationWebhook() || res.HasConversionWebhook() {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/cert-manager/certificate.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/cert-manager/certificate.go
new file mode 100644
index 00000000000..01a90f4f8cb
--- /dev/null
+++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/cert-manager/certificate.go
@@ -0,0 +1,110 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package webhook
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Certificate{}
+
+// Certificate scaffolds the Certificate for webhooks in the Helm chart
+type Certificate struct {
+ machinery.TemplateMixin
+ machinery.ProjectNameMixin
+
+ // HasWebhooks is true when webhooks were found in the config
+ HasWebhooks bool
+}
+
+// SetTemplateDefaults sets the default template configuration
+func (f *Certificate) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join("dist", "chart", "templates", "certmanager", "certificate.yaml")
+ }
+
+ f.TemplateBody = certificateTemplate
+
+ f.IfExistsAction = machinery.OverwriteFile
+
+ return nil
+}
+
+const certificateTemplate = `{{` + "`" + `{{- if .Values.certmanager.enable }}` + "`" + `}}
+# Self-signed Issuer
+apiVersion: cert-manager.io/v1
+kind: Issuer
+metadata:
+ labels:
+ {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
+ name: selfsigned-issuer
+ namespace: {{ "{{ .Release.Namespace }}" }}
+spec:
+ selfSigned: {}
+{{- if .HasWebhooks }}
+{{ "{{- if .Values.webhook.enable }}" }}
+---
+# Certificate for the webhook
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+ annotations:
+ {{ "{{- if .Values.crd.keep }}" }}
+ "helm.sh/resource-policy": keep
+ {{ "{{- end }}" }}
+ name: serving-cert
+ namespace: {{ "{{ .Release.Namespace }}" }}
+ labels:
+ {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
+spec:
+ dnsNames:
+ - {{ .ProjectName }}.{{ "{{ .Release.Namespace }}" }}.svc
+ - {{ .ProjectName }}.{{ "{{ .Release.Namespace }}" }}.svc.cluster.local
+ - {{ .ProjectName }}-webhook-service.{{ "{{ .Release.Namespace }}" }}.svc
+ issuerRef:
+ kind: Issuer
+ name: selfsigned-issuer
+ secretName: webhook-server-cert
+{{` + "`" + `{{- end }}` + "`" + `}}
+{{- end }}
+{{ "{{- if .Values.metrics.enable }}" }}
+---
+# Certificate for the metrics
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+ annotations:
+ {{ "{{- if .Values.crd.keep }}" }}
+ "helm.sh/resource-policy": keep
+ {{ "{{- end }}" }}
+ labels:
+ {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
+ name: metrics-certs
+ namespace: {{ "{{ .Release.Namespace }}" }}
+spec:
+ dnsNames:
+ - {{ .ProjectName }}.{{ "{{ .Release.Namespace }}" }}.svc
+ - {{ .ProjectName }}.{{ "{{ .Release.Namespace }}" }}.svc.cluster.local
+ - {{ .ProjectName }}-metrics-service.{{ "{{ .Release.Namespace }}" }}.svc
+ issuerRef:
+ kind: Issuer
+ name: selfsigned-issuer
+ secretName: metrics-server-cert
+{{` + "`" + `{{- end }}` + "`" + `}}
+{{` + "`" + `{{- end }}` + "`" + `}}
+`
diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/helpers_tpl.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/helpers_tpl.go
new file mode 100644
index 00000000000..42d6c9f245f
--- /dev/null
+++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/helpers_tpl.go
@@ -0,0 +1,104 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package charttemplates
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &HelmHelpers{}
+
+// HelmHelpers scaffolds the _helpers.tpl file for Helm charts
+type HelmHelpers struct {
+ machinery.TemplateMixin
+ machinery.ProjectNameMixin
+}
+
+// SetTemplateDefaults sets the default template configuration
+func (f *HelmHelpers) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join("dist", "chart", "templates", "_helpers.tpl")
+ }
+
+ f.TemplateBody = helmHelpersTemplate
+
+ f.IfExistsAction = machinery.SkipFile
+
+ return nil
+}
+
+const helmHelpersTemplate = `{{` + "`" + `{{- define "chart.name" -}}` + "`" + `}}
+{{` + "`" + `{{- if .Chart }}` + "`" + `}}
+ {{` + "`" + `{{- if .Chart.Name }}` + "`" + `}}
+ {{` + "`" + `{{- .Chart.Name | trunc 63 | trimSuffix "-" }}` + "`" + `}}
+ {{` + "`" + `{{- else if .Values.nameOverride }}` + "`" + `}}
+ {{` + "`" + `{{ .Values.nameOverride | trunc 63 | trimSuffix "-" }}` + "`" + `}}
+ {{` + "`" + `{{- else }}` + "`" + `}}
+ {{ .ProjectName }}
+ {{` + "`" + `{{- end }}` + "`" + `}}
+{{` + "`" + `{{- else }}` + "`" + `}}
+ {{ .ProjectName }}
+{{` + "`" + `{{- end }}` + "`" + `}}
+{{` + "`" + `{{- end }}` + "`" + `}}
+
+{{/*
+Common labels for the chart.
+*/}}
+{{` + "`" + `{{- define "chart.labels" -}}` + "`" + `}}
+{{` + "`" + `{{- if .Chart.AppVersion -}}` + "`" + `}}
+app.kubernetes.io/version: {{` + "`" + `{{ .Chart.AppVersion | quote }}` + "`" + `}}
+{{` + "`" + `{{- end }}` + "`" + `}}
+{{` + "`" + `{{- if .Chart.Version }}` + "`" + `}}
+helm.sh/chart: {{` + "`" + `{{ .Chart.Version | quote }}` + "`" + `}}
+{{` + "`" + `{{- end }}` + "`" + `}}
+app.kubernetes.io/name: {{` + "`" + `{{ include "chart.name" . }}` + "`" + `}}
+app.kubernetes.io/instance: {{` + "`" + `{{ .Release.Name }}` + "`" + `}}
+app.kubernetes.io/managed-by: {{` + "`" + `{{ .Release.Service }}` + "`" + `}}
+{{` + "`" + `{{- end }}` + "`" + `}}
+
+{{/*
+Selector labels for the chart.
+*/}}
+{{` + "`" + `{{- define "chart.selectorLabels" -}}` + "`" + `}}
+app.kubernetes.io/name: {{` + "`" + `{{ include "chart.name" . }}` + "`" + `}}
+app.kubernetes.io/instance: {{` + "`" + `{{ .Release.Name }}` + "`" + `}}
+{{` + "`" + `{{- end }}` + "`" + `}}
+
+{{/*
+Helper to check if mutating webhooks exist in the services.
+*/}}
+{{` + "`" + `{{- define "chart.hasMutatingWebhooks" -}}` + "`" + `}}
+{{` + "`" + `{{- $hasMutating := false }}` + "`" + `}}
+{{` + "`" + `{{- range . }}` + "`" + `}}
+ {{` + "`" + `{{- if eq .type "mutating" }}` + "`" + `}}
+ {{` + "`" + `$hasMutating = true }}{{- end }}` + "`" + `}}
+{{` + "`" + `{{- end }}` + "`" + `}}
+{{` + "`" + `{{ $hasMutating }}}}{{- end }}` + "`" + `}}
+
+{{/*
+Helper to check if validating webhooks exist in the services.
+*/}}
+{{` + "`" + `{{- define "chart.hasValidatingWebhooks" -}}` + "`" + `}}
+{{` + "`" + `{{- $hasValidating := false }}` + "`" + `}}
+{{` + "`" + `{{- range . }}` + "`" + `}}
+ {{` + "`" + `{{- if eq .type "validating" }}` + "`" + `}}
+ {{` + "`" + `$hasValidating = true }}{{- end }}` + "`" + `}}
+{{` + "`" + `{{- end }}` + "`" + `}}
+{{` + "`" + `{{ $hasValidating }}}}{{- end }}` + "`" + `}}
+`
diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/manager/manager.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/manager/manager.go
new file mode 100644
index 00000000000..25099ff68d4
--- /dev/null
+++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/manager/manager.go
@@ -0,0 +1,162 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package manager
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Deployment{}
+
+// Deployment scaffolds the manager Deployment for the Helm chart
+type Deployment struct {
+ machinery.TemplateMixin
+ machinery.ProjectNameMixin
+
+ // DeployImages if true will scaffold the env with the images
+ DeployImages bool
+ // Force if true allow overwrite the scaffolded file
+ Force bool
+ // HasWebhooks is true when webhooks were found in the config
+ HasWebhooks bool
+}
+
+// SetTemplateDefaults sets the default template configuration
+func (f *Deployment) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join("dist", "chart", "templates", "manager", "manager.yaml")
+ }
+
+ f.TemplateBody = managerDeploymentTemplate
+
+ if f.Force {
+ f.IfExistsAction = machinery.OverwriteFile
+ } else {
+ f.IfExistsAction = machinery.SkipFile
+ }
+
+ return nil
+}
+
+//nolint:lll
+const managerDeploymentTemplate = `apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ .ProjectName }}-controller-manager
+ namespace: {{ "{{ .Release.Namespace }}" }}
+ labels:
+ {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
+ control-plane: controller-manager
+spec:
+ replicas: {{ "{{ .Values.controllerManager.replicas }}" }}
+ selector:
+ matchLabels:
+ {{ "{{- include \"chart.selectorLabels\" . | nindent 6 }}" }}
+ control-plane: controller-manager
+ template:
+ metadata:
+ annotations:
+ kubectl.kubernetes.io/default-container: manager
+ labels:
+ {{ "{{- include \"chart.labels\" . | nindent 8 }}" }}
+ control-plane: controller-manager
+ {{ "{{- if and .Values.controllerManager.pod .Values.controllerManager.pod.labels }}" }}
+ {{ "{{- range $key, $value := .Values.controllerManager.pod.labels }}" }}
+ {{ "{{ $key }}" }}: {{ "{{ $value }}" }}
+ {{ "{{- end }}" }}
+ {{ "{{- end }}" }}
+ spec:
+ containers:
+ - name: manager
+ args:
+ {{ "{{- range .Values.controllerManager.container.args }}" }}
+ - {{ "{{ . }}" }}
+ {{ "{{- end }}" }}
+ command:
+ - /manager
+ image: {{ "{{ .Values.controllerManager.container.image.repository }}" }}:{{ "{{ .Values.controllerManager.container.image.tag }}" }}
+ {{ "{{- if .Values.controllerManager.container.imagePullPolicy }}" }}
+ imagePullPolicy: {{ "{{ .Values.controllerManager.container.imagePullPolicy }}" }}
+ {{ "{{- end }}" }}
+ {{ "{{- if .Values.controllerManager.container.env }}" }}
+ env:
+ {{ "{{- range $key, $value := .Values.controllerManager.container.env }}" }}
+ - name: {{ "{{ $key }}" }}
+ value: {{ "{{ $value }}" }}
+ {{ "{{- end }}" }}
+ {{ "{{- end }}" }}
+ livenessProbe:
+ {{ "{{- toYaml .Values.controllerManager.container.livenessProbe | nindent 12 }}" }}
+ readinessProbe:
+ {{ "{{- toYaml .Values.controllerManager.container.readinessProbe | nindent 12 }}" }}
+{{- if .HasWebhooks }}
+ {{ "{{- if .Values.webhook.enable }}" }}
+ ports:
+ - containerPort: 9443
+ name: webhook-server
+ protocol: TCP
+ {{ "{{- end }}" }}
+{{- end }}
+ resources:
+ {{ "{{- toYaml .Values.controllerManager.container.resources | nindent 12 }}" }}
+ securityContext:
+ {{ "{{- toYaml .Values.controllerManager.container.securityContext | nindent 12 }}" }}
+{{- if .HasWebhooks }}
+ {{ "{{- if and .Values.certmanager.enable (or .Values.webhook.enable .Values.metrics.enable) }}" }}
+{{- else }}
+ {{ "{{- if and .Values.certmanager.enable .Values.metrics.enable }}" }}
+{{- end }}
+ volumeMounts:
+{{- if .HasWebhooks }}
+ {{ "{{- if and .Values.webhook.enable .Values.certmanager.enable }}" }}
+ - name: webhook-cert
+ mountPath: /tmp/k8s-webhook-server/serving-certs
+ readOnly: true
+ {{ "{{- end }}" }}
+{{- end }}
+ {{ "{{- if and .Values.metrics.enable .Values.certmanager.enable }}" }}
+ - name: metrics-certs
+ mountPath: /tmp/k8s-metrics-server/metrics-certs
+ readOnly: true
+ {{ "{{- end }}" }}
+ {{ "{{- end }}" }}
+ securityContext:
+ {{ "{{- toYaml .Values.controllerManager.securityContext | nindent 8 }}" }}
+ serviceAccountName: {{ "{{ .Values.controllerManager.serviceAccountName }}" }}
+ terminationGracePeriodSeconds: {{ "{{ .Values.controllerManager.terminationGracePeriodSeconds }}" }}
+{{- if .HasWebhooks }}
+ {{ "{{- if and .Values.certmanager.enable (or .Values.webhook.enable .Values.metrics.enable) }}" }}
+{{- else }}
+ {{ "{{- if and .Values.certmanager.enable .Values.metrics.enable }}" }}
+{{- end }}
+ volumes:
+{{- if .HasWebhooks }}
+ {{ "{{- if and .Values.webhook.enable .Values.certmanager.enable }}" }}
+ - name: webhook-cert
+ secret:
+ secretName: webhook-server-cert
+ {{ "{{- end }}" }}
+{{- end }}
+ {{ "{{- if and .Values.metrics.enable .Values.certmanager.enable }}" }}
+ - name: metrics-certs
+ secret:
+ secretName: metrics-server-cert
+ {{ "{{- end }}" }}
+ {{ "{{- end }}" }}
+`
diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/metrics/metrics_service.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/metrics/metrics_service.go
new file mode 100644
index 00000000000..06edd0d0757
--- /dev/null
+++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/metrics/metrics_service.go
@@ -0,0 +1,63 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package metrics
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Service{}
+
+// Service scaffolds the Service for metrics in the Helm chart
+type Service struct {
+ machinery.TemplateMixin
+ machinery.ProjectNameMixin
+}
+
+// SetTemplateDefaults sets the default template configuration
+func (f *Service) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join("dist", "chart", "templates", "metrics", "metrics-service.yaml")
+ }
+
+ f.TemplateBody = metricsServiceTemplate
+
+ f.IfExistsAction = machinery.OverwriteFile
+
+ return nil
+}
+
+const metricsServiceTemplate = `{{` + "`" + `{{- if .Values.metrics.enable }}` + "`" + `}}
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .ProjectName }}-controller-manager-metrics-service
+ namespace: {{ "{{ .Release.Namespace }}" }}
+ labels:
+ {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
+ control-plane: controller-manager
+spec:
+ ports:
+ - port: 8443
+ targetPort: 8443
+ protocol: TCP
+ name: https
+ selector:
+ control-plane: controller-manager
+{{` + "`" + `{{- end }}` + "`" + `}}
+`
diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/prometheus/monitor.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/prometheus/monitor.go
new file mode 100644
index 00000000000..eb157cad611
--- /dev/null
+++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/prometheus/monitor.go
@@ -0,0 +1,85 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package prometheus
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Monitor{}
+
+// Monitor scaffolds the ServiceMonitor for Prometheus in the Helm chart
+type Monitor struct {
+ machinery.TemplateMixin
+ machinery.ProjectNameMixin
+}
+
+// SetTemplateDefaults sets the default template configuration
+func (f *Monitor) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join("dist", "chart", "templates", "prometheus", "monitor.yaml")
+ }
+
+ f.TemplateBody = monitorTemplate
+
+ f.IfExistsAction = machinery.OverwriteFile
+
+ return nil
+}
+
+const monitorTemplate = `# To integrate with Prometheus.
+{{ "{{- if .Values.prometheus.enable }}" }}
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+ labels:
+ {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
+ control-plane: controller-manager
+ name: {{ .ProjectName }}-controller-manager-metrics-monitor
+ namespace: {{ "{{ .Release.Namespace }}" }}
+spec:
+ endpoints:
+ - path: /metrics
+ port: https
+ scheme: https
+ bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
+ tlsConfig:
+ {{ "{{- if .Values.certmanager.enable }}" }}
+ serverName: {{ .ProjectName }}-controller-manager-metrics-service.{{ "{{ .Release.Namespace }}" }}.svc
+ # Apply secure TLS configuration with cert-manager
+ insecureSkipVerify: false
+ ca:
+ secret:
+ name: metrics-server-cert
+ key: ca.crt
+ cert:
+ secret:
+ name: metrics-server-cert
+ key: tls.crt
+ keySecret:
+ name: metrics-server-cert
+ key: tls.key
+ {{ "{{- else }}" }}
+ # Development/Test mode (insecure configuration)
+ insecureSkipVerify: true
+ {{ "{{- end }}" }}
+ selector:
+ matchLabels:
+ control-plane: controller-manager
+{{ "{{- end }}" }}
+`
diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/service.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/service.go
new file mode 100644
index 00000000000..f29dac307f1
--- /dev/null
+++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/service.go
@@ -0,0 +1,64 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package webhook
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Service{}
+
+// Service scaffolds the Service for webhooks in the Helm chart
+type Service struct {
+ machinery.TemplateMixin
+ machinery.ProjectNameMixin
+
+ // Force if true allows overwriting the scaffolded file
+ Force bool
+}
+
+// SetTemplateDefaults sets the default template configuration
+func (f *Service) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join("dist", "chart", "templates", "webhook", "service.yaml")
+ }
+
+ f.TemplateBody = webhookServiceTemplate
+
+ f.IfExistsAction = machinery.OverwriteFile
+
+ return nil
+}
+
+const webhookServiceTemplate = `{{` + "`" + `{{- if .Values.webhook.enable }}` + "`" + `}}
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .ProjectName }}-webhook-service
+ namespace: {{ "{{ .Release.Namespace }}" }}
+ labels:
+ {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
+spec:
+ ports:
+ - port: 443
+ protocol: TCP
+ targetPort: 9443
+ selector:
+ control-plane: controller-manager
+{{` + "`" + `{{- end }}` + "`" + `}}
+`
diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/webhook.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/webhook.go
new file mode 100644
index 00000000000..b8b5b0d5214
--- /dev/null
+++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/webhook.go
@@ -0,0 +1,166 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package webhook
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &Template{}
+
+// Template scaffolds both MutatingWebhookConfiguration and ValidatingWebhookConfiguration for the Helm chart
+type Template struct {
+ machinery.TemplateMixin
+ machinery.ProjectNameMixin
+
+ MutatingWebhooks []DataWebhook
+ ValidatingWebhooks []DataWebhook
+}
+
+// SetTemplateDefaults sets default configuration for the webhook template
+func (f *Template) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join("dist", "chart", "templates", "webhook", "webhooks.yaml")
+ }
+
+ f.TemplateBody = webhookTemplate
+ f.IfExistsAction = machinery.OverwriteFile
+ return nil
+}
+
+// DataWebhook helps generate manifests based on the data gathered from the kustomize files
+type DataWebhook struct {
+ ServiceName string
+ Name string
+ Path string
+ Type string
+ FailurePolicy string
+ SideEffects string
+ AdmissionReviewVersions []string
+ Rules []DataWebhookRule
+}
+
+// DataWebhookRule helps generate manifests based on the data gathered from the kustomize files
+type DataWebhookRule struct {
+ Operations []string
+ APIGroups []string
+ APIVersions []string
+ Resources []string
+}
+
+const webhookTemplate = `{{` + "`" + `{{- if .Values.webhook.enable }}` + "`" + `}}
+
+{{- if .MutatingWebhooks }}
+apiVersion: admissionregistration.k8s.io/v1
+kind: MutatingWebhookConfiguration
+metadata:
+ name: {{ .ProjectName }}-mutating-webhook-configuration
+ namespace: {{ "{{ .Release.Namespace }}" }}
+ annotations:
+ {{` + "`" + `{{- if .Values.certmanager.enable }}` + "`" + `}}
+ cert-manager.io/inject-ca-from: "{{` + "`" + `{{ $.Release.Namespace }}` + "`" + `}}/serving-cert"
+ {{` + "`" + `{{- end }}` + "`" + `}}
+ labels:
+ {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
+webhooks:
+ {{- range .MutatingWebhooks }}
+ - name: {{ .Name }}
+ clientConfig:
+ service:
+ name: {{ .ServiceName }}
+ namespace: {{ "{{ .Release.Namespace }}" }}
+ path: {{ .Path }}
+ failurePolicy: {{ .FailurePolicy }}
+ sideEffects: {{ .SideEffects }}
+ admissionReviewVersions:
+ {{- range .AdmissionReviewVersions }}
+ - {{ . }}
+ {{- end }}
+ rules:
+ {{- range .Rules }}
+ - operations:
+ {{- range .Operations }}
+ - {{ . }}
+ {{- end }}
+ apiGroups:
+ {{- range .APIGroups }}
+ - {{ . }}
+ {{- end }}
+ apiVersions:
+ {{- range .APIVersions }}
+ - {{ . }}
+ {{- end }}
+ resources:
+ {{- range .Resources }}
+ - {{ . }}
+ {{- end }}
+ {{- end -}}
+ {{- end }}
+{{- end }}
+{{- if and .MutatingWebhooks .ValidatingWebhooks }}
+---
+{{- end }}
+{{- if .ValidatingWebhooks }}
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+ name: {{ .ProjectName }}-validating-webhook-configuration
+ namespace: {{ "{{ .Release.Namespace }}" }}
+ annotations:
+ {{` + "`" + `{{- if .Values.certmanager.enable }}` + "`" + `}}
+ cert-manager.io/inject-ca-from: "{{` + "`" + `{{ $.Release.Namespace }}` + "`" + `}}/serving-cert"
+ {{` + "`" + `{{- end }}` + "`" + `}}
+ labels:
+ {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
+webhooks:
+ {{- range .ValidatingWebhooks }}
+ - name: {{ .Name }}
+ clientConfig:
+ service:
+ name: {{ .ServiceName }}
+ namespace: {{ "{{ .Release.Namespace }}" }}
+ path: {{ .Path }}
+ failurePolicy: {{ .FailurePolicy }}
+ sideEffects: {{ .SideEffects }}
+ admissionReviewVersions:
+ {{- range .AdmissionReviewVersions }}
+ - {{ . }}
+ {{- end }}
+ rules:
+ {{- range .Rules }}
+ - operations:
+ {{- range .Operations }}
+ - {{ . }}
+ {{- end }}
+ apiGroups:
+ {{- range .APIGroups }}
+ - {{ . }}
+ {{- end }}
+ apiVersions:
+ {{- range .APIVersions }}
+ - {{ . }}
+ {{- end }}
+ resources:
+ {{- range .Resources }}
+ - {{ . }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+{{- end }}
+{{` + "`" + `{{- end }}` + "`" + `}}
+`
diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart.go
new file mode 100644
index 00000000000..aebc7a142cd
--- /dev/null
+++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart.go
@@ -0,0 +1,53 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package templates
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &HelmChart{}
+
+// HelmChart scaffolds a file that defines the Helm chart structure
+type HelmChart struct {
+ machinery.TemplateMixin
+ machinery.ProjectNameMixin
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *HelmChart) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join("dist", "chart", "Chart.yaml")
+ }
+
+ f.TemplateBody = helmChartTemplate
+
+ f.IfExistsAction = machinery.SkipFile
+
+ return nil
+}
+
+const helmChartTemplate = `apiVersion: v2
+name: {{ .ProjectName }}
+description: A Helm chart to distribute the project {{ .ProjectName }}
+type: application
+version: 0.1.0
+appVersion: "0.1.0"
+icon: "https://example.com/icon.png"
+`
diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/github/test_chart.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/github/test_chart.go
new file mode 100644
index 00000000000..30bd74fd3d6
--- /dev/null
+++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/github/test_chart.go
@@ -0,0 +1,137 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package github
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &HelmChartCI{}
+
+// HelmChartCI scaffolds the GitHub Action for testing Helm charts
+type HelmChartCI struct {
+ machinery.TemplateMixin
+ machinery.ProjectNameMixin
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *HelmChartCI) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join(".github", "workflows", "test-chart.yml")
+ }
+
+ f.TemplateBody = testChartTemplate
+
+ f.IfExistsAction = machinery.SkipFile
+
+ return nil
+}
+
+//nolint:lll
+const testChartTemplate = `name: Test Chart
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ test-e2e:
+ name: Run on Ubuntu
+ runs-on: ubuntu-latest
+ steps:
+ - name: Clone the code
+ uses: actions/checkout@v4
+
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+
+ - name: Install the latest version of kind
+ run: |
+ curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH)
+ chmod +x ./kind
+ sudo mv ./kind /usr/local/bin/kind
+
+ - name: Verify kind installation
+ run: kind version
+
+ - name: Create kind cluster
+ run: kind create cluster
+
+ - name: Prepare {{ .ProjectName }}
+ run: |
+ go mod tidy
+ make docker-build IMG={{ .ProjectName }}:v0.1.0
+ kind load docker-image {{ .ProjectName }}:v0.1.0
+
+ - name: Install Helm
+ run: |
+ curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
+
+ - name: Verify Helm installation
+ run: helm version
+
+ - name: Lint Helm Chart
+ run: |
+ helm lint ./dist/chart
+
+# TODO: Uncomment if cert-manager is enabled
+# - name: Install cert-manager via Helm
+# run: |
+# helm repo add jetstack https://charts.jetstack.io
+# helm repo update
+# helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set crds.enabled=true
+#
+# - name: Wait for cert-manager to be ready
+# run: |
+# kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager
+# kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-cainjector
+# kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-webhook
+
+# TODO: Uncomment if Prometheus is enabled
+# - name: Install Prometheus Operator CRDs
+# run: |
+# helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
+# helm repo update
+# helm install prometheus-crds prometheus-community/prometheus-operator-crds
+#
+# - name: Install Prometheus via Helm
+# run: |
+# helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
+# helm repo update
+# helm install prometheus prometheus-community/prometheus --namespace monitoring --create-namespace
+#
+# - name: Wait for Prometheus to be ready
+# run: |
+# kubectl wait --namespace monitoring --for=condition=available --timeout=300s deployment/prometheus-server
+
+ - name: Install Helm chart for project
+ run: |
+ helm install my-release ./dist/chart --create-namespace --namespace {{ .ProjectName }}-system
+
+ - name: Check Helm release status
+ run: |
+ helm status my-release --namespace {{ .ProjectName }}-system
+
+# TODO: Uncomment if prometheus.enabled is set to true to confirm that the ServiceMonitor gets created
+# - name: Check Presence of ServiceMonitor
+# run: |
+# kubectl wait --namespace {{ .ProjectName }}-system --for=jsonpath='{.kind}'=ServiceMonitor servicemonitor/{{ .ProjectName }}-controller-manager-metrics-monitor
+`
diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/helmignore.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/helmignore.go
new file mode 100644
index 00000000000..5aef329f3e7
--- /dev/null
+++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/helmignore.go
@@ -0,0 +1,70 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package templates
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &HelmIgnore{}
+
+// HelmIgnore scaffolds a file that defines the .helmignore for Helm packaging
+type HelmIgnore struct {
+ machinery.TemplateMixin
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *HelmIgnore) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join("dist", "chart", ".helmignore")
+ }
+
+ f.TemplateBody = helmIgnoreTemplate
+
+ f.IfExistsAction = machinery.SkipFile
+
+ return nil
+}
+
+const helmIgnoreTemplate = `# Patterns to ignore when building Helm packages.
+# Operating system files
+.DS_Store
+
+# Version control directories
+.git/
+.gitignore
+.bzr/
+.hg/
+.hgignore
+.svn/
+
+# Backup and temporary files
+*.swp
+*.tmp
+*.bak
+*.orig
+*~
+
+# IDE and editor-related files
+.idea/
+.vscode/
+
+# Helm chart artifacts
+dist/chart/*.tgz
+`
diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/values.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/values.go
new file mode 100644
index 00000000000..d24e2ec3884
--- /dev/null
+++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/values.go
@@ -0,0 +1,145 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package templates
+
+import (
+ "path/filepath"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+)
+
+var _ machinery.Template = &HelmValues{}
+
+// HelmValues scaffolds a file that defines the values.yaml structure for the Helm chart
+type HelmValues struct {
+ machinery.TemplateMixin
+ machinery.ProjectNameMixin
+
+ // DeployImages stores the images used for the DeployImage plugin
+ DeployImages map[string]string
+ // Force if true allows overwriting the scaffolded file
+ Force bool
+ // HasWebhooks is true when webhooks were found in the config
+ HasWebhooks bool
+}
+
+// SetTemplateDefaults implements machinery.Template
+func (f *HelmValues) SetTemplateDefaults() error {
+ if f.Path == "" {
+ f.Path = filepath.Join("dist", "chart", "values.yaml")
+ }
+ f.TemplateBody = helmValuesTemplate
+
+ if f.Force {
+ f.IfExistsAction = machinery.OverwriteFile
+ } else {
+ f.IfExistsAction = machinery.SkipFile
+ }
+
+ return nil
+}
+
+const helmValuesTemplate = `# [MANAGER]: Manager Deployment Configurations
+controllerManager:
+ replicas: 1
+ container:
+ image:
+ repository: controller
+ tag: latest
+ imagePullPolicy: IfNotPresent
+ args:
+ - "--leader-elect"
+ - "--metrics-bind-address=:8443"
+ - "--health-probe-bind-address=:8081"
+ resources:
+ limits:
+ cpu: 500m
+ memory: 128Mi
+ requests:
+ cpu: 10m
+ memory: 64Mi
+ livenessProbe:
+ initialDelaySeconds: 15
+ periodSeconds: 20
+ httpGet:
+ path: /healthz
+ port: 8081
+ readinessProbe:
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ httpGet:
+ path: /readyz
+ port: 8081
+ {{- if .DeployImages }}
+ env:
+ {{- range $kind, $image := .DeployImages }}
+ {{ $kind }}_IMAGE: {{ $image }}
+ {{- end }}
+ {{- end }}
+ securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - "ALL"
+ securityContext:
+ runAsNonRoot: true
+ seccompProfile:
+ type: RuntimeDefault
+ terminationGracePeriodSeconds: 10
+ serviceAccountName: {{ .ProjectName }}-controller-manager
+
+# [RBAC]: To enable RBAC (Permissions) configurations
+rbac:
+ enable: true
+
+# [CRDs]: To enable the CRDs
+crd:
+ # This option determines whether the CRDs are included
+ # in the installation process.
+ enable: true
+
+ # Enabling this option adds the "helm.sh/resource-policy": keep
+ # annotation to the CRD, ensuring it remains installed even when
+ # the Helm release is uninstalled.
+ # NOTE: Removing the CRDs will also remove all cert-manager CR(s)
+ # (Certificates, Issuers, ...) due to garbage collection.
+ keep: true
+
+# [METRICS]: Set to true to generate manifests for exporting metrics.
+# To disable metrics export set false, and ensure that the
+# ControllerManager argument "--metrics-bind-address=:8443" is removed.
+metrics:
+ enable: true
+{{ if .HasWebhooks }}
+# [WEBHOOKS]: Webhooks configuration
+# The following configuration is automatically generated from the manifests
+# generated by controller-gen. To update run 'make manifests' and
+# the edit command with the '--force' flag
+webhook:
+ enable: true
+{{ end }}
+# [PROMETHEUS]: To enable a ServiceMonitor to export metrics to Prometheus set true
+prometheus:
+ enable: false
+
+# [CERT-MANAGER]: To enable cert-manager injection to webhooks set true
+certmanager:
+ enable: {{ .HasWebhooks }}
+
+# [NETWORK POLICIES]: To enable NetworkPolicies set true
+networkPolicy:
+ enable: false
+`
diff --git a/pkg/plugins/optional/helm/v2alpha/edit.go b/pkg/plugins/optional/helm/v2alpha/edit.go
new file mode 100644
index 00000000000..87d9e1b778e
--- /dev/null
+++ b/pkg/plugins/optional/helm/v2alpha/edit.go
@@ -0,0 +1,217 @@
+/*
+Copyright 2025 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v2alpha
+
+import (
+ "errors"
+ "fmt"
+ "log/slog"
+ "os"
+ "path/filepath"
+
+ "github.com/spf13/pflag"
+
+ "sigs.k8s.io/kubebuilder/v4/pkg/config"
+ "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
+ "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v2alpha/scaffolds"
+)
+
+const (
+ // DefaultManifestsFile is the default path for kustomize output manifests
+ DefaultManifestsFile = "dist/install.yaml"
+ // DefaultOutputDir is the default output directory for Helm charts
+ DefaultOutputDir = "dist"
+)
+
+var _ plugin.EditSubcommand = &editSubcommand{}
+
+type editSubcommand struct {
+ config config.Config
+ force bool
+ manifestsFile string
+ outputDir string
+}
+
+//nolint:lll
+func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
+ subcmdMeta.Description = `Generate a Helm chart from your project's kustomize output.
+
+This plugin dynamically generates Helm chart templates by parsing the output of 'make build-installer'
+(dist/install.yaml by default). The generated chart preserves all customizations made to your kustomize
+configuration including environment variables, labels, and annotations.
+
+The chart structure mirrors your config/ directory organization for easy maintenance.`
+
+ subcmdMeta.Examples = fmt.Sprintf(`# Generate Helm chart from default manifests (dist/install.yaml) to default output (dist/)
+ %[1]s edit --plugins=%[2]s
+
+# Generate Helm chart and overwrite existing files (useful for updates)
+ %[1]s edit --plugins=%[2]s --force
+
+# Generate Helm chart from a custom manifests file
+ %[1]s edit --plugins=%[2]s --manifests=path/to/custom-install.yaml
+
+# Generate Helm chart to a custom output directory
+ %[1]s edit --plugins=%[2]s --output-dir=charts
+
+# Generate from custom manifests to custom output directory
+ %[1]s edit --plugins=%[2]s --manifests=manifests/install.yaml --output-dir=helm-charts
+
+# Typical workflow:
+ make build-installer # Generate dist/install.yaml with latest changes
+ %[1]s edit --plugins=%[2]s # Generate/update Helm chart in dist/chart/
+
+**NOTE**: The plugin preserves customizations in values.yaml, Chart.yaml, _helpers.tpl, and .helmignore
+unless --force is used. All template files are regenerated to match your current kustomize output.
+
+The generated chart structure mirrors your config/ directory:
+