Skip to content

Commit 7adcffd

Browse files
authored
Merge pull request #1803 from camilamacedo86/code-generate-plugin
📖 proposal for new plugin to generate code ( `deploy-image.go.kubebuilder.io/v1beta1` )
2 parents 6224a06 + 53b2cf6 commit 7adcffd

File tree

1 file changed

+309
-0
lines changed

1 file changed

+309
-0
lines changed
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
---
2+
title: Neat-Enhancement-Idea
3+
authors:
4+
- "@camilamacedo86"
5+
reviewers:
6+
- TBD
7+
approvers:
8+
- TBD
9+
creation-date: 2020-11-09
10+
last-updated: 2021-02-14
11+
status: implementable
12+
---
13+
14+
# New Plugin (`deploy-image.go.kubebuilder.io/v1beta1`) to generate code
15+
16+
## Summary
17+
18+
This proposal defines a new plugin which allow users get the scaffold with the
19+
required code to have a project that will deploy and manage an image on the cluster following the the guidelines and what have been considered as good practices.
20+
21+
## Motivation
22+
23+
The biggest part of the Kubebuilder users looking for to create a project that will at the end only deploy an image. In this way, one of the mainly motivations of this proposal is to abstract the complexities to achieve this goal and still giving the possibility of users improve and customize their projects according to their requirements.
24+
25+
**Note:** This plugin will address requests that has been raised for a while and for many users in the community. Check [here](https://github.com/operator-framework/operator-sdk/pull/2158), for example, a request done in the past for the SDK project which is integrated with Kubebuidler to address the same need.
26+
27+
### Goals
28+
29+
- Add a new plugin to generate the code required to deploy and manage an image on the cluster
30+
- Promote the best practices as give example of common implementations
31+
- Make the process to develop operators projects easier and more agil.
32+
- Give flexibility to the users and allow them to change the code according to their needs
33+
- Provide examples of code implementations and of the most common features usage and reduce the learning curve
34+
35+
### Non-Goals
36+
37+
The idea of this proposal is provide a facility for the users. This plugin can be improved
38+
in the future, however, this proposal just covers the basic requirements. In this way, is a non-goal
39+
allow extra configurations such as; scaffold the project using webhooks and the controller covered by tests.
40+
41+
## Proposal
42+
43+
Add the new plugin code generate which will scaffold code implementation to deploy the image informed which would like such as; `kubebuilder create api --group=crew --version=v1 --image=myexample:0.0.1 --kind=App --plugins=deploy-image.go.kubebuilder.io/v1beta1` which will:
44+
45+
- Add a code implementation which will do the Custom Resource reconciliation and create a Deployment resource for the `--image`;
46+
47+
- Add an EnvVar on the manager manifest (`config/manager/manager.yaml`) which will store the image informed and shows its possibility to users:
48+
49+
```yaml
50+
..
51+
spec:
52+
containers:
53+
- name: manager
54+
env:
55+
- name: {{ resource}}-IMAGE
56+
value: {{image:tag}}
57+
image: controller:latest
58+
...
59+
```
60+
61+
- Add a check into reconcile to ensure that the replicas of the deployment on cluster are equals the size defined in the CR:
62+
63+
```go
64+
// Ensure the deployment size is the same as the spec
65+
size := {{ resource }}.Spec.Size
66+
if *found.Spec.Replicas != size {
67+
found.Spec.Replicas = &size
68+
err = r.Update(ctx, found)
69+
if err != nil {
70+
log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
71+
return ctrl.Result{}, err
72+
}
73+
// Spec updated - return and requeue
74+
return ctrl.Result{Requeue: true}, nil
75+
}
76+
```
77+
78+
- Add the watch feature for the Deployment managed by the controller:
79+
80+
```go
81+
func (r *{{ resource }}Reconciler) SetupWithManager(mgr ctrl.Manager) error {
82+
return ctrl.NewControllerManagedBy(mgr).
83+
For(&cachev1alpha1.{{ resource }}{}).
84+
Owns(&appsv1.Deployment{}).
85+
Complete(r)
86+
}
87+
```
88+
89+
- Add the RBAC permissions required for the scenario such as:
90+
91+
```go
92+
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
93+
```
94+
95+
- A status [conditions][conditions] to allow users check that if the deployment occurred successfully or its errors
96+
97+
- Add a [marker][markers] in the spec definition to demonstrate how to use OpenAPI schemas validation such as `+kubebuilder:validation:Minimum=1`
98+
99+
- Add the specs on the `_types.go` to generate the CRD/CR sample with default values for `ImagePullPolicy` (`Always`), `ContainerPort` (`80`) and the `Replicas Size` (`3`)
100+
101+
- Add a finalizer implementation with TODO for the CR managed by the controller such as described in the SDK doc [Handle Cleanup on Deletion](https://sdk.operatorframework.io/docs/building-operators/golang/advanced-topics/#handle-cleanup-on-deletion)
102+
103+
### User Stories
104+
105+
- I am as user, would like to use a command to scaffold my common need which is deploy an image of my application, so that I do not need to know exactly how to implement it
106+
107+
- I am as user, would like to have a good example code base which uses the common features, so that I can easily learn its concepts and have a good start point to address my needs.
108+
109+
- I am as maintainer, would like to have a good example to address the common questions, so that I can easily describe how to implement the projects and/or use the common features.
110+
111+
### Implementation Details/Notes/Constraints
112+
113+
**Example of the controller template**
114+
115+
```go
116+
// +kubebuilder:rbac:groups=cache.example.com,resources={{ resource.plural }},verbs=get;list;watch;create;update;patch;delete
117+
// +kubebuilder:rbac:groups=cache.example.com,resources={{ resource.plural }}/status,verbs=get;update;patch
118+
// +kubebuilder:rbac:groups=cache.example.com,resources={{ resource.plural }}/finalizers,verbs=update
119+
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
120+
121+
func (r *{{ resource }}.Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
122+
ctx := context.Background()
123+
log := r.Log.WithValues("{{ resource }}", req.NamespacedName)
124+
125+
// Fetch the {{ resource }} instance
126+
{{ resource }} := &{{ apiimportalias }}.{{ resource }}{}
127+
err := r.Get(ctx, req.NamespacedName, {{ resource }})
128+
if err != nil {
129+
if errors.IsNotFound(err) {
130+
// Request object not found, could have been deleted after reconcile request.
131+
// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
132+
// Return and don't requeue
133+
log.Info("{{ resource }} resource not found. Ignoring since object must be deleted")
134+
return ctrl.Result{}, nil
135+
}
136+
// Error reading the object - requeue the request.
137+
log.Error(err, "Failed to get {{ resource }}")
138+
return ctrl.Result{}, err
139+
}
140+
141+
// Check if the deployment already exists, if not create a new one
142+
found := &appsv1.Deployment{}
143+
err = r.Get(ctx, types.NamespacedName{Name: {{ resource }}.Name, Namespace: {{ resource }}.Namespace}, found)
144+
if err != nil && errors.IsNotFound(err) {
145+
// Define a new deployment
146+
dep := r.deploymentFor{{ resource }}({{ resource }})
147+
log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
148+
err = r.Create(ctx, dep)
149+
if err != nil {
150+
log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
151+
return ctrl.Result{}, err
152+
}
153+
// Deployment created successfully - return and requeue
154+
return ctrl.Result{Requeue: true}, nil
155+
} else if err != nil {
156+
log.Error(err, "Failed to get Deployment")
157+
return ctrl.Result{}, err
158+
}
159+
160+
// Ensure the deployment size is the same as the spec
161+
size := {{ resource }}.Spec.Size
162+
if *found.Spec.Replicas != size {
163+
found.Spec.Replicas = &size
164+
err = r.Update(ctx, found)
165+
if err != nil {
166+
log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
167+
return ctrl.Result{}, err
168+
}
169+
// Spec updated - return and requeue
170+
return ctrl.Result{Requeue: true}, nil
171+
}
172+
173+
// TODO: add here code implementation to update/manage the status
174+
175+
return ctrl.Result{}, nil
176+
}
177+
178+
// deploymentFor{{ resource }} returns a {{ resource }} Deployment object
179+
func (r *{{ resource }}Reconciler) deploymentFor{{ resource }}(m *{{ apiimportalias }}.{{ resource }}) *appsv1.Deployment {
180+
ls := labelsFor{{ resource }}(m.Name)
181+
replicas := m.Spec.Size
182+
183+
dep := &appsv1.Deployment{
184+
ObjectMeta: metav1.ObjectMeta{
185+
Name: m.Name,
186+
Namespace: m.Namespace,
187+
},
188+
Spec: appsv1.DeploymentSpec{
189+
Replicas: &replicas,
190+
Selector: &metav1.LabelSelector{
191+
MatchLabels: ls,
192+
},
193+
Template: corev1.PodTemplateSpec{
194+
ObjectMeta: metav1.ObjectMeta{
195+
Labels: ls,
196+
},
197+
Spec: corev1.PodSpec{
198+
Containers: []corev1.Container{{
199+
Image: imageFor{{ resource }}(m.Name),
200+
Name: {{ resource }},
201+
ImagePullPolicy: {{ resource }}.Spec.ContainerImagePullPolicy,
202+
Command: []string{"{{ resource }}"},
203+
Ports: []corev1.ContainerPort{{
204+
ContainerPort: {{ resource }}.Spec.ContainerPort,
205+
Name: "{{ resource }}",
206+
}},
207+
}},
208+
},
209+
},
210+
},
211+
}
212+
// Set {{ resource }} instance as the owner and controller
213+
ctrl.SetControllerReference(m, dep, r.Scheme)
214+
return dep
215+
}
216+
217+
// labelsFor{{ resource }} returns the labels for selecting the resources
218+
// belonging to the given {{ resource }} CR name.
219+
func labelsFor{{ resource }}(name string) map[string]string {
220+
return map[string]string{"type": "{{ resource }}", "{{ resource }}_cr": name}
221+
}
222+
223+
// imageFor{{ resource }} returns the image for the resources
224+
// belonging to the given {{ resource }} CR name.
225+
func imageFor{{ resource }}(name string) string {
226+
// TODO: this method will return the value of the envvar create to store the image:tag informed
227+
}
228+
229+
func (r *{{ resource }}Reconciler) SetupWithManager(mgr ctrl.Manager) error {
230+
return ctrl.NewControllerManagedBy(mgr).
231+
For(&cachev1alpha1.{{ resource }}{}).
232+
Owns(&appsv1.Deployment{}).
233+
Complete(r)
234+
}
235+
236+
```
237+
238+
**Example of the spec for the <kind>_types.go template**
239+
240+
```go
241+
// {{ resource }}Spec defines the desired state of {{ resource }}
242+
type {{ resource }}Spec struct {
243+
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
244+
// Important: Run "make" to regenerate code after modifying this file
245+
246+
// +kubebuilder:validation:Minimum=1
247+
// Size defines the number of {{ resource }} instances
248+
Size int32 `json:"size,omitempty"`
249+
250+
// ImagePullPolicy defines the policy to pull the container images
251+
ImagePullPolicy string `json:"image-pull-policy,omitempty"`
252+
253+
// ContainerPort specifies the port which will be used by the image container
254+
ContainerPort int `json:"container-port,omitempty"`
255+
256+
}
257+
```
258+
259+
## Design Details
260+
261+
### Test Plan
262+
263+
To ensure this implementation a new project example should be generated in the [testdata](../testdata/) directory of the project. See the [test/testadata/generate.sh](../test/testadata/generate.sh). Also, we should use this scaffold in the [integration tests](../test/e2e/) to ensure that the data scaffolded with works on the cluster as expected.
264+
265+
### Graduation Criteria
266+
267+
- The new plugin will only be support `project-version=3`
268+
- The attribute image with the value informed should be added to the resources model in the PROJECT file to let the tool know that the Resource get done with the common basic code implementation:
269+
270+
```yaml
271+
plugins:
272+
deploy-image.go.kubebuilder.io/v1beta1:
273+
resources:
274+
- domain: example.io
275+
group: crew
276+
kind: Captain
277+
version: v1
278+
image: "<some-registry>/<project-name>:<tag>
279+
```
280+
281+
For further information check the definition agreement register in the comment https://github.com/kubernetes-sigs/kubebuilder/issues/1941#issuecomment-778649947.
282+
283+
## Open Questions
284+
285+
1. Should we allow to scaffold the code for an API that is already created for the project?
286+
No, at least in the first moment to keep the simplicity.
287+
288+
2. Should we support StatefulSet and Deployments?
289+
The idea is we start it by using a Deployment. However, we can improve the feature in follow-ups to support more default types of scaffolds which could be like `kubebuilder create api --group=crew --version=v1 --image=myexample:0.0.1 --kind=App --plugins=deploy-image.go.kubebuilder.io/v1beta1 --type=[deployment|statefulset|webhook]`
290+
291+
3. Could this feature be useful to other languages or is it just valid to Go based operators?
292+
293+
This plugin would is reponsable to scaffold content and files for Go-based operators. In a future, if other language-based operators starts to be supported (either officially or by the community) this plugin could be used as reference to create an equivalent one for their languages. Therefore, it should probably not to be a `subdomain` of `go.kubebuilder.io.`
294+
295+
For its integration with SDK, it might be valid for the Ansible-based operators where a new `playbook/role` could be generated as well. However, for example, for the Helm plugin it might to be useless. E.g `deploy-image.ansible.sdk.operatorframework.io/v1beta1`
296+
297+
4. Should we consider create a separate repo for plugins?
298+
299+
In the long term yes. However, see that currently, Kubebuilder has not too many plugins yet. And then, and the preliminary support for plugins did not indeed release. For more info see the [Extensible CLI and Scaffolding Plugins][plugins-phase1-design-doc].
300+
301+
In this way, at this moment, it shows to be a little Premature Optimization. Note that the issue [#2016](https://github.com/kubernetes-sigs/kubebuilder/issues/1378) will check the possibility of the plugins be as separate binaries that can be discovered by the Kubebuilder CLI binary via user-specified plugin file paths. Then, the discussion over the best approach to dealing with many plugins and if they should or not leave in the Kubebuilder repository would be better addressed after that.
302+
303+
5. Is Kubebuilder prepared to receive this implementation already?
304+
305+
The [Extensible CLI and Scaffolding Plugins - Phase 1.5](extensible-cli-and-scaffolding-plugins-phase-1-5.md) and the issue #1941 requires to be implemented before this proposal. Also, to have a better idea over the proposed solutions made so for the Plugin Ecosystem see the meta issue [#2016](https://github.com/kubernetes-sigs/kubebuilder/issues/2016)
306+
307+
[markers]: ../docs/book/src/reference/markers.md
308+
[conditions]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
309+
[plugins-phase1-design-doc]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/designs/extensible-cli-and-scaffolding-plugins-phase-1.md

0 commit comments

Comments
 (0)