diff --git a/cmd/main.go b/cmd/main.go index 5b9e3e9adac..526e02d5acc 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -30,6 +30,7 @@ import ( kustomizecommonv2alpha "sigs.k8s.io/kubebuilder/v3/pkg/plugins/common/kustomize/v2-alpha" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" declarativev1 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/declarative/v1" + deployimagev1alpha1 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/deploy-image/v1alpha1" golangv2 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2" golangv3 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3" ) @@ -60,6 +61,7 @@ func main() { &kustomizecommonv1.Plugin{}, &kustomizecommonv2alpha.Plugin{}, &declarativev1.Plugin{}, + &deployimagev1alpha1.Plugin{}, ), cli.WithPlugins(externalPlugins...), cli.WithDefaultPlugins(cfgv2.Version, golangv2.Plugin{}), diff --git a/pkg/plugins/golang/deploy-image/v1alpha1/api.go b/pkg/plugins/golang/deploy-image/v1alpha1/api.go new file mode 100644 index 00000000000..5200a1afe09 --- /dev/null +++ b/pkg/plugins/golang/deploy-image/v1alpha1/api.go @@ -0,0 +1,250 @@ +/* +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 v1alpha1 + +import ( + "errors" + "fmt" + "os" + + goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" + + "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" + + "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/plugins/golang/deploy-image/v1alpha1/scaffolds" +) + +const ( + // defaultCRDVersion is the default CRD API version to scaffold. + defaultCRDVersion = "v1" +) + +const deprecateMsg = "The v1beta1 API version for CRDs and Webhooks are deprecated and are no longer supported since " + + "the Kubernetes release 1.22. This flag no longer required to exist in future releases. Also, we would like to " + + "recommend you no longer use these API versions." + + "More info: https://kubernetes.io/docs/reference/using-api/deprecation-guide/#v1-22" + +// 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 + + // image indicates the image that will be used to scaffold the deployment + image string + + // runMake indicates whether to run make or not after scaffolding APIs + runMake bool + + // runManifests indicates whether to run manifests or not after scaffolding APIs + runManifests bool + + // imageCommand indicates the command that we should use to init the deployment + imageContainerCommand string + + // imageContainerPort indicates the port that we should use in the scaffold + imageContainerPort string +} + +func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + //nolint: lll + subcmdMeta.Description = `Scaffold the code implementation to deploy and manage your Operand which is represented by the API informed and will be reconciled by its controller. This plugin will generate the code implementation to help you out. + + Note: In general, it’s recommended to have one controller responsible for managing each API created for the project to properly follow the design goals set by Controller Runtime(https://github.com/kubernetes-sigs/controller-runtime). + + This plugin will work as the common behaviour of the flag --force and will scaffold the API and controller always. Use core types or external APIs is not officially support by default with. +` + //nolint: lll + subcmdMeta.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1, Kind: Frigate to represent the + Image: example.com/frigate:v0.0.1 and its controller with a code to deploy and manage this Operand. + + Note that in the following example we are also adding the optional options to let you inform the command which should be used to create the container and initialize itvia the flag --image-container-command as the Port that should be used + + - By informing the command (--image-container-command="memcached,-m=64,-o,modern,-v") your deployment will be scaffold with, i.e.: + + Command: []string{"memcached","-m=64","-o","modern","-v"}, + + - By informing the Port (--image-container-port) will deployment will be scaffold with, i.e: + + Ports: []corev1.ContainerPort{ + ContainerPort: Memcached.Spec.ContainerPort, + Name: "Memcached", + }, + + Therefore, the default values informed will be used to scaffold specs for the API. + + %[1]s create api --group example.com --version v1alpha1 --kind Memcached --image=memcached:1.6.15-alpine --image-container-command="memcached -m=64 modern -v" --image-container-port="11211" --plugins="deploy-image/v1-alpha" --make=false --namespaced=false + + # 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.StringVar(&p.image, "image", "", "inform the Operand image. "+ + "The controller will be scaffolded with an example code to deploy and manage this image.") + + fs.StringVar(&p.imageContainerCommand, "image-container-command", "", "[Optional] if informed, "+ + "will be used to scaffold the container command that should be used to init a container to run the image in "+ + "the controller and its spec in the API (CRD/CR). (i.e. --image-container-command=\"memcached,-m=64,modern,-o,-v\")") + fs.StringVar(&p.imageContainerPort, "image-container-port", "", "[Optional] if informed, "+ + "will be used to scaffold the container port that should be used by container image in "+ + "the controller and its spec in the API (CRD/CR). (i.e --image-container-port=\"11211\") ") + + fs.BoolVar(&p.runMake, "make", true, "if true, run `make generate` after generating files") + fs.BoolVar(&p.runManifests, "manifests", true, "if true, run `make manifests` after generating files") + + p.options = &goPlugin.Options{} + + fs.StringVar(&p.options.CRDVersion, "crd-version", defaultCRDVersion, + "version of CustomResourceDefinition to scaffold. Options: [v1, v1beta1]") + + fs.StringVar(&p.options.Plural, "plural", "", "resource irregular plural form") + fs.BoolVar(&p.options.Namespaced, "namespaced", true, "resource is namespaced") + + // (not required raise an error in this case) + // nolint:errcheck,gosec + fs.MarkDeprecated("crd-version", deprecateMsg) +} + +func (p *createAPISubcommand) InjectConfig(c config.Config) error { + p.config = c + + return nil +} + +func (p *createAPISubcommand) InjectResource(res *resource.Resource) error { + p.resource = res + p.options.DoAPI = true + p.options.DoController = true + p.options.UpdateResource(p.resource, p.config) + + if err := p.resource.Validate(); err != nil { + return err + } + + // 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) + } + + // 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 { + if len(p.image) == 0 { + return fmt.Errorf("you MUST inform the image that will be used in the reconciliation") + } + + // 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 { + fmt.Println("updating scaffold with deploy-image/v1alpha1 plugin...") + + scaffolder := scaffolds.NewDeployImageScaffolder(p.config, + *p.resource, + p.image, + p.imageContainerCommand, + p.imageContainerPort) + scaffolder.InjectFS(fs) + err := scaffolder.Scaffold() + if err != nil { + return err + } + + // Track the resources following a declarative approach + cfg := pluginConfig{} + if err := p.config.DecodePluginConfig(pluginKey, &cfg); errors.As(err, &config.UnsupportedFieldError{}) { + // Config doesn't support per-plugin configuration, so we can't track them + } else { + // Fail unless they key wasn't found, which just means it is the first resource tracked + if err != nil && !errors.As(err, &config.PluginKeyNotFoundError{}) { + return err + } + cfg.Resources = append(cfg.Resources, p.resource.GVK) + // Track the options informed + cfg.Image = p.image + cfg.ContainerCommand = p.imageContainerCommand + cfg.ContainerPort = p.imageContainerPort + if err := p.config.EncodePluginConfig(pluginKey, cfg); err != nil { + return err + } + } + + return nil +} + +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 + } + } + + if p.runManifests && p.resource.HasAPI() { + err = util.RunCmd("Running make", "make", "manifests") + if err != nil { + return err + } + } + + fmt.Print("Next: check the implementation of your new API and controller. " + + "If you do changes in the API run the manifests with:\n$ make manifests\n") + + return nil +} diff --git a/pkg/plugins/golang/deploy-image/v1alpha1/plugin.go b/pkg/plugins/golang/deploy-image/v1alpha1/plugin.go new file mode 100644 index 00000000000..2183bb7b5d5 --- /dev/null +++ b/pkg/plugins/golang/deploy-image/v1alpha1/plugin.go @@ -0,0 +1,61 @@ +/* +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 v1alpha1 + +import ( + "sigs.k8s.io/kubebuilder/v3/pkg/config" + cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/model/stage" + "sigs.k8s.io/kubebuilder/v3/pkg/plugin" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" +) + +const pluginName = "deploy-image." + golang.DefaultNameQualifier + +var ( + pluginVersion = plugin.Version{Number: 1, Stage: stage.Alpha} + supportedProjectVersions = []config.Version{cfgv3.Version} + pluginKey = plugin.KeyFor(Plugin{}) +) + +var _ plugin.CreateAPI = Plugin{} + +// Plugin implements the plugin.Full interface +type Plugin struct { + createAPISubcommand +} + +// 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 } + +// GetCreateAPISubcommand will return the subcommand which is responsible for scaffolding apis +func (p Plugin) GetCreateAPISubcommand() plugin.CreateAPISubcommand { return &p.createAPISubcommand } + +type pluginConfig struct { + Resources []resource.GVK `json:"resources,omitempty"` + // image indicates the image that will be used to scaffold the deployment + Image string `json:"image,omitempty"` + ContainerCommand string `json:"containerCommand,omitempty"` + ContainerPort string `json:"containerPort,omitempty"` +} diff --git a/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/api.go b/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/api.go new file mode 100644 index 00000000000..f16d005b881 --- /dev/null +++ b/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/api.go @@ -0,0 +1,227 @@ +/* +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" + "path/filepath" + "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/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" + kustomizev1scaffolds "sigs.k8s.io/kubebuilder/v3/pkg/plugins/common/kustomize/v1/scaffolds" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/api" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/config/samples" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/controllers" + golangv3scaffolds "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds" +) + +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 + image string + command string + port string + + // fs is the filesystem that will be used by the scaffolder + fs machinery.Filesystem +} + +// NewAPIScaffolder returns a new Scaffolder for declarative +//nolint: lll +func NewDeployImageScaffolder(config config.Config, res resource.Resource, image, + command, port string) plugins.Scaffolder { + return &apiScaffolder{ + config: config, + resource: res, + image: image, + command: command, + port: port, + } +} + +// 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...") + + if err := s.scaffoldCreateAPIFromGolang(); err != nil { + return fmt.Errorf("error scaffolding APIs: %v", err) + } + + if err := s.scaffoldCreateAPIFromKustomize(); err != nil { + return fmt.Errorf("error scaffolding kustomize file for the new APIs: %v", err) + } + + // Load the boilerplate + boilerplate, err := afero.ReadFile(s.fs.FS, filepath.Join("hack", "boilerplate.go.txt")) + 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), + ) + + if err := scaffold.Execute( + &api.Types{Port: s.port}, + ); err != nil { + return fmt.Errorf("error updating APIs: %v", err) + } + + if err := s.scafffoldControllerWithImage(scaffold); err != nil { + return fmt.Errorf("error updating controller: %v", err) + } + + if err := scaffold.Execute( + &samples.CRDSample{Port: s.port}, + ); err != nil { + return fmt.Errorf("error updating config/samples: %v", err) + } + + return nil +} + +func (s *apiScaffolder) scafffoldControllerWithImage(scaffold *machinery.Scaffold) error { + controller := &controllers.Controller{ControllerRuntimeVersion: golangv3scaffolds.ControllerRuntimeVersion, + Image: s.image, + } + if err := scaffold.Execute( + controller, + ); err != nil { + return fmt.Errorf("error scaffolding controller: %v", err) + } + + controllerPath := controller.Path + if err := util.ReplaceInFile(controllerPath, "//TODO: scaffold container", + fmt.Sprintf(containerTemplate, + s.image, // value for the image + strings.ToLower(s.resource.Kind), // value for the name of the container + ), + ); err != nil { + return fmt.Errorf("error scaffolding container in the controller: %v", err) + } + + // Scaffold the command if informed + if len(s.command) > 0 { + // TODO: improve it to be an spec in the sample and api instead so that + // users can change the values + var res string + for _, value := range strings.Split(s.command, ",") { + res += fmt.Sprintf("\"%s\",", strings.TrimSpace(value)) + } + res = res[:len(res)-1] + err := util.InsertCode(controllerPath, `SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: &[]bool{true}[0], + RunAsUser: &[]int64{1000}[0], + AllowPrivilegeEscalation: &[]bool{false}[0], + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{ + "ALL", + }, + }, + },`, fmt.Sprintf(commandTemplate, res)) + if err != nil { + return fmt.Errorf("error scaffolding command in the controller: %v", err) + } + } + + // Scaffold the port if informed + if len(s.port) > 0 { + err := util.InsertCode(controllerPath, `SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: &[]bool{true}[0], + RunAsUser: &[]int64{1000}[0], + AllowPrivilegeEscalation: &[]bool{false}[0], + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{ + "ALL", + }, + }, + },`, fmt.Sprintf(portTemplate, strings.ToLower(s.resource.Kind))) + if err != nil { + return fmt.Errorf("error scaffolding container port in the controller: %v", err) + } + } + return nil +} + +func (s *apiScaffolder) scaffoldCreateAPIFromKustomize() error { + // Now we need call the kustomize/v1 plugin to do its scaffolds when we create a new API + // todo: when we have the go/v4-alpha plugin we will also need to check what is the plugin used + // in the Project layout to know if we should use kustomize/v1 OR kustomize/v2-alpha + kustomizeV1Scaffolder := kustomizev1scaffolds.NewAPIScaffolder(s.config, + s.resource, true) + kustomizeV1Scaffolder.InjectFS(s.fs) + if err := kustomizeV1Scaffolder.Scaffold(); err != nil { + return fmt.Errorf("error scaffolding kustomize files for the APIs: %v", err) + } + return nil +} + +func (s *apiScaffolder) scaffoldCreateAPIFromGolang() error { + // Now we need call the kustomize/v1 plugin to do its scaffolds when we create a new API + // todo: when we have the go/v4-alpha plugin we will also need to check what is the plugin used + // in the Project layout to know if we should use kustomize/v1 OR kustomize/v2-alpha + + golangV3Scaffolder := golangv3scaffolds.NewAPIScaffolder(s.config, + s.resource, true) + golangV3Scaffolder.InjectFS(s.fs) + return golangV3Scaffolder.Scaffold() +} + +const containerTemplate = `Containers: []corev1.Container{{ + Image: "%s", + Name: "%s", + ImagePullPolicy: corev1.PullAlways, + // Ensure restrictive context for the container + // More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: &[]bool{true}[0], + RunAsUser: &[]int64{1000}[0], + AllowPrivilegeEscalation: &[]bool{false}[0], + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{ + "ALL", + }, + }, + }, + }}` + +const commandTemplate = ` + Command: []string{%s},` + +const portTemplate = ` + Ports: []corev1.ContainerPort{{ + ContainerPort: m.Spec.ContainerPort, + Name: "%s", + }},` diff --git a/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/api/types.go b/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/api/types.go new file mode 100644 index 00000000000..3297aee0cc7 --- /dev/null +++ b/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/api/types.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 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 + + // Port if informed we will create the scaffold with this spec + Port string +} + +// 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 + + f.IfExistsAction = machinery.OverwriteFile + + 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 + + // Size defines the number of {{ .Resource.Kind }} instances + Size int32 ` + "`" + `json:"size,omitempty"` + "`" + ` + + {{ if not (isEmptyStr .Port) -}} + // Port defines the port that will be used to init the container with the image + ContainerPort int32 ` + "`" + `json:"containerPort,omitempty"` + "`" + ` + {{- end }} +} + +// {{ .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/deploy-image/v1alpha1/scaffolds/internal/templates/config/samples/crd_sample.go b/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/config/samples/crd_sample.go new file mode 100644 index 00000000000..ea88d0e41c1 --- /dev/null +++ b/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/config/samples/crd_sample.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. +*/ + +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 + + // Port if informed we will create the scaffold with this spec + Port string +} + +// 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) + + f.IfExistsAction = machinery.OverwriteFile + + f.TemplateBody = crdSampleTemplate + + return nil +} + +const crdSampleTemplate = `apiVersion: {{ .Resource.QualifiedGroup }}/{{ .Resource.Version }} +kind: {{ .Resource.Kind }} +metadata: + name: {{ lower .Resource.Kind }}-sample +spec: + # TODO(user): edit the following value to ensure the number + # of Pods/Instances your Operand must have on cluster + size: 1 + + {{ if not (isEmptyStr .Port) -}} + # TODO(user): edit the following value to ensure the the container has the right port to be initialized + containerPort: {{ .Port }} + {{- end }} +` diff --git a/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/controllers/controller.go b/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/controllers/controller.go new file mode 100644 index 00000000000..489a3d54f9d --- /dev/null +++ b/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/controllers/controller.go @@ -0,0 +1,240 @@ +/* +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" + "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 + + Image string +} + +// 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("creating import for %", f.Resource.Path) + f.TemplateBody = controllerTemplate + + // This one is to overwrite the controller if it exist + f.IfExistsAction = machinery.OverwriteFile + + return nil +} + +//nolint:lll +const controllerTemplate = `{{ .Boilerplate }} + +package {{ if and .MultiGroup .Resource.Group }}{{ .Resource.PackageName }}{{ else }}controllers{{ end }} + +import ( + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "context" + "time" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "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 +} +// The following markers are used to generate the rules permissions on config/rbac using controller-gen +// when the command is executed. +// To know more about markers see: https://book.kubebuilder.io/reference/markers.html + +//+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 +//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. + +// Note: It is essential for the controller's reconciliation loop to be idempotent. By following the Operator +// pattern(https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) you will create +// Controllers(https://kubernetes.io/docs/concepts/architecture/controller/) which provide a reconcile function +// responsible for synchronizing resources until the desired state is reached on the cluster. Breaking this +// recommendation goes against the design principles of Controller-runtime(https://github.com/kubernetes-sigs/controller-runtime) +// and may lead to unforeseen consequences such as resources becoming stuck and requiring manual intervention. +// +// 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) { + log := log.FromContext(ctx) + + // Fetch the {{ .Resource.Kind }} instance + // The purpose is check if the Custom Resource for the Kind {{ .Resource.Kind }} + // is applied on the cluster if not we return nill to stop the reconciliation + {{ lower .Resource.Kind }} := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{} + err := r.Get(ctx, req.NamespacedName, {{ lower .Resource.Kind }}) + if err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + log.Info("{{ lower .Resource.Kind }} resource not found. Ignoring since object must be deleted") + return ctrl.Result{}, nil + } + // Error reading the object - requeue the request. + log.Error(err, "Failed to get {{ lower .Resource.Kind }} }}") + return ctrl.Result{}, err + } + + // Check if the deployment already exists, if not create a new one + found := &appsv1.Deployment{} + err = r.Get(ctx, types.NamespacedName{Name: {{ lower .Resource.Kind }}.Name, Namespace: {{ lower .Resource.Kind }}.Namespace}, found) + if err != nil && errors.IsNotFound(err) { + // Define a new deployment + dep := r.deploymentFor{{ .Resource.Kind }}({{ lower .Resource.Kind }}) + log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + err = r.Create(ctx, dep) + if err != nil { + log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + return ctrl.Result{}, err + } + // Deployment created successfully + // We will requeue the reconciliation so that we can ensure the state + // and move forward for the next operations + return ctrl.Result{RequeueAfter: time.Minute}, nil + } else if err != nil { + log.Error(err, "Failed to get Deployment") + // Let's return the error for the reconciliation be re-trigged again + return ctrl.Result{}, err + } + + // The API is defining that the {{ .Resource.Kind }} type, have a {{ .Resource.Kind }}Spec.Size field to set the quantity of {{ .Resource.Kind }} instances (CRs) to be deployed. + // The following code ensure the deployment size is the same as the spec + size := {{ lower .Resource.Kind }}.Spec.Size + if *found.Spec.Replicas != size { + found.Spec.Replicas = &size + err = r.Update(ctx, found) + if err != nil { + log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) + return ctrl.Result{}, err + } + // Since it fails we want to re-queue the reconciliation + // The reconciliation will only stop when we be able to ensure + // the desired state on the cluster + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, nil +} + +// deploymentFor{{ .Resource.Kind }} returns a {{ .Resource.Kind }} Deployment object +func (r *{{ .Resource.Kind }}Reconciler) deploymentFor{{ .Resource.Kind }}(m *{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}) *appsv1.Deployment { + ls := labelsFor{{ .Resource.Kind }}(m.Name) + replicas := m.Spec.Size + + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: m.Name, + Namespace: m.Namespace, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: ls, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: ls, + }, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyAlways, + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &[]bool{true}[0], + // IMPORTANT: seccomProfile was introduced with Kubernetes 1.19 + // If you are looking for to produce solutions to be supported + // on lower versions you must remove this option. + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + }, + //TODO: scaffold container, + }, + }, + }, + } + // Set {{ .Resource.Kind }} instance as the owner and controller + // You should use the method ctrl.SetControllerReference for all resources + // which are created by your controller so that when the Custom Resource be deleted + // all resources owned by it (child) will also be deleted. + // To know more about it see: https://kubernetes.io/docs/tasks/administer-cluster/use-cascading-deletion/ + ctrl.SetControllerReference(m, dep, r.Scheme) + return dep +} + +// labelsFor{{ .Resource.Kind }} returns the labels for selecting the resources +// belonging to the given {{ .Resource.Kind }} CR name. +func labelsFor{{ .Resource.Kind }}(name string) map[string]string { + return map[string]string{"type": "{{ lower .Resource.Kind }}", "{{ lower .Resource.Kind }}_cr": name} +} + +// SetupWithManager sets up the controller with the Manager. +// The following code specifies how the controller is built to watch a CR +// and other resources that are owned and managed by that controller. +// In this way, the reconciliation can be re-trigged when the CR and/or the Deployment +// be created/edit/delete. +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 }} + Owns(&appsv1.Deployment{}). + Complete(r) +} +` diff --git a/test/e2e/deployimage/e2e_suite_test.go b/test/e2e/deployimage/e2e_suite_test.go new file mode 100644 index 00000000000..b24e218205c --- /dev/null +++ b/test/e2e/deployimage/e2e_suite_test.go @@ -0,0 +1,32 @@ +/* +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 deployimage + +import ( + "fmt" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +// Run e2e tests using the Ginkgo runner. +func TestE2E(t *testing.T) { + RegisterFailHandler(Fail) + fmt.Fprintf(GinkgoWriter, "Starting kubebuilder suite\n") + RunSpecs(t, "Kubebuilder e2e suite") +} diff --git a/test/e2e/deployimage/plugin_cluster_test.go b/test/e2e/deployimage/plugin_cluster_test.go new file mode 100644 index 00000000000..7b5f4e28e69 --- /dev/null +++ b/test/e2e/deployimage/plugin_cluster_test.go @@ -0,0 +1,306 @@ +/* +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 deployimage + +import ( + "errors" + "fmt" + "os/exec" + "path/filepath" + "strings" + "time" + + "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" + + //nolint:golint + //nolint:revive + . "github.com/onsi/ginkgo" + + //nolint:golint + //nolint:revive + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/test/e2e/utils" +) + +var _ = Describe("kubebuilder", func() { + Context("deploy image plugin 3", func() { + var ( + kbc *utils.TestContext + ) + + BeforeEach(func() { + var err error + kbc, err = utils.NewTestContext(util.KubebuilderBinName, "GO111MODULE=on") + Expect(err).NotTo(HaveOccurred()) + Expect(kbc.Prepare()).To(Succeed()) + + By("installing prometheus operator") + Expect(kbc.InstallPrometheusOperManager()).To(Succeed()) + }) + + AfterEach(func() { + By("clean up API objects created during the test") + kbc.CleanupManifests(filepath.Join("config", "default")) + + By("uninstalling the Prometheus manager bundle") + kbc.UninstallPrometheusOperManager() + + By("removing controller image and working dir") + kbc.Destroy() + }) + + It("should generate a runnable project with deploy-image/v1-alpha options ", func() { + // Skip if cluster version < 1.16, when v1 CRDs and webhooks did not exist. + // Skip if cluster version < 1.19, because securityContext.seccompProfile only works from 1.19 + // Otherwise, unknown field "seccompProfile" in io.k8s.api.core.v1.PodSecurityContext will be faced + if srvVer := kbc.K8sVersion.ServerVersion; srvVer.GetMajorInt() <= 1 && srvVer.GetMinorInt() < 19 { + Skip(fmt.Sprintf("cluster version %s does not support "+ + "securityContext.seccompProfile", srvVer.GitVersion)) + } + + var err error + + By("initializing a project with go/v3") + err = kbc.Init( + "--plugins", "go/v3", + "--project-version", "3", + "--domain", kbc.Domain, + "--fetch-deps=false", + ) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("creating API definition with deploy-image/v1-alpha plugin") + err = kbc.CreateAPI( + "--group", kbc.Group, + "--version", kbc.Version, + "--kind", kbc.Kind, + "--plugins", "deploy-image/v1-alpha", + "--image", "memcached:1.4.36-alpine", + "--image-container-port", "11211", + "--image-container-command", "memcached,-m=64,-o,modern,-v", + "--make=false", + "--manifests=false", + ) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("uncomment kustomization.yaml to enable prometheus") + ExpectWithOffset(1, util.UncommentCode( + filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"), + "#- ../prometheus", "#")).To(Succeed()) + + By("uncomment kustomize files to ensure that pods are restricted") + uncommentPodStandards(kbc) + + Run(kbc, "memcached:1.4.36-alpine") + }) + + It("should generate a runnable project with deploy-image/v1-alpha without options ", func() { + // Skip if cluster version < 1.16, when v1 CRDs and webhooks did not exist. + // Skip if cluster version < 1.19, because securityContext.seccompProfile only works from 1.19 + // Otherwise, unknown field "seccompProfile" in io.k8s.api.core.v1.PodSecurityContext will be faced + if srvVer := kbc.K8sVersion.ServerVersion; srvVer.GetMajorInt() <= 1 && srvVer.GetMinorInt() < 19 { + Skip(fmt.Sprintf("cluster version %s does not support "+ + "securityContext.seccompProfile", srvVer.GitVersion)) + } + + var err error + + By("initializing a project with go/v3") + err = kbc.Init( + "--plugins", "go/v3", + "--project-version", "3", + "--domain", kbc.Domain, + "--fetch-deps=false", + ) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("creating API definition with deploy-image/v1-alpha plugin") + err = kbc.CreateAPI( + "--group", kbc.Group, + "--version", kbc.Version, + "--kind", kbc.Kind, + "--plugins", "deploy-image/v1-alpha", + "--image", "busybox:1.28", + "--make=false", + "--manifests=false", + ) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("uncomment kustomization.yaml to enable prometheus") + ExpectWithOffset(1, util.UncommentCode( + filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"), + "#- ../prometheus", "#")).To(Succeed()) + + By("uncomment kustomize files to ensure that pods are restricted") + uncommentPodStandards(kbc) + + Run(kbc, "busybox:1.28") + }) + }) +}) + +// Run runs a set of e2e tests for a scaffolded project defined by a TestContext. +func Run(kbc *utils.TestContext, imageCR string) { + var controllerPodName string + var err error + + By("updating the go.mod") + err = kbc.Tidy() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("creating manager namespace") + err = kbc.CreateManagerNamespace() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("labeling all namespaces to warn about restricted") + err = kbc.LabelAllNamespacesToWarnAboutRestricted() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("enforce that namespace where the sample will be applied can only run restricted containers") + _, err = kbc.Kubectl.Command("label", "--overwrite", "ns", kbc.Kubectl.Namespace, + "pod-security.kubernetes.io/audit=restricted", + "pod-security.kubernetes.io/enforce-version=v1.24", + "pod-security.kubernetes.io/enforce=restricted") + Expect(err).To(Not(HaveOccurred())) + + By("building the controller image") + err = kbc.Make("docker-build", "IMG="+kbc.ImageName) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("deploying the controller-manager") + cmd := exec.Command("make", "deploy", "IMG="+kbc.ImageName) + outputMake, err := kbc.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("validating that manager Pod/container(s) are restricted") + ExpectWithOffset(1, outputMake).NotTo(ContainSubstring("Warning: would violate PodSecurity")) + + By("loading the controller docker image into the kind cluster") + err = kbc.LoadImageToKindCluster() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("pulling image") + err = kbc.LoadImageToKindClusterWithName(imageCR) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + // NOTE: If you want to run the test against a GKE cluster, you will need to grant yourself permission. + // Otherwise, you may see "... is forbidden: attempt to grant extra privileges" + // $ kubectl create clusterrolebinding myname-cluster-admin-binding \ + // --clusterrole=cluster-admin --user=myname@mycompany.com + // https://cloud.google.com/kubernetes-engine/docs/how-to/role-based-access-control + By("deploying the controller-manager") + err = kbc.Make("deploy", "IMG="+kbc.ImageName) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("validating that the controller-manager pod is running as expected") + verifyControllerUp := func() error { + // Get pod name + podOutput, err := kbc.Kubectl.Get( + true, + "pods", "-l", "control-plane=controller-manager", + "-o", "go-template={{ range .items }}{{ if not .metadata.deletionTimestamp }}{{ .metadata.name }}"+ + "{{ \"\\n\" }}{{ end }}{{ end }}") + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + podNames := util.GetNonEmptyLines(podOutput) + if len(podNames) != 1 { + return fmt.Errorf("expect 1 controller pods running, but got %d", len(podNames)) + } + controllerPodName = podNames[0] + ExpectWithOffset(2, controllerPodName).Should(ContainSubstring("controller-manager")) + + // Validate pod status + status, err := kbc.Kubectl.Get( + true, + "pods", controllerPodName, "-o", "jsonpath={.status.phase}") + ExpectWithOffset(2, err).NotTo(HaveOccurred()) + if status != "Running" { + return fmt.Errorf("controller pod in %s status", status) + } + return nil + } + defer func() { + out, err := kbc.Kubectl.CommandInNamespace("describe", "all") + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + fmt.Fprintln(GinkgoWriter, out) + }() + EventuallyWithOffset(1, verifyControllerUp, time.Minute, time.Second).Should(Succeed()) + + By("creating an instance of the CR") + sampleFile := filepath.Join("config", "samples", + fmt.Sprintf("%s_%s_%s.yaml", kbc.Group, kbc.Version, strings.ToLower(kbc.Kind))) + + sampleFilePath, err := filepath.Abs(filepath.Join(fmt.Sprintf("e2e-%s", kbc.TestSuffix), sampleFile)) + Expect(err).To(Not(HaveOccurred())) + + EventuallyWithOffset(1, func() error { + _, err = kbc.Kubectl.Apply(true, "-f", sampleFilePath) + return err + }, time.Minute, time.Second).Should(Succeed()) + + By("applying the CRD Editor Role") + crdEditorRole := filepath.Join("config", "rbac", + fmt.Sprintf("%s_editor_role.yaml", strings.ToLower(kbc.Kind))) + EventuallyWithOffset(1, func() error { + _, err = kbc.Kubectl.Apply(true, "-f", crdEditorRole) + return err + }, time.Minute, time.Second).Should(Succeed()) + + By("applying the CRD Viewer Role") + crdViewerRole := filepath.Join("config", "rbac", fmt.Sprintf("%s_viewer_role.yaml", strings.ToLower(kbc.Kind))) + EventuallyWithOffset(1, func() error { + _, err = kbc.Kubectl.Apply(true, "-f", crdViewerRole) + return err + }, time.Minute, time.Second).Should(Succeed()) + + By("validating that pod(s) status.phase=Running") + var podsOutput string + getPods := func() error { + podsOutput, err = kbc.Kubectl.Get(true, "pods", "-o", + "jsonpath={range .items[*]}{.metadata.name},{.status.phase} {end}") + if err == nil && strings.TrimSpace(podsOutput) == "" { + err = errors.New("empty pod output, continue") + } + + return err + } + Eventually(getPods, 3*time.Minute, time.Second).Should(Succeed()) + podSlice := strings.Split(strings.TrimSpace(podsOutput), " ") + Expect(len(podSlice)).To(BeNumerically(">", 0)) + for _, pod := range podSlice { + // make sure any pod that contains the substring "memcached" is in the running state + if strings.Contains(pod, fmt.Sprintf("%s-sample", strings.ToLower(kbc.Kind))) { + Expect(pod).To(ContainSubstring(",Running")) + } + } +} + +func uncommentPodStandards(kbc *utils.TestContext) { + configManager := filepath.Join(kbc.Dir, "config", "manager", "manager.yaml") + + //nolint:lll + if err := util.ReplaceInFile(configManager, `# TODO(user): For common cases that do not require escalating privileges + # it is recommended to ensure that all your Pods/Containers are restrictive. + # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + # Please uncomment the following code if your project does NOT have to work on old Kubernetes + # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). + # seccompProfile: + # type: RuntimeDefault`, `seccompProfile: + type: RuntimeDefault`); err == nil { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } +} diff --git a/test/e2e/setup.sh b/test/e2e/setup.sh index 5d02e89037f..f3a8cb773f0 100755 --- a/test/e2e/setup.sh +++ b/test/e2e/setup.sh @@ -56,6 +56,7 @@ function delete_cluster { function test_cluster { local flags="$@" + go test $(dirname "$0")/deployimage $flags go test $(dirname "$0")/v2 $flags go test $(dirname "$0")/v3 $flags -timeout 30m } diff --git a/test/e2e/utils/test_context.go b/test/e2e/utils/test_context.go index d586bb65bd4..6717764cfb7 100644 --- a/test/e2e/utils/test_context.go +++ b/test/e2e/utils/test_context.go @@ -262,6 +262,22 @@ func (t *TestContext) Destroy() { } } +// CreateManagerNamespace will create the namespace where the manager is deployed +func (t *TestContext) CreateManagerNamespace() error { + _, err := t.Kubectl.Command("create", "ns", t.Kubectl.Namespace) + return err +} + +// LabelAllNamespacesToWarnAboutRestricted will label all namespaces so that we can verify +// if a warning with `Warning: would violate PodSecurity` will be raised when the manifests are applied +func (t *TestContext) LabelAllNamespacesToWarnAboutRestricted() error { + _, err := t.Kubectl.Command("label", "--overwrite", "ns", "--all", + "pod-security.kubernetes.io/audit=restricted", + "pod-security.kubernetes.io/enforce-version=v1.24", + "pod-security.kubernetes.io/warn=restricted") + return err +} + // LoadImageToKindCluster loads a local docker image to the kind cluster func (t *TestContext) LoadImageToKindCluster() error { cluster := "kind" @@ -274,6 +290,23 @@ func (t *TestContext) LoadImageToKindCluster() error { return err } +// LoadImageToKindClusterWithName loads a local docker image with the name informed to the kind cluster +func (tc TestContext) LoadImageToKindClusterWithName(image string) error { + cmd := exec.Command("docker", "pull", image) + _, err := tc.Run(cmd) + if err != nil { + return err + } + cluster := "kind" + if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { + cluster = v + } + kindOptions := []string{"load", "docker-image", "--name", cluster, image} + cmd = exec.Command("kind", kindOptions...) + _, err = tc.Run(cmd) + return err +} + // CmdContext provides context for command execution type CmdContext struct { // environment variables in k=v format. diff --git a/test/testdata/generate.sh b/test/testdata/generate.sh index 9fb9ae9259c..e97d8bb2c39 100755 --- a/test/testdata/generate.sh +++ b/test/testdata/generate.sh @@ -107,6 +107,11 @@ function scaffold_test_project { $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false $kb create api --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false $kb create api --group crew --version v1 --kind Admiral --controller=true --resource=true --namespaced=false --make=false + elif [[ $project == "project-v3-with-deploy-image" ]]; then + header_text 'Creating Memcached API with deploy-image plugin ...' + $kb create api --group example.com --version v1alpha1 --kind Memcached --image=memcached:1.4.36-alpine --image-container-command="memcached,-m=64,-o,modern,-v" --image-container-port="11211" --plugins="deploy-image/v1-alpha" --make=false --namespaced=false + header_text 'Creating Memcached webhook ...' + $kb create webhook --group example.com --version v1alpha1 --kind Memcached --programmatic-validation fi make generate manifests @@ -125,3 +130,4 @@ scaffold_test_project project-v3-multigroup scaffold_test_project project-v3-addon --plugins="go/v3,declarative" scaffold_test_project project-v3-config --component-config scaffold_test_project project-v3-with-kustomize-v2 --plugins="kustomize/v2-alpha,base.go.kubebuilder.io/v3" +scaffold_test_project project-v3-with-deploy-image diff --git a/test/testdata/test.sh b/test/testdata/test.sh index 7a3b786c2e9..4f91d67f5c0 100755 --- a/test/testdata/test.sh +++ b/test/testdata/test.sh @@ -36,3 +36,4 @@ test_project project-v3-multigroup test_project project-v3-addon test_project project-v3-config test_project project-v3-with-kustomize-v2 +test_project project-v3-with-deploy-image diff --git a/testdata/project-v3-with-deploy-image/.dockerignore b/testdata/project-v3-with-deploy-image/.dockerignore new file mode 100644 index 00000000000..0f046820f18 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/.dockerignore @@ -0,0 +1,4 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ +testbin/ diff --git a/testdata/project-v3-with-deploy-image/.gitignore b/testdata/project-v3-with-deploy-image/.gitignore new file mode 100644 index 00000000000..c0a7a54cac5 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/.gitignore @@ -0,0 +1,25 @@ + +# 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/testdata/project-v3-with-deploy-image/Dockerfile b/testdata/project-v3-with-deploy-image/Dockerfile new file mode 100644 index 00000000000..5a355c21489 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/Dockerfile @@ -0,0 +1,27 @@ +# Build the manager binary +FROM golang:1.18 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 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/testdata/project-v3-with-deploy-image/Makefile b/testdata/project-v3-with-deploy-image/Makefile new file mode 100644 index 00000000000..2bc5e8c7c94 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/Makefile @@ -0,0 +1,133 @@ + +# Image URL to use all building/pushing image targets +IMG ?= controller:latest +# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +ENVTEST_K8S_VERSION = 1.24.1 + +# 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 + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# This is a requirement for 'setup-envtest.sh' in the test target. +# 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 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 + +.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. + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + +.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 envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out + +##@ Build + +.PHONY: build +build: generate fmt vet ## Build manager binary. + go build -o bin/manager main.go + +.PHONY: run +run: manifests generate fmt vet ## Run a controller from your host. + go run ./main.go + +.PHONY: docker-build +docker-build: test ## Build docker image with the manager. + docker build -t ${IMG} . + +.PHONY: docker-push +docker-push: ## Push docker image with the manager. + docker push ${IMG} + +##@ 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. + $(KUSTOMIZE) build config/crd | kubectl apply -f - + +.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. + $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + +.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: ## 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 - + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +## Tool Binaries +KUSTOMIZE ?= $(LOCALBIN)/kustomize +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +ENVTEST ?= $(LOCALBIN)/setup-envtest + +## Tool Versions +KUSTOMIZE_VERSION ?= v3.8.7 +CONTROLLER_TOOLS_VERSION ?= v0.9.0 + +KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" +.PHONY: kustomize +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. +$(KUSTOMIZE): $(LOCALBIN) + curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN) + +.PHONY: controller-gen +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. +$(CONTROLLER_GEN): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +.PHONY: envtest +envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +$(ENVTEST): $(LOCALBIN) + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest diff --git a/testdata/project-v3-with-deploy-image/PROJECT b/testdata/project-v3-with-deploy-image/PROJECT new file mode 100644 index 00000000000..19a5734d322 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/PROJECT @@ -0,0 +1,28 @@ +domain: testproject.org +layout: +- go.kubebuilder.io/v3 +plugins: + deploy-image.go.kubebuilder.io/v1-alpha: + containerCommand: memcached,-m=64,-o,modern,-v + containerPort: "11211" + image: memcached:1.4.36-alpine + resources: + - domain: testproject.org + group: example.com + kind: Memcached + version: v1alpha1 +projectName: project-v3-with-deploy-image +repo: sigs.k8s.io/kubebuilder/testdata/project-v3-with-deploy-image +resources: +- api: + crdVersion: v1 + controller: true + domain: testproject.org + group: example.com + kind: Memcached + path: sigs.k8s.io/kubebuilder/testdata/project-v3-with-deploy-image/api/v1alpha1 + version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 +version: "3" diff --git a/testdata/project-v3-with-deploy-image/README.md b/testdata/project-v3-with-deploy-image/README.md new file mode 100644 index 00000000000..08064c4cf66 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/README.md @@ -0,0 +1,94 @@ +# project-v3-with-deploy-image +// TODO(user): Add simple overview of use/purpose + +## Description +// TODO(user): An in-depth paragraph about your project and overview of use + +## Getting Started +You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster. +**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). + +### Running on the cluster +1. Install Instances of Custom Resources: + +```sh +kubectl apply -f config/samples/ +``` + +2. Build and push your image to the location specified by `IMG`: + +```sh +make docker-build docker-push IMG=/project-v3-with-deploy-image:tag +``` + +3. Deploy the controller to the cluster with the image specified by `IMG`: + +```sh +make deploy IMG=/project-v3-with-deploy-image:tag +``` + +### Uninstall CRDs +To delete the CRDs from the cluster: + +```sh +make uninstall +``` + +### Undeploy controller +UnDeploy the controller to the cluster: + +```sh +make undeploy +``` + +## Contributing +// TODO(user): Add detailed information on how you would like others to contribute to this project + +### How it works +This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) + +It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/) +which provides a reconcile function responsible for synchronizing resources untile the desired state is reached on the cluster + +### Test It Out +1. Install the CRDs into the cluster: + +```sh +make install +``` + +2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): + +```sh +make run +``` + +**NOTE:** You can also run this in one step by running: `make install run` + +### Modifying the API definitions +If you are editing the API definitions, generate the manifests such as CRs or CRDs using: + +```sh +make manifests +``` + +**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 + +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. + diff --git a/testdata/project-v3-with-deploy-image/api/v1alpha1/groupversion_info.go b/testdata/project-v3-with-deploy-image/api/v1alpha1/groupversion_info.go new file mode 100644 index 00000000000..fdc788ae755 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +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 v1alpha1 contains API Schema definitions for the example.com v1alpha1 API group +//+kubebuilder:object:generate=true +//+groupName=example.com.testproject.org +package v1alpha1 + +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: "example.com.testproject.org", Version: "v1alpha1"} + + // 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/testdata/project-v3-with-deploy-image/api/v1alpha1/memcached_types.go b/testdata/project-v3-with-deploy-image/api/v1alpha1/memcached_types.go new file mode 100644 index 00000000000..dc31fd48054 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/api/v1alpha1/memcached_types.go @@ -0,0 +1,68 @@ +/* +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 v1alpha1 + +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. + +// MemcachedSpec defines the desired state of Memcached +type MemcachedSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Size defines the number of Memcached instances + Size int32 `json:"size,omitempty"` + + // Port defines the port that will be used to init the container with the image + ContainerPort int32 `json:"containerPort,omitempty"` +} + +// MemcachedStatus defines the observed state of Memcached +type MemcachedStatus 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 +//+kubebuilder:resource:scope=Cluster + +// Memcached is the Schema for the memcacheds API +type Memcached struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MemcachedSpec `json:"spec,omitempty"` + Status MemcachedStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// MemcachedList contains a list of Memcached +type MemcachedList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Memcached `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Memcached{}, &MemcachedList{}) +} diff --git a/testdata/project-v3-with-deploy-image/api/v1alpha1/memcached_webhook.go b/testdata/project-v3-with-deploy-image/api/v1alpha1/memcached_webhook.go new file mode 100644 index 00000000000..579f86841b6 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/api/v1alpha1/memcached_webhook.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 v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var memcachedlog = logf.Log.WithName("memcached-resource") + +func (r *Memcached) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +//+kubebuilder:webhook:path=/validate-example-com-testproject-org-v1alpha1-memcached,mutating=false,failurePolicy=fail,sideEffects=None,groups=example.com.testproject.org,resources=memcacheds,verbs=create;update,versions=v1alpha1,name=vmemcached.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &Memcached{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *Memcached) ValidateCreate() error { + memcachedlog.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 *Memcached) ValidateUpdate(old runtime.Object) error { + memcachedlog.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 *Memcached) ValidateDelete() error { + memcachedlog.Info("validate delete", "name", r.Name) + + // TODO(user): fill in your validation logic upon object deletion. + return nil +} diff --git a/testdata/project-v3-with-deploy-image/api/v1alpha1/webhook_suite_test.go b/testdata/project-v3-with-deploy-image/api/v1alpha1/webhook_suite_test.go new file mode 100644 index 00000000000..334a10cad6d --- /dev/null +++ b/testdata/project-v3-with-deploy-image/api/v1alpha1/webhook_suite_test.go @@ -0,0 +1,135 @@ +/* +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 v1alpha1 + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + admissionv1beta1 "k8s.io/api/admission/v1beta1" + //+kubebuilder:scaffold:imports + "k8s.io/apimachinery/pkg/runtime" + "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" + "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("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "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 := runtime.NewScheme() + err = AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + 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()) + + err = (&Memcached{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:webhook + + 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:%d", 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/testdata/project-v3-with-deploy-image/api/v1alpha1/zz_generated.deepcopy.go b/testdata/project-v3-with-deploy-image/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..6e61b9350bd --- /dev/null +++ b/testdata/project-v3-with-deploy-image/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,115 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Memcached) DeepCopyInto(out *Memcached) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Memcached. +func (in *Memcached) DeepCopy() *Memcached { + if in == nil { + return nil + } + out := new(Memcached) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Memcached) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MemcachedList) DeepCopyInto(out *MemcachedList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Memcached, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemcachedList. +func (in *MemcachedList) DeepCopy() *MemcachedList { + if in == nil { + return nil + } + out := new(MemcachedList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MemcachedList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MemcachedSpec) DeepCopyInto(out *MemcachedSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemcachedSpec. +func (in *MemcachedSpec) DeepCopy() *MemcachedSpec { + if in == nil { + return nil + } + out := new(MemcachedSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MemcachedStatus) DeepCopyInto(out *MemcachedStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemcachedStatus. +func (in *MemcachedStatus) DeepCopy() *MemcachedStatus { + if in == nil { + return nil + } + out := new(MemcachedStatus) + in.DeepCopyInto(out) + return out +} diff --git a/testdata/project-v3-with-deploy-image/config/certmanager/certificate.yaml b/testdata/project-v3-with-deploy-image/config/certmanager/certificate.yaml new file mode 100644 index 00000000000..52d866183c7 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/certmanager/certificate.yaml @@ -0,0 +1,25 @@ +# 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 v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: selfsigned-issuer + namespace: system +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +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/testdata/project-v3-with-deploy-image/config/certmanager/kustomization.yaml b/testdata/project-v3-with-deploy-image/config/certmanager/kustomization.yaml new file mode 100644 index 00000000000..bebea5a595e --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/certmanager/kustomization.yaml @@ -0,0 +1,5 @@ +resources: +- certificate.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/testdata/project-v3-with-deploy-image/config/certmanager/kustomizeconfig.yaml b/testdata/project-v3-with-deploy-image/config/certmanager/kustomizeconfig.yaml new file mode 100644 index 00000000000..90d7c313ca1 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/certmanager/kustomizeconfig.yaml @@ -0,0 +1,16 @@ +# 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/testdata/project-v3-with-deploy-image/config/crd/bases/example.com.testproject.org_memcacheds.yaml b/testdata/project-v3-with-deploy-image/config/crd/bases/example.com.testproject.org_memcacheds.yaml new file mode 100644 index 00000000000..fe6cc05f7da --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/crd/bases/example.com.testproject.org_memcacheds.yaml @@ -0,0 +1,55 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.0 + creationTimestamp: null + name: memcacheds.example.com.testproject.org +spec: + group: example.com.testproject.org + names: + kind: Memcached + listKind: MemcachedList + plural: memcacheds + singular: memcached + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Memcached is the Schema for the memcacheds API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: MemcachedSpec defines the desired state of Memcached + properties: + containerPort: + description: Port defines the port that will be used to init the container + with the image + format: int32 + type: integer + size: + description: Size defines the number of Memcached instances + format: int32 + type: integer + type: object + status: + description: MemcachedStatus defines the observed state of Memcached + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/testdata/project-v3-with-deploy-image/config/crd/kustomization.yaml b/testdata/project-v3-with-deploy-image/config/crd/kustomization.yaml new file mode 100644 index 00000000000..a9317ccdf9e --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/crd/kustomization.yaml @@ -0,0 +1,21 @@ +# 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: +- bases/example.com.testproject.org_memcacheds.yaml +#+kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- patches/webhook_in_memcacheds.yaml +#+kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- patches/cainjection_in_memcacheds.yaml +#+kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/testdata/project-v3-with-deploy-image/config/crd/kustomizeconfig.yaml b/testdata/project-v3-with-deploy-image/config/crd/kustomizeconfig.yaml new file mode 100644 index 00000000000..ec5c150a9df --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/crd/kustomizeconfig.yaml @@ -0,0 +1,19 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + version: v1 + group: apiextensions.k8s.io + path: spec/conversion/webhook/clientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/testdata/project-v3-with-deploy-image/config/crd/patches/cainjection_in_memcacheds.yaml b/testdata/project-v3-with-deploy-image/config/crd/patches/cainjection_in_memcacheds.yaml new file mode 100644 index 00000000000..7f5b50acfba --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/crd/patches/cainjection_in_memcacheds.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: memcacheds.example.com.testproject.org diff --git a/testdata/project-v3-with-deploy-image/config/crd/patches/webhook_in_memcacheds.yaml b/testdata/project-v3-with-deploy-image/config/crd/patches/webhook_in_memcacheds.yaml new file mode 100644 index 00000000000..4a56b0f4c69 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/crd/patches/webhook_in_memcacheds.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: memcacheds.example.com.testproject.org +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/testdata/project-v3-with-deploy-image/config/default/kustomization.yaml b/testdata/project-v3-with-deploy-image/config/default/kustomization.yaml new file mode 100644 index 00000000000..c6dd3c530d5 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/default/kustomization.yaml @@ -0,0 +1,74 @@ +# Adds namespace to all resources. +namespace: project-v3-with-deploy-image-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: project-v3-with-deploy-image- + +# 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 + +# Mount the controller config file for loading manager configurations +# through a ComponentConfig type +#- manager_config_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: v1 +# 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: v1 +# 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/testdata/project-v3-with-deploy-image/config/default/manager_auth_proxy_patch.yaml b/testdata/project-v3-with-deploy-image/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 00000000000..28a6ef7c794 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,39 @@ +# 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 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.12.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=0" + ports: + - containerPort: 8443 + protocol: TCP + name: https + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi + - name: manager + args: + - "--health-probe-bind-address=:8081" + - "--metrics-bind-address=127.0.0.1:8080" + - "--leader-elect" diff --git a/testdata/project-v3-with-deploy-image/config/default/manager_config_patch.yaml b/testdata/project-v3-with-deploy-image/config/default/manager_config_patch.yaml new file mode 100644 index 00000000000..6c400155cfb --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/default/manager_config_patch.yaml @@ -0,0 +1,20 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + args: + - "--config=controller_manager_config.yaml" + volumeMounts: + - name: manager-config + mountPath: /controller_manager_config.yaml + subPath: controller_manager_config.yaml + volumes: + - name: manager-config + configMap: + name: manager-config diff --git a/testdata/project-v3-with-deploy-image/config/default/manager_webhook_patch.yaml b/testdata/project-v3-with-deploy-image/config/default/manager_webhook_patch.yaml new file mode 100644 index 00000000000..738de350b71 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/default/manager_webhook_patch.yaml @@ -0,0 +1,23 @@ +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/testdata/project-v3-with-deploy-image/config/default/webhookcainjection_patch.yaml b/testdata/project-v3-with-deploy-image/config/default/webhookcainjection_patch.yaml new file mode 100644 index 00000000000..02ab515d428 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/default/webhookcainjection_patch.yaml @@ -0,0 +1,15 @@ +# 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/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) diff --git a/testdata/project-v3-with-deploy-image/config/manager/controller_manager_config.yaml b/testdata/project-v3-with-deploy-image/config/manager/controller_manager_config.yaml new file mode 100644 index 00000000000..95af7d3eef1 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/manager/controller_manager_config.yaml @@ -0,0 +1,21 @@ +apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 +kind: ControllerManagerConfig +health: + healthProbeBindAddress: :8081 +metrics: + bindAddress: 127.0.0.1:8080 +webhook: + port: 9443 +leaderElection: + leaderElect: true + resourceName: 4d73a076.testproject.org +# leaderElectionReleaseOnCancel defines if the leader should step down volume +# 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 diff --git a/testdata/project-v3-with-deploy-image/config/manager/kustomization.yaml b/testdata/project-v3-with-deploy-image/config/manager/kustomization.yaml new file mode 100644 index 00000000000..2bcd3eeaa94 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/manager/kustomization.yaml @@ -0,0 +1,10 @@ +resources: +- manager.yaml + +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: +- name: manager-config + files: + - controller_manager_config.yaml diff --git a/testdata/project-v3-with-deploy-image/config/manager/manager.yaml b/testdata/project-v3-with-deploy-image/config/manager/manager.yaml new file mode 100644 index 00000000000..878ad48666a --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/manager/manager.yaml @@ -0,0 +1,70 @@ +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: + annotations: + kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + spec: + securityContext: + runAsNonRoot: true + # TODO(user): For common cases that do not require escalating privileges + # it is recommended to ensure that all your Pods/Containers are restrictive. + # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + # Please uncomment the following code if your project does NOT have to work on old Kubernetes + # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). + # seccompProfile: + # type: RuntimeDefault + containers: + - command: + - /manager + args: + - --leader-elect + image: controller:latest + name: manager + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 diff --git a/testdata/project-v3-with-deploy-image/config/prometheus/kustomization.yaml b/testdata/project-v3-with-deploy-image/config/prometheus/kustomization.yaml new file mode 100644 index 00000000000..ed137168a1d --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/prometheus/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- monitor.yaml diff --git a/testdata/project-v3-with-deploy-image/config/prometheus/monitor.yaml b/testdata/project-v3-with-deploy-image/config/prometheus/monitor.yaml new file mode 100644 index 00000000000..d19136ae710 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/prometheus/monitor.yaml @@ -0,0 +1,20 @@ + +# 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/testdata/project-v3-with-deploy-image/config/rbac/auth_proxy_client_clusterrole.yaml b/testdata/project-v3-with-deploy-image/config/rbac/auth_proxy_client_clusterrole.yaml new file mode 100644 index 00000000000..51a75db47a5 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/rbac/auth_proxy_client_clusterrole.yaml @@ -0,0 +1,9 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: metrics-reader +rules: +- nonResourceURLs: + - "/metrics" + verbs: + - get diff --git a/testdata/project-v3-with-deploy-image/config/rbac/auth_proxy_role.yaml b/testdata/project-v3-with-deploy-image/config/rbac/auth_proxy_role.yaml new file mode 100644 index 00000000000..80e1857c594 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,17 @@ +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/testdata/project-v3-with-deploy-image/config/rbac/auth_proxy_role_binding.yaml b/testdata/project-v3-with-deploy-image/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 00000000000..ec7acc0a1b7 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,12 @@ +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: controller-manager + namespace: system diff --git a/testdata/project-v3-with-deploy-image/config/rbac/auth_proxy_service.yaml b/testdata/project-v3-with-deploy-image/config/rbac/auth_proxy_service.yaml new file mode 100644 index 00000000000..71f1797279e --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + protocol: TCP + targetPort: https + selector: + control-plane: controller-manager diff --git a/testdata/project-v3-with-deploy-image/config/rbac/kustomization.yaml b/testdata/project-v3-with-deploy-image/config/rbac/kustomization.yaml new file mode 100644 index 00000000000..731832a6ac3 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/rbac/kustomization.yaml @@ -0,0 +1,18 @@ +resources: +# All RBAC will be applied under this service account in +# the deployment namespace. You may comment out this resource +# if your manager will use a service account that exists at +# runtime. Be sure to update RoleBinding and ClusterRoleBinding +# subjects if changing service account names. +- service_account.yaml +- 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/testdata/project-v3-with-deploy-image/config/rbac/leader_election_role.yaml b/testdata/project-v3-with-deploy-image/config/rbac/leader_election_role.yaml new file mode 100644 index 00000000000..4190ec8059e --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/rbac/leader_election_role.yaml @@ -0,0 +1,37 @@ +# 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: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/testdata/project-v3-with-deploy-image/config/rbac/leader_election_role_binding.yaml b/testdata/project-v3-with-deploy-image/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 00000000000..1d1321ed4f0 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,12 @@ +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: controller-manager + namespace: system diff --git a/testdata/project-v3-with-deploy-image/config/rbac/memcached_editor_role.yaml b/testdata/project-v3-with-deploy-image/config/rbac/memcached_editor_role.yaml new file mode 100644 index 00000000000..901d61bc992 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/rbac/memcached_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit memcacheds. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: memcached-editor-role +rules: +- apiGroups: + - example.com.testproject.org + resources: + - memcacheds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - example.com.testproject.org + resources: + - memcacheds/status + verbs: + - get diff --git a/testdata/project-v3-with-deploy-image/config/rbac/memcached_viewer_role.yaml b/testdata/project-v3-with-deploy-image/config/rbac/memcached_viewer_role.yaml new file mode 100644 index 00000000000..4bf66fbbbf4 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/rbac/memcached_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view memcacheds. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: memcached-viewer-role +rules: +- apiGroups: + - example.com.testproject.org + resources: + - memcacheds + verbs: + - get + - list + - watch +- apiGroups: + - example.com.testproject.org + resources: + - memcacheds/status + verbs: + - get diff --git a/testdata/project-v3-with-deploy-image/config/rbac/role.yaml b/testdata/project-v3-with-deploy-image/config/rbac/role.yaml new file mode 100644 index 00000000000..68a7c615df5 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/rbac/role.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: manager-role +rules: +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch +- apiGroups: + - example.com.testproject.org + resources: + - memcacheds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - example.com.testproject.org + resources: + - memcacheds/finalizers + verbs: + - update +- apiGroups: + - example.com.testproject.org + resources: + - memcacheds/status + verbs: + - get + - patch + - update diff --git a/testdata/project-v3-with-deploy-image/config/rbac/role_binding.yaml b/testdata/project-v3-with-deploy-image/config/rbac/role_binding.yaml new file mode 100644 index 00000000000..2070ede4462 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/rbac/role_binding.yaml @@ -0,0 +1,12 @@ +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: controller-manager + namespace: system diff --git a/testdata/project-v3-with-deploy-image/config/rbac/service_account.yaml b/testdata/project-v3-with-deploy-image/config/rbac/service_account.yaml new file mode 100644 index 00000000000..7cd6025bfc4 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/rbac/service_account.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: controller-manager + namespace: system diff --git a/testdata/project-v3-with-deploy-image/config/samples/example.com_v1alpha1_memcached.yaml b/testdata/project-v3-with-deploy-image/config/samples/example.com_v1alpha1_memcached.yaml new file mode 100644 index 00000000000..d2f52d75eec --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/samples/example.com_v1alpha1_memcached.yaml @@ -0,0 +1,11 @@ +apiVersion: example.com.testproject.org/v1alpha1 +kind: Memcached +metadata: + name: memcached-sample +spec: + # TODO(user): edit the following value to ensure the number + # of Pods/Instances your Operand must have on cluster + size: 1 + + # TODO(user): edit the following value to ensure the the container has the right port to be initialized + containerPort: 11211 diff --git a/testdata/project-v3-with-deploy-image/config/webhook/kustomization.yaml b/testdata/project-v3-with-deploy-image/config/webhook/kustomization.yaml new file mode 100644 index 00000000000..9cf26134e4d --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/webhook/kustomization.yaml @@ -0,0 +1,6 @@ +resources: +- manifests.yaml +- service.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/testdata/project-v3-with-deploy-image/config/webhook/kustomizeconfig.yaml b/testdata/project-v3-with-deploy-image/config/webhook/kustomizeconfig.yaml new file mode 100644 index 00000000000..25e21e3c963 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,25 @@ +# 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/testdata/project-v3-with-deploy-image/config/webhook/manifests.yaml b/testdata/project-v3-with-deploy-image/config/webhook/manifests.yaml new file mode 100644 index 00000000000..51a4ab30497 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/webhook/manifests.yaml @@ -0,0 +1,27 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + creationTimestamp: null + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-example-com-testproject-org-v1alpha1-memcached + failurePolicy: Fail + name: vmemcached.kb.io + rules: + - apiGroups: + - example.com.testproject.org + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - memcacheds + sideEffects: None diff --git a/testdata/project-v3-with-deploy-image/config/webhook/service.yaml b/testdata/project-v3-with-deploy-image/config/webhook/service.yaml new file mode 100644 index 00000000000..3f638bd9c68 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/config/webhook/service.yaml @@ -0,0 +1,13 @@ + +apiVersion: v1 +kind: Service +metadata: + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager diff --git a/testdata/project-v3-with-deploy-image/controllers/memcached_controller.go b/testdata/project-v3-with-deploy-image/controllers/memcached_controller.go new file mode 100644 index 00000000000..f60925670a0 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/controllers/memcached_controller.go @@ -0,0 +1,207 @@ +/* +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 ( + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "context" + "time" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v3-with-deploy-image/api/v1alpha1" +) + +// MemcachedReconciler reconciles a Memcached object +type MemcachedReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// The following markers are used to generate the rules permissions on config/rbac using controller-gen +// when the command is executed. +// To know more about markers see: https://book.kubebuilder.io/reference/markers.html + +//+kubebuilder:rbac:groups=example.com.testproject.org,resources=memcacheds,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=example.com.testproject.org,resources=memcacheds/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=example.com.testproject.org,resources=memcacheds/finalizers,verbs=update +//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. + +// Note: It is essential for the controller's reconciliation loop to be idempotent. By following the Operator +// pattern(https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) you will create +// Controllers(https://kubernetes.io/docs/concepts/architecture/controller/) which provide a reconcile function +// responsible for synchronizing resources until the desired state is reached on the cluster. Breaking this +// recommendation goes against the design principles of Controller-runtime(https://github.com/kubernetes-sigs/controller-runtime) +// and may lead to unforeseen consequences such as resources becoming stuck and requiring manual intervention. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.1/pkg/reconcile +func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + + // Fetch the Memcached instance + // The purpose is check if the Custom Resource for the Kind Memcached + // is applied on the cluster if not we return nill to stop the reconciliation + memcached := &examplecomv1alpha1.Memcached{} + err := r.Get(ctx, req.NamespacedName, memcached) + if err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + log.Info("memcached resource not found. Ignoring since object must be deleted") + return ctrl.Result{}, nil + } + // Error reading the object - requeue the request. + log.Error(err, "Failed to get memcached }}") + return ctrl.Result{}, err + } + + // Check if the deployment already exists, if not create a new one + found := &appsv1.Deployment{} + err = r.Get(ctx, types.NamespacedName{Name: memcached.Name, Namespace: memcached.Namespace}, found) + if err != nil && errors.IsNotFound(err) { + // Define a new deployment + dep := r.deploymentForMemcached(memcached) + log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + err = r.Create(ctx, dep) + if err != nil { + log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + return ctrl.Result{}, err + } + // Deployment created successfully + // We will requeue the reconciliation so that we can ensure the state + // and move forward for the next operations + return ctrl.Result{RequeueAfter: time.Minute}, nil + } else if err != nil { + log.Error(err, "Failed to get Deployment") + // Let's return the error for the reconciliation be re-trigged again + return ctrl.Result{}, err + } + + // The API is defining that the Memcached type, have a MemcachedSpec.Size field to set the quantity of Memcached instances (CRs) to be deployed. + // The following code ensure the deployment size is the same as the spec + size := memcached.Spec.Size + if *found.Spec.Replicas != size { + found.Spec.Replicas = &size + err = r.Update(ctx, found) + if err != nil { + log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) + return ctrl.Result{}, err + } + // Since it fails we want to re-queue the reconciliation + // The reconciliation will only stop when we be able to ensure + // the desired state on the cluster + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, nil +} + +// deploymentForMemcached returns a Memcached Deployment object +func (r *MemcachedReconciler) deploymentForMemcached(m *examplecomv1alpha1.Memcached) *appsv1.Deployment { + ls := labelsForMemcached(m.Name) + replicas := m.Spec.Size + + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: m.Name, + Namespace: m.Namespace, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: ls, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: ls, + }, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyAlways, + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &[]bool{true}[0], + // IMPORTANT: seccomProfile was introduced with Kubernetes 1.19 + // If you are looking for to produce solutions to be supported + // on lower versions you must remove this option. + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + }, + Containers: []corev1.Container{{ + Image: "memcached:1.4.36-alpine", + Name: "memcached", + ImagePullPolicy: corev1.PullAlways, + // Ensure restrictive context for the container + // More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: &[]bool{true}[0], + RunAsUser: &[]int64{1000}[0], + AllowPrivilegeEscalation: &[]bool{false}[0], + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{ + "ALL", + }, + }, + }, + Ports: []corev1.ContainerPort{{ + ContainerPort: m.Spec.ContainerPort, + Name: "memcached", + }}, + Command: []string{"memcached","-m=64","-o","modern","-v"}, + }}, + }, + }, + }, + } + // Set Memcached instance as the owner and controller + // You should use the method ctrl.SetControllerReference for all resources + // which are created by your controller so that when the Custom Resource be deleted + // all resources owned by it (child) will also be deleted. + // To know more about it see: https://kubernetes.io/docs/tasks/administer-cluster/use-cascading-deletion/ + ctrl.SetControllerReference(m, dep, r.Scheme) + return dep +} + +// labelsForMemcached returns the labels for selecting the resources +// belonging to the given Memcached CR name. +func labelsForMemcached(name string) map[string]string { + return map[string]string{"type": "memcached", "memcached_cr": name} +} + +// SetupWithManager sets up the controller with the Manager. +// The following code specifies how the controller is built to watch a CR +// and other resources that are owned and managed by that controller. +// In this way, the reconciliation can be re-trigged when the CR and/or the Deployment +// be created/edit/delete. +func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&examplecomv1alpha1.Memcached{}). + Owns(&appsv1.Deployment{}). + Complete(r) +} diff --git a/testdata/project-v3-with-deploy-image/controllers/suite_test.go b/testdata/project-v3-with-deploy-image/controllers/suite_test.go new file mode 100644 index 00000000000..f40524e3556 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/controllers/suite_test.go @@ -0,0 +1,82 @@ +/* +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 ( + "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" + + examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v3-with-deploy-image/api/v1alpha1" + //+kubebuilder:scaffold:imports +) + +// 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("..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = examplecomv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + 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/testdata/project-v3-with-deploy-image/go.mod b/testdata/project-v3-with-deploy-image/go.mod new file mode 100644 index 00000000000..d760718f1da --- /dev/null +++ b/testdata/project-v3-with-deploy-image/go.mod @@ -0,0 +1,83 @@ +module sigs.k8s.io/kubebuilder/testdata/project-v3-with-deploy-image + +go 1.18 + +require ( + github.com/onsi/ginkgo v1.16.5 + github.com/onsi/gomega v1.18.1 + k8s.io/api v0.24.0 + k8s.io/apimachinery v0.24.0 + k8s.io/client-go v0.24.0 + sigs.k8s.io/controller-runtime v0.12.1 +) + +require ( + cloud.google.com/go v0.81.0 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.18 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/logger v0.2.1 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful v2.9.5+incompatible // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect + github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/go-logr/logr v1.2.0 // indirect + github.com/go-logr/zapr v1.2.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.5 // indirect + github.com/go-openapi/swag v0.19.14 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/go-cmp v0.5.5 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nxadm/tail v1.4.8 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.12.1 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.19.1 // indirect + golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect + golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect + golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect + golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.27.1 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + k8s.io/apiextensions-apiserver v0.24.0 // indirect + k8s.io/component-base v0.24.0 // indirect + k8s.io/klog/v2 v2.60.1 // indirect + k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect + k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect + sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/testdata/project-v3-with-deploy-image/hack/boilerplate.go.txt b/testdata/project-v3-with-deploy-image/hack/boilerplate.go.txt new file mode 100644 index 00000000000..b54e305f300 --- /dev/null +++ b/testdata/project-v3-with-deploy-image/hack/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +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. +*/ \ No newline at end of file diff --git a/testdata/project-v3-with-deploy-image/main.go b/testdata/project-v3-with-deploy-image/main.go new file mode 100644 index 00000000000..25c7b75d47f --- /dev/null +++ b/testdata/project-v3-with-deploy-image/main.go @@ -0,0 +1,119 @@ +/* +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 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/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v3-with-deploy-image/api/v1alpha1" + "sigs.k8s.io/kubebuilder/testdata/project-v3-with-deploy-image/controllers" + //+kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(examplecomv1alpha1.AddToScheme(scheme)) + //+kubebuilder:scaffold:scheme +} + +func main() { + 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.") + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + MetricsBindAddress: metricsAddr, + Port: 9443, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "4d73a076.testproject.org", + // 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) + } + + if err = (&controllers.MemcachedReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Memcached") + os.Exit(1) + } + if err = (&examplecomv1alpha1.Memcached{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Memcached") + os.Exit(1) + } + //+kubebuilder:scaffold:builder + + 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) + } +}